Welcome to Wallaroo! Whether you’re using our free Community Edition or the Enterprise Edition, these Quick Start Guides are made to help learn how to deploy your ML Models with Wallaroo. Each of these guides includes a Jupyter Notebook with the documentation, code, and sample data that you can download and run wherever you have Wallaroo installed.
Wallaroo Feature Tutorials
- 1: Setting Up Wallaroo for Sample Pipelines and Models
- 2: Wallaroo ML Models Upload and Registrations
- 2.1: Model Registry Service with Wallaroo Tutorials
- 2.2: Wallaroo SDK Upload Tutorials: Arbitrary Python
- 2.2.1: Wallaroo SDK Upload Arbitrary Python Tutorial: Generate VGG16 Model
- 2.2.2: Wallaroo SDK Upload Arbitrary Python Tutorial: Deploy VGG16 Model
- 2.3: Wallaroo SDK Upload Tutorials: Hugging Face
- 2.3.1: Wallaroo API Upload Tutorial: Hugging Face Zero Shot Classification
- 2.3.2: Wallaroo SDK Upload Tutorial: Hugging Face Zero Shot Classification
- 2.4: Wallaroo SDK Upload Tutorials: Python Step Shape
- 2.5: Wallaroo SDK Upload Tutorials: Pytorch
- 2.5.1: Wallaroo SDK Upload Tutorial: Pytorch Single IO
- 2.5.2: Wallaroo SDK Upload Tutorial: Pytorch Multiple IO
- 2.6: Wallaroo SDK Upload Tutorials: SKLearn
- 2.6.1: Wallaroo SDK Upload Tutorial: SKLearn Clustering Kmeans
- 2.6.2: Wallaroo SDK Upload Tutorial: SKLearn Clustering SVM
- 2.6.3: Wallaroo SDK Upload Tutorial: SKLearn Linear Regression
- 2.6.4: Wallaroo SDK Upload Tutorial: SKLearn Logistic Regression
- 2.6.5: Wallaroo SDK Upload Tutorial: SKLearn SVM PCA
- 2.7: Wallaroo SDK Upload Tutorials: Tensorflow
- 2.8: Wallaroo SDK Upload Tutorials: Tensorflow Keras
- 2.9: Wallaroo SDK Upload Tutorials: XGBoost
- 3: Model Cookbooks
- 3.1: Aloha Quick Tutorial
- 3.2: Computer Vision Tutorials
- 3.2.1: Step 01: Detecting Objects Using mobilenet
- 3.2.2: Step 02: Detecting Objects Using resnet50
- 3.2.3: Step 03: mobilenet and resnet50 Shadow Deploy
- 3.3: Computer Vision: Yolov8n Demonstration
- 3.4: Computer Vision: Image Detection for Health Care
- 3.4.1: Computer Vision: Image Detection for Health Care
- 3.4.2: Computer Vision: Image Detection for Health Care
- 3.5: CLIP ViT-B/32 Transformer Demonstration with Wallaroo
- 3.6: Containerized MLFlow Tutorial
- 3.7: Demand Curve Quick Start Guide
- 3.8: IMDB Tutorial
- 4: Wallaroo Features Tutorials
- 4.1: Hot Swap Models Tutorial
- 4.2: Inference URL Tutorials
- 4.2.1: Wallaroo SDK Inferencing with Pipeline Inference URL Tutorial
- 4.2.2: Wallaroo MLOps API Inferencing with Pipeline Inference URL Tutorial
- 4.3: Onnx Deployment with Multi Input-Output Tutorial
- 4.4: Model Insights Tutorial
- 4.5: Pipeline Logs Tutorial
- 4.6: Pipeline Logs MLOps API Tutorial
- 4.7: Statsmodel Forecast with Wallaroo Features
- 4.7.1: Statsmodel Forecast with Wallaroo Features: Model Creation
- 4.7.2: Statsmodel Forecast with Wallaroo Features: Deploy and Test Infer
- 4.7.3: Statsmodel Forecast with Wallaroo Features: Parallel Inference
- 4.7.4: Statsmodel Forecast with Wallaroo Features: Data Connection
- 4.7.5: Statsmodel Forecast with Wallaroo Features: ML Workload Orchestration
- 4.8: Tags Tutorial
- 4.9: Large Language Model with GPU Pipeline Deployment in Wallaroo Demonstration
- 4.10: Simulated Edge Tutorial
- 5: Model Validation and Testing in Wallaroo
- 5.1: A/B Testing Tutorial
- 5.2: Anomaly Testing Tutorial
- 5.3: House Price Testing Life Cycle
- 5.4: Shadow Deployment Tutorial
- 6: Using Jupyter Notebooks in Production
- 6.1: Data Exploration And Model Selection
- 6.2: From Jupyter to Production
- 6.3: Deploy the Model in Wallaroo
- 6.4: Regular Batch Inference
- 7: ML Workload Orchestration Tutorials
- 7.1: Wallaroo ML Workload Orchestration API Tutorial
- 7.2: Wallaroo Connection API with Google BigQuery Tutorial
- 7.3: Wallaroo ML Workload Orchestration Simple Tutorial
- 7.4: Wallaroo ML Workload Orchestration Google BigQuery with House Price Model Tutorial
- 7.5: Wallaroo ML Workload Orchestration Google BigQuery with Statsmodel Forecast Tutorial
- 7.6: Wallaroo ML Workload Orchestration Comprehensive Tutorial
- 8: Model Conversion Tutorials
- 9: Wallaroo Tools Tutorials
1 - Setting Up Wallaroo for Sample Pipelines and Models
Notice
This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.Welcome to Wallaroo! We’re excited to help you get your ML models deployed as quickly as possible. These Quick Start Guides are meant to show simple but thorough steps in creating new pipelines and models. For full guides on using Wallaroo, see the Wallaroo SDK and the Wallaroo Operations Guide.
This first guide is just how to get Wallaroo ready for your first models and pipelines.
Prerequisites
This guide assumes that you’ve installed Wallaroo in a cluster cloud Kubernetes cluster.
Basic Wallaroo Configuration
The following will install the Wallaroo Quick Start samples into your Wallaroo Jupyter Hub environment.
From the Wallaroo Quick Start Samples repository, visit the Releases page. As of the 2023.1 release, each tutorial can be downloaded as a separate .zip file instead of downloading the entire release.
Download the release that matches your version of Wallaroo, Either a
.zip
or.tar.gz
file can be downloaded. The example below uses the.zip
file.Access your Jupyter Hub from your Wallaroo environment.
Either drag and drop or use the Upload feature to upload the quick start guide .zip file.
From the launcher, select Terminal.
Unzip the files with
unzip {ZIP FILE NAME}
. For example, if the most recent release isWallaroo_Tutorials.zip
, the command would be:unzip Wallaroo_Tutorials.zip
Exit the terminal shell when complete.
The quick start samples along with models and data will be ready for use in the unzipped folder.
2 - Wallaroo ML Models Upload and Registrations
Tutorials on uploading and registering ML models of different frameworks into Wallaroo.
Additional References
2.1 - Model Registry Service with Wallaroo Tutorials
The following tutorials demonstrate how to add ML Models from a model registry service into a Wallaroo instance.
Artifact Requirements
Models are uploaded to the Wallaroo instance as the specific artifact - the “file” or other data that represents the file itself. This must comply with the Wallaroo model requirements framework and version or it will not be deployed. Note that for models that fall outside of the supported model types, they can be registered to a Wallaroo workspace as MLFlow 1.30.0 containerized models.
Supported Models
The following frameworks are supported. Frameworks fall under either Native or Containerized runtimes in the Wallaroo engine. For more details, see the specific framework what runtime a specific model framework runs in.
Runtime Display | Model Runtime Space | Pipeline Configuration |
---|---|---|
tensorflow | Native | Native Runtime Configuration Methods |
onnx | Native | Native Runtime Configuration Methods |
python | Native | Native Runtime Configuration Methods |
mlflow | Containerized | Containerized Runtime Deployment |
Please note the following.
IMPORTANT NOTICE: FRAMEWORK VERSIONS
The supported frameworks include the specific version of the model framework supported by Wallaroo. It is highly recommended to verify that models uploaded to Wallaroo meet the library and version requirements to ensure proper functioning.Wallaroo natively supports Open Neural Network Exchange (ONNX) models into the Wallaroo engine.
Parameter | Description |
---|---|
Web Site | https://onnx.ai/ |
Supported Libraries | See table below. |
Framework | Framework.ONNX aka onnx |
Runtime | Native aka onnx |
The following ONNX versions models are supported:
Wallaroo Version | ONNX Version | ONNX IR Version | ONNX OPset Version | ONNX ML Opset Version |
---|---|---|---|---|
2023.2.1 (July 2023) | 1.12.1 | 8 | 17 | 3 |
2023.2 (May 2023) | 1.12.1 | 8 | 17 | 3 |
2023.1 (March 2023) | 1.12.1 | 8 | 17 | 3 |
2022.4 (December 2022) | 1.12.1 | 8 | 17 | 3 |
After April 2022 until release 2022.4 (December 2022) | 1.10.* | 7 | 15 | 2 |
Before April 2022 | 1.6.* | 7 | 13 | 2 |
For the most recent release of Wallaroo 2023.2.1, the following native runtimes are supported:
- If converting another ML Model to ONNX (PyTorch, XGBoost, etc) using the onnxconverter-common library, the supported
DEFAULT_OPSET_NUMBER
is 17.
Using different versions or settings outside of these specifications may result in inference issues and other unexpected behavior.
ONNX models always run in the native runtime space.
Data Schemas
ONNX models deployed to Wallaroo have the following data requirements.
- Equal rows constraint: The number of input rows and output rows must match.
- All inputs are tensors: The inputs are tensor arrays with the same shape.
- Data Type Consistency: Data types within each tensor are of the same type.
Equal Rows Constraint
Inference performed through ONNX models are assumed to be in batch format, where each input row corresponds to an output row. This is reflected in the in
fields returned for an inference. In the following example, each input row for an inference is related directly to the inference output.
df = pd.read_json('./data/cc_data_1k.df.json')
display(df.head())
result = ccfraud_pipeline.infer(df.head())
display(result)
INPUT
 | tensor |
---|---|
0 | [-1.0603297501, 2.3544967095000002, -3.5638788326, 5.1387348926, -1.2308457019, -0.7687824608, -3.5881228109, 1.8880837663, -3.2789674274, -3.9563254554, 4.0993439118, -5.6539176395, -0.8775733373, -9.131571192000001, -0.6093537873, -3.7480276773, -5.0309125017, -0.8748149526000001, 1.9870535692, 0.7005485718000001, 0.9204422758, -0.1041491809, 0.3229564351, -0.7418141657, 0.0384120159, 1.0993439146, 1.2603409756, -0.1466244739, -1.4463212439] |
1 | [-1.0603297501, 2.3544967095000002, -3.5638788326, 5.1387348926, -1.2308457019, -0.7687824608, -3.5881228109, 1.8880837663, -3.2789674274, -3.9563254554, 4.0993439118, -5.6539176395, -0.8775733373, -9.131571192000001, -0.6093537873, -3.7480276773, -5.0309125017, -0.8748149526000001, 1.9870535692, 0.7005485718000001, 0.9204422758, -0.1041491809, 0.3229564351, -0.7418141657, 0.0384120159, 1.0993439146, 1.2603409756, -0.1466244739, -1.4463212439] |
2 | [-1.0603297501, 2.3544967095000002, -3.5638788326, 5.1387348926, -1.2308457019, -0.7687824608, -3.5881228109, 1.8880837663, -3.2789674274, -3.9563254554, 4.0993439118, -5.6539176395, -0.8775733373, -9.131571192000001, -0.6093537873, -3.7480276773, -5.0309125017, -0.8748149526000001, 1.9870535692, 0.7005485718000001, 0.9204422758, -0.1041491809, 0.3229564351, -0.7418141657, 0.0384120159, 1.0993439146, 1.2603409756, -0.1466244739, -1.4463212439] |
3 | [-1.0603297501, 2.3544967095000002, -3.5638788326, 5.1387348926, -1.2308457019, -0.7687824608, -3.5881228109, 1.8880837663, -3.2789674274, -3.9563254554, 4.0993439118, -5.6539176395, -0.8775733373, -9.131571192000001, -0.6093537873, -3.7480276773, -5.0309125017, -0.8748149526000001, 1.9870535692, 0.7005485718000001, 0.9204422758, -0.1041491809, 0.3229564351, -0.7418141657, 0.0384120159, 1.0993439146, 1.2603409756, -0.1466244739, -1.4463212439] |
4 | [0.5817662108, 0.09788155100000001, 0.1546819424, 0.4754101949, -0.19788623060000002, -0.45043448540000003, 0.016654044700000002, -0.0256070551, 0.0920561602, -0.2783917153, 0.059329944100000004, -0.0196585416, -0.4225083157, -0.12175388770000001, 1.5473094894000001, 0.2391622864, 0.3553974881, -0.7685165301, -0.7000849355000001, -0.1190043285, -0.3450517133, -1.1065114108, 0.2523411195, 0.0209441826, 0.2199267436, 0.2540689265, -0.0450225094, 0.10867738980000001, 0.2547179311] |
OUTPUT
 | time | in.tensor | out.dense_1 | check_failures |
---|---|---|---|---|
0 | 2023-11-17 20:34:17.005 | [-1.0603297501, 2.3544967095, -3.5638788326, 5.1387348926, -1.2308457019, -0.7687824608, -3.5881228109, 1.8880837663, -3.2789674274, -3.9563254554, 4.0993439118, -5.6539176395, -0.8775733373, -9.131571192, -0.6093537873, -3.7480276773, -5.0309125017, -0.8748149526, 1.9870535692, 0.7005485718, 0.9204422758, -0.1041491809, 0.3229564351, -0.7418141657, 0.0384120159, 1.0993439146, 1.2603409756, -0.1466244739, -1.4463212439] | [0.99300325] | 0 |
1 | 2023-11-17 20:34:17.005 | [-1.0603297501, 2.3544967095, -3.5638788326, 5.1387348926, -1.2308457019, -0.7687824608, -3.5881228109, 1.8880837663, -3.2789674274, -3.9563254554, 4.0993439118, -5.6539176395, -0.8775733373, -9.131571192, -0.6093537873, -3.7480276773, -5.0309125017, -0.8748149526, 1.9870535692, 0.7005485718, 0.9204422758, -0.1041491809, 0.3229564351, -0.7418141657, 0.0384120159, 1.0993439146, 1.2603409756, -0.1466244739, -1.4463212439] | [0.99300325] | 0 |
2 | 2023-11-17 20:34:17.005 | [-1.0603297501, 2.3544967095, -3.5638788326, 5.1387348926, -1.2308457019, -0.7687824608, -3.5881228109, 1.8880837663, -3.2789674274, -3.9563254554, 4.0993439118, -5.6539176395, -0.8775733373, -9.131571192, -0.6093537873, -3.7480276773, -5.0309125017, -0.8748149526, 1.9870535692, 0.7005485718, 0.9204422758, -0.1041491809, 0.3229564351, -0.7418141657, 0.0384120159, 1.0993439146, 1.2603409756, -0.1466244739, -1.4463212439] | [0.99300325] | 0 |
3 | 2023-11-17 20:34:17.005 | [-1.0603297501, 2.3544967095, -3.5638788326, 5.1387348926, -1.2308457019, -0.7687824608, -3.5881228109, 1.8880837663, -3.2789674274, -3.9563254554, 4.0993439118, -5.6539176395, -0.8775733373, -9.131571192, -0.6093537873, -3.7480276773, -5.0309125017, -0.8748149526, 1.9870535692, 0.7005485718, 0.9204422758, -0.1041491809, 0.3229564351, -0.7418141657, 0.0384120159, 1.0993439146, 1.2603409756, -0.1466244739, -1.4463212439] | [0.99300325] | 0 |
4 | 2023-11-17 20:34:17.005 | [0.5817662108, 0.097881551, 0.1546819424, 0.4754101949, -0.1978862306, -0.4504344854, 0.0166540447, -0.0256070551, 0.0920561602, -0.2783917153, 0.0593299441, -0.0196585416, -0.4225083157, -0.1217538877, 1.5473094894, 0.2391622864, 0.3553974881, -0.7685165301, -0.7000849355, -0.1190043285, -0.3450517133, -1.1065114108, 0.2523411195, 0.0209441826, 0.2199267436, 0.2540689265, -0.0450225094, 0.1086773898, 0.2547179311] | [0.0010916889] | 0 |
All Inputs Are Tensors
All inputs into an ONNX model must be tensors. This requires that the shape of each element is the same. For example, the following is a proper input:
t [
[2.35, 5.75],
[3.72, 8.55],
[5.55, 97.2]
]
Another example is a 2,2,3 tensor, where the shape of each element is (3,), and each element has 2 rows.
t = [
[2.35, 5.75, 19.2],
[3.72, 8.55, 10.5]
],
[
[5.55, 7.2, 15.7],
[9.6, 8.2, 2.3]
]
In this example each element has a shape of (2,)
. Tensors with elements of different shapes, known as ragged tensors, are not supported. For example:
t = [
[2.35, 5.75],
[3.72, 8.55, 10.5],
[5.55, 97.2]
])
**INVALID SHAPE**
For models that require ragged tensor or other shapes, see other data formatting options such as Bring Your Own Predict models.
Data Type Consistency
All inputs into an ONNX model must have the same internal data type. For example, the following is valid because all of the data types within each element are float32
.
t = [
[2.35, 5.75],
[3.72, 8.55],
[5.55, 97.2]
]
The following is invalid, as it mixes floats and strings in each element:
t = [
[2.35, "Bob"],
[3.72, "Nancy"],
[5.55, "Wani"]
]
The following inputs are valid, as each data type is consistent within the elements.
df = pd.DataFrame({
"t": [
[2.35, 5.75, 19.2],
[5.55, 7.2, 15.7],
],
"s": [
["Bob", "Nancy", "Wani"],
["Jason", "Rita", "Phoebe"]
]
})
df
 | t | s |
---|---|---|
0 | [2.35, 5.75, 19.2] | [Bob, Nancy, Wani] |
1 | [5.55, 7.2, 15.7] | [Jason, Rita, Phoebe] |
Parameter | Description |
---|---|
Web Site | https://www.tensorflow.org/ |
Supported Libraries | tensorflow==2.9.1 |
Framework | Framework.TENSORFLOW aka tensorflow |
Runtime | Native aka tensorflow |
Supported File Types | SavedModel format as .zip file |
IMPORTANT NOTE
These requirements are <strong>not</strong> for Tensorflow Keras models, only for non-Keras Tensorflow models in the SavedModel format. For Tensorflow Keras deployment in Wallaroo, see the Tensorflow Keras requirements.
TensorFlow File Format
TensorFlow models are .zip file of the SavedModel format. For example, the Aloha sample TensorFlow model is stored in the directory alohacnnlstm
:
├── saved_model.pb
└── variables
├── variables.data-00000-of-00002
├── variables.data-00001-of-00002
└── variables.index
This is compressed into the .zip file alohacnnlstm.zip
with the following command:
zip -r alohacnnlstm.zip alohacnnlstm/
ML models that meet the Tensorflow and SavedModel format will run as Wallaroo Native runtimes by default.
See the SavedModel guide for full details.
Parameter | Description |
---|---|
Web Site | https://www.python.org/ |
Supported Libraries | python==3.8 |
Framework | Framework.PYTHON aka python |
Runtime | Native aka python |
Python models uploaded to Wallaroo are executed as a native runtime.
Note that Python models - aka “Python steps” - are standalone python scripts that use the python libraries natively supported by the Wallaroo platform. These are used for either simple model deployment (such as ARIMA Statsmodels), or data formatting such as the postprocessing steps. A Wallaroo Python model will be composed of one Python script that matches the Wallaroo requirements.
This is contrasted with Arbitrary Python models, also known as Bring Your Own Predict (BYOP) allow for custom model deployments with supporting scripts and artifacts. These are used with pre-trained models (PyTorch, Tensorflow, etc) along with whatever supporting artifacts they require. Supporting artifacts can include other Python modules, model files, etc. These are zipped with all scripts, artifacts, and a requirements.txt
file that indicates what other Python models need to be imported that are outside of the typical Wallaroo platform.
Python Models Requirements
Python models uploaded to Wallaroo are Python scripts that must include the wallaroo_json
method as the entry point for the Wallaroo engine to use it as a Pipeline step.
This method receives the results of the previous Pipeline step, and its return value will be used in the next Pipeline step.
If the Python model is the first step in the pipeline, then it will be receiving the inference request data (for example: a preprocessing step). If it is the last step in the pipeline, then it will be the data returned from the inference request.
In the example below, the Python model is used as a post processing step for another ML model. The Python model expects to receive data from a ML Model who’s output is a DataFrame with the column dense_2
. It then extracts the values of that column as a list, selects the first element, and returns a DataFrame with that element as the value of the column output
.
def wallaroo_json(data: pd.DataFrame):
print(data)
return [{"output": [data["dense_2"].to_list()[0][0]]}]
In line with other Wallaroo inference results, the outputs of a Python step that returns a pandas DataFrame or Arrow Table will be listed in the out.
metadata, with all inference outputs listed as out.{variable 1}
, out.{variable 2}
, etc. In the example above, this results the output
field as the out.output
field in the Wallaroo inference result.
 | time | in.tensor | out.output | check_failures |
---|---|---|---|---|
0 | 2023-06-20 20:23:28.395 | [0.6878518042, 0.1760734021, -0.869514083, 0.3.. | [12.886651039123535] | 0 |
Parameter | Description |
---|---|
Web Site | https://huggingface.co/models |
Supported Libraries |
|
Frameworks | The following Hugging Face pipelines are supported by Wallaroo.
|
Runtime | Containerized aka tensorflow / mlflow |
Hugging Face Schemas
Input and output schemas for each Hugging Face pipeline are defined below. Note that adding additional inputs not specified below will raise errors, except for the following:
Framework.HUGGING-FACE-IMAGE-TO-TEXT
Framework.HUGGING-FACE-TEXT-CLASSIFICATION
Framework.HUGGING-FACE-SUMMARIZATION
Framework.HUGGING-FACE-TRANSLATION
Additional inputs added to these Hugging Face pipelines will be added as key/pair value arguments to the model’s generate method. If the argument is not required, then the model will default to the values coded in the original Hugging Face model’s source code.
See the Hugging Face Pipeline documentation for more details on each pipeline and framework.
Wallaroo Framework | Reference |
---|---|
Framework.HUGGING-FACE-FEATURE-EXTRACTION |
Schemas:
input_schema = pa.schema([
pa.field('inputs', pa.string())
])
output_schema = pa.schema([
pa.field('output', pa.list_(
pa.list_(
pa.float64(),
list_size=128
),
))
])
Wallaroo Framework | Reference |
---|---|
Framework.HUGGING-FACE-IMAGE-CLASSIFICATION |
Schemas:
input_schema = pa.schema([
pa.field('inputs', pa.list_(
pa.list_(
pa.list_(
pa.int64(),
list_size=3
),
list_size=100
),
list_size=100
)),
pa.field('top_k', pa.int64()),
])
output_schema = pa.schema([
pa.field('score', pa.list_(pa.float64(), list_size=2)),
pa.field('label', pa.list_(pa.string(), list_size=2)),
])
Wallaroo Framework | Reference |
---|---|
Framework.HUGGING-FACE-IMAGE-SEGMENTATION |
Schemas:
input_schema = pa.schema([
pa.field('inputs',
pa.list_(
pa.list_(
pa.list_(
pa.int64(),
list_size=3
),
list_size=100
),
list_size=100
)),
pa.field('threshold', pa.float64()),
pa.field('mask_threshold', pa.float64()),
pa.field('overlap_mask_area_threshold', pa.float64()),
])
output_schema = pa.schema([
pa.field('score', pa.list_(pa.float64())),
pa.field('label', pa.list_(pa.string())),
pa.field('mask',
pa.list_(
pa.list_(
pa.list_(
pa.int64(),
list_size=100
),
list_size=100
),
)),
])
Wallaroo Framework | Reference |
---|---|
Framework.HUGGING-FACE-IMAGE-TO-TEXT |
Any parameter that is not part of the required inputs
list will be forwarded to the model as a key/pair value to the underlying models generate
method. If the additional input is not supported by the model, an error will be returned.
Schemas:
input_schema = pa.schema([
pa.field('inputs', pa.list_( #required
pa.list_(
pa.list_(
pa.int64(),
list_size=3
),
list_size=100
),
list_size=100
)),
# pa.field('max_new_tokens', pa.int64()), # optional
])
output_schema = pa.schema([
pa.field('generated_text', pa.list_(pa.string())),
])
Wallaroo Framework | Reference |
---|---|
Framework.HUGGING-FACE-OBJECT-DETECTION |
Schemas:
input_schema = pa.schema([
pa.field('inputs',
pa.list_(
pa.list_(
pa.list_(
pa.int64(),
list_size=3
),
list_size=100
),
list_size=100
)),
pa.field('threshold', pa.float64()),
])
output_schema = pa.schema([
pa.field('score', pa.list_(pa.float64())),
pa.field('label', pa.list_(pa.string())),
pa.field('box',
pa.list_( # dynamic output, i.e. dynamic number of boxes per input image, each sublist contains the 4 box coordinates
pa.list_(
pa.int64(),
list_size=4
),
),
),
])
Wallaroo Framework | Reference |
---|---|
Framework.HUGGING-FACE-QUESTION-ANSWERING |
Schemas:
input_schema = pa.schema([
pa.field('question', pa.string()),
pa.field('context', pa.string()),
pa.field('top_k', pa.int64()),
pa.field('doc_stride', pa.int64()),
pa.field('max_answer_len', pa.int64()),
pa.field('max_seq_len', pa.int64()),
pa.field('max_question_len', pa.int64()),
pa.field('handle_impossible_answer', pa.bool_()),
pa.field('align_to_words', pa.bool_()),
])
output_schema = pa.schema([
pa.field('score', pa.float64()),
pa.field('start', pa.int64()),
pa.field('end', pa.int64()),
pa.field('answer', pa.string()),
])
Wallaroo Framework | Reference |
---|---|
Framework.HUGGING-FACE-STABLE-DIFFUSION-TEXT-2-IMG |
Schemas:
input_schema = pa.schema([
pa.field('prompt', pa.string()),
pa.field('height', pa.int64()),
pa.field('width', pa.int64()),
pa.field('num_inference_steps', pa.int64()), # optional
pa.field('guidance_scale', pa.float64()), # optional
pa.field('negative_prompt', pa.string()), # optional
pa.field('num_images_per_prompt', pa.string()), # optional
pa.field('eta', pa.float64()) # optional
])
output_schema = pa.schema([
pa.field('images', pa.list_(
pa.list_(
pa.list_(
pa.int64(),
list_size=3
),
list_size=128
),
list_size=128
)),
])
Wallaroo Framework | Reference |
---|---|
Framework.HUGGING-FACE-SUMMARIZATION |
Any parameter that is not part of the required inputs
list will be forwarded to the model as a key/pair value to the underlying models generate
method. If the additional input is not supported by the model, an error will be returned.
Schemas:
input_schema = pa.schema([
pa.field('inputs', pa.string()),
pa.field('return_text', pa.bool_()),
pa.field('return_tensors', pa.bool_()),
pa.field('clean_up_tokenization_spaces', pa.bool_()),
# pa.field('extra_field', pa.int64()), # every extra field you specify will be forwarded as a key/value pair
])
output_schema = pa.schema([
pa.field('summary_text', pa.string()),
])
Wallaroo Framework | Reference |
---|---|
Framework.HUGGING-FACE-TEXT-CLASSIFICATION |
Schemas
input_schema = pa.schema([
pa.field('inputs', pa.string()), # required
pa.field('top_k', pa.int64()), # optional
pa.field('function_to_apply', pa.string()), # optional
])
output_schema = pa.schema([
pa.field('label', pa.list_(pa.string(), list_size=2)), # list with a number of items same as top_k, list_size can be skipped but may lead in worse performance
pa.field('score', pa.list_(pa.float64(), list_size=2)), # list with a number of items same as top_k, list_size can be skipped but may lead in worse performance
])
Wallaroo Framework | Reference |
---|---|
Framework.HUGGING-FACE-TRANSLATION |
Any parameter that is not part of the required inputs
list will be forwarded to the model as a key/pair value to the underlying models generate
method. If the additional input is not supported by the model, an error will be returned.
Schemas:
input_schema = pa.schema([
pa.field('inputs', pa.string()), # required
pa.field('return_tensors', pa.bool_()), # optional
pa.field('return_text', pa.bool_()), # optional
pa.field('clean_up_tokenization_spaces', pa.bool_()), # optional
pa.field('src_lang', pa.string()), # optional
pa.field('tgt_lang', pa.string()), # optional
# pa.field('extra_field', pa.int64()), # every extra field you specify will be forwarded as a key/value pair
])
output_schema = pa.schema([
pa.field('translation_text', pa.string()),
])
Wallaroo Framework | Reference |
---|---|
Framework.HUGGING-FACE-ZERO-SHOT-CLASSIFICATION |
Schemas:
input_schema = pa.schema([
pa.field('inputs', pa.string()), # required
pa.field('candidate_labels', pa.list_(pa.string(), list_size=2)), # required
pa.field('hypothesis_template', pa.string()), # optional
pa.field('multi_label', pa.bool_()), # optional
])
output_schema = pa.schema([
pa.field('sequence', pa.string()),
pa.field('scores', pa.list_(pa.float64(), list_size=2)), # same as number of candidate labels, list_size can be skipped by may result in slightly worse performance
pa.field('labels', pa.list_(pa.string(), list_size=2)), # same as number of candidate labels, list_size can be skipped by may result in slightly worse performance
])
Wallaroo Framework | Reference |
---|---|
Framework.HUGGING-FACE-ZERO-SHOT-IMAGE-CLASSIFICATION |
Schemas:
input_schema = pa.schema([
pa.field('inputs', # required
pa.list_(
pa.list_(
pa.list_(
pa.int64(),
list_size=3
),
list_size=100
),
list_size=100
)),
pa.field('candidate_labels', pa.list_(pa.string(), list_size=2)), # required
pa.field('hypothesis_template', pa.string()), # optional
])
output_schema = pa.schema([
pa.field('score', pa.list_(pa.float64(), list_size=2)), # same as number of candidate labels
pa.field('label', pa.list_(pa.string(), list_size=2)), # same as number of candidate labels
])
Wallaroo Framework | Reference |
---|---|
Framework.HUGGING-FACE-ZERO-SHOT-OBJECT-DETECTION |
Schemas:
input_schema = pa.schema([
pa.field('images',
pa.list_(
pa.list_(
pa.list_(
pa.int64(),
list_size=3
),
list_size=640
),
list_size=480
)),
pa.field('candidate_labels', pa.list_(pa.string(), list_size=3)),
pa.field('threshold', pa.float64()),
# pa.field('top_k', pa.int64()), # we want the model to return exactly the number of predictions, we shouldn't specify this
])
output_schema = pa.schema([
pa.field('score', pa.list_(pa.float64())), # variable output, depending on detected objects
pa.field('label', pa.list_(pa.string())), # variable output, depending on detected objects
pa.field('box',
pa.list_( # dynamic output, i.e. dynamic number of boxes per input image, each sublist contains the 4 box coordinates
pa.list_(
pa.int64(),
list_size=4
),
),
),
])
Wallaroo Framework | Reference |
---|---|
Framework.HUGGING-FACE-SENTIMENT-ANALYSIS | Hugging Face Sentiment Analysis |
Wallaroo Framework | Reference |
---|---|
Framework.HUGGING-FACE-TEXT-GENERATION |
Any parameter that is not part of the required inputs
list will be forwarded to the model as a key/pair value to the underlying models generate
method. If the additional input is not supported by the model, an error will be returned.
input_schema = pa.schema([
pa.field('inputs', pa.string()),
pa.field('return_tensors', pa.bool_()), # optional
pa.field('return_text', pa.bool_()), # optional
pa.field('return_full_text', pa.bool_()), # optional
pa.field('clean_up_tokenization_spaces', pa.bool_()), # optional
pa.field('prefix', pa.string()), # optional
pa.field('handle_long_generation', pa.string()), # optional
# pa.field('extra_field', pa.int64()), # every extra field you specify will be forwarded as a key/value pair
])
output_schema = pa.schema([
pa.field('generated_text', pa.list_(pa.string(), list_size=1))
])
Parameter | Description |
---|---|
Web Site | https://pytorch.org/ |
Supported Libraries |
|
Framework | Framework.PYTORCH aka pytorch |
Supported File Types | pt ot pth in TorchScript format |
Runtime | Containerized aka mlflow |
Sci-kit Learn aka SKLearn.
Parameter | Description |
---|---|
Web Site | https://scikit-learn.org/stable/index.html |
Supported Libraries |
|
Framework | Framework.SKLEARN aka sklearn |
Runtime | Containerized aka tensorflow / mlflow |
SKLearn Schema Inputs
SKLearn schema follows a different format than other models. To prevent inputs from being out of order, the inputs should be submitted in a single row in the order the model is trained to accept, with all of the data types being the same. For example, the following DataFrame has 4 columns, each column a float
.
 | sepal length (cm) | sepal width (cm) | petal length (cm) | petal width (cm) |
---|---|---|---|---|
0 | 5.1 | 3.5 | 1.4 | 0.2 |
1 | 4.9 | 3.0 | 1.4 | 0.2 |
For submission to an SKLearn model, the data input schema will be a single array with 4 float values.
input_schema = pa.schema([
pa.field('inputs', pa.list_(pa.float64(), list_size=4))
])
When submitting as an inference, the DataFrame is converted to rows with the column data expressed as a single array. The data must be in the same order as the model expects, which is why the data is submitted as a single array rather than JSON labeled columns: this insures that the data is submitted in the exact order as the model is trained to accept.
Original DataFrame:
 | sepal length (cm) | sepal width (cm) | petal length (cm) | petal width (cm) |
---|---|---|---|---|
0 | 5.1 | 3.5 | 1.4 | 0.2 |
1 | 4.9 | 3.0 | 1.4 | 0.2 |
Converted DataFrame:
 | inputs |
---|---|
0 | [5.1, 3.5, 1.4, 0.2] |
1 | [4.9, 3.0, 1.4, 0.2] |
SKLearn Schema Outputs
Outputs for SKLearn that are meant to be predictions
or probabilities
when output by the model are labeled in the output schema for the model when uploaded to Wallaroo. For example, a model that outputs either 1 or 0 as its output would have the output schema as follows:
output_schema = pa.schema([
pa.field('predictions', pa.int32())
])
When used in Wallaroo, the inference result is contained in the out
metadata as out.predictions
.
pipeline.infer(dataframe)
 | time | in.inputs | out.predictions | check_failures |
---|---|---|---|---|
0 | 2023-07-05 15:11:29.776 | [5.1, 3.5, 1.4, 0.2] | 0 | 0 |
1 | 2023-07-05 15:11:29.776 | [4.9, 3.0, 1.4, 0.2] | 0 | 0 |
Parameter | Description |
---|---|
Web Site | https://www.tensorflow.org/api_docs/python/tf/keras/Model |
Supported Libraries |
|
Framework | Framework.KERAS aka keras |
Supported File Types | SavedModel format as .zip file and HDF5 format |
Runtime | Containerized aka mlflow |
TensorFlow Keras SavedModel Format
TensorFlow Keras SavedModel models are .zip file of the SavedModel format. For example, the Aloha sample TensorFlow model is stored in the directory alohacnnlstm
:
├── saved_model.pb
└── variables
├── variables.data-00000-of-00002
├── variables.data-00001-of-00002
└── variables.index
This is compressed into the .zip file alohacnnlstm.zip
with the following command:
zip -r alohacnnlstm.zip alohacnnlstm/
See the SavedModel guide for full details.
TensorFlow Keras H5 Format
Wallaroo supports the H5 for Tensorflow Keras models.
Parameter | Description |
---|---|
Web Site | https://xgboost.ai/ |
Supported Libraries | xgboost==1.7.4 |
Framework | Framework.XGBOOST aka xgboost |
Supported File Types | pickle (XGB files are not supported.) |
Runtime | Containerized aka tensorflow / mlflow |
XGBoost Schema Inputs
XGBoost schema follows a different format than other models. To prevent inputs from being out of order, the inputs should be submitted in a single row in the order the model is trained to accept, with all of the data types being the same. If a model is originally trained to accept inputs of different data types, it will need to be retrained to only accept one data type for each column - typically pa.float64()
is a good choice.
For example, the following DataFrame has 4 columns, each column a float
.
 | sepal length (cm) | sepal width (cm) | petal length (cm) | petal width (cm) |
---|---|---|---|---|
0 | 5.1 | 3.5 | 1.4 | 0.2 |
1 | 4.9 | 3.0 | 1.4 | 0.2 |
For submission to an XGBoost model, the data input schema will be a single array with 4 float values.
input_schema = pa.schema([
pa.field('inputs', pa.list_(pa.float64(), list_size=4))
])
When submitting as an inference, the DataFrame is converted to rows with the column data expressed as a single array. The data must be in the same order as the model expects, which is why the data is submitted as a single array rather than JSON labeled columns: this insures that the data is submitted in the exact order as the model is trained to accept.
Original DataFrame:
 | sepal length (cm) | sepal width (cm) | petal length (cm) | petal width (cm) |
---|---|---|---|---|
0 | 5.1 | 3.5 | 1.4 | 0.2 |
1 | 4.9 | 3.0 | 1.4 | 0.2 |
Converted DataFrame:
 | inputs |
---|---|
0 | [5.1, 3.5, 1.4, 0.2] |
1 | [4.9, 3.0, 1.4, 0.2] |
XGBoost Schema Outputs
Outputs for XGBoost are labeled based on the trained model outputs. For this example, the output is simply a single output listed as output
. In the Wallaroo inference result, it is grouped with the metadata out
as out.output
.
output_schema = pa.schema([
pa.field('output', pa.int32())
])
pipeline.infer(dataframe)
 | time | in.inputs | out.output | check_failures |
---|---|---|---|---|
0 | 2023-07-05 15:11:29.776 | [5.1, 3.5, 1.4, 0.2] | 0 | 0 |
1 | 2023-07-05 15:11:29.776 | [4.9, 3.0, 1.4, 0.2] | 0 | 0 |
Parameter | Description |
---|---|
Web Site | https://www.python.org/ |
Supported Libraries | python==3.8 |
Framework | Framework.CUSTOM aka custom |
Runtime | Containerized aka mlflow |
Arbitrary Python models, also known as Bring Your Own Predict (BYOP) allow for custom model deployments with supporting scripts and artifacts. These are used with pre-trained models (PyTorch, Tensorflow, etc) along with whatever supporting artifacts they require. Supporting artifacts can include other Python modules, model files, etc. These are zipped with all scripts, artifacts, and a requirements.txt
file that indicates what other Python models need to be imported that are outside of the typical Wallaroo platform.
Contrast this with Wallaroo Python models - aka “Python steps”. These are standalone python scripts that use the python libraries natively supported by the Wallaroo platform. These are used for either simple model deployment (such as ARIMA Statsmodels), or data formatting such as the postprocessing steps. A Wallaroo Python model will be composed of one Python script that matches the Wallaroo requirements.
Arbitrary Python File Requirements
Arbitrary Python (BYOP) models are uploaded to Wallaroo via a ZIP file with the following components:
Artifact | Type | Description |
---|---|---|
Python scripts aka .py files with classes that extend mac.inference.Inference and mac.inference.creation.InferenceBuilder | Python Script | Extend the classes mac.inference.Inference and mac.inference.creation.InferenceBuilder . These are included with the Wallaroo SDK. Further details are in Arbitrary Python Script Requirements. Note that there is no specified naming requirements for the classes that extend mac.inference.Inference and mac.inference.creation.InferenceBuilder - any qualified class name is sufficient as long as these two classes are extended as defined below. |
requirements.txt | Python requirements file | This sets the Python libraries used for the arbitrary python model. These libraries should be targeted for Python 3.8 compliance. These requirements and the versions of libraries should be exactly the same between creating the model and deploying it in Wallaroo. This insures that the script and methods will function exactly the same as during the model creation process. |
Other artifacts | Files | Other models, files, and other artifacts used in support of this model. |
For example, the if the arbitrary python model will be known as vgg_clustering
, the contents may be in the following structure, with vgg_clustering
as the storage directory:
vgg_clustering\
feature_extractor.h5
kmeans.pkl
custom_inference.py
requirements.txt
Note the inclusion of the custom_inference.py
file. This file name is not required - any Python script or scripts that extend the classes listed above are sufficient. This Python script could have been named vgg_custom_model.py
or any other name as long as it includes the extension of the classes listed above.
The sample arbitrary python model file is created with the command zip -r vgg_clustering.zip vgg_clustering/
.
Wallaroo Arbitrary Python uses the Wallaroo SDK mac
module, included in the Wallaroo SDK 2023.2.1 and above. See the Wallaroo SDK Install Guides for instructions on installing the Wallaroo SDK.
Arbitrary Python Script Requirements
The entry point of the arbitrary python model is any python script that extends the following classes. These are included with the Wallaroo SDK. The required methods that must be overridden are specified in each section below.
mac.inference.Inference
interface serves model inferences based on submitted input some input. Its purpose is to serve inferences for any supported arbitrary model framework (e.g.scikit
,keras
etc.).classDiagram class Inference { <<Abstract>> +model Optional[Any] +expected_model_types()* Set +predict(input_data: InferenceData)* InferenceData -raise_error_if_model_is_not_assigned() None -raise_error_if_model_is_wrong_type() None }
mac.inference.creation.InferenceBuilder
builds a concreteInference
, i.e. instantiates anInference
object, loads the appropriate model and assigns the model to to the Inference object.classDiagram class InferenceBuilder { +create(config InferenceConfig) * Inference -inference()* Any }
mac.inference.Inference
mac.inference.Inference Objects
Object | Type | Description |
---|---|---|
model Optional[Any] | An optional list of models that match the supported frameworks from wallaroo.framework.Framework included in the arbitrary python script. Note that this is optional - no models are actually required. A BYOP can refer to a specific model(s) used, be used for data processing and reshaping for later pipeline steps, or other needs. |
mac.inference.Inference Methods
Method | Returns | Description |
---|---|---|
expected_model_types (Required) | Set | Returns a Set of models expected for the inference as defined by the developer. Typically this is a set of one. Wallaroo checks the expected model types to verify that the model submitted through the InferenceBuilder method matches what this Inference class expects. |
_predict (input_data: mac.types.InferenceData) (Required) | mac.types.InferenceData | The entry point for the Wallaroo inference with the following input and output parameters that are defined when the model is updated.
InferenceDataValidationError exception is raised when the input data does not match mac.types.InferenceData . |
raise_error_if_model_is_not_assigned | N/A | Error when expected_model_types is not set. |
raise_error_if_model_is_wrong_type | N/A | Error when the model does not match the expected_model_types . |
mac.inference.creation.InferenceBuilder
InferenceBuilder
builds a concrete Inference
, i.e. instantiates an Inference
object, loads the appropriate model and assigns the model to the Inference.
classDiagram class InferenceBuilder { +create(config InferenceConfig) * Inference -inference()* Any }
Each model that is included requires its own InferenceBuilder
. InferenceBuilder
loads one model, then submits it to the Inference
class when created. The Inference
class checks this class against its expected_model_types()
Set.
mac.inference.creation.InferenceBuilder Methods
Method | Returns | Description |
---|---|---|
create(config mac.config.inference.CustomInferenceConfig) (Required) | The custom Inference instance. | Creates an Inference subclass, then assigns a model and attributes. The CustomInferenceConfig is used to retrieve the config.model_path , which is a pathlib.Path object pointing to the folder where the model artifacts are saved. Every artifact loaded must be relative to config.model_path . This is set when the arbitrary python .zip file is uploaded and the environment for running it in Wallaroo is set. For example: loading the artifact vgg_clustering\feature_extractor.h5 would be set with config.model_path \ feature_extractor.h5 . The model loaded must match an existing module. For our example, this is from sklearn.cluster import KMeans , and this must match the Inference expected_model_types . |
inference | custom Inference instance. | Returns the instantiated custom Inference object created from the create method. |
Arbitrary Python Runtime
Arbitrary Python always run in the containerized model runtime.
Parameter | Description |
---|---|
Web Site | https://mlflow.org |
Supported Libraries | mlflow==1.30.0 |
Runtime | Containerized aka mlflow |
For models that do not fall under the supported model frameworks, organizations can use containerized MLFlow ML Models.
This guide details how to add ML Models from a model registry service into Wallaroo.
Wallaroo supports both public and private containerized model registries. See the Wallaroo Private Containerized Model Container Registry Guide for details on how to configure a Wallaroo instance with a private model registry.
Wallaroo users can register their trained MLFlow ML Models from a containerized model container registry into their Wallaroo instance and perform inferences with it through a Wallaroo pipeline.
As of this time, Wallaroo only supports MLFlow 1.30.0 containerized models. For information on how to containerize an MLFlow model, see the MLFlow Documentation.
Wallaroo supports both public and private containerized model registries. See the Wallaroo Private Containerized Model Container Registry Guide for details on how to configure a Wallaroo instance with a private model registry.
List Wallaroo Frameworks
Wallaroo frameworks are listed from the Wallaroo.Framework class. The following demonstrates listing all available supported frameworks.
from wallaroo.framework import Framework
[e.value for e in Framework]
['onnx',
'tensorflow',
'python',
'keras',
'sklearn',
'pytorch',
'xgboost',
'hugging-face-feature-extraction',
'hugging-face-image-classification',
'hugging-face-image-segmentation',
'hugging-face-image-to-text',
'hugging-face-object-detection',
'hugging-face-question-answering',
'hugging-face-stable-diffusion-text-2-img',
'hugging-face-summarization',
'hugging-face-text-classification',
'hugging-face-translation',
'hugging-face-zero-shot-classification',
'hugging-face-zero-shot-image-classification',
'hugging-face-zero-shot-object-detection',
'hugging-face-sentiment-analysis',
'hugging-face-text-generation']
2.1.1 - Model Registry Service with Wallaroo SDK Demonstration
This tutorial can be downloaded as part of the Wallaroo Tutorials repository.
MLFLow Registry Model Upload Demonstration
Wallaroo users can register their trained machine learning models from a model registry into their Wallaroo instance and perform inferences with it through a Wallaroo pipeline.
This guide details how to add ML Models from a model registry service into a Wallaroo instance.
Artifact Requirements
Models are uploaded to the Wallaroo instance as the specific artifact - the “file” or other data that represents the file itself. This must comply with the Wallaroo model requirements framework and version or it will not be deployed.
This tutorial will:
- Create a Wallaroo workspace and pipeline.
- Show how to connect a Wallaroo Registry that connects to a Model Registry Service.
- Use the registry connection details to upload a sample model to Wallaroo.
- Perform a sample inference.
Prerequisites
- A Wallaroo version 2023.2.1 or above instance.
- A Model (aka Artifact) Registry Service
References
Tutorial Steps
Import Libraries
We’ll start with importing the libraries we need for the tutorial.
import os
import wallaroo
Connect to the Wallaroo Instance through the User Interface
The next step is to connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. For more information on Wallaroo Client settings, see the Client Connection guide.
wl=wallaroo.Client()
wl = wallaroo.Client()
wallarooPrefix = "doc-test."
wallarooSuffix = "wallarooexample.ai"
wl = wallaroo.Client(api_endpoint=f"https://{wallarooPrefix}api.{wallarooSuffix}",
auth_endpoint=f"https://{wallarooPrefix}keycloak.{wallarooSuffix}",
auth_type="sso")
Please log into the following URL in a web browser:
https://doc-test.keycloak.wallarooexample.ai/auth/realms/master/device?user_code=YOSQ-MZCY
Login successful!
Connect to Model Registry
The Wallaroo Registry stores the URL and authentication token to the Model Registry service, with the assigned name. Note that in this demonstration all URLs and token are examples.
registry = wl.create_model_registry(name="JeffRegistry45",
token="dapi67c8c0b04606f730e78b7ae5e3221015-3",
url="https://sample.registry.service.azuredatabricks.net")
registry
Field | Value |
---|---|
Name | JeffRegistry45 |
URL | https://sample.registry.service.azuredatabricks.net |
Workspaces | john.hummel@wallaroo.ai - Default Workspace |
Created At | 2023-17-Jul 19:54:49 |
Updated At | 2023-17-Jul 19:54:49 |
List Model Registries
Registries associated with a workspace are listed with the Wallaroo.Client.list_model_registries()
method.
# List all registries in this workspace
registries = wl.list_model_registries()
registries
name | registry url | created at | updated at |
---|---|---|---|
JeffRegistry45 | https://sample.registry.service.azuredatabricks.net | 2023-17-Jul 17:56:52 | 2023-17-Jul 17:56:52 |
JeffRegistry45 | https://sample.registry.service.azuredatabricks.net | 2023-17-Jul 19:54:49 | 2023-17-Jul 19:54:49 |
Create Workspace
For this demonstration, we will create a random Wallaroo workspace, then attach our registry to the workspace so it is accessible by other workspace users.
Add Registry to Workspace
Registries are assigned to a Wallaroo workspace with the Wallaroo.registry.add_registry_to_workspace
method. This allows members of the workspace to access the registry connection. A registry can be associated with one or more workspaces.
Add Registry to Workspace Parameters
Parameter | Type | Description |
---|---|---|
name | string (Required) | The numerical identifier of the workspace. |
# Make a random new workspace
import math
import random
num = math.floor(random.random()* 1000)
workspace_id = wl.create_workspace(f"test{num}").id()
registry.add_registry_to_workspace(workspace_id=workspace_id)
Field | Value |
---|---|
Name | JeffRegistry45 |
URL | https://sample.registry.service.azuredatabricks.net |
Workspaces | test68, john.hummel@wallaroo.ai - Default Workspace |
Created At | 2023-17-Jul 19:54:49 |
Updated At | 2023-17-Jul 19:54:49 |
Remove Registry from Workspace
Registries are removed from a Wallaroo workspace with the Registry remove_registry_from_workspace
method.
Remove Registry from Workspace Parameters
Parameter | Type | Description |
---|---|---|
workspace_id | Integer (Required) | The numerical identifier of the workspace. |
registry.remove_registry_from_workspace(workspace_id=workspace_id)
Field | Value |
---|---|
Name | JeffRegistry45 |
URL | https://sample.registry.service.azuredatabricks.net |
Workspaces | john.hummel@wallaroo.ai - Default Workspace |
Created At | 2023-17-Jul 19:54:49 |
Updated At | 2023-17-Jul 19:54:49 |
List Models in a Registry
A List of models available to the Wallaroo instance through the MLFlow Registry is performed with the Wallaroo.Registry.list_models()
method.
registry_models = registry.list_models()
registry_models
Name | Registry User | Versions | Created At | Updated At |
Select Model from Registry
Registry models are selected from the Wallaroo.Registry.list_models()
method, then specifying the model to use.
single_registry_model = registry_models[4]
single_registry_model
Name | verified-working |
Registry User | gib.bhojraj@wallaroo.ai |
Versions | 1 |
Created At | 2023-11-Jul 16:18:03 |
Updated At | 2023-11-Jul 16:57:54 |
List Model Versions
The Registry Model attribute versions
shows the complete list of versions for the particular model.
single_registry_model.versions()
Name | Version | Description |
verified-working | 3 | None |
List Model Version Artifacts
Artifacts belonging to a MLFlow registry model are listed with the Model Version list_artifacts()
method. This returns all artifacts for the model.
single_registry_model.versions()[1].list_artifacts()
File Name | File Size | Full Path |
---|---|---|
MLmodel | 559B | https://sample.registry.service.azuredatabricks.net/api/2.0/dbfs/read?path=/databricks/mlflow-registry/9168792a16cb40a88de6959ef31e42a2/models/√erified-working/MLmodel |
conda.yaml | 182B | https://sample.registry.service.azuredatabricks.net/api/2.0/dbfs/read?path=/databricks/mlflow-registry/9168792a16cb40a88de6959ef31e42a2/models/√erified-working/conda.yaml |
model.pkl | 829B | https://sample.registry.service.azuredatabricks.net/api/2.0/dbfs/read?path=/databricks/mlflow-registry/9168792a16cb40a88de6959ef31e42a2/models/√erified-working/model.pkl |
python_env.yaml | 122B | https://sample.registry.service.azuredatabricks.net/api/2.0/dbfs/read?path=/databricks/mlflow-registry/9168792a16cb40a88de6959ef31e42a2/models/√erified-working/python_env.yaml |
requirements.txt | 73B | https://sample.registry.service.azuredatabricks.net/api/2.0/dbfs/read?path=/databricks/mlflow-registry/9168792a16cb40a88de6959ef31e42a2/models/√erified-working/requirements.txt |
Configure Data Schemas
To upload a ML Model to Wallaroo, the input and output schemas must be defined in pyarrow.lib.Schema
format.
from wallaroo.framework import Framework
import pyarrow as pa
input_schema = pa.schema([
pa.field('inputs', pa.list_(pa.float64(), list_size=4))
])
output_schema = pa.schema([
pa.field('predictions', pa.int32()),
pa.field('probabilities', pa.list_(pa.float64(), list_size=3))
])
Upload a Model from a Registry
Models uploaded to the Wallaroo workspace are uploaded from a MLFlow Registry with the Wallaroo.Registry.upload
method.
Upload a Model from a Registry Parameters
Parameter | Type | Description |
---|---|---|
name | string (Required) | The name to assign the model once uploaded. Model names are unique within a workspace. Models assigned the same name as an existing model will be uploaded as a new model version. |
path | string (Required) | The full path to the model artifact in the registry. |
framework | string (Required) | The Wallaroo model Framework . See Model Uploads and Registrations Supported Frameworks |
input_schema | pyarrow.lib.Schema (Required for non-native runtimes) | The input schema in Apache Arrow schema format. |
output_schema | pyarrow.lib.Schema (Required for non-native runtimes) | The output schema in Apache Arrow schema format. |
model = registry.upload_model(
name="verified-working",
path="https://sample.registry.service.azuredatabricks.net/api/2.0/dbfs/read?path=/databricks/mlflow-registry/9168792a16cb40a88de6959ef31e42a2/models/√erified-working/model.pkl",
framework=Framework.SKLEARN,
input_schema=input_schema,
output_schema=output_schema)
model
Name | verified-working |
Version | cf194b65-65b2-4d42-a4e2-6ca6fa5bfc42 |
File Name | model.pkl |
SHA | 5f4c25b0b564ab9fe0ea437424323501a460aa74463e81645a6419be67933ca4 |
Status | pending_conversion |
Image Path | None |
Updated At | 2023-17-Jul 17:57:23 |
Verify the Model Status
Once uploaded, the model will undergo conversion. The following will loop through the model status until it is ready. Once ready, it is available for deployment.
import time
while model.status() != "ready" and model.status() != "error":
print(model.status())
time.sleep(3)
print(model.status())
pending_conversion
pending_conversion
pending_conversion
pending_conversion
pending_conversion
pending_conversion
pending_conversion
pending_conversion
pending_conversion
pending_conversion
converting
converting
converting
converting
converting
converting
converting
converting
converting
converting
converting
converting
converting
converting
converting
ready
Model Runtime
Once uploaded and converted, the model runtime is derived. This determines whether to allocate resources to pipeline’s native runtime environment or containerized runtime environment. For more details, see the Wallaroo SDK Essentials Guide: Pipeline Deployment Configuration guide.
model.config().runtime()
'mlflow'
Deploy Pipeline
The model is uploaded and ready for use. We’ll add it as a step in our pipeline, then deploy the pipeline. For this example we’re allocated 0.5 cpu to the runtime environment and 1 CPU to the containerized runtime environment.
import os, json
from wallaroo.deployment_config import DeploymentConfigBuilder
deployment_config = DeploymentConfigBuilder().cpus(0.5).sidekick_cpus(model, 1).build()
pipeline = wl.build_pipeline("jefftest1")
pipeline = pipeline.add_model_step(model)
deployment = pipeline.deploy(deployment_config=deployment_config)
pipeline.status()
{'status': 'Running',
'details': [],
'engines': [{'ip': '10.244.3.148',
'name': 'engine-86c7fc5c95-8kwh5',
'status': 'Running',
'reason': None,
'details': [],
'pipeline_statuses': {'pipelines': [{'id': 'jefftest1',
'status': 'Running'}]},
'model_statuses': {'models': [{'name': 'verified-working',
'version': 'cf194b65-65b2-4d42-a4e2-6ca6fa5bfc42',
'sha': '5f4c25b0b564ab9fe0ea437424323501a460aa74463e81645a6419be67933ca4',
'status': 'Running'}]}}],
'engine_lbs': [{'ip': '10.244.4.203',
'name': 'engine-lb-584f54c899-tpv5b',
'status': 'Running',
'reason': None,
'details': []}],
'sidekicks': [{'ip': '10.244.0.225',
'name': 'engine-sidekick-verified-working-43-74f957566d-9zdfh',
'status': 'Running',
'reason': None,
'details': [],
'statuses': '\n'}]}
Run Inference
A sample inference will be run. First the pandas DataFrame used for the inference is created, then the inference run through the pipeline’s infer
method.
import pandas as pd
from sklearn.datasets import load_iris
data = load_iris(as_frame=True)
X = data['data'].values
dataframe = pd.DataFrame({"inputs": data['data'][:2].values.tolist()})
dataframe
inputs | |
---|---|
0 | [5.1, 3.5, 1.4, 0.2] |
1 | [4.9, 3.0, 1.4, 0.2] |
deployment.infer(dataframe)
time | in.inputs | out.predictions | out.probabilities | check_failures | |
---|---|---|---|---|---|
0 | 2023-07-17 17:59:18.840 | [5.1, 3.5, 1.4, 0.2] | 0 | [0.981814913291491, 0.018185072312411506, 1.43... | 0 |
1 | 2023-07-17 17:59:18.840 | [4.9, 3.0, 1.4, 0.2] | 0 | [0.9717552971628304, 0.02824467272952288, 3.01... | 0 |
Undeploy Pipelines
With the tutorial complete, the pipeline is undeployed to return the resources back to the cluster.
pipeline.undeploy()
name | jefftest1 |
---|---|
created | 2023-07-17 17:59:05.922172+00:00 |
last_updated | 2023-07-17 17:59:06.684060+00:00 |
deployed | False |
tags | |
versions | c2cca319-fcad-47b2-9de0-ad5b2852d1a2, f1e6d1b5-96ee-46a1-bfdf-174310ff4270 |
steps | verified-working |
2.2 - Wallaroo SDK Upload Tutorials: Arbitrary Python
The following tutorials cover how to upload sample arbitrary python models into a Wallaroo instance.
Parameter | Description |
---|---|
Web Site | https://www.python.org/ |
Supported Libraries | python==3.8 |
Framework | Framework.CUSTOM aka custom |
Runtime | Containerized aka mlflow |
Arbitrary Python models, also known as Bring Your Own Predict (BYOP) allow for custom model deployments with supporting scripts and artifacts. These are used with pre-trained models (PyTorch, Tensorflow, etc) along with whatever supporting artifacts they require. Supporting artifacts can include other Python modules, model files, etc. These are zipped with all scripts, artifacts, and a requirements.txt
file that indicates what other Python models need to be imported that are outside of the typical Wallaroo platform.
Contrast this with Wallaroo Python models - aka “Python steps”. These are standalone python scripts that use the python libraries natively supported by the Wallaroo platform. These are used for either simple model deployment (such as ARIMA Statsmodels), or data formatting such as the postprocessing steps. A Wallaroo Python model will be composed of one Python script that matches the Wallaroo requirements.
Arbitrary Python File Requirements
Arbitrary Python (BYOP) models are uploaded to Wallaroo via a ZIP file with the following components:
Artifact | Type | Description |
---|---|---|
Python scripts aka .py files with classes that extend mac.inference.Inference and mac.inference.creation.InferenceBuilder | Python Script | Extend the classes mac.inference.Inference and mac.inference.creation.InferenceBuilder . These are included with the Wallaroo SDK. Further details are in Arbitrary Python Script Requirements. Note that there is no specified naming requirements for the classes that extend mac.inference.Inference and mac.inference.creation.InferenceBuilder - any qualified class name is sufficient as long as these two classes are extended as defined below. |
requirements.txt | Python requirements file | This sets the Python libraries used for the arbitrary python model. These libraries should be targeted for Python 3.8 compliance. These requirements and the versions of libraries should be exactly the same between creating the model and deploying it in Wallaroo. This insures that the script and methods will function exactly the same as during the model creation process. |
Other artifacts | Files | Other models, files, and other artifacts used in support of this model. |
For example, the if the arbitrary python model will be known as vgg_clustering
, the contents may be in the following structure, with vgg_clustering
as the storage directory:
vgg_clustering\
feature_extractor.h5
kmeans.pkl
custom_inference.py
requirements.txt
Note the inclusion of the custom_inference.py
file. This file name is not required - any Python script or scripts that extend the classes listed above are sufficient. This Python script could have been named vgg_custom_model.py
or any other name as long as it includes the extension of the classes listed above.
The sample arbitrary python model file is created with the command zip -r vgg_clustering.zip vgg_clustering/
.
Wallaroo Arbitrary Python uses the Wallaroo SDK mac
module, included in the Wallaroo SDK 2023.2.1 and above. See the Wallaroo SDK Install Guides for instructions on installing the Wallaroo SDK.
Arbitrary Python Script Requirements
The entry point of the arbitrary python model is any python script that extends the following classes. These are included with the Wallaroo SDK. The required methods that must be overridden are specified in each section below.
mac.inference.Inference
interface serves model inferences based on submitted input some input. Its purpose is to serve inferences for any supported arbitrary model framework (e.g.scikit
,keras
etc.).classDiagram class Inference { <<Abstract>> +model Optional[Any] +expected_model_types()* Set +predict(input_data: InferenceData)* InferenceData -raise_error_if_model_is_not_assigned() None -raise_error_if_model_is_wrong_type() None }
mac.inference.creation.InferenceBuilder
builds a concreteInference
, i.e. instantiates anInference
object, loads the appropriate model and assigns the model to to the Inference object.classDiagram class InferenceBuilder { +create(config InferenceConfig) * Inference -inference()* Any }
mac.inference.Inference
mac.inference.Inference Objects
Object | Type | Description |
---|---|---|
model Optional[Any] | An optional list of models that match the supported frameworks from wallaroo.framework.Framework included in the arbitrary python script. Note that this is optional - no models are actually required. A BYOP can refer to a specific model(s) used, be used for data processing and reshaping for later pipeline steps, or other needs. |
mac.inference.Inference Methods
Method | Returns | Description |
---|---|---|
expected_model_types (Required) | Set | Returns a Set of models expected for the inference as defined by the developer. Typically this is a set of one. Wallaroo checks the expected model types to verify that the model submitted through the InferenceBuilder method matches what this Inference class expects. |
_predict (input_data: mac.types.InferenceData) (Required) | mac.types.InferenceData | The entry point for the Wallaroo inference with the following input and output parameters that are defined when the model is updated.
InferenceDataValidationError exception is raised when the input data does not match mac.types.InferenceData . |
raise_error_if_model_is_not_assigned | N/A | Error when expected_model_types is not set. |
raise_error_if_model_is_wrong_type | N/A | Error when the model does not match the expected_model_types . |
mac.inference.creation.InferenceBuilder
InferenceBuilder
builds a concrete Inference
, i.e. instantiates an Inference
object, loads the appropriate model and assigns the model to the Inference.
classDiagram class InferenceBuilder { +create(config InferenceConfig) * Inference -inference()* Any }
Each model that is included requires its own InferenceBuilder
. InferenceBuilder
loads one model, then submits it to the Inference
class when created. The Inference
class checks this class against its expected_model_types()
Set.
mac.inference.creation.InferenceBuilder Methods
Method | Returns | Description |
---|---|---|
create(config mac.config.inference.CustomInferenceConfig) (Required) | The custom Inference instance. | Creates an Inference subclass, then assigns a model and attributes. The CustomInferenceConfig is used to retrieve the config.model_path , which is a pathlib.Path object pointing to the folder where the model artifacts are saved. Every artifact loaded must be relative to config.model_path . This is set when the arbitrary python .zip file is uploaded and the environment for running it in Wallaroo is set. For example: loading the artifact vgg_clustering\feature_extractor.h5 would be set with config.model_path \ feature_extractor.h5 . The model loaded must match an existing module. For our example, this is from sklearn.cluster import KMeans , and this must match the Inference expected_model_types . |
inference | custom Inference instance. | Returns the instantiated custom Inference object created from the create method. |
Arbitrary Python Runtime
Arbitrary Python always run in the containerized model runtime.
2.2.1 - Wallaroo SDK Upload Arbitrary Python Tutorial: Generate VGG16 Model
This tutorial can be downloaded as part of the Wallaroo Tutorials repository.
Wallaroo SDK Upload Arbitrary Python Tutorial: Generate Model
This tutorial demonstrates how to use arbitrary python as a ML Model in Wallaroo. Arbitrary Python allows organizations to use Python scripts that require specific libraries and artifacts as models in the Wallaroo engine. This allows for highly flexible use of ML models with supporting scripts.
Tutorial Goals
This tutorial is split into two parts:
- Wallaroo SDK Upload Arbitrary Python Tutorial: Generate Model: Train a dummy
KMeans
model for clustering images using a pre-trainedVGG16
model oncifar10
as a feature extractor. The Python entry points used for Wallaroo deployment will be added and described.- A copy of the arbitrary Python model
models/model-auto-conversion-BYOP-vgg16-clustering.zip
is included in this tutorial, so this step can be skipped.
- A copy of the arbitrary Python model
- Arbitrary Python Tutorial Deploy Model in Wallaroo Upload and Deploy: Deploys the
KMeans
model in an arbitrary Python package in Wallaroo, and perform sample inferences. The filemodels/model-auto-conversion-BYOP-vgg16-clustering.zip
is provided so users can go right to testing deployment.
Arbitrary Python models, also known as Bring Your Own Predict (BYOP) allow for custom model deployments with supporting scripts and artifacts. These are used with pre-trained models (PyTorch, Tensorflow, etc) along with whatever supporting artifacts they require. Supporting artifacts can include other Python modules, model files, etc. These are zipped with all scripts, artifacts, and a requirements.txt
file that indicates what other Python models need to be imported that are outside of the typical Wallaroo platform.
Contrast this with Wallaroo Python models - aka “Python steps”. These are standalone python scripts that use the python libraries natively supported by the Wallaroo platform. These are used for either simple model deployment (such as ARIMA Statsmodels), or data formatting such as the postprocessing steps. A Wallaroo Python model will be composed of one Python script that matches the Wallaroo requirements.
Arbitrary Python File Requirements
Arbitrary Python (BYOP) models are uploaded to Wallaroo via a ZIP file with the following components:
Artifact | Type | Description |
---|---|---|
Python scripts aka .py files with classes that extend mac.inference.Inference and mac.inference.creation.InferenceBuilder | Python Script | Extend the classes mac.inference.Inference and mac.inference.creation.InferenceBuilder . These are included with the Wallaroo SDK. Further details are in Arbitrary Python Script Requirements. Note that there is no specified naming requirements for the classes that extend mac.inference.Inference and mac.inference.creation.InferenceBuilder - any qualified class name is sufficient as long as these two classes are extended as defined below. |
requirements.txt | Python requirements file | This sets the Python libraries used for the arbitrary python model. These libraries should be targeted for Python 3.8 compliance. These requirements and the versions of libraries should be exactly the same between creating the model and deploying it in Wallaroo. This insures that the script and methods will function exactly the same as during the model creation process. |
Other artifacts | Files | Other models, files, and other artifacts used in support of this model. |
For example, the if the arbitrary python model will be known as vgg_clustering
, the contents may be in the following structure, with vgg_clustering
as the storage directory:
vgg_clustering\
feature_extractor.h5
kmeans.pkl
custom_inference.py
requirements.txt
Note the inclusion of the custom_inference.py
file. This file name is not required - any Python script or scripts that extend the classes listed above are sufficient. This Python script could have been named vgg_custom_model.py
or any other name as long as it includes the extension of the classes listed above.
The sample arbitrary python model file is created with the command zip -r vgg_clustering.zip vgg_clustering/
.
Wallaroo Arbitrary Python uses the Wallaroo SDK mac
module, included in the Wallaroo SDK 2023.2.1 and above. See the Wallaroo SDK Install Guides for instructions on installing the Wallaroo SDK.
Arbitrary Python Script Requirements
The entry point of the arbitrary python model is any python script that extends the following classes. These are included with the Wallaroo SDK. The required methods that must be overridden are specified in each section below.
mac.inference.Inference
interface serves model inferences based on submitted input some input. Its purpose is to serve inferences for any supported arbitrary model framework (e.g.scikit
,keras
etc.).classDiagram class Inference { <<Abstract>> +model Optional[Any] +expected_model_types()* Set +predict(input_data: InferenceData)* InferenceData -raise_error_if_model_is_not_assigned() None -raise_error_if_model_is_wrong_type() None }
mac.inference.creation.InferenceBuilder
builds a concreteInference
, i.e. instantiates anInference
object, loads the appropriate model and assigns the model to to the Inference object.classDiagram class InferenceBuilder { +create(config InferenceConfig) * Inference -inference()* Any }
mac.inference.Inference
mac.inference.Inference Objects
Object | Type | Description |
---|---|---|
model Optional[Any] | An optional list of models that match the supported frameworks from wallaroo.framework.Framework included in the arbitrary python script. Note that this is optional - no models are actually required. A BYOP can refer to a specific model(s) used, be used for data processing and reshaping for later pipeline steps, or other needs. |
mac.inference.Inference Methods
Method | Returns | Description |
---|---|---|
expected_model_types (Required) | Set | Returns a Set of models expected for the inference as defined by the developer. Typically this is a set of one. Wallaroo checks the expected model types to verify that the model submitted through the InferenceBuilder method matches what this Inference class expects. |
_predict (input_data: mac.types.InferenceData) (Required) | mac.types.InferenceData | The entry point for the Wallaroo inference with the following input and output parameters that are defined when the model is updated.
InferenceDataValidationError exception is raised when the input data does not match mac.types.InferenceData . |
raise_error_if_model_is_not_assigned | N/A | Error when expected_model_types is not set. |
raise_error_if_model_is_wrong_type | N/A | Error when the model does not match the expected_model_types . |
mac.inference.creation.InferenceBuilder
InferenceBuilder
builds a concrete Inference
, i.e. instantiates an Inference
object, loads the appropriate model and assigns the model to the Inference.
classDiagram class InferenceBuilder { +create(config InferenceConfig) * Inference -inference()* Any }
Each model that is included requires its own InferenceBuilder
. InferenceBuilder
loads one model, then submits it to the Inference
class when created. The Inference
class checks this class against its expected_model_types()
Set.
mac.inference.creation.InferenceBuilder Methods
Method | Returns | Description |
---|---|---|
create(config mac.config.inference.CustomInferenceConfig) (Required) | The custom Inference instance. | Creates an Inference subclass, then assigns a model and attributes. The CustomInferenceConfig is used to retrieve the config.model_path , which is a pathlib.Path object pointing to the folder where the model artifacts are saved. Every artifact loaded must be relative to config.model_path . This is set when the arbitrary python .zip file is uploaded and the environment for running it in Wallaroo is set. For example: loading the artifact vgg_clustering\feature_extractor.h5 would be set with config.model_path \ feature_extractor.h5 . The model loaded must match an existing module. For our example, this is from sklearn.cluster import KMeans , and this must match the Inference expected_model_types . |
inference | custom Inference instance. | Returns the instantiated custom Inference object created from the create method. |
VGG16 Model Training Steps
This process will train a dummy KMeans
model for clustering images using a pre-trained VGG16
model on cifar10
as a feature extractor. This model consists of the following elements:
- All elements are stored in the folder
models/vgg16_clustering
. This will be converted to the zip filemodel-auto-conversion-BYOP-vgg16-clustering.zip
. models/vgg16_clustering
will contain the following:- All necessary model artifacts
- One or multiple Python files implementing the classes
Inference
andInferenceBuilder
. The implemented classes can have any naming they desire as long as they inherit from the appropriate base classes. - a
requirements.txt
file with all necessary pip requirements to successfully run the inference
Import Libraries
The first step is to import the libraries we’ll be using. These are included by default in the Wallaroo instance’s JupyterHub service.
import numpy as np
import pandas as pd
import json
import os
import pickle
import pyarrow as pa
import tensorflow as tf
import warnings
warnings.filterwarnings('ignore')
from sklearn.cluster import KMeans
from tensorflow.keras.datasets import cifar10
from tensorflow.keras import Model
from tensorflow.keras.layers import Flatten
2023-07-07 16:16:26.511340: W tensorflow/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libcudart.so.11.0'; dlerror: libcudart.so.11.0: cannot open shared object file: No such file or directory
2023-07-07 16:16:26.511369: I tensorflow/stream_executor/cuda/cudart_stub.cc:29] Ignore above cudart dlerror if you do not have a GPU set up on your machine.
Variables
We’ll use these variables in later steps rather than hard code them in. In this case, the directory where we’ll store our artifacts.
model_directory = './models/vgg16_clustering'
Load Data Set
In this section, we will load our sample data and shape it.
# Load and preprocess the CIFAR-10 dataset
(X_train, y_train), (X_test, y_test) = cifar10.load_data()
# Normalize the pixel values to be between 0 and 1
X_train = X_train / 255.0
X_test = X_test / 255.0
X_train.shape
(50000, 32, 32, 3)
Train KMeans with VGG16 as feature extractor
Now we will train our model.
pretrained_model = tf.keras.applications.VGG16(include_top=False,
weights='imagenet',
input_shape=(32, 32, 3)
)
embedding_model = Model(inputs=pretrained_model.input,
outputs=Flatten()(pretrained_model.output))
2023-07-07 16:16:30.207936: W tensorflow/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libcuda.so.1'; dlerror: libcuda.so.1: cannot open shared object file: No such file or directory
2023-07-07 16:16:30.207966: W tensorflow/stream_executor/cuda/cuda_driver.cc:269] failed call to cuInit: UNKNOWN ERROR (303)
2023-07-07 16:16:30.207987: I tensorflow/stream_executor/cuda/cuda_diagnostics.cc:156] kernel driver does not appear to be running on this host (jupyter-john-2ehummel-40wallaroo-2eai): /proc/driver/nvidia/version does not exist
2023-07-07 16:16:30.208169: 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.
X_train_embeddings = embedding_model.predict(X_train[:100])
X_test_embeddings = embedding_model.predict(X_test[:100])
kmeans = KMeans(n_clusters=2, random_state=0).fit(X_train_embeddings)
Save Models
Let’s first create the directory where the model artifacts will be saved:
os.makedirs(model_directory, exist_ok=True)
And now save the two models:
with open(f'{model_directory}/kmeans.pkl', 'wb') as fp:
pickle.dump(kmeans, fp)
embedding_model.save(f'{model_directory}/feature_extractor.h5')
WARNING:tensorflow:Compiled the loaded model, but the compiled metrics have yet to be built. `model.compile_metrics` will be empty until you train or evaluate the model.
All needed model artifacts have been now saved under our model directory.
Sample Arbitrary Python Script
The following shows an example of extending the Inference and InferenceBuilder classes for our specific model. This script is located in our model directory under ./models/vgg16_clustering
.
"""This module features an example implementation of a custom Inference and its
corresponding InferenceBuilder."""
import pathlib
import pickle
from typing import Any, Set
import tensorflow as tf
from mac.config.inference import CustomInferenceConfig
from mac.inference import Inference
from mac.inference.creation import InferenceBuilder
from mac.types import InferenceData
from sklearn.cluster import KMeans
class ImageClustering(Inference):
"""Inference class for image clustering, that uses
a pre-trained VGG16 model on cifar10 as a feature extractor
and performs clustering on a trained KMeans model.
Attributes:
- feature_extractor: The embedding model we will use
as a feature extractor (i.e. a trained VGG16).
- expected_model_types: A set of model instance types that are expected by this inference.
- model: The model on which the inference is calculated.
"""
def __init__(self, feature_extractor: tf.keras.Model):
self.feature_extractor = feature_extractor
super().__init__()
@property
def expected_model_types(self) -> Set[Any]:
return {KMeans}
@Inference.model.setter # type: ignore
def model(self, model) -> None:
"""Sets the model on which the inference is calculated.
:param model: A model instance on which the inference is calculated.
:raises TypeError: If the model is not an instance of expected_model_types
(i.e. KMeans).
"""
self._raise_error_if_model_is_wrong_type(model) # this will make sure an error will be raised if the model is of wrong type
self._model = model
def _predict(self, input_data: InferenceData) -> InferenceData:
"""Calculates the inference on the given input data.
This is the core function that each subclass needs to implement
in order to calculate the inference.
:param input_data: The input data on which the inference is calculated.
It is of type InferenceData, meaning it comes as a dictionary of numpy
arrays.
:raises InferenceDataValidationError: If the input data is not valid.
Ideally, every subclass should raise this error if the input data is not valid.
:return: The output of the model, that is a dictionary of numpy arrays.
"""
# input_data maps to the input_schema we have defined
# with PyArrow, coming as a dictionary of numpy arrays
inputs = input_data["images"]
# Forward inputs to the models
embeddings = self.feature_extractor(inputs)
predictions = self.model.predict(embeddings.numpy())
# Return predictions as dictionary of numpy arrays
return {"predictions": predictions}
class ImageClusteringBuilder(InferenceBuilder):
"""InferenceBuilder subclass for ImageClustering, that loads
a pre-trained VGG16 model on cifar10 as a feature extractor
and a trained KMeans model, and creates an ImageClustering object."""
@property
def inference(self) -> ImageClustering:
return ImageClustering
def create(self, config: CustomInferenceConfig) -> ImageClustering:
"""Creates an Inference subclass and assigns a model and additionally
needed attributes to it.
:param config: Custom inference configuration. In particular, we're
interested in `config.model_path` that is a pathlib.Path object
pointing to the folder where the model artifacts are saved.
Every artifact we need to load from this folder has to be
relative to `config.model_path`.
:return: A custom Inference instance.
"""
feature_extractor = self._load_feature_extractor(
config.model_path / "feature_extractor.h5"
)
inference = self.inference(feature_extractor)
model = self._load_model(config.model_path / "kmeans.pkl")
inference.model = model
return inference
def _load_feature_extractor(
self, file_path: pathlib.Path
) -> tf.keras.Model:
return tf.keras.models.load_model(file_path)
def _load_model(self, file_path: pathlib.Path) -> KMeans:
with open(file_path.as_posix(), "rb") as fp:
model = pickle.load(fp)
return model
Create Requirements File
As a last step we need to create a requirements.txt
file and save it under our vgg_clustering/
. The file should contain all the necessary pip requirements needed to run the inference. It will have this data inside.
- IMPORTANT NOTE: Verify that the library versions match the required model versions and libraries for Wallaroo. Otherwise the deployed models
tensorflow==2.8.0
scikit-learn==1.2.2
Zip model folder
Assuming we have stored the following files inside out model directory models/vgg_clustering/
:
feature_extractor.h5
kmeans.pkl
custom_inference.py
requirements.txt
Now we will zip the file. This is performed with the zip
command and the -r
option to zip the contents of the entire directory.
zip -r model-auto-conversion-BYOP-vgg16-clustering.zip vgg16_clustering/
The arbitrary Python custom model can now be uploaded to the Wallaroo instance.
2.2.2 - Wallaroo SDK Upload Arbitrary Python Tutorial: Deploy VGG16 Model
This tutorial can be downloaded as part of the Wallaroo Tutorials repository.
Arbitrary Python Tutorial Deploy Model in Wallaroo Upload and Deploy
This tutorial demonstrates how to use arbitrary python as a ML Model in Wallaroo. Arbitrary Python allows organizations to use Python scripts that require specific libraries and artifacts as models in the Wallaroo engine. This allows for highly flexible use of ML models with supporting scripts.
Tutorial Goals
This tutorial is split into two parts:
- Wallaroo SDK Upload Arbitrary Python Tutorial: Generate Model: Train a dummy
KMeans
model for clustering images using a pre-trainedVGG16
model oncifar10
as a feature extractor. The Python entry points used for Wallaroo deployment will be added and described.- A copy of the arbitrary Python model
models/model-auto-conversion-BYOP-vgg16-clustering.zip
is included in this tutorial, so this step can be skipped.
- A copy of the arbitrary Python model
- Arbitrary Python Tutorial Deploy Model in Wallaroo Upload and Deploy: Deploys the
KMeans
model in an arbitrary Python package in Wallaroo, and perform sample inferences. The filemodels/model-auto-conversion-BYOP-vgg16-clustering.zip
is provided so users can go right to testing deployment.
Arbitrary Python models, also known as Bring Your Own Predict (BYOP) allow for custom model deployments with supporting scripts and artifacts. These are used with pre-trained models (PyTorch, Tensorflow, etc) along with whatever supporting artifacts they require. Supporting artifacts can include other Python modules, model files, etc. These are zipped with all scripts, artifacts, and a requirements.txt
file that indicates what other Python models need to be imported that are outside of the typical Wallaroo platform.
Contrast this with Wallaroo Python models - aka “Python steps”. These are standalone python scripts that use the python libraries natively supported by the Wallaroo platform. These are used for either simple model deployment (such as ARIMA Statsmodels), or data formatting such as the postprocessing steps. A Wallaroo Python model will be composed of one Python script that matches the Wallaroo requirements.
Arbitrary Python File Requirements
Arbitrary Python (BYOP) models are uploaded to Wallaroo via a ZIP file with the following components:
Artifact | Type | Description |
---|---|---|
Python scripts aka .py files with classes that extend mac.inference.Inference and mac.inference.creation.InferenceBuilder | Python Script | Extend the classes mac.inference.Inference and mac.inference.creation.InferenceBuilder . These are included with the Wallaroo SDK. Further details are in Arbitrary Python Script Requirements. Note that there is no specified naming requirements for the classes that extend mac.inference.Inference and mac.inference.creation.InferenceBuilder - any qualified class name is sufficient as long as these two classes are extended as defined below. |
requirements.txt | Python requirements file | This sets the Python libraries used for the arbitrary python model. These libraries should be targeted for Python 3.8 compliance. These requirements and the versions of libraries should be exactly the same between creating the model and deploying it in Wallaroo. This insures that the script and methods will function exactly the same as during the model creation process. |
Other artifacts | Files | Other models, files, and other artifacts used in support of this model. |
For example, the if the arbitrary python model will be known as vgg_clustering
, the contents may be in the following structure, with vgg_clustering
as the storage directory:
vgg_clustering\
feature_extractor.h5
kmeans.pkl
custom_inference.py
requirements.txt
Note the inclusion of the custom_inference.py
file. This file name is not required - any Python script or scripts that extend the classes listed above are sufficient. This Python script could have been named vgg_custom_model.py
or any other name as long as it includes the extension of the classes listed above.
The sample arbitrary python model file is created with the command zip -r vgg_clustering.zip vgg_clustering/
.
Wallaroo Arbitrary Python uses the Wallaroo SDK mac
module, included in the Wallaroo SDK 2023.2.1 and above. See the Wallaroo SDK Install Guides for instructions on installing the Wallaroo SDK.
Arbitrary Python Script Requirements
The entry point of the arbitrary python model is any python script that extends the following classes. These are included with the Wallaroo SDK. The required methods that must be overridden are specified in each section below.
mac.inference.Inference
interface serves model inferences based on submitted input some input. Its purpose is to serve inferences for any supported arbitrary model framework (e.g.scikit
,keras
etc.).classDiagram class Inference { <<Abstract>> +model Optional[Any] +expected_model_types()* Set +predict(input_data: InferenceData)* InferenceData -raise_error_if_model_is_not_assigned() None -raise_error_if_model_is_wrong_type() None }
mac.inference.creation.InferenceBuilder
builds a concreteInference
, i.e. instantiates anInference
object, loads the appropriate model and assigns the model to to the Inference object.classDiagram class InferenceBuilder { +create(config InferenceConfig) * Inference -inference()* Any }
mac.inference.Inference
mac.inference.Inference Objects
Object | Type | Description |
---|---|---|
model Optional[Any] | An optional list of models that match the supported frameworks from wallaroo.framework.Framework included in the arbitrary python script. Note that this is optional - no models are actually required. A BYOP can refer to a specific model(s) used, be used for data processing and reshaping for later pipeline steps, or other needs. |
mac.inference.Inference Methods
Method | Returns | Description |
---|---|---|
expected_model_types (Required) | Set | Returns a Set of models expected for the inference as defined by the developer. Typically this is a set of one. Wallaroo checks the expected model types to verify that the model submitted through the InferenceBuilder method matches what this Inference class expects. |
_predict (input_data: mac.types.InferenceData) (Required) | mac.types.InferenceData | The entry point for the Wallaroo inference with the following input and output parameters that are defined when the model is updated.
InferenceDataValidationError exception is raised when the input data does not match mac.types.InferenceData . |
raise_error_if_model_is_not_assigned | N/A | Error when expected_model_types is not set. |
raise_error_if_model_is_wrong_type | N/A | Error when the model does not match the expected_model_types . |
mac.inference.creation.InferenceBuilder
InferenceBuilder
builds a concrete Inference
, i.e. instantiates an Inference
object, loads the appropriate model and assigns the model to the Inference.
classDiagram class InferenceBuilder { +create(config InferenceConfig) * Inference -inference()* Any }
Each model that is included requires its own InferenceBuilder
. InferenceBuilder
loads one model, then submits it to the Inference
class when created. The Inference
class checks this class against its expected_model_types()
Set.
mac.inference.creation.InferenceBuilder Methods
Method | Returns | Description |
---|---|---|
create(config mac.config.inference.CustomInferenceConfig) (Required) | The custom Inference instance. | Creates an Inference subclass, then assigns a model and attributes. The CustomInferenceConfig is used to retrieve the config.model_path , which is a pathlib.Path object pointing to the folder where the model artifacts are saved. Every artifact loaded must be relative to config.model_path . This is set when the arbitrary python .zip file is uploaded and the environment for running it in Wallaroo is set. For example: loading the artifact vgg_clustering\feature_extractor.h5 would be set with config.model_path \ feature_extractor.h5 . The model loaded must match an existing module. For our example, this is from sklearn.cluster import KMeans , and this must match the Inference expected_model_types . |
inference | custom Inference instance. | Returns the instantiated custom Inference object created from the create method. |
Tutorial Steps
Import Libraries
The first step is to import the libraries we’ll be using. These are included by default in the Wallaroo instance’s JupyterHub service.
import numpy as np
import pandas as pd
import json
import os
import pickle
import pyarrow as pa
import tensorflow as tf
import wallaroo
from sklearn.cluster import KMeans
from tensorflow.keras.datasets import cifar10
from tensorflow.keras import Model
from tensorflow.keras.layers import Flatten
from wallaroo.pipeline import Pipeline
from wallaroo.deployment_config import DeploymentConfigBuilder
from wallaroo.framework import Framework
2023-07-07 16:18:13.974516: W tensorflow/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libcudart.so.11.0'; dlerror: libcudart.so.11.0: cannot open shared object file: No such file or directory
2023-07-07 16:18:13.974543: I tensorflow/stream_executor/cuda/cudart_stub.cc:29] Ignore above cudart dlerror if you do not have a GPU set up on your machine.
Open a Connection to Wallaroo
The next step is connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. If logging in externally, update the wallarooPrefix
and wallarooSuffix
variables with the proper DNS information. For more information on Wallaroo DNS settings, see the Wallaroo DNS Integration Guide.
wl = wallaroo.Client()
Set Variables and Helper Functions
We’ll set the name of our workspace, pipeline, models and files. Workspace names must be unique across the Wallaroo workspace. For this, we’ll add in a randomly generated 4 characters to the workspace name to prevent collisions with other users’ workspaces. If running this tutorial, we recommend hard coding the workspace name so it will function in the same workspace each time it’s run.
We’ll set up some helper functions that will either use existing workspaces and pipelines, or create them if they do not already exist.
import string
import random
# make a random 4 character suffix to prevent overwriting other user's workspaces
suffix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
workspace_name = f'vgg16-clustering-workspace{suffix}'
pipeline_name = f'vgg16-clustering-pipeline'
model_name = 'vgg16-clustering'
model_file_name = './models/model-auto-conversion-BYOP-vgg16-clustering.zip'
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
pipeline = wl.build_pipeline(name)
return pipeline
Create Workspace and Pipeline
We will now create the Wallaroo workspace to store our model and set it as the current workspace. Future commands will default to this workspace for pipeline creation, model uploads, etc. We’ll create our Wallaroo pipeline that is used to deploy our arbitrary Python model.
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
pipeline = get_pipeline(pipeline_name)
Upload Arbitrary Python Model
Arbitrary Python models are uploaded to Wallaroo through the Wallaroo Client upload_model
method.
Upload Arbitrary Python Model Parameters
The following parameters are required for Arbitrary Python models. Note that while some fields are considered as optional for the upload_model
method, they are required for proper uploading of a Arbitrary Python model to Wallaroo.
Parameter | Type | Description |
---|---|---|
name | string (Required) | The name of the model. Model names are unique per workspace. Models that are uploaded with the same name are assigned as a new version of the model. |
path | string (Required) | The path to the model file being uploaded. |
framework | string (Upload Method Optional, Arbitrary Python model Required) | Set as Framework.CUSTOM . |
input_schema | pyarrow.lib.Schema (Upload Method Optional, Arbitrary Python model Required) | The input schema in Apache Arrow schema format. |
output_schema | pyarrow.lib.Schema (Upload Method Optional, Arbitrary Python model Required) | The output schema in Apache Arrow schema format. |
convert_wait | bool (Upload Method Optional, Arbitrary Python model Optional) (Default: True) |
|
Once the upload process starts, the model is containerized by the Wallaroo instance. This process may take up to 10 minutes.
Upload Arbitrary Python Model Return
The following is returned with a successful model upload and conversion.
Field | Type | Description |
---|---|---|
name | string | The name of the model. |
version | string | The model version as a unique UUID. |
file_name | string | The file name of the model as stored in Wallaroo. |
image_path | string | The image used to deploy the model in the Wallaroo engine. |
last_update_time | DateTime | When the model was last updated. |
For our example, we’ll start with setting the input_schema
and output_schema
that is expected by our ImageClustering._predict()
method.
input_schema = pa.schema([
pa.field('images', pa.list_(
pa.list_(
pa.list_(
pa.int64(),
list_size=3
),
list_size=32
),
list_size=32
)),
])
output_schema = pa.schema([
pa.field('predictions', pa.int64()),
])
Upload Model
Now we’ll upload our model. The framework is Framework.CUSTOM
for arbitrary Python models, and we’ll specify the input and output schemas for the upload.
model = wl.upload_model(model_name,
model_file_name,
framework=Framework.CUSTOM,
input_schema=input_schema,
output_schema=output_schema,
convert_wait=True)
model
Waiting for model conversion... It may take up to 10.0min.
Model is Pending conversion..Converting..................Ready.
Name | vgg16-clustering |
Version | 86eaa743-f659-4eac-9544-23893ea0101c |
File Name | model-auto-conversion-BYOP-vgg16-clustering.zip |
SHA | 9701562daa747b15846ce6e5eb20ba5d8b6ac77c38b62e58298da56252aa493f |
Status | ready |
Image Path | proxy.replicated.com/proxy/wallaroo/ghcr.io/wallaroolabs/mlflow-deploy:v2023.3.0-main-3481 |
Updated At | 2023-07-Jul 16:20:20 |
Deploy Pipeline
The model is uploaded and ready for use. We’ll add it as a step in our pipeline, then deploy the pipeline. For this example we’re allocated 0.25 cpu and 4 Gi RAM to the pipeline through the pipeline’s deployment configuration.
pipeline.add_model_step(model)
name | vgg16-clustering-pipeline |
---|---|
created | 2023-06-28 16:37:14.436166+00:00 |
last_updated | 2023-07-07 02:09:17.095252+00:00 |
deployed | False |
tags | |
versions | 21fb5777-f32d-4b86-99c1-3b099f0f671d, 610afdf4-850d-48f6-aad9-3115f389ee78, 13ed3d22-a82b-4a45-9b1d-5d668e9b2452, 9049c924-146f-4f7a-9e16-0b2565491547 |
steps | vgg16-clustering |
deployment_config = DeploymentConfigBuilder() \
.cpus(0.25).memory('4Gi') \
.build()
pipeline.deploy(deployment_config=deployment_config)
pipeline.status()
Waiting for deployment - this will take up to 90s ....................... ok
{‘status’: ‘Running’,
‘details’: [],
’engines’: [{‘ip’: ‘10.244.17.211’,
’name’: ’engine-85bd45d44b-dstdp’,
‘status’: ‘Running’,
‘reason’: None,
‘details’: [],
‘pipeline_statuses’: {‘pipelines’: [{‘id’: ‘vgg16-clustering-pipeline’,
‘status’: ‘Running’}]},
‘model_statuses’: {‘models’: [{’name’: ‘vgg16-clustering’,
‘version’: ‘86eaa743-f659-4eac-9544-23893ea0101c’,
‘sha’: ‘9701562daa747b15846ce6e5eb20ba5d8b6ac77c38b62e58298da56252aa493f’,
‘status’: ‘Running’}]}}],
’engine_lbs’: [{‘ip’: ‘10.244.17.212’,
’name’: ’engine-lb-584f54c899-8mmpl’,
‘status’: ‘Running’,
‘reason’: None,
‘details’: []}],
‘sidekicks’: [{‘ip’: ‘10.244.17.210’,
’name’: ’engine-sidekick-vgg16-clustering-174-79f64f7cb4-25wqf’,
‘status’: ‘Running’,
‘reason’: None,
‘details’: [],
‘statuses’: ‘\n’}]}
Run inference
Everything is in place - we’ll now run a sample inference with some toy data. In this case we’re randomly generating some values in the data shape the model expects, then submitting an inference request through our deployed pipeline.
input_data = {
"images": [np.random.randint(0, 256, (32, 32, 3), dtype=np.uint8)] * 2,
}
dataframe = pd.DataFrame(input_data)
dataframe
images | |
---|---|
0 | [[[10, 214, 168], [50, 238, 47], [189, 15, 55]... |
1 | [[[10, 214, 168], [50, 238, 47], [189, 15, 55]... |
pipeline.infer(dataframe, timeout=10000)
time | in.images | out.predictions | check_failures | |
---|---|---|---|---|
0 | 2023-07-07 16:20:48.450 | [10, 214, 168, 50, 238, 47, 189, 15, 55, 218, ... | 1 | 0 |
1 | 2023-07-07 16:20:48.450 | [10, 214, 168, 50, 238, 47, 189, 15, 55, 218, ... | 1 | 0 |
Undeploy Pipelines
The inference is successful, so we will undeploy the pipeline and return the resources back to the cluster.
pipeline.undeploy()
Waiting for undeployment - this will take up to 45s ..................................... ok
name | vgg16-clustering-pipeline |
---|---|
created | 2023-07-07 16:20:23.354098+00:00 |
last_updated | 2023-07-07 16:20:23.354098+00:00 |
deployed | False |
tags | |
versions | 62f3b65c-91e7-4057-a645-6ff5e62e3b21 |
steps | vgg16-clustering |
2.3 - Wallaroo SDK Upload Tutorials: Hugging Face
The following tutorials cover how to upload sample Hugging Face models. The complete list of supported Hugging Face pipelines are as follows.
Parameter | Description |
---|---|
Web Site | https://huggingface.co/models |
Supported Libraries |
|
Frameworks | The following Hugging Face pipelines are supported by Wallaroo.
|
Runtime | Containerized aka tensorflow / mlflow |
Hugging Face Schemas
Input and output schemas for each Hugging Face pipeline are defined below. Note that adding additional inputs not specified below will raise errors, except for the following:
Framework.HUGGING-FACE-IMAGE-TO-TEXT
Framework.HUGGING-FACE-TEXT-CLASSIFICATION
Framework.HUGGING-FACE-SUMMARIZATION
Framework.HUGGING-FACE-TRANSLATION
Additional inputs added to these Hugging Face pipelines will be added as key/pair value arguments to the model’s generate method. If the argument is not required, then the model will default to the values coded in the original Hugging Face model’s source code.
See the Hugging Face Pipeline documentation for more details on each pipeline and framework.
Wallaroo Framework | Reference |
---|---|
Framework.HUGGING-FACE-FEATURE-EXTRACTION |
Schemas:
input_schema = pa.schema([
pa.field('inputs', pa.string())
])
output_schema = pa.schema([
pa.field('output', pa.list_(
pa.list_(
pa.float64(),
list_size=128
),
))
])
Wallaroo Framework | Reference |
---|---|
Framework.HUGGING-FACE-IMAGE-CLASSIFICATION |
Schemas:
input_schema = pa.schema([
pa.field('inputs', pa.list_(
pa.list_(
pa.list_(
pa.int64(),
list_size=3
),
list_size=100
),
list_size=100
)),
pa.field('top_k', pa.int64()),
])
output_schema = pa.schema([
pa.field('score', pa.list_(pa.float64(), list_size=2)),
pa.field('label', pa.list_(pa.string(), list_size=2)),
])
Wallaroo Framework | Reference |
---|---|
Framework.HUGGING-FACE-IMAGE-SEGMENTATION |
Schemas:
input_schema = pa.schema([
pa.field('inputs',
pa.list_(
pa.list_(
pa.list_(
pa.int64(),
list_size=3
),
list_size=100
),
list_size=100
)),
pa.field('threshold', pa.float64()),
pa.field('mask_threshold', pa.float64()),
pa.field('overlap_mask_area_threshold', pa.float64()),
])
output_schema = pa.schema([
pa.field('score', pa.list_(pa.float64())),
pa.field('label', pa.list_(pa.string())),
pa.field('mask',
pa.list_(
pa.list_(
pa.list_(
pa.int64(),
list_size=100
),
list_size=100
),
)),
])
Wallaroo Framework | Reference |
---|---|
Framework.HUGGING-FACE-IMAGE-TO-TEXT |
Any parameter that is not part of the required inputs
list will be forwarded to the model as a key/pair value to the underlying models generate
method. If the additional input is not supported by the model, an error will be returned.
Schemas:
input_schema = pa.schema([
pa.field('inputs', pa.list_( #required
pa.list_(
pa.list_(
pa.int64(),
list_size=3
),
list_size=100
),
list_size=100
)),
# pa.field('max_new_tokens', pa.int64()), # optional
])
output_schema = pa.schema([
pa.field('generated_text', pa.list_(pa.string())),
])
Wallaroo Framework | Reference |
---|---|
Framework.HUGGING-FACE-OBJECT-DETECTION |
Schemas:
input_schema = pa.schema([
pa.field('inputs',
pa.list_(
pa.list_(
pa.list_(
pa.int64(),
list_size=3
),
list_size=100
),
list_size=100
)),
pa.field('threshold', pa.float64()),
])
output_schema = pa.schema([
pa.field('score', pa.list_(pa.float64())),
pa.field('label', pa.list_(pa.string())),
pa.field('box',
pa.list_( # dynamic output, i.e. dynamic number of boxes per input image, each sublist contains the 4 box coordinates
pa.list_(
pa.int64(),
list_size=4
),
),
),
])
Wallaroo Framework | Reference |
---|---|
Framework.HUGGING-FACE-QUESTION-ANSWERING |
Schemas:
input_schema = pa.schema([
pa.field('question', pa.string()),
pa.field('context', pa.string()),
pa.field('top_k', pa.int64()),
pa.field('doc_stride', pa.int64()),
pa.field('max_answer_len', pa.int64()),
pa.field('max_seq_len', pa.int64()),
pa.field('max_question_len', pa.int64()),
pa.field('handle_impossible_answer', pa.bool_()),
pa.field('align_to_words', pa.bool_()),
])
output_schema = pa.schema([
pa.field('score', pa.float64()),
pa.field('start', pa.int64()),
pa.field('end', pa.int64()),
pa.field('answer', pa.string()),
])
Wallaroo Framework | Reference |
---|---|
Framework.HUGGING-FACE-STABLE-DIFFUSION-TEXT-2-IMG |
Schemas:
input_schema = pa.schema([
pa.field('prompt', pa.string()),
pa.field('height', pa.int64()),
pa.field('width', pa.int64()),
pa.field('num_inference_steps', pa.int64()), # optional
pa.field('guidance_scale', pa.float64()), # optional
pa.field('negative_prompt', pa.string()), # optional
pa.field('num_images_per_prompt', pa.string()), # optional
pa.field('eta', pa.float64()) # optional
])
output_schema = pa.schema([
pa.field('images', pa.list_(
pa.list_(
pa.list_(
pa.int64(),
list_size=3
),
list_size=128
),
list_size=128
)),
])
Wallaroo Framework | Reference |
---|---|
Framework.HUGGING-FACE-SUMMARIZATION |
Any parameter that is not part of the required inputs
list will be forwarded to the model as a key/pair value to the underlying models generate
method. If the additional input is not supported by the model, an error will be returned.
Schemas:
input_schema = pa.schema([
pa.field('inputs', pa.string()),
pa.field('return_text', pa.bool_()),
pa.field('return_tensors', pa.bool_()),
pa.field('clean_up_tokenization_spaces', pa.bool_()),
# pa.field('extra_field', pa.int64()), # every extra field you specify will be forwarded as a key/value pair
])
output_schema = pa.schema([
pa.field('summary_text', pa.string()),
])
Wallaroo Framework | Reference |
---|---|
Framework.HUGGING-FACE-TEXT-CLASSIFICATION |
Schemas
input_schema = pa.schema([
pa.field('inputs', pa.string()), # required
pa.field('top_k', pa.int64()), # optional
pa.field('function_to_apply', pa.string()), # optional
])
output_schema = pa.schema([
pa.field('label', pa.list_(pa.string(), list_size=2)), # list with a number of items same as top_k, list_size can be skipped but may lead in worse performance
pa.field('score', pa.list_(pa.float64(), list_size=2)), # list with a number of items same as top_k, list_size can be skipped but may lead in worse performance
])
Wallaroo Framework | Reference |
---|---|
Framework.HUGGING-FACE-TRANSLATION |
Any parameter that is not part of the required inputs
list will be forwarded to the model as a key/pair value to the underlying models generate
method. If the additional input is not supported by the model, an error will be returned.
Schemas:
input_schema = pa.schema([
pa.field('inputs', pa.string()), # required
pa.field('return_tensors', pa.bool_()), # optional
pa.field('return_text', pa.bool_()), # optional
pa.field('clean_up_tokenization_spaces', pa.bool_()), # optional
pa.field('src_lang', pa.string()), # optional
pa.field('tgt_lang', pa.string()), # optional
# pa.field('extra_field', pa.int64()), # every extra field you specify will be forwarded as a key/value pair
])
output_schema = pa.schema([
pa.field('translation_text', pa.string()),
])
Wallaroo Framework | Reference |
---|---|
Framework.HUGGING-FACE-ZERO-SHOT-CLASSIFICATION |
Schemas:
input_schema = pa.schema([
pa.field('inputs', pa.string()), # required
pa.field('candidate_labels', pa.list_(pa.string(), list_size=2)), # required
pa.field('hypothesis_template', pa.string()), # optional
pa.field('multi_label', pa.bool_()), # optional
])
output_schema = pa.schema([
pa.field('sequence', pa.string()),
pa.field('scores', pa.list_(pa.float64(), list_size=2)), # same as number of candidate labels, list_size can be skipped by may result in slightly worse performance
pa.field('labels', pa.list_(pa.string(), list_size=2)), # same as number of candidate labels, list_size can be skipped by may result in slightly worse performance
])
Wallaroo Framework | Reference |
---|---|
Framework.HUGGING-FACE-ZERO-SHOT-IMAGE-CLASSIFICATION |
Schemas:
input_schema = pa.schema([
pa.field('inputs', # required
pa.list_(
pa.list_(
pa.list_(
pa.int64(),
list_size=3
),
list_size=100
),
list_size=100
)),
pa.field('candidate_labels', pa.list_(pa.string(), list_size=2)), # required
pa.field('hypothesis_template', pa.string()), # optional
])
output_schema = pa.schema([
pa.field('score', pa.list_(pa.float64(), list_size=2)), # same as number of candidate labels
pa.field('label', pa.list_(pa.string(), list_size=2)), # same as number of candidate labels
])
Wallaroo Framework | Reference |
---|---|
Framework.HUGGING-FACE-ZERO-SHOT-OBJECT-DETECTION |
Schemas:
input_schema = pa.schema([
pa.field('images',
pa.list_(
pa.list_(
pa.list_(
pa.int64(),
list_size=3
),
list_size=640
),
list_size=480
)),
pa.field('candidate_labels', pa.list_(pa.string(), list_size=3)),
pa.field('threshold', pa.float64()),
# pa.field('top_k', pa.int64()), # we want the model to return exactly the number of predictions, we shouldn't specify this
])
output_schema = pa.schema([
pa.field('score', pa.list_(pa.float64())), # variable output, depending on detected objects
pa.field('label', pa.list_(pa.string())), # variable output, depending on detected objects
pa.field('box',
pa.list_( # dynamic output, i.e. dynamic number of boxes per input image, each sublist contains the 4 box coordinates
pa.list_(
pa.int64(),
list_size=4
),
),
),
])
Wallaroo Framework | Reference |
---|---|
Framework.HUGGING-FACE-SENTIMENT-ANALYSIS | Hugging Face Sentiment Analysis |
Wallaroo Framework | Reference |
---|---|
Framework.HUGGING-FACE-TEXT-GENERATION |
Any parameter that is not part of the required inputs
list will be forwarded to the model as a key/pair value to the underlying models generate
method. If the additional input is not supported by the model, an error will be returned.
input_schema = pa.schema([
pa.field('inputs', pa.string()),
pa.field('return_tensors', pa.bool_()), # optional
pa.field('return_text', pa.bool_()), # optional
pa.field('return_full_text', pa.bool_()), # optional
pa.field('clean_up_tokenization_spaces', pa.bool_()), # optional
pa.field('prefix', pa.string()), # optional
pa.field('handle_long_generation', pa.string()), # optional
# pa.field('extra_field', pa.int64()), # every extra field you specify will be forwarded as a key/value pair
])
output_schema = pa.schema([
pa.field('generated_text', pa.list_(pa.string(), list_size=1))
])
2.3.1 - Wallaroo API Upload Tutorial: Hugging Face Zero Shot Classification
The Wallaroo 101 tutorial can be downloaded as part of the Wallaroo Tutorials repository.
Wallaroo Model Upload via MLops API: Hugging Face Zero Shot Classification
The following tutorial demonstrates how to upload a Hugging Face Zero Shot model to a Wallaroo instance.
Tutorial Goals
Demonstrate the following:
- Upload a Hugging Face Zero Shot Model to a Wallaroo instance.
- Create a pipeline and add the model as a pipeline step.
- Perform a sample inference.
Prerequisites
- A Wallaroo version 2023.2.1 or above instance
References
- Wallaroo MLOps API Essentials Guide: Model Upload and Registrations
- Wallaroo API Connection Guide
- DNS Integration Guide
Tutorial Steps
Import Libraries
import json
import os
import requests
import base64
import wallaroo
from wallaroo.pipeline import Pipeline
from wallaroo.deployment_config import DeploymentConfigBuilder
from wallaroo.framework import Framework
import pyarrow as pa
import numpy as np
import pandas as pd
Connect to Wallaroo
To perform the various Wallaroo MLOps API requests, we will use the Wallaroo SDK to generate the necessary tokens. For details on other methods of requesting and using authentication tokens with the Wallaroo MLOps API, see the Wallaroo API Connection Guide.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. For more information on Wallaroo Client settings, see the Client Connection guide.
wl = wallaroo.Client()
Variables
The following variables will be set for the rest of the tutorial to set the following:
- Wallaroo Workspace
- Wallaroo Pipeline
- Wallaroo Model name and path
- Wallaroo Model Framework
- The DNS prefix and suffix for the Wallaroo instance.
To allow this tutorial to be run multiple times or by multiple users in the same Wallaroo instance, a random 4 character prefix will be added to the workspace, pipeline, and model.
Verify that the DNS prefix and suffix match the Wallaroo instance used for this tutorial. See the DNS Integration Guide for more details.
import string
import random
# make a random 4 character suffix to prevent overwriting other user's workspaces
suffix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
workspace_name = f'hugging-face-zero-shot-api{suffix}'
pipeline_name = f'hugging-face-zero-shot'
model_name = f'zero-shot-classification'
model_file_name = "./models/model-auto-conversion_hugging-face_dummy-pipelines_zero-shot-classification-pipeline.zip"
framework = "hugging-face-zero-shot-classification"
wallarooPrefix = "YOUR PREFIX."
wallarooPrefix = "YOUR SUFFIX"
wallarooPrefix = "doc-test."
wallarooSuffix = "wallarooexample.ai"
APIURL=f"https://{wallarooPrefix}api.{wallarooSuffix}"
APIURL
'https://doc-test.api.wallarooexample.ai'
Create the Workspace
In a production environment, the Wallaroo workspace that contains the pipeline and models would be created and deployed. We will quickly recreate those steps using the MLOps API.
Workspaces are created through the MLOps API with the /v1/api/workspaces/create
command. This requires the workspace name be provided, and that the workspace not already exist in the Wallaroo instance.
Reference: MLOps API Create Workspace
# Retrieve the token
headers = wl.auth.auth_header()
# set Content-Type type
headers['Content-Type']='application/json'
# Create workspace
apiRequest = f"{APIURL}/v1/api/workspaces/create"
data = {
"workspace_name": workspace_name
}
response = requests.post(apiRequest, json=data, headers=headers, verify=True).json()
display(response)
# Stored for future examples
workspaceId = response['workspace_id']
{'workspace_id': 9}
Upload the Model
- Endpoint:
/v1/api/models/upload_and_convert
- Headers:
- Content-Type:
multipart/form-data
- Content-Type:
- Parameters
- name (String Required): The model name.
- visibility (String Required): Either
public
orprivate
. - workspace_id (String Required): The numerical ID of the workspace to upload the model to.
- conversion (String Required): The conversion parameters that include the following:
- framework (String Required): The framework of the model being uploaded. See the list of supported models for more details.
- python_version (String Required): The version of Python required for model.
- requirements (String Required): Required libraries. Can be
[]
if the requirements are default Wallaroo JupyterHub libraries. - input_schema (String Optional): The input schema from the Apache Arrow
pyarrow.lib.Schema
format, encoded withbase64.b64encode
. Only required for non-native runtime models. - output_schema (String Optional): The output schema from the Apache Arrow
pyarrow.lib.Schema
format, encoded withbase64.b64encode
. Only required for non-native runtime models.
Set the Schemas
The input and output schemas will be defined according to the Wallaroo Hugging Face schema requirements. The inputs are then base64 encoded for attachment in the API request.
input_schema = pa.schema([
pa.field('inputs', pa.string()), # required
pa.field('candidate_labels', pa.list_(pa.string(), list_size=2)), # required
pa.field('hypothesis_template', pa.string()), # optional
pa.field('multi_label', pa.bool_()), # optional
])
output_schema = pa.schema([
pa.field('sequence', pa.string()),
pa.field('scores', pa.list_(pa.float64(), list_size=2)), # same as number of candidate labels, list_size can be skipped by may result in slightly worse performance
pa.field('labels', pa.list_(pa.string(), list_size=2)), # same as number of candidate labels, list_size can be skipped by may result in slightly worse performance
])
encoded_input_schema = base64.b64encode(
bytes(input_schema.serialize())
).decode("utf8")
encoded_output_schema = base64.b64encode(
bytes(output_schema.serialize())
).decode("utf8")
Build the Request
We will now build the request to include the required data. We will be using the workspaceId
returned when we created our workspace in a previous step, specifying the input and output schemas, and the framework.
metadata = {
"name": model_name,
"visibility": "private",
"workspace_id": workspaceId,
"conversion": {
"framework": framework,
"python_version": "3.8",
"requirements": []
},
"input_schema": encoded_input_schema,
"output_schema": encoded_output_schema,
}
Upload Model API Request
Now we will make our upload and convert request. The model is is stored for the next set of steps.
headers = wl.auth.auth_header()
files = {
'metadata': (None, json.dumps(metadata), "application/json"),
'file': (model_name, open(model_file_name,'rb'),'application/octet-stream')
}
response = requests.post(f'{APIURL}/v1/api/models/upload_and_convert',
headers=headers,
files=files)
print(response.json())
{'insert_models': {'returning': [{'models': [{'id': 9}]}]}}
# Get the model details
# Retrieve the token
headers = wl.auth.auth_header()
# set Content-Type type
headers['Content-Type']='application/json'
apiRequest = f"{APIURL}/v1/api/models/list_versions"
data = {
"model_id": model_name,
"models_pk_id" : modelId
}
status = None
while status != 'ready':
response = requests.post(apiRequest, json=data, headers=headers, verify=True).json()
# verify we have the right version
display(model)
model = next(model for model in response if model["id"] == modelId)
display(model)
status = model['status']
{'sha': '3dcc14dd925489d4f0a3960e90a7ab5917ab685ce955beca8924aa7bb9a69398',
'models_pk_id': 7,
'model_version': '284a2e7c-679a-40cc-9121-2747b81a0228',
'owner_id': '""',
'model_id': 'zero-shot-classification',
'id': 7,
'file_name': 'zero-shot-classification',
'image_path': 'proxy.replicated.com/proxy/wallaroo/ghcr.io/wallaroolabs/mlflow-deploy:v2023.3.0-main-3509',
'status': 'ready'}
{‘sha’: ‘3dcc14dd925489d4f0a3960e90a7ab5917ab685ce955beca8924aa7bb9a69398’,
‘models_pk_id’: 7,
‘model_version’: ‘284a2e7c-679a-40cc-9121-2747b81a0228’,
‘owner_id’: ‘""’,
‘model_id’: ‘zero-shot-classification’,
‘id’: 7,
‘file_name’: ‘zero-shot-classification’,
‘image_path’: ‘proxy.replicated.com/proxy/wallaroo/ghcr.io/wallaroolabs/mlflow-deploy:v2023.3.0-main-3509’,
‘status’: ‘ready’}
Model Upload Complete
With that, the model upload is complete and can be deployed into a Wallaroo pipeline.
2.3.2 - Wallaroo SDK Upload Tutorial: Hugging Face Zero Shot Classification
The Wallaroo 101 tutorial can be downloaded as part of the Wallaroo Tutorials repository.
Wallaroo Model Upload via the Wallaroo SDK: Hugging Face Zero Shot Classification
The following tutorial demonstrates how to upload a Hugging Face Zero Shot model to a Wallaroo instance.
Tutorial Goals
Demonstrate the following:
- Upload a Hugging Face Zero Shot Model to a Wallaroo instance.
- Create a pipeline and add the model as a pipeline step.
- Perform a sample inference.
Prerequisites
- A Wallaroo version 2023.2.1 or above instance.
References
- Wallaroo MLOps API Essentials Guide: Model Upload and Registrations
- Wallaroo API Connection Guide
- DNS Integration Guide
Tutorial Steps
Import Libraries
The first step is to import the libraries we’ll be using. These are included by default in the Wallaroo instance’s JupyterHub service.
import json
import os
import wallaroo
from wallaroo.pipeline import Pipeline
from wallaroo.deployment_config import DeploymentConfigBuilder
from wallaroo.framework import Framework
from wallaroo.object import EntityNotFoundError
import os
os.environ["MODELS_ENABLED"] = "true"
import pyarrow as pa
import numpy as np
import pandas as pd
Open a Connection to Wallaroo
The next step is connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. If logging in externally, update the wallarooPrefix
and wallarooSuffix
variables with the proper DNS information. For more information on Wallaroo DNS settings, see the Wallaroo DNS Integration Guide.
wl = wallaroo.Client()
Set Variables and Helper Functions
We’ll set the name of our workspace, pipeline, models and files. Workspace names must be unique across the Wallaroo workspace. For this, we’ll add in a randomly generated 4 characters to the workspace name to prevent collisions with other users’ workspaces. If running this tutorial, we recommend hard coding the workspace name so it will function in the same workspace each time it’s run.
We’ll set up some helper functions that will either use existing workspaces and pipelines, or create them if they do not already exist.
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
pipeline = wl.build_pipeline(name)
return pipeline
import string
import random
# make a random 4 character suffix to prevent overwriting other user's workspaces
suffix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
workspace_name = f'hf-zero-shot-classification{suffix}'
pipeline_name = f'hf-zero-shot-classification'
model_name = 'hf-zero-shot-classification'
model_file_name = './models/model-auto-conversion_hugging-face_dummy-pipelines_zero-shot-classification-pipeline.zip'
Create Workspace and Pipeline
We will now create the Wallaroo workspace to store our model and set it as the current workspace. Future commands will default to this workspace for pipeline creation, model uploads, etc. We’ll create our Wallaroo pipeline to deploy our model.
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
pipeline = get_pipeline(pipeline_name)
Configure Data Schemas
The following parameters are required for Hugging Face models. Note that while some fields are considered as optional for the upload_model
method, they are required for proper uploading of a Hugging Face model to Wallaroo.
Parameter | Type | Description |
---|---|---|
name | string (Required) | The name of the model. Model names are unique per workspace. Models that are uploaded with the same name are assigned as a new version of the model. |
path | string (Required) | The path to the model file being uploaded. |
framework | string (Upload Method Optional, Hugging Face model Required) | Set as the framework. |
input_schema | pyarrow.lib.Schema (Upload Method Optional, Hugging Face model Required) | The input schema in Apache Arrow schema format. |
output_schema | pyarrow.lib.Schema (Upload Method Optional, Hugging Face model Required) | The output schema in Apache Arrow schema format. |
convert_wait | bool (Upload Method Optional, Hugging Face model Optional) (Default: True) |
|
The input and output schemas will be configured for the data inputs and outputs. More information on the available inputs under the official 🤗 Hugging Face source code.
input_schema = pa.schema([
pa.field('inputs', pa.string()), # required
pa.field('candidate_labels', pa.list_(pa.string(), list_size=2)), # required
pa.field('hypothesis_template', pa.string()), # optional
pa.field('multi_label', pa.bool_()), # optional
])
output_schema = pa.schema([
pa.field('sequence', pa.string()),
pa.field('scores', pa.list_(pa.float64(), list_size=2)), # same as number of candidate labels, list_size can be skipped by may result in slightly worse performance
pa.field('labels', pa.list_(pa.string(), list_size=2)), # same as number of candidate labels, list_size can be skipped by may result in slightly worse performance
])
Upload Model
The model will be uploaded with the framework set as Framework.HUGGING_FACE_ZERO_SHOT_CLASSIFICATION
.
framework=Framework.HUGGING_FACE_ZERO_SHOT_CLASSIFICATION
model = wl.upload_model(model_name,
model_file_name,
framework=framework,
input_schema=input_schema,
output_schema=output_schema,
convert_wait=True)
model
Waiting for model conversion... It may take up to 10.0min.
Model is Pending conversion..Converting.............Ready.
Name | hf-zero-shot-classification |
Version | 56bd73be-74d1-46c9-81ad-f2aa7b0c3466 |
File Name | model-auto-conversion_hugging-face_dummy-pipelines_zero-shot-classification-pipeline.zip |
SHA | 3dcc14dd925489d4f0a3960e90a7ab5917ab685ce955beca8924aa7bb9a69398 |
Status | ready |
Image Path | proxy.replicated.com/proxy/wallaroo/ghcr.io/wallaroolabs/mlflow-deploy:v2023.3.0-main-3509 |
Updated At | 2023-13-Jul 16:24:31 |
model.config().runtime()
'mlflow'
Deploy Pipeline
The model is uploaded and ready for use. We’ll add it as a step in our pipeline, then deploy the pipeline. For this example we’re allocated 0.25 cpu and 4 Gi RAM to the pipeline through the pipeline’s deployment configuration.
deployment_config = DeploymentConfigBuilder() \
.cpus(0.25).memory('1Gi') \
.build()
pipeline = get_pipeline(pipeline_name)
# clear the pipeline if used previously
pipeline.undeploy()
pipeline.clear()
pipeline.add_model_step(model)
pipeline.deploy(deployment_config=deployment_config)
pipeline.status()
{'status': 'Running',
'details': [],
'engines': [{'ip': '10.244.9.82',
'name': 'engine-7c9767fbcc-t9f9d',
'status': 'Running',
'reason': None,
'details': [],
'pipeline_statuses': {'pipelines': [{'id': 'hf-zero-shot-classification',
'status': 'Running'}]},
'model_statuses': {'models': [{'name': 'hf-zero-shot-classification',
'version': '56bd73be-74d1-46c9-81ad-f2aa7b0c3466',
'sha': '3dcc14dd925489d4f0a3960e90a7ab5917ab685ce955beca8924aa7bb9a69398',
'status': 'Running'}]}}],
'engine_lbs': [{'ip': '10.244.9.81',
'name': 'engine-lb-584f54c899-kz67n',
'status': 'Running',
'reason': None,
'details': []}],
'sidekicks': [{'ip': '10.244.9.79',
'name': 'engine-sidekick-hf-zero-shot-classification-251-5874655d474f5zt',
'status': 'Running',
'reason': None,
'details': [],
'statuses': '\n'}]}
Run Inference
A sample inference will be run. First the pandas DataFrame used for the inference is created, then the inference run through the pipeline’s infer
method.
input_data = {
"inputs": ["this is a test", "this is another test"], # required
"candidate_labels": [["english", "german"], ["english", "german"]], # optional: using the defaults, similar to not passing this parameter
"hypothesis_template": ["This example is {}.", "This example is {}."], # optional: using the defaults, similar to not passing this parameter
"multi_label": [False, False], # optional: using the defaults, similar to not passing this parameter
}
dataframe = pd.DataFrame(input_data)
dataframe
inputs | candidate_labels | hypothesis_template | multi_label | |
---|---|---|---|---|
0 | this is a test | [english, german] | This example is {}. | False |
1 | this is another test | [english, german] | This example is {}. | False |
%time
pipeline.infer(dataframe)
CPU times: user 3 µs, sys: 0 ns, total: 3 µs
Wall time: 3.81 µs
time | in.candidate_labels | in.hypothesis_template | in.inputs | in.multi_label | out.labels | out.scores | out.sequence | check_failures | |
---|---|---|---|---|---|---|---|---|---|
0 | 2023-07-13 16:25:46.100 | [english, german] | This example is {}. | this is a test | False | [english, german] | [0.5040545463562012, 0.49594542384147644] | this is a test | 0 |
1 | 2023-07-13 16:25:46.100 | [english, german] | This example is {}. | this is another test | False | [english, german] | [0.5037839412689209, 0.4962160289287567] | this is another test | 0 |
Undeploy Pipelines
With the tutorial complete, the pipeline is undeployed to return the resources back to the cluster.
pipeline.undeploy()
name | hf-zero-shot-classification |
---|---|
created | 2023-07-13 16:25:22.793477+00:00 |
last_updated | 2023-07-13 16:25:22.793477+00:00 |
deployed | False |
tags | |
versions | dedeaa61-8c3f-4a7d-8ac4-642b52d7afde |
steps | hf-zero-shot-classification |
2.4 - Wallaroo SDK Upload Tutorials: Python Step Shape
The following tutorials cover how to upload sample Python Step Shape models into a Wallaroo instance.
Python scripts are uploaded to Wallaroo and and treated like an ML Models in Pipeline steps. These will be referred to as Python steps.
Python steps can include:
- Preprocessing steps to prepare the data received to be handed to ML Model deployed as another Pipeline step.
- Postprocessing steps to take data output by a ML Model as part of a Pipeline step, and prepare the data to be received by some other data store or entity.
- A model contained within a Python script.
In all of these, the requirements for uploading a Python script as a ML Model in Wallaroo are the same.
Parameter | Description |
---|---|
Web Site | https://www.python.org/ |
Supported Libraries | python==3.8 |
Framework | Framework.PYTHON aka python |
Runtime | Native aka python |
Python models uploaded to Wallaroo are executed as a native runtime.
Note that Python models - aka “Python steps” - are standalone python scripts that use the python libraries natively supported by the Wallaroo platform. These are used for either simple model deployment (such as ARIMA Statsmodels), or data formatting such as the postprocessing steps. A Wallaroo Python model will be composed of one Python script that matches the Wallaroo requirements.
This is contrasted with Arbitrary Python models, also known as Bring Your Own Predict (BYOP) allow for custom model deployments with supporting scripts and artifacts. These are used with pre-trained models (PyTorch, Tensorflow, etc) along with whatever supporting artifacts they require. Supporting artifacts can include other Python modules, model files, etc. These are zipped with all scripts, artifacts, and a requirements.txt
file that indicates what other Python models need to be imported that are outside of the typical Wallaroo platform.
Python Models Requirements
Python models uploaded to Wallaroo are Python scripts that must include the wallaroo_json
method as the entry point for the Wallaroo engine to use it as a Pipeline step.
This method receives the results of the previous Pipeline step, and its return value will be used in the next Pipeline step.
If the Python model is the first step in the pipeline, then it will be receiving the inference request data (for example: a preprocessing step). If it is the last step in the pipeline, then it will be the data returned from the inference request.
In the example below, the Python model is used as a post processing step for another ML model. The Python model expects to receive data from a ML Model who’s output is a DataFrame with the column dense_2
. It then extracts the values of that column as a list, selects the first element, and returns a DataFrame with that element as the value of the column output
.
def wallaroo_json(data: pd.DataFrame):
print(data)
return [{"output": [data["dense_2"].to_list()[0][0]]}]
In line with other Wallaroo inference results, the outputs of a Python step that returns a pandas DataFrame or Arrow Table will be listed in the out.
metadata, with all inference outputs listed as out.{variable 1}
, out.{variable 2}
, etc. In the example above, this results the output
field as the out.output
field in the Wallaroo inference result.
 | time | in.tensor | out.output | check_failures |
---|---|---|---|---|
0 | 2023-06-20 20:23:28.395 | [0.6878518042, 0.1760734021, -0.869514083, 0.3.. | [12.886651039123535] | 0 |
2.4.1 - Python Model Shape Upload to Wallaroo with the Wallaroo SDK
This tutorial can be downloaded as part of the Wallaroo Tutorials repository.
Python Model Upload to Wallaroo
Python scripts can be deployed to Wallaroo as Python Models. These are treated like other models, and are used for:
- ML Models: Models written entirely in Python script.
- Data Formatting: Typically preprocess or post process modules that shape incoming data into what a ML model expects, or receives data output by a ML model and changes the data for other processes to accept.
Models are added to Wallaroo pipelines as pipeline steps, with the data from the previous step submitted to the next one. Python steps require the entry method wallaroo_json
. These methods should be structured to receive and send pandas DataFrames as the inputs and outputs.
This allows inference requests to a Wallaroo pipeline to receive pandas DataFrames or Apache Arrow tables, and return the same for consistent results.
This tutorial will:
- Create a Wallaroo workspace and pipeline.
- Upload the sample Python model and ONNX model.
- Demonstrate the outputs of the ONNX model to an inference request.
- Demonstrate the functionality of the Python model in reshaping data after an inference request.
- Use both the ONNX model and the Python model together as pipeline steps to perform an inference request and export the data for use.
Prerequisites
- A Wallaroo version 2023.2.1 or above instance.
References
Tutorial Steps
Import Libraries
We’ll start with importing the libraries we need for the tutorial. The main libraries used are:
- Wallaroo: To connect with the Wallaroo instance and perform the MLOps commands.
pyarrow
: Used for formatting the data.pandas
: Used for pandas DataFrame tables.
import wallaroo
from wallaroo.object import EntityNotFoundError
from wallaroo.framework import Framework
from wallaroo.deployment_config import DeploymentConfigBuilder
import pandas as pd
#import os
# import json
import pyarrow as pa
Connect to the Wallaroo Instance through the User Interface
The next step is to connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. For more information on Wallaroo Client settings, see the Client Connection guide.
# Login through local Wallaroo instance
wl = wallaroo.Client()
Set Variables and Helper Functions
We’ll set the name of our workspace, pipeline, models and files. Workspace names must be unique across the Wallaroo workspace. For this, we’ll add in a randomly generated 4 characters to the workspace name to prevent collisions with other users’ workspaces. If running this tutorial, we recommend hard coding the workspace name so it will function in the same workspace each time it’s run.
We’ll set up some helper functions that will either use existing workspaces and pipelines, or create them if they do not already exist.
import string
import random
# make a random 4 character suffix to prevent overwriting other user's workspaces
suffix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
workspace_name = f'python-demo{suffix}'
pipeline_name = f'python-step-demo-pipeline'
onnx_model_name = 'house-price-sample'
onnx_model_file_name = './models/house_price_keras.onnx'
python_model_name = 'python-step'
python_model_file_name = './models/step.py'
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
pipeline = wl.build_pipeline(name)
return pipeline
Create a New Workspace
For our tutorial, we’ll create the workspace, set it as the current workspace, then the pipeline we’ll add our models to.
Create New Workspace References
- Wallaroo SDK Essentials Guide: Workspace Management
- Wallaroo SDK Essentials Guide: Pipeline Management
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
pipeline = get_pipeline(pipeline_name)
pipeline
name | python-step-demo-pipeline |
---|---|
created | 2023-07-13 17:24:56.476527+00:00 |
last_updated | 2023-07-13 17:27:07.599003+00:00 |
deployed | False |
tags | |
versions | 58a293f0-30ab-43f4-a22a-be2d98ae6850, bbc3f22f-a2de-4f33-bc7a-0470db060b0e, 0d5be505-57dd-4098-ba81-896083633364, dbd52766-9448-4f1c-938f-2e38e7b7ce1c |
steps | house-price-sample |
Model Descriptions
We have two models we’ll be using.
./models/house_price_keras.onnx
: A ML model trained to forecast hour prices based on inputs. This forecast is stored in the columndense_2
../models/step.py
: A Python script that accepts the data from the house price model, and reformats the output. We’ll be using it as a post-processing step.
For the Python step, it contains the method wallaroo_json
as the entry point used by Wallaroo when deployed as a pipeline step. Our sample script has the following:
# take a dataframe output of the house price model, and reformat the `dense_2`
# column as `output`
def wallaroo_json(data: pd.DataFrame):
print(data)
return [{"output": [data["dense_2"].to_list()[0][0]]}]
As seen from the description, all those function will do it take the DataFrame output of the house price model, and output a DataFrame replacing the first element in the list from column dense_2
with output
.
Upload Models
Both of these models will be uploaded to our current workspace using the method upload_model(name, path, framework).configure(framework, input_schema, output_schema)
.
- For
./models/house_price_keras.onnx
, we will specify it asFramework.ONNX
. We do not need to specify the input and output schemas. - For
./models/step.py
, we will set the input and output schemas in the requiredpyarrow.lib.Schema
format.
Upload Model References
- Wallaroo SDK Essentials Guide: Model Uploads and Registrations: ONNX
- Wallaroo SDK Essentials Guide: Model Uploads and Registrations: Python Models
house_price_model = (wl.upload_model(onnx_model_name,
onnx_model_file_name,
framework=Framework.ONNX)
.configure('onnx')
)
input_schema = pa.schema([
pa.field('dense_2', pa.list_(pa.float64()))
])
output_schema = pa.schema([
pa.field('output', pa.list_(pa.float64()))
])
step = (wl.upload_model(python_model_name,
python_model_file_name,
framework=Framework.PYTHON)
.configure(
'python',
input_schema=input_schema,
output_schema=output_schema
)
)
Pipeline Steps
With our models uploaded, we’ll perform different configurations of the pipeline steps.
First we’ll add just the house price model to the pipeline, deploy it, and submit a sample inference.
# used to restrict the resources needed for this demonstration
deployment_config = DeploymentConfigBuilder() \
.cpus(0.25).memory('1Gi') \
.build()
# clear the pipeline if this tutorial was run before
pipeline.undeploy()
pipeline.clear()
pipeline.add_model_step(house_price_model)
pipeline.deploy(deployment_config=deployment_config)
name | python-step-demo-pipeline |
---|---|
created | 2023-07-13 21:58:10.584660+00:00 |
last_updated | 2023-07-13 21:58:10.584660+00:00 |
deployed | True |
tags | |
versions | a4b11320-1867-4801-94bb-66ca5c625502 |
steps | house-price-sample |
## sample inference data
data = pd.DataFrame.from_dict({"tensor": [[0.6878518042239091,
0.17607340208535074,
-0.8695140830357148,
0.34638762962802144,
-0.0916270832672289,
-0.022063226781124278,
-0.13969884765926363,
1.002792335666138,
-0.3067449033633758,
0.9272000630461978,
0.28326687982544635,
0.35935375728372815,
-0.682562654045523,
0.532642794275658,
-0.22705189652659302,
0.5743846356405602,
-0.18805086358065454]]})
results = pipeline.infer(data)
display(results)
time | in.tensor | out.dense_2 | check_failures | |
---|---|---|---|---|
0 | 2023-07-13 21:58:21.397 | [0.6878518042, 0.1760734021, -0.869514083, 0.3... | [12.886651] | 0 |
Inference with Pipeline Step
Our inference result had the results in the out.dense_2
column. We’ll clear the pipeline, then add in as the pipeline step just the Python postprocessing step we’ve created. Then for our inference request, we’ll just submit the output of the house price model. Our result should be the first element in the array returned in the out.output
column.
pipeline.clear()
pipeline.add_model_step(step)
pipeline.deploy(deployment_config=deployment_config)
name | python-step-demo-pipeline |
---|---|
created | 2023-07-13 21:58:10.584660+00:00 |
last_updated | 2023-07-13 21:59:47.248922+00:00 |
deployed | True |
tags | |
versions | 1b0969ac-d0cb-42f0-96ff-a5b4a1cb5702, a75268c3-7a5d-4c27-b3e9-3ae79ad46363, a4b11320-1867-4801-94bb-66ca5c625502 |
steps | house-price-sample |
data = pd.DataFrame.from_dict({"dense_2": [12.886651]})
python_result = pipeline.infer(data)
display(python_result)
time | in.dense_2 | out.output | check_failures | |
---|---|---|---|---|
0 | 2023-07-13 21:59:53.065 | 12.886651 | [12.886651] | 0 |
Putting Both Models Together
Now we’ll do one last pipeline deployment with 2 steps:
- First the house price model that outputs the inference result into
dense_2
. - Second the python step so it will accept the output of the house price model, and reshape it into
output
.
import datetime
inference_start = datetime.datetime.now()
pipeline.undeploy()
pipeline.clear()
pipeline.add_model_step(house_price_model)
pipeline.add_model_step(step)
pipeline.deploy(deployment_config=deployment_config)
data = pd.DataFrame.from_dict({"tensor": [[0.6878518042239091,
0.17607340208535074,
-0.8695140830357148,
0.34638762962802144,
-0.0916270832672289,
-0.022063226781124278,
-0.13969884765926363,
1.002792335666138,
-0.3067449033633758,
0.9272000630461978,
0.28326687982544635,
0.35935375728372815,
-0.682562654045523,
0.532642794275658,
-0.22705189652659302,
0.5743846356405602,
-0.18805086358065454]]})
results = pipeline.infer(data)
display(results)
time | in.tensor | out.output | check_failures | |
---|---|---|---|---|
0 | 2023-07-13 22:05:58.341 | [0.6878518042, 0.1760734021, -0.869514083, 0.3... | [12.886651039123535] | 0 |
Pipeline Logs
As the data was exported by the pipeline step as a pandas DataFrame, it will be reflected in the pipeline logs. We’ll retrieve the most recent log from our most recent inference.
inference_end = datetime.datetime.now()
pipeline.logs(start_datetime=inference_start, end_datetime=inference_end)
time | in.tensor | out.output | check_failures | |
---|---|---|---|---|
0 | 2023-07-13 22:05:58.341 | [0.6878518042, 0.1760734021, -0.869514083, 0.3... | [12.886651039123535] | 0 |
Undeploy the Pipeline
With our tutorial complete, we’ll undeploy the pipeline and return the resources back to the cluster.
This process demonstrated how to structure a postprocessing Python script as a Wallaroo Pipeline step. This can be used for pre or post processing, Python based models, and other use cases.
pipeline.undeploy()
name | python-step-demo-pipeline |
---|---|
created | 2023-07-13 21:58:10.584660+00:00 |
last_updated | 2023-07-13 22:05:41.759261+00:00 |
deployed | False |
tags | |
versions | a232bd5a-bdcc-4c33-a0fe-515508c7405f, e7012369-90b5-4127-bd25-70f7c7aa65ba, 6a84493f-cae6-458a-be7c-8160517e7b18, 1b0969ac-d0cb-42f0-96ff-a5b4a1cb5702, a75268c3-7a5d-4c27-b3e9-3ae79ad46363, a4b11320-1867-4801-94bb-66ca5c625502 |
steps | house-price-sample |
2.5 - Wallaroo SDK Upload Tutorials: Pytorch
The following tutorials cover how to upload sample Pytorch models.
Parameter | Description |
---|---|
Web Site | https://pytorch.org/ |
Supported Libraries |
|
Framework | Framework.PYTORCH aka pytorch |
Supported File Types | pt ot pth in TorchScript format |
Runtime | Containerized aka mlflow |
2.5.1 - Wallaroo SDK Upload Tutorial: Pytorch Single IO
This tutorial can be downloaded as part of the Wallaroo Tutorials repository.
Wallaroo Model Upload via the Wallaroo SDK: Pytorch Single Input Output
The following tutorial demonstrates how to upload a Pytorch Single Input Output model to a Wallaroo instance.
Tutorial Goals
Demonstrate the following:
- Upload a Pytorch Single Input Output to a Wallaroo instance.
- Create a pipeline and add the model as a pipeline step.
- Perform a sample inference.
Prerequisites
- A Wallaroo version 2023.2.1 or above instance.
References
- Wallaroo MLOps API Essentials Guide: Model Upload and Registrations
- Wallaroo API Connection Guide
- DNS Integration Guide
Tutorial Steps
Import Libraries
The first step is to import the libraries we’ll be using. These are included by default in the Wallaroo instance’s JupyterHub service.
import json
import os
import pickle
import wallaroo
from wallaroo.pipeline import Pipeline
from wallaroo.deployment_config import DeploymentConfigBuilder
from wallaroo.object import EntityNotFoundError
from wallaroo.framework import Framework
import os
os.environ["MODELS_ENABLED"] = "true"
import pyarrow as pa
import numpy as np
import pandas as pd
Open a Connection to Wallaroo
The next step is connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. If logging in externally, update the wallarooPrefix
and wallarooSuffix
variables with the proper DNS information. For more information on Wallaroo DNS settings, see the Wallaroo DNS Integration Guide.
wl = wallaroo.Client()
wallarooPrefix = ""
wallarooSuffix = "autoscale-uat-ee.wallaroo.dev"
wl = wallaroo.Client(api_endpoint=f"https://{wallarooPrefix}api.{wallarooSuffix}",
auth_endpoint=f"https://{wallarooPrefix}keycloak.{wallarooSuffix}",
auth_type="sso")
Set Variables and Helper Functions
We’ll set the name of our workspace, pipeline, models and files. Workspace names must be unique across the Wallaroo workspace. For this, we’ll add in a randomly generated 4 characters to the workspace name to prevent collisions with other users’ workspaces. If running this tutorial, we recommend hard coding the workspace name so it will function in the same workspace each time it’s run.
We’ll set up some helper functions that will either use existing workspaces and pipelines, or create them if they do not already exist.
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
pipeline = wl.build_pipeline(name)
return pipeline
import string
import random
# make a random 4 character suffix to prevent overwriting other user's workspaces
suffix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
workspace_name = f'pytorch-single-io{suffix}'
pipeline_name = f'pytorch-single-io'
model_name = 'pytorch-single-io'
model_file_name = "./models/model-auto-conversion_pytorch_single_io_model.pt"
Create Workspace and Pipeline
We will now create the Wallaroo workspace to store our model and set it as the current workspace. Future commands will default to this workspace for pipeline creation, model uploads, etc. We’ll create our Wallaroo pipeline to deploy our model.
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
pipeline = get_pipeline(pipeline_name)
Configure Data Schemas
The following parameters are required for PyTorch models. Note that while some fields are considered as optional for the upload_model
method, they are required for proper uploading of a PyTorch model to Wallaroo.
Parameter | Type | Description |
---|---|---|
name | string (Required) | The name of the model. Model names are unique per workspace. Models that are uploaded with the same name are assigned as a new version of the model. |
path | string (Required) | The path to the model file being uploaded. |
framework | string (Upload Method Optional, PyTorch model Required) | Set as the Framework.PyTorch . |
input_schema | pyarrow.lib.Schema (Upload Method Optional, PyTorch model Required) | The input schema in Apache Arrow schema format. |
output_schema | pyarrow.lib.Schema (Upload Method Optional, PyTorch model Required) | The output schema in Apache Arrow schema format. |
convert_wait | bool (Upload Method Optional, PyTorch model Optional) (Default: True) |
|
Once the upload process starts, the model is containerized by the Wallaroo instance. This process may take up to 10 minutes.
input_schema = pa.schema([
pa.field('input', pa.list_(pa.float64(), list_size=10))
])
output_schema = pa.schema([
pa.field('output', pa.list_(pa.float64(), list_size=1))
])
Upload Model
The model will be uploaded with the framework set as Framework.PYTORCH
.
framework=Framework.PYTORCH
model = wl.upload_model(model_name,
model_file_name,
framework=framework,
input_schema=input_schema,
output_schema=output_schema
)
model
Waiting for model conversion... It may take up to 10.0min.
Model is Pending conversion..Converting.........Ready.
Name | pytorch-single-io |
Version | 60401663-c752-4448-b78d-7e4432e764f2 |
File Name | model-auto-conversion_pytorch_single_io_model.pt |
SHA | 23bdbafc51c3df7ac84e5f8b2833c592d7da2b27715a7da3e45bf732ea85b8bb |
Status | ready |
Image Path | proxy.replicated.com/proxy/wallaroo/ghcr.io/wallaroolabs/mlflow-deploy:v2023.3.0-main-3509 |
Updated At | 2023-13-Jul 17:44:21 |
model.config().runtime()
'mlflow'
Deploy Pipeline
The model is uploaded and ready for use. We’ll add it as a step in our pipeline, then deploy the pipeline. For this example we’re allocated 0.25 cpu and 4 Gi RAM to the pipeline through the pipeline’s deployment configuration.
deployment_config = DeploymentConfigBuilder() \
.cpus(0.25).memory('1Gi') \
.build()
# clear the pipeline if it was used before
pipeline.undeploy()
pipeline.clear()
pipeline.add_model_step(model)
pipeline.deploy(deployment_config=deployment_config)
pipeline.status()
{'status': 'Running',
'details': [],
'engines': [{'ip': '10.244.9.174',
'name': 'engine-d4c87f97-bxgtn',
'status': 'Running',
'reason': None,
'details': [],
'pipeline_statuses': {'pipelines': [{'id': 'pytorch-single-io',
'status': 'Running'}]},
'model_statuses': {'models': [{'name': 'pytorch-single-io',
'version': '60401663-c752-4448-b78d-7e4432e764f2',
'sha': '23bdbafc51c3df7ac84e5f8b2833c592d7da2b27715a7da3e45bf732ea85b8bb',
'status': 'Running'}]}}],
'engine_lbs': [{'ip': '10.244.9.172',
'name': 'engine-lb-584f54c899-vd24j',
'status': 'Running',
'reason': None,
'details': []}],
'sidekicks': [{'ip': '10.244.9.173',
'name': 'engine-sidekick-pytorch-single-io-267-74ff565f46-r64l9',
'status': 'Running',
'reason': None,
'details': [],
'statuses': '\n'}]}
Run Inference
A sample inference will be run. First the pandas DataFrame used for the inference is created, then the inference run through the pipeline’s infer
method.
mock_inference_data = np.random.rand(10, 10)
mock_dataframe = pd.DataFrame({"input": mock_inference_data.tolist()})
pipeline.infer(mock_dataframe)
time | in.input | out.output | check_failures | |
---|---|---|---|---|
0 | 2023-07-13 17:47:36.920 | [0.9793556002, 0.7185564171, 0.3036665062, 0.2... | [-0.17520469427108765] | 0 |
1 | 2023-07-13 17:47:36.920 | [0.1405072075, 0.6040986499, 0.4725817666, 0.9... | [-0.17446672916412354] | 0 |
2 | 2023-07-13 17:47:36.920 | [0.3610862346, 0.0217239609, 0.5346289561, 0.9... | [-0.18389971554279327] | 0 |
3 | 2023-07-13 17:47:36.920 | [0.9739280826, 0.4035630357, 0.7590709887, 0.7... | [-0.1760045289993286] | 0 |
4 | 2023-07-13 17:47:36.920 | [0.6488859167, 0.8076425858, 0.3775445684, 0.7... | [-0.17518186569213867] | 0 |
5 | 2023-07-13 17:47:36.920 | [0.7711169349, 0.3743345638, 0.3789860132, 0.6... | [-0.11498415470123291] | 0 |
6 | 2023-07-13 17:47:36.920 | [0.4095596117, 0.0426551485, 0.9338234503, 0.2... | [0.06910310685634613] | 0 |
7 | 2023-07-13 17:47:36.920 | [0.0986111718, 0.4338122288, 0.2258331516, 0.3... | [0.0066347867250442505] | 0 |
8 | 2023-07-13 17:47:36.920 | [0.2659302336, 0.9380703173, 0.7125008616, 0.9... | [-0.061735235154628754] | 0 |
9 | 2023-07-13 17:47:36.920 | [0.1378982605, 0.6003082091, 0.7641944662, 0.9... | [-0.1069975420832634] | 0 |
Undeploy Pipelines
With the tutorial complete, the pipeline is undeployed to return the resources back to the cluster.
pipeline.undeploy()
name | pytorch-single-io |
---|---|
created | 2023-07-13 17:43:23.742820+00:00 |
last_updated | 2023-07-13 17:44:26.785226+00:00 |
deployed | False |
tags | |
versions | 49fe2e5b-b043-4a9e-bf68-2054b09cc051, 7405b506-7cce-47be-be4c-98baf0d11f75 |
steps | pytorch-single-io |
2.5.2 - Wallaroo SDK Upload Tutorial: Pytorch Multiple IO
This tutorial can be downloaded as part of the Wallaroo Tutorials repository.
Wallaroo Model Upload via the Wallaroo SDK: Pytorch Multiple Input Output
The following tutorial demonstrates how to upload a Pytorch Multiple Input Output model to a Wallaroo instance.
Tutorial Goals
Demonstrate the following:
- Upload a Pytorch Multiple Input Output to a Wallaroo instance.
- Create a pipeline and add the model as a pipeline step.
- Perform a sample inference.
Prerequisites
- A Wallaroo version 2023.2.1 or above instance.
References
- Wallaroo MLOps API Essentials Guide: Model Upload and Registrations
- Wallaroo API Connection Guide
- DNS Integration Guide
Tutorial Steps
Import Libraries
The first step is to import the libraries we’ll be using. These are included by default in the Wallaroo instance’s JupyterHub service.
import json
import os
import pickle
import wallaroo
from wallaroo.pipeline import Pipeline
from wallaroo.deployment_config import DeploymentConfigBuilder
from wallaroo.object import EntityNotFoundError
from wallaroo.framework import Framework
import os
os.environ["MODELS_ENABLED"] = "true"
import pyarrow as pa
import numpy as np
import pandas as pd
Open a Connection to Wallaroo
The next step is connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. If logging in externally, update the wallarooPrefix
and wallarooSuffix
variables with the proper DNS information. For more information on Wallaroo DNS settings, see the Wallaroo DNS Integration Guide.
wl = wallaroo.Client()
Set Variables and Helper Functions
We’ll set the name of our workspace, pipeline, models and files. Workspace names must be unique across the Wallaroo workspace. For this, we’ll add in a randomly generated 4 characters to the workspace name to prevent collisions with other users’ workspaces. If running this tutorial, we recommend hard coding the workspace name so it will function in the same workspace each time it’s run.
We’ll set up some helper functions that will either use existing workspaces and pipelines, or create them if they do not already exist.
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
pipeline = wl.build_pipeline(name)
return pipeline
import string
import random
# make a random 4 character suffix to prevent overwriting other user's workspaces
suffix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
workspace_name = f'pytorch-multi-io{suffix}'
pipeline_name = f'pytorch-multi-io'
model_name = 'pytorch-multi-io'
model_file_name = "./models/model-auto-conversion_pytorch_multi_io_model.pt"
Create Workspace and Pipeline
We will now create the Wallaroo workspace to store our model and set it as the current workspace. Future commands will default to this workspace for pipeline creation, model uploads, etc. We’ll create our Wallaroo pipeline to deploy our model.
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
pipeline = get_pipeline(pipeline_name)
Configure Data Schemas
The following parameters are required for PyTorch models. Note that while some fields are considered as optional for the upload_model
method, they are required for proper uploading of a PyTorch model to Wallaroo.
Parameter | Type | Description |
---|---|---|
name | string (Required) | The name of the model. Model names are unique per workspace. Models that are uploaded with the same name are assigned as a new version of the model. |
path | string (Required) | The path to the model file being uploaded. |
framework | string (Upload Method Optional, PyTorch model Required) | Set as the Framework.PyTorch . |
input_schema | pyarrow.lib.Schema (Upload Method Optional, PyTorch model Required) | The input schema in Apache Arrow schema format. |
output_schema | pyarrow.lib.Schema (Upload Method Optional, PyTorch model Required) | The output schema in Apache Arrow schema format. |
convert_wait | bool (Upload Method Optional, PyTorch model Optional) (Default: True) |
|
Once the upload process starts, the model is containerized by the Wallaroo instance. This process may take up to 10 minutes.
input_schema = pa.schema([
pa.field('input_1', pa.list_(pa.float64(), list_size=10)),
pa.field('input_2', pa.list_(pa.float64(), list_size=5))
])
output_schema = pa.schema([
pa.field('output_1', pa.list_(pa.float64(), list_size=3)),
pa.field('output_2', pa.list_(pa.float64(), list_size=2))
])
Upload Model
The model will be uploaded with the framework set as Framework.PYTORCH
.
model = wl.upload_model(model_name,
model_file_name,
framework=Framework.PYTORCH,
input_schema=input_schema,
output_schema=output_schema
)
model
Waiting for model conversion... It may take up to 10.0min.
Model is Pending conversion.Converting..........Ready.
Name | pytorch-multi-io |
Version | d503b511-7a0c-4c90-9cbc-022467886dcd |
File Name | model-auto-conversion_pytorch_multi_io_model.pt |
SHA | 792db9ee9f41aded3c1d4705f50ccdedd21cafb8b6232c03e4a849b6da1050a8 |
Status | ready |
Image Path | proxy.replicated.com/proxy/wallaroo/ghcr.io/wallaroolabs/mlflow-deploy:v2023.3.0-main-3509 |
Updated At | 2023-13-Jul 17:40:39 |
model.config().runtime()
'mlflow'
Deploy Pipeline
The model is uploaded and ready for use. We’ll add it as a step in our pipeline, then deploy the pipeline. For this example we’re allocated 0.25 cpu and 4 Gi RAM to the pipeline through the pipeline’s deployment configuration.
deployment_config = DeploymentConfigBuilder() \
.cpus(0.25).memory('1Gi') \
.build()
# clear the pipeline if it was used before
pipeline.undeploy()
pipeline.clear()
pipeline.add_model_step(model)
pipeline.deploy(deployment_config=deployment_config)
pipeline.status()
{'status': 'Running',
'details': [],
'engines': [{'ip': '10.244.9.169',
'name': 'engine-7d8fbd4b74-kc5l5',
'status': 'Running',
'reason': None,
'details': [],
'pipeline_statuses': {'pipelines': [{'id': 'pytorch-multi-io',
'status': 'Running'}]},
'model_statuses': {'models': [{'name': 'pytorch-multi-io',
'version': 'd503b511-7a0c-4c90-9cbc-022467886dcd',
'sha': '792db9ee9f41aded3c1d4705f50ccdedd21cafb8b6232c03e4a849b6da1050a8',
'status': 'Running'}]}}],
'engine_lbs': [{'ip': '10.244.9.167',
'name': 'engine-lb-584f54c899-xb4wd',
'status': 'Running',
'reason': None,
'details': []}],
'sidekicks': [{'ip': '10.244.9.168',
'name': 'engine-sidekick-pytorch-multi-io-266-c9fdfd57c-r26jj',
'status': 'Running',
'reason': None,
'details': [],
'statuses': '\n'}]}
Run Inference
A sample inference will be run. First the pandas DataFrame used for the inference is created, then the inference run through the pipeline’s infer
method.
mock_inference_data = [np.random.rand(10, 10), np.random.rand(10, 5)]
mock_dataframe = pd.DataFrame(
{
"input_1": mock_inference_data[0].tolist(),
"input_2": mock_inference_data[1].tolist(),
}
)
pipeline.infer(mock_dataframe)
time | in.input_1 | in.input_2 | out.output_1 | out.output_2 | check_failures | |
---|---|---|---|---|---|---|
0 | 2023-07-13 17:41:15.828 | [0.7193870373, 0.0442841786, 0.0050186971, 0.9... | [0.5665563078, 0.2719984279, 0.8020988158, 0.9... | [-0.054150406271219254, -0.08067788183689117, ... | [-0.0867157131433487, 0.03102545440196991] | 0 |
1 | 2023-07-13 17:41:15.828 | [0.3303183352, 0.7338588864, 0.4901244227, 0.4... | [0.0020920459, 0.7473108704, 0.4701280309, 0.8... | [-0.0814041793346405, -0.008595120161771774, 0... | [0.22059735655784607, -0.0954931378364563] | 0 |
2 | 2023-07-13 17:41:15.828 | [0.8350163099, 0.9301095252, 0.4634737661, 0.0... | [0.2042988348, 0.3131013315, 0.2396516618, 0.8... | [-0.0936204195022583, 0.07057543098926544, 0.2... | [0.07758928835391998, 0.02205061912536621] | 0 |
3 | 2023-07-13 17:41:15.828 | [0.9560662259, 0.7334543871, 0.8347215148, 0.5... | [0.0846331092, 0.4104567348, 0.9964352268, 0.7... | [-0.07443580776453018, 0.00262654572725296, 0.... | [0.08881741762161255, 0.184173583984375] | 0 |
4 | 2023-07-13 17:41:15.828 | [0.8893996022, 0.0422634898, 0.094115839, 0.30... | [0.7091750984, 0.2677670739, 0.797334875, 0.60... | [0.011080481112003326, -0.03954530879855156, 0... | [0.02079789713025093, -0.023370355367660522] | 0 |
5 | 2023-07-13 17:41:15.828 | [0.360097173, 0.5326510416, 0.7427586985, 0.73... | [0.4059651678, 0.8209608747, 0.6650853071, 0.5... | [-0.041532136499881744, -0.02947094291448593, ... | [0.19275487959384918, -0.09685075283050537] | 0 |
6 | 2023-07-13 17:41:15.828 | [0.8493053911, 0.9772701228, 0.1395685377, 0.6... | [0.5020254829, 0.664290124, 0.0900637878, 0.27... | [0.023206554353237152, -0.05458785593509674, 0... | [0.252506822347641, -0.056555017828941345] | 0 |
7 | 2023-07-13 17:41:15.828 | [0.1783516801, 0.7383490632, 0.8826853, 0.8707... | [0.197614553, 0.7345261372, 0.3909055798, 0.12... | [-0.007199503481388092, -0.008408397436141968,... | [0.15768739581108093, 0.10399264097213745] | 0 |
8 | 2023-07-13 17:41:15.828 | [0.7671391397, 0.4079364465, 0.763576349, 0.26... | [0.3169436757, 0.3800284784, 0.1143413322, 0.2... | [-0.027935631573200226, 0.08666972815990448, 0... | [0.25775399804115295, -0.042692944407463074] | 0 |
9 | 2023-07-13 17:41:15.828 | [0.8885134534, 0.2440000822, 0.56551096, 0.780... | [0.1742113245, 0.624024604, 0.267043414, 0.153... | [-0.004899069666862488, 0.04411523416638374, 0... | [0.22322586178779602, -0.12406840920448303] | 0 |
Undeploy Pipelines
With the tutorial complete, the pipeline is undeployed to return the resources back to the cluster.
pipeline.undeploy()
name | pytorch-multi-io |
---|---|
created | 2023-07-13 17:38:02.341959+00:00 |
last_updated | 2023-07-13 17:40:44.329100+00:00 |
deployed | False |
tags | |
versions | 6867e6c5-7193-44d8-9756-4dfbc8b7db5c, 7b21d715-e95e-4557-abd1-a856eaea6e42 |
steps | pytorch-multi-io |
2.6 - Wallaroo SDK Upload Tutorials: SKLearn
The following tutorials cover how to upload sample SKLearn models.
Sci-kit Learn aka SKLearn.
Parameter | Description |
---|---|
Web Site | https://scikit-learn.org/stable/index.html |
Supported Libraries |
|
Framework | Framework.SKLEARN aka sklearn |
Runtime | Containerized aka tensorflow / mlflow |
SKLearn Schema Inputs
SKLearn schema follows a different format than other models. To prevent inputs from being out of order, the inputs should be submitted in a single row in the order the model is trained to accept, with all of the data types being the same. For example, the following DataFrame has 4 columns, each column a float
.
 | sepal length (cm) | sepal width (cm) | petal length (cm) | petal width (cm) |
---|---|---|---|---|
0 | 5.1 | 3.5 | 1.4 | 0.2 |
1 | 4.9 | 3.0 | 1.4 | 0.2 |
For submission to an SKLearn model, the data input schema will be a single array with 4 float values.
input_schema = pa.schema([
pa.field('inputs', pa.list_(pa.float64(), list_size=4))
])
When submitting as an inference, the DataFrame is converted to rows with the column data expressed as a single array. The data must be in the same order as the model expects, which is why the data is submitted as a single array rather than JSON labeled columns: this insures that the data is submitted in the exact order as the model is trained to accept.
Original DataFrame:
 | sepal length (cm) | sepal width (cm) | petal length (cm) | petal width (cm) |
---|---|---|---|---|
0 | 5.1 | 3.5 | 1.4 | 0.2 |
1 | 4.9 | 3.0 | 1.4 | 0.2 |
Converted DataFrame:
 | inputs |
---|---|
0 | [5.1, 3.5, 1.4, 0.2] |
1 | [4.9, 3.0, 1.4, 0.2] |
SKLearn Schema Outputs
Outputs for SKLearn that are meant to be predictions
or probabilities
when output by the model are labeled in the output schema for the model when uploaded to Wallaroo. For example, a model that outputs either 1 or 0 as its output would have the output schema as follows:
output_schema = pa.schema([
pa.field('predictions', pa.int32())
])
When used in Wallaroo, the inference result is contained in the out
metadata as out.predictions
.
pipeline.infer(dataframe)
 | time | in.inputs | out.predictions | check_failures |
---|---|---|---|---|
0 | 2023-07-05 15:11:29.776 | [5.1, 3.5, 1.4, 0.2] | 0 | 0 |
1 | 2023-07-05 15:11:29.776 | [4.9, 3.0, 1.4, 0.2] | 0 | 0 |
2.6.1 - Wallaroo SDK Upload Tutorial: SKLearn Clustering Kmeans
This tutorial can be downloaded as part of the Wallaroo Tutorials repository.
Wallaroo Model Upload via the Wallaroo SDK: SKLearn Clustering KMeans
The following tutorial demonstrates how to upload a SKLearn Clustering KMeans model to a Wallaroo instance.
Tutorial Goals
Demonstrate the following:
- Upload a SKLearn Clustering KMeans model to a Wallaroo instance.
- Create a pipeline and add the model as a pipeline step.
- Perform a sample inference.
Prerequisites
- A Wallaroo version 2023.2.1 or above instance.
References
- Wallaroo MLOps API Essentials Guide: Model Upload and Registrations
- Wallaroo API Connection Guide
- DNS Integration Guide
Tutorial Steps
Import Libraries
The first step is to import the libraries we’ll be using. These are included by default in the Wallaroo instance’s JupyterHub service.
import json
import os
import pickle
import wallaroo
from wallaroo.pipeline import Pipeline
from wallaroo.deployment_config import DeploymentConfigBuilder
from wallaroo.object import EntityNotFoundError
from wallaroo.framework import Framework
import os
os.environ["MODELS_ENABLED"] = "true"
import pyarrow as pa
import numpy as np
import pandas as pd
Open a Connection to Wallaroo
The next step is connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. If logging in externally, update the wallarooPrefix
and wallarooSuffix
variables with the proper DNS information. For more information on Wallaroo DNS settings, see the Wallaroo DNS Integration Guide.
wl = wallaroo.Client()
Set Variables and Helper Functions
We’ll set the name of our workspace, pipeline, models and files. Workspace names must be unique across the Wallaroo workspace. For this, we’ll add in a randomly generated 4 characters to the workspace name to prevent collisions with other users’ workspaces. If running this tutorial, we recommend hard coding the workspace name so it will function in the same workspace each time it’s run.
We’ll set up some helper functions that will either use existing workspaces and pipelines, or create them if they do not already exist.
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
pipeline = wl.build_pipeline(name)
return pipeline
import string
import random
# make a random 4 character suffix to prevent overwriting other user's workspaces
suffix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
workspace_name = f'sklearn-clustering-kmeans{suffix}'
pipeline_name = f'sklearn-clustering-kmeans'
model_name = 'sklearn-clustering-kmeans'
model_file_name = "models/model-auto-conversion_sklearn_kmeans.pkl"
Create Workspace and Pipeline
We will now create the Wallaroo workspace to store our model and set it as the current workspace. Future commands will default to this workspace for pipeline creation, model uploads, etc. We’ll create our Wallaroo pipeline to deploy our model.
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
pipeline = get_pipeline(pipeline_name)
Configure Data Schemas
SKLearn models are uploaded to Wallaroo through the Wallaroo Client upload_model
method.
Upload SKLearn Model Parameters
The following parameters are required for SKLearn models. Note that while some fields are considered as optional for the upload_model
method, they are required for proper uploading of a SKLearn model to Wallaroo.
Parameter | Type | Description |
---|---|---|
name | string (Required) | The name of the model. Model names are unique per workspace. Models that are uploaded with the same name are assigned as a new version of the model. |
path | string (Required) | The path to the model file being uploaded. |
framework | string (Upload Method Optional, SKLearn model Required) | Set as the Framework.SKLEARN . |
input_schema | pyarrow.lib.Schema (Upload Method Optional, SKLearn model Required) | The input schema in Apache Arrow schema format. |
output_schema | pyarrow.lib.Schema (Upload Method Optional, SKLearn model Required) | The output schema in Apache Arrow schema format. |
convert_wait | bool (Upload Method Optional, SKLearn model Optional) (Default: True) |
|
Once the upload process starts, the model is containerized by the Wallaroo instance. This process may take up to 10 minutes.
input_schema = pa.schema([
pa.field('inputs', pa.list_(pa.float64(), list_size=4))
])
output_schema = pa.schema([
pa.field('predictions', pa.int32())
])
Upload Model
The model will be uploaded with the framework set as Framework.SKLEARN
.
model = wl.upload_model(model_name,
model_file_name,
framework=Framework.SKLEARN,
input_schema=input_schema,
output_schema=output_schema)
model
Waiting for model conversion... It may take up to 10.0min.
Model is Pending conversion.......Converting............Ready.
Name | sklearn-clustering-kmeans |
Version | 1644dac9-5447-4654-b39f-a12560b1db55 |
File Name | model-auto-conversion_sklearn_kmeans.pkl |
SHA | b378a614854619dd573ec65b9b4ac73d0b397d50a048e733d96b68c5fdbec896 |
Status | ready |
Image Path | proxy.replicated.com/proxy/wallaroo/ghcr.io/wallaroolabs/mlflow-deploy:v2023.3.0-main-3509 |
Updated At | 2023-13-Jul 17:57:33 |
model.config().runtime()
'mlflow'
Deploy Pipeline
The model is uploaded and ready for use. We’ll add it as a step in our pipeline, then deploy the pipeline. For this example we’re allocated 0.25 cpu and 4 Gi RAM to the pipeline through the pipeline’s deployment configuration.
deployment_config = DeploymentConfigBuilder() \
.cpus(0.25).memory('1Gi') \
.build()
# clear the pipeline if it was used before
pipeline.undeploy()
pipeline.clear()
pipeline.add_model_step(model)
pipeline.deploy(deployment_config=deployment_config)
pipeline.status()
{'status': 'Running',
'details': [],
'engines': [{'ip': '10.244.9.186',
'name': 'engine-6c546c75dd-xhjx6',
'status': 'Running',
'reason': None,
'details': [],
'pipeline_statuses': {'pipelines': [{'id': 'sklearn-clustering-kmeans',
'status': 'Running'}]},
'model_statuses': {'models': [{'name': 'sklearn-clustering-kmeans',
'version': '1644dac9-5447-4654-b39f-a12560b1db55',
'sha': 'b378a614854619dd573ec65b9b4ac73d0b397d50a048e733d96b68c5fdbec896',
'status': 'Running'}]}}],
'engine_lbs': [{'ip': '10.244.9.185',
'name': 'engine-lb-584f54c899-9j8gf',
'status': 'Running',
'reason': None,
'details': []}],
'sidekicks': [{'ip': '10.244.9.184',
'name': 'engine-sidekick-sklearn-clustering-kmeans-269-5d9597dc9f-gqqxf',
'status': 'Running',
'reason': None,
'details': [],
'statuses': '\n'}]}
Inference
SKLearn models must have all of the data as one line to prevent columns from being read out of order when submitting in JSON. The following will take in the data, convert the rows into a single inputs
for the table, then perform the inference. From the output_schema
we have defined the output as predictions
which will be displayed in our inference result output as out.predictions
.
data = pd.read_json('./data/test-sklearn-kmeans.json')
display(data)
# move the column values to a single array input
mock_dataframe = pd.DataFrame({"inputs": data[:2].values.tolist()})
display(mock_dataframe)
sepal length (cm) | sepal width (cm) | petal length (cm) | petal width (cm) | |
---|---|---|---|---|
0 | 5.1 | 3.5 | 1.4 | 0.2 |
1 | 4.9 | 3.0 | 1.4 | 0.2 |
inputs | |
---|---|
0 | [5.1, 3.5, 1.4, 0.2] |
1 | [4.9, 3.0, 1.4, 0.2] |
result = pipeline.infer(mock_dataframe)
display(result)
time | in.inputs | out.predictions | check_failures | |
---|---|---|---|---|
0 | 2023-07-13 18:01:14.756 | [5.1, 3.5, 1.4, 0.2] | 1 | 0 |
1 | 2023-07-13 18:01:14.756 | [4.9, 3.0, 1.4, 0.2] | 1 | 0 |
Undeploy Pipelines
With the tutorial complete, the pipeline is undeployed to return the resources back to the cluster.
pipeline.undeploy()
name | sklearn-clustering-kmeans |
---|---|
created | 2023-07-13 17:58:35.744857+00:00 |
last_updated | 2023-07-13 17:58:35.744857+00:00 |
deployed | False |
tags | |
versions | 386117da-6d79-4349-a774-9610cc9df1bb |
steps | sklearn-clustering-kmeans |
2.6.2 - Wallaroo SDK Upload Tutorial: SKLearn Clustering SVM
This tutorial can be downloaded as part of the Wallaroo Tutorials repository.
Wallaroo Model Upload via the Wallaroo SDK: Sklearn Clustering SVM
The following tutorial demonstrates how to upload a SKLearn Clustering Support Vector Machine(SVM) model to a Wallaroo instance.
Tutorial Goals
Demonstrate the following:
- Upload a Sklearn Clustering SVM model to a Wallaroo instance.
- Create a pipeline and add the model as a pipeline step.
- Perform a sample inference.
Prerequisites
- A Wallaroo version 2023.2.1 or above instance.
References
- Wallaroo MLOps API Essentials Guide: Model Upload and Registrations
- Wallaroo API Connection Guide
- DNS Integration Guide
Tutorial Steps
Import Libraries
The first step is to import the libraries we’ll be using. These are included by default in the Wallaroo instance’s JupyterHub service.
import json
import os
import pickle
import wallaroo
from wallaroo.pipeline import Pipeline
from wallaroo.deployment_config import DeploymentConfigBuilder
from wallaroo.object import EntityNotFoundError
from wallaroo.framework import Framework
import os
os.environ["MODELS_ENABLED"] = "true"
import pyarrow as pa
import numpy as np
import pandas as pd
Open a Connection to Wallaroo
The next step is connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. If logging in externally, update the wallarooPrefix
and wallarooSuffix
variables with the proper DNS information. For more information on Wallaroo DNS settings, see the Wallaroo DNS Integration Guide.
wl = wallaroo.Client()
Set Variables and Helper Functions
We’ll set the name of our workspace, pipeline, models and files. Workspace names must be unique across the Wallaroo workspace. For this, we’ll add in a randomly generated 4 characters to the workspace name to prevent collisions with other users’ workspaces. If running this tutorial, we recommend hard coding the workspace name so it will function in the same workspace each time it’s run.
We’ll set up some helper functions that will either use existing workspaces and pipelines, or create them if they do not already exist.
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
pipeline = wl.build_pipeline(name)
return pipeline
import string
import random
# make a random 4 character suffix to prevent overwriting other user's workspaces
suffix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
workspace_name = f'sklearn-clustering-svm{suffix}'
pipeline_name = f'sklearn-clustering-svm'
model_name = 'sklearn-clustering-svm'
model_file_name = './models/model-auto-conversion_sklearn_svm_pipeline.pkl'
Create Workspace and Pipeline
We will now create the Wallaroo workspace to store our model and set it as the current workspace. Future commands will default to this workspace for pipeline creation, model uploads, etc. We’ll create our Wallaroo pipeline to deploy our model.
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
pipeline = get_pipeline(pipeline_name)
Configure Data Schemas
SKLearn models are uploaded to Wallaroo through the Wallaroo Client upload_model
method.
Upload SKLearn Model Parameters
The following parameters are required for SKLearn models. Note that while some fields are considered as optional for the upload_model
method, they are required for proper uploading of a SKLearn model to Wallaroo.
Parameter | Type | Description |
---|---|---|
name | string (Required) | The name of the model. Model names are unique per workspace. Models that are uploaded with the same name are assigned as a new version of the model. |
path | string (Required) | The path to the model file being uploaded. |
framework | string (Upload Method Optional, SKLearn model Required) | Set as the Framework.SKLEARN . |
input_schema | pyarrow.lib.Schema (Upload Method Optional, SKLearn model Required) | The input schema in Apache Arrow schema format. |
output_schema | pyarrow.lib.Schema (Upload Method Optional, SKLearn model Required) | The output schema in Apache Arrow schema format. |
convert_wait | bool (Upload Method Optional, SKLearn model Optional) (Default: True) |
|
Once the upload process starts, the model is containerized by the Wallaroo instance. This process may take up to 10 minutes.
input_schema = pa.schema([
pa.field('inputs', pa.list_(pa.float64(), list_size=4))
])
output_schema = pa.schema([
pa.field('predictions', pa.int32())
])
Upload Model
The model will be uploaded with the framework set as Framework.SKLEARN
.
model = wl.upload_model(model_name,
model_file_name,
framework=Framework.SKLEARN,
input_schema=input_schema,
output_schema=output_schema)
model
Waiting for model conversion... It may take up to 10.0min.
Model is Pending conversion.Converting..Pending conversion.Converting.......Ready.
Name | sklearn-clustering-svm |
Version | 984494d8-3908-4cf7-9bcb-c52116b8da7a |
File Name | model-auto-conversion_sklearn_svm_pipeline.pkl |
SHA | c6eec69d96f7eeb3db034600dea6b12da1d2b832c39252ec4942d02f68f52f40 |
Status | ready |
Image Path | proxy.replicated.com/proxy/wallaroo/ghcr.io/wallaroolabs/mlflow-deploy:v2023.3.0-main-3509 |
Updated At | 2023-13-Jul 18:15:54 |
model.config().runtime()
'mlflow'
Deploy Pipeline
The model is uploaded and ready for use. We’ll add it as a step in our pipeline, then deploy the pipeline. For this example we’re allocated 0.25 cpu and 4 Gi RAM to the pipeline through the pipeline’s deployment configuration.
deployment_config = DeploymentConfigBuilder() \
.cpus(0.25).memory('1Gi') \
.build()
# clear the pipeline if it was used before
pipeline.undeploy()
pipeline.clear()
pipeline.add_model_step(model)
pipeline.deploy(deployment_config=deployment_config)
pipeline.status()
{'status': 'Running',
'details': [],
'engines': [{'ip': '10.244.9.198',
'name': 'engine-665c7458c4-z92xn',
'status': 'Running',
'reason': None,
'details': [],
'pipeline_statuses': {'pipelines': [{'id': 'sklearn-clustering-svm',
'status': 'Running'}]},
'model_statuses': {'models': [{'name': 'sklearn-clustering-svm',
'version': '984494d8-3908-4cf7-9bcb-c52116b8da7a',
'sha': 'c6eec69d96f7eeb3db034600dea6b12da1d2b832c39252ec4942d02f68f52f40',
'status': 'Running'}]}}],
'engine_lbs': [{'ip': '10.244.9.199',
'name': 'engine-lb-584f54c899-qwn65',
'status': 'Running',
'reason': None,
'details': []}],
'sidekicks': [{'ip': '10.244.9.197',
'name': 'engine-sidekick-sklearn-clustering-svm-271-7fc57d45d9-wg8bl',
'status': 'Running',
'reason': None,
'details': [],
'statuses': '\n'}]}
Inference
SKLearn models must have all of the data as one line to prevent columns from being read out of order when submitting in JSON. The following will take in the data, convert the rows into a single inputs
for the table, then perform the inference. From the output_schema
we have defined the output as predictions
which will be displayed in our inference result output as out.predictions
.
data = pd.read_json('./data/test_cluster-svm.json')
display(data)
# move the column values to a single array input
dataframe = pd.DataFrame({"inputs": data[:2].values.tolist()})
display(dataframe)
sepal length (cm) | sepal width (cm) | petal length (cm) | petal width (cm) | |
---|---|---|---|---|
0 | 5.1 | 3.5 | 1.4 | 0.2 |
1 | 4.9 | 3.0 | 1.4 | 0.2 |
inputs | |
---|---|
0 | [5.1, 3.5, 1.4, 0.2] |
1 | [4.9, 3.0, 1.4, 0.2] |
pipeline.infer(dataframe)
time | in.inputs | out.predictions | check_failures | |
---|---|---|---|---|
0 | 2023-07-13 18:17:07.199 | [5.1, 3.5, 1.4, 0.2] | 0 | 0 |
1 | 2023-07-13 18:17:07.199 | [4.9, 3.0, 1.4, 0.2] | 0 | 0 |
Undeploy Pipelines
With the tutorial complete, the pipeline is undeployed to return the resources back to the cluster.
pipeline.undeploy()
name | sklearn-clustering-svm |
---|---|
created | 2023-07-13 18:14:58.440836+00:00 |
last_updated | 2023-07-13 18:16:50.545217+00:00 |
deployed | False |
tags | |
versions | 749e5f51-376e-4b09-a0e2-8c47c69eba53, 09ef9b6a-912a-4ca8-9b82-09837cdccdb6 |
steps | sklearn-clustering-svm |
2.6.3 - Wallaroo SDK Upload Tutorial: SKLearn Linear Regression
This tutorial can be downloaded as part of the Wallaroo Tutorials repository.
Wallaroo Model Upload via the Wallaroo SDK: SKLearn Linear Regression
The following tutorial demonstrates how to upload a SKLearn Linear Regression model to a Wallaroo instance.
Tutorial Goals
Demonstrate the following:
- Upload a SKLearn Linear Regression model to a Wallaroo instance.
- Create a pipeline and add the model as a pipeline step.
- Perform a sample inference.
Prerequisites
- A Wallaroo version 2023.2.1 or above instance.
References
- Wallaroo MLOps API Essentials Guide: Model Upload and Registrations
- Wallaroo API Connection Guide
- DNS Integration Guide
Tutorial Steps
Import Libraries
The first step is to import the libraries we’ll be using. These are included by default in the Wallaroo instance’s JupyterHub service.
import json
import os
import pickle
import wallaroo
from wallaroo.pipeline import Pipeline
from wallaroo.deployment_config import DeploymentConfigBuilder
from wallaroo.object import EntityNotFoundError
from wallaroo.framework import Framework
import os
os.environ["MODELS_ENABLED"] = "true"
import pyarrow as pa
import numpy as np
import pandas as pd
Open a Connection to Wallaroo
The next step is connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. If logging in externally, update the wallarooPrefix
and wallarooSuffix
variables with the proper DNS information. For more information on Wallaroo DNS settings, see the Wallaroo DNS Integration Guide.
wl = wallaroo.Client()
Set Variables and Helper Functions
We’ll set the name of our workspace, pipeline, models and files. Workspace names must be unique across the Wallaroo workspace. For this, we’ll add in a randomly generated 4 characters to the workspace name to prevent collisions with other users’ workspaces. If running this tutorial, we recommend hard coding the workspace name so it will function in the same workspace each time it’s run.
We’ll set up some helper functions that will either use existing workspaces and pipelines, or create them if they do not already exist.
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
pipeline = wl.build_pipeline(name)
return pipeline
import string
import random
# make a random 4 character suffix to prevent overwriting other user's workspaces
suffix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
workspace_name = f'sklearn-linear-regression{suffix}'
pipeline_name = f'sklearn-linear-regression'
model_name = 'sklearn-linear-regression'
model_file_name = 'models/model-auto-conversion_sklearn_linreg_diabetes.pkl'
Create Workspace and Pipeline
We will now create the Wallaroo workspace to store our model and set it as the current workspace. Future commands will default to this workspace for pipeline creation, model uploads, etc. We’ll create our Wallaroo pipeline to deploy our model.
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
pipeline = get_pipeline(pipeline_name)
Configure Data Schemas
SKLearn models are uploaded to Wallaroo through the Wallaroo Client upload_model
method.
Upload SKLearn Model Parameters
The following parameters are required for SKLearn models. Note that while some fields are considered as optional for the upload_model
method, they are required for proper uploading of a SKLearn model to Wallaroo.
Parameter | Type | Description |
---|---|---|
name | string (Required) | The name of the model. Model names are unique per workspace. Models that are uploaded with the same name are assigned as a new version of the model. |
path | string (Required) | The path to the model file being uploaded. |
framework | string (Upload Method Optional, SKLearn model Required) | Set as the Framework.SKLEARN . |
input_schema | pyarrow.lib.Schema (Upload Method Optional, SKLearn model Required) | The input schema in Apache Arrow schema format. |
output_schema | pyarrow.lib.Schema (Upload Method Optional, SKLearn model Required) | The output schema in Apache Arrow schema format. |
convert_wait | bool (Upload Method Optional, SKLearn model Optional) (Default: True) |
|
Once the upload process starts, the model is containerized by the Wallaroo instance. This process may take up to 10 minutes.
input_schema = pa.schema([
pa.field('inputs', pa.list_(pa.float64(), list_size=10))
])
output_schema = pa.schema([
pa.field('predictions', pa.float64())
])
Upload Model
The model will be uploaded with the framework set as Framework.SKLEARN
.
model = wl.upload_model(model_name,
model_file_name,
framework=Framework.SKLEARN,
input_schema=input_schema,
output_schema=output_schema)
model
Waiting for model conversion... It may take up to 10.0min.
Model is Pending conversion.Converting..Pending conversion.Converting......Ready.
Name | sklearn-linear-regression |
Version | aa2741c1-1099-4856-8581-a011a8bfc584 |
File Name | model-auto-conversion_sklearn_linreg_diabetes.pkl |
SHA | 6a9085e2d65bf0379934651d2272d3c6c4e020e36030933d85df3a8d15135a45 |
Status | ready |
Image Path | proxy.replicated.com/proxy/wallaroo/ghcr.io/wallaroolabs/mlflow-deploy:v2023.3.0-main-3509 |
Updated At | 2023-13-Jul 18:20:33 |
model.config().runtime()
'mlflow'
Deploy Pipeline
The model is uploaded and ready for use. We’ll add it as a step in our pipeline, then deploy the pipeline. For this example we’re allocated 0.25 cpu and 4 Gi RAM to the pipeline through the pipeline’s deployment configuration.
deployment_config = DeploymentConfigBuilder() \
.cpus(0.25).memory('1Gi') \
.build()
# clear the pipeline if it was used before
pipeline.undeploy()
pipeline.clear()
pipeline.add_model_step(model)
pipeline.deploy(deployment_config=deployment_config)
pipeline.status()
{'status': 'Running',
'details': [],
'engines': [{'ip': '10.244.9.204',
'name': 'engine-747f8b6656-6bd92',
'status': 'Running',
'reason': None,
'details': [],
'pipeline_statuses': {'pipelines': [{'id': 'sklearn-linear-regression',
'status': 'Running'}]},
'model_statuses': {'models': [{'name': 'sklearn-linear-regression',
'version': 'aa2741c1-1099-4856-8581-a011a8bfc584',
'sha': '6a9085e2d65bf0379934651d2272d3c6c4e020e36030933d85df3a8d15135a45',
'status': 'Running'}]}}],
'engine_lbs': [{'ip': '10.244.9.205',
'name': 'engine-lb-584f54c899-fbznx',
'status': 'Running',
'reason': None,
'details': []}],
'sidekicks': [{'ip': '10.244.9.203',
'name': 'engine-sidekick-sklearn-linear-regression-272-cf6f55ff8-j2z2f',
'status': 'Running',
'reason': None,
'details': [],
'statuses': '\n'}]}
Inference
SKLearn models must have all of the data as one line to prevent columns from being read out of order when submitting in JSON. The following will take in the data, convert the rows into a single inputs
for the table, then perform the inference. From the output_schema
we have defined the output as predictions
which will be displayed in our inference result output as out.predictions
.
data = pd.read_json('data/test_linear_regression_data.json')
display(data)
# move the column values to a single array input
dataframe = pd.DataFrame({"inputs": data[:2].values.tolist()})
display(dataframe)
age | sex | bmi | bp | s1 | s2 | s3 | s4 | s5 | s6 | |
---|---|---|---|---|---|---|---|---|---|---|
0 | 0.038076 | 0.050680 | 0.061696 | 0.021872 | -0.044223 | -0.034821 | -0.043401 | -0.002592 | 0.019907 | -0.017646 |
1 | -0.001882 | -0.044642 | -0.051474 | -0.026328 | -0.008449 | -0.019163 | 0.074412 | -0.039493 | -0.068332 | -0.092204 |
inputs | |
---|---|
0 | [0.0380759064, 0.0506801187, 0.0616962065, 0.0... |
1 | [-0.0018820165, -0.0446416365, -0.051474061200... |
pipeline.infer(dataframe)
time | in.inputs | out.predictions | check_failures | |
---|---|---|---|---|
0 | 2023-07-13 18:20:51.820 | [0.0380759064, 0.0506801187, 0.0616962065, 0.0... | 206.116677 | 0 |
1 | 2023-07-13 18:20:51.820 | [-0.0018820165, -0.0446416365, -0.0514740612, ... | 68.071033 | 0 |
Undeploy Pipelines
With the tutorial complete, the pipeline is undeployed to return the resources back to the cluster.
pipeline.undeploy()
name | sklearn-linear-regression |
---|---|
created | 2023-07-13 18:19:38.157202+00:00 |
last_updated | 2023-07-13 18:20:35.237728+00:00 |
deployed | False |
tags | |
versions | 72782c06-edb6-4cca-8598-a3103e39c24b, 7e8403a9-41e1-42b9-ba36-2523634105d0 |
steps | sklearn-linear-regression |
2.6.4 - Wallaroo SDK Upload Tutorial: SKLearn Logistic Regression
This tutorial can be downloaded as part of the Wallaroo Tutorials repository.
Wallaroo Model Upload via the Wallaroo SDK: SKLearn Logistic Regression
The following tutorial demonstrates how to upload a SKLearn Logistic Regression model to a Wallaroo instance.
Tutorial Goals
Demonstrate the following:
- Upload a SKLearn Logistic Regression model to a Wallaroo instance.
- Create a pipeline and add the model as a pipeline step.
- Perform a sample inference.
Prerequisites
- A Wallaroo version 2023.2.1 or above instance.
References
- Wallaroo MLOps API Essentials Guide: Model Upload and Registrations
- Wallaroo API Connection Guide
- DNS Integration Guide
Tutorial Steps
Import Libraries
The first step is to import the libraries we’ll be using. These are included by default in the Wallaroo instance’s JupyterHub service.
import json
import os
import pickle
import wallaroo
from wallaroo.pipeline import Pipeline
from wallaroo.deployment_config import DeploymentConfigBuilder
from wallaroo.object import EntityNotFoundError
from wallaroo.framework import Framework
import os
os.environ["MODELS_ENABLED"] = "true"
import pyarrow as pa
import numpy as np
import pandas as pd
Open a Connection to Wallaroo
The next step is connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. If logging in externally, update the wallarooPrefix
and wallarooSuffix
variables with the proper DNS information. For more information on Wallaroo DNS settings, see the Wallaroo DNS Integration Guide.
wl = wallaroo.Client()
Set Variables and Helper Functions
We’ll set the name of our workspace, pipeline, models and files. Workspace names must be unique across the Wallaroo workspace. For this, we’ll add in a randomly generated 4 characters to the workspace name to prevent collisions with other users’ workspaces. If running this tutorial, we recommend hard coding the workspace name so it will function in the same workspace each time it’s run.
We’ll set up some helper functions that will either use existing workspaces and pipelines, or create them if they do not already exist.
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
pipeline = wl.build_pipeline(name)
return pipeline
import string
import random
# make a random 4 character suffix to prevent overwriting other user's workspaces
suffix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
workspace_name = f'sklearn-logistic-regression{suffix}'
pipeline_name = f'sklearn-logistic-regression'
model_name = 'sklearn-logistic-regression'
model_file_name = 'models/logreg.pkl'
Create Workspace and Pipeline
We will now create the Wallaroo workspace to store our model and set it as the current workspace. Future commands will default to this workspace for pipeline creation, model uploads, etc. We’ll create our Wallaroo pipeline to deploy our model.
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
pipeline = get_pipeline(pipeline_name)
Configure Data Schemas
SKLearn models are uploaded to Wallaroo through the Wallaroo Client upload_model
method.
Upload SKLearn Model Parameters
The following parameters are required for SKLearn models. Note that while some fields are considered as optional for the upload_model
method, they are required for proper uploading of a SKLearn model to Wallaroo.
Parameter | Type | Description |
---|---|---|
name | string (Required) | The name of the model. Model names are unique per workspace. Models that are uploaded with the same name are assigned as a new version of the model. |
path | string (Required) | The path to the model file being uploaded. |
framework | string (Upload Method Optional, SKLearn model Required) | Set as the Framework.SKLEARN . |
input_schema | pyarrow.lib.Schema (Upload Method Optional, SKLearn model Required) | The input schema in Apache Arrow schema format. |
output_schema | pyarrow.lib.Schema (Upload Method Optional, SKLearn model Required) | The output schema in Apache Arrow schema format. |
convert_wait | bool (Upload Method Optional, SKLearn model Optional) (Default: True) |
|
Once the upload process starts, the model is containerized by the Wallaroo instance. This process may take up to 10 minutes.
input_schema = pa.schema([
pa.field('inputs', pa.list_(pa.float64(), list_size=4))
])
output_schema = pa.schema([
pa.field('predictions', pa.int32()),
pa.field('probabilities', pa.list_(pa.float64(), list_size=3))
])
Upload Model
The model will be uploaded with the framework set as Framework.SKLEARN
.
model = wl.upload_model(model_name,
model_file_name,
framework=Framework.SKLEARN,
input_schema=input_schema,
output_schema=output_schema)
model
Waiting for model conversion... It may take up to 10.0min.
Model is Pending conversion.Converting..Pending conversion.Converting......Ready.
Name | sklearn-logistic-regression |
Version | ae956bce-4791-4093-8f6e-a71a2ac60350 |
File Name | logreg.pkl |
SHA | 9302df6cc64a2c0d12daa257657f07f9db0bb2072bb3fb92396500b21358e0b9 |
Status | ready |
Image Path | proxy.replicated.com/proxy/wallaroo/ghcr.io/wallaroolabs/mlflow-deploy:v2023.3.0-main-3509 |
Updated At | 2023-13-Jul 18:24:06 |
model.config().runtime()
'mlflow'
Deploy Pipeline
The model is uploaded and ready for use. We’ll add it as a step in our pipeline, then deploy the pipeline. For this example we’re allocated 0.25 cpu and 4 Gi RAM to the pipeline through the pipeline’s deployment configuration.
deployment_config = DeploymentConfigBuilder() \
.cpus(0.25).memory('1Gi') \
.build()
# clear the pipeline if it was used before
pipeline.undeploy()
pipeline.clear()
pipeline.add_model_step(model)
pipeline.deploy(deployment_config=deployment_config)
pipeline.status()
{'status': 'Running',
'details': [],
'engines': [{'ip': '10.244.9.209',
'name': 'engine-74bc88dc9b-bmwn9',
'status': 'Running',
'reason': None,
'details': [],
'pipeline_statuses': {'pipelines': [{'id': 'sklearn-logistic-regression',
'status': 'Running'}]},
'model_statuses': {'models': [{'name': 'sklearn-logistic-regression',
'version': 'ae956bce-4791-4093-8f6e-a71a2ac60350',
'sha': '9302df6cc64a2c0d12daa257657f07f9db0bb2072bb3fb92396500b21358e0b9',
'status': 'Running'}]}}],
'engine_lbs': [{'ip': '10.244.9.210',
'name': 'engine-lb-584f54c899-zsb9l',
'status': 'Running',
'reason': None,
'details': []}],
'sidekicks': [{'ip': '10.244.9.211',
'name': 'engine-sidekick-sklearn-logistic-regression-273-b4b54f497-glkts',
'status': 'Running',
'reason': None,
'details': [],
'statuses': '\n'}]}
Inference
SKLearn models must have all of the data as one line to prevent columns from being read out of order when submitting in JSON. The following will take in the data, convert the rows into a single inputs
for the table, then perform the inference. From the output_schema
we have defined the output as predictions
which will be displayed in our inference result output as out.predictions
.
data = pd.read_json('data/test_logreg_data.json')
display(data)
# move the column values to a single array input
dataframe = pd.DataFrame({"inputs": data[:2].values.tolist()})
display(dataframe)
sepal length (cm) | sepal width (cm) | petal length (cm) | petal width (cm) | |
---|---|---|---|---|
0 | 5.1 | 3.5 | 1.4 | 0.2 |
1 | 4.9 | 3.0 | 1.4 | 0.2 |
inputs | |
---|---|
0 | [5.1, 3.5, 1.4, 0.2] |
1 | [4.9, 3.0, 1.4, 0.2] |
pipeline.infer(dataframe)
time | in.inputs | out.predictions | out.probabilities | check_failures | |
---|---|---|---|---|---|
0 | 2023-07-13 18:24:26.268 | [5.1, 3.5, 1.4, 0.2] | 0 | [0.9815821465852236, 0.018417838912958125, 1.4... | 0 |
1 | 2023-07-13 18:24:26.268 | [4.9, 3.0, 1.4, 0.2] | 0 | [0.9713374799347873, 0.028662489870060148, 3.0... | 0 |
Undeploy Pipelines
With the tutorial complete, the pipeline is undeployed to return the resources back to the cluster.
pipeline.undeploy()
name | sklearn-logistic-regression |
---|---|
created | 2023-07-13 18:23:10.490850+00:00 |
last_updated | 2023-07-13 18:24:07.723159+00:00 |
deployed | False |
tags | |
versions | ef89df8f-4748-4c4d-b838-b0faa2b1b4ec, 7218317a-1df8-4d4a-8fbf-dbba19ac2853 |
steps | sklearn-logistic-regression |
2.6.5 - Wallaroo SDK Upload Tutorial: SKLearn SVM PCA
This tutorial can be downloaded as part of the Wallaroo Tutorials repository.
Wallaroo Model Upload via the Wallaroo SDK: Sklearn Clustering SVM PCA
The following tutorial demonstrates how to upload a SKLearn Clustering Support Vector Machine(SVM) Principal Component Analysis (PCA) model to a Wallaroo instance.
Tutorial Goals
Demonstrate the following:
- Upload a Sklearn Clustering SVM PCA model to a Wallaroo instance.
- Create a pipeline and add the model as a pipeline step.
- Perform a sample inference.
Prerequisites
- A Wallaroo version 2023.2.1 or above instance.
References
- Wallaroo MLOps API Essentials Guide: Model Upload and Registrations
- Wallaroo API Connection Guide
- DNS Integration Guide
Tutorial Steps
Import Libraries
The first step is to import the libraries we’ll be using. These are included by default in the Wallaroo instance’s JupyterHub service.
import json
import os
import pickle
import wallaroo
from wallaroo.pipeline import Pipeline
from wallaroo.deployment_config import DeploymentConfigBuilder
from wallaroo.object import EntityNotFoundError
from wallaroo.framework import Framework
import os
os.environ["MODELS_ENABLED"] = "true"
import pyarrow as pa
import numpy as np
import pandas as pd
Open a Connection to Wallaroo
The next step is connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. If logging in externally, update the wallarooPrefix
and wallarooSuffix
variables with the proper DNS information. For more information on Wallaroo DNS settings, see the Wallaroo DNS Integration Guide.
wl = wallaroo.Client()
Set Variables and Helper Functions
We’ll set the name of our workspace, pipeline, models and files. Workspace names must be unique across the Wallaroo workspace. For this, we’ll add in a randomly generated 4 characters to the workspace name to prevent collisions with other users’ workspaces. If running this tutorial, we recommend hard coding the workspace name so it will function in the same workspace each time it’s run.
We’ll set up some helper functions that will either use existing workspaces and pipelines, or create them if they do not already exist.
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
pipeline = wl.build_pipeline(name)
return pipeline
import string
import random
# make a random 4 character suffix to prevent overwriting other user's workspaces
suffix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
workspace_name = f'sklearn-clustering-svm-pca{suffix}'
pipeline_name = f'sklearn-clustering-svm-pca'
model_name = 'sklearn-clustering-svm-pca'
model_file_name = 'models/model-auto-conversion_sklearn_svm_pca_pipeline.pkl'
Create Workspace and Pipeline
We will now create the Wallaroo workspace to store our model and set it as the current workspace. Future commands will default to this workspace for pipeline creation, model uploads, etc. We’ll create our Wallaroo pipeline to deploy our model.
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
pipeline = get_pipeline(pipeline_name)
Configure Data Schemas
SKLearn models are uploaded to Wallaroo through the Wallaroo Client upload_model
method.
Upload SKLearn Model Parameters
The following parameters are required for SKLearn models. Note that while some fields are considered as optional for the upload_model
method, they are required for proper uploading of a SKLearn model to Wallaroo.
Parameter | Type | Description |
---|---|---|
name | string (Required) | The name of the model. Model names are unique per workspace. Models that are uploaded with the same name are assigned as a new version of the model. |
path | string (Required) | The path to the model file being uploaded. |
framework | string (Upload Method Optional, SKLearn model Required) | Set as the Framework.SKLEARN . |
input_schema | pyarrow.lib.Schema (Upload Method Optional, SKLearn model Required) | The input schema in Apache Arrow schema format. |
output_schema | pyarrow.lib.Schema (Upload Method Optional, SKLearn model Required) | The output schema in Apache Arrow schema format. |
convert_wait | bool (Upload Method Optional, SKLearn model Optional) (Default: True) |
|
Once the upload process starts, the model is containerized by the Wallaroo instance. This process may take up to 10 minutes.
input_schema = pa.schema([
pa.field('inputs', pa.list_(pa.float64(), list_size=4))
])
output_schema = pa.schema([
pa.field('predictions', pa.int32())
])
Upload Model
The model will be uploaded with the framework set as Framework.SKLEARN
.
model = wl.upload_model(model_name,
model_file_name,
framework=Framework.SKLEARN,
input_schema=input_schema,
output_schema=output_schema)
model
Waiting for model conversion... It may take up to 10.0min.
Model is Pending conversion.Converting..Pending conversion..Converting.......Ready.
Name | sklearn-clustering-svm-pca |
Version | 863c32cb-b7aa-4e8a-b473-7b0bf7f10258 |
File Name | model-auto-conversion_sklearn_svm_pca_pipeline.pkl |
SHA | 524b05d22f13fa4ce5feaf07b86710b447f0c80a02601be86ee5b6bc748fe7fd |
Status | ready |
Image Path | proxy.replicated.com/proxy/wallaroo/ghcr.io/wallaroolabs/mlflow-deploy:v2023.3.0-main-3509 |
Updated At | 2023-13-Jul 18:10:51 |
model.config().runtime()
'mlflow'
Deploy Pipeline
The model is uploaded and ready for use. We’ll add it as a step in our pipeline, then deploy the pipeline. For this example we’re allocated 0.25 cpu and 4 Gi RAM to the pipeline through the pipeline’s deployment configuration.
deployment_config = DeploymentConfigBuilder() \
.cpus(0.25).memory('1Gi') \
.build()
# clear the pipeline if it was used before
pipeline.undeploy()
pipeline.clear()
pipeline.add_model_step(model)
pipeline.deploy(deployment_config=deployment_config)
pipeline.status()
{'status': 'Running',
'details': [],
'engines': [{'ip': '10.244.9.193',
'name': 'engine-6bbc4c5f5f-xw5nb',
'status': 'Running',
'reason': None,
'details': [],
'pipeline_statuses': {'pipelines': [{'id': 'sklearn-clustering-svm-pca',
'status': 'Running'}]},
'model_statuses': {'models': [{'name': 'sklearn-clustering-svm-pca',
'version': '863c32cb-b7aa-4e8a-b473-7b0bf7f10258',
'sha': '524b05d22f13fa4ce5feaf07b86710b447f0c80a02601be86ee5b6bc748fe7fd',
'status': 'Running'}]}}],
'engine_lbs': [{'ip': '10.244.9.192',
'name': 'engine-lb-584f54c899-r8mxl',
'status': 'Running',
'reason': None,
'details': []}],
'sidekicks': [{'ip': '10.244.9.191',
'name': 'engine-sidekick-sklearn-clustering-svm-pca-270-54cd5fcbb5-c42bx',
'status': 'Running',
'reason': None,
'details': [],
'statuses': '\n'}]}
Inference
SKLearn models must have all of the data as one line to prevent columns from being read out of order when submitting in JSON. The following will take in the data, convert the rows into a single inputs
for the table, then perform the inference. From the output_schema
we have defined the output as predictions
which will be displayed in our inference result output as out.predictions
.
data = pd.read_json('data/test-sklearn-kmeans.json')
display(data)
# move the column values to a single array input
dataframe = pd.DataFrame({"inputs": data[:2].values.tolist()})
display(dataframe)
sepal length (cm) | sepal width (cm) | petal length (cm) | petal width (cm) | |
---|---|---|---|---|
0 | 5.1 | 3.5 | 1.4 | 0.2 |
1 | 4.9 | 3.0 | 1.4 | 0.2 |
inputs | |
---|---|
0 | [5.1, 3.5, 1.4, 0.2] |
1 | [4.9, 3.0, 1.4, 0.2] |
pipeline.infer(dataframe)
time | in.inputs | out.predictions | check_failures | |
---|---|---|---|---|
0 | 2023-07-13 18:11:14.856 | [5.1, 3.5, 1.4, 0.2] | 0 | 0 |
1 | 2023-07-13 18:11:14.856 | [4.9, 3.0, 1.4, 0.2] | 0 | 0 |
Undeploy Pipelines
With the tutorial complete, the pipeline is undeployed to return the resources back to the cluster.
pipeline.undeploy()
name | sklearn-clustering-svm-pca |
---|---|
created | 2023-07-13 18:09:46.085151+00:00 |
last_updated | 2023-07-13 18:10:54.850527+00:00 |
deployed | False |
tags | |
versions | 1cd17197-b4ee-472a-9ccf-b0fb6bd150b8, 8b24f044-7115-451a-8614-eaf4b4068737 |
steps | sklearn-clustering-svm-pca |
2.7 - Wallaroo SDK Upload Tutorials: Tensorflow
The following tutorials cover how to upload sample Tensorflow models.
Parameter | Description |
---|---|
Web Site | https://www.tensorflow.org/ |
Supported Libraries | tensorflow==2.9.1 |
Framework | Framework.TENSORFLOW aka tensorflow |
Runtime | Native aka tensorflow |
Supported File Types | SavedModel format as .zip file |
IMPORTANT NOTE
These requirements are not for Tensorflow Keras models, only for non-Keras Tensorflow models in the SavedModel format. For Tensorflow Keras deployment in Wallaroo, see the Tensorflow Keras requirements.TensorFlow File Format
TensorFlow models are .zip file of the SavedModel format. For example, the Aloha sample TensorFlow model is stored in the directory alohacnnlstm
:
├── saved_model.pb
└── variables
├── variables.data-00000-of-00002
├── variables.data-00001-of-00002
└── variables.index
This is compressed into the .zip file alohacnnlstm.zip
with the following command:
zip -r alohacnnlstm.zip alohacnnlstm/
ML models that meet the Tensorflow and SavedModel format will run as Wallaroo Native runtimes by default.
See the SavedModel guide for full details.
2.7.1 - Wallaroo SDK Upload Tutorial: Tensorflow Aloha
This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.
Wallaroo SDK Upload Tutorial: Tensorflow
In this notebook we will walk through uploading a Tensorflow model to a Wallaroo instance and performing sample inferences. For this example we will be using an open source model that uses an Aloha CNN LSTM model for classifying Domain names as being either legitimate or being used for nefarious purposes such as malware distribution.
Prerequisites
- An installed Wallaroo instance.
- The following Python libraries installed:
Tutorial Goals
For our example, we will perform the following:
- Create a workspace for our work.
- Upload the Aloha model.
- Create a pipeline that can ingest our submitted data, submit it to the model, and export the results.
All sample data and models are available through the Wallaroo Quick Start Guide Samples repository.
Connect to the Wallaroo Instance
The first step is to connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. For more information on Wallaroo Client settings, see the Client Connection guide.e/wallaroo-sdk-essentials-client/).
import wallaroo
from wallaroo.object import EntityNotFoundError
from wallaroo.framework import Framework
# to display dataframe tables
from IPython.display import display
# used to display dataframe information without truncating
import os
os.environ["MODELS_ENABLED"] = "true"
import pandas as pd
pd.set_option('display.max_colwidth', None)
import pyarrow as pa
# Login through local Wallaroo instance
wl = wallaroo.Client()
wallarooPrefix = ""
wallarooSuffix = "autoscale-uat-ee.wallaroo.dev"
wl = wallaroo.Client(api_endpoint=f"https://{wallarooPrefix}api.{wallarooSuffix}",
auth_endpoint=f"https://{wallarooPrefix}keycloak.{wallarooSuffix}",
auth_type="sso")
Create the Workspace
We will create a workspace to work in and call it the “alohaworkspace”, then set it as current workspace environment. We’ll also create our pipeline in advance as alohapipeline
. The model name and the model file will be specified for use in later steps.
To allow this tutorial to be run multiple times or by multiple users in the same Wallaroo instance, a random 4 character prefix will be added to the workspace, pipeline, and model.
import string
import random
# make a random 4 character suffix to verify uniqueness in tutorials
suffix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
workspace_name = f'tensorflowuploadexampleworkspace{suffix}'
pipeline_name = f'tensorflowuploadexample{suffix}'
model_name = f'tensorflowuploadexample{suffix}'
model_file_name = './models/alohacnnlstm.zip'
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
pipeline = wl.build_pipeline(name)
return pipeline
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
aloha_pipeline = get_pipeline(pipeline_name)
aloha_pipeline
name | tensorflowuploadexampletxdg |
---|---|
created | 2023-07-13 18:29:16.306846+00:00 |
last_updated | 2023-07-13 18:29:16.306846+00:00 |
deployed | (none) |
tags | |
versions | 755813ea-cb7f-4e9e-9075-2f5eea0f883f |
steps |
We can verify the workspace is created the current default workspace with the get_current_workspace()
command.
wl.get_current_workspace()
{'name': 'tensorflowuploadexampleworkspacetxdg', 'id': 435, 'archived': False, 'created_by': 'd9a72bd9-2a1c-44dd-989f-3c7c15130885', 'created_at': '2023-07-13T18:29:15.359159+00:00', 'models': [], 'pipelines': [{'name': 'tensorflowuploadexampletxdg', 'create_time': datetime.datetime(2023, 7, 13, 18, 29, 16, 306846, tzinfo=tzutc()), 'definition': '[]'}]}
Upload the Models
Now we will upload our models. Note that for this example we are applying the model from a .ZIP file. The Aloha model is a protobuf file that has been defined for evaluating web pages, and we will configure it to use data in the tensorflow
format.
The following parameters are required for TensorFlow models. Tensorflow models are native runtimes in Wallaroo, so the input_schema
and output_schema
parameters are optional.
Parameter | Type | Description |
---|---|---|
name | string (Required) | The name of the model. Model names are unique per workspace. Models that are uploaded with the same name are assigned as a new version of the model. |
path | string (Required) | The path to the model file being uploaded. |
framework | string (Required) | Set as the Framework.TENSORFLOW . |
input_schema | pyarrow.lib.Schema (Optional) | The input schema in Apache Arrow schema format. |
output_schema | pyarrow.lib.Schema (Optional) | The output schema in Apache Arrow schema format. |
convert_wait | bool (Optional) (Default: True) | Not required for native runtimes.
|
TensorFlow File Format
TensorFlow models are .zip file of the SavedModel format. For example, the Aloha sample TensorFlow model is stored in the directory alohacnnlstm
:
├── saved_model.pb
└── variables
├── variables.data-00000-of-00002
├── variables.data-00001-of-00002
└── variables.index
This is compressed into the .zip file alohacnnlstm.zip
with the following command:
zip -r alohacnnlstm.zip alohacnnlstm/
model = wl.upload_model(model_name, model_file_name, Framework.TENSORFLOW).configure("tensorflow")
Deploy a model
Now that we have a model that we want to use we will create a deployment for it.
We will tell the deployment we are using a tensorflow model and give the deployment name and the configuration we want for the deployment.
aloha_pipeline.add_model_step(model)
name | tensorflowuploadexampletxdg |
---|---|
created | 2023-07-13 18:29:16.306846+00:00 |
last_updated | 2023-07-13 18:29:16.306846+00:00 |
deployed | (none) |
tags | |
versions | 755813ea-cb7f-4e9e-9075-2f5eea0f883f |
steps |
aloha_pipeline.deploy()
name | tensorflowuploadexampletxdg |
---|---|
created | 2023-07-13 18:29:16.306846+00:00 |
last_updated | 2023-07-13 18:29:21.045528+00:00 |
deployed | True |
tags | |
versions | ea2f9aba-a21f-46c1-8dc6-677147af6e1e, 755813ea-cb7f-4e9e-9075-2f5eea0f883f |
steps | tensorflowuploadexampletxdg |
We can verify that the pipeline is running and list what models are associated with it.
aloha_pipeline.status()
{'status': 'Running',
'details': [],
'engines': [{'ip': '10.244.9.216',
'name': 'engine-57fb6bd474-tm9xr',
'status': 'Running',
'reason': None,
'details': [],
'pipeline_statuses': {'pipelines': [{'id': 'tensorflowuploadexampletxdg',
'status': 'Running'}]},
'model_statuses': {'models': [{'name': 'tensorflowuploadexampletxdg',
'version': 'd3e73b1e-31d0-4f27-89d0-a135814c4cfe',
'sha': 'd71d9ffc61aaac58c2b1ed70a2db13d1416fb9d3f5b891e5e4e2e97180fe22f8',
'status': 'Running'}]}}],
'engine_lbs': [{'ip': '10.244.9.217',
'name': 'engine-lb-584f54c899-9bf5d',
'status': 'Running',
'reason': None,
'details': []}],
'sidekicks': []}
Interferences
Infer 1 row
Now that the pipeline is deployed and our Aloha model is in place, we’ll perform a smoke test to verify the pipeline is up and running properly. We’ll use the infer_from_file
command to load a single encoded URL into the inference engine and print the results back out.
The result should tell us that the tokenized URL is legitimate (0) or fraud (1). This sample data should return close to 1 in out.main
.
smoke_test = pd.DataFrame.from_records(
[
{
"text_input":[
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
28,
16,
32,
23,
29,
32,
30,
19,
26,
17
]
}
]
)
result = aloha_pipeline.infer(smoke_test)
display(result.loc[:, ["time","out.main"]])
time | out.main | |
---|---|---|
0 | 2023-07-13 18:29:32.824 | [0.997564] |
Undeploy Pipeline
When finished with our tests, we will undeploy the pipeline so we have the Kubernetes resources back for other tasks. Note that if the deployment variable is unchanged aloha_pipeline.deploy() will restart the inference engine in the same configuration as before.
aloha_pipeline.undeploy()
name | tensorflowuploadexampletxdg |
---|---|
created | 2023-07-13 18:29:16.306846+00:00 |
last_updated | 2023-07-13 18:29:21.045528+00:00 |
deployed | False |
tags | |
versions | ea2f9aba-a21f-46c1-8dc6-677147af6e1e, 755813ea-cb7f-4e9e-9075-2f5eea0f883f |
steps | tensorflowuploadexampletxdg |
2.8 - Wallaroo SDK Upload Tutorials: Tensorflow Keras
The following tutorials cover how to upload sample Tensorflow Keras models.
Parameter | Description |
---|---|
Web Site | https://www.tensorflow.org/api_docs/python/tf/keras/Model |
Supported Libraries |
|
Framework | Framework.KERAS aka keras |
Supported File Types | SavedModel format as .zip file and HDF5 format |
Runtime | Containerized aka mlflow |
TensorFlow Keras SavedModel Format
TensorFlow Keras SavedModel models are .zip file of the SavedModel format. For example, the Aloha sample TensorFlow model is stored in the directory alohacnnlstm
:
├── saved_model.pb
└── variables
├── variables.data-00000-of-00002
├── variables.data-00001-of-00002
└── variables.index
This is compressed into the .zip file alohacnnlstm.zip
with the following command:
zip -r alohacnnlstm.zip alohacnnlstm/
See the SavedModel guide for full details.
TensorFlow Keras H5 Format
Wallaroo supports the H5 for Tensorflow Keras models.
2.8.1 - Wallaroo SDK Upload Tutorial: Keras Sequential Single IO
This tutorial can be downloaded as part of the Wallaroo Tutorials repository.
Wallaroo Model Upload via the Wallaroo SDK: TensorFlow keras Sequential Single IO
The following tutorial demonstrates how to upload a TensorFlow keras Sequential Single IO model to a Wallaroo instance.
Tutorial Goals
Demonstrate the following:
- Upload a TensorFlow keras Sequential Single IO to a Wallaroo instance.
- Create a pipeline and add the model as a pipeline step.
- Perform a sample inference.
Prerequisites
- A Wallaroo version 2023.2.1 or above instance.
References
- Wallaroo MLOps API Essentials Guide: Model Upload and Registrations
- Wallaroo API Connection Guide
- DNS Integration Guide
Tutorial Steps
Import Libraries
The first step is to import the libraries we’ll be using. These are included by default in the Wallaroo instance’s JupyterHub service.
import json
import os
import pickle
import wallaroo
from wallaroo.pipeline import Pipeline
from wallaroo.deployment_config import DeploymentConfigBuilder
from wallaroo.framework import Framework
from wallaroo.object import EntityNotFoundError
import os
os.environ["MODELS_ENABLED"] = "true"
import pyarrow as pa
import numpy as np
import pandas as pd
from sklearn.datasets import load_iris
from sklearn.linear_model import LogisticRegression
import datetime
Open a Connection to Wallaroo
The next step is connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. If logging in externally, update the wallarooPrefix
and wallarooSuffix
variables with the proper DNS information. For more information on Wallaroo DNS settings, see the Wallaroo DNS Integration Guide.
wl = wallaroo.Client()
wallarooPrefix = ""
wallarooSuffix = "autoscale-uat-ee.wallaroo.dev"
wl = wallaroo.Client(api_endpoint=f"https://{wallarooPrefix}api.{wallarooSuffix}",
auth_endpoint=f"https://{wallarooPrefix}keycloak.{wallarooSuffix}",
auth_type="sso")
Set Variables and Helper Functions
We’ll set the name of our workspace, pipeline, models and files. Workspace names must be unique across the Wallaroo workspace. For this, we’ll add in a randomly generated 4 characters to the workspace name to prevent collisions with other users’ workspaces. If running this tutorial, we recommend hard coding the workspace name so it will function in the same workspace each time it’s run.
We’ll set up some helper functions that will either use existing workspaces and pipelines, or create them if they do not already exist.
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
pipeline = wl.build_pipeline(name)
return pipeline
import string
import random
# make a random 4 character suffix to prevent overwriting other user's workspaces
suffix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
workspace_name = f'keras-sequential-single-io{suffix}'
pipeline_name = f'keras-sequential-single-io'
model_name = 'keras-sequential-single-io'
model_file_name = 'models/model-auto-conversion_keras_single_io_keras_sequential_model.h5'
Create Workspace and Pipeline
We will now create the Wallaroo workspace to store our model and set it as the current workspace. Future commands will default to this workspace for pipeline creation, model uploads, etc. We’ll create our Wallaroo pipeline to deploy our model.
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
pipeline = get_pipeline(pipeline_name)
Configure Data Schemas
The following parameters are required for TensorFlow keras models. Note that while some fields are considered as optional for the upload_model
method, they are required for proper uploading of a TensorFlow Keras model to Wallaroo.
Parameter | Type | Description |
---|---|---|
name | string (Required) | The name of the model. Model names are unique per workspace. Models that are uploaded with the same name are assigned as a new version of the model. |
path | string (Required) | The path to the model file being uploaded. |
framework | string (Upload Method Optional, TensorFlow keras model Required) | Set as the Framework.KERAS . |
input_schema | pyarrow.lib.Schema (Upload Method Optional, TensorFlow Keras model Required) | The input schema in Apache Arrow schema format. |
output_schema | pyarrow.lib.Schema (Upload Method Optional, TensorFlow Keras model Required) | The output schema in Apache Arrow schema format. |
convert_wait | bool (Upload Method Optional, TensorFlow model Optional) (Default: True) |
|
Once the upload process starts, the model is containerized by the Wallaroo instance. This process may take up to 10 minutes.
input_schema = pa.schema([
pa.field('input', pa.list_(pa.float64(), list_size=10))
])
output_schema = pa.schema([
pa.field('output', pa.list_(pa.float64(), list_size=32))
])
Upload Model
The model will be uploaded with the framework set as Framework.KERAS
.
framework=Framework.KERAS
model = wl.upload_model(model_name,
model_file_name,
framework=framework,
input_schema=input_schema,
output_schema=output_schema)
model
Waiting for model conversion... It may take up to 10.0min.
Model is Pending conversion.Converting..Pending conversion.Converting.............Ready.
Name | keras-sequential-single-io |
Version | 79e34aa4-ce71-4c3a-90fc-79bfa0a40052 |
File Name | model-auto-conversion_keras_single_io_keras_sequential_model.h5 |
SHA | f7e49538e38bebe066ce8df97bac8be239ae8c7d2733e500c8cd633706ae95a8 |
Status | ready |
Image Path | proxy.replicated.com/proxy/wallaroo/ghcr.io/wallaroolabs/mlflow-deploy:v2023.3.0-main-3509 |
Updated At | 2023-13-Jul 17:50:07 |
model.config().runtime()
'mlflow'
Deploy Pipeline
The model is uploaded and ready for use. We’ll add it as a step in our pipeline, then deploy the pipeline. For this example we’re allocated 0.25 cpu and 4 Gi RAM to the pipeline through the pipeline’s deployment configuration.
deployment_config = DeploymentConfigBuilder() \
.cpus(0.25).memory('1Gi') \
.build()
# clear the pipeline if used in a previous tutorial
pipeline.undeploy()
pipeline.clear()
pipeline.add_model_step(model)
pipeline.deploy(deployment_config=deployment_config)
pipeline.status()
{'status': 'Running',
'details': [],
'engines': [{'ip': '10.244.9.178',
'name': 'engine-75cb64bc94-lc74f',
'status': 'Running',
'reason': None,
'details': [],
'pipeline_statuses': {'pipelines': [{'id': 'keras-sequential-single-io',
'status': 'Running'}]},
'model_statuses': {'models': [{'name': 'keras-sequential-single-io',
'version': '79e34aa4-ce71-4c3a-90fc-79bfa0a40052',
'sha': 'f7e49538e38bebe066ce8df97bac8be239ae8c7d2733e500c8cd633706ae95a8',
'status': 'Running'}]}}],
'engine_lbs': [{'ip': '10.244.9.179',
'name': 'engine-lb-584f54c899-96vb6',
'status': 'Running',
'reason': None,
'details': []}],
'sidekicks': [{'ip': '10.244.9.180',
'name': 'engine-sidekick-keras-sequential-single-io-268-84cff895cf-bm2tl',
'status': 'Running',
'reason': None,
'details': [],
'statuses': '\n'}]}
Run Inference
A sample inference will be run. First the pandas DataFrame used for the inference is created, then the inference run through the pipeline’s infer
method.
input_data = np.random.rand(10, 10)
mock_dataframe = pd.DataFrame({
"input": input_data.tolist()
})
mock_dataframe
input | |
---|---|
0 | [0.5809853116232783, 0.14701285145269583, 0.58... |
1 | [0.7257919427827948, 0.7589800713851653, 0.297... |
2 | [0.3764542634917982, 0.5494748793973108, 0.485... |
3 | [0.5025570851921953, 0.8837007217828465, 0.406... |
4 | [0.0866396068940275, 0.10670979528669655, 0.09... |
5 | [0.8860315511881905, 0.6706861365257704, 0.412... |
6 | [0.37994954016981175, 0.7429705751348403, 0.12... |
7 | [0.49027013691203447, 0.7105289734919781, 0.99... |
8 | [0.4446469438043267, 0.09139454740740094, 0.24... |
9 | [0.932824358657356, 0.3388034065847041, 0.0416... |
pipeline.infer(mock_dataframe)
time | in.input | out.output | check_failures | |
---|---|---|---|---|
0 | 2023-07-13 17:50:42.609 | [0.5809853116, 0.1470128515, 0.5859677386, 0.2... | [0.025315184146165848, 0.023196307942271233, 0... | 0 |
1 | 2023-07-13 17:50:42.609 | [0.7257919428, 0.7589800714, 0.297258173, 0.39... | [0.022579584270715714, 0.026824792847037315, 0... | 0 |
2 | 2023-07-13 17:50:42.609 | [0.3764542635, 0.5494748794, 0.4852001553, 0.8... | [0.02744304947555065, 0.03327963128685951, 0.0... | 0 |
3 | 2023-07-13 17:50:42.609 | [0.5025570852, 0.8837007218, 0.4064710644, 0.5... | [0.03851581737399101, 0.021599330008029938, 0.... | 0 |
4 | 2023-07-13 17:50:42.609 | [0.0866396069, 0.1067097953, 0.0918865633, 0.2... | [0.020835522562265396, 0.034067943692207336, 0... | 0 |
5 | 2023-07-13 17:50:42.609 | [0.8860315512, 0.6706861365, 0.4123840879, 0.2... | [0.034137945622205734, 0.01922944001853466, 0.... | 0 |
6 | 2023-07-13 17:50:42.609 | [0.3799495402, 0.7429705751, 0.1207460912, 0.3... | [0.03986137732863426, 0.019290560856461525, 0.... | 0 |
7 | 2023-07-13 17:50:42.609 | [0.4902701369, 0.7105289735, 0.9948842471, 0.2... | [0.026285773143172264, 0.02646280638873577, 0.... | 0 |
8 | 2023-07-13 17:50:42.609 | [0.4446469438, 0.0913945474, 0.24660973, 0.456... | [0.023244783282279968, 0.033836156129837036, 0... | 0 |
9 | 2023-07-13 17:50:42.609 | [0.9328243587, 0.3388034066, 0.0416730168, 0.4... | [0.02200852520763874, 0.027223799377679825, 0.... | 0 |
Undeploy Pipelines
With the tutorial complete, the pipeline is undeployed to return the resources back to the cluster.
pipeline.undeploy()
name | keras-sequential-single-io |
---|---|
created | 2023-07-13 17:50:13.553180+00:00 |
last_updated | 2023-07-13 17:50:13.553180+00:00 |
deployed | False |
tags | |
versions | e04712da-19d6-40d1-88d3-2dab8ab950e1 |
steps | keras-sequential-single-io |
2.9 - Wallaroo SDK Upload Tutorials: XGBoost
The following tutorials cover how to upload sample XGBoost models.
Parameter | Description |
---|---|
Web Site | https://xgboost.ai/ |
Supported Libraries | xgboost==1.7.4 |
Framework | Framework.XGBOOST aka xgboost |
Supported File Types | pickle (XGB files are not supported.) |
Runtime | Containerized aka tensorflow / mlflow |
XGBoost Schema Inputs
XGBoost schema follows a different format than other models. To prevent inputs from being out of order, the inputs should be submitted in a single row in the order the model is trained to accept, with all of the data types being the same. If a model is originally trained to accept inputs of different data types, it will need to be retrained to only accept one data type for each column - typically pa.float64()
is a good choice.
For example, the following DataFrame has 4 columns, each column a float
.
 | sepal length (cm) | sepal width (cm) | petal length (cm) | petal width (cm) |
---|---|---|---|---|
0 | 5.1 | 3.5 | 1.4 | 0.2 |
1 | 4.9 | 3.0 | 1.4 | 0.2 |
For submission to an XGBoost model, the data input schema will be a single array with 4 float values.
input_schema = pa.schema([
pa.field('inputs', pa.list_(pa.float64(), list_size=4))
])
When submitting as an inference, the DataFrame is converted to rows with the column data expressed as a single array. The data must be in the same order as the model expects, which is why the data is submitted as a single array rather than JSON labeled columns: this insures that the data is submitted in the exact order as the model is trained to accept.
Original DataFrame:
 | sepal length (cm) | sepal width (cm) | petal length (cm) | petal width (cm) |
---|---|---|---|---|
0 | 5.1 | 3.5 | 1.4 | 0.2 |
1 | 4.9 | 3.0 | 1.4 | 0.2 |
Converted DataFrame:
 | inputs |
---|---|
0 | [5.1, 3.5, 1.4, 0.2] |
1 | [4.9, 3.0, 1.4, 0.2] |
XGBoost Schema Outputs
Outputs for XGBoost are labeled based on the trained model outputs. For this example, the output is simply a single output listed as output
. In the Wallaroo inference result, it is grouped with the metadata out
as out.output
.
output_schema = pa.schema([
pa.field('output', pa.int32())
])
pipeline.infer(dataframe)
 | time | in.inputs | out.output | check_failures |
---|---|---|---|---|
0 | 2023-07-05 15:11:29.776 | [5.1, 3.5, 1.4, 0.2] | 0 | 0 |
1 | 2023-07-05 15:11:29.776 | [4.9, 3.0, 1.4, 0.2] | 0 | 0 |
2.9.1 - Wallaroo SDK Upload Tutorial: RF Regressor Tutorial
This tutorial can be downloaded as part of the Wallaroo Tutorials repository.
Wallaroo Model Upload via the Wallaroo SDK: XGBoost RF Regressor
The following tutorial demonstrates how to upload a XGBoost RF Regressor model to a Wallaroo instance.
Tutorial Goals
Demonstrate the following:
- Upload a XGBoost RF Regressor model to a Wallaroo instance.
- Create a pipeline and add the model as a pipeline step.
- Perform a sample inference.
Prerequisites
- A Wallaroo version 2023.2.1 or above instance.
References
- Wallaroo MLOps API Essentials Guide: Model Upload and Registrations
- Wallaroo API Connection Guide
- DNS Integration Guide
Tutorial Steps
Import Libraries
The first step is to import the libraries we’ll be using. These are included by default in the Wallaroo instance’s JupyterHub service.
import json
import os
import pickle
import wallaroo
from wallaroo.pipeline import Pipeline
from wallaroo.deployment_config import DeploymentConfigBuilder
from wallaroo.object import EntityNotFoundError
from wallaroo.framework import Framework
import os
os.environ["MODELS_ENABLED"] = "true"
import pyarrow as pa
import numpy as np
import pandas as pd
Open a Connection to Wallaroo
The next step is connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. If logging in externally, update the wallarooPrefix
and wallarooSuffix
variables with the proper DNS information. For more information on Wallaroo DNS settings, see the Wallaroo DNS Integration Guide.
wl = wallaroo.Client()
wallarooPrefix = ""
wallarooSuffix = "autoscale-uat-ee.wallaroo.dev"
wl = wallaroo.Client(api_endpoint=f"https://{wallarooPrefix}api.{wallarooSuffix}",
auth_endpoint=f"https://{wallarooPrefix}keycloak.{wallarooSuffix}",
auth_type="sso")
Set Variables and Helper Functions
We’ll set the name of our workspace, pipeline, models and files. Workspace names must be unique across the Wallaroo workspace. For this, we’ll add in a randomly generated 4 characters to the workspace name to prevent collisions with other users’ workspaces. If running this tutorial, we recommend hard coding the workspace name so it will function in the same workspace each time it’s run.
We’ll set up some helper functions that will either use existing workspaces and pipelines, or create them if they do not already exist.
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
pipeline = wl.build_pipeline(name)
return pipeline
import string
import random
# make a random 4 character suffix to prevent overwriting other user's workspaces
suffix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
workspace_name = f'xgboost-rf-regressor{suffix}'
pipeline_name = f'xgboost-rf-regressor'
model_name = 'xgboost-rf-regressor'
model_file_name = 'models/model-auto-conversion_xgboost_xgb_rf_regressor_diabetes.pkl'
Create Workspace and Pipeline
We will now create the Wallaroo workspace to store our model and set it as the current workspace. Future commands will default to this workspace for pipeline creation, model uploads, etc. We’ll create our Wallaroo pipeline to deploy our model.
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
pipeline = get_pipeline(pipeline_name)
Configure Data Schemas
XGBoost models are uploaded to Wallaroo through the Wallaroo Client upload_model
method.
Upload XGBoost Model Parameters
The following parameters are required for XGBoost models. Note that while some fields are considered as optional for the upload_model
method, they are required for proper uploading of a XGBoost model to Wallaroo.
Parameter | Type | Description |
---|---|---|
name | string (Required) | The name of the model. Model names are unique per workspace. Models that are uploaded with the same name are assigned as a new version of the model. |
path | string (Required) | The path to the model file being uploaded. |
framework | string (Upload Method Optional, SKLearn model Required) | Set as the Framework.XGBOOST . |
input_schema | pyarrow.lib.Schema (Upload Method Optional, SKLearn model Required) | The input schema in Apache Arrow schema format. |
output_schema | pyarrow.lib.Schema (Upload Method Optional, SKLearn model Required) | The output schema in Apache Arrow schema format. |
convert_wait | bool (Upload Method Optional, SKLearn model Optional) (Default: True) |
|
Once the upload process starts, the model is containerized by the Wallaroo instance. This process may take up to 10 minutes.
XGBoost Schema Inputs
XGBoost schema follows a different format than other models. To prevent inputs from being out of order, the inputs should be submitted in a single row in the order the model is trained to accept, with all of the data types being the same. If a model is originally trained to accept inputs of different data types, it will need to be retrained to only accept one data type for each column - typically pa.float64()
is a good choice.
For example, the following DataFrame has 4 columns, each column a float
.
 | sepal length (cm) | sepal width (cm) | petal length (cm) | petal width (cm) |
---|---|---|---|---|
0 | 5.1 | 3.5 | 1.4 | 0.2 |
1 | 4.9 | 3.0 | 1.4 | 0.2 |
For submission to an XGBoost model, the data input schema will be a single array with 4 float values.
input_schema = pa.schema([
pa.field('inputs', pa.list_(pa.float64(), list_size=4))
])
When submitting as an inference, the DataFrame is converted to rows with the column data expressed as a single array. The data must be in the same order as the model expects, which is why the data is submitted as a single array rather than JSON labeled columns: this insures that the data is submitted in the exact order as the model is trained to accept.
Original DataFrame:
 | sepal length (cm) | sepal width (cm) | petal length (cm) | petal width (cm) |
---|---|---|---|---|
0 | 5.1 | 3.5 | 1.4 | 0.2 |
1 | 4.9 | 3.0 | 1.4 | 0.2 |
Converted DataFrame:
 | inputs |
---|---|
0 | [5.1, 3.5, 1.4, 0.2] |
1 | [4.9, 3.0, 1.4, 0.2] |
XGBoost Schema Outputs
Outputs for XGBoost are labeled based on the trained model outputs. For this example, the output is simply a single output listed as output
. In the Wallaroo inference result, it is grouped with the metadata out
as out.output
.
output_schema = pa.schema([
pa.field('output', pa.int32())
])
pipeline.infer(dataframe)
 | time | in.inputs | out.output | check_failures |
---|---|---|---|---|
0 | 2023-07-05 15:11:29.776 | [5.1, 3.5, 1.4, 0.2] | 0 | 0 |
1 | 2023-07-05 15:11:29.776 | [4.9, 3.0, 1.4, 0.2] | 0 | 0 |
input_schema = pa.schema([
pa.field('inputs', pa.list_(pa.float64(), list_size=10))
])
output_schema = pa.schema([
pa.field('output', pa.float64())
])
Upload Model
The model will be uploaded with the framework set as Framework.XGBOOST
.
model = wl.upload_model(model_name,
model_file_name,
framework=Framework.XGBOOST,
input_schema=input_schema,
output_schema=output_schema)
model
Waiting for model conversion... It may take up to 10.0min.
Model is Pending conversion.Converting..Pending conversion.Converting........Ready.
Name | xgboost-rf-regressor |
Version | 18786ea1-2964-4406-ad1b-2eea413326d8 |
File Name | model-auto-conversion_xgboost_xgb_rf_regressor_diabetes.pkl |
SHA | 461341d78d54a9bfc8e4faa94be6037aef15217974ba59bad92d31ef48e6bd99 |
Status | ready |
Image Path | proxy.replicated.com/proxy/wallaroo/ghcr.io/wallaroolabs/mlflow-deploy:v2023.3.0-main-3509 |
Updated At | 2023-13-Jul 20:02:19 |
model.config().runtime()
'mlflow'
Deploy Pipeline
The model is uploaded and ready for use. We’ll add it as a step in our pipeline, then deploy the pipeline. For this example we’re allocated 0.25 cpu and 4 Gi RAM to the pipeline through the pipeline’s deployment configuration.
deployment_config = DeploymentConfigBuilder() \
.cpus(0.25).memory('1Gi') \
.build()
# clear the pipeline if it was used before
pipeline.undeploy()
pipeline.clear()
pipeline.add_model_step(model)
pipeline.deploy(deployment_config=deployment_config)
pipeline.status()
{'status': 'Running',
'details': [],
'engines': [{'ip': '10.244.9.3',
'name': 'engine-68746578dc-zh47j',
'status': 'Running',
'reason': None,
'details': [],
'pipeline_statuses': {'pipelines': [{'id': 'xgboost-rf-regressor',
'status': 'Running'}]},
'model_statuses': {'models': [{'name': 'xgboost-rf-regressor',
'version': '18786ea1-2964-4406-ad1b-2eea413326d8',
'sha': '461341d78d54a9bfc8e4faa94be6037aef15217974ba59bad92d31ef48e6bd99',
'status': 'Running'}]}}],
'engine_lbs': [{'ip': '10.244.9.5',
'name': 'engine-lb-584f54c899-kxfbn',
'status': 'Running',
'reason': None,
'details': []}],
'sidekicks': [{'ip': '10.244.9.6',
'name': 'engine-sidekick-xgboost-rf-regressor-288-cfcc58bb4-b47cl',
'status': 'Running',
'reason': None,
'details': [],
'statuses': '\n'}]}
Run Inference
A sample inference will be run. First the pandas DataFrame used for the inference is created, then the inference run through the pipeline’s infer
method.
data = pd.read_json('./data/test_xgb_rf-regressor.json')
display(data)
dataframe = pd.DataFrame({"inputs": data[:2].values.tolist()})
display(dataframe)
pipeline.infer(dataframe)
age | sex | bmi | bp | s1 | s2 | s3 | s4 | s5 | s6 | |
---|---|---|---|---|---|---|---|---|---|---|
0 | 0.038076 | 0.050680 | 0.061696 | 0.021872 | -0.044223 | -0.034821 | -0.043401 | -0.002592 | 0.019907 | -0.017646 |
1 | -0.001882 | -0.044642 | -0.051474 | -0.026328 | -0.008449 | -0.019163 | 0.074412 | -0.039493 | -0.068332 | -0.092204 |
inputs | |
---|---|
0 | [0.0380759064, 0.0506801187, 0.0616962065, 0.0... |
1 | [-0.0018820165, -0.0446416365, -0.051474061200... |
time | in.inputs | out.output | check_failures | |
---|---|---|---|---|
0 | 2023-07-13 20:02:41.843 | [0.0380759064, 0.0506801187, 0.0616962065, 0.0... | 166.618774 | 0 |
1 | 2023-07-13 20:02:41.843 | [-0.0018820165, -0.0446416365, -0.0514740612, ... | 76.189583 | 0 |
Undeploy Pipelines
With the tutorial complete, the pipeline is undeployed to return the resources back to the cluster.
pipeline.undeploy()
name | xgboost-rf-regressor |
---|---|
created | 2023-07-13 20:01:14.889724+00:00 |
last_updated | 2023-07-13 20:02:25.945653+00:00 |
deployed | False |
tags | |
versions | c9d392fa-96d7-4709-810f-beae105c879f, cb4f88e6-2c8c-4fef-beb5-5718bc64a2ea |
steps | xgboost-rf-regressor |
2.9.2 - Wallaroo SDK Upload Tutorial: XGBoost Classification
This tutorial can be downloaded as part of the Wallaroo Tutorials repository.
Wallaroo Model Upload via the Wallaroo SDK: XGBoost Classification
The following tutorial demonstrates how to upload a XGBoost Classification model to a Wallaroo instance.
Tutorial Goals
Demonstrate the following:
- Upload a XGBoost Classification model to a Wallaroo instance.
- Create a pipeline and add the model as a pipeline step.
- Perform a sample inference.
Prerequisites
- A Wallaroo version 2023.2.1 or above instance.
References
- Wallaroo MLOps API Essentials Guide: Model Upload and Registrations
- Wallaroo API Connection Guide
- DNS Integration Guide
Tutorial Steps
Import Libraries
The first step is to import the libraries we’ll be using. These are included by default in the Wallaroo instance’s JupyterHub service.
import json
import os
import pickle
import wallaroo
from wallaroo.pipeline import Pipeline
from wallaroo.deployment_config import DeploymentConfigBuilder
from wallaroo.object import EntityNotFoundError
from wallaroo.framework import Framework
import os
os.environ["MODELS_ENABLED"] = "true"
import pyarrow as pa
import numpy as np
import pandas as pd
Open a Connection to Wallaroo
The next step is connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. If logging in externally, update the wallarooPrefix
and wallarooSuffix
variables with the proper DNS information. For more information on Wallaroo DNS settings, see the Wallaroo DNS Integration Guide.
wl = wallaroo.Client()
Set Variables and Helper Functions
We’ll set the name of our workspace, pipeline, models and files. Workspace names must be unique across the Wallaroo workspace. For this, we’ll add in a randomly generated 4 characters to the workspace name to prevent collisions with other users’ workspaces. If running this tutorial, we recommend hard coding the workspace name so it will function in the same workspace each time it’s run.
We’ll set up some helper functions that will either use existing workspaces and pipelines, or create them if they do not already exist.
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
pipeline = wl.build_pipeline(name)
return pipeline
import string
import random
# make a random 4 character suffix to prevent overwriting other user's workspaces
suffix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
workspace_name = f'xgboost-classification{suffix}'
pipeline_name = f'xgboost-classification'
model_name = 'xgboost-classification'
model_file_name = './models/model-auto-conversion_xgboost_xgb_classification_iris.pkl'
Create Workspace and Pipeline
We will now create the Wallaroo workspace to store our model and set it as the current workspace. Future commands will default to this workspace for pipeline creation, model uploads, etc. We’ll create our Wallaroo pipeline to deploy our model.
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
pipeline = get_pipeline(pipeline_name)
Configure Data Schemas
XGBoost models are uploaded to Wallaroo through the Wallaroo Client upload_model
method.
Upload XGBoost Model Parameters
The following parameters are required for XGBoost models. Note that while some fields are considered as optional for the upload_model
method, they are required for proper uploading of a XGBoost model to Wallaroo.
Parameter | Type | Description |
---|---|---|
name | string (Required) | The name of the model. Model names are unique per workspace. Models that are uploaded with the same name are assigned as a new version of the model. |
path | string (Required) | The path to the model file being uploaded. |
framework | string (Upload Method Optional, SKLearn model Required) | Set as the Framework.XGBOOST . |
input_schema | pyarrow.lib.Schema (Upload Method Optional, SKLearn model Required) | The input schema in Apache Arrow schema format. |
output_schema | pyarrow.lib.Schema (Upload Method Optional, SKLearn model Required) | The output schema in Apache Arrow schema format. |
convert_wait | bool (Upload Method Optional, SKLearn model Optional) (Default: True) |
|
Once the upload process starts, the model is containerized by the Wallaroo instance. This process may take up to 10 minutes.
XGBoost Schema Inputs
XGBoost schema follows a different format than other models. To prevent inputs from being out of order, the inputs should be submitted in a single row in the order the model is trained to accept, with all of the data types being the same. If a model is originally trained to accept inputs of different data types, it will need to be retrained to only accept one data type for each column - typically pa.float64()
is a good choice.
For example, the following DataFrame has 4 columns, each column a float
.
 | sepal length (cm) | sepal width (cm) | petal length (cm) | petal width (cm) |
---|---|---|---|---|
0 | 5.1 | 3.5 | 1.4 | 0.2 |
1 | 4.9 | 3.0 | 1.4 | 0.2 |
For submission to an XGBoost model, the data input schema will be a single array with 4 float values.
input_schema = pa.schema([
pa.field('inputs', pa.list_(pa.float64(), list_size=4))
])
When submitting as an inference, the DataFrame is converted to rows with the column data expressed as a single array. The data must be in the same order as the model expects, which is why the data is submitted as a single array rather than JSON labeled columns: this insures that the data is submitted in the exact order as the model is trained to accept.
Original DataFrame:
 | sepal length (cm) | sepal width (cm) | petal length (cm) | petal width (cm) |
---|---|---|---|---|
0 | 5.1 | 3.5 | 1.4 | 0.2 |
1 | 4.9 | 3.0 | 1.4 | 0.2 |
Converted DataFrame:
 | inputs |
---|---|
0 | [5.1, 3.5, 1.4, 0.2] |
1 | [4.9, 3.0, 1.4, 0.2] |
XGBoost Schema Outputs
Outputs for XGBoost are labeled based on the trained model outputs. For this example, the output is simply a single output listed as output
. In the Wallaroo inference result, it is grouped with the metadata out
as out.output
.
output_schema = pa.schema([
pa.field('output', pa.int32())
])
pipeline.infer(dataframe)
 | time | in.inputs | out.output | check_failures |
---|---|---|---|---|
0 | 2023-07-05 15:11:29.776 | [5.1, 3.5, 1.4, 0.2] | 0 | 0 |
1 | 2023-07-05 15:11:29.776 | [4.9, 3.0, 1.4, 0.2] | 0 | 0 |
input_schema = pa.schema([
pa.field('inputs', pa.list_(pa.float64(), list_size=4))
])
output_schema = pa.schema([
pa.field('output', pa.float64())
])
Upload Model
The model will be uploaded with the framework set as Framework.XGBOOST
.
model = wl.upload_model(model_name,
model_file_name,
framework=Framework.XGBOOST,
input_schema=input_schema,
output_schema=output_schema)
model
Waiting for model conversion... It may take up to 10.0min.
Model is Pending conversion........Converting.............Ready.
Name | xgboost-classification |
Version | 35b2bba5-cfe5-46f0-8a17-242e022fa9d1 |
File Name | model-auto-conversion_xgboost_xgb_classification_iris.pkl |
SHA | 4a1844c460e8c8503207305fb807e3a28e788062588925021807c54ee80cc7f9 |
Status | ready |
Image Path | proxy.replicated.com/proxy/wallaroo/ghcr.io/wallaroolabs/mlflow-deploy:v2023.3.0-main-3509 |
Updated At | 2023-13-Jul 19:29:33 |
model.config().runtime()
'mlflow'
Deploy Pipeline
The model is uploaded and ready for use. We’ll add it as a step in our pipeline, then deploy the pipeline. For this example we’re allocated 0.25 cpu and 4 Gi RAM to the pipeline through the pipeline’s deployment configuration.
deployment_config = DeploymentConfigBuilder() \
.cpus(0.25).memory('1Gi') \
.build()
# clear the pipeline if it was used before
pipeline.undeploy()
pipeline.clear()
pipeline.add_model_step(model)
pipeline.deploy(deployment_config=deployment_config)
pipeline.status()
{'status': 'Running',
'details': [],
'engines': [{'ip': '10.244.9.234',
'name': 'engine-cf56c9c99-l76rt',
'status': 'Running',
'reason': None,
'details': [],
'pipeline_statuses': {'pipelines': [{'id': 'xgboost-classification',
'status': 'Running'}]},
'model_statuses': {'models': [{'name': 'xgboost-classification',
'version': '35b2bba5-cfe5-46f0-8a17-242e022fa9d1',
'sha': '4a1844c460e8c8503207305fb807e3a28e788062588925021807c54ee80cc7f9',
'status': 'Running'}]}}],
'engine_lbs': [{'ip': '10.244.9.235',
'name': 'engine-lb-584f54c899-c29jw',
'status': 'Running',
'reason': None,
'details': []}],
'sidekicks': [{'ip': '10.244.9.233',
'name': 'engine-sidekick-xgboost-classification-284-5fddbdc4c8-6vttd',
'status': 'Running',
'reason': None,
'details': [],
'statuses': '\n'}]}
Run Inference
A sample inference will be run. First the pandas DataFrame used for the inference is created, then the inference run through the pipeline’s infer
method.
data = pd.read_json('data/test-xgboost-classification-data.json')
display(data)
dataframe = pd.DataFrame({"inputs": data[:2].values.tolist()})
display(dataframe)
results = pipeline.infer(dataframe)
display(results)
sepal length (cm) | sepal width (cm) | petal length (cm) | petal width (cm) | |
---|---|---|---|---|
0 | 5.1 | 3.5 | 1.4 | 0.2 |
1 | 4.9 | 3.0 | 1.4 | 0.2 |
inputs | |
---|---|
0 | [5.1, 3.5, 1.4, 0.2] |
1 | [4.9, 3.0, 1.4, 0.2] |
time | in.inputs | out.output | check_failures | |
---|---|---|---|---|
0 | 2023-07-13 19:29:51.614 | [5.1, 3.5, 1.4, 0.2] | 0.0 | 0 |
1 | 2023-07-13 19:29:51.614 | [4.9, 3.0, 1.4, 0.2] | 0.0 | 0 |
Undeploy Pipelines
With the tutorial complete, the pipeline is undeployed to return the resources back to the cluster.
pipeline.undeploy()
name | xgboost-classification |
---|---|
created | 2023-07-13 19:27:34.750372+00:00 |
last_updated | 2023-07-13 19:29:35.910554+00:00 |
deployed | False |
tags | |
versions | 4d693511-388f-48a5-b0d1-e5aa5b11538a, 20e79a32-0739-413a-84c3-6695adc901ec |
steps | xgboost-classification |
2.9.3 - Wallaroo SDK Upload Tutorial: XGBoost Regressor
This tutorial can be downloaded as part of the Wallaroo Tutorials repository.
Wallaroo Model Upload via the Wallaroo SDK: XGBoost Regressor
The following tutorial demonstrates how to upload a XGBoost Regressor model to a Wallaroo instance.
Tutorial Goals
Demonstrate the following:
- Upload a XGBoost Regressor model to a Wallaroo instance.
- Create a pipeline and add the model as a pipeline step.
- Perform a sample inference.
Prerequisites
- A Wallaroo version 2023.2.1 or above instance.
References
- Wallaroo MLOps API Essentials Guide: Model Upload and Registrations
- Wallaroo API Connection Guide
- DNS Integration Guide
Tutorial Steps
Import Libraries
The first step is to import the libraries we’ll be using. These are included by default in the Wallaroo instance’s JupyterHub service.
import json
import os
import pickle
import wallaroo
from wallaroo.pipeline import Pipeline
from wallaroo.deployment_config import DeploymentConfigBuilder
from wallaroo.object import EntityNotFoundError
from wallaroo.framework import Framework
import os
os.environ["MODELS_ENABLED"] = "true"
import pyarrow as pa
import numpy as np
import pandas as pd
Open a Connection to Wallaroo
The next step is connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. If logging in externally, update the wallarooPrefix
and wallarooSuffix
variables with the proper DNS information. For more information on Wallaroo DNS settings, see the Wallaroo DNS Integration Guide.
wl = wallaroo.Client()
wl = wallaroo.Client()
wallarooPrefix = ""
wallarooSuffix = "autoscale-uat-ee.wallaroo.dev"
wl = wallaroo.Client(api_endpoint=f"https://{wallarooPrefix}api.{wallarooSuffix}",
auth_endpoint=f"https://{wallarooPrefix}keycloak.{wallarooSuffix}",
auth_type="sso")
Set Variables and Helper Functions
We’ll set the name of our workspace, pipeline, models and files. Workspace names must be unique across the Wallaroo workspace. For this, we’ll add in a randomly generated 4 characters to the workspace name to prevent collisions with other users’ workspaces. If running this tutorial, we recommend hard coding the workspace name so it will function in the same workspace each time it’s run.
We’ll set up some helper functions that will either use existing workspaces and pipelines, or create them if they do not already exist.
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
pipeline = wl.build_pipeline(name)
return pipeline
import string
import random
# make a random 4 character suffix to prevent overwriting other user's workspaces
suffix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
workspace_name = f'xgboost-regressor{suffix}'
pipeline_name = f'xgboost-regressor'
model_name = 'xgboost-regressor'
model_file_name = 'models/model-auto-conversion_xgboost_xgb_regressor_diabetes.pkl'
Create Workspace and Pipeline
We will now create the Wallaroo workspace to store our model and set it as the current workspace. Future commands will default to this workspace for pipeline creation, model uploads, etc. We’ll create our Wallaroo pipeline to deploy our model.
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
pipeline = get_pipeline(pipeline_name)
Configure Data Schemas
XGBoost models are uploaded to Wallaroo through the Wallaroo Client upload_model
method.
Upload XGBoost Model Parameters
The following parameters are required for XGBoost models. Note that while some fields are considered as optional for the upload_model
method, they are required for proper uploading of a XGBoost model to Wallaroo.
Parameter | Type | Description |
---|---|---|
name | string (Required) | The name of the model. Model names are unique per workspace. Models that are uploaded with the same name are assigned as a new version of the model. |
path | string (Required) | The path to the model file being uploaded. |
framework | string (Upload Method Optional, SKLearn model Required) | Set as the Framework.XGBOOST . |
input_schema | pyarrow.lib.Schema (Upload Method Optional, SKLearn model Required) | The input schema in Apache Arrow schema format. |
output_schema | pyarrow.lib.Schema (Upload Method Optional, SKLearn model Required) | The output schema in Apache Arrow schema format. |
convert_wait | bool (Upload Method Optional, SKLearn model Optional) (Default: True) |
|
Once the upload process starts, the model is containerized by the Wallaroo instance. This process may take up to 10 minutes.
XGBoost Schema Inputs
XGBoost schema follows a different format than other models. To prevent inputs from being out of order, the inputs should be submitted in a single row in the order the model is trained to accept, with all of the data types being the same. If a model is originally trained to accept inputs of different data types, it will need to be retrained to only accept one data type for each column - typically pa.float64()
is a good choice.
For example, the following DataFrame has 4 columns, each column a float
.
 | sepal length (cm) | sepal width (cm) | petal length (cm) | petal width (cm) |
---|---|---|---|---|
0 | 5.1 | 3.5 | 1.4 | 0.2 |
1 | 4.9 | 3.0 | 1.4 | 0.2 |
For submission to an XGBoost model, the data input schema will be a single array with 4 float values.
input_schema = pa.schema([
pa.field('inputs', pa.list_(pa.float64(), list_size=4))
])
When submitting as an inference, the DataFrame is converted to rows with the column data expressed as a single array. The data must be in the same order as the model expects, which is why the data is submitted as a single array rather than JSON labeled columns: this insures that the data is submitted in the exact order as the model is trained to accept.
Original DataFrame:
 | sepal length (cm) | sepal width (cm) | petal length (cm) | petal width (cm) |
---|---|---|---|---|
0 | 5.1 | 3.5 | 1.4 | 0.2 |
1 | 4.9 | 3.0 | 1.4 | 0.2 |
Converted DataFrame:
 | inputs |
---|---|
0 | [5.1, 3.5, 1.4, 0.2] |
1 | [4.9, 3.0, 1.4, 0.2] |
XGBoost Schema Outputs
Outputs for XGBoost are labeled based on the trained model outputs. For this example, the output is simply a single output listed as output
. In the Wallaroo inference result, it is grouped with the metadata out
as out.output
.
output_schema = pa.schema([
pa.field('output', pa.int32())
])
pipeline.infer(dataframe)
 | time | in.inputs | out.output | check_failures |
---|---|---|---|---|
0 | 2023-07-05 15:11:29.776 | [5.1, 3.5, 1.4, 0.2] | 0 | 0 |
1 | 2023-07-05 15:11:29.776 | [4.9, 3.0, 1.4, 0.2] | 0 | 0 |
input_schema = pa.schema([
pa.field('inputs', pa.list_(pa.float64(), list_size=10))
])
output_schema = pa.schema([
pa.field('output', pa.float64())
])
Upload Model
The model will be uploaded with the framework set as Framework.XGBOOST
.
model = wl.upload_model(model_name,
model_file_name,
framework=Framework.XGBOOST,
input_schema=input_schema,
output_schema=output_schema)
model
Waiting for model conversion... It may take up to 10.0min.
Model is Pending conversion.Converting..Pending conversion..Converting.........Ready.
Name | xgboost-regressor |
Version | bfa8afd7-6a57-4325-a538-01663d37d430 |
File Name | model-auto-conversion_xgboost_xgb_regressor_diabetes.pkl |
SHA | 17e2e4e635b287f1234ed7c59a8447faebf4d69d7974749113233d0007b08e29 |
Status | ready |
Image Path | proxy.replicated.com/proxy/wallaroo/ghcr.io/wallaroolabs/mlflow-deploy:v2023.3.0-main-3509 |
Updated At | 2023-13-Jul 19:40:21 |
model.config().runtime()
'mlflow'
Deploy Pipeline
The model is uploaded and ready for use. We’ll add it as a step in our pipeline, then deploy the pipeline. For this example we’re allocated 0.25 cpu and 4 Gi RAM to the pipeline through the pipeline’s deployment configuration.
deployment_config = DeploymentConfigBuilder() \
.cpus(0.25).memory('1Gi') \
.build()
# clear the pipeline if it was used before
pipeline.undeploy()
pipeline.clear()
pipeline.add_model_step(model)
pipeline.deploy(deployment_config=deployment_config)
pipeline.status()
{'status': 'Running',
'details': [],
'engines': [{'ip': '10.244.9.240',
'name': 'engine-69c69947d8-zm6wv',
'status': 'Running',
'reason': None,
'details': [],
'pipeline_statuses': {'pipelines': [{'id': 'xgboost-regressor',
'status': 'Running'}]},
'model_statuses': {'models': [{'name': 'xgboost-regressor',
'version': 'bfa8afd7-6a57-4325-a538-01663d37d430',
'sha': '17e2e4e635b287f1234ed7c59a8447faebf4d69d7974749113233d0007b08e29',
'status': 'Running'}]}}],
'engine_lbs': [{'ip': '10.244.9.242',
'name': 'engine-lb-584f54c899-82tpj',
'status': 'Running',
'reason': None,
'details': []}],
'sidekicks': [{'ip': '10.244.9.241',
'name': 'engine-sidekick-xgboost-regressor-285-9b997df5c-dszld',
'status': 'Running',
'reason': None,
'details': [],
'statuses': '\n'}]}
Run Inference
A sample inference will be run. First the pandas DataFrame used for the inference is created, then the inference run through the pipeline’s infer
method.
data = pd.read_json('./data/test_xgb_regressor.json')
display(data)
dataframe = pd.DataFrame({"inputs": data[:2].values.tolist()})
display(dataframe)
results = pipeline.infer(dataframe)
display(results)
age | sex | bmi | bp | s1 | s2 | s3 | s4 | s5 | s6 | |
---|---|---|---|---|---|---|---|---|---|---|
0 | 0.038076 | 0.050680 | 0.061696 | 0.021872 | -0.044223 | -0.034821 | -0.043401 | -0.002592 | 0.019907 | -0.017646 |
1 | -0.001882 | -0.044642 | -0.051474 | -0.026328 | -0.008449 | -0.019163 | 0.074412 | -0.039493 | -0.068332 | -0.092204 |
inputs | |
---|---|
0 | [0.0380759064, 0.0506801187, 0.0616962065, 0.0... |
1 | [-0.0018820165, -0.0446416365, -0.051474061200... |
time | in.inputs | out.output | check_failures | |
---|---|---|---|---|
0 | 2023-07-13 19:41:07.855 | [0.0380759064, 0.0506801187, 0.0616962065, 0.0... | 151.001358 | 0 |
1 | 2023-07-13 19:41:07.855 | [-0.0018820165, -0.0446416365, -0.0514740612, ... | 74.999573 | 0 |
Undeploy Pipelines
With the tutorial complete, the pipeline is undeployed to return the resources back to the cluster.
pipeline.undeploy()
name | xgboost-regressor |
---|---|
created | 2023-07-13 19:39:02.980261+00:00 |
last_updated | 2023-07-13 19:40:25.981813+00:00 |
deployed | False |
tags | |
versions | 4b8b79bc-e3d1-4204-a58c-5c604dd9a0c6, e20c1212-406b-4384-8308-93437b3f17f4 |
steps | xgboost-regressor |
2.9.4 - Wallaroo SDK Upload Tutorial: XGBoost RF Classification
This tutorial can be downloaded as part of the Wallaroo Tutorials repository.
Wallaroo Model Upload via the Wallaroo SDK: XGBoost RF Classification
The following tutorial demonstrates how to upload a XGBoost RF Classification model to a Wallaroo instance.
Tutorial Goals
Demonstrate the following:
- Upload a XGBoost RF Classification model to a Wallaroo instance.
- Create a pipeline and add the model as a pipeline step.
- Perform a sample inference.
Prerequisites
- A Wallaroo version 2023.2.1 or above instance.
References
- Wallaroo MLOps API Essentials Guide: Model Upload and Registrations
- Wallaroo API Connection Guide
- DNS Integration Guide
Tutorial Steps
Import Libraries
The first step is to import the libraries we’ll be using. These are included by default in the Wallaroo instance’s JupyterHub service.
import json
import os
import pickle
import wallaroo
from wallaroo.pipeline import Pipeline
from wallaroo.deployment_config import DeploymentConfigBuilder
from wallaroo.object import EntityNotFoundError
from wallaroo.framework import Framework
import os
os.environ["MODELS_ENABLED"] = "true"
import pyarrow as pa
import numpy as np
import pandas as pd
Open a Connection to Wallaroo
The next step is connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. If logging in externally, update the wallarooPrefix
and wallarooSuffix
variables with the proper DNS information. For more information on Wallaroo DNS settings, see the Wallaroo DNS Integration Guide.
wl = wallaroo.Client()
Set Variables and Helper Functions
We’ll set the name of our workspace, pipeline, models and files. Workspace names must be unique across the Wallaroo workspace. For this, we’ll add in a randomly generated 4 characters to the workspace name to prevent collisions with other users’ workspaces. If running this tutorial, we recommend hard coding the workspace name so it will function in the same workspace each time it’s run.
We’ll set up some helper functions that will either use existing workspaces and pipelines, or create them if they do not already exist.
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
pipeline = wl.build_pipeline(name)
return pipeline
import string
import random
# make a random 4 character suffix to prevent overwriting other user's workspaces
suffix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
workspace_name = f'xgboost-rf-classification{suffix}'
pipeline_name = f'xgboost-rf-classification'
model_name = 'xgboost-rf-classification'
model_file_name = './models/model-auto-conversion_xgboost_xgb_rf_classification_iris.pkl'
Create Workspace and Pipeline
We will now create the Wallaroo workspace to store our model and set it as the current workspace. Future commands will default to this workspace for pipeline creation, model uploads, etc. We’ll create our Wallaroo pipeline to deploy our model.
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
pipeline = get_pipeline(pipeline_name)
Configure Data Schemas
XGBoost models are uploaded to Wallaroo through the Wallaroo Client upload_model
method.
Upload XGBoost Model Parameters
The following parameters are required for XGBoost models. Note that while some fields are considered as optional for the upload_model
method, they are required for proper uploading of a XGBoost model to Wallaroo.
Parameter | Type | Description |
---|---|---|
name | string (Required) | The name of the model. Model names are unique per workspace. Models that are uploaded with the same name are assigned as a new version of the model. |
path | string (Required) | The path to the model file being uploaded. |
framework | string (Upload Method Optional, SKLearn model Required) | Set as the Framework.XGBOOST . |
input_schema | pyarrow.lib.Schema (Upload Method Optional, SKLearn model Required) | The input schema in Apache Arrow schema format. |
output_schema | pyarrow.lib.Schema (Upload Method Optional, SKLearn model Required) | The output schema in Apache Arrow schema format. |
convert_wait | bool (Upload Method Optional, SKLearn model Optional) (Default: True) |
|
Once the upload process starts, the model is containerized by the Wallaroo instance. This process may take up to 10 minutes.
XGBoost Schema Inputs
XGBoost schema follows a different format than other models. To prevent inputs from being out of order, the inputs should be submitted in a single row in the order the model is trained to accept, with all of the data types being the same. If a model is originally trained to accept inputs of different data types, it will need to be retrained to only accept one data type for each column - typically pa.float64()
is a good choice.
For example, the following DataFrame has 4 columns, each column a float
.
 | sepal length (cm) | sepal width (cm) | petal length (cm) | petal width (cm) |
---|---|---|---|---|
0 | 5.1 | 3.5 | 1.4 | 0.2 |
1 | 4.9 | 3.0 | 1.4 | 0.2 |
For submission to an XGBoost model, the data input schema will be a single array with 4 float values.
input_schema = pa.schema([
pa.field('inputs', pa.list_(pa.float64(), list_size=4))
])
When submitting as an inference, the DataFrame is converted to rows with the column data expressed as a single array. The data must be in the same order as the model expects, which is why the data is submitted as a single array rather than JSON labeled columns: this insures that the data is submitted in the exact order as the model is trained to accept.
Original DataFrame:
 | sepal length (cm) | sepal width (cm) | petal length (cm) | petal width (cm) |
---|---|---|---|---|
0 | 5.1 | 3.5 | 1.4 | 0.2 |
1 | 4.9 | 3.0 | 1.4 | 0.2 |
Converted DataFrame:
 | inputs |
---|---|
0 | [5.1, 3.5, 1.4, 0.2] |
1 | [4.9, 3.0, 1.4, 0.2] |
XGBoost Schema Outputs
Outputs for XGBoost are labeled based on the trained model outputs. For this example, the output is simply a single output listed as output
. In the Wallaroo inference result, it is grouped with the metadata out
as out.output
.
output_schema = pa.schema([
pa.field('output', pa.int32())
])
pipeline.infer(dataframe)
 | time | in.inputs | out.output | check_failures |
---|---|---|---|---|
0 | 2023-07-05 15:11:29.776 | [5.1, 3.5, 1.4, 0.2] | 0 | 0 |
1 | 2023-07-05 15:11:29.776 | [4.9, 3.0, 1.4, 0.2] | 0 | 0 |
input_schema = pa.schema([
pa.field('inputs', pa.list_(pa.float64(), list_size=4))
])
output_schema = pa.schema([
pa.field('output', pa.float64())
])
Upload Model
The model will be uploaded with the framework set as Framework.XGBOOST
.
model = wl.upload_model(model_name,
model_file_name,
framework=Framework.XGBOOST,
input_schema=input_schema,
output_schema=output_schema)
model
Waiting for model conversion... It may take up to 10.0min.
Model is Pending conversion.Converting..Pending conversion.Converting........Ready.
Name | xgboost-rf-classification |
Version | b8c761bc-abeb-4105-988b-da1e73b609ee |
File Name | model-auto-conversion_xgboost_xgb_rf_classification_iris.pkl |
SHA | 2aeb56c084a279770abdd26d14caba949159698c1a5d260d2aafe73090e6cb03 |
Status | ready |
Image Path | proxy.replicated.com/proxy/wallaroo/ghcr.io/wallaroolabs/mlflow-deploy:v2023.3.0-main-3509 |
Updated At | 2023-13-Jul 19:50:13 |
model.config().runtime()
'mlflow'
Deploy Pipeline
The model is uploaded and ready for use. We’ll add it as a step in our pipeline, then deploy the pipeline. For this example we’re allocated 0.25 cpu and 4 Gi RAM to the pipeline through the pipeline’s deployment configuration.
deployment_config = DeploymentConfigBuilder() \
.cpus(0.25).memory('1Gi') \
.build()
# clear the pipeline if it was used before
pipeline.undeploy()
pipeline.clear()
pipeline.add_model_step(model)
pipeline.deploy(deployment_config=deployment_config)
pipeline.status()
{'status': 'Running',
'details': [],
'engines': [{'ip': '10.244.9.251',
'name': 'engine-597bc987fb-kzvhv',
'status': 'Running',
'reason': None,
'details': [],
'pipeline_statuses': {'pipelines': [{'id': 'xgboost-rf-classification',
'status': 'Running'}]},
'model_statuses': {'models': [{'name': 'xgboost-rf-classification',
'version': 'b8c761bc-abeb-4105-988b-da1e73b609ee',
'sha': '2aeb56c084a279770abdd26d14caba949159698c1a5d260d2aafe73090e6cb03',
'status': 'Running'}]}}],
'engine_lbs': [{'ip': '10.244.9.250',
'name': 'engine-lb-584f54c899-xjmbp',
'status': 'Running',
'reason': None,
'details': []}],
'sidekicks': [{'ip': '10.244.9.249',
'name': 'engine-sidekick-xgboost-rf-classification-287-556b5c5d98-fbqr2',
'status': 'Running',
'reason': None,
'details': [],
'statuses': '\n'}]}
Run Inference
A sample inference will be run. First the pandas DataFrame used for the inference is created, then the inference run through the pipeline’s infer
method.
data = pd.read_json('./data/test-xgboost-rf-classification-data.json')
data
dataframe = pd.DataFrame({"inputs": data[:2].values.tolist()})
dataframe
pipeline.infer(dataframe)
time | in.inputs | out.output | check_failures | |
---|---|---|---|---|
0 | 2023-07-13 19:50:34.747 | [5.1, 3.5, 1.4, 0.2] | 0.0 | 0 |
1 | 2023-07-13 19:50:34.747 | [4.9, 3.0, 1.4, 0.2] | 0.0 | 0 |
Undeploy Pipelines
With the tutorial complete, the pipeline is undeployed to return the resources back to the cluster.
pipeline.undeploy()
name | xgboost-rf-classification |
---|---|
created | 2023-07-13 19:49:09.300062+00:00 |
last_updated | 2023-07-13 19:50:17.818961+00:00 |
deployed | False |
tags | |
versions | 19d76345-8bd8-4e2a-9c2a-3fa2550baf4d, cc753e7a-62e1-4750-80c8-3cbfb688b1e8 |
steps | xgboost-rf-classification |
3 - Model Cookbooks
Recipes for deploying common models in Wallaroo.
3.1 - Aloha Quick Tutorial
This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.
Aloha Demo
In this notebook we will walk through a simple pipeline deployment to inference on a model. For this example we will be using an open source model that uses an Aloha CNN LSTM model for classifying Domain names as being either legitimate or being used for nefarious purposes such as malware distribution.
Prerequisites
- An installed Wallaroo instance.
- The following Python libraries installed:
Tutorial Goals
For our example, we will perform the following:
- Create a workspace for our work.
- Upload the Aloha model.
- Create a pipeline that can ingest our submitted data, submit it to the model, and export the results
- Run a sample inference through our pipeline by loading a file
- Run a sample inference through our pipeline’s URL and store the results in a file.
All sample data and models are available through the Wallaroo Quick Start Guide Samples repository.
Connect to the Wallaroo Instance
The first step is to connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. For more information on Wallaroo Client settings, see the Client Connection guide.e/wallaroo-sdk-essentials-client/).
import wallaroo
from wallaroo.object import EntityNotFoundError
# to display dataframe tables
from IPython.display import display
# used to display dataframe information without truncating
import pandas as pd
pd.set_option('display.max_colwidth', None)
import pyarrow as pa
# Login through local Wallaroo instance
wl = wallaroo.Client()
Create the Workspace
We will create a workspace to work in and call it the “alohaworkspace”, then set it as current workspace environment. We’ll also create our pipeline in advance as alohapipeline
. The model name and the model file will be specified for use in later steps.
To allow this tutorial to be run multiple times or by multiple users in the same Wallaroo instance, a random 4 character prefix will be added to the workspace, pipeline, and model.
import string
import random
# make a random 4 character suffix to verify uniqueness in tutorials
suffix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
workspace_name = f'alohaworkspace{suffix}'
pipeline_name = f'alohapipeline{suffix}'
model_name = f'alohamodel{suffix}'
model_file_name = './alohacnnlstm.zip'
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
pipeline = wl.build_pipeline(name)
return pipeline
wl.list_workspaces()[0:5]
[{'name': 'john.hummel@wallaroo.ai - Default Workspace', 'id': 1, 'archived': False, 'created_by': 'db364f8c-b866-4865-96b7-0b65662cb384', 'created_at': '2023-08-24T16:55:38.589652+00:00', 'models': [{'name': 'hf-summarization-demoyns2', 'versions': 1, 'owner_id': '""', 'last_update_time': datetime.datetime(2023, 8, 25, 15, 49, 36, 877913, tzinfo=tzutc()), 'created_at': datetime.datetime(2023, 8, 25, 15, 49, 36, 877913, tzinfo=tzutc())}], 'pipelines': [{'name': 'hf-summarization-pipeline-edge', 'create_time': datetime.datetime(2023, 8, 25, 15, 52, 2, 329988, tzinfo=tzutc()), 'definition': '[]'}]},
{'name': 'edge-publish-demojohn', 'id': 5, 'archived': False, 'created_by': 'db364f8c-b866-4865-96b7-0b65662cb384', 'created_at': '2023-08-24T16:57:29.048825+00:00', 'models': [{'name': 'ccfraud', 'versions': 1, 'owner_id': '""', 'last_update_time': datetime.datetime(2023, 8, 24, 16, 57, 29, 410708, tzinfo=tzutc()), 'created_at': datetime.datetime(2023, 8, 24, 16, 57, 29, 410708, tzinfo=tzutc())}], 'pipelines': [{'name': 'edge-pipeline', 'create_time': datetime.datetime(2023, 8, 24, 16, 57, 29, 486951, tzinfo=tzutc()), 'definition': '[]'}]},
{'name': 'workshop-workspace-sample', 'id': 6, 'archived': False, 'created_by': 'db364f8c-b866-4865-96b7-0b65662cb384', 'created_at': '2023-08-24T21:01:46.66355+00:00', 'models': [{'name': 'house-price-prime', 'versions': 2, 'owner_id': '""', 'last_update_time': datetime.datetime(2023, 8, 24, 21, 9, 13, 887924, tzinfo=tzutc()), 'created_at': datetime.datetime(2023, 8, 24, 21, 8, 35, 654241, tzinfo=tzutc())}], 'pipelines': [{'name': 'houseprice-estimator', 'create_time': datetime.datetime(2023, 8, 24, 21, 16, 0, 405934, tzinfo=tzutc()), 'definition': '[]'}]},
{'name': 'john-is-nice', 'id': 7, 'archived': False, 'created_by': 'db364f8c-b866-4865-96b7-0b65662cb384', 'created_at': '2023-08-24T21:05:57.618824+00:00', 'models': [], 'pipelines': []},
{'name': 'edge-hf-summarization', 'id': 8, 'archived': False, 'created_by': 'db364f8c-b866-4865-96b7-0b65662cb384', 'created_at': '2023-08-25T18:43:02.41099+00:00', 'models': [{'name': 'hf-summarization', 'versions': 1, 'owner_id': '""', 'last_update_time': datetime.datetime(2023, 8, 25, 18, 49, 26, 195772, tzinfo=tzutc()), 'created_at': datetime.datetime(2023, 8, 25, 18, 49, 26, 195772, tzinfo=tzutc())}], 'pipelines': [{'name': 'edge-hf-summarization', 'create_time': datetime.datetime(2023, 8, 25, 18, 53, 19, 465175, tzinfo=tzutc()), 'definition': '[]'}]}]
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
aloha_pipeline = get_pipeline(pipeline_name)
aloha_pipeline
name | alohapipelineudjo |
---|---|
created | 2023-08-28 17:10:05.043499+00:00 |
last_updated | 2023-08-28 17:10:05.043499+00:00 |
deployed | (none) |
tags | |
versions | e3f6ddff-9a25-48cc-9f94-fbc7bd67db29 |
steps | |
published | False |
We can verify the workspace is created the current default workspace with the get_current_workspace()
command.
wl.get_current_workspace()
{'name': 'alohaworkspaceudjo', 'id': 13, 'archived': False, 'created_by': 'db364f8c-b866-4865-96b7-0b65662cb384', 'created_at': '2023-08-28T17:10:04.351972+00:00', 'models': [], 'pipelines': [{'name': 'alohapipelineudjo', 'create_time': datetime.datetime(2023, 8, 28, 17, 10, 5, 43499, tzinfo=tzutc()), 'definition': '[]'}]}
Upload the Models
Now we will upload our models. Note that for this example we are applying the model from a .ZIP file. The Aloha model is a protobuf file that has been defined for evaluating web pages, and we will configure it to use data in the tensorflow
format.
from wallaroo.framework import Framework
model = wl.upload_model(model_name,
model_file_name,
framework=Framework.TENSORFLOW
)
Deploy a model
Now that we have a model that we want to use we will create a deployment for it.
We will tell the deployment we are using a tensorflow model and give the deployment name and the configuration we want for the deployment.
To do this, we’ll create our pipeline that can ingest the data, pass the data to our Aloha model, and give us a final output. We’ll call our pipeline aloha-test-demo
, then deploy it so it’s ready to receive data. The deployment process usually takes about 45 seconds.
- Note: If you receive an error that the pipeline could not be deployed because there are not enough resources, undeploy any other pipelines and deploy this one again. This command can quickly undeploy all pipelines to regain resources. We recommend not running this command in a production environment since it will cancel any running pipelines:
for p in wl.list_pipelines(): p.undeploy()
aloha_pipeline.add_model_step(model)
name | alohapipelineudjo |
---|---|
created | 2023-08-28 17:10:05.043499+00:00 |
last_updated | 2023-08-28 17:10:05.043499+00:00 |
deployed | (none) |
tags | |
versions | e3f6ddff-9a25-48cc-9f94-fbc7bd67db29 |
steps | |
published | False |
aloha_pipeline.deploy()
name | alohapipelineudjo |
---|---|
created | 2023-08-28 17:10:05.043499+00:00 |
last_updated | 2023-08-28 17:10:10.176032+00:00 |
deployed | True |
tags | |
versions | 750fec3b-f178-4abd-9643-2208e0fd1fbd, e3f6ddff-9a25-48cc-9f94-fbc7bd67db29 |
steps | alohamodeludjo |
published | False |
We can verify that the pipeline is running and list what models are associated with it.
aloha_pipeline.status()
{'status': 'Running',
'details': [],
'engines': [{'ip': '10.244.3.99',
'name': 'engine-7f6ccf6879-hmw2p',
'status': 'Running',
'reason': None,
'details': [],
'pipeline_statuses': {'pipelines': [{'id': 'alohapipelineudjo',
'status': 'Running'}]},
'model_statuses': {'models': [{'name': 'alohamodeludjo',
'version': 'b99dd290-870d-425e-8835-953decd402be',
'sha': 'd71d9ffc61aaac58c2b1ed70a2db13d1416fb9d3f5b891e5e4e2e97180fe22f8',
'status': 'Running'}]}}],
'engine_lbs': [{'ip': '10.244.4.114',
'name': 'engine-lb-584f54c899-4g6rh',
'status': 'Running',
'reason': None,
'details': []}],
'sidekicks': []}
Interferences
Infer 1 row
Now that the pipeline is deployed and our Aloha model is in place, we’ll perform a smoke test to verify the pipeline is up and running properly. We’ll use the infer_from_file
command to load a single encoded URL into the inference engine and print the results back out.
The result should tell us that the tokenized URL is legitimate (0) or fraud (1). This sample data should return close to 1 in out.main
.
smoke_test = pd.DataFrame.from_records(
[
{
"text_input":[
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
28,
16,
32,
23,
29,
32,
30,
19,
26,
17
]
}
]
)
result = aloha_pipeline.infer(smoke_test)
display(result.loc[:, ["time","out.main"]])
time | out.main | |
---|---|---|
0 | 2023-08-28 17:10:27.482 | [0.997564] |
Infer From File
This time, we’ll give it a bigger set of data to infer. ./data/data_1k.arrow
is an Apache Arrow table with 1,000 records in it. Once submitted, we’ll turn the result into a DataFrame and display the first five results.
result = aloha_pipeline.infer_from_file('./data/data_1k.arrow')
display(result.to_pandas().loc[:, ["time","out.main"]])
time | out.main | |
---|---|---|
0 | 2023-08-28 17:10:28.399 | [0.997564] |
1 | 2023-08-28 17:10:28.399 | [0.9885122] |
2 | 2023-08-28 17:10:28.399 | [0.9993358] |
3 | 2023-08-28 17:10:28.399 | [0.99999857] |
4 | 2023-08-28 17:10:28.399 | [0.9984837] |
... | ... | ... |
995 | 2023-08-28 17:10:28.399 | [0.9999754] |
996 | 2023-08-28 17:10:28.399 | [0.9999727] |
997 | 2023-08-28 17:10:28.399 | [0.66066873] |
998 | 2023-08-28 17:10:28.399 | [0.9998954] |
999 | 2023-08-28 17:10:28.399 | [0.99999803] |
1000 rows × 2 columns
outputs = result.to_pandas()
display(outputs.loc[:5, ["time","out.main"]])
time | out.main | |
---|---|---|
0 | 2023-08-28 17:10:28.399 | [0.997564] |
1 | 2023-08-28 17:10:28.399 | [0.9885122] |
2 | 2023-08-28 17:10:28.399 | [0.9993358] |
3 | 2023-08-28 17:10:28.399 | [0.99999857] |
4 | 2023-08-28 17:10:28.399 | [0.9984837] |
5 | 2023-08-28 17:10:28.399 | [1.0] |
Batch Inference
Now that our smoke test is successful, let’s really give it some data. We have two inference files we can use:
data_1k.arrow
: Contains 10,000 inferencesdata_25k.arrow
: Contains 25,000 inferences
When Apache Arrow tables are submitted to a Wallaroo Pipeline, the inference is processed natively as an Arrow table, and the results are returned as an Arrow table. This allows for faster data processing than with JSON files or DataFrame objects.
We’ll pipe the data_25k.arrow
file through the aloha_pipeline
deployment URL, and place the results in a file named response.arrow
. We’ll also display the time this takes. Note that for larger batches of 50,000 inferences or more can be difficult to view in Jupyter Hub because of its size, so we’ll only display the first five rows.
- IMPORTANT NOTE: The
_deployment._url()
method will return an internal URL when using Python commands from within the Wallaroo instance - for example, the Wallaroo JupyterHub service. When connecting via an external connection,_deployment._url()
returns an external URL. External URL connections requires the authentication be included in the HTTP request, and that Model Endpoints Guide external endpoints are enabled in the Wallaroo configuration options.
inference_url = aloha_pipeline._deployment._url()
inference_url
'https://doc-test.api.wallarooexample.ai/v1/api/pipelines/infer/alohapipelineudjo-15/alohapipelineudjo'
connection =wl.mlops().__dict__
token = connection['token']
dataFile="./data/data_25k.arrow"
contentType="application/vnd.apache.arrow.file"
!curl -X POST {inference_url} -H "Authorization: Bearer {token}" -H "Content-Type:{contentType}" --data-binary @{dataFile} > curl_response.df
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 25.9M 100 21.1M 100 4874k 1740k 391k 0:00:12 0:00:12 --:--:-- 4647k
cc_data_from_file = pd.read_json('./curl_response.df', orient="records")
display(cc_data_from_file.head(5).loc[:5, ["time","out"]])
time | out | |
---|---|---|
0 | 1693242632007 | {'banjori': [0.0015195821], 'corebot': [0.9829147500000001], 'cryptolocker': [0.012099549000000001], 'dircrypt': [4.7591115e-05], 'gozi': [2.0289428e-05], 'kraken': [0.00031977256999999996], 'locky': [0.011029262000000001], 'main': [0.997564], 'matsnu': [0.010341609], 'pykspa': [0.008038961], 'qakbot': [0.016155055], 'ramdo': [0.00623623], 'ramnit': [0.0009985747000000001], 'simda': [1.7933434e-26], 'suppobox': [1.388995e-27]} |
1 | 1693242632007 | {'banjori': [7.447196e-18], 'corebot': [6.7359245e-08], 'cryptolocker': [0.1708199], 'dircrypt': [1.3220122000000002e-09], 'gozi': [1.2758705999999999e-24], 'kraken': [0.22559543], 'locky': [0.34209849999999997], 'main': [0.99999994], 'matsnu': [0.3080186], 'pykspa': [0.1828217], 'qakbot': [3.8022549999999994e-11], 'ramdo': [0.2062254], 'ramnit': [0.15215826], 'simda': [1.1701982e-30], 'suppobox': [3.1514454e-38]} |
2 | 1693242632007 | {'banjori': [2.8598648999999997e-21], 'corebot': [9.302004000000001e-08], 'cryptolocker': [0.04445298], 'dircrypt': [6.1637580000000004e-09], 'gozi': [8.3496755e-23], 'kraken': [0.48234479999999996], 'locky': [0.26332903], 'main': [1.0], 'matsnu': [0.29800338], 'pykspa': [0.22361776], 'qakbot': [1.5238920999999999e-06], 'ramdo': [0.32820392], 'ramnit': [0.029332489000000003], 'simda': [1.1995622e-31], 'suppobox': [0.0]} |
3 | 1693242632007 | {'banjori': [2.1387213e-15], 'corebot': [3.8817485e-10], 'cryptolocker': [0.045599736], 'dircrypt': [1.9090386e-07], 'gozi': [1.3140123e-25], 'kraken': [0.59542626], 'locky': [0.17374137], 'main': [0.9999996999999999], 'matsnu': [0.23151578], 'pykspa': [0.17591679999999998], 'qakbot': [1.0876152e-09], 'ramdo': [0.21832279999999998], 'ramnit': [0.0128692705], 'simda': [6.1588803e-28], 'suppobox': [1.4386237e-35]} |
4 | 1693242632007 | {'banjori': [9.453342500000001e-15], 'corebot': [7.091151e-10], 'cryptolocker': [0.049815163], 'dircrypt': [5.2914135e-09], 'gozi': [7.4132087e-19], 'kraken': [1.5504574999999998e-13], 'locky': [1.079181e-15], 'main': [0.9999988999999999], 'matsnu': [1.5003075e-15], 'pykspa': [0.33075705], 'qakbot': [2.6258850000000004e-07], 'ramdo': [0.5036279], 'ramnit': [0.020393765], 'simda': [0.0], 'suppobox': [2.3292326e-38]} |
Undeploy Pipeline
When finished with our tests, we will undeploy the pipeline so we have the Kubernetes resources back for other tasks. Note that if the deployment variable is unchanged aloha_pipeline.deploy() will restart the inference engine in the same configuration as before.
aloha_pipeline.undeploy()
name | alohapipelineudjo |
---|---|
created | 2023-08-28 17:10:05.043499+00:00 |
last_updated | 2023-08-28 17:10:10.176032+00:00 |
deployed | False |
tags | |
versions | 750fec3b-f178-4abd-9643-2208e0fd1fbd, e3f6ddff-9a25-48cc-9f94-fbc7bd67db29 |
steps | alohamodeludjo |
published | False |
3.2 - Computer Vision Tutorials
This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.
Step 00: Introduction and Setup
This tutorial demonstrates how to use the Wallaroo to detect objects in images through the following models:
- rnn mobilenet: A single stage object detector that performs fast inferences. Mobilenet is typically good at identifying objects at a distance.
- resnet50: A dual stage object detector with slower inferencing but but is able to detect objects that are closer to each other.
This tutorial series will demonstrate the following:
- How to deploy a Wallaroo pipeline with trained rnn mobilenet model and perform sample inferences to detect objects in pictures, then display those objects.
- How to deploy a Wallaroo pipeline with a trained resnet50 model and perform sample inferences to detect objects in pictures, then display those objects.
- Use the Wallaroo feature shadow deploy to have both models perform inferences, then select the inference result with the higher confidence and show the objects detected.
This tutorial assumes that users have installed the Wallaroo SDK or are running these tutorials from within their Wallaroo instance’s JupyterHub service.
This demonstration should be run within a Wallaroo JupyterHub instance for best results.
Prerequisites
The included OpenCV class is included in this demonstration as CVDemoUtils.py
, and requires the following dependencies:
- ffmpeg
- libsm
- libxext
Internal JupyterHub Service
To install these dependencies in the Wallaroo JupyterHub service, use the following commands from a terminal shell via the following procedure:
Launch the JupyterHub Service within the Wallaroo install.
Select File->New->Terminal.
Enter the following:
sudo apt-get update
sudo apt-get install ffmpeg libsm6 libxext6 -y
External SDK Users
For users using the Wallaroo SDK to connect with a remote Wallaroo instance, the following commands will install the required dependancies:
For Linux users, this can be installed with:
sudo apt-get update
sudo apt-get install ffmpeg libsm6 libxext6 -y
MacOS users can prepare their environments using a package manager such as Brew with the following:
brew install ffmpeg libsm libxext
Libraries and Dependencies
- This repository may use large file sizes for the models. If necessary, install Git Large File Storage (LFS) or use the Wallaroo Tutorials Releases to download a .zip file of the most recent computer vision tutorial that includes the models.
- Import the following Python libraries into your environment:
These can be installed by running the command below in the Wallaroo JupyterHub service. Note the use of pip install torch --no-cache-dir
for low memory environments.
!pip install torchvision
!pip install torch --no-cache-dir
!pip install opencv-python
!pip install onnx
!pip install onnxruntime
!pip install imutils
!pip install pytz
!pip install ipywidgets
The rest of the tutorials will rely on these libraries and applications, so finish their installation before running the tutorials in this series.
Models for Wallaroo Computer Vision Tutorials
In order for the wallaroo tutorial notebooks to run properly, models must be downloaded to the models directory. As they are too large for Git to contain without extra steps, this tutorial and all models are available as a separate download. These are downloaded via the following procedure:
- Go to https://github.com/WallarooLabs/Wallaroo_Tutorials/releases.
- Select the most recent release.
- Download the file
computer_vision.zip
.
This contains the entire tutorial, plus the model files. The most current version of this link is there:
3.2.1 - Step 01: Detecting Objects Using mobilenet
This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.
Step 01: Detecting Objects Using mobilenet
The following tutorial demonstrates how to use a trained mobilenet model deployed in Wallaroo to detect objects. This process will use the following steps:
- Create a Wallaroo workspace and pipeline.
- Upload a trained mobilenet ML model and add it as a pipeline step.
- Deploy the pipeline.
- Perform an inference on a sample image.
- Draw the detected objects, their bounding boxes, their classifications, and the confidence of the classifications on the provided image.
- Review our results.
Steps
Import Libraries
The first step will be to import our libraries. Please check with Step 00: Introduction and Setup and verify that the necessary libraries and applications are added to your environment.
import torch
import pickle
import wallaroo
from wallaroo.object import EntityNotFoundError
from wallaroo.framework import Framework
import numpy as np
import json
import requests
import time
import pandas as pd
from CVDemoUtils import CVDemo
# used to display dataframe information without truncating
from IPython.display import display
import pandas as pd
pd.set_option('display.max_colwidth', None)
# used for unique connection names
import string
import random
suffix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
Connect to the Wallaroo Instance
The first step is to connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. For more information on Wallaroo Client settings, see the Client Connection guide.
# Login through local service
wl = wallaroo.Client()
Set Variables
The following variables and methods are used later to create or connect to an existing workspace, pipeline, and model.
workspace_name = f'mobilenetworkspacetest{suffix}'
pipeline_name = f'mobilenetpipeline{suffix}'
model_name = f'mobilenet{suffix}'
model_file_name = 'models/mobilenet.pt.onnx'
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
pipeline = wl.build_pipeline(name)
return pipeline
Create Workspace
The workspace will be created or connected to, and set as the default workspace for this session. Once that is done, then all models and pipelines will be set in that workspace.
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
wl.get_current_workspace()
{'name': 'mobilenetworkspacetesthsec', 'id': 15, 'archived': False, 'created_by': '4e296632-35b3-460e-85fe-565e311bc566', 'created_at': '2023-07-14T15:14:43.837337+00:00', 'models': [], 'pipelines': []}
Create Pipeline and Upload Model
We will now create or connect to an existing pipeline as named in the variables above.
pipeline = get_pipeline(pipeline_name)
mobilenet_model = wl.upload_model(model_name, model_file_name, framework=Framework.ONNX).configure(batch_config="single")
Deploy Pipeline
With the model uploaded, we can add it is as a step in the pipeline, then deploy it. Once deployed, resources from the Wallaroo instance will be reserved and the pipeline will be ready to use the model to perform inference requests.
pipeline.add_model_step(mobilenet_model)
pipeline.deploy()
name | mobilenetpipelinehsec |
---|---|
created | 2023-07-14 15:14:45.891178+00:00 |
last_updated | 2023-07-14 15:15:07.488456+00:00 |
deployed | True |
tags | |
versions | 13e17927-aef3-410c-b695-6bd713248f93, ae7cd8ab-7ad0-48ce-ba01-8e8e12595f45 |
steps | mobilenethsec |
Prepare input image
Next we will load a sample image and resize it to the width and height required for the object detector. Once complete, it the image will be converted to a numpy ndim array and added to a dictionary.
# The size the image will be resized to
width = 640
height = 480
# Only objects that have a confidence > confidence_target will be displayed on the image
cvDemo = CVDemo()
imagePath = 'data/images/input/example/dairy_bottles.png'
# The image width and height needs to be set to what the model was trained for. In this case 640x480.
tensor, resizedImage = cvDemo.loadImageAndResize(imagePath, width, height)
# get npArray from the tensorFloat
npArray = tensor.cpu().numpy()
#creates a dictionary with the wallaroo "tensor" key and the numpy ndim array representing image as the value.
dictData = {"tensor":[npArray]}
dataframedata = pd.DataFrame(dictData)
Run Inference
With that done, we can have the model detect the objects on the image by running an inference through the pipeline, and storing the results for the next step.
startTime = time.time()
# pass the dataframe in
infResults = pipeline.infer(dataframedata, dataset=["*", "metadata.elapsed"])
endTime = time.time()
Draw the Inference Results
With our inference results, we can take them and use the Wallaroo CVDemo class and draw them onto the original image. The bounding boxes and the confidence value will only be drawn on images where the model returned a 90% confidence rate in the object’s identity.
df = pd.DataFrame(columns=['classification','confidence','x','y','width','height'])
pd.options.mode.chained_assignment = None # default='warn'
pd.options.display.float_format = '{:.2%}'.format
# Points to where all the inference results are
boxList = infResults.loc[0]["out.output"]
# # reshape this to an array of bounding box coordinates converted to ints
boxA = np.array(boxList)
boxes = boxA.reshape(-1, 4)
boxes = boxes.astype(int)
df[['x', 'y','width','height']] = pd.DataFrame(boxes)
classes = infResults.loc[0]["out.2519"]
confidences = infResults.loc[0]["out.2518"]
infResults = {
'model_name' : model_name,
'pipeline_name' : pipeline_name,
'width': width,
'height': height,
'image' : resizedImage,
'boxes' : boxes,
'classes' : classes,
'confidences' : confidences,
'confidence-target' : 0.90,
'inference-time': (endTime-startTime),
'onnx-time' : int(infResults.loc[0]["metadata.elapsed"][1]) / 1e+9,
'color':(255,0,0)
}
image = cvDemo.drawAndDisplayDetectedObjectsWithClassification(infResults)
Extract the Inference Information
To show what is going on in the background, we’ll extract the inference results create a dataframe with columns representing the classification, confidence, and bounding boxes of the objects identified.
idx = 0
for idx in range(0,len(classes)):
df['classification'][idx] = cvDemo.CLASSES[classes[idx]] # Classes contains the 80 different COCO classificaitons
df['confidence'][idx] = confidences[idx]
df
classification | confidence | x | y | width | height | |
---|---|---|---|---|---|---|
0 | bottle | 98.65% | 0 | 210 | 85 | 479 |
1 | bottle | 90.12% | 72 | 197 | 151 | 468 |
2 | bottle | 60.78% | 211 | 184 | 277 | 420 |
3 | bottle | 59.22% | 143 | 203 | 216 | 448 |
4 | refrigerator | 53.73% | 13 | 41 | 640 | 480 |
5 | bottle | 45.13% | 106 | 206 | 159 | 463 |
6 | bottle | 43.73% | 278 | 1 | 321 | 93 |
7 | bottle | 43.09% | 462 | 104 | 510 | 224 |
8 | bottle | 40.85% | 310 | 1 | 352 | 94 |
9 | bottle | 39.19% | 528 | 268 | 636 | 475 |
10 | bottle | 35.76% | 220 | 0 | 258 | 90 |
11 | bottle | 31.81% | 552 | 96 | 600 | 233 |
12 | bottle | 26.45% | 349 | 0 | 404 | 98 |
13 | bottle | 23.06% | 450 | 264 | 619 | 472 |
14 | bottle | 20.48% | 261 | 193 | 307 | 408 |
15 | bottle | 17.46% | 509 | 101 | 544 | 235 |
16 | bottle | 17.31% | 592 | 100 | 633 | 239 |
17 | bottle | 16.00% | 475 | 297 | 551 | 468 |
18 | bottle | 14.91% | 368 | 163 | 423 | 362 |
19 | book | 13.66% | 120 | 0 | 175 | 81 |
20 | book | 13.32% | 72 | 0 | 143 | 85 |
21 | bottle | 12.22% | 271 | 200 | 305 | 274 |
22 | book | 12.13% | 161 | 0 | 213 | 85 |
23 | bottle | 11.96% | 162 | 0 | 214 | 83 |
24 | bottle | 11.53% | 310 | 190 | 367 | 397 |
25 | bottle | 9.62% | 396 | 166 | 441 | 360 |
26 | cake | 8.65% | 439 | 256 | 640 | 473 |
27 | bottle | 7.84% | 544 | 375 | 636 | 472 |
28 | vase | 7.23% | 272 | 2 | 306 | 96 |
29 | bottle | 6.28% | 453 | 303 | 524 | 463 |
30 | bottle | 5.28% | 609 | 94 | 635 | 211 |
Undeploy the Pipeline
With the inference complete, we can undeploy the pipeline and return the resources back to the Wallaroo instance.
pipeline.undeploy()
name | mobilenetpipelinehsec |
---|---|
created | 2023-07-14 15:14:45.891178+00:00 |
last_updated | 2023-07-14 15:15:07.488456+00:00 |
deployed | False |
tags | |
versions | 13e17927-aef3-410c-b695-6bd713248f93, ae7cd8ab-7ad0-48ce-ba01-8e8e12595f45 |
steps | mobilenethsec |
3.2.2 - Step 02: Detecting Objects Using resnet50
This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.
Step 02: Detecting Objects Using resnet50
The following tutorial demonstrates how to use a trained mobilenet model deployed in Wallaroo to detect objects. This process will use the following steps:
- Create a Wallaroo workspace and pipeline.
- Upload a trained resnet50 ML model and add it as a pipeline step.
- Deploy the pipeline.
- Perform an inference on a sample image.
- Draw the detected objects, their bounding boxes, their classifications, and the confidence of the classifications on the provided image.
- Review our results.
Steps
Import Libraries
The first step will be to import our libraries. Please check with Step 00: Introduction and Setup and verify that the necessary libraries and applications are added to your environment.
import torch
import pickle
import wallaroo
from wallaroo.object import EntityNotFoundError
from wallaroo.framework import Framework
import numpy as np
import json
import requests
import time
import pandas as pd
from CVDemoUtils import CVDemo
# used to display dataframe information without truncating
from IPython.display import display
import pandas as pd
pd.set_option('display.max_colwidth', None)
# used for unique connection names
import string
import random
suffix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
Connect to the Wallaroo Instance
The first step is to connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. For more information on Wallaroo Client settings, see the Client Connection guide.
# Login through local service
wl = wallaroo.Client()
Set Variables
The following variables and methods are used later to create or connect to an existing workspace, pipeline, and model. This example has both the resnet model, and a post process script.
workspace_name = f'resnetworkspace{suffix}'
pipeline_name = f'resnetnetpipeline{suffix}'
model_name = f'resnet50{suffix}'
model_file_name = 'models/frcnn-resnet.pt.onnx'
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
pipeline = wl.build_pipeline(name)
return pipeline
Create Workspace
The workspace will be created or connected to, and set as the default workspace for this session. Once that is done, then all models and pipelines will be set in that workspace.
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
wl.get_current_workspace()
{'name': 'resnetworkspaceejdf', 'id': 16, 'archived': False, 'created_by': '4e296632-35b3-460e-85fe-565e311bc566', 'created_at': '2023-07-14T15:16:00.093465+00:00', 'models': [], 'pipelines': []}
Create Pipeline and Upload Model
We will now create or connect to an existing pipeline as named in the variables above.
pipeline = get_pipeline(pipeline_name)
resnet_model = wl.upload_model(model_name, model_file_name, framework=Framework.ONNX).configure(batch_config="single")
Deploy Pipeline
With the model uploaded, we can add it is as a step in the pipeline, then deploy it. Once deployed, resources from the Wallaroo instance will be reserved and the pipeline will be ready to use the model to perform inference requests.
pipeline.add_model_step(resnet_model)
name | resnetnetpipelineejdf |
---|---|
created | 2023-07-14 15:16:02.616654+00:00 |
last_updated | 2023-07-14 15:16:02.616654+00:00 |
deployed | (none) |
tags | |
versions | 3a75189a-d406-4377-a762-a16fb1464cec |
steps |
pipeline.deploy()
name | resnetnetpipelineejdf |
---|---|
created | 2023-07-14 15:16:02.616654+00:00 |
last_updated | 2023-07-14 15:16:51.888236+00:00 |
deployed | True |
tags | |
versions | 5c9b6b1f-43e0-4db9-a469-7f00fbf75dee, 3a75189a-d406-4377-a762-a16fb1464cec |
steps | resnet50ejdf |
Test the pipeline by running inference on a sample image
Prepare input image
Next we will load a sample image and resize it to the width and height required for the object detector.
We will convert the image to a numpy ndim array and add it do a dictionary
#The size the image will be resized to
width = 640
height = 480
cvDemo = CVDemo()
imagePath = 'data/images/input/example/dairy_bottles.png'
# The image width and height needs to be set to what the model was trained for. In this case 640x480.
tensor, resizedImage = cvDemo.loadImageAndResize(imagePath, width, height)
# get npArray from the tensorFloat
npArray = tensor.cpu().numpy()
dictData = {"tensor":[npArray]}
dataframedata = pd.DataFrame(dictData)
Run Inference
With that done, we can have the model detect the objects on the image by running an inference through the pipeline, and storing the results for the next step.
IMPORTANT NOTE: If necessary, add timeout=60
to the infer
method if more time is needed to upload the data file for the inference request.
startTime = time.time()
# pass the dataframe in
infResults = pipeline.infer(dataframedata, dataset=["*", "metadata.elapsed"])
endTime = time.time()
Draw the Inference Results
With our inference results, we can take them and use the Wallaroo CVDemo class and draw them onto the original image. The bounding boxes and the confidence value will only be drawn on images where the model returned a 50% confidence rate in the object’s identity.
df = pd.DataFrame(columns=['classification','confidence','x','y','width','height'])
pd.options.mode.chained_assignment = None # default='warn'
pd.options.display.float_format = '{:.2%}'.format
# Points to where all the inference results are
boxList = infResults.loc[0]["out.output"]
# # reshape this to an array of bounding box coordinates converted to ints
boxA = np.array(boxList)
boxes = boxA.reshape(-1, 4)
boxes = boxes.astype(int)
df[['x', 'y','width','height']] = pd.DataFrame(boxes)
classes = infResults.loc[0]["out.3070"]
confidences = infResults.loc[0]["out.3069"]
infResults = {
'model_name' : model_name,
'pipeline_name' : pipeline_name,
'width': width,
'height': height,
'image' : resizedImage,
'boxes' : boxes,
'classes' : classes,
'confidences' : confidences,
'confidence-target' : 0.90,
'inference-time': (endTime-startTime),
'onnx-time' : int(infResults.loc[0]["metadata.elapsed"][1]) / 1e+9,
'color':(255,0,0)
}
image = cvDemo.drawAndDisplayDetectedObjectsWithClassification(infResults)
Extract the Inference Information
To show what is going on in the background, we’ll extract the inference results create a dataframe with columns representing the classification, confidence, and bounding boxes of the objects identified.
idx = 0
for idx in range(0,len(classes)):
cocoClasses = cvDemo.getCocoClasses()
df['classification'][idx] = cocoClasses[classes[idx]] # Classes contains the 80 different COCO classificaitons
df['confidence'][idx] = confidences[idx]
df
classification | confidence | x | y | width | height | |
---|---|---|---|---|---|---|
0 | bottle | 99.65% | 2 | 193 | 76 | 475 |
1 | bottle | 98.83% | 610 | 98 | 639 | 232 |
2 | bottle | 97.00% | 544 | 98 | 581 | 230 |
3 | bottle | 96.96% | 454 | 113 | 484 | 210 |
4 | bottle | 96.48% | 502 | 331 | 551 | 476 |
... | ... | ... | ... | ... | ... | ... |
95 | bottle | 5.72% | 556 | 287 | 580 | 322 |
96 | refrigerator | 5.66% | 80 | 161 | 638 | 480 |
97 | bottle | 5.60% | 455 | 334 | 480 | 349 |
98 | bottle | 5.46% | 613 | 267 | 635 | 375 |
99 | bottle | 5.37% | 345 | 2 | 395 | 99 |
100 rows × 6 columns
Undeploy the Pipeline
With the inference complete, we can undeploy the pipeline and return the resources back to the Wallaroo instance.
pipeline.undeploy()
name | resnetnetpipelineejdf |
---|---|
created | 2023-07-14 15:16:02.616654+00:00 |
last_updated | 2023-07-14 15:16:51.888236+00:00 |
deployed | False |
tags | |
versions | 5c9b6b1f-43e0-4db9-a469-7f00fbf75dee, 3a75189a-d406-4377-a762-a16fb1464cec |
steps | resnet50ejdf |
3.2.3 - Step 03: mobilenet and resnet50 Shadow Deploy
This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.
Step 03: Detecting Objects Using Shadow Deploy
The following tutorial demonstrates how to use two trained models, one based on the resnet50, the other on mobilenet, deployed in Wallaroo to detect objects. This builds on the previous tutorials in this series, Step 01: Detecting Objects Using mobilenet" and “Step 02: Detecting Objects Using resnet50”.
For this tutorial, the Wallaroo feature Shadow Deploy will be used to submit inference requests to both models at once. The mobilnet object detector is the control and the faster-rcnn object detector is the challenger. The results between the two will be compared for their confidence, and that confidence will be used to draw bounding boxes around identified objects.
This process will use the following steps:
- Create a Wallaroo workspace and pipeline.
- Upload a trained resnet50 ML model and trained mobilenet model and add them as a shadow deployed step with the mobilenet as the control model.
- Deploy the pipeline.
- Perform an inference on a sample image.
- Based on the
- Draw the detected objects, their bounding boxes, their classifications, and the confidence of the classifications on the provided image.
- Review our results.
Steps
Import Libraries
The first step will be to import our libraries. Please check with Step 00: Introduction and Setup and verify that the necessary libraries and applications are added to your environment.
import torch
import pickle
import wallaroo
from wallaroo.object import EntityNotFoundError
from wallaroo.framework import Framework
import numpy as np
import json
import requests
import time
import pandas as pd
from CVDemoUtils import CVDemo
# used to display dataframe information without truncating
from IPython.display import display
import pandas as pd
pd.set_option('display.max_colwidth', None)
# used for unique connection names
import string
import random
suffix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
Connect to the Wallaroo Instance
The first step is to connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. For more information on Wallaroo Client settings, see the Client Connection guide.
# Login through local service
wl = wallaroo.Client()
Set Variables
The following variables and methods are used later to create or connect to an existing workspace, pipeline, and model. This example has both the resnet model, and a post process script.
workspace_name = f'shadowimageworkspacetest{suffix}'
pipeline_name = f'shadowimagepipelinetest{suffix}'
control_model_name = f'mobilenet{suffix}'
control_model_file_name = 'models/mobilenet.pt.onnx'
challenger_model_name = f'resnet50{suffix}'
challenger_model_file_name = 'models/frcnn-resnet.pt.onnx'
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
pipeline = wl.build_pipeline(name)
return pipeline
Create Workspace
The workspace will be created or connected to, and set as the default workspace for this session. Once that is done, then all models and pipelines will be set in that workspace.
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
wl.get_current_workspace()
{'name': 'shadowimageworkspacetestydhi', 'id': 17, 'archived': False, 'created_by': '4e296632-35b3-460e-85fe-565e311bc566', 'created_at': '2023-07-14T15:18:03.466347+00:00', 'models': [], 'pipelines': []}
Create Pipeline and Upload Model
We will now create or connect to an existing pipeline as named in the variables above, then upload each of the models.
pipeline = get_pipeline(pipeline_name)
control = wl.upload_model(control_model_name, control_model_file_name, framework=Framework.ONNX).configure(batch_config="single")
challenger = wl.upload_model(challenger_model_name, challenger_model_file_name, framework=Framework.ONNX).configure(batch_config="single")
Shadow Deploy Pipeline
For this step, rather than deploying each model into a separate step, both will be deployed into a single step as a Shadow Deploy step. This will take the inference input data and process it through both pipelines at the same time. The inference results for the control will be stored in it’s ['outputs']
array, while the results for the challenger are stored the ['shadow_data']
array.
pipeline.add_shadow_deploy(control, [challenger])
name | shadowimagepipelinetestydhi |
---|---|
created | 2023-07-14 15:18:05.557027+00:00 |
last_updated | 2023-07-14 15:18:05.557027+00:00 |
deployed | (none) |
tags | |
versions | 05cd00dc-cf35-4a96-bdc3-f624d7b36477 |
steps |
pipeline.deploy()
name | shadowimagepipelinetestydhi |
---|---|
created | 2023-07-14 15:18:05.557027+00:00 |
last_updated | 2023-07-14 15:19:41.498743+00:00 |
deployed | True |
tags | |
versions | 64f3cfe9-bce4-4c4f-8328-9ee1cef3fe2d, 05cd00dc-cf35-4a96-bdc3-f624d7b36477 |
steps | mobilenetydhi |
pipeline.status()
{'status': 'Running',
'details': [],
'engines': [{'ip': '10.244.3.137',
'name': 'engine-c7bdfbd9c-qfgf9',
'status': 'Running',
'reason': None,
'details': [],
'pipeline_statuses': {'pipelines': [{'id': 'shadowimagepipelinetestydhi',
'status': 'Running'}]},
'model_statuses': {'models': [{'name': 'mobilenetydhi',
'version': '06ab0c1b-4151-4ab7-ba74-9a83a5c288f2',
'sha': 'f4c7009e53b679f5e44d70d9612e8dc365565cec88c25b5efa11b903b6b7bdc6',
'status': 'Running'},
{'name': 'resnet50ydhi',
'version': '7811489f-cf8b-469e-a917-9070c842a969',
'sha': 'ee606dc9776a1029420b3adf59b6d29395c89d1d9460d75045a1f2f152d288e7',
'status': 'Running'}]}}],
'engine_lbs': [{'ip': '10.244.4.182',
'name': 'engine-lb-584f54c899-qdzqk',
'status': 'Running',
'reason': None,
'details': []}],
'sidekicks': []}
Prepare input image
Next we will load a sample image and resize it to the width and height required for the object detector.
We will convert the image to a numpy ndim array and add it do a dictionary
imagePath = 'data/images/input/example/store-front.png'
# The image width and height needs to be set to what the model was trained for. In this case 640x480.
cvDemo = CVDemo()
# The size the image will be resized to meet the input requirements of the object detector
width = 640
height = 480
tensor, controlImage = cvDemo.loadImageAndResize(imagePath, width, height)
challengerImage = controlImage.copy()
# get npArray from the tensorFloat
npArray = tensor.cpu().numpy()
#creates a dictionary with the wallaroo "tensor" key and the numpy ndim array representing image as the value.
# dictData = {"tensor": npArray.tolist()}
dictData = {"tensor":[npArray]}
dataframedata = pd.DataFrame(dictData)
Run Inference using Shadow Deployment
Now lets have the model detect the objects on the image by running inference and extracting the results
startTime = time.time()
infResults = pipeline.infer(dataframedata, dataset=["*", "metadata.elapsed"])
endTime = time.time()
Extract Control Inference Results
First we’ll extract the inference result data for the control model and map it onto the image.
df = pd.DataFrame(columns=['classification','confidence','x','y','width','height'])
pd.options.mode.chained_assignment = None # default='warn'
pd.options.display.float_format = '{:.2%}'.format
# Points to where all the inference results are
# boxList = infResults[0]["out.output"]
boxList = infResults.loc[0]["out.output"]
# # reshape this to an array of bounding box coordinates converted to ints
boxA = np.array(boxList)
controlBoxes = boxA.reshape(-1, 4)
controlBoxes = controlBoxes.astype(int)
df[['x', 'y','width','height']] = pd.DataFrame(controlBoxes)
controlClasses = infResults.loc[0]["out.2519"]
controlConfidences = infResults.loc[0]["out.2518"]
results = {
'model_name' : control.name(),
'pipeline_name' : pipeline.name(),
'width': width,
'height': height,
'image' : controlImage,
'boxes' : controlBoxes,
'classes' : controlClasses,
'confidences' : controlConfidences,
'confidence-target' : 0.9,
'color':CVDemo.RED, # color to draw bounding boxes and the text in the statistics
'inference-time': (endTime-startTime),
'onnx-time' : 0,
}
cvDemo.drawAndDisplayDetectedObjectsWithClassification(results)
Display the Control Results
Here we will use the Wallaroo CVDemo helper class to draw the control model results on the image.
The full results will be displayed in a dataframe with columns representing the classification, confidence, and bounding boxes of the objects identified.
Once extracted from the results we will want to reshape the flattened array into an array with 4 elements (x,y,width,height).
idx = 0
cocoClasses = cvDemo.getCocoClasses()
for idx in range(0,len(controlClasses)):
df['classification'][idx] = cocoClasses[controlClasses[idx]] # Classes contains the 80 different COCO classificaitons
df['confidence'][idx] = controlConfidences[idx]
df
classification | confidence | x | y | width | height | |
---|---|---|---|---|---|---|
0 | car | 99.82% | 278 | 335 | 494 | 471 |
1 | person | 95.43% | 32 | 303 | 66 | 365 |
2 | umbrella | 81.33% | 117 | 256 | 209 | 322 |
3 | person | 72.38% | 183 | 310 | 203 | 367 |
4 | umbrella | 58.16% | 213 | 273 | 298 | 309 |
5 | person | 47.49% | 155 | 307 | 180 | 365 |
6 | person | 45.20% | 263 | 315 | 303 | 422 |
7 | person | 44.17% | 8 | 304 | 36 | 361 |
8 | person | 41.89% | 608 | 330 | 628 | 375 |
9 | person | 40.04% | 557 | 330 | 582 | 395 |
10 | potted plant | 39.22% | 241 | 193 | 315 | 292 |
11 | person | 38.94% | 547 | 329 | 573 | 397 |
12 | person | 38.50% | 615 | 331 | 634 | 372 |
13 | person | 37.89% | 553 | 321 | 576 | 374 |
14 | person | 37.04% | 147 | 304 | 170 | 366 |
15 | person | 36.11% | 515 | 322 | 537 | 369 |
16 | person | 34.55% | 562 | 317 | 586 | 373 |
17 | person | 32.37% | 531 | 329 | 557 | 399 |
18 | person | 32.19% | 239 | 306 | 279 | 428 |
19 | person | 30.28% | 320 | 308 | 343 | 359 |
20 | person | 26.50% | 289 | 311 | 310 | 380 |
21 | person | 23.09% | 371 | 307 | 394 | 337 |
22 | person | 22.66% | 295 | 300 | 340 | 373 |
23 | person | 22.23% | 1 | 306 | 25 | 362 |
24 | person | 21.88% | 484 | 319 | 506 | 349 |
25 | person | 21.13% | 272 | 327 | 297 | 405 |
26 | person | 20.15% | 136 | 304 | 160 | 363 |
27 | person | 19.68% | 520 | 338 | 543 | 392 |
28 | person | 16.86% | 478 | 317 | 498 | 348 |
29 | person | 16.55% | 365 | 319 | 391 | 344 |
30 | person | 16.22% | 621 | 339 | 639 | 403 |
31 | potted plant | 16.18% | 0 | 361 | 215 | 470 |
32 | person | 15.13% | 279 | 313 | 300 | 387 |
33 | person | 10.62% | 428 | 312 | 444 | 337 |
34 | umbrella | 10.01% | 215 | 252 | 313 | 315 |
35 | umbrella | 9.10% | 295 | 294 | 346 | 357 |
36 | umbrella | 7.95% | 358 | 293 | 402 | 319 |
37 | umbrella | 7.81% | 319 | 307 | 344 | 356 |
38 | potted plant | 7.18% | 166 | 331 | 221 | 439 |
39 | umbrella | 6.38% | 129 | 264 | 200 | 360 |
40 | person | 5.69% | 428 | 318 | 450 | 343 |
Display the Challenger Results
Here we will use the Wallaroo CVDemo helper class to draw the challenger model results on the input image.
challengerDf = pd.DataFrame(columns=['classification','confidence','x','y','width','height'])
pd.options.mode.chained_assignment = None # default='warn'
pd.options.display.float_format = '{:.2%}'.format
# Points to where all the inference results are
boxList = infResults.loc[0][f"out_{challenger_model_name}.output"]
# outputs = results['outputs']
# boxes = outputs[0]
# # reshape this to an array of bounding box coordinates converted to ints
# boxList = boxes['Float']['data']
boxA = np.array(boxList)
challengerBoxes = boxA.reshape(-1, 4)
challengerBoxes = challengerBoxes.astype(int)
challengerDf[['x', 'y','width','height']] = pd.DataFrame(challengerBoxes)
challengerClasses = infResults.loc[0][f"out_{challenger_model_name}.3070"]
challengerConfidences = infResults.loc[0][f"out_{challenger_model_name}.3069"]
results = {
'model_name' : challenger.name(),
'pipeline_name' : pipeline.name(),
'width': width,
'height': height,
'image' : challengerImage,
'boxes' : challengerBoxes,
'classes' : challengerClasses,
'confidences' : challengerConfidences,
'confidence-target' : 0.9,
'color':CVDemo.RED, # color to draw bounding boxes and the text in the statistics
'inference-time': (endTime-startTime),
'onnx-time' : 0,
}
cvDemo.drawAndDisplayDetectedObjectsWithClassification(results)
Display Challenger Results
The inference results for the objects detected by the challenger model will be displayed including the confidence values. Once extracted from the results we will want to reshape the flattened array into an array with 4 elements (x,y,width,height).
idx = 0
for idx in range(0,len(challengerClasses)):
challengerDf['classification'][idx] = cvDemo.CLASSES[challengerClasses[idx]] # Classes contains the 80 different COCO classificaitons
challengerDf['confidence'][idx] = challengerConfidences[idx]
challengerDf
classification | confidence | x | y | width | height | |
---|---|---|---|---|---|---|
0 | car | 99.91% | 274 | 332 | 496 | 472 |
1 | person | 99.77% | 536 | 320 | 563 | 409 |
2 | person | 98.88% | 31 | 305 | 69 | 370 |
3 | car | 97.02% | 617 | 335 | 639 | 424 |
4 | potted plant | 96.82% | 141 | 337 | 164 | 365 |
... | ... | ... | ... | ... | ... | ... |
81 | person | 5.61% | 312 | 316 | 341 | 371 |
82 | umbrella | 5.60% | 328 | 275 | 418 | 337 |
83 | person | 5.54% | 416 | 320 | 425 | 331 |
84 | person | 5.52% | 406 | 317 | 419 | 331 |
85 | person | 5.14% | 277 | 308 | 292 | 390 |
86 rows × 6 columns
pipeline.undeploy()
name | shadowimagepipelinetestydhi |
---|---|
created | 2023-07-14 15:18:05.557027+00:00 |
last_updated | 2023-07-14 15:19:41.498743+00:00 |
deployed | False |
tags | |
versions | 64f3cfe9-bce4-4c4f-8328-9ee1cef3fe2d, 05cd00dc-cf35-4a96-bdc3-f624d7b36477 |
steps | mobilenetydhi |
Conclusion
Notice the difference in the control confidence and the challenger confidence. Clearly we can see in this example the challenger resnet50 model is performing better than the control mobilenet model. This is likely due to the fact that frcnn resnet50 model is a 2 stage object detector vs the frcnn mobilenet is a single stage detector.
This completes using Wallaroo’s shadow deployment feature to compare different computer vision models.
3.3 - Computer Vision: Yolov8n Demonstration
The following tutorial is available on the Wallaroo Github Repository.
Computer Vision Yolov8n Deployment in Wallaroo
The Yolov8 computer vision model is used for fast recognition of objects in images. This tutorial demonstrates how to deploy a Yolov8n pre-trained model into a Wallaroo Ops server and perform inferences on it.
For this tutorial, the helper module CVDemoUtils
and WallarooUtils
are used to transform a sample image into a pandas DataFrame. This DataFrame is then submitted to the Yolov8n model deployed in Wallaroo.
This demonstration follows these steps:
- Upload the Yolo8 model to Wallaroo
- Add the Yolo8 model as a Wallaroo pipeline step
- Deploy the Wallaroo pipeline and allocate cluster resources to the pipeline
- Perform sample inferences
- Undeploy and return the resources
References
- Wallaroo Workspaces: Workspaces are environments were users upload models, create pipelines and other artifacts. The workspace should be considered the fundamental area where work is done. Workspaces are shared with other users to give them access to the same models, pipelines, etc.
- Wallaroo Model Upload and Registration: ML Models are uploaded to Wallaroo through the SDK or the MLOps API to a workspace. ML models include default runtimes (ONNX, Python Step, and TensorFlow) that are run directly through the Wallaroo engine, and containerized runtimes (Hugging Face, PyTorch, etc) that are run through in a container through the Wallaroo engine.
- Wallaroo Pipelines: Pipelines are used to deploy models for inferencing. Each model is a pipeline step in a pipelines, where the inputs of the previous step are fed into the next. Pipeline steps can be ML models, Python scripts, or Arbitrary Python (these contain necessary models and artifacts for running a model).
Steps
Load Libraries
The first step is loading the required libraries including the Wallaroo Python module.
# Import Wallaroo Python SDK
import wallaroo
from wallaroo.object import EntityNotFoundError
from wallaroo.framework import Framework
from CVDemoUtils import CVDemo
from WallarooUtils import Util
cvDemo = CVDemo()
util = Util()
# used to display DataFrame information without truncating
from IPython.display import display
import pandas as pd
pd.set_option('display.max_colwidth', None)
Connect to the Wallaroo Instance through the User Interface
The next step is to connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. For more information on Wallaroo Client settings, see the Client Connection guide.
wl = wallaroo.Client()
Create a New Workspace
We’ll use the SDK below to create our workspace , assign as our current workspace, then display all of the workspaces we have at the moment. We’ll also set up variables for our models and pipelines down the road, so we have one spot to change names to whatever fits your organization’s standards best.
To allow this tutorial to be run by multiple users in the same Wallaroo instance, a random 4 character prefix will be added to the workspace, pipeline, and model. Feel free to set suffix=''
if this is not required.
# import string
# import random
# # make a random 4 character suffix to verify uniqueness in tutorials
# suffix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
suffix = ''
model_name = 'yolov8n'
model_filename = 'models/yolov8n.onnx'
pipeline_name = 'yolo8demonstration'
workspace_name = f'yolo8-demonstration{suffix}'
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
Upload the Model
When a model is uploaded to a Wallaroo cluster, it is optimized and packaged to make it ready to run as part of a pipeline. In many times, the Wallaroo Server can natively run a model without any Python overhead. In other cases, such as a Python script, a custom Python environment will be automatically generated. This is comparable to the process of “containerizing” a model by adding a small HTTP server and other wrapping around it.
Our pretrained model is in ONNX format, which is specified in the framework
parameter. For this model, the tensor fields are set to images
to match the input parameters, and the batch configuration is set to single
- only one record will be submitted at a time.
# Upload Retrained Yolo8 Model
yolov8_model = (wl.upload_model(model_name,
model_filename,
framework=Framework.ONNX)
.configure(tensor_fields=['images'],
batch_config="single"
)
)
Pipeline Deployment Configuration
For our pipeline we set the deployment configuration to only use 1 cpu and 1 GiB of RAM.
deployment_config = wallaroo.DeploymentConfigBuilder() \
.replica_count(1) \
.cpus(1) \
.memory("1Gi") \
.build()
Build and Deploy the Pipeline
Now we build our pipeline and set our Yolo8 model as a pipeline step, then deploy the pipeline using the deployment configuration above.
pipeline = wl.build_pipeline(pipeline_name) \
.add_model_step(yolov8_model)
pipeline.deploy(deployment_config=deployment_config)
name | yolo8demonstration |
---|---|
created | 2023-10-19 15:33:26.144685+00:00 |
last_updated | 2023-10-19 15:33:27.154726+00:00 |
deployed | True |
tags | |
versions | 39b02242-de8b-46cd-849e-8a896226a84a, bed60f2a-ddd6-4a48-a9fd-debe6b1e1bca |
steps | yolov8n |
published | False |
Convert Image to DataFrame
The sample image dogbike.png
was converted to a DataFrame using the cvDemo
helper modules. The converted DataFrame is stored as ./data/dogbike.df.json
to save time.
The code sample below demonstrates how to use this module to convert the sample image to a DataFrame.
# convert the image to a tensor
width, height = 640, 640
tensor1, resizedImage1 = cvDemo.loadImageAndResize('dogbike.png', width, height)
tensor1.flatten()
# add the tensor to a DataFrame and save the DataFrame in pandas record format
df = util.convert_data(tensor1,'images')
df.to_json("data.json", orient = 'records')
# convert the image to a tensor
width, height = 640, 640
tensor1, resizedImage1 = cvDemo.loadImageAndResize('./data/dogbike.png', width, height)
tensor1.flatten()
# add the tensor to a DataFrame and save the DataFrame in pandas record format
df = util.convert_data(tensor1,'images')
df.to_json("data.json", orient = 'records')
Inference Request
We submit the DataFrame to the pipeline using wallaroo.pipeline.infer_from_file
, and store the results in the variable inf1
.
inf1 = pipeline.infer_from_file('./data/dogbike.df.json')
Display Bounding Boxes
Using our helper method cvDemo
we’ll identify the objects detected in the photo and their bounding boxes. Only objects with a confidence threshold of 50% or more are shown.
inf1.loc[:, ['out.output0']]
out.output0 | |
---|---|
0 | [17.09787, 16.459343, 17.259743, 19.960602, 43.600235, 59.986958, 62.826073, 68.24793, 77.43261, 80.82158, 89.44183, 96.168915, 99.22421, 112.584045, 126.75803, 131.9707, 137.1645, 141.93822, 146.29594, 152.00876, 155.94037, 165.20976, 175.27249, 184.05307, 193.66891, 201.51189, 215.04979, 223.80424, 227.24472, 234.19638, 244.9743, 248.5781, 252.42526, 264.95795, 278.48563, 285.758, 293.1897, 300.48227, 305.47742, 314.46085, 319.89404, 324.83658, 335.99536, 345.1116, 350.31964, 352.41107, 365.44934, 381.30008, 391.52316, 399.29163, 405.78503, 411.33804, 415.93207, 421.6868, 431.67108, 439.9069, 447.71542, 459.38522, 474.13187, 479.32642, 484.49884, 493.5153, 501.29932, 507.7967, 514.26044, 523.1473, 531.3479, 542.5094, 555.619, 557.7229, 564.6408, 571.5525, 572.8373, 587.95703, 604.2997, 609.452, 616.31714, 623.5797, 624.13153, 634.47266, 16.970057, 16.788723, 17.441803, 17.900642, 36.188023, 57.277973, 61.664352, 62.556896, 63.43486, 79.50621, 83.844, 95.983765, 106.166, 115.368454, 123.09253, 124.5821, 128.65866, 139.16113, 142.02315, 143.69855, ...] |
confidence_thres = 0.50
iou_thres = 0.25
cvDemo.drawYolo8Boxes(inf1, resizedImage1, width, height, confidence_thres, iou_thres, draw=True)
Score: 86.47% | Class: Dog | Bounding Box: [108, 250, 149, 356]
Score: 81.13% | Class: Bicycle | Bounding Box: [97, 149, 375, 323]
Score: 63.16% | Class: Car | Bounding Box: [390, 85, 186, 108]
array([[[ 34, 34, 34],
[ 35, 35, 35],
[ 33, 33, 33],
...,
[ 33, 33, 33],
[ 33, 33, 33],
[ 35, 35, 35]],
[[ 33, 33, 33],
[ 34, 34, 34],
[ 34, 34, 34],
...,
[ 34, 34, 34],
[ 33, 33, 33],
[ 34, 34, 34]],
[[ 53, 54, 48],
[ 54, 55, 49],
[ 54, 55, 49],
...,
[153, 178, 111],
[151, 183, 108],
[159, 176, 99]],
...,
[[159, 167, 178],
[159, 165, 177],
[158, 163, 175],
...,
[126, 127, 121],
[127, 125, 120],
[128, 120, 117]],
[[160, 168, 179],
[156, 162, 174],
[152, 157, 169],
...,
[126, 127, 121],
[129, 127, 122],
[127, 118, 116]],
[[155, 163, 174],
[155, 162, 174],
[152, 158, 170],
...,
[127, 127, 121],
[130, 126, 122],
[128, 119, 116]]], dtype=uint8)
Inference Through Pipeline API
Another method of performing an inference using the pipeline’s deployment url.
Performing an inference through an API requires the following:
- The authentication token to authorize the connection to the pipeline.
- The pipeline’s inference URL.
- Inference data to sent to the pipeline - in JSON, DataFrame records format, or Apache Arrow.
Full details are available through the Wallaroo API Connection Guide on how retrieve an authorization token and perform inferences through the pipeline’s API.
For this demonstration we’ll submit the pandas record, request a pandas record as the return, and set the authorization header. The results will be stored in the file curl_response.df
.
deploy_url = pipeline._deployment._url()
headers = wl.auth.auth_header()
headers['Content-Type']='application/json; format=pandas-records'
headers['Accept']='application/json; format=pandas-records'
!curl -X POST {deploy_url} \
-H "Authorization:{headers['Authorization']}" \
-H "Content-Type:application/json; format=pandas-records" \
-H "Accept:application/json; format=pandas-records" \
--data @./data/dogbike.df.json > curl_response.df
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 38.0M 100 22.9M 100 15.0M 5624k 3701k 0:00:04 0:00:04 --:--:-- 9334k
pipeline.undeploy()
name | yolo8demonstration |
---|---|
created | 2023-10-11 14:37:32.252497+00:00 |
last_updated | 2023-10-11 14:59:03.213137+00:00 |
deployed | False |
tags | |
versions | 2a3933c4-52db-40c6-b80f-9031664fd08a, 95bbabf1-1f15-4e4b-9e67-f7730c2b2cbd, 6c672144-ed4f-4505-97eb-a5b1763af847, 7149e0bc-089b-4d57-9a0b-5d4f4a9a4097, 329e394b-5105-4dc3-b0ff-5411623fc139, 7acaea4e-6ae3-426b-9f97-5e3dcc39c48e, a8b2c009-e7b5-4b96-81b9-40447797a05f, 09952a45-2401-4ebd-8e85-c678365b64a7, d870a558-10ef-448e-b00d-068c10c7e82b, fa531e16-1706-43c4-98d9-e0dd6355fe6f, 4c0b535e-b39b-40f4-82a7-34965b2f7c2a, 3507964d-382f-4e1c-84c7-64c5e27f819c, 9971f8dd-a17b-4d6a-ab72-d786d4990fab, b92a035f-903c-4039-8303-8ceb979a53c2 |
steps | yolov8n |
published | False |
3.4 - Computer Vision: Image Detection for Health Care
This tutorial can be found on the Wallaroo Tutorials Github Repository.
Image Detection for Health Care Computer Vision Tutorial Part 00: Prerequisites
The following tutorial demonstrates how to use Wallaroo to detect mitochondria from high resolution images. For this example we will be using a high resolution 1536x2048 image that is broken down into “patches” of 256x256 images that can be quickly analyzed.
Mitochondria are known as the “powerhouse” of the cell, and having a healthy amount of mitochondria indicates that a patient has enough energy to live a healthy life, or may have underlying issues that a doctor can check for.
Scanning high resolution images of patient cells can be used to count how many mitochondria a patient has, but the process is laborious. The following ML Model is trained to examine an image of cells, then detect which structures are mitochondria. This is used to speed up the process of testing patients and determining next steps.
Prerequisites
The included TiffImagesUtils class is included in this demonstration as CVDemoUtils.py
, and requires the following dependencies:
- ffmpeg
- libsm
- libxext
Internal JupyterHub Service
To install these dependencies in the Wallaroo JupyterHub service, use the following commands from a terminal shell via the following procedure:
Launch the JupyterHub Service within the Wallaroo install.
Select File->New->Terminal.
Enter the following:
sudo apt-get update
sudo apt-get install ffmpeg libsm6 libxext6 -y
External SDK Users
For users using the Wallaroo SDK to connect with a remote Wallaroo instance, the following commands will install the required dependancies:
For Linux users, this can be installed with:
sudo apt-get update
sudo apt-get install ffmpeg libsm6 libxext6 -y
MacOS users can prepare their environments using a package manager such as Brew with the following:
brew install ffmpeg libsm libxext
### Libraries and Dependencies
1. This repository may use large file sizes for the models. If necessary, install [Git Large File Storage (LFS)](https://git-lfs.com) or use the [Wallaroo Tutorials Releases](https://github.com/WallarooLabs/Wallaroo_Tutorials/releases) to download a .zip file of the most recent computer vision tutorial that includes the models.
1. Import the following Python libraries into your environment:
1. [torch](https://pypi.org/project/torch/)
1. [wallaroo](https://pypi.org/project/wallaroo/)
1. [torchvision](https://pypi.org/project/torchvision/)
1. [opencv-python](https://pypi.org/project/opencv-python/)
1. [onnx](https://pypi.org/project/onnx/)
1. [onnxruntime](https://pypi.org/project/onnxruntime/)
1. [imutils](https://pypi.org/project/imutils/)
1. [pytz](https://pypi.org/project/pytz/)
1. [ipywidgets](https://pypi.org/project/ipywidgets/)
These can be installed by running the command below in the Wallaroo JupyterHub service. Note the use of `pip install torch --no-cache-dir` for low memory environments.
```python
!pip install torchvision==0.15.2
!pip install torch==2.0.1 --no-cache-dir
!pip install opencv-python==4.7.0.72
!pip install onnx==1.12.0
!pip install onnxruntime==1.15.0
!pip install imutils==0.5.4
!pip install pytz
!pip install ipywidgets==8.0.6
!pip install patchify==0.2.3
!pip install tifffile==2023.4.12
!pip install piexif==1.1.3
Requirement already satisfied: torchvision==0.15.2 in /opt/conda/lib/python3.9/site-packages (0.15.2)
Requirement already satisfied: requests in /opt/conda/lib/python3.9/site-packages (from torchvision==0.15.2) (2.25.1)
Requirement already satisfied: pillow!=8.3.*,>=5.3.0 in /opt/conda/lib/python3.9/site-packages (from torchvision==0.15.2) (9.2.0)
Requirement already satisfied: torch==2.0.1 in /opt/conda/lib/python3.9/site-packages (from torchvision==0.15.2) (2.0.1)
Requirement already satisfied: numpy in /opt/conda/lib/python3.9/site-packages (from torchvision==0.15.2) (1.22.3)
Requirement already satisfied: nvidia-cuda-nvrtc-cu11==11.7.99 in /opt/conda/lib/python3.9/site-packages (from torch==2.0.1->torchvision==0.15.2) (11.7.99)
Requirement already satisfied: filelock in /opt/conda/lib/python3.9/site-packages (from torch==2.0.1->torchvision==0.15.2) (3.12.0)
Requirement already satisfied: jinja2 in /opt/conda/lib/python3.9/site-packages (from torch==2.0.1->torchvision==0.15.2) (3.1.2)
Requirement already satisfied: nvidia-cufft-cu11==10.9.0.58 in /opt/conda/lib/python3.9/site-packages (from torch==2.0.1->torchvision==0.15.2) (10.9.0.58)
Requirement already satisfied: nvidia-cudnn-cu11==8.5.0.96 in /opt/conda/lib/python3.9/site-packages (from torch==2.0.1->torchvision==0.15.2) (8.5.0.96)
Requirement already satisfied: nvidia-cublas-cu11==11.10.3.66 in /opt/conda/lib/python3.9/site-packages (from torch==2.0.1->torchvision==0.15.2) (11.10.3.66)
Requirement already satisfied: nvidia-cusparse-cu11==11.7.4.91 in /opt/conda/lib/python3.9/site-packages (from torch==2.0.1->torchvision==0.15.2) (11.7.4.91)
Requirement already satisfied: networkx in /opt/conda/lib/python3.9/site-packages (from torch==2.0.1->torchvision==0.15.2) (3.1)
Requirement already satisfied: nvidia-nccl-cu11==2.14.3 in /opt/conda/lib/python3.9/site-packages (from torch==2.0.1->torchvision==0.15.2) (2.14.3)
Requirement already satisfied: nvidia-curand-cu11==10.2.10.91 in /opt/conda/lib/python3.9/site-packages (from torch==2.0.1->torchvision==0.15.2) (10.2.10.91)
Requirement already satisfied: triton==2.0.0 in /opt/conda/lib/python3.9/site-packages (from torch==2.0.1->torchvision==0.15.2) (2.0.0)
Requirement already satisfied: nvidia-nvtx-cu11==11.7.91 in /opt/conda/lib/python3.9/site-packages (from torch==2.0.1->torchvision==0.15.2) (11.7.91)
Requirement already satisfied: typing-extensions in /opt/conda/lib/python3.9/site-packages (from torch==2.0.1->torchvision==0.15.2) (4.3.0)
Requirement already satisfied: sympy in /opt/conda/lib/python3.9/site-packages (from torch==2.0.1->torchvision==0.15.2) (1.12)
Requirement already satisfied: nvidia-cusolver-cu11==11.4.0.1 in /opt/conda/lib/python3.9/site-packages (from torch==2.0.1->torchvision==0.15.2) (11.4.0.1)
Requirement already satisfied: nvidia-cuda-cupti-cu11==11.7.101 in /opt/conda/lib/python3.9/site-packages (from torch==2.0.1->torchvision==0.15.2) (11.7.101)
Requirement already satisfied: nvidia-cuda-runtime-cu11==11.7.99 in /opt/conda/lib/python3.9/site-packages (from torch==2.0.1->torchvision==0.15.2) (11.7.99)
Requirement already satisfied: wheel in /opt/conda/lib/python3.9/site-packages (from nvidia-cublas-cu11==11.10.3.66->torch==2.0.1->torchvision==0.15.2) (0.37.1)
Requirement already satisfied: setuptools in /opt/conda/lib/python3.9/site-packages (from nvidia-cublas-cu11==11.10.3.66->torch==2.0.1->torchvision==0.15.2) (62.3.2)
Requirement already satisfied: cmake in /opt/conda/lib/python3.9/site-packages (from triton==2.0.0->torch==2.0.1->torchvision==0.15.2) (3.26.4)
Requirement already satisfied: lit in /opt/conda/lib/python3.9/site-packages (from triton==2.0.0->torch==2.0.1->torchvision==0.15.2) (16.0.5.post0)
Requirement already satisfied: urllib3<1.27,>=1.21.1 in /opt/conda/lib/python3.9/site-packages (from requests->torchvision==0.15.2) (1.26.9)
Requirement already satisfied: certifi>=2017.4.17 in /opt/conda/lib/python3.9/site-packages (from requests->torchvision==0.15.2) (2022.5.18.1)
Requirement already satisfied: idna<3,>=2.5 in /opt/conda/lib/python3.9/site-packages (from requests->torchvision==0.15.2) (2.10)
Requirement already satisfied: chardet<5,>=3.0.2 in /opt/conda/lib/python3.9/site-packages (from requests->torchvision==0.15.2) (4.0.0)
Requirement already satisfied: MarkupSafe>=2.0 in /opt/conda/lib/python3.9/site-packages (from jinja2->torch==2.0.1->torchvision==0.15.2) (2.1.1)
Requirement already satisfied: mpmath>=0.19 in /opt/conda/lib/python3.9/site-packages (from sympy->torch==2.0.1->torchvision==0.15.2) (1.3.0)
Requirement already satisfied: torch==2.0.1 in /opt/conda/lib/python3.9/site-packages (2.0.1)
Requirement already satisfied: jinja2 in /opt/conda/lib/python3.9/site-packages (from torch==2.0.1) (3.1.2)
Requirement already satisfied: nvidia-cusparse-cu11==11.7.4.91 in /opt/conda/lib/python3.9/site-packages (from torch==2.0.1) (11.7.4.91)
Requirement already satisfied: networkx in /opt/conda/lib/python3.9/site-packages (from torch==2.0.1) (3.1)
Requirement already satisfied: nvidia-cuda-nvrtc-cu11==11.7.99 in /opt/conda/lib/python3.9/site-packages (from torch==2.0.1) (11.7.99)
Requirement already satisfied: nvidia-curand-cu11==10.2.10.91 in /opt/conda/lib/python3.9/site-packages (from torch==2.0.1) (10.2.10.91)
Requirement already satisfied: triton==2.0.0 in /opt/conda/lib/python3.9/site-packages (from torch==2.0.1) (2.0.0)
Requirement already satisfied: nvidia-cuda-runtime-cu11==11.7.99 in /opt/conda/lib/python3.9/site-packages (from torch==2.0.1) (11.7.99)
Requirement already satisfied: nvidia-cufft-cu11==10.9.0.58 in /opt/conda/lib/python3.9/site-packages (from torch==2.0.1) (10.9.0.58)
Requirement already satisfied: nvidia-nccl-cu11==2.14.3 in /opt/conda/lib/python3.9/site-packages (from torch==2.0.1) (2.14.3)
Requirement already satisfied: filelock in /opt/conda/lib/python3.9/site-packages (from torch==2.0.1) (3.12.0)
Requirement already satisfied: nvidia-cuda-cupti-cu11==11.7.101 in /opt/conda/lib/python3.9/site-packages (from torch==2.0.1) (11.7.101)
Requirement already satisfied: nvidia-cublas-cu11==11.10.3.66 in /opt/conda/lib/python3.9/site-packages (from torch==2.0.1) (11.10.3.66)
Requirement already satisfied: sympy in /opt/conda/lib/python3.9/site-packages (from torch==2.0.1) (1.12)
Requirement already satisfied: nvidia-cudnn-cu11==8.5.0.96 in /opt/conda/lib/python3.9/site-packages (from torch==2.0.1) (8.5.0.96)
Requirement already satisfied: nvidia-cusolver-cu11==11.4.0.1 in /opt/conda/lib/python3.9/site-packages (from torch==2.0.1) (11.4.0.1)
Requirement already satisfied: nvidia-nvtx-cu11==11.7.91 in /opt/conda/lib/python3.9/site-packages (from torch==2.0.1) (11.7.91)
Requirement already satisfied: typing-extensions in /opt/conda/lib/python3.9/site-packages (from torch==2.0.1) (4.3.0)
Requirement already satisfied: setuptools in /opt/conda/lib/python3.9/site-packages (from nvidia-cublas-cu11==11.10.3.66->torch==2.0.1) (62.3.2)
Requirement already satisfied: wheel in /opt/conda/lib/python3.9/site-packages (from nvidia-cublas-cu11==11.10.3.66->torch==2.0.1) (0.37.1)
Requirement already satisfied: cmake in /opt/conda/lib/python3.9/site-packages (from triton==2.0.0->torch==2.0.1) (3.26.4)
Requirement already satisfied: lit in /opt/conda/lib/python3.9/site-packages (from triton==2.0.0->torch==2.0.1) (16.0.5.post0)
Requirement already satisfied: MarkupSafe>=2.0 in /opt/conda/lib/python3.9/site-packages (from jinja2->torch==2.0.1) (2.1.1)
Requirement already satisfied: mpmath>=0.19 in /opt/conda/lib/python3.9/site-packages (from sympy->torch==2.0.1) (1.3.0)
Requirement already satisfied: opencv-python==4.7.0.72 in /opt/conda/lib/python3.9/site-packages (4.7.0.72)
Requirement already satisfied: numpy>=1.19.3 in /opt/conda/lib/python3.9/site-packages (from opencv-python==4.7.0.72) (1.22.3)
Requirement already satisfied: onnx==1.12.0 in /opt/conda/lib/python3.9/site-packages (1.12.0)
Requirement already satisfied: typing-extensions>=3.6.2.1 in /opt/conda/lib/python3.9/site-packages (from onnx==1.12.0) (4.3.0)
Requirement already satisfied: protobuf<=3.20.1,>=3.12.2 in /opt/conda/lib/python3.9/site-packages (from onnx==1.12.0) (3.19.5)
Requirement already satisfied: numpy>=1.16.6 in /opt/conda/lib/python3.9/site-packages (from onnx==1.12.0) (1.22.3)
Requirement already satisfied: onnxruntime==1.15.0 in /opt/conda/lib/python3.9/site-packages (1.15.0)
Requirement already satisfied: packaging in /opt/conda/lib/python3.9/site-packages (from onnxruntime==1.15.0) (21.3)
Requirement already satisfied: flatbuffers in /opt/conda/lib/python3.9/site-packages (from onnxruntime==1.15.0) (1.12)
Requirement already satisfied: protobuf in /opt/conda/lib/python3.9/site-packages (from onnxruntime==1.15.0) (3.19.5)
Requirement already satisfied: numpy>=1.21.6 in /opt/conda/lib/python3.9/site-packages (from onnxruntime==1.15.0) (1.22.3)
Requirement already satisfied: coloredlogs in /opt/conda/lib/python3.9/site-packages (from onnxruntime==1.15.0) (15.0.1)
Requirement already satisfied: sympy in /opt/conda/lib/python3.9/site-packages (from onnxruntime==1.15.0) (1.12)
Requirement already satisfied: humanfriendly>=9.1 in /opt/conda/lib/python3.9/site-packages (from coloredlogs->onnxruntime==1.15.0) (10.0)
Requirement already satisfied: pyparsing!=3.0.5,>=2.0.2 in /opt/conda/lib/python3.9/site-packages (from packaging->onnxruntime==1.15.0) (3.0.9)
Requirement already satisfied: mpmath>=0.19 in /opt/conda/lib/python3.9/site-packages (from sympy->onnxruntime==1.15.0) (1.3.0)
Requirement already satisfied: imutils==0.5.4 in /opt/conda/lib/python3.9/site-packages (0.5.4)
Requirement already satisfied: pytz in /opt/conda/lib/python3.9/site-packages (2022.1)
Requirement already satisfied: ipywidgets==8.0.6 in /opt/conda/lib/python3.9/site-packages (8.0.6)
Requirement already satisfied: jupyterlab-widgets~=3.0.7 in /opt/conda/lib/python3.9/site-packages (from ipywidgets==8.0.6) (3.0.7)
Requirement already satisfied: ipython>=6.1.0 in /opt/conda/lib/python3.9/site-packages (from ipywidgets==8.0.6) (7.24.1)
Requirement already satisfied: ipykernel>=4.5.1 in /opt/conda/lib/python3.9/site-packages (from ipywidgets==8.0.6) (6.13.0)
Requirement already satisfied: traitlets>=4.3.1 in /opt/conda/lib/python3.9/site-packages (from ipywidgets==8.0.6) (5.2.1.post0)
Requirement already satisfied: widgetsnbextension~=4.0.7 in /opt/conda/lib/python3.9/site-packages (from ipywidgets==8.0.6) (4.0.7)
Requirement already satisfied: psutil in /opt/conda/lib/python3.9/site-packages (from ipykernel>=4.5.1->ipywidgets==8.0.6) (5.9.1)
Requirement already satisfied: matplotlib-inline>=0.1 in /opt/conda/lib/python3.9/site-packages (from ipykernel>=4.5.1->ipywidgets==8.0.6) (0.1.3)
Requirement already satisfied: debugpy>=1.0 in /opt/conda/lib/python3.9/site-packages (from ipykernel>=4.5.1->ipywidgets==8.0.6) (1.6.0)
Requirement already satisfied: nest-asyncio in /opt/conda/lib/python3.9/site-packages (from ipykernel>=4.5.1->ipywidgets==8.0.6) (1.5.5)
Requirement already satisfied: packaging in /opt/conda/lib/python3.9/site-packages (from ipykernel>=4.5.1->ipywidgets==8.0.6) (21.3)
Requirement already satisfied: tornado>=6.1 in /opt/conda/lib/python3.9/site-packages (from ipykernel>=4.5.1->ipywidgets==8.0.6) (6.1)
Requirement already satisfied: jupyter-client>=6.1.12 in /opt/conda/lib/python3.9/site-packages (from ipykernel>=4.5.1->ipywidgets==8.0.6) (7.3.1)
Requirement already satisfied: pexpect>4.3 in /opt/conda/lib/python3.9/site-packages (from ipython>=6.1.0->ipywidgets==8.0.6) (4.8.0)
Requirement already satisfied: backcall in /opt/conda/lib/python3.9/site-packages (from ipython>=6.1.0->ipywidgets==8.0.6) (0.2.0)
Requirement already satisfied: jedi>=0.16 in /opt/conda/lib/python3.9/site-packages (from ipython>=6.1.0->ipywidgets==8.0.6) (0.18.1)
Requirement already satisfied: pickleshare in /opt/conda/lib/python3.9/site-packages (from ipython>=6.1.0->ipywidgets==8.0.6) (0.7.5)
Requirement already satisfied: decorator in /opt/conda/lib/python3.9/site-packages (from ipython>=6.1.0->ipywidgets==8.0.6) (5.1.1)
Requirement already satisfied: prompt-toolkit!=3.0.0,!=3.0.1,<3.1.0,>=2.0.0 in /opt/conda/lib/python3.9/site-packages (from ipython>=6.1.0->ipywidgets==8.0.6) (3.0.29)
Requirement already satisfied: setuptools>=18.5 in /opt/conda/lib/python3.9/site-packages (from ipython>=6.1.0->ipywidgets==8.0.6) (62.3.2)
Requirement already satisfied: pygments in /opt/conda/lib/python3.9/site-packages (from ipython>=6.1.0->ipywidgets==8.0.6) (2.12.0)
Requirement already satisfied: parso<0.9.0,>=0.8.0 in /opt/conda/lib/python3.9/site-packages (from jedi>=0.16->ipython>=6.1.0->ipywidgets==8.0.6) (0.8.3)
Requirement already satisfied: entrypoints in /opt/conda/lib/python3.9/site-packages (from jupyter-client>=6.1.12->ipykernel>=4.5.1->ipywidgets==8.0.6) (0.4)
Requirement already satisfied: python-dateutil>=2.8.2 in /opt/conda/lib/python3.9/site-packages (from jupyter-client>=6.1.12->ipykernel>=4.5.1->ipywidgets==8.0.6) (2.8.2)
Requirement already satisfied: jupyter-core>=4.9.2 in /opt/conda/lib/python3.9/site-packages (from jupyter-client>=6.1.12->ipykernel>=4.5.1->ipywidgets==8.0.6) (4.10.0)
Requirement already satisfied: pyzmq>=22.3 in /opt/conda/lib/python3.9/site-packages (from jupyter-client>=6.1.12->ipykernel>=4.5.1->ipywidgets==8.0.6) (23.0.0)
Requirement already satisfied: ptyprocess>=0.5 in /opt/conda/lib/python3.9/site-packages (from pexpect>4.3->ipython>=6.1.0->ipywidgets==8.0.6) (0.7.0)
Requirement already satisfied: wcwidth in /opt/conda/lib/python3.9/site-packages (from prompt-toolkit!=3.0.0,!=3.0.1,<3.1.0,>=2.0.0->ipython>=6.1.0->ipywidgets==8.0.6) (0.2.5)
Requirement already satisfied: pyparsing!=3.0.5,>=2.0.2 in /opt/conda/lib/python3.9/site-packages (from packaging->ipykernel>=4.5.1->ipywidgets==8.0.6) (3.0.9)
Requirement already satisfied: six>=1.5 in /opt/conda/lib/python3.9/site-packages (from python-dateutil>=2.8.2->jupyter-client>=6.1.12->ipykernel>=4.5.1->ipywidgets==8.0.6) (1.16.0)
Requirement already satisfied: patchify==0.2.3 in /opt/conda/lib/python3.9/site-packages (0.2.3)
Requirement already satisfied: numpy<2,>=1 in /opt/conda/lib/python3.9/site-packages (from patchify==0.2.3) (1.22.3)
Requirement already satisfied: tifffile==2023.4.12 in /opt/conda/lib/python3.9/site-packages (2023.4.12)
Requirement already satisfied: numpy in /opt/conda/lib/python3.9/site-packages (from tifffile==2023.4.12) (1.22.3)
Requirement already satisfied: piexif==1.1.3 in /opt/conda/lib/python3.9/site-packages (1.1.3)
Wallaroo SDK
The Wallaroo SDK is provided by default with the Wallaroo instance’s JupyterHub service. To install the Wallaroo SDK manually, it is provided from the Python Package Index and is installed with pip
. Verify that the same version of the Wallaroo SDK is the same version as the Wallaroo instance. For example for Wallaroo release 2023.2, the SDK install command is:
pip install wallaroo==2023.2.1
See the Wallaroo SDK Install Guides for full details.
3.4.1 - Computer Vision: Image Detection for Health Care
This tutorial can be found on the Wallaroo Tutorials Github Repository.
Image Detection for Health Care Computer Vision Tutorial Part 01: Mitochondria Detection
The following tutorial demonstrates how to use Wallaroo to detect mitochondria from high resolution images. For this example we will be using a high resolution 1536x2048 image that is broken down into “patches” of 256x256 images that can be quickly analyzed.
Mitochondria are known as the “powerhouse” of the cell, and having a healthy amount of mitochondria indicates that a patient has enough energy to live a healthy life, or may have underling issues that a doctor can check for.
Scanning high resolution images of patient cells can be used to count how many mitochondria a patient has, but the process is laborious. The following ML Model is trained to examine an image of cells, then detect which structures are mitochondria. This is used to speed up the process of testing patients and determining next steps.
Tutorial Goals
This tutorial will perform the following:
- Upload and deploy the
mitochondria_epochs_15.onnx
model to a Wallaroo pipeline. - Randomly select from from a selection of 256x256 images that were originally part of a larger 1536x2048 image.
- Convert the images into a numpy array inserted into a pandas DataFrame.
- Submit the DataFrame to the Wallaroo pipeline and use the results to create a mask image of where the model detects mitochondria.
- Compare the original image against a map of “ground truth” and the model’s mask image.
- Undeploy the pipeline and return the resources back to the Wallaroo instance.
Prerequisites
Complete the steps from Mitochondria Detection Computer Vision Tutorial Part 00: Prerequisites.
Mitochondria Computer Vision Detection Steps
Import Libraries
The first step is to import the necessary libraries. Included with this tutorial are the following custom modules:
tiff_utils
: Organizes the tiff images to perform random image selections and other tasks.
Note that tensorflow may return warnings depending on the environment.
import json
import IPython.display as display
import time
import matplotlib.pyplot as plt
from IPython.display import clear_output, display
from lib.TiffImageUtils import TiffUtils
import tifffile as tiff
import pandas as pd
import wallaroo
from wallaroo.object import EntityNotFoundError
import numpy as np
from matplotlib import pyplot as plt
import cv2
from keras.utils import normalize
tiff_utils = TiffUtils()
# used for unique connection names
import string
import random
suffix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
# ignoring warnings for demonstration
import warnings
warnings.filterwarnings('ignore')
Open a Connection to Wallaroo
The next step is connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. If logging in externally, update the wallarooPrefix
and wallarooSuffix
variables with the proper DNS information. For more information on Wallaroo DNS settings, see the Wallaroo DNS Integration Guide.
wl = wallaroo.Client()
Create Workspace and Pipeline
We will create a workspace to manage our pipeline and models. The following variables will set the name of our sample workspace then set it as the current workspace.
Workspace, pipeline, and model names should be unique to each Wallaroo instance, so we’ll add in a randomly generated suffix so multiple people can run this tutorial in a Wallaroo instance without affecting each other.
workspace_name = f'biolabsworkspace{suffix}'
pipeline_name = f'biolabspipeline{suffix}'
model_name = f'biolabsmodel{suffix}'
model_file_name = 'models/mitochondria_epochs_15.onnx'
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
pipeline = wl.build_pipeline(name)
return pipeline
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
pipeline = get_pipeline(pipeline_name)
pipeline
name | biolabspipelinebspy |
---|---|
created | 2023-07-14 15:28:32.639523+00:00 |
last_updated | 2023-07-14 15:28:32.639523+00:00 |
deployed | (none) |
tags | |
versions | c70dbdfe-e380-41b0-9da6-97bbfae90554 |
steps |
Upload the Models
Now we will:
- Upload our model.
- Apply it as a step in our pipeline.
- Create a pipeline deployment with enough memory to perform the inferences.
- Deploy the pipeline.
deployment_config = wallaroo.DeploymentConfigBuilder().replica_count(1).cpus(1).memory("2Gi").build()
model = wl.upload_model(model_name, model_file_name, framework=wallaroo.framework.Framework.ONNX)
pipeline = wl.build_pipeline(pipeline_name) \
.add_model_step(model) \
.deploy(deployment_config = deployment_config)
Retrieve Image and Convert to Data
The next step is to process the image into a numpy array that the model is trained to detect from.
We start by retrieving all the patch images from a recorded time series tiff recorded on one of our microscopes.
sample_mitochondria_patches_path = "./patches/ms-01-atl-3-22-23_9-50"
patches = tiff_utils.get_all_patches(sample_mitochondria_patches_path)
Randomly we will retrieve a 256x256 patch image and use it to do our semantic segmentation prediction.
We’ll then convert it into a numpy array and insert into a DataFrame for a single inference.
The following helper function loadImageAndConvertTiff
is used to convert the image into a numpy, then insert that into the DataFrame. This allows a later command to take the randomly grabbed image perform the process on other images.
def loadImageAndConvertTiff(imagePath, width, height):
img = cv2.imread(imagePath, 0)
imgNorm = np.expand_dims(normalize(np.array(img), axis=1),2)
imgNorm=imgNorm[:,:,0][:,:,None]
imgNorm=np.expand_dims(imgNorm, 0)
resizedImage = None
#creates a dictionary with the wallaroo "tensor" key and the numpy ndim array representing image as the value.
dictData = {"tensor":[imgNorm]}
dataframedata = pd.DataFrame(dictData)
# display(dataframedata)
return dataframedata, resizedImage
def run_semantic_segmentation_inference(pipeline, input_tiff_image, width, height, threshold):
tensor, resizedImage = loadImageAndConvertTiff(input_tiff_image, width, height)
# print(tensor)
# #
# # run inference on the 256x256 patch image get the predicted mitochandria mask
# #
output = pipeline.infer(tensor)
# print(output)
# # Obtain the flattened predicted mitochandria mask result
list1d = output.loc[0]["out.conv2d_37"]
np1d = np.array(list1d)
# # unflatten it
predicted_mask = np1d.reshape(1,width,height,1)
# # perform the element-wise comaprison operation using the threshold provided
predicted_mask = (predicted_mask[0,:,:,0] > threshold).astype(np.uint8)
# return predicted_mask
return predicted_mask
Infer and Display Results
We will now perform our inferences and display the results. This results in a predicted mask showing us where the mitochondria cells are located.
- The first image is the input image.
- The 2nd image is the ground truth. The mask was created by a human who identified the mitochondria cells in the input image
- The 3rd image is the predicted mask after running inference on the Wallaroo pipeline.
We’ll perform this 10 times to show how quickly the inferences can be submitted.
for x in range(10):
# get a sample 256x256 mitochondria image
random_patch = tiff_utils.get_random_patch_sample(patches)
# build the path to the image
patch_image_path = sample_mitochondria_patches_path + "/images/" + random_patch['patch_image_file']
# run inference in order to get the predicted 256x256 mask
predicted_mask = run_semantic_segmentation_inference(pipeline, patch_image_path, 256,256, 0.2)
# # plot the results
test_image = random_patch['patch_image'][:,:,0]
test_image_title = f"Testing Image - {random_patch['index']}"
ground_truth_image = random_patch['patch_mask'][:,:,0]
ground_truth_image_title = "Ground Truth Mask"
predicted_mask_title = 'Predicted Mask'
tiff_utils.plot_test_results(test_image, test_image_title, \
ground_truth_image, ground_truth_image_title, \
predicted_mask, predicted_mask_title)
Complete Tutorial
With the demonstration complete, the pipeline is undeployed and the resources returned back to the Wallaroo instance.
pipeline.undeploy()
name | biolabspipelinebspy |
---|---|
created | 2023-07-14 15:28:32.639523+00:00 |
last_updated | 2023-07-14 15:28:38.163950+00:00 |
deployed | False |
tags | |
versions | 0460ef47-3de3-43b2-8f62-16e76be8ce93, ef41bc7b-8213-4dd4-a1b9-54ac1253c652, c70dbdfe-e380-41b0-9da6-97bbfae90554 |
steps | biolabsmodelbspy |
3.4.2 - Computer Vision: Image Detection for Health Care
This tutorial can be found on the Wallaroo Tutorials Github Repository.
Image Detection for Health Care Computer Vision Tutorial Part 02: External Data Connection Stores
The first example in this tutorial series showed selecting from 10 random images of cells and detecting mitochondria from within them.
This tutorial will expand on that by using a Wallaroo connection to retrieve a high resolution 1536x2048 image, break it down into 256x256 “patches” that can be quickly analyzed.
Wallaroo connections are definitions set by MLOps engineers that are used by other Wallaroo users for connection information to a data source. For this example, the data source will be a GitHub URL, but they could be Google BigQuery databases, Kafka topics, or any number of user defined examples.
Tutorial Goals
This tutorial will perform the following:
- Upload and deploy the
mitochondria_epochs_15.onnx
model to a Wallaroo pipeline. - Create a Wallaroo connection pointing to a location for a high resolution image of cells.
- Break down the image into 256x256 images based on the how the model was trained to detect mitochondria.
- Convert the images into a numpy array inserted into a pandas DataFrame.
- Submit the DataFrame to the Wallaroo pipeline and use the results to create a mask image of where the model detects mitochondria.
- Compare the original image against a map of “ground truth” and the model’s mask image.
- Undeploy the pipeline and return the resources back to the Wallaroo instance.
Prerequisites
Prerequisites
Complete the steps from Mitochondria Detection Computer Vision Tutorial Part 00: Prerequisites.
Mitochondria Computer Vision Detection Steps
Import Libraries
The first step is to import the necessary libraries. Included with this tutorial are the following custom modules:
tiff_utils
: Organizes the tiff images to perform random image selections and other tasks.
import json
import IPython.display as display
import time
import matplotlib.pyplot as plt
from IPython.display import clear_output, display
from lib.TiffImageUtils import TiffUtils
import tifffile as tiff
import pandas as pd
import wallaroo
from wallaroo.object import EntityNotFoundError, RequiredAttributeMissing
import numpy as np
from matplotlib import pyplot as plt
import cv2
from keras.utils import normalize
tiff_utils = TiffUtils()
# used for unique connection names
import string
import random
suffix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
# ignoring warnings for demonstration
import warnings
warnings.filterwarnings('ignore')
Open a Connection to Wallaroo
The next step is connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. If logging in externally, update the wallarooPrefix
and wallarooSuffix
variables with the proper DNS information. For more information on Wallaroo DNS settings, see the Wallaroo DNS Integration Guide.
wl = wallaroo.Client()
Create Workspace and Pipeline
We will create a workspace to manage our pipeline and models. The following variables will set the name of our sample workspace then set it as the current workspace.
Workspace, pipeline, and model names should be unique to each Wallaroo instance, so we’ll add in a randomly generated suffix so multiple people can run this tutorial in a Wallaroo instance without effecting each other.
workspace_name = f'biolabsconnectionworkspace{suffix}'
pipeline_name = f'biolabsconnectionpipeline{suffix}'
model_name = f'biolabsconnectionmodel{suffix}'
model_file_name = 'models/mitochondria_epochs_15.onnx'
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
pipeline = wl.build_pipeline(name)
return pipeline
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
pipeline = get_pipeline(pipeline_name)
pipeline
name | biolabsconnectionpipelinepdrv |
---|---|
created | 2023-07-14 15:29:16.969523+00:00 |
last_updated | 2023-07-14 15:29:16.969523+00:00 |
deployed | (none) |
tags | |
versions | 79d3b6ca-823f-406d-bf23-88a23b49ff1b |
steps |
Upload the Models
Now we will:
- Upload our model.
- Apply it as a step in our pipeline.
- Create a pipeline deployment with enough memory to perform the inferences.
- Deploy the pipeline.
deployment_config = wallaroo.DeploymentConfigBuilder().replica_count(1).cpus(1).memory("2Gi").build()
model = wl.upload_model(model_name, model_file_name, framework=wallaroo.framework.Framework.ONNX)
pipeline = wl.build_pipeline(pipeline_name) \
.add_model_step(model) \
.deploy(deployment_config = deployment_config)
Create the Wallaroo Connection
The Wallaroo connection will point to the location of our high resolution images:
- One the cell photos to be retrieved
- The other “ground truth” masks that are mapped compared against the model’s predictions.
The images will be retrieved, then parsed into a series of 256x256 images.
image_connection_name = f'mitochondria_image_source{suffix}'
image_connection_type = "HTTP"
image_connection_argument = {
'cell_images':'https://storage.googleapis.com/wallaroo-public-data/csa_demo/computer-vision/examples/medical/bio-labs/atl-lab/images/ms-01-atl-3-22-23_9-50.tiff',
'ground_truth_masks': 'https://storage.googleapis.com/wallaroo-public-data/csa_demo/computer-vision/examples/medical/bio-labs/atl-lab/masks/ms-01-atl-3-22-23_9-50-masks.tiff'
}
connection = wl.create_connection(image_connection_name, image_connection_type, image_connection_argument)
Retrieve Images
We’ll use our new connection to reach out, retrieve the images and store them locally for processing. The information is stored in the details()
method for the connection, which is hidden by default when showing the connection, but the data can be retrieved when necessary.
inference_source_connection = wl.get_connection(name=image_connection_name)
display(inference_source_connection)
Field | Value |
---|---|
Name | mitochondria_image_sourcepdrv |
Connection Type | HTTP |
Details | ***** |
Created At | 2023-07-14T15:32:00.930954+00:00 |
Linked Workspaces | [] |
patches_dict = tiff_utils.build_patches("downloaded_patches",
(256,256),
256,
inference_source_connection.details()['cell_images'],
inference_source_connection.details()['ground_truth_masks'] )
created dir downloaded_patches/ms-01-atl-3-22-23_9-50
saving file downloaded_patches/ms-01-atl-3-22-23_9-50/ms-01-atl-3-22-23_9-50.tiff
Retrieve Image and Convert to Data
The next step is to process the image into a numpy array that the model is trained to detect from.
We start by retrieving all the patch images from a recorded time series tiff recorded on one of our microscopes.
sample_mitochondria_patches_path = "./downloaded_patches/ms-01-atl-3-22-23_9-50"
patches = tiff_utils.get_all_patches(sample_mitochondria_patches_path)
Randomly we will retrieve a 256x256 patch image and use it to do our semantic segmentation prediction.
We’ll then convert it into a numpy array and insert into a DataFrame for a single inference.
The following helper function loadImageAndConvertTiff
is used to convert the image into a numpy, then insert that into the DataFrame. This allows a later command to take the randomly grabbed image perform the process on other images.
def loadImageAndConvertTiff(imagePath, width, height):
img = cv2.imread(imagePath, 0)
imgNorm = np.expand_dims(normalize(np.array(img), axis=1),2)
imgNorm=imgNorm[:,:,0][:,:,None]
imgNorm=np.expand_dims(imgNorm, 0)
resizedImage = None
#creates a dictionary with the wallaroo "tensor" key and the numpy ndim array representing image as the value.
dictData = {"tensor":[imgNorm]}
dataframedata = pd.DataFrame(dictData)
# display(dataframedata)
return dataframedata, resizedImage
def run_semantic_segmentation_inference(pipeline, input_tiff_image, width, height, threshold):
tensor, resizedImage = loadImageAndConvertTiff(input_tiff_image, width, height)
# print(tensor)
# #
# # run inference on the 256x256 patch image get the predicted mitochandria mask
# #
output = pipeline.infer(tensor)
# print(output)
# # Obtain the flattened predicted mitochandria mask result
list1d = output.loc[0]["out.conv2d_37"]
np1d = np.array(list1d)
# # unflatten it
predicted_mask = np1d.reshape(1,width,height,1)
# # perform the element-wise comaprison operation using the threshold provided
predicted_mask = (predicted_mask[0,:,:,0] > threshold).astype(np.uint8)
# return predicted_mask
return predicted_mask
Infer and Display Results
We will now perform our inferences and display the results. This results in a predicted mask showing us where the mitochondria cells are located.
- The first image is the input image.
- The 2nd image is the ground truth. The mask was created by a human who identified the mitochondria cells in the input image
- The 3rd image is the predicted mask after running inference on the Wallaroo pipeline.
We’ll perform this 10 times to show how quickly the inferences can be submitted.
for x in range(10):
# get a sample 256x256 mitochondria image
random_patch = tiff_utils.get_random_patch_sample(patches)
# build the path to the image
patch_image_path = sample_mitochondria_patches_path + "/images/" + random_patch['patch_image_file']
# run inference in order to get the predicted 256x256 mask
predicted_mask = run_semantic_segmentation_inference(pipeline, patch_image_path, 256,256, 0.2)
# # plot the results
test_image = random_patch['patch_image'][:,:,0]
test_image_title = f"Testing Image - {random_patch['index']}"
ground_truth_image = random_patch['patch_mask'][:,:,0]
ground_truth_image_title = "Ground Truth Mask"
predicted_mask_title = 'Predicted Mask'
tiff_utils.plot_test_results(test_image, test_image_title, \
ground_truth_image, ground_truth_image_title, \
predicted_mask, predicted_mask_title)
Complete Tutorial
With the demonstration complete, the pipeline is undeployed and the resources returned back to the Wallaroo instance.
pipeline.undeploy()
name | biolabsconnectionpipelinepdrv |
---|---|
created | 2023-07-14 15:29:16.969523+00:00 |
last_updated | 2023-07-14 15:29:23.004729+00:00 |
deployed | False |
tags | |
versions | 0daae212-f578-4723-a0e5-edbcfa548976, 143eb35c-8606-471c-8053-230640249ad6, 79d3b6ca-823f-406d-bf23-88a23b49ff1b |
steps | biolabsconnectionmodelpdrv |
3.5 - CLIP ViT-B/32 Transformer Demonstration with Wallaroo
This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.
CLIP ViT-B/32 Transformer Demonstration with Wallaroo
The following tutorial demonstrates deploying and performing sample inferences with the Hugging Face CLIP ViT-B/32 Transformer model.
Prerequisites
This tutorial is geared towards the Wallaroo version 2023.2.1 and above. The model clip-vit-base-patch-32.zip
must be downloaded and placed into the ./models
directory. This is available from the following URL:
https://storage.googleapis.com/wallaroo-public-data/hf-clip-vit-b32/clip-vit-base-patch-32.zip
If performing this tutorial from outside the Wallaroo JupyterHub environment, install the Wallaroo SDK.
Steps
Imports
The first step is to import the libraries used for the example.
import json
import os
import requests
import wallaroo
from wallaroo.pipeline import Pipeline
from wallaroo.deployment_config import DeploymentConfigBuilder
from wallaroo.framework import Framework
from wallaroo.object import EntityNotFoundError
import pyarrow as pa
import numpy as np
import pandas as pd
from PIL import Image
Connect to the Wallaroo Instance
The first step is to connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. For more information on Wallaroo Client settings, see the Client Connection guide.
## blank space to log in
wl = wallaroo.Client()
## blank space to log in
wl = wallaroo.Client()
Set Workspace and Pipeline
The next step is to create the Wallaroo workspace and pipeline used for the inference requests.
- References
## convenience functions from the previous notebooks
# return the workspace called <name> through the Wallaroo client.
def get_workspace(name, client):
workspace = None
for ws in client.list_workspaces():
if ws.name() == name:
workspace= ws
return workspace
# if no workspaces were found
if workspace==None:
workspace = wl.create_workspace(name)
return workspace
# get a pipeline by name in the workspace
def get_pipeline(pipeline_name, workspace, client):
plist = workspace.pipelines()
pipeline = [p for p in plist if p.name() == pipeline_name]
if len(pipeline) <= 0:
pipeline = client.build_pipeline(pipeline_name)
return pipeline
return pipeline[0]
# create the workspace and pipeline
workspace_name = 'clip-demo'
pipeline_name = 'clip-demo'
workspace = get_workspace(workspace_name, wl)
wl.set_current_workspace(workspace)
display(wl.get_current_workspace())
pipeline = get_pipeline(pipeline_name, workspace, wl)
pipeline
{'name': 'clip-demo', 'id': 8, 'archived': False, 'created_by': '12ea09d1-0f49-405e-bed1-27eb6d10fde4', 'created_at': '2023-11-20T18:57:53.667873+00:00', 'models': [], 'pipelines': [{'name': 'clip-demo', 'create_time': datetime.datetime(2023, 11, 20, 18, 57, 55, 550690, tzinfo=tzutc()), 'definition': '[]'}]}
name | clip-demo |
---|---|
created | 2023-11-20 18:57:55.550690+00:00 |
last_updated | 2023-11-20 18:57:55.550690+00:00 |
deployed | (none) |
arch | None |
tags | |
versions | ff0e4846-f0f9-4fb0-bc06-d6daa21797bf |
steps | |
published | False |
Configure and Upload Model
The 🤗 Hugging Face model is uploaded to Wallaroo by defining the input and output schema, and specifying the model’s framework as wallaroo.framework.Framework.HUGGING_FACE_ZERO_SHOT_IMAGE_CLASSIFICATION
.
The data schemas are defined in Apache PyArrow Schema format.
The model is converted to the Wallaroo Containerized runtime after the upload is complete.
input_schema = pa.schema([
pa.field('inputs', # required, fixed image dimensions
pa.list_(
pa.list_(
pa.list_(
pa.int64(),
list_size=3
),
list_size=640
),
list_size=480
)),
pa.field('candidate_labels', pa.list_(pa.string(), list_size=4)), # required, equivalent to `options` in the provided demo
])
output_schema = pa.schema([
pa.field('score', pa.list_(pa.float64(), list_size=4)), # has to be same as number of candidate labels
pa.field('label', pa.list_(pa.string(), list_size=4)), # has to be same as number of candidate labels
])
Upload Model
model = wl.upload_model('clip-vit', './models/clip-vit-base-patch-32.zip',
framework=Framework.HUGGING_FACE_ZERO_SHOT_IMAGE_CLASSIFICATION,
input_schema=input_schema,
output_schema=output_schema)
model
Waiting for model loading - this will take up to 10.0min.
Model is pending loading to a container runtime..
Model is attempting loading to a container runtime............................................................successful
Ready
Name | clip-vit |
Version | 57594cc7-7db1-43c3-b21a-6dcaba846f26 |
File Name | clip-vit-base-patch-32.zip |
SHA | 4efc24685a14e1682301cc0085b9db931aeb5f3f8247854bedc6863275ed0646 |
Status | ready |
Image Path | proxy.replicated.com/proxy/wallaroo/ghcr.io/wallaroolabs/mlflow-deploy:v2023.4.0-4103 |
Architecture | None |
Updated At | 2023-20-Nov 20:51:44 |
Deploy Pipeline
With the model uploaded and prepared, we add the model as a pipeline step and deploy it. For this example, we will allocate 4 Gi of RAM and 1 CPU to the model’s use through the pipeline deployment configuration.
deployment_config = wallaroo.DeploymentConfigBuilder() \
.cpus(.25).memory('1Gi') \
.sidekick_memory(model, '4Gi') \
.sidekick_cpus(model, 1.0) \
.build()
pipeline.clear()
pipeline.add_model_step(model)
pipeline.deploy(deployment_config=deployment_config)
name | clip-demo |
---|---|
created | 2023-11-20 18:57:55.550690+00:00 |
last_updated | 2023-11-20 20:51:59.304917+00:00 |
deployed | True |
arch | None |
tags | |
versions | 7a6168c2-f807-465f-a122-33fe7b5d8a3d, ff0e4846-f0f9-4fb0-bc06-d6daa21797bf |
steps | clip-vit |
published | False |
Run Inference
We verify the pipeline is deployed by checking the status()
.
The sample images in the ./data
directory are converted into numpy arrays, and the candidate labels added as inputs. Both are set as DataFrame arrays where the field inputs
are the image values, and candidate_labels
the labels.
pipeline.status()
{'status': 'Running',
'details': [],
'engines': [{'ip': '10.244.3.140',
'name': 'engine-bb48bb959-2xctn',
'status': 'Running',
'reason': None,
'details': [],
'pipeline_statuses': {'pipelines': [{'id': 'clip-demo',
'status': 'Running'}]},
'model_statuses': {'models': [{'name': 'clip-vit',
'version': '57594cc7-7db1-43c3-b21a-6dcaba846f26',
'sha': '4efc24685a14e1682301cc0085b9db931aeb5f3f8247854bedc6863275ed0646',
'status': 'Running'}]}}],
'engine_lbs': [{'ip': '10.244.4.152',
'name': 'engine-lb-584f54c899-gcbpj',
'status': 'Running',
'reason': None,
'details': []}],
'sidekicks': [{'ip': '10.244.3.139',
'name': 'engine-sidekick-clip-vit-11-75c5666b69-tqfbk',
'status': 'Running',
'reason': None,
'details': [],
'statuses': '\n'}]}
image_paths = [
"./data/bear-in-tree.jpg",
"./data/elephant-and-zebras.jpg",
"./data/horse-and-dogs.jpg",
"./data/kittens.jpg",
"./data/remote-monitor.jpg"
]
images = []
for iu in image_paths:
image = Image.open(iu)
image = image.resize((640, 480)) # fixed image dimensions
images.append(np.array(image))
dataframe = pd.DataFrame({"images": images})
input_data = {
"inputs": images,
"candidate_labels": [["cat", "dog", "horse", "elephant"]] * 5,
}
dataframe = pd.DataFrame(input_data)
dataframe
inputs | candidate_labels | |
---|---|---|
0 | [[[60, 62, 61], [62, 64, 63], [67, 69, 68], [7... | [cat, dog, horse, elephant] |
1 | [[[228, 235, 241], [229, 236, 242], [230, 237,... | [cat, dog, horse, elephant] |
2 | [[[177, 177, 177], [177, 177, 177], [177, 177,... | [cat, dog, horse, elephant] |
3 | [[[140, 25, 56], [144, 25, 67], [146, 24, 73],... | [cat, dog, horse, elephant] |
4 | [[[24, 20, 11], [22, 18, 9], [18, 14, 5], [21,... | [cat, dog, horse, elephant] |
Inference Outputs
The inference is run, and the labels with their corresponding confidence values for each label are mapped to out.label
and out.score
for each image.
results = pipeline.infer(dataframe,timeout=600,dataset=["in", "out", "metadata.elapsed", "time", "check_failures"])
pd.set_option('display.max_colwidth', None)
display(results.loc[:, ['out.label', 'out.score']])
out.label | out.score | |
---|---|---|
0 | [elephant, dog, horse, cat] | [0.4146825075149536, 0.3483847379684448, 0.1285749077796936, 0.10835790634155273] |
1 | [elephant, horse, dog, cat] | [0.9981434345245361, 0.001765866531059146, 6.823801231803373e-05, 2.2441448891186155e-05] |
2 | [horse, dog, elephant, cat] | [0.7596800923347473, 0.217111736536026, 0.020392831414937973, 0.0028152535669505596] |
3 | [cat, dog, elephant, horse] | [0.9870228171348572, 0.0066468678414821625, 0.0032716328278183937, 0.003058753442019224] |
4 | [dog, horse, cat, elephant] | [0.5713965892791748, 0.17229433357715607, 0.15523898601531982, 0.1010700911283493] |
Undeploy Pipelines
With the tutorial complete, the pipeline is undeployed and the resources returned back to the cluster.
pipeline.undeploy()
name | clip-demo |
---|---|
created | 2023-11-20 18:57:55.550690+00:00 |
last_updated | 2023-11-20 20:51:59.304917+00:00 |
deployed | False |
arch | None |
tags | |
versions | 7a6168c2-f807-465f-a122-33fe7b5d8a3d, ff0e4846-f0f9-4fb0-bc06-d6daa21797bf |
steps | clip-vit |
published | False |
3.6 - Containerized MLFlow Tutorial
Setting Private Registry Configuration in Wallaroo
Configure Via Kots
If Wallaroo was installed via kots
, use the following procedure to add the private model registry information.
Launch the Wallaroo Administrative Dashboard through a terminal linked to the Kubernetes cluster. Replace the namespace with the one used in your installation.
kubectl kots admin-console --namespace wallaroo
Launch the dashboard, by default at
http://localhost:8800
.From the admin dashboard, select Config -> Private Model Container Registry.
Enable Provide private container registry credentials for model images.
Provide the following:
- Registry URL: The URL of the Containerized Model Container Registry. Typically in the format
host:port
. In this example, the registry for GitHub is used. NOTE: When setting the URL for the Containerized Model Container Registry, only the actual service address is needed. For example: with the full URL of the model asghcr.io/wallaroolabs/wallaroo_tutorials/mlflow-statsmodels-example:2022.4
, the URL would beghcr.io/wallaroolabs
. - email: The email address of the user authenticating to the registry service.
- username: The username of the user authentication to the registry service.
- password: The password of the user authentication or token to the registry service.
- Registry URL: The URL of the Containerized Model Container Registry. Typically in the format
Scroll down and select Save config.
Deploy the new version.
Once complete, the Wallaroo instance will be able to authenticate to the Containerized Model Container Registry and retrieve the images.
Configure via Helm
During either the installation process or updates, set the following in the
local-values.yaml
file:privateModelRegistry
:enabled
: truesecretName
:model-registry-secret
registry
: The URL of the private registry.email
: The email address of the user authenticating to the registry service.username
: The username of the user authentication to the registry service.password
: The password of the user authentication to the registry service.
For example:
# Other settings - DNS entries, etc. # The private registry settings privateModelRegistry: enabled: true secretName: model-registry-secret registry: "YOUR REGISTRY URL:YOUR REGISTRY PORT" email: "YOUR EMAIL ADDRESS" username: "YOUR USERNAME" password: "Your Password here"
Install or update the Wallaroo instance via Helm as per the Wallaroo Helm Install instructions.
Once complete, the Wallaroo instance will be able to authenticate to the registry service and retrieve the images.
This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.
MLFlow Inference with Wallaroo Tutorial
Wallaroo users can register their trained MLFlow ML Models from a containerized model container registry into their Wallaroo instance and perform inferences with it through a Wallaroo pipeline.
As of this time, Wallaroo only supports MLFlow 1.3.0 containerized models. For information on how to containerize an MLFlow model, see the MLFlow Documentation.
This tutorial assumes that you have a Wallaroo instance, and have either your own containerized model or use the one from the reference and are running this Notebook from the Wallaroo Jupyter Hub service.
See the Wallaroo Private Containerized Model Container Registry Guide for details on how to configure a Wallaroo instance with a private model registry.
MLFlow Data Formats
When using containerized MLFlow models with Wallaroo, the inputs and outputs must be named. For example, the following output:
[-12.045839810372835]
Would need to be wrapped with the data values named:
[{"prediction": -12.045839810372835}]
A short sample code for wrapping data may be:
output_df = pd.DataFrame(prediction, columns=["prediction"])
return output_df
MLFlow Models and Wallaroo
MLFlow models are composed of two parts: the model, and the flavors. When submitting a MLFlow model to Wallaroo, both aspects must be part of the ML Model included in the container. For full information about MLFlow model structure, see the MLFlow Documentation.
Wallaroo registers the models from container registries. Organizations will either have to make their containers available in a public or through a private Containerized Model Container Registry service. For examples on setting up a private container registry service, see the Docker Documentation “Deploy a registry server”. For more details on setting up a container registry in a cloud environment, see the related documentation for your preferred cloud provider:
- Google Cloud Platform Container Registry
- Amazon Web Services Elastic Container Registry
- Microsoft Azure Container Registry
For this example, we will be using the MLFlow containers that was registered in a GitHub container registry service in MLFlow Creation Tutorial Part 03: Container Registration. The address of those containers are:
- postprocess: ghcr.io/johnhansarickwallaroo/mlflowtests/mlflow-postprocess-example . Used for format data after the statsmodel inferences.
- statsmodel: ghcr.io/johnhansarickwallaroo/mlflowtests/mlflow-statsmodels-example . The statsmodel generated in MLFlow Creation Tutorial Part 01: Model Creation.
Prerequisites
Before uploading and running an inference with a MLFlow model in Wallaroo the following will be required:
- MLFlow Input Schema: The input schema with the fields and data types for each MLFlow model type uploaded to Wallaroo. In the examples below, the data types are imported using the
pyarrow
library. - An installed Wallaroo instance.
- The following Python libraries installed:
os
wallaroo
: The Wallaroo SDK. Included with the Wallaroo JupyterHub service by default.
IMPORTANT NOTE: Wallaroo supports MLFlow 1.3.0. Please ensure the MLFlow models used in Wallaroo meet this specification.
MLFlow Inference Steps
To register a containerized MLFlow ML Model into Wallaroo, use the following general step:
- Import Libraries
- Connect to Wallaroo
- Set MLFlow Input Schemas
- Register MLFlow Model
- Create Pipeline and Add Model Steps
- Run Inference
Import Libraries
We start by importing the libraries we will need to connect to Wallaroo and use our MLFlow models. This includes the wallaroo
libraries, pyarrow
for data types, and the json
library for handling JSON data.
import wallaroo
from wallaroo.object import EntityNotFoundError
import pyarrow as pa
import pandas as pd
Connect to the Wallaroo Instance
The first step is to connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. For more information on Wallaroo Client settings, see the Client Connection guide.
# Login through local Wallaroo instance
wl = wallaroo.Client()
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
pipeline = wl.build_pipeline(name)
return pipeline
prefix = 'mlflow'
workspace_name= f"{prefix}statsmodelworkspace"
pipeline_name = f"{prefix}statsmodelpipeline"
mlflowworkspace = get_workspace(workspace_name)
wl.set_current_workspace(mlflowworkspace)
pipeline = get_pipeline(pipeline_name)
Set MLFlow Input Schemas
Set the MLFlow input schemas through the pyarrow
library. In the examples below, the input schemas for both the MLFlow model statsmodels-test
and the statsmodels-test-postprocess
model.
sm_input_schema = pa.schema([
pa.field('temp', pa.float32()),
pa.field('holiday', pa.uint8()),
pa.field('workingday', pa.uint8()),
pa.field('windspeed', pa.float32())
])
pp_input_schema = pa.schema([
pa.field('predicted_mean', pa.float32())
])
Register MLFlow Model
Use the register_model_image
method to register the Docker container containing the MLFlow models.
statsmodelUrl = "ghcr.io/wallaroolabs/wallaroo_tutorials/mlflow-statsmodels-example:2023.1"
postprocessUrl = "ghcr.io/wallaroolabs/wallaroo_tutorials/mlflow-postprocess-example:2023.1"
sm_model = wl.register_model_image(
name=f"{prefix}statmodels",
image=f"{statsmodelUrl}"
).configure("mlflow", input_schema=sm_input_schema, output_schema=pp_input_schema)
pp_model = wl.register_model_image(
name=f"{prefix}postprocess",
image=f"{postprocessUrl}"
).configure("mlflow", input_schema=pp_input_schema, output_schema=pp_input_schema)
Create Pipeline and Add Model Steps
With the models registered, we can add the MLFlow models as steps in the pipeline. Once ready, we will deploy the pipeline so it is available for submitting data for running inferences.
pipeline.add_model_step(sm_model)
pipeline.add_model_step(pp_model)
name | mlflowstatsmodelpipeline |
---|---|
created | 2023-07-14 15:32:52.546581+00:00 |
last_updated | 2023-07-14 15:32:52.546581+00:00 |
deployed | (none) |
tags | |
versions | efa60e66-b2b7-4bb2-bc37-dd9ea7377467 |
steps |
from wallaroo.deployment_config import DeploymentConfigBuilder
deployment_config = DeploymentConfigBuilder() \
.cpus(0.25).memory('1Gi') \
.sidekick_env(sm_model, {"GUNICORN_CMD_ARGS": "--timeout=180 --workers=1"}) \
.sidekick_env(pp_model, {"GUNICORN_CMD_ARGS": "--timeout=180 --workers=1"}) \
.build()
pipeline.deploy(deployment_config=deployment_config)
name | mlflowstatsmodelpipeline |
---|---|
created | 2023-07-14 15:32:52.546581+00:00 |
last_updated | 2023-07-14 15:35:09.901078+00:00 |
deployed | True |
tags | |
versions | 2f8e9483-a9ad-436e-9068-7826cd8166c4, 539aae6b-4437-4f8c-8be0-db9ef11e80fb, efa60e66-b2b7-4bb2-bc37-dd9ea7377467 |
steps | mlflowstatmodels |
pipeline.status()
{'status': 'Running',
'details': [],
'engines': [{'ip': '10.244.3.142',
'name': 'engine-c55d468-qgk48',
'status': 'Running',
'reason': None,
'details': [],
'pipeline_statuses': {'pipelines': [{'id': 'mlflowstatsmodelpipeline',
'status': 'Running'}]},
'model_statuses': {'models': [{'name': 'mlflowpostprocess',
'version': 'b7cd3426-825a-467d-8616-011ae8901d23',
'sha': '825ebae48014d297134930028ab0e823bc0d9551334b9a4402c87a714e8156b2',
'status': 'Running'},
{'name': 'mlflowstatmodels',
'version': '1a547bbb-36c7-4b5c-93a8-f357e3542d6f',
'sha': '3afd13d9c5070679e284050cd099e84aa2e5cb7c08a788b21d6cb2397615d018',
'status': 'Running'}]}}],
'engine_lbs': [{'ip': '10.244.4.188',
'name': 'engine-lb-584f54c899-xhdmq',
'status': 'Running',
'reason': None,
'details': []}],
'sidekicks': [{'ip': '10.244.0.72',
'name': 'engine-sidekick-mlflowstatmodels-27-786dc6b47c-bprqb',
'status': 'Running',
'reason': None,
'details': [],
'statuses': '\n'},
{'ip': '10.244.0.73',
'name': 'engine-sidekick-mlflowpostprocess-28-9fff54995-2n2nn',
'status': 'Running',
'reason': None,
'details': [],
'statuses': '\n'}]}
Run Inference
Once the pipeline is running, we can submit our data to the pipeline and return our results. Once finished, we will undeploy the pipeline to return the resources back to the cluster.
# Initial container run may need extra time to finish deploying - adding 90 second timeout to compensate
results = pipeline.infer_from_file('./resources/bike_day_eval_engine.df.json', timeout=90)
display(results.loc[:,["out.predicted_mean"]])
out.predicted_mean | |
---|---|
0 | 0.281983 |
1 | 0.658847 |
2 | 0.572368 |
3 | 0.619873 |
4 | -1.217801 |
5 | -1.849156 |
6 | 0.933885 |
pipeline.undeploy()
name | mlflowstatsmodelpipeline |
---|---|
created | 2023-07-14 15:32:52.546581+00:00 |
last_updated | 2023-07-14 15:32:56.591792+00:00 |
deployed | False |
tags | |
versions | 539aae6b-4437-4f8c-8be0-db9ef11e80fb, efa60e66-b2b7-4bb2-bc37-dd9ea7377467 |
steps | mlflowstatmodels |
3.7 - Demand Curve Quick Start Guide
This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.
Demand Curve Pipeline Tutorial
This worksheet demonstrates a Wallaroo pipeline with data preprocessing, a model, and data postprocessing.
The model is a “demand curve” that predicts the expected number of units of a product that will be sold to a customer as a function of unit price and facts about the customer. Such models can be used for price optimization or sales volume forecasting. This is purely a “toy” demonstration, but is useful for detailing the process of working with models and pipelines.
Data preprocessing is required to create the features used by the model. Simple postprocessing prevents nonsensical estimates (e.g. negative units sold).
Prerequisites
- An installed Wallaroo instance.
- The following Python libraries installed:
os
wallaroo
: The Wallaroo SDK. Included with the Wallaroo JupyterHub service by default.
import json
import wallaroo
from wallaroo.object import EntityNotFoundError
import pandas
import numpy
import conversion
from wallaroo.object import EntityNotFoundError
import pyarrow as pa
# used to display dataframe information without truncating
from IPython.display import display
import pandas as pd
pd.set_option('display.max_colwidth', None)
# ignoring warnings for demonstration
import warnings
warnings.filterwarnings('ignore')
Connect to the Wallaroo Instance
The first step is to connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. For more information on Wallaroo Client settings, see the Client Connection guide.
# Login through local Wallaroo instance
wl = wallaroo.Client()
Now that the Wallaroo client has been initialized, we can create the workspace and call it demandcurveworkspace
, then set it as our current workspace. We’ll also create our pipeline so it’s ready when we add our models to it.
We’ll set some variables and methods to create our workspace, pipelines and models. Note that as of the July 2022 release of Wallaroo, workspace names must be unique. Pipelines with the same name will be created as a new version when built.
workspace_name = 'demandcurveworkspace'
pipeline_name = 'demandcurvepipeline'
model_name = 'demandcurvemodel'
model_file_name = './demand_curve_v1.onnx'
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
pipeline = wl.build_pipeline(name)
return pipeline
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
demandcurve_pipeline = get_pipeline(pipeline_name)
demandcurve_pipeline
name | demandcurvepipeline |
---|---|
created | 2023-09-11 18:28:08.036841+00:00 |
last_updated | 2023-09-11 18:28:08.036841+00:00 |
deployed | (none) |
tags | |
versions | 6e7452b9-1464-488b-9a28-85ff765f18d6 |
steps | |
published | False |
With our workspace established, we’ll upload two models:
demand_curve_v1.onnx
: Our demand_curve model. We’ll store the upload configuration intodemand_curve_model
.postprocess.py
: A Python step that will zero out any negative values and return the output variable as “prediction”.
Note that the order we upload our models isn’t important - we’ll be establishing the actual process of moving data from one model to the next when we set up our pipeline.
demand_curve_model = wl.upload_model(model_name, model_file_name, framework=wallaroo.framework.Framework.ONNX)
preprocess_input_schema = pa.schema([
pa.field('Date', pa.string()),
pa.field('cust_known', pa.bool_()),
pa.field('StockCode', pa.int32()),
pa.field('UnitPrice', pa.float32()),
pa.field('UnitsSold', pa.int32())
])
preprocess_input_output = pa.schema([
pa.field('tensor', pa.list_(pa.float64()))
])
preprocess_step = (wl.upload_model('curve-preprocess',
'./preprocess.py',
framework=wallaroo.framework.Framework.PYTHON)
.configure(
'python',
input_schema=preprocess_input_schema,
output_schema=preprocess_input_output
)
)
input_schema = pa.schema([
pa.field('variable', pa.list_(pa.float64()))
])
output_schema = pa.schema([
pa.field('prediction', pa.list_(pa.float64()))
])
step = (wl.upload_model('curve-postprocess',
'./postprocess.py',
framework=wallaroo.framework.Framework.PYTHON)
.configure(
'python',
input_schema=input_schema,
output_schema=output_schema
)
)
With our models uploaded, we’re going to create our own pipeline and give it three steps:
- First, we apply the data to our
demand_curve_model
. - And finally, we prepare our data for output with the
module_post
.
# now make a pipeline
demandcurve_pipeline.undeploy()
demandcurve_pipeline.clear()
demandcurve_pipeline.add_model_step(preprocess_step)
demandcurve_pipeline.add_model_step(demand_curve_model)
demandcurve_pipeline.add_model_step(step)
name | demandcurvepipeline |
---|---|
created | 2023-09-11 18:28:08.036841+00:00 |
last_updated | 2023-09-11 18:28:08.036841+00:00 |
deployed | (none) |
tags | |
versions | 6e7452b9-1464-488b-9a28-85ff765f18d6 |
steps | |
published | False |
And with that - let’s deploy our model pipeline. This usually takes about 45 seconds for the deployment to finish.
deploy_config = wallaroo.DeploymentConfigBuilder().replica_count(1).cpus(1).memory("1Gi").build()
demandcurve_pipeline.deploy(deployment_config=deploy_config)
name | demandcurvepipeline |
---|---|
created | 2023-09-11 18:28:08.036841+00:00 |
last_updated | 2023-09-11 18:28:14.101496+00:00 |
deployed | True |
tags | |
versions | cf1e4357-c98c-490d-822f-8c252a43ff99, 6e7452b9-1464-488b-9a28-85ff765f18d6 |
steps | curve-preprocess |
published | False |
We can check the status of our pipeline to make sure everything was set up correctly:
demandcurve_pipeline.status()
{'status': 'Running',
'details': [],
'engines': [{'ip': '10.244.3.164',
'name': 'engine-68597cfb4-6fxxd',
'status': 'Running',
'reason': None,
'details': [],
'pipeline_statuses': {'pipelines': [{'id': 'demandcurvepipeline',
'status': 'Running'}]},
'model_statuses': {'models': [{'name': 'curve-preprocess',
'version': '15ea1ce0-2091-4f18-8759-266d113cfa86',
'sha': '3b2b32fb6408ccee5a1886ef5cdb493692080ff6699f49de518b20d9a6f6a133',
'status': 'Running'},
{'name': 'demandcurvemodel',
'version': 'c9a1b515-8302-49f2-a772-3a32a182bad3',
'sha': '2820b42c9e778ae259918315f25afc8685ecab9967bad0a3d241e6191b414a0d',
'status': 'Running'},
{'name': 'curve-postprocess',
'version': '76f7f617-8e29-4aa8-a06e-563228581d79',
'sha': 'ecf1a555bb27bcf62bfa42922cf69db23e7188f456015fe8299f02867c3167b2',
'status': 'Running'}]}}],
'engine_lbs': [{'ip': '10.244.4.200',
'name': 'engine-lb-584f54c899-lqm67',
'status': 'Running',
'reason': None,
'details': []}],
'sidekicks': []}
Everything is ready. Let’s feed our pipeline some data. We have some information prepared with the daily_purchasses.csv
spreadsheet. We’ll start with just one row to make sure that everything is working correctly.
# read in some purchase data
purchases = pandas.read_csv('daily_purchases.csv')
# start with a one-row data frame for testing
subsamp_raw = purchases.iloc[0:1,: ]
subsamp_raw
Date | cust_known | StockCode | UnitPrice | UnitsSold | |
---|---|---|---|---|---|
0 | 2010-12-01 | False | 21928 | 4.21 | 1 |
result = demandcurve_pipeline.infer(subsamp_raw)
display(result)
time | in.Date | in.StockCode | in.UnitPrice | in.UnitsSold | in.cust_known | out.prediction | check_failures | |
---|---|---|---|---|---|---|---|---|
0 | 2023-09-11 18:28:28.627 | 2010-12-01 | 21928 | 4.21 | 1 | False | [6.68025518653071] | 0 |
We can see from the out.prediction
field that the demand curve has a predicted slope of 6.68 from our sample data. We can isolate that by specifying just the data output below.
display(result.loc[0, ['out.prediction']][0])
[6.68025518653071]
Bulk Inference
The initial test went perfectly. Now let’s throw some more data into our pipeline. We’ll draw 10 random rows from our spreadsheet, perform an inference from that, and then display the results and the logs showing the pipeline’s actions.
ix = numpy.random.choice(purchases.shape[0], size=10, replace=False)
converted = conversion.pandas_to_dict(purchases.iloc[ix,: ])
test_df = pd.DataFrame(converted['query'], columns=converted['colnames'])
display(test_df)
output = demandcurve_pipeline.infer(test_df)
display(output)
Date | cust_known | StockCode | UnitPrice | UnitsSold | |
---|---|---|---|---|---|
0 | 2011-09-15 | False | 20713 | 4.13 | 17 |
1 | 2011-11-30 | True | 85099C | 2.08 | 12 |
2 | 2011-10-13 | False | 85099B | 4.13 | 15 |
3 | 2011-08-11 | True | 22411 | 2.08 | 30 |
4 | 2011-08-10 | False | 23200 | 4.13 | 4 |
5 | 2011-11-03 | False | 22385 | 4.13 | 3 |
6 | 2011-09-19 | True | 85099B | 1.79 | 400 |
7 | 2011-09-18 | True | 20712 | 2.08 | 2 |
8 | 2011-05-09 | False | 23201 | 4.13 | 6 |
9 | 2011-11-17 | False | 85099B | 4.13 | 6 |
time | in.Date | in.StockCode | in.UnitPrice | in.UnitsSold | in.cust_known | out.prediction | check_failures | |
---|---|---|---|---|---|---|---|---|
0 | 2023-09-11 18:28:29.105 | 2011-09-15 | 20713 | 4.13 | 17 | False | [6.771545926800889] | 0 |
1 | 2023-09-11 18:28:29.105 | 2011-11-30 | 85099C | 2.08 | 12 | True | [33.125323160373426] | 0 |
2 | 2023-09-11 18:28:29.105 | 2011-10-13 | 85099B | 4.13 | 15 | False | [6.771545926800889] | 0 |
3 | 2023-09-11 18:28:29.105 | 2011-08-11 | 22411 | 2.08 | 30 | True | [33.125323160373426] | 0 |
4 | 2023-09-11 18:28:29.105 | 2011-08-10 | 23200 | 4.13 | 4 | False | [6.771545926800889] | 0 |
5 | 2023-09-11 18:28:29.105 | 2011-11-03 | 22385 | 4.13 | 3 | False | [6.771545926800889] | 0 |
6 | 2023-09-11 18:28:29.105 | 2011-09-19 | 85099B | 1.79 | 400 | True | [49.73419363867448] | 0 |
7 | 2023-09-11 18:28:29.105 | 2011-09-18 | 20712 | 2.08 | 2 | True | [33.125323160373426] | 0 |
8 | 2023-09-11 18:28:29.105 | 2011-05-09 | 23201 | 4.13 | 6 | False | [6.771545926800889] | 0 |
9 | 2023-09-11 18:28:29.105 | 2011-11-17 | 85099B | 4.13 | 6 | False | [6.771545926800889] | 0 |
Undeploy the Pipeline
Once we’ve finished with our demand curve demo, we’ll undeploy the pipeline and give the resources back to our Kubernetes cluster.
demandcurve_pipeline.undeploy()
name | demandcurvepipeline |
---|---|
created | 2023-09-11 18:28:08.036841+00:00 |
last_updated | 2023-09-11 18:28:14.101496+00:00 |
deployed | False |
tags | |
versions | cf1e4357-c98c-490d-822f-8c252a43ff99, 6e7452b9-1464-488b-9a28-85ff765f18d6 |
steps | curve-preprocess |
published | False |
Thank you for being a part of this demonstration. If you have additional questions, please feel free to contact us at Wallaroo.
3.8 - IMDB Tutorial
This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.
IMDB Sample
The following example demonstrates how to use Wallaroo with chained models. In this example, we will be using information from the IMDB (Internet Movie DataBase) with a sentiment model to detect whether a given review is positive or negative. Imagine using this to automatically scan Tweets regarding your product and finding either customers who need help or have nice things to say about your product.
Note that this example is considered a “toy” model - only the first 100 words in the review were tokenized, and the embedding is very small.
The following example is based on the Large Movie Review Dataset, and sample data can be downloaded from the aclIMDB dataset.
Prerequisites
- An installed Wallaroo instance.
- The following Python libraries installed:
import wallaroo
from wallaroo.object import EntityNotFoundError
# to display dataframe tables
from IPython.display import display
# used to display dataframe information without truncating
import pandas as pd
pd.set_option('display.max_colwidth', None)
import pyarrow as pa
print(wallaroo.__version__)
2023.2.1rc2
Connect to the Wallaroo Instance
The first step is to connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. For more information on Wallaroo Client settings, see the Client Connection guide.
# Login through local Wallaroo instance
wl = wallaroo.Client()
To test this model, we will perform the following:
- Create a workspace for our models.
- Upload two models:
embedder
: Takes pre-tokenized text documents (model input: 100 integers/datum; output 800 numbers/datum) and creates an embedding from them.sentiment
: The second model classifies the resulting embeddings from 0 to 1, which 0 being an unfavorable review, 1 being a favorable review.
- Create a pipeline that will take incoming data and pass it to the embedder, which will pass the output to the sentiment model, and then export the final result.
- To test it, we will use information that has already been tokenized and submit it to our pipeline and gauge the results.
Just for the sake of this tutorial, we’ll use the SDK below to create our workspace , assign as our current workspace, then display all of the workspaces we have at the moment. We’ll also set up for our models and pipelines down the road, so we have one spot to change names to whatever fits your organization’s standards best.
To allow this tutorial to be run multiple times or by multiple users in the same Wallaroo instance, a random 4 character prefix will be added to the workspace, pipeline, and model.
When we create our new workspace, we’ll save it in the Python variable workspace
so we can refer to it as needed.
First we’ll create a workspace for our environment, and call it imdbworkspace
. We’ll also set up our pipeline so it’s ready for our models.
import string
import random
# make a random 4 character prefix
prefix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
workspace_name = f'{prefix}imdbworkspace'
pipeline_name = f'{prefix}imdbpipeline'
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
pipeline = wl.build_pipeline(name)
return pipeline
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
imdb_pipeline = get_pipeline(pipeline_name)
imdb_pipeline
name | bggqimdbpipeline |
---|---|
created | 2023-07-14 15:29:46.732165+00:00 |
last_updated | 2023-07-14 15:29:46.732165+00:00 |
deployed | (none) |
tags | |
versions | 844d5e89-1cb8-44a3-b9ff-2dbf1e75db27 |
steps |
Just to make sure, let’s list our current workspace. If everything is going right, it will show us we’re in the imdb-workspace
.
wl.get_current_workspace()
{'name': 'bggqimdbworkspace', 'id': 21, 'archived': False, 'created_by': '4e296632-35b3-460e-85fe-565e311bc566', 'created_at': '2023-07-14T15:29:45.662776+00:00', 'models': [], 'pipelines': [{'name': 'bggqimdbpipeline', 'create_time': datetime.datetime(2023, 7, 14, 15, 29, 46, 732165, tzinfo=tzutc()), 'definition': '[]'}]}
Now we’ll upload our two models:
embedder.onnx
: This will be used to embed the tokenized documents for evaluation.sentiment_model.onnx
: This will be used to analyze the review and determine if it is a positive or negative review. The closer to 0, the more likely it is a negative review, while the closer to 1 the more likely it is to be a positive review.
embedder = wl.upload_model(f'{prefix}embedder-o', './embedder.onnx', framework=wallaroo.framework.Framework.ONNX).configure()
smodel = wl.upload_model(f'{prefix}smodel-o', './sentiment_model.onnx', framework=wallaroo.framework.Framework.ONNX).configure(runtime="onnx", tensor_fields=["flatten_1"])
With our models uploaded, now we’ll create our pipeline that will contain two steps:
- First, it runs the data through the embedder.
- Second, it applies it to our sentiment model.
# now make a pipeline
imdb_pipeline.add_model_step(embedder)
imdb_pipeline.add_model_step(smodel)
name | bggqimdbpipeline |
---|---|
created | 2023-07-14 15:29:46.732165+00:00 |
last_updated | 2023-07-14 15:29:46.732165+00:00 |
deployed | (none) |
tags | |
versions | 844d5e89-1cb8-44a3-b9ff-2dbf1e75db27 |
steps |
Now that we have our pipeline set up with the steps, we can deploy the pipeline.
imdb_pipeline.deploy()
name | bggqimdbpipeline |
---|---|
created | 2023-07-14 15:29:46.732165+00:00 |
last_updated | 2023-07-14 15:29:52.678281+00:00 |
deployed | True |
tags | |
versions | 8fd7b44a-f5d3-4291-b741-db398c478537, 844d5e89-1cb8-44a3-b9ff-2dbf1e75db27 |
steps | bggqembedder-o |
We’ll check the pipeline status to verify it’s deployed and the models are ready.
imdb_pipeline.status()
{'status': 'Running',
'details': [],
'engines': [{'ip': '10.244.3.140',
'name': 'engine-6cb454f6bb-4fz9b',
'status': 'Running',
'reason': None,
'details': [],
'pipeline_statuses': {'pipelines': [{'id': 'bggqimdbpipeline',
'status': 'Running'}]},
'model_statuses': {'models': [{'name': 'bggqsmodel-o',
'version': 'aaca7ea2-b6d6-471f-9c5f-ea194de3112b',
'sha': '3473ea8700fbf1a1a8bfb112554a0dde8aab36758030dcde94a9357a83fd5650',
'status': 'Running'},
{'name': 'bggqembedder-o',
'version': '2ef7af8b-1ad6-4ae9-b126-f836a05e9e37',
'sha': 'd083fd87fa84451904f71ab8b9adfa88580beb92ca77c046800f79780a20b7e4',
'status': 'Running'}]}}],
'engine_lbs': [{'ip': '10.244.4.186',
'name': 'engine-lb-584f54c899-5zpkl',
'status': 'Running',
'reason': None,
'details': []}],
'sidekicks': []}
To test this out, we’ll start with a single piece of information from our data directory.
singleton = pd.DataFrame.from_records(
[
{
"tensor":[
1607.0,
2635.0,
5749.0,
199.0,
49.0,
351.0,
16.0,
2919.0,
159.0,
5092.0,
2457.0,
8.0,
11.0,
1252.0,
507.0,
42.0,
287.0,
316.0,
15.0,
65.0,
136.0,
2.0,
133.0,
16.0,
4311.0,
131.0,
286.0,
153.0,
5.0,
2826.0,
175.0,
54.0,
548.0,
48.0,
1.0,
17.0,
9.0,
183.0,
1.0,
111.0,
15.0,
1.0,
17.0,
284.0,
982.0,
18.0,
28.0,
211.0,
1.0,
1382.0,
8.0,
146.0,
1.0,
19.0,
12.0,
9.0,
13.0,
21.0,
1898.0,
122.0,
14.0,
70.0,
14.0,
9.0,
97.0,
25.0,
74.0,
1.0,
189.0,
12.0,
9.0,
6.0,
31.0,
3.0,
244.0,
2497.0,
3659.0,
2.0,
665.0,
2497.0,
63.0,
180.0,
1.0,
17.0,
6.0,
287.0,
3.0,
646.0,
44.0,
15.0,
161.0,
50.0,
71.0,
438.0,
351.0,
31.0,
5749.0,
2.0,
0.0,
0.0
]
}
]
)
results = imdb_pipeline.infer(singleton)
display(results)
time | in.tensor | out.dense_1 | check_failures | |
---|---|---|---|---|
0 | 2023-07-14 15:30:09.930 | [1607.0, 2635.0, 5749.0, 199.0, 49.0, 351.0, 16.0, 2919.0, 159.0, 5092.0, 2457.0, 8.0, 11.0, 1252.0, 507.0, 42.0, 287.0, 316.0, 15.0, 65.0, 136.0, 2.0, 133.0, 16.0, 4311.0, 131.0, 286.0, 153.0, 5.0, 2826.0, 175.0, 54.0, 548.0, 48.0, 1.0, 17.0, 9.0, 183.0, 1.0, 111.0, 15.0, 1.0, 17.0, 284.0, 982.0, 18.0, 28.0, 211.0, 1.0, 1382.0, 8.0, 146.0, 1.0, 19.0, 12.0, 9.0, 13.0, 21.0, 1898.0, 122.0, 14.0, 70.0, 14.0, 9.0, 97.0, 25.0, 74.0, 1.0, 189.0, 12.0, 9.0, 6.0, 31.0, 3.0, 244.0, 2497.0, 3659.0, 2.0, 665.0, 2497.0, 63.0, 180.0, 1.0, 17.0, 6.0, 287.0, 3.0, 646.0, 44.0, 15.0, 161.0, 50.0, 71.0, 438.0, 351.0, 31.0, 5749.0, 2.0, 0.0, 0.0] | [0.37142318] | 0 |
Since that works, let’s load up all 50,000 rows and do a full inference on each of them via an Apache Arrow file. Wallaroo pipeline inferences use Apache Arrow as their core data type, making this inference fast.
We’ll do a demonstration with a pandas DataFrame and display the first 5 results.
results = imdb_pipeline.infer_from_file('./data/test_data_50K.arrow')
# using pandas DataFrame
outputs = results.to_pandas()
display(outputs.loc[:5, ["time","out.dense_1"]])
time | out.dense_1 | |
---|---|---|
0 | 2023-07-14 15:30:17.404 | [0.8980188] |
1 | 2023-07-14 15:30:17.404 | [0.056596935] |
2 | 2023-07-14 15:30:17.404 | [0.9260802] |
3 | 2023-07-14 15:30:17.404 | [0.926919] |
4 | 2023-07-14 15:30:17.404 | [0.6618577] |
5 | 2023-07-14 15:30:17.404 | [0.48736304] |
Undeploy
With our pipeline’s work done, we’ll undeploy it and give our Kubernetes environment back its resources.
imdb_pipeline.undeploy()
name | bggqimdbpipeline |
---|---|
created | 2023-07-14 15:29:46.732165+00:00 |
last_updated | 2023-07-14 15:29:52.678281+00:00 |
deployed | False |
tags | |
versions | 8fd7b44a-f5d3-4291-b741-db398c478537, 844d5e89-1cb8-44a3-b9ff-2dbf1e75db27 |
steps | bggqembedder-o |
And there is our example. Please feel free to contact us at Wallaroo for if you have any questions.
4 - Wallaroo Features Tutorials
The following tutorials are created to highlight specific features in Wallaroo.
4.1 - Hot Swap Models Tutorial
This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.
Model Hot Swap Tutorial
One of the biggest challenges facing organizations once they have a model trained is deploying the model: Getting all of the resources together, MLOps configured and systems prepared to allow inferences to run.
The next biggest challenge? Replacing the model while keeping the existing production systems running.
This tutorial demonstrates how Wallaroo model hot swap can update a pipeline step with a new model with one command. This lets organizations keep their production systems running while changing a ML model, with the change taking only milliseconds, and any inference requests in that time are processed after the hot swap is completed.
This example and sample data comes from the Machine Learning Group’s demonstration on Credit Card Fraud detection.
This tutorial provides the following:
- Models:
rf_model.onnx
: The champion model that has been used in this environment for some time.xgb_model.onnx
andgbr_model.onnx
: Rival models that we will swap out from the champion model.
- Data:
- xtest-1.df.json and xtest-1k.df.json: DataFrame JSON inference inputs with 1 input and 1,000 inputs.
- xtest-1.arrow and xtest-1k.arrow: Apache Arrow inference inputs with 1 input and 1,000 inputs.
Reference
For more information about Wallaroo and related features, see the Wallaroo Documentation Site.
Prerequisites
- A deployed Wallaroo instance
- The following Python libraries installed:
Steps
The following steps demonstrate the following:
- Connect to a Wallaroo instance.
- Create a workspace and pipeline.
- Upload both models to the workspace.
- Deploy the pipe with the
rf_model.onnx
model as a pipeline step. - Perform sample inferences.
- Hot swap and replace the existing model with the
xgb_model.onnx
while keeping the pipeline deployed. - Conduct additional inferences to demonstrate the model hot swap was successful.
- Hot swap again with gbr_model.onnx, and perform more sample inferences.
- Undeploy the pipeline and return the resources back to the Wallaroo instance.
Load the Libraries
Load the Python libraries used to connect and interact with the Wallaroo instance.
import wallaroo
from wallaroo.object import EntityNotFoundError
# to display dataframe tables
from IPython.display import display
# used to display dataframe information without truncating
import pandas as pd
pd.set_option('display.max_colwidth', None)
import pyarrow as pa
Connect to the Wallaroo Instance
The first step is to connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. For more information on Wallaroo Client settings, see the Client Connection guide.
# Login through local Wallaroo instance
wl = wallaroo.Client()
Set the Variables
The following variables are used in the later steps for creating the workspace, pipeline, and uploading the models. Modify them according to your organization’s requirements.
Just for the sake of this tutorial, we’ll use the SDK below to create our workspace , assign as our current workspace, then display all of the workspaces we have at the moment. We’ll also set up for our models and pipelines down the road, so we have one spot to change names to whatever fits your organization’s standards best.
To allow this tutorial to be run multiple times or by multiple users in the same Wallaroo instance, a random 4 character prefix will be added to the workspace, pipeline, and model.
import string
import random
# make a random 4 character prefix
prefix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
workspace_name = f'{prefix}hotswapworkspace'
pipeline_name = f'{prefix}hotswappipeline'
original_model_name = f'{prefix}housingmodelcontrol'
original_model_file_name = './models/rf_model.onnx'
replacement_model_name01 = f'{prefix}gbrhousingchallenger'
replacement_model_file_name01 = './models/gbr_model.onnx'
replacement_model_name02 = f'{prefix}xgbhousingchallenger'
replacement_model_file_name02 = './models/xgb_model.onnx'
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
pipeline = wl.build_pipeline(name)
return pipeline
Create the Workspace
We will create a workspace based on the variable names set above, and set the new workspace as the current
workspace. This workspace is where new pipelines will be created in and store uploaded models for this session.
Once set, the pipeline will be created.
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
pipeline = get_pipeline(pipeline_name)
pipeline
name | hjfkhotswappipeline |
---|---|
created | 2023-07-14 15:36:48.697941+00:00 |
last_updated | 2023-07-14 15:36:48.697941+00:00 |
deployed | (none) |
tags | |
versions | 2b84d42b-bda9-4cc8-b182-cb3856c2882b |
steps |
Upload Models
We can now upload both of the models. In a later step, only one model will be added as a pipeline step, where the pipeline will submit inference requests to the pipeline.
original_model = wl.upload_model(original_model_name , original_model_file_name, framework=wallaroo.framework.Framework.ONNX)
replacement_model01 = wl.upload_model(replacement_model_name01 , replacement_model_file_name01, framework=wallaroo.framework.Framework.ONNX)
replacement_model02 = wl.upload_model(replacement_model_name02 , replacement_model_file_name02, framework=wallaroo.framework.Framework.ONNX)
wl.list_models()
Name | # of Versions | Owner ID | Last Updated | Created At |
---|---|---|---|---|
hjfkxgbhousingchallenger | 1 | "" | 2023-07-14 15:36:52.193971+00:00 | 2023-07-14 15:36:52.193971+00:00 |
hjfkgbrhousingchallenger | 1 | "" | 2023-07-14 15:36:51.451443+00:00 | 2023-07-14 15:36:51.451443+00:00 |
hjfkhousingmodelcontrol | 1 | "" | 2023-07-14 15:36:50.705124+00:00 | 2023-07-14 15:36:50.705124+00:00 |
Add Model to Pipeline Step
With the models uploaded, we will add the original model as a pipeline step, then deploy the pipeline so it is available for performing inferences.
pipeline.add_model_step(original_model)
pipeline
name | hjfkhotswappipeline |
---|---|
created | 2023-07-14 15:36:48.697941+00:00 |
last_updated | 2023-07-14 15:36:48.697941+00:00 |
deployed | (none) |
tags | |
versions | 2b84d42b-bda9-4cc8-b182-cb3856c2882b |
steps |
pipeline.deploy()
name | hjfkhotswappipeline |
---|---|
created | 2023-07-14 15:36:48.697941+00:00 |
last_updated | 2023-07-14 15:36:55.684558+00:00 |
deployed | True |
tags | |
versions | 88c2fa3b-9d7e-494c-a84f-5786509b59f4, 2b84d42b-bda9-4cc8-b182-cb3856c2882b |
steps | hjfkhousingmodelcontrol |
pipeline.status()
{'status': 'Running',
'details': [],
'engines': [{'ip': '10.244.3.143',
'name': 'engine-96ddf456f-nlxtl',
'status': 'Running',
'reason': None,
'details': [],
'pipeline_statuses': {'pipelines': [{'id': 'hjfkhotswappipeline',
'status': 'Running'}]},
'model_statuses': {'models': [{'name': 'hjfkhousingmodelcontrol',
'version': '5c97c14e-b8f4-412c-b812-ec67ccc964b9',
'sha': 'e22a0831aafd9917f3cc87a15ed267797f80e2afa12ad7d8810ca58f173b8cc6',
'status': 'Running'}]}}],
'engine_lbs': [{'ip': '10.244.4.189',
'name': 'engine-lb-584f54c899-fz26k',
'status': 'Running',
'reason': None,
'details': []}],
'sidekicks': []}
Verify the Model
The pipeline is deployed with our model. The following will verify that the model is operating correctly. The high_fraud.json
file contains data that the model should process as a high likelihood of being a fraudulent transaction.
normal_input = pd.DataFrame.from_records({"tensor": [[4.0, 2.5, 2900.0, 5505.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2900.0, 0.0, 47.6063, -122.02, 2970.0, 5251.0, 12.0, 0.0, 0.0]]})
result = pipeline.infer(normal_input)
display(result)
time | in.tensor | out.variable | check_failures | |
---|---|---|---|---|
0 | 2023-07-14 15:37:09.422 | [4.0, 2.5, 2900.0, 5505.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2900.0, 0.0, 47.6063, -122.02, 2970.0, 5251.0, 12.0, 0.0, 0.0] | [718013.7] | 0 |
large_house_input = pd.DataFrame.from_records({'tensor': [[4.0, 3.0, 3710.0, 20000.0, 2.0, 0.0, 2.0, 5.0, 10.0, 2760.0, 950.0, 47.6696, -122.261, 3970.0, 20000.0, 79.0, 0.0, 0.0]]})
large_house_result = pipeline.infer(large_house_input)
display(large_house_result)
time | in.tensor | out.variable | check_failures | |
---|---|---|---|---|
0 | 2023-07-14 15:37:09.871 | [4.0, 3.0, 3710.0, 20000.0, 2.0, 0.0, 2.0, 5.0, 10.0, 2760.0, 950.0, 47.6696, -122.261, 3970.0, 20000.0, 79.0, 0.0, 0.0] | [1514079.4] | 0 |
Replace the Model
The pipeline is currently deployed and is able to handle inferences. The model will now be replaced without having to undeploy the pipeline. This is done using the pipeline method replace_with_model_step(index, model)
. Steps start at 0
, so the method called below will replace step 0 in our pipeline with the replacement model.
As an exercise, this deployment can be performed while inferences are actively being submitted to the pipeline to show how quickly the swap takes place.
pipeline.replace_with_model_step(0, replacement_model01).deploy()
Verify the Swap
To verify the swap, we’ll submit the same inferences and display the result. Note that out.variable
has a different output than with the original model.
normal_input = pd.DataFrame.from_records({"tensor": [[4.0, 2.5, 2900.0, 5505.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2900.0, 0.0, 47.6063, -122.02, 2970.0, 5251.0, 12.0, 0.0, 0.0]]})
result02 = pipeline.infer(normal_input)
display(result02)
time | in.tensor | out.variable | check_failures | |
---|---|---|---|---|
0 | 2023-07-14 15:37:25.853 | [4.0, 2.5, 2900.0, 5505.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2900.0, 0.0, 47.6063, -122.02, 2970.0, 5251.0, 12.0, 0.0, 0.0] | [704901.9] | 0 |
large_house_input = pd.DataFrame.from_records({'tensor': [[4.0, 3.0, 3710.0, 20000.0, 2.0, 0.0, 2.0, 5.0, 10.0, 2760.0, 950.0, 47.6696, -122.261, 3970.0, 20000.0, 79.0, 0.0, 0.0]]})
large_house_result02 = pipeline.infer(large_house_input)
display(large_house_result02)
time | in.tensor | out.variable | check_failures | |
---|---|---|---|---|
0 | 2023-07-14 15:37:26.255 | [4.0, 3.0, 3710.0, 20000.0, 2.0, 0.0, 2.0, 5.0, 10.0, 2760.0, 950.0, 47.6696, -122.261, 3970.0, 20000.0, 79.0, 0.0, 0.0] | [1981238.0] | 0 |
Replace the Model Again
Let’s do one more hot swap, this time with our replacement_model02
, then get some test inferences.
pipeline.replace_with_model_step(0, replacement_model02).deploy()
name | hjfkhotswappipeline |
---|---|
created | 2023-07-14 15:36:48.697941+00:00 |
last_updated | 2023-07-14 15:37:27.279807+00:00 |
deployed | True |
tags | |
versions | 9126c35d-68db-4b41-915e-14ebef5b1b51, 5c643e10-c9bf-48db-ad25-a5e38b6faf5f, 88c2fa3b-9d7e-494c-a84f-5786509b59f4, 2b84d42b-bda9-4cc8-b182-cb3856c2882b |
steps | hjfkhousingmodelcontrol |
normal_input = pd.DataFrame.from_records({"tensor": [[4.0, 2.5, 2900.0, 5505.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2900.0, 0.0, 47.6063, -122.02, 2970.0, 5251.0, 12.0, 0.0, 0.0]]})
result03 = pipeline.infer(normal_input)
display(result03)
time | in.tensor | out.variable | check_failures | |
---|---|---|---|---|
0 | 2023-07-14 15:37:31.114 | [4.0, 2.5, 2900.0, 5505.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2900.0, 0.0, 47.6063, -122.02, 2970.0, 5251.0, 12.0, 0.0, 0.0] | [659806.0] | 0 |
large_house_input = pd.DataFrame.from_records({'tensor': [[4.0, 3.0, 3710.0, 20000.0, 2.0, 0.0, 2.0, 5.0, 10.0, 2760.0, 950.0, 47.6696, -122.261, 3970.0, 20000.0, 79.0, 0.0, 0.0]]})
large_house_result03 = pipeline.infer(large_house_input)
display(large_house_result03)
time | in.tensor | out.variable | check_failures | |
---|---|---|---|---|
0 | 2023-07-14 15:37:31.514 | [4.0, 3.0, 3710.0, 20000.0, 2.0, 0.0, 2.0, 5.0, 10.0, 2760.0, 950.0, 47.6696, -122.261, 3970.0, 20000.0, 79.0, 0.0, 0.0] | [2176827.0] | 0 |
Compare Outputs
We’ll display the outputs of our inferences through the different models for comparison.
display([original_model_name, result.loc[0, "out.variable"]])
display([replacement_model_name01, result02.loc[0, "out.variable"]])
display([replacement_model_name02, result03.loc[0, "out.variable"]])
['hjfkhousingmodelcontrol', [718013.7]]
[‘hjfkgbrhousingchallenger’, [704901.9]]
[‘hjfkxgbhousingchallenger’, [659806.0]]
display([original_model_name, large_house_result.loc[0, "out.variable"]])
display([replacement_model_name01, large_house_result02.loc[0, "out.variable"]])
display([replacement_model_name02, large_house_result03.loc[0, "out.variable"]])
['hjfkhousingmodelcontrol', [1514079.4]]
[‘hjfkgbrhousingchallenger’, [1981238.0]]
[‘hjfkxgbhousingchallenger’, [2176827.0]]
Undeploy the Pipeline
With the tutorial complete, the pipeline is undeployed to return the resources back to the Wallaroo instance.
pipeline.undeploy()
name | hjfkhotswappipeline |
---|---|
created | 2023-07-14 15:36:48.697941+00:00 |
last_updated | 2023-07-14 15:37:27.279807+00:00 |
deployed | False |
tags | |
versions | 9126c35d-68db-4b41-915e-14ebef5b1b51, 5c643e10-c9bf-48db-ad25-a5e38b6faf5f, 88c2fa3b-9d7e-494c-a84f-5786509b59f4, 2b84d42b-bda9-4cc8-b182-cb3856c2882b |
steps | hjfkhousingmodelcontrol |
4.2 - Inference URL Tutorials
Wallaroo provides multiple methods of performing inferences through a deployed pipeline.
4.2.1 - Wallaroo SDK Inferencing with Pipeline Inference URL Tutorial
This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.
Wallaroo SDK Inference Tutorial
Wallaroo provides the ability to perform inferences through deployed pipelines via the Wallaroo SDK and the Wallaroo MLOps API. This tutorial demonstrates performing inferences using the Wallaroo SDK.
This tutorial provides the following:
ccfraud.onnx
: A pre-trained credit card fraud detection model.data/cc_data_1k.arrow
,data/cc_data_10k.arrow
: Sample testing data in Apache Arrow format with 1,000 and 10,000 records respectively.wallaroo-model-endpoints-sdk.py
: A code-only version of this tutorial as a Python script.
This tutorial and sample data comes from the Machine Learning Group’s demonstration on Credit Card Fraud detection.
Prerequisites
The following is required for this tutorial:
- A deployed Wallaroo instance with Model Endpoints Enabled
- The following Python libraries:
Tutorial Goals
This demonstration provides a quick tutorial on performing inferences using the Wallaroo SDK using the Pipeline infer
and infer_from_file
methods. This following steps will be performed:
- Connect to a Wallaroo instance using environmental variables. This bypasses the browser link confirmation for a seamless login. For more information, see the Wallaroo SDK Essentials Guide: Client Connection.
- Create a workspace for our models and pipelines.
- Upload the
ccfraud
model. - Create a pipeline and add the
ccfraud
model as a pipeline step. - Run a sample inference through SDK Pipeline
infer
method. - Run a batch inference through SDK Pipeline
infer_from_file
method. - Run a DataFrame and Arrow based inference through the pipeline Inference URL.
Open a Connection to Wallaroo
The first step is to connect to Wallaroo through the Wallaroo client. This example will store the user’s credentials into the file ./creds.json
which contains the following:
{
"username": "{Connecting User's Username}",
"password": "{Connecting User's Password}",
"email": "{Connecting User's Email Address}"
}
Replace the username
, password
, and email
fields with the user account connecting to the Wallaroo instance. This allows a seamless connection to the Wallaroo instance and bypasses the standard browser based confirmation link. For more information, see the Wallaroo SDK Essentials Guide: Client Connection.
If running this example within the internal Wallaroo JupyterHub service, use the wallaroo.Client(auth_type="user_password")
method. If connecting externally via the Wallaroo SDK, use the following to specify the URL of the Wallaroo instance as defined in the Wallaroo DNS Integration Guide, replacing wallarooPrefix.
and wallarooSuffix
with your Wallaroo instance’s DNS prefix and suffix.
Note the .
is part of the prefix. If there is no prefix, then wallarooPrefix = ""
import wallaroo
from wallaroo.object import EntityNotFoundError
import pandas as pd
import os
import pyarrow as pa
# used to display dataframe information without truncating
from IPython.display import display
pd.set_option('display.max_colwidth', None)
import requests
# Used to create unique workspace and pipeline names
import string
import random
# make a random 4 character suffix to prevent workspace and pipeline name clobbering
suffix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
# Retrieve the login credentials.
os.environ["WALLAROO_SDK_CREDENTIALS"] = './creds.json'
# Client connection from local Wallaroo instance
wl = wallaroo.Client(auth_type="user_password")
Create the Workspace
We will create a workspace to work in and call it the sdkinferenceexampleworkspace
, then set it as current workspace environment. We’ll also create our pipeline in advance as sdkinferenceexamplepipeline
.
The model to be uploaded and used for inference will be labeled as ccfraud
.
workspace_name = f'sdkinferenceexampleworkspace{suffix}'
pipeline_name = f'sdkinferenceexamplepipeline{suffix}'
model_name = f'ccfraud{suffix}'
model_file_name = './ccfraud.onnx'
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
pipeline = wl.build_pipeline(name)
return pipeline
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
{'name': 'sdkinferenceexampleworkspacesrsw', 'id': 47, 'archived': False, 'created_by': 'fec5b97a-934b-487f-b95b-ade7f3b81f9c', 'created_at': '2023-05-19T15:14:02.432103+00:00', 'models': [], 'pipelines': []}
Build Pipeline
In a production environment, the pipeline would already be set up with the model and pipeline steps. We would then select it and use it to perform our inferences.
For this example we will create the pipeline and add the ccfraud
model as a pipeline step and deploy it. Deploying a pipeline allocates resources from the Kubernetes cluster hosting the Wallaroo instance and prepares it for performing inferences.
If this process was already completed, it can be commented out and skipped for the next step Select Pipeline.
Then we will list the pipelines and select the one we will be using for the inference demonstrations.
# Create or select the current pipeline
ccfraudpipeline = get_pipeline(pipeline_name)
# Add ccfraud model as the pipeline step
ccfraud_model = wl.upload_model(model_name, model_file_name, framework=wallaroo.framework.Framework.ONNX).configure()
ccfraudpipeline.add_model_step(ccfraud_model).deploy()
name | sdkinferenceexamplepipelinesrsw |
---|---|
created | 2023-05-19 15:14:03.916503+00:00 |
last_updated | 2023-05-19 15:14:05.162541+00:00 |
deployed | True |
tags | |
versions | 81840bdb-a1bc-48b9-8df0-4c7a196fa79a, 49cfc2cc-16fb-4dfa-8d1b-579fa86dab07 |
steps | ccfraudsrsw |
Select Pipeline
This step assumes that the pipeline is prepared with ccfraud
as the current step. The method pipelines_by_name(name)
returns an array of pipelines with names matching the pipeline_name
field. This example assumes only one pipeline is assigned the name sdkinferenceexamplepipeline
.
# List the pipelines by name in the current workspace - just the first several to save space.
display(wl.list_pipelines()[:5])
# Set the `pipeline` variable to our sample pipeline.
pipeline = wl.pipelines_by_name(pipeline_name)[0]
display(pipeline)
[{'name': 'sdkinferenceexamplepipelinesrsw', 'create_time': datetime.datetime(2023, 5, 19, 15, 14, 3, 916503, tzinfo=tzutc()), 'definition': '[]'},
{'name': 'ccshadoweonn', 'create_time': datetime.datetime(2023, 5, 19, 15, 13, 48, 963815, tzinfo=tzutc()), 'definition': '[]'},
{'name': 'ccshadowgozg', 'create_time': datetime.datetime(2023, 5, 19, 15, 8, 23, 58929, tzinfo=tzutc()), 'definition': '[]'}]
name | sdkinferenceexamplepipelinesrsw |
---|---|
created | 2023-05-19 15:14:03.916503+00:00 |
last_updated | 2023-05-19 15:14:05.162541+00:00 |
deployed | True |
tags | |
versions | 81840bdb-a1bc-48b9-8df0-4c7a196fa79a, 49cfc2cc-16fb-4dfa-8d1b-579fa86dab07 |
steps | ccfraudsrsw |
Interferences via SDK
Once a pipeline has been deployed, an inference can be run. This will submit data to the pipeline, where it is processed through each of the pipeline’s steps with the output of the previous step providing the input for the next step. The final step will then output the result of all of the pipeline’s steps.
- Inputs are either sent one of the following:
- pandas.DataFrame. The return value will be a pandas.DataFrame.
- Apache Arrow. The return value will be an Apache Arrow table.
- Custom JSON. The return value will be a Wallaroo InferenceResult object.
Inferences are performed through the Wallaroo SDK via the Pipeline infer
and infer_from_file
methods.
infer Method
Now that the pipeline is deployed we’ll perform an inference using the Pipeline infer
method, and submit a pandas DataFrame as our input data. This will return a pandas DataFrame as the inference output.
For more information, see the Wallaroo SDK Essentials Guide: Inferencing: Run Inference through Local Variable.
smoke_test = pd.DataFrame.from_records([
{
"tensor":[
1.0678324729,
0.2177810266,
-1.7115145262,
0.682285721,
1.0138553067,
-0.4335000013,
0.7395859437,
-0.2882839595,
-0.447262688,
0.5146124988,
0.3791316964,
0.5190619748,
-0.4904593222,
1.1656456469,
-0.9776307444,
-0.6322198963,
-0.6891477694,
0.1783317857,
0.1397992467,
-0.3554220649,
0.4394217877,
1.4588397512,
-0.3886829615,
0.4353492889,
1.7420053483,
-0.4434654615,
-0.1515747891,
-0.2668451725,
-1.4549617756
]
}
])
result = pipeline.infer(smoke_test)
display(result)
time | in.tensor | out.dense_1 | check_failures | |
---|---|---|---|---|
0 | 2023-05-19 15:14:22.066 | [1.0678324729, 0.2177810266, -1.7115145262, 0.682285721, 1.0138553067, -0.4335000013, 0.7395859437, -0.2882839595, -0.447262688, 0.5146124988, 0.3791316964, 0.5190619748, -0.4904593222, 1.1656456469, -0.9776307444, -0.6322198963, -0.6891477694, 0.1783317857, 0.1397992467, -0.3554220649, 0.4394217877, 1.4588397512, -0.3886829615, 0.4353492889, 1.7420053483, -0.4434654615, -0.1515747891, -0.2668451725, -1.4549617756] | [0.0014974177] | 0 |
infer_from_file Method
This example uses the Pipeline method infer_from_file
to submit 10,000 records as a batch using an Apache Arrow table. The method will return an Apache Arrow table. For more information, see the Wallaroo SDK Essentials Guide: Inferencing: Run Inference From A File
The results will be converted into a pandas.DataFrame. The results will be filtered by transactions likely to be credit card fraud.
result = pipeline.infer_from_file('./data/cc_data_10k.arrow')
display(result)
pyarrow.Table
time: timestamp[ms]
in.tensor: list<item: float> not null
child 0, item: float
out.dense_1: list<inner: float not null> not null
child 0, inner: float not null
check_failures: int8
----
time: [[2023-05-19 15:14:22.851,2023-05-19 15:14:22.851,2023-05-19 15:14:22.851,2023-05-19 15:14:22.851,2023-05-19 15:14:22.851,...,2023-05-19 15:14:22.851,2023-05-19 15:14:22.851,2023-05-19 15:14:22.851,2023-05-19 15:14:22.851,2023-05-19 15:14:22.851]]
in.tensor: [[[-1.0603298,2.3544967,-3.5638788,5.138735,-1.2308457,...,0.038412016,1.0993439,1.2603409,-0.14662448,-1.4463212],[-1.0603298,2.3544967,-3.5638788,5.138735,-1.2308457,...,0.038412016,1.0993439,1.2603409,-0.14662448,-1.4463212],...,[-2.1694233,-3.1647356,1.2038506,-0.2649221,0.0899006,...,1.8174038,-0.19327773,0.94089776,0.825025,1.6242892],[-0.12405868,0.73698884,1.0311689,0.59917533,0.11831961,...,-0.36567155,-0.87004745,0.41288367,0.49470216,-0.6710689]]]
out.dense_1: [[[0.99300325],[0.99300325],...,[0.00024175644],[0.0010648072]]]
check_failures: [[0,0,0,0,0,...,0,0,0,0,0]]
# use pyarrow to convert results to a pandas DataFrame and display only the results with > 0.75
list = [0.75]
outputs = result.to_pandas()
# display(outputs)
filter = [elt[0] > 0.75 for elt in outputs['out.dense_1']]
outputs = outputs.loc[filter]
display(outputs)
time | in.tensor | out.dense_1 | check_failures | |
---|---|---|---|---|
0 | 2023-05-19 15:14:22.851 | [-1.0603298, 2.3544967, -3.5638788, 5.138735, -1.2308457, -0.76878244, -3.5881228, 1.8880838, -3.2789674, -3.9563255, 4.099344, -5.653918, -0.8775733, -9.131571, -0.6093538, -3.7480276, -5.0309124, -0.8748149, 1.9870535, 0.7005486, 0.9204423, -0.10414918, 0.32295644, -0.74181414, 0.038412016, 1.0993439, 1.2603409, -0.14662448, -1.4463212] | [0.99300325] | 0 |
1 | 2023-05-19 15:14:22.851 | [-1.0603298, 2.3544967, -3.5638788, 5.138735, -1.2308457, -0.76878244, -3.5881228, 1.8880838, -3.2789674, -3.9563255, 4.099344, -5.653918, -0.8775733, -9.131571, -0.6093538, -3.7480276, -5.0309124, -0.8748149, 1.9870535, 0.7005486, 0.9204423, -0.10414918, 0.32295644, -0.74181414, 0.038412016, 1.0993439, 1.2603409, -0.14662448, -1.4463212] | [0.99300325] | 0 |
2 | 2023-05-19 15:14:22.851 | [-1.0603298, 2.3544967, -3.5638788, 5.138735, -1.2308457, -0.76878244, -3.5881228, 1.8880838, -3.2789674, -3.9563255, 4.099344, -5.653918, -0.8775733, -9.131571, -0.6093538, -3.7480276, -5.0309124, -0.8748149, 1.9870535, 0.7005486, 0.9204423, -0.10414918, 0.32295644, -0.74181414, 0.038412016, 1.0993439, 1.2603409, -0.14662448, -1.4463212] | [0.99300325] | 0 |
3 | 2023-05-19 15:14:22.851 | [-1.0603298, 2.3544967, -3.5638788, 5.138735, -1.2308457, -0.76878244, -3.5881228, 1.8880838, -3.2789674, -3.9563255, 4.099344, -5.653918, -0.8775733, -9.131571, -0.6093538, -3.7480276, -5.0309124, -0.8748149, 1.9870535, 0.7005486, 0.9204423, -0.10414918, 0.32295644, -0.74181414, 0.038412016, 1.0993439, 1.2603409, -0.14662448, -1.4463212] | [0.99300325] | 0 |
161 | 2023-05-19 15:14:22.851 | [-9.716793, 9.174981, -14.450761, 8.653825, -11.039951, 0.6602411, -22.825525, -9.919395, -8.064324, -16.737926, 4.852197, -12.563343, -1.0762653, -7.524591, -3.2938414, -9.62102, -15.6501045, -7.089741, 1.7687134, 5.044906, -11.365625, 4.5987034, 4.4777045, 0.31702697, -2.2731977, 0.07944675, -10.052058, -2.024108, -1.0611985] | [1.0] | 0 |
941 | 2023-05-19 15:14:22.851 | [-0.50492376, 1.9348029, -3.4217603, 2.2165704, -0.6545315, -1.9004827, -1.6786858, 0.5380051, -2.7229102, -5.265194, 3.504164, -5.4661765, 0.68954825, -8.725291, 2.0267954, -5.4717045, -4.9123807, -1.6131229, 3.8021576, 1.3881834, 1.0676425, 0.28200775, -0.30759808, -0.48498034, 0.9507336, 1.5118006, 1.6385275, 1.072455, 0.7959132] | [0.9873102] | 0 |
1445 | 2023-05-19 15:14:22.851 | [-7.615594, 4.659706, -12.057331, 7.975307, -5.1068773, -1.6116138, -12.146941, -0.5952333, -6.4605103, -12.535655, 10.017626, -14.839381, 0.34900802, -14.953928, -0.3901092, -9.342014, -14.285043, -5.758632, 0.7512068, 1.4632998, -3.3777077, 0.9950705, -0.5855211, -1.6528498, 1.9089833, 1.6860862, 5.5044003, -3.703297, -1.4715525] | [1.0] | 0 |
2092 | 2023-05-19 15:14:22.851 | [-14.115489, 9.905631, -18.67885, 4.602589, -15.404288, -3.7169847, -15.887272, 15.616176, -3.2883947, -7.0224414, 4.086536, -5.7809114, 1.2251061, -5.4301147, -0.14021407, -6.0200763, -12.957546, -5.545689, 0.86074656, 2.2463796, 2.492611, -2.9649208, -2.265674, 0.27490455, 3.9263225, -0.43438172, 3.1642237, 1.2085277, 0.8223642] | [0.99999] | 0 |
2220 | 2023-05-19 15:14:22.851 | [-0.1098309, 2.5842443, -3.5887418, 4.63558, 1.1825614, -1.2139517, -0.7632139, 0.6071841, -3.7244265, -3.501917, 4.3637576, -4.612757, -0.44275254, -10.346612, 0.66243565, -0.33048683, 1.5961986, 2.5439718, 0.8787973, 0.7406088, 0.34268215, -0.68495077, -0.48357907, -1.9404846, -0.059520483, 1.1553137, 0.9918434, 0.7067319, -1.6016251] | [0.91080534] | 0 |
4135 | 2023-05-19 15:14:22.851 | [-0.547029, 2.2944348, -4.149202, 2.8648357, -0.31232587, -1.5427867, -2.1489344, 0.9471863, -2.663241, -4.2572775, 2.1116028, -6.2264414, -1.1307784, -6.9296007, 1.0049651, -5.876498, -5.6855297, -1.5800936, 3.567338, 0.5962099, 1.6361043, 1.8584082, -0.08202618, 0.46620172, -2.234368, -0.18116793, 1.744976, 2.1414309, -1.6081295] | [0.98877275] | 0 |
4236 | 2023-05-19 15:14:22.851 | [-3.135635, -1.483817, -3.0833669, 1.6626456, -0.59695035, -0.30199608, -3.316563, 1.869609, -1.8006078, -4.5662026, 2.8778172, -4.0887237, -0.43401834, -3.5816982, 0.45171788, -5.725131, -8.982029, -4.0279546, 0.89264476, 0.24721873, 1.8289508, 1.6895254, -2.5555577, -2.4714024, -0.4500012, 0.23333028, 2.2119386, -2.041805, 1.1568314] | [0.95601666] | 0 |
5658 | 2023-05-19 15:14:22.851 | [-5.4078765, 3.9039962, -8.98522, 5.128742, -7.373224, -2.946234, -11.033238, 5.914019, -5.669241, -12.041053, 6.950792, -12.488795, 1.2236942, -14.178565, 1.6514667, -12.47019, -22.350504, -8.928755, 4.54775, -0.11478994, 3.130207, -0.70128506, -0.40275285, 0.7511918, -0.1856308, 0.92282087, 0.146656, -1.3761806, 0.42997098] | [1.0] | 0 |
6768 | 2023-05-19 15:14:22.851 | [-16.900557, 11.7940855, -21.349983, 4.746453, -17.54182, -3.415758, -19.897173, 13.8569145, -3.570626, -7.388376, 3.0761156, -4.0583425, 1.2901028, -2.7997534, -0.4298746, -4.777225, -11.371295, -5.2725616, 0.0964799, 4.2148075, -0.8343371, -2.3663573, -1.6571938, 0.2110055, 4.438088, -0.49057993, 2.342008, 1.4479793, -1.4715525] | [0.9999745] | 0 |
6780 | 2023-05-19 15:14:22.851 | [-0.74893713, 1.3893062, -3.7477517, 2.4144504, -0.11061429, -1.0737498, -3.1504633, 1.2081385, -1.332872, -4.604276, 4.438548, -7.687688, 1.1683422, -5.3296027, -0.19838685, -5.294243, -5.4928794, -1.3254275, 4.387228, 0.68643385, 0.87228596, -0.1154091, -0.8364338, -0.61202216, 0.10518055, 2.2618086, 1.1435078, -0.32623357, -1.6081295] | [0.9852645] | 0 |
7133 | 2023-05-19 15:14:22.851 | [-7.5131927, 6.507386, -12.439463, 5.7453, -9.513038, -1.4236209, -17.402607, -3.0903268, -5.378041, -15.169325, 5.7585907, -13.448207, -0.45244268, -8.495097, -2.2323692, -11.429063, -19.578058, -8.367617, 1.8869618, 2.1813896, -4.799091, 2.4388566, 2.9503248, 0.6293566, -2.6906652, -2.1116931, -6.4196434, -1.4523355, -1.4715525] | [1.0] | 0 |
7566 | 2023-05-19 15:14:22.851 | [-2.1804514, 1.0243497, -4.3890443, 3.4924, -3.7609894, 0.023624033, -2.7677023, 1.1786921, -2.9450424, -6.8823, 6.1294384, -9.564066, -1.6273017, -10.940607, 0.3062539, -8.854589, -15.382658, -5.419305, 3.2210033, -0.7381137, 0.9632334, 0.6612066, 2.1337948, -0.90536207, 0.7498649, -0.019404415, 5.5950212, 0.26602694, 1.7534728] | [0.9999705] | 0 |
7911 | 2023-05-19 15:14:22.851 | [-1.594454, 1.8545462, -2.6311765, 2.759316, -2.6988854, -0.08155677, -3.8566258, -0.04912437, -1.9640644, -4.2058415, 3.391933, -6.471933, -0.9877536, -6.188904, 1.2249585, -8.652863, -11.170872, -6.134417, 2.5400054, -0.29327056, 3.591464, 0.3057127, -0.052313827, 0.06196331, -0.82863224, -0.2595842, 1.0207018, 0.019899422, 1.0935433] | [0.9980203] | 0 |
8921 | 2023-05-19 15:14:22.851 | [-0.21756083, 1.786712, -3.4240367, 2.7769134, -1.420116, -2.1018193, -3.4615245, 0.7367844, -2.3844852, -6.3140697, 4.382665, -8.348951, -1.6409378, -10.611383, 1.1813216, -6.251184, -10.577264, -3.5184007, 0.7997489, 0.97915924, 1.081642, -0.7852368, -0.4761941, -0.10635195, 2.066527, -0.4103488, 2.8288178, 1.9340333, -1.4715525] | [0.99950194] | 0 |
9244 | 2023-05-19 15:14:22.851 | [-3.314442, 2.4431305, -6.1724143, 3.6737356, -3.81542, -1.5950849, -4.8292923, 2.9850774, -4.22416, -7.5519834, 6.1932964, -8.59886, 0.25443414, -11.834097, -0.39583337, -6.015362, -13.532762, -4.226845, 1.1153877, 0.17989528, 1.3166595, -0.64433384, 0.2305495, -0.5776498, 0.7609739, 2.2197483, 4.01189, -1.2347667, 1.2847253] | [0.9999876] | 0 |
10176 | 2023-05-19 15:14:22.851 | [-5.0815525, 3.9294617, -8.4077635, 6.373701, -7.391173, -2.1574461, -10.345097, 5.5896044, -6.3736906, -11.330594, 6.618754, -12.93748, 1.1884484, -13.9628935, 1.0340953, -12.278127, -23.333889, -8.886669, 3.5720036, -0.3243157, 3.4229393, 0.493529, 0.08469851, 0.791218, 0.30968663, 0.6811129, 0.39306796, -1.5204874, 0.9061435] | [1.0] | 0 |
Inferences via HTTP POST
Each pipeline has its own Inference URL that allows HTTP/S POST submissions of inference requests. Full details are available from the Inferencing via the Wallaroo MLOps API.
This example will demonstrate performing inferences with a DataFrame input and an Apache Arrow input.
Request JWT Token
There are two ways to retrieve the JWT token used to authenticate to the Wallaroo MLOps API.
- Wallaroo SDK. This method requires a Wallaroo based user.
- API Clent Secret. This is the recommended method as it is user independent. It allows any valid user to make an inference request.
This tutorial will use the Wallaroo SDK method Wallaroo Client wl.auth.auth_header()
method, extracting the Authentication header from the response.
Reference: MLOps API Retrieve Token Through Wallaroo SDK
headers = wl.auth.auth_header()
display(headers)
{'Authorization': 'Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJhSFpPS1RacGhxT1JQVkw4Y19JV25qUDNMU29iSnNZNXBtNE5EQTA1NVZNIn0.eyJleHAiOjE2ODQ1MDkzMDgsImlhdCI6MTY4NDUwOTI0OCwiYXV0aF90aW1lIjoxNjg0NTA4ODUwLCJqdGkiOiJkZmU3ZTIyMS02ODMyLTRiOGItYjJiMS1hYzFkYWY2YjVmMWYiLCJpc3MiOiJodHRwczovL3NwYXJrbHktYXBwbGUtMzAyNi5rZXljbG9hay53YWxsYXJvby5jb21tdW5pdHkvYXV0aC9yZWFsbXMvbWFzdGVyIiwiYXVkIjpbIm1hc3Rlci1yZWFsbSIsImFjY291bnQiXSwic3ViIjoiZmVjNWI5N2EtOTM0Yi00ODdmLWI5NWItYWRlN2YzYjgxZjljIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoic2RrLWNsaWVudCIsInNlc3Npb25fc3RhdGUiOiI5ZTQ0YjNmNC0xYzg2LTRiZmQtOGE3My0yYjc0MjY5ZmNiNGMiLCJhY3IiOiIwIiwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImNyZWF0ZS1yZWFsbSIsImRlZmF1bHQtcm9sZXMtbWFzdGVyIiwib2ZmbGluZV9hY2Nlc3MiLCJhZG1pbiIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsibWFzdGVyLXJlYWxtIjp7InJvbGVzIjpbInZpZXctaWRlbnRpdHktcHJvdmlkZXJzIiwidmlldy1yZWFsbSIsIm1hbmFnZS1pZGVudGl0eS1wcm92aWRlcnMiLCJpbXBlcnNvbmF0aW9uIiwiY3JlYXRlLWNsaWVudCIsIm1hbmFnZS11c2VycyIsInF1ZXJ5LXJlYWxtcyIsInZpZXctYXV0aG9yaXphdGlvbiIsInF1ZXJ5LWNsaWVudHMiLCJxdWVyeS11c2VycyIsIm1hbmFnZS1ldmVudHMiLCJtYW5hZ2UtcmVhbG0iLCJ2aWV3LWV2ZW50cyIsInZpZXctdXNlcnMiLCJ2aWV3LWNsaWVudHMiLCJtYW5hZ2UtYXV0aG9yaXphdGlvbiIsIm1hbmFnZS1jbGllbnRzIiwicXVlcnktZ3JvdXBzIl19LCJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6ImVtYWlsIHByb2ZpbGUiLCJzaWQiOiI5ZTQ0YjNmNC0xYzg2LTRiZmQtOGE3My0yYjc0MjY5ZmNiNGMiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiaHR0cHM6Ly9oYXN1cmEuaW8vand0L2NsYWltcyI6eyJ4LWhhc3VyYS11c2VyLWlkIjoiZmVjNWI5N2EtOTM0Yi00ODdmLWI5NWItYWRlN2YzYjgxZjljIiwieC1oYXN1cmEtZGVmYXVsdC1yb2xlIjoidXNlciIsIngtaGFzdXJhLWFsbG93ZWQtcm9sZXMiOlsidXNlciJdLCJ4LWhhc3VyYS11c2VyLWdyb3VwcyI6Int9In0sInByZWZlcnJlZF91c2VybmFtZSI6ImpvaG4uaHVtbWVsQHdhbGxhcm9vLmFpIiwiZW1haWwiOiJqb2huLmh1bW1lbEB3YWxsYXJvby5haSJ9.YksrXBWIxMHz2Mh0dhM8GVvFUQJH5sCVTfA5qYiMIquME5vROVjqlm72k2FwdHQmRdwbwKGU1fGfuw6ijAfvVvd50lMdhYrT6TInhdaXX6UZ0pqsuuXyC1HxaTfC5JA7yOQo7SGQ3rjVvsSo_tHhf08HW6gmg2FO9Sdsbo3y2cPEqG7xR_vbB93s_lmQHjN6T8lAdq_io2jkDFUlKtAapAQ3Z5d68-Na5behVqtGeYRb6UKJTUoH-dso7zRwZ1RcqX5_3kT2xEL-dfkAndkvzRCfjOz-OJQEjo2j9iJFWpVaNjsUA45FCUhSNfuG1-zYtAOWcSmq8DyxAt6hY-fgaA'}
Retrieve the Pipeline Inference URL
The Pipeline Inference URL is retrieved via the Wallaroo SDK with the Pipeline ._deployment._url()
method.
- IMPORTANT NOTE: The
_deployment._url()
method will return an internal URL when using Python commands from within the Wallaroo instance - for example, the Wallaroo JupyterHub service. When connecting via an external connection,_deployment._url()
returns an external URL.- External URL connections requires the authentication be included in the HTTP request, and Model Endpoints are enabled in the Wallaroo configuration options.
deploy_url = pipeline._deployment._url()
print(deploy_url)
https://sparkly-apple-3026.api.wallaroo.community/v1/api/pipelines/infer/sdkinferenceexamplepipelinesrsw-28/sdkinferenceexamplepipelinesrsw
HTTP Inference with DataFrame Input
The following example performs a HTTP Inference request with a DataFrame input. The request will be made with first a Python requests
method, then using curl
.
# get authorization header
headers = wl.auth.auth_header()
## Inference through external URL using dataframe
# retrieve the json data to submit
data = pd.DataFrame.from_records([
{
"tensor":[
1.0678324729,
0.2177810266,
-1.7115145262,
0.682285721,
1.0138553067,
-0.4335000013,
0.7395859437,
-0.2882839595,
-0.447262688,
0.5146124988,
0.3791316964,
0.5190619748,
-0.4904593222,
1.1656456469,
-0.9776307444,
-0.6322198963,
-0.6891477694,
0.1783317857,
0.1397992467,
-0.3554220649,
0.4394217877,
1.4588397512,
-0.3886829615,
0.4353492889,
1.7420053483,
-0.4434654615,
-0.1515747891,
-0.2668451725,
-1.4549617756
]
}
])
# set the content type for pandas records
headers['Content-Type']= 'application/json; format=pandas-records'
# set accept as pandas-records
headers['Accept']='application/json; format=pandas-records'
# submit the request via POST, import as pandas DataFrame
response = pd.DataFrame.from_records(
requests.post(
deploy_url,
data=data.to_json(orient="records"),
headers=headers)
.json()
)
display(response.loc[:,["time", "out"]])
time | out | |
---|---|---|
0 | 1684509263640 | {'dense_1': [0.0014974177]} |
!curl -X POST {deploy_url} -H "Authorization: {headers['Authorization']}" -H "Content-Type:{headers['Content-Type']}" -H "Accept:{headers['Accept']}" --data '{data.to_json(orient="records")}'
[{"time":1684509264292,"in":{"tensor":[1.0678324729,0.2177810266,-1.7115145262,0.682285721,1.0138553067,-0.4335000013,0.7395859437,-0.2882839595,-0.447262688,0.5146124988,0.3791316964,0.5190619748,-0.4904593222,1.1656456469,-0.9776307444,-0.6322198963,-0.6891477694,0.1783317857,0.1397992467,-0.3554220649,0.4394217877,1.4588397512,-0.3886829615,0.4353492889,1.7420053483,-0.4434654615,-0.1515747891,-0.2668451725,-1.4549617756]},"out":{"dense_1":[0.0014974177]},"check_failures":[],"metadata":{"last_model":"{\"model_name\":\"ccfraudsrsw\",\"model_sha\":\"bc85ce596945f876256f41515c7501c399fd97ebcb9ab3dd41bf03f8937b4507\"}","pipeline_version":"81840bdb-a1bc-48b9-8df0-4c7a196fa79a","elapsed":[62451,212744]}}]
HTTP Inference with Arrow Input
The following example performs a HTTP Inference request with an Apache Arrow input. The request will be made with first a Python requests
method, then using curl
.
Only the first 5 rows will be displayed for space purposes.
# get authorization header
headers = wl.auth.auth_header()
# Submit arrow file
dataFile="./data/cc_data_10k.arrow"
data = open(dataFile,'rb').read()
# set the content type for Arrow table
headers['Content-Type']= "application/vnd.apache.arrow.file"
# set accept as Apache Arrow
headers['Accept']="application/vnd.apache.arrow.file"
response = requests.post(
deploy_url,
headers=headers,
data=data,
verify=True
)
# Arrow table is retrieved
with pa.ipc.open_file(response.content) as reader:
arrow_table = reader.read_all()
# convert to Polars DataFrame and display the first 5 rows
display(arrow_table.to_pandas().head(5).loc[:,["time", "out"]])
time | out | |
---|---|---|
0 | 1684509265142 | {'dense_1': [0.99300325]} |
1 | 1684509265142 | {'dense_1': [0.99300325]} |
2 | 1684509265142 | {'dense_1': [0.99300325]} |
3 | 1684509265142 | {'dense_1': [0.99300325]} |
4 | 1684509265142 | {'dense_1': [0.0010916889]} |
!curl -X POST {deploy_url} -H "Authorization: {headers['Authorization']}" -H "Content-Type:{headers['Content-Type']}" -H "Accept:{headers['Accept']}" --data-binary @{dataFile} > curl_response.arrow
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 4200k 100 3037k 100 1162k 1980k 757k 0:00:01 0:00:01 --:--:-- 2766k
Undeploy Pipeline
When finished with our tests, we will undeploy the pipeline so we have the Kubernetes resources back for other tasks.
pipeline.undeploy()
name | sdkinferenceexamplepipelinesrsw |
---|---|
created | 2023-05-19 15:14:03.916503+00:00 |
last_updated | 2023-05-19 15:14:05.162541+00:00 |
deployed | False |
tags | |
versions | 81840bdb-a1bc-48b9-8df0-4c7a196fa79a, 49cfc2cc-16fb-4dfa-8d1b-579fa86dab07 |
steps | ccfraudsrsw |
4.2.2 - Wallaroo MLOps API Inferencing with Pipeline Inference URL Tutorial
This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.
Wallaroo API Inference Tutorial
Wallaroo provides the ability to perform inferences through deployed pipelines via the Wallaroo SDK and the Wallaroo MLOps API. This tutorial demonstrates performing inferences using the Wallaroo MLOps API.
This tutorial provides the following:
ccfraud.onnx
: A pre-trained credit card fraud detection model.data/cc_data_1k.arrow
,data/cc_data_10k.arrow
: Sample testing data in Apache Arrow format with 1,000 and 10,000 records respectively.wallaroo-model-endpoints-api.py
: A code-only version of this tutorial as a Python script.
This tutorial and sample data comes from the Machine Learning Group’s demonstration on Credit Card Fraud detection.
Prerequisites
The following is required for this tutorial:
- A deployed Wallaroo instance with Model Endpoints Enabled
- The following Python libraries:
Tutorial Goals
This demonstration provides a quick tutorial on performing inferences using the Wallaroo MLOps API using a deployed pipeline’s Inference URL. This following steps will be performed:
- Connect to a Wallaroo instance using the Wallaroo SDK and environmental variables. This bypasses the browser link confirmation for a seamless login, and provides a simple method of retrieving the JWT token used for Wallaroo MLOps API calls. For more information, see the Wallaroo SDK Essentials Guide: Client Connection and the Wallaroo MLOps API Essentials Guide.
- Create a workspace for our models and pipelines.
- Upload the
ccfraud
model. - Create a pipeline and add the
ccfraud
model as a pipeline step. - Run sample inferences with pandas DataFrame inputs and Apache Arrow inputs.
Retrieve Token
There are two methods of retrieving the JWT token used to authenticate to the Wallaroo instance’s API service:
- Wallaroo SDK. This method requires a Wallaroo based user.
- API Client Secret. This is the recommended method as it is user independent. It allows any valid user to make an inference request.
This tutorial will use the Wallaroo SDK method for convenience with environmental variables for a seamless login without browser validation. For more information, see the Wallaroo SDK Essentials Guide: Client Connection.
API Request Methods
All Wallaroo API endpoints follow the format:
https://$URLPREFIX.api.$URLSUFFIX/v1/api$COMMAND
Where $COMMAND
is the specific endpoint. For example, for the command to list of workspaces in the Wallaroo instance would use the above format based on these settings:
$URLPREFIX
:smooth-moose-1617
$URLSUFFIX
:example.wallaroo.ai
$COMMAND
:/workspaces/list
This would create the following API endpoint:
https://smooth-moose-1617.api.example.wallaroo.ai/v1/api/workspaces/list
Connect to Wallaroo
For this example, a connection to the Wallaroo SDK is used. This will be used to retrieve the JWT token for the MLOps API calls.
This example will store the user’s credentials into the file ./creds.json
which contains the following:
{
"username": "{Connecting User's Username}",
"password": "{Connecting User's Password}",
"email": "{Connecting User's Email Address}"
}
Replace the username
, password
, and email
fields with the user account connecting to the Wallaroo instance. This allows a seamless connection to the Wallaroo instance and bypasses the standard browser based confirmation link. For more information, see the Wallaroo SDK Essentials Guide: Client Connection.
Update wallarooPrefix = "YOUR PREFIX."
and wallarooSuffix = "YOUR SUFFIX"
to match the Wallaroo instance used for this demonstration. Note the .
is part of the prefix. If there is no prefix, then wallarooPrefix = ""
import wallaroo
from wallaroo.object import EntityNotFoundError
import pandas as pd
import os
import base64
import pyarrow as pa
import requests
from requests.auth import HTTPBasicAuth
# Used to create unique workspace and pipeline names
import string
import random
# make a random 4 character prefix
suffix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
display(suffix)
import json
# used to display dataframe information without truncating
from IPython.display import display
pd.set_option('display.max_colwidth', None)
'atwc'
# Retrieve the login credentials.
os.environ["WALLAROO_SDK_CREDENTIALS"] = './creds.json.example'
# wl = wallaroo.Client(auth_type="user_password")
# Client connection from local Wallaroo instance
wallarooPrefix = ""
wallarooSuffix = "autoscale-uat-ee.wallaroo.dev"
wl = wallaroo.Client(api_endpoint=f"https://{wallarooPrefix}api.{wallarooSuffix}",
auth_endpoint=f"https://{wallarooPrefix}keycloak.{wallarooSuffix}",
auth_type="user_password")
wallarooPrefix = "YOUR PREFIX."
wallarooPrefix = "YOUR SUFFIX"
wallarooPrefix = ""
wallarooSuffix = "autoscale-uat-ee.wallaroo.dev"
APIURL=f"https://{wallarooPrefix}api.{wallarooSuffix}"
APIURL
'https://api.autoscale-uat-ee.wallaroo.dev'
Retrieve the JWT Token
As mentioned earlier, there are multiple methods of authenticating to the Wallaroo instance for MLOps API calls. This tutorial will use the Wallaroo SDK method Wallaroo Client wl.auth.auth_header()
method, extracting the token from the response.
Reference: MLOps API Retrieve Token Through Wallaroo SDK
# Retrieve the token
headers = wl.auth.auth_header()
display(headers)
{'Authorization': 'Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJEWkc4UE4tOHJ0TVdPdlVGc0V0RWpacXNqbkNjU0tJY3Zyak85X3FxcXc0In0.eyJleHAiOjE2ODg3NTE2NjQsImlhdCI6MTY4ODc1MTYwNCwianRpIjoiNGNmNmFjMzQtMTVjMy00MzU0LWI0ZTYtMGYxOWIzNjg3YmI2IiwiaXNzIjoiaHR0cHM6Ly9rZXljbG9hay5hdXRvc2NhbGUtdWF0LWVlLndhbGxhcm9vLmRldi9hdXRoL3JlYWxtcy9tYXN0ZXIiLCJhdWQiOlsibWFzdGVyLXJlYWxtIiwiYWNjb3VudCJdLCJzdWIiOiJkOWE3MmJkOS0yYTFjLTQ0ZGQtOTg5Zi0zYzdjMTUxMzA4ODUiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJzZGstY2xpZW50Iiwic2Vzc2lvbl9zdGF0ZSI6Ijk0MjkxNTAwLWE5MDgtNGU2Ny1hMzBiLTA4MTczMzNlNzYwOCIsImFjciI6IjEiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1tYXN0ZXIiLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsibWFzdGVyLXJlYWxtIjp7InJvbGVzIjpbIm1hbmFnZS11c2VycyIsInZpZXctdXNlcnMiLCJxdWVyeS1ncm91cHMiLCJxdWVyeS11c2VycyJdfSwiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJwcm9maWxlIGVtYWlsIiwic2lkIjoiOTQyOTE1MDAtYTkwOC00ZTY3LWEzMGItMDgxNzMzM2U3NjA4IiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJodHRwczovL2hhc3VyYS5pby9qd3QvY2xhaW1zIjp7IngtaGFzdXJhLXVzZXItaWQiOiJkOWE3MmJkOS0yYTFjLTQ0ZGQtOTg5Zi0zYzdjMTUxMzA4ODUiLCJ4LWhhc3VyYS1kZWZhdWx0LXJvbGUiOiJ1c2VyIiwieC1oYXN1cmEtYWxsb3dlZC1yb2xlcyI6WyJ1c2VyIl0sIngtaGFzdXJhLXVzZXItZ3JvdXBzIjoie30ifSwibmFtZSI6IkpvaG4gSGFuc2FyaWNrIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiam9obi5odW1tZWxAd2FsbGFyb28uYWkiLCJnaXZlbl9uYW1lIjoiSm9obiIsImZhbWlseV9uYW1lIjoiSGFuc2FyaWNrIiwiZW1haWwiOiJqb2huLmh1bW1lbEB3YWxsYXJvby5haSJ9.QE5WJ6NI5bQob0p2M7KsVXxrAiUUxnsIjZPuHIx7_6kTsDt4zarcCu2b5X6s6wg0EZQDX22oANWUAXnkWRTQd_E6zE7DkKF7H5kodtyu90ewiFM8ULx2iOWy2GkafQTdiuW90-BGDIjAcOiQtOkdHNaNHqJ9go2Lsom1t_b4-FOhh8bAGhMM3aDS0w-Y8dGKClxW_xFSTmOjNLaPxbFs5NCib-_QAsR_PiyfSFNJ_kjIV8f2mdzeyOauj0YOE-w5nXjhbrDvhS1kJ3n_8C2J2eOnEg85OGd3m6VKVzoR7oPzoZH15Jtl8shKTDS6BEUWpzZNfjYjwZdy1KTenCbzAQ'}
Create Workspace
In a production environment, the Wallaroo workspace that contains the pipeline and models would be created and deployed. We will quickly recreate those steps using the MLOps API. If the workspace and pipeline have already been created through the Wallaroo SDK Inference Tutorial, then we can skip directly to Deploy Pipeline.
Workspaces are created through the MLOps API with the /v1/api/workspaces/create
command. This requires the workspace name be provided, and that the workspace not already exist in the Wallaroo instance.
Reference: MLOps API Create Workspace
# Retrieve the token
headers = wl.auth.auth_header()
# set Content-Type type
headers['Content-Type']='application/json'
# Create workspace
apiRequest = f"{APIURL}/v1/api/workspaces/create"
workspace_name = f"apiinferenceexampleworkspace{suffix}"
data = {
"workspace_name": workspace_name
}
response = requests.post(apiRequest, json=data, headers=headers, verify=True).json()
display(response)
# Stored for future examples
workspaceId = response['workspace_id']
{'workspace_id': 374}
Upload Model
The model is uploaded using the /v1/api/models/upload_and_convert
command. This uploads a ML Model to a Wallaroo workspace via POST with Content-Type: multipart/form-data
and takes the following parameters:
- Parameters
- name - (REQUIRED string): Name of the model
- visibility - (OPTIONAL string): The visibility of the model as either
public
orprivate
. - workspace_id - (REQUIRED int): The numerical id of the workspace to upload the model to. Stored earlier as
workspaceId
.
Directly after we will use the /models/list_versions
to retrieve model details used for later steps.
Reference: Wallaroo MLOps API Essentials Guide: Model Management: Upload Model to Workspace
## upload model
# Retrieve the token
headers = wl.auth.auth_header()
apiRequest = f"{APIURL}/v1/api/models/upload_and_convert"
framework='onnx'
model_name = f"{suffix}ccfraud"
data = {
"name": model_name,
"visibility": "public",
"workspace_id": workspaceId,
"conversion": {
"framework": framework,
"python_version": "3.8",
"requirements": []
}
}
files = {
"metadata": (None, json.dumps(data), "application/json"),
'file': (model_name, open('./ccfraud.onnx', 'rb'), "application/octet-stream")
}
response = requests.post(apiRequest, files=files, headers=headers).json()
display(response)
modelId=response['insert_models']['returning'][0]['models'][0]['id']
{'insert_models': {'returning': [{'models': [{'id': 176}]}]}}
# Get the model details
# Retrieve the token
headers = wl.auth.auth_header()
# set Content-Type type
headers['Content-Type']='application/json'
apiRequest = f"{APIURL}/v1/api/models/get_by_id"
data = {
"id": modelId
}
response = requests.post(apiRequest, json=data, headers=headers, verify=True).json()
display(response)
{'msg': 'The provided model id was not found.', 'code': 400}
# Get the model details
# Retrieve the token
headers = wl.auth.auth_header()
# set Content-Type type
headers['Content-Type']='application/json'
apiRequest = f"{APIURL}/v1/api/models/list_versions"
data = {
"model_id": model_name,
"models_pk_id" : modelId
}
response = requests.post(apiRequest, json=data, headers=headers, verify=True).json()
display(response)
[{'sha': 'bc85ce596945f876256f41515c7501c399fd97ebcb9ab3dd41bf03f8937b4507',
'models_pk_id': 175,
'model_version': 'fa4c2f8c-769e-4ee1-9a91-fe029a4beffc',
'owner_id': '""',
'model_id': 'vsnaccfraud',
'id': 176,
'file_name': 'vsnaccfraud',
'image_path': 'proxy.replicated.com/proxy/wallaroo/ghcr.io/wallaroolabs/mlflow-deploy:v2023.3.0-main-3481',
'status': 'ready'},
{'sha': 'bc85ce596945f876256f41515c7501c399fd97ebcb9ab3dd41bf03f8937b4507',
'models_pk_id': 175,
'model_version': '701be439-8702-4896-88b5-644bb5cb4d61',
'owner_id': '""',
'model_id': 'vsnaccfraud',
'id': 175,
'file_name': 'vsnaccfraud',
'image_path': 'proxy.replicated.com/proxy/wallaroo/ghcr.io/wallaroolabs/mlflow-deploy:v2023.3.0-main-3481',
'status': 'ready'}]
model_version = response[0]['model_version']
display(model_version)
model_sha = response[0]['sha']
display(model_sha)
'fa4c2f8c-769e-4ee1-9a91-fe029a4beffc'
‘bc85ce596945f876256f41515c7501c399fd97ebcb9ab3dd41bf03f8937b4507’
Create Pipeline
Create Pipeline in a Workspace with the /v1/api/pipelines/create
command. This creates a new pipeline in the specified workspace.
- Parameters
- pipeline_id - (REQUIRED string): Name of the new pipeline.
- workspace_id - (REQUIRED int): Numerical id of the workspace for the new pipeline. Stored earlier as
workspaceId
. - definition - (REQUIRED string): Pipeline definitions, can be
{}
for none.
For our example, we are setting the pipeline steps through the definition
field. This will direct inference requests to the model before output.
Reference: Wallaroo MLOps API Essentials Guide: Pipeline Management: Create Pipeline in a Workspace
# Create pipeline
# Retrieve the token
headers = wl.auth.auth_header()
# set Content-Type type
headers['Content-Type']='application/json'
apiRequest = f"{APIURL}/v1/api/pipelines/create"
pipeline_name=f"{suffix}apiinferenceexamplepipeline"
data = {
"pipeline_id": pipeline_name,
"workspace_id": workspaceId,
"definition": {'steps': [{'ModelInference': {'models': [{'name': f'{model_name}', 'version': model_version, 'sha': model_sha}]}}]}
}
response = requests.post(apiRequest, json=data, headers=headers, verify=True).json()
pipeline_id = response['pipeline_pk_id']
pipeline_variant_id=response['pipeline_variant_pk_id']
pipeline_variant_version=['pipeline_variant_version']
Deploy Pipeline
With the pipeline created and the model uploaded into the workspace, the pipeline can be deployed. This will allocate resources from the Kubernetes cluster hosting the Wallaroo instance and prepare the pipeline to process inference requests.
Pipelines are deployed through the MLOps API command /v1/api/pipelines/deploy
which takes the following parameters:
- Parameters
- deploy_id (REQUIRED string): The name for the pipeline deployment.
- engine_config (OPTIONAL string): Additional configuration options for the pipeline.
- pipeline_version_pk_id (REQUIRED int): Pipeline version id. Captured earlier as
pipeline_variant_id
. - model_configs (OPTIONAL Array int): Ids of model configs to apply.
- model_ids (OPTIONAL Array int): Ids of models to apply to the pipeline. If passed in, model_configs will be created automatically.
- models (OPTIONAL Array models): If the model ids are not available as a pipeline step, the models’ data can be passed to it through this method. The options below are only required if
models
are provided as a parameter.- name (REQUIRED string): Name of the uploaded model that is in the same workspace as the pipeline. Captured earlier as the
model_name
variable. - version (REQUIRED string): Version of the model to use.
- sha (REQUIRED string): SHA value of the model.
- name (REQUIRED string): Name of the uploaded model that is in the same workspace as the pipeline. Captured earlier as the
- pipeline_id (REQUIRED int): Numerical value of the pipeline to deploy.
- Returns
- id (int): The deployment id.
Reference: Wallaroo MLOps API Essentials Guide: Pipeline Management: Deploy a Pipeline
# Deploy Pipeline
# Retrieve the token
headers = wl.auth.auth_header()
# set Content-Type type
headers['Content-Type']='application/json'
apiRequest = f"{APIURL}/v1/api/pipelines/deploy"
exampleModelDeployId=pipeline_name
data = {
"deploy_id": exampleModelDeployId,
"pipeline_version_pk_id": pipeline_variant_id,
"model_ids": [
modelId
],
"pipeline_id": pipeline_id
}
response = requests.post(apiRequest, json=data, headers=headers, verify=True).json()
display(response)
exampleModelDeploymentId=response['id']
# wait 45 seconds for the pipeline to complete deployment
import time
time.sleep(45)
{'id': 260}
Get Deployment Status
This returns the deployment status - we’re waiting until the deployment has the status “Ready.”
- Parameters
- name - (REQUIRED string): The deployment in the format {deployment_name}-{deploymnent-id}.
Example: The deployed empty and model pipelines status will be displayed.
# Retrieve the token
headers = wl.auth.auth_header()
# set Content-Type type
headers['Content-Type']='application/json'
# Get model pipeline deployment
api_request = f"{APIURL}/v1/api/status/get_deployment"
data = {
"name": f"{pipeline_name}-{exampleModelDeploymentId}"
}
response = requests.post(api_request, json=data, headers=headers, verify=True).json()
response
{'status': 'Running',
'details': [],
'engines': [{'ip': '10.244.17.3',
'name': 'engine-f77b5c44b-4j2n5',
'status': 'Running',
'reason': None,
'details': [],
'pipeline_statuses': {'pipelines': [{'id': 'vsnaapiinferenceexamplepipeline',
'status': 'Running'}]},
'model_statuses': {'models': [{'name': 'vsnaccfraud',
'version': 'fa4c2f8c-769e-4ee1-9a91-fe029a4beffc',
'sha': 'bc85ce596945f876256f41515c7501c399fd97ebcb9ab3dd41bf03f8937b4507',
'status': 'Running'}]}}],
'engine_lbs': [{'ip': '10.244.17.4',
'name': 'engine-lb-584f54c899-q877m',
'status': 'Running',
'reason': None,
'details': []}],
'sidekicks': []}
Get External Inference URL
The API command /admin/get_pipeline_external_url
retrieves the external inference URL for a specific pipeline in a workspace.
- Parameters
- workspace_id (REQUIRED integer): The workspace integer id.
- pipeline_name (REQUIRED string): The name of the pipeline.
In this example, a list of the workspaces will be retrieved. Based on the setup from the Internal Pipeline Deployment URL Tutorial, the workspace matching urlworkspace
will have it’s workspace id stored and used for the /admin/get_pipeline_external_url
request with the pipeline urlpipeline
.
The External Inference URL will be stored as a variable for the next step.
Reference: Wallaroo MLOps API Essentials Guide: Pipeline Management: Get External Inference URL
# Retrieve the token
headers = wl.auth.auth_header()
# set Content-Type type
headers['Content-Type']='application/json'
## Retrieve the pipeline's External Inference URL
apiRequest = f"{APIURL}/v1/api/admin/get_pipeline_external_url"
data = {
"workspace_id": workspaceId,
"pipeline_name": pipeline_name
}
response = requests.post(apiRequest, json=data, headers=headers, verify=True).json()
deployurl = response['url']
deployurl
'https://api.autoscale-uat-ee.wallaroo.dev/v1/api/pipelines/infer/vsnaapiinferenceexamplepipeline-260/vsnaapiinferenceexamplepipeline'
Perform Inference Through External URL
The inference can now be performed through the External Inference URL. This URL will accept the same inference data file that is used with the Wallaroo SDK, or with an Internal Inference URL as used in the Internal Pipeline Inference URL Tutorial.
For this example, the externalUrl
retrieved through the Get External Inference URL is used to submit a single inference request through the data file data-1.json
.
Reference: Wallaroo MLOps API Essentials Guide: Pipeline Management: Perform Inference Through External URL
# Retrieve the token
headers = wl.auth.auth_header()
# set Content-Type type
headers['Content-Type']='application/json; format=pandas-records'
## Inference through external URL using dataframe
# retrieve the json data to submit
data = [
{
"tensor":[
1.0678324729,
0.2177810266,
-1.7115145262,
0.682285721,
1.0138553067,
-0.4335000013,
0.7395859437,
-0.2882839595,
-0.447262688,
0.5146124988,
0.3791316964,
0.5190619748,
-0.4904593222,
1.1656456469,
-0.9776307444,
-0.6322198963,
-0.6891477694,
0.1783317857,
0.1397992467,
-0.3554220649,
0.4394217877,
1.4588397512,
-0.3886829615,
0.4353492889,
1.7420053483,
-0.4434654615,
-0.1515747891,
-0.2668451725,
-1.4549617756
]
}
]
# submit the request via POST, import as pandas DataFrame
response = pd.DataFrame.from_records(
requests.post(
deployurl,
json=data,
headers=headers)
.json()
)
display(response.loc[:,["time", "out"]])
time | out | |
---|---|---|
0 | 1688750664105 | {'dense_1': [0.0014974177]} |
# Retrieve the token
headers = wl.auth.auth_header()
# set Content-Type type
headers['Content-Type']='application/vnd.apache.arrow.file'
# set accept as apache arrow table
headers['Accept']="application/vnd.apache.arrow.file"
# Submit arrow file
dataFile="./data/cc_data_10k.arrow"
data = open(dataFile,'rb').read()
response = requests.post(
deployurl,
headers=headers,
data=data,
verify=True
)
# Arrow table is retrieved
with pa.ipc.open_file(response.content) as reader:
arrow_table = reader.read_all()
# convert to Polars DataFrame and display the first 5 rows
display(arrow_table.to_pandas().head(5).loc[:,["time", "out"]])
time | out | |
---|---|---|
0 | 1688750664889 | {'dense_1': [0.99300325]} |
1 | 1688750664889 | {'dense_1': [0.99300325]} |
2 | 1688750664889 | {'dense_1': [0.99300325]} |
3 | 1688750664889 | {'dense_1': [0.99300325]} |
4 | 1688750664889 | {'dense_1': [0.0010916889]} |
Undeploy the Pipeline
With the tutorial complete, we’ll undeploy the pipeline with /v1/api/pipelines/undeploy
and return the resources back to the Wallaroo instance.
Reference: Wallaroo MLOps API Essentials Guide: Pipeline Management: Undeploy a Pipeline
# Retrieve the token
headers = wl.auth.auth_header()
# set Content-Type type
headers['Content-Type']='application/json'
apiRequest = f"{APIURL}/v1/api/pipelines/undeploy"
data = {
"pipeline_id": pipeline_id,
"deployment_id":exampleModelDeploymentId
}
response = requests.post(apiRequest, json=data, headers=headers, verify=True).json()
display(response)
None
Wallaroo supports the ability to perform inferences through the SDK and through the API for each deployed pipeline. For more information on how to use Wallaroo, see the Wallaroo Documentation Site for full details.
4.3 - Onnx Deployment with Multi Input-Output Tutorial
This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.
ONNX Multiple Input Output Example
The following example demonstrates some of the data and input requirements when working with ONNX models in Wallaroo. This example will:
- Upload an ONNX model trained to accept multiple inputs and return multiple outputs.
- Deploy the model, and show how to format the data for inference requests through a Wallaroo pipeline.
For more information on using ONNX models with Wallaroo, see Wallaroo SDK Essentials Guide: Model Uploads and Registrations: ONNX.
Steps
Import Libraries
The first step is to import the libraries used for our demonstration - primarily the Wallaroo SDK, which is used to connect to the Wallaroo Ops instance, upload models, etc.
- References
import wallaroo
from wallaroo.deployment_config import DeploymentConfigBuilder
from wallaroo.framework import Framework
import pyarrow as pa
import numpy as np
import pandas as pd
Connect to the Wallaroo Instance
The next step is to connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. For more information on Wallaroo Client settings, see the Client Connection guide.
# Login through local Wallaroo instance
wl = wallaroo.Client()
Create Workspace
We will create a workspace to manage our pipeline and models. The following variables will set the name of our sample workspace then set it as the current workspace. If this tutorial has been run before, the helper function get_workspace
will either create or connect to an existing workspace.
Workspace names must be unique; verify that no other workspaces have the same name when running this tutorial. We then set the current workspace to our new workspace; all model uploads and other requests will use this
def get_workspace(workspace_name, wallaroo_client):
workspace = None
for ws in wallaroo_client.list_workspaces():
if ws.name() == workspace_name:
workspace= ws
if(workspace == None):
workspace = wallaroo_client.create_workspace(workspace_name)
return workspace
workspace_name = 'onnx-tutorial'
workspace = get_workspace(workspace_name, wl)
wl.set_current_workspace(workspace)
{'name': 'onnx-tutorial', 'id': 9, 'archived': False, 'created_by': '12ea09d1-0f49-405e-bed1-27eb6d10fde4', 'created_at': '2023-11-22T16:24:47.786643+00:00', 'models': [], 'pipelines': []}
Upload Model
The ONNX model ./models/multi_io.onnx
will be uploaded with the wallaroo.client.upload_model
method. This requires:
- The designated model name.
- The path for the file.
- The framework aka what kind of model it is based on the
wallaroo.framework.Framework
options.
If we wanted to overwrite the name of the input fields, we could use the wallaroo.client.upload_model.configure(tensor_fields[field_names])
option. This ONNX model takes the inputs input_1
and input_2
.
model = wl.upload_model('onnx-multi-io-model',
"./models/multi_io.onnx",
framework=Framework.ONNX)
model
Name | onnx-multi-io-model |
Version | 7adb9245-53c2-43b4-95df-2c907bb88161 |
File Name | multi_io.onnx |
SHA | bb3e51dfdaa6440359c2396033a84a4248656d0f81ba1f662751520b3f93de27 |
Status | ready |
Image Path | None |
Architecture | None |
Updated At | 2023-22-Nov 16:24:51 |
Create the Pipeline and Add Steps
A new pipeline ‘multi-io-example’ is created with the wallaroo.client.build_pipeline
method that creates a new Wallaroo pipeline within our current workspace. We then add our onnx-multi-io-model
as a pipeline step.
pipeline_name = 'multi-io-example'
pipeline = wl.build_pipeline(pipeline_name)
# in case this pipeline was run before
pipeline.clear()
pipeline.add_model_step(model)
name | multi-io-example |
---|---|
created | 2023-11-22 16:24:53.843958+00:00 |
last_updated | 2023-11-22 16:24:54.523098+00:00 |
deployed | True |
arch | None |
tags | |
versions | 73c1b57d-3227-471a-8e9b-4a8af62188dd, c8fb97d9-50cd-475d-8f36-1d2290e4c585 |
steps | onnx-multi-io-model |
published | False |
Deploy Pipeline
With the model set, deploy the pipeline with a deployment configuration. This sets the number of resources that the pipeline will be allocated from the Wallaroo Ops cluster and makes it available for inference requests.
- References
deployment_config = DeploymentConfigBuilder() \
.cpus(0.25).memory('1Gi') \
.build()
pipeline.deploy(deployment_config=deployment_config)
pipeline.status()
{'status': 'Running',
'details': [],
'engines': [{'ip': '10.244.3.143',
'name': 'engine-857444867-nldj5',
'status': 'Running',
'reason': None,
'details': [],
'pipeline_statuses': {'pipelines': [{'id': 'multi-io-example',
'status': 'Running'}]},
'model_statuses': {'models': [{'name': 'onnx-multi-io-model',
'version': '7adb9245-53c2-43b4-95df-2c907bb88161',
'sha': 'bb3e51dfdaa6440359c2396033a84a4248656d0f81ba1f662751520b3f93de27',
'status': 'Running'}]}}],
'engine_lbs': [{'ip': '10.244.4.155',
'name': 'engine-lb-584f54c899-h647p',
'status': 'Running',
'reason': None,
'details': []}],
'sidekicks': []}
Sample Inference
For our inference request, we will create a dummy DataFrame with the following fields:
- input_1: A list of randomly generated numbers.
- input_2: A list of randomly generated numbers.
10 rows will be created.
Inference requests for Wallaroo for ONNX models must meet the following criteria:
- Equal rows constraint: The number of input rows and output rows must match.
- All inputs are tensors: The inputs are tensor arrays with the same shape.
- Data Type Consistency: Data types within each tensor are of the same type.
Note that each input meets these requirements:
- Each input is one row, and will correspond to a single output row.
- Each input is a tensor. Field values are a list contained within their field.
- Each input is the same data type - for example, a list of floats.
For more details, see Wallaroo SDK Essentials Guide: Model Uploads and Registrations: ONNX.
np.random.seed(1)
mock_inference_data = [np.random.rand(10, 10), np.random.rand(10, 5)]
mock_dataframe = pd.DataFrame(
{
"input_1": mock_inference_data[0].tolist(),
"input_2": mock_inference_data[1].tolist(),
}
)
display(mock_dataframe)
input_1 | input_2 | |
---|---|---|
0 | [0.417022004702574, 0.7203244934421581, 0.0001... | [0.32664490177209615, 0.5270581022576093, 0.88... |
1 | [0.4191945144032948, 0.6852195003967595, 0.204... | [0.6233601157918027, 0.015821242846556283, 0.9... |
2 | [0.8007445686755367, 0.9682615757193975, 0.313... | [0.17234050834532855, 0.13713574962887776, 0.9... |
3 | [0.0983468338330501, 0.42110762500505217, 0.95... | [0.7554630526024664, 0.7538761884612464, 0.923... |
4 | [0.9888610889064947, 0.7481656543798394, 0.280... | [0.01988013383979559, 0.026210986877719278, 0.... |
5 | [0.019366957870297075, 0.678835532939891, 0.21... | [0.5388310643416528, 0.5528219786857659, 0.842... |
6 | [0.10233442882782584, 0.4140559878195683, 0.69... | [0.5857592714582879, 0.9695957483196745, 0.561... |
7 | [0.9034019152878835, 0.13747470414623753, 0.13... | [0.23297427384102043, 0.8071051956187791, 0.38... |
8 | [0.8833060912058098, 0.6236722070556089, 0.750... | [0.5562402339904189, 0.13645522566068502, 0.05... |
9 | [0.11474597295337519, 0.9494892587070712, 0.44... | [0.1074941291060929, 0.2257093386078547, 0.712... |
We now perform an inference with our sample inference request with the wallaroo.pipeline.infer
method. The returning DataFrame displays the input variables as in.{variable_name}
, and the output variables as out.{variable_name}
. Each inference output row corresponds with an input row.
results = pipeline.infer(mock_dataframe)
results
time | in.input_1 | in.input_2 | out.output_1 | out.output_2 | check_failures | |
---|---|---|---|---|---|---|
0 | 2023-11-22 16:27:10.632 | [0.4170220047, 0.7203244934, 0.0001143748, 0.3... | [0.3266449018, 0.5270581023, 0.8859420993, 0.3... | [-0.16188532, -0.2735075, -0.10427341] | [-0.18745898, -0.035904408] | 0 |
1 | 2023-11-22 16:27:10.632 | [0.4191945144, 0.6852195004, 0.2044522497, 0.8... | [0.6233601158, 0.0158212428, 0.9294372337, 0.6... | [-0.16437894, -0.24449202, -0.10489924] | [-0.17241219, -0.09285815] | 0 |
2 | 2023-11-22 16:27:10.632 | [0.8007445687, 0.9682615757, 0.3134241782, 0.6... | [0.1723405083, 0.1371357496, 0.932595463, 0.69... | [-0.1431846, -0.33338487, -0.1858185] | [-0.25035447, -0.095617786] | 0 |
3 | 2023-11-22 16:27:10.632 | [0.0983468338, 0.421107625, 0.9578895302, 0.53... | [0.7554630526, 0.7538761885, 0.9230245355, 0.7... | [-0.21010575, -0.38097042, -0.26413786] | [-0.081432916, -0.12933002] | 0 |
4 | 2023-11-22 16:27:10.632 | [0.9888610889, 0.7481656544, 0.2804439921, 0.7... | [0.0198801338, 0.0262109869, 0.028306488, 0.24... | [-0.29807547, -0.362104, -0.04459526] | [-0.23403212, 0.019275911] | 0 |
5 | 2023-11-22 16:27:10.632 | [0.0193669579, 0.6788355329, 0.211628116, 0.26... | [0.5388310643, 0.5528219787, 0.8420308924, 0.1... | [-0.14283556, -0.29290834, -0.1613777] | [-0.20929304, -0.10064016] | 0 |
6 | 2023-11-22 16:27:10.632 | [0.1023344288, 0.4140559878, 0.6944001577, 0.4... | [0.5857592715, 0.9695957483, 0.5610302193, 0.0... | [-0.2372348, -0.29803842, -0.17791237] | [-0.20062584, -0.026013546] | 0 |
7 | 2023-11-22 16:27:10.632 | [0.9034019153, 0.1374747041, 0.1392763473, 0.8... | [0.2329742738, 0.8071051956, 0.3878606441, 0.8... | [-0.27525327, -0.46431914, -0.2719731] | [-0.17208403, -0.1618222] | 0 |
8 | 2023-11-22 16:27:10.632 | [0.8833060912, 0.6236722071, 0.750942434, 0.34... | [0.556240234, 0.1364552257, 0.0599176895, 0.12... | [-0.3599869, -0.37006766, 0.05214046] | [-0.26465484, 0.08243461] | 0 |
9 | 2023-11-22 16:27:10.632 | [0.114745973, 0.9494892587, 0.4499121335, 0.57... | [0.1074941291, 0.2257093386, 0.7129889804, 0.5... | [-0.20812269, -0.3822521, -0.14788152] | [-0.19157144, -0.12436578] | 0 |
Undeploy the Pipeline
With the tutorial complete, we will undeploy the pipeline and return the resources back to the cluster.
pipeline.undeploy()
4.4 - Model Insights Tutorial
This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.
The Model Insights feature lets you monitor how the environment that your model operates within may be changing in ways that affect it’s predictions so that you can intervene (retrain) in an efficient and timely manner. Changes in the inputs, data drift, can occur due to errors in the data processing pipeline or due to changes in the environment such as user preference or behavior.
The validation framework performs per inference range checks with count frequency based thresholds for alerts and is ideal for catching many errors in input and output data.
In complement to the validation framework model insights focuses on the differences in the distributions of data in a time based window measured against a baseline for a given pipeline and can detect situations where values are still within the expected range but the distribution has shifted. For example, if your model predicts housing prices you might expect the predictions to be between \$200,000 and \$1,000,000 with a distribution centered around \$400,000. If your model suddenly starts predicting prices centered around \$250,000 or \$750,000 the predictions may still be within the expected range but the shift may signal something has changed that should be investigated.
Ideally we’d also monitor the quality of the predictions, concept drift. However this can be difficult as true labels are often not available or are severely delayed in practice. That is there may be a signficant lag between the time the prediction is made and the true (sale price) value is observed.
Consequently, model insights uses data drift detection techniques on both inputs and outputs to detect changes in the distributions of the data.
There are many useful statistical tests for calculating the difference between distributions; however, they typically require assumptions about the underlying distributions or confusing and expensive calculations. We’ve implemented a data drift framework that is easy to understand, fast to compute, runs in an automated fashion and is extensible to many specific use cases.
The methodology currently revolves around calculating the specific percentile-based bins of the baseline distribution and measuring how future distributions fall into these bins. This approach is both visually intuitive and supports an easy to calculate difference score between distributions. Users can tune the scoring mechanism to emphasize different regions of the distribution: for example, you may only care if there is a change in the top 20th percentile of the distribution, compared to the baseline.
You can specify the inputs or outputs that you want to monitor and the data to use for your baselines. You can also specify how often you want to monitor distributions and set parameters to define what constitutes a meaningful change in a distribution for your application.
Once you’ve set up a monitoring task, called an assay, comparisons against your baseline are then run automatically on a scheduled basis. You can be notified if the system notices any abnormally different behavior. The framework also allows you to quickly investigate the cause of any unexpected drifts in your predictions.
The rest of this notebook will shows how to create assays to monitor your pipelines.
NOTE: model insights operates over time and is difficult to demo in a notebook without pre-canned data. We assume you have an active pipeline that has been running and making predictions over time and show you the code you may use to analyze your pipeline.
Prerequisites
- A deployed Wallaroo instance
- The following Python libraries installed:
os
datetime
json
string
random
numpy
matplotlib
wallaroo
: The Wallaroo SDK. Included with the Wallaroo JupyterHub service by default.pandas
: Pandas, mainly used for Pandas DataFrame.pyarrow
: PyArrow for Apache Arrow support.
Workflow
Model Insights has the capability to perform interactive assays so that you can explore the data from a pipeline and learn how the data is behaving. With this information and the knowledge of your particular business use case you can then choose appropriate thresholds for persistent automatic assays as desired.
To get started lets import some libraries we’ll need.
import datetime as dt
from datetime import datetime, timedelta, timezone, tzinfo
import wallaroo
from wallaroo.object import EntityNotFoundError
import wallaroo.assay
from wallaroo.assay_config import BinMode, Aggregation, Metric
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import json
from IPython.display import display
# used to display dataframe information without truncating
from IPython.display import display
pd.set_option('display.max_colwidth', None)
plt.rcParams["figure.figsize"] = (12,6)
pd.options.display.float_format = '{:,.2f}'.format
# ignoring warnings for demonstration
import warnings
warnings.filterwarnings('ignore')
wallaroo.__version__
'2023.2.0rc3'
Connect to the Wallaroo Instance
The first step is to connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. For more information on Wallaroo Client settings, see the Client Connection guide.
# Client connection from local Wallaroo instance
wl = wallaroo.Client()
Connect to Workspace and Pipeline
We will now connect to the existing workspace and pipeline. Update the variables below to match the ones used for past inferences.
workspace_name = 'housepricedrift'
pipeline_name = 'housepricepipe'
model_name = 'housepricemodel'
# Used to generate a unique assay name for each run
import string
import random
# make a random 4 character prefix
prefix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
assay_name = f"{prefix}example assay"
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
pipeline = wl.build_pipeline(name)
return pipeline
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
pipeline = get_pipeline(pipeline_name)
pipeline
name | housepricepipe |
---|---|
created | 2023-05-17 20:41:50.504206+00:00 |
last_updated | 2023-05-17 20:41:50.757679+00:00 |
deployed | False |
tags | |
versions | 4d9dfb3b-c9ae-402a-96fc-20ae0a2b2279, fc68f5f2-7bbf-435e-b434-e0c89c28c6a9 |
steps | housepricemodel |
We assume the pipeline has been running for a while and there is a period of time that is free of errors that we’d like to use as the baseline. Lets note the start and end times. For this example we have 30 days of data from Jan 2023 and well use Jan 1 data as our baseline.
import datetime
baseline_start = datetime.datetime.fromisoformat('2023-01-01T00:00:00+00:00')
baseline_end = datetime.datetime.fromisoformat('2023-01-02T00:00:00+00:00')
last_day = datetime.datetime.fromisoformat('2023-02-01T00:00:00+00:00')
Lets create an assay using that pipeline and the model in the pipeline. We also specify the start end end of the baseline.
It is highly recommended when creating assays to set the input/output path with the add_iopath
method. This specifies:
- Whether to track the input or output variables of an inference.
- The name of the field to track.
- The index of the field.
In our example, that is output dense_2 0
for “track the outputs, by the field dense_2
, and the index of dense_2
at 0.
assay_builder = wl.build_assay(assay_name, pipeline, model_name, baseline_start, baseline_end).add_iopath("output dense_2 0")
We don’t know much about our baseline data yet so lets examine the data and create a couple of visual representations. First lets get some basic stats on the baseline data.
baseline_run = assay_builder.build().interactive_baseline_run()
baseline_run.baseline_stats()
Baseline | |
---|---|
count | 182 |
min | 12.00 |
max | 14.97 |
mean | 12.94 |
median | 12.88 |
std | 0.45 |
start | 2023-01-01T00:00:00Z |
end | 2023-01-02T00:00:00Z |
Another option is the baseline_dataframe
method to retrieve the baseline data with each field as a DataFrame column. To cut down on space, we’ll display just the output_dense_2_0
column, which corresponds to the output output_dense 2
iopath set earlier.
assay_dataframe = assay_builder.baseline_dataframe()
display(assay_dataframe.loc[:, ["time", "metadata", "output_dense_2_0"]])
time | metadata | output_dense_2_0 | |
---|---|---|---|
0 | 1672531200000 | {'last_model': '{"model_name": "housepricemodel", "model_sha": "test_version"}', 'profile': '{"elapsed_ns": 243}'} | 12.53 |
1 | 1672531676753 | {'last_model': '{"model_name": "housepricemodel", "model_sha": "test_version"}', 'profile': '{"elapsed_ns": 216}'} | 13.36 |
2 | 1672532153506 | {'last_model': '{"model_name": "housepricemodel", "model_sha": "test_version"}', 'profile': '{"elapsed_ns": 128}'} | 12.80 |
3 | 1672532630259 | {'last_model': '{"model_name": "housepricemodel", "model_sha": "test_version"}', 'profile': '{"elapsed_ns": 333}'} | 12.79 |
4 | 1672533107013 | {'last_model': '{"model_name": "housepricemodel", "model_sha": "test_version"}', 'profile': '{"elapsed_ns": 53}'} | 13.16 |
... | ... | ... | ... |
177 | 1672615585332 | {'last_model': '{"model_name": "housepricemodel", "model_sha": "test_version"}', 'profile': '{"elapsed_ns": 228}'} | 12.37 |
178 | 1672616062086 | {'last_model': '{"model_name": "housepricemodel", "model_sha": "test_version"}', 'profile': '{"elapsed_ns": 195}'} | 12.96 |
179 | 1672616538839 | {'last_model': '{"model_name": "housepricemodel", "model_sha": "test_version"}', 'profile': '{"elapsed_ns": 113}'} | 12.37 |
180 | 1672617015592 | {'last_model': '{"model_name": "housepricemodel", "model_sha": "test_version"}', 'profile': '{"elapsed_ns": 94}'} | 12.61 |
181 | 1672617492346 | {'last_model': '{"model_name": "housepricemodel", "model_sha": "test_version"}', 'profile': '{"elapsed_ns": 211}'} | 12.47 |
182 rows × 3 columns
Now lets look at a histogram, kernel density estimate (KDE), and Emperical Cumulative Distribution (ecdf) charts of the baseline data. These will give us insite into the distributions of the predictions and features that the assay is configured for.
assay_builder.baseline_histogram()
assay_builder.baseline_kde()
assay_builder.baseline_ecdf()
List Assays
Assays are listed through the Wallaroo Client list_assays
method.
wl.list_assays()
name | active | status | warning_threshold | alert_threshold | pipeline_name |
---|---|---|---|---|---|
api_assay | True | created | 0.0 | 0.1 | housepricepipe |
Interactive Baseline Runs
We can do an interactive run of just the baseline part to see how the baseline data will be put into bins. This assay uses quintiles so all 5 bins (not counting the outlier bins) have 20% of the predictions. We can see the bin boundaries along the x-axis.
baseline_run.chart()
baseline mean = 12.940910643273655
baseline median = 12.884286880493164
bin_mode = Quantile
aggregation = Density
metric = PSI
weighted = False
We can also get a dataframe with the bin/edge information.
baseline_run.baseline_bins()
b_edges | b_edge_names | b_aggregated_values | b_aggregation | |
---|---|---|---|---|
0 | 12.00 | left_outlier | 0.00 | Density |
1 | 12.55 | q_20 | 0.20 | Density |
2 | 12.81 | q_40 | 0.20 | Density |
3 | 12.98 | q_60 | 0.20 | Density |
4 | 13.33 | q_80 | 0.20 | Density |
5 | 14.97 | q_100 | 0.20 | Density |
6 | inf | right_outlier | 0.00 | Density |
The previous assay used quintiles so all of the bins had the same percentage/count of samples. To get bins that are divided equally along the range of values we can use BinMode.EQUAL
.
equal_bin_builder = wl.build_assay(assay_name, pipeline, model_name, baseline_start, baseline_end)
equal_bin_builder.summarizer_builder.add_bin_mode(BinMode.EQUAL)
equal_baseline = equal_bin_builder.build().interactive_baseline_run()
equal_baseline.chart()
baseline mean = 12.940910643273655
baseline median = 12.884286880493164
bin_mode = Equal
aggregation = Density
metric = PSI
weighted = False
We now see very different bin edges and sample percentages per bin.
equal_baseline.baseline_bins()
b_edges | b_edge_names | b_aggregated_values | b_aggregation | |
---|---|---|---|---|
0 | 12.00 | left_outlier | 0.00 | Density |
1 | 12.60 | p_1.26e1 | 0.24 | Density |
2 | 13.19 | p_1.32e1 | 0.49 | Density |
3 | 13.78 | p_1.38e1 | 0.22 | Density |
4 | 14.38 | p_1.44e1 | 0.04 | Density |
5 | 14.97 | p_1.50e1 | 0.01 | Density |
6 | inf | right_outlier | 0.00 | Density |
Interactive Assay Runs
By default the assay builder creates an assay with some good starting parameters. In particular the assay is configured to run a new analysis for every 24 hours starting at the end of the baseline period. Additionally, it sets the number of bins to 5 so creates quintiles, and sets the target iopath
to "outputs 0 0"
which means we want to monitor the first column of the first output/prediction.
We can do an interactive run of just the baseline part to see how the baseline data will be put into bins. This assay uses quintiles so all 5 bins (not counting the outlier bins) have 20% of the predictions. We can see the bin boundaries along the x-axis.
We then run it with interactive_run
and convert it to a dataframe for easy analysis with to_dataframe
.
Now lets do an interactive run of the first assay as it is configured. Interactive runs don’t save the assay to the database (so they won’t be scheduled in the future) nor do they save the assay results. Instead the results are returned after a short while for further analysis.
assay_builder = wl.build_assay(assay_name, pipeline, model_name, baseline_start, baseline_end)
assay_config = assay_builder.add_run_until(last_day).build()
assay_results = assay_config.interactive_run()
assay_df = assay_results.to_dataframe()
assay_df.loc[:, ~assay_df.columns.isin(['assay_id', 'iopath', 'name', 'warning_threshold'])]
score | start | min | max | mean | median | std | alert_threshold | status | |
---|---|---|---|---|---|---|---|---|---|
0 | 0.00 | 2023-01-02T00:00:00+00:00 | 12.05 | 14.71 | 12.97 | 12.90 | 0.48 | 0.25 | Ok |
1 | 0.09 | 2023-01-03T00:00:00+00:00 | 12.04 | 14.65 | 12.96 | 12.93 | 0.41 | 0.25 | Ok |
2 | 0.04 | 2023-01-04T00:00:00+00:00 | 11.87 | 14.02 | 12.98 | 12.95 | 0.46 | 0.25 | Ok |
3 | 0.06 | 2023-01-05T00:00:00+00:00 | 11.92 | 14.46 | 12.93 | 12.87 | 0.46 | 0.25 | Ok |
4 | 0.02 | 2023-01-06T00:00:00+00:00 | 12.02 | 14.15 | 12.95 | 12.90 | 0.43 | 0.25 | Ok |
5 | 0.03 | 2023-01-07T00:00:00+00:00 | 12.18 | 14.58 | 12.96 | 12.93 | 0.44 | 0.25 | Ok |
6 | 0.02 | 2023-01-08T00:00:00+00:00 | 12.01 | 14.60 | 12.92 | 12.90 | 0.46 | 0.25 | Ok |
7 | 0.04 | 2023-01-09T00:00:00+00:00 | 12.01 | 14.40 | 13.00 | 12.97 | 0.45 | 0.25 | Ok |
8 | 0.06 | 2023-01-10T00:00:00+00:00 | 11.99 | 14.79 | 12.94 | 12.91 | 0.46 | 0.25 | Ok |
9 | 0.02 | 2023-01-11T00:00:00+00:00 | 11.90 | 14.66 | 12.91 | 12.88 | 0.45 | 0.25 | Ok |
10 | 0.02 | 2023-01-12T00:00:00+00:00 | 11.96 | 14.82 | 12.94 | 12.90 | 0.46 | 0.25 | Ok |
11 | 0.03 | 2023-01-13T00:00:00+00:00 | 12.07 | 14.61 | 12.96 | 12.93 | 0.47 | 0.25 | Ok |
12 | 0.15 | 2023-01-14T00:00:00+00:00 | 12.00 | 14.20 | 13.06 | 13.03 | 0.43 | 0.25 | Ok |
13 | 2.92 | 2023-01-15T00:00:00+00:00 | 12.74 | 15.62 | 14.00 | 14.01 | 0.57 | 0.25 | Alert |
14 | 7.89 | 2023-01-16T00:00:00+00:00 | 14.64 | 17.19 | 15.91 | 15.87 | 0.63 | 0.25 | Alert |
15 | 8.87 | 2023-01-17T00:00:00+00:00 | 16.60 | 19.23 | 17.94 | 17.94 | 0.63 | 0.25 | Alert |
16 | 8.87 | 2023-01-18T00:00:00+00:00 | 18.67 | 21.29 | 20.01 | 20.04 | 0.64 | 0.25 | Alert |
17 | 8.87 | 2023-01-19T00:00:00+00:00 | 20.72 | 23.57 | 22.17 | 22.18 | 0.65 | 0.25 | Alert |
18 | 8.87 | 2023-01-20T00:00:00+00:00 | 23.04 | 25.72 | 24.32 | 24.33 | 0.66 | 0.25 | Alert |
19 | 8.87 | 2023-01-21T00:00:00+00:00 | 25.06 | 27.67 | 26.48 | 26.49 | 0.63 | 0.25 | Alert |
20 | 8.87 | 2023-01-22T00:00:00+00:00 | 27.21 | 29.89 | 28.63 | 28.58 | 0.65 | 0.25 | Alert |
21 | 8.87 | 2023-01-23T00:00:00+00:00 | 29.36 | 32.18 | 30.82 | 30.80 | 0.67 | 0.25 | Alert |
22 | 8.87 | 2023-01-24T00:00:00+00:00 | 31.56 | 34.35 | 32.98 | 32.98 | 0.65 | 0.25 | Alert |
23 | 8.87 | 2023-01-25T00:00:00+00:00 | 33.68 | 36.44 | 35.14 | 35.14 | 0.66 | 0.25 | Alert |
24 | 8.87 | 2023-01-26T00:00:00+00:00 | 35.93 | 38.51 | 37.31 | 37.33 | 0.65 | 0.25 | Alert |
25 | 3.69 | 2023-01-27T00:00:00+00:00 | 12.06 | 39.91 | 29.29 | 38.65 | 12.66 | 0.25 | Alert |
26 | 0.05 | 2023-01-28T00:00:00+00:00 | 11.87 | 13.88 | 12.92 | 12.90 | 0.38 | 0.25 | Ok |
27 | 0.10 | 2023-01-29T00:00:00+00:00 | 12.02 | 14.36 | 12.98 | 12.96 | 0.38 | 0.25 | Ok |
28 | 0.11 | 2023-01-30T00:00:00+00:00 | 11.99 | 14.44 | 12.89 | 12.88 | 0.37 | 0.25 | Ok |
29 | 0.01 | 2023-01-31T00:00:00+00:00 | 12.00 | 14.64 | 12.92 | 12.89 | 0.40 | 0.25 | Ok |
Basic functionality for creating quick charts is included.
assay_results.chart_scores()
We see that the difference scores are low for a while and then jump up to indicate there is an issue. We can examine that particular window to help us decide if that threshold is set correctly or not.
We can generate a quick chart of the results. This chart shows the 5 quantile bins (quintiles) derived from the baseline data plus one for left outliers and one for right outliers. We also see that the data from the window falls within the baseline quintiles but in a different proportion and is skewing higher. Whether this is an issue or not is specific to your use case.
First lets examine a day that is only slightly different than the baseline. We see that we do see some values that fall outside of the range from the baseline values, the left and right outliers, and that the bin values are different but similar.
assay_results[0].chart()
baseline mean = 12.940910643273655
window mean = 12.969964654406132
baseline median = 12.884286880493164
window median = 12.899214744567873
bin_mode = Quantile
aggregation = Density
metric = PSI
weighted = False
score = 0.0029273068646199748
scores = [0.0, 0.000514261205558409, 0.0002139202456922972, 0.0012617897456473992, 0.0002139202456922972, 0.0007234154220295724, 0.0]
index = None
Other days, however are significantly different.
assay_results[12].chart()
baseline mean = 12.940910643273655
window mean = 13.06380216891949
baseline median = 12.884286880493164
window median = 13.027600288391112
bin_mode = Quantile
aggregation = Density
metric = PSI
weighted = False
score = 0.15060511096978788
scores = [4.6637149189075455e-05, 0.05969428191167242, 0.00806617426854112, 0.008316273402678306, 0.07090885609902021, 0.003572888138686759, 0.0]
index = None
assay_results[13].chart()
baseline mean = 12.940910643273655
window mean = 14.004728427908038
baseline median = 12.884286880493164
window median = 14.009637832641602
bin_mode = Quantile
aggregation = Density
metric = PSI
weighted = False
score = 2.9220486095961196
scores = [0.0, 0.7090936334784107, 0.7130482300184766, 0.33500731896676245, 0.12171058214520876, 0.9038825518183468, 0.1393062931689142]
index = None
If we want to investigate further, we can run interactive assays on each of the inputs to see if any of them show anything abnormal. In this example we’ll provide the feature labels to create more understandable titles.
The current assay expects continuous data. Sometimes categorical data is encoded as 1 or 0 in a feature and sometimes in a limited number of values such as 1, 2, 3. If one value has high a percentage the analysis emits a warning so that we know the scores for that feature may not behave as we expect.
labels = ['bedrooms', 'bathrooms', 'lat', 'long', 'waterfront', 'sqft_living', 'sqft_lot', 'floors', 'view', 'condition', 'grade', 'sqft_above', 'sqft_basement', 'yr_built', 'yr_renovated', 'sqft_living15', 'sqft_lot15']
topic = wl.get_topic_name(pipeline.id())
all_inferences = wl.get_raw_pipeline_inference_logs(topic, baseline_start, last_day, model_name, limit=1_000_000)
assay_builder = wl.build_assay("Input Assay", pipeline, model_name, baseline_start, baseline_end).add_run_until(last_day)
assay_builder.window_builder().add_width(hours=4)
assay_config = assay_builder.build()
assay_results = assay_config.interactive_input_run(all_inferences, labels)
iadf = assay_results.to_dataframe()
display(iadf.loc[:, ~iadf.columns.isin(['assay_id', 'iopath', 'name', 'warning_threshold'])])
column distinct_vals label largest_pct
0 17 bedrooms 0.4244
1 44 bathrooms 0.2398
2 3281 lat 0.0014
3 959 long 0.0066
4 4 waterfront 0.9156 *** May not be continuous feature
5 3901 sqft_living 0.0032
6 3487 sqft_lot 0.0173
7 11 floors 0.4567
8 10 view 0.8337
9 9 condition 0.5915
10 19 grade 0.3943
11 745 sqft_above 0.0096
12 309 sqft_basement 0.5582
13 224 yr_built 0.0239
14 77 yr_renovated 0.8889
15 649 sqft_living15 0.0093
16 3280 sqft_lot15 0.0199
score | start | min | max | mean | median | std | alert_threshold | status | |
---|---|---|---|---|---|---|---|---|---|
0 | 0.19 | 2023-01-02T00:00:00+00:00 | -2.54 | 1.75 | 0.21 | 0.68 | 0.99 | 0.25 | Ok |
1 | 0.03 | 2023-01-02T04:00:00+00:00 | -1.47 | 2.82 | 0.21 | -0.40 | 0.95 | 0.25 | Ok |
2 | 0.09 | 2023-01-02T08:00:00+00:00 | -2.54 | 3.89 | -0.04 | -0.40 | 1.22 | 0.25 | Ok |
3 | 0.05 | 2023-01-02T12:00:00+00:00 | -1.47 | 2.82 | -0.12 | -0.40 | 0.94 | 0.25 | Ok |
4 | 0.08 | 2023-01-02T16:00:00+00:00 | -1.47 | 1.75 | -0.00 | -0.40 | 0.76 | 0.25 | Ok |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
3055 | 0.08 | 2023-01-31T04:00:00+00:00 | -0.42 | 4.87 | 0.25 | -0.17 | 1.13 | 0.25 | Ok |
3056 | 0.58 | 2023-01-31T08:00:00+00:00 | -0.43 | 2.01 | -0.04 | -0.21 | 0.48 | 0.25 | Alert |
3057 | 0.13 | 2023-01-31T12:00:00+00:00 | -0.32 | 7.75 | 0.30 | -0.20 | 1.57 | 0.25 | Ok |
3058 | 0.26 | 2023-01-31T16:00:00+00:00 | -0.43 | 5.88 | 0.19 | -0.18 | 1.17 | 0.25 | Alert |
3059 | 0.84 | 2023-01-31T20:00:00+00:00 | -0.40 | 0.52 | -0.17 | -0.25 | 0.18 | 0.25 | Alert |
3060 rows × 9 columns
We can chart each of the iopaths and do a visual inspection. From the charts we see that if any of the input features had significant differences in the first two days which we can choose to inspect further. Here we choose to show 3 charts just to save space in this notebook.
assay_results.chart_iopaths(labels=labels, selected_labels=['bedrooms', 'lat', 'sqft_living'])
When we are comfortable with what alert threshold should be for our specific purposes we can create and save an assay that will be automatically run on a daily basis.
In this example we’re create an assay that runs everyday against the baseline and has an alert threshold of 0.5.
Once we upload it it will be saved and scheduled for future data as well as run against past data.
alert_threshold = 0.5
import string
import random
prefix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
assay_name = f"{prefix}example assay"
assay_builder = wl.build_assay(assay_name, pipeline, model_name, baseline_start, baseline_end).add_alert_threshold(alert_threshold)
assay_id = assay_builder.upload()
After a short while, we can get the assay results for further analysis.
When we get the assay results, we see that the assays analysis is similar to the interactive run we started with though the analysis for the third day does not exceed the new alert threshold we set. And since we called upload
instead of interactive_run
the assay was saved to the system and will continue to run automatically on schedule from now on.
Scheduling Assays
By default assays are scheduled to run every 24 hours starting immediately after the baseline period ends.
However, you can control the start time by setting start
and the frequency by setting interval
on the window.
So to recap:
- The window width is the size of the window. The default is 24 hours.
- The interval is how often the analysis is run, how far the window is slid into the future based on the last run. The default is the window width.
- The window start is when the analysis should start. The default is the end of the baseline period.
For example to run an analysis every 12 hours on the previous 24 hours of data you’d set the window width to 24 (the default) and the interval to 12.
assay_builder = wl.build_assay(assay_name, pipeline, model_name, baseline_start, baseline_end)
assay_builder = assay_builder.add_run_until(last_day)
assay_builder.window_builder().add_width(hours=24).add_interval(hours=12)
assay_config = assay_builder.build()
assay_results = assay_config.interactive_run()
print(f"Generated {len(assay_results)} analyses")
Generated 59 analyses
assay_results.chart_scores()
To start a weekly analysis of the previous week on a specific day, set the start date (taking care to specify the desired timezone), and the width and interval to 1 week and of course an analysis won’t be generated till a window is complete.
report_start = datetime.datetime.fromisoformat('2022-01-03T00:00:00+00:00')
assay_builder = wl.build_assay(assay_name, pipeline, model_name, baseline_start, baseline_end)
assay_builder = assay_builder.add_run_until(last_day)
assay_builder.window_builder().add_width(weeks=1).add_interval(weeks=1).add_start(report_start)
assay_config = assay_builder.build()
assay_results = assay_config.interactive_run()
print(f"Generated {len(assay_results)} analyses")
Generated 5 analyses
assay_results.chart_scores()
Advanced Configuration
The assay can be configured in a variety of ways to help customize it to your particular needs. Specifically you can:
- change the BinMode to evenly spaced, quantile or user provided
- change the number of bins to use
- provide weights to use when scoring the bins
- calculate the score using the sum of differences, maximum difference or population stability index
- change the value aggregation for the bins to density, cumulative or edges
Lets take a look at these in turn.
Default configuration
First lets look at the default configuration. This is a lot of information but much of it is useful to know where it is available.
We see that the assay is broken up into 4 sections. A top level meta data section, a section for the baseline specification, a section for the window specification and a section that specifies the summarization configuration.
In the meta section we see the name of the assay, that it runs on the first column of the first output "outputs 0 0"
and that there is a default threshold of 0.25.
The summarizer section shows us the defaults of Quantile, Density and PSI on 5 bins.
The baseline section shows us that it is configured as a fixed baseline with the specified start and end date times.
And the window tells us what model in the pipeline we are analyzing and how often.
assay_builder = wl.build_assay(assay_name, pipeline, model_name, baseline_start, baseline_end).add_run_until(last_day)
print(assay_builder.build().to_json())
{
"name": "onmyexample assay",
"pipeline_id": 1,
"pipeline_name": "housepricepipe",
"active": true,
"status": "created",
"iopath": "output dense_2 0",
"baseline": {
"Fixed": {
"pipeline": "housepricepipe",
"model": "housepricemodel",
"start_at": "2023-01-01T00:00:00+00:00",
"end_at": "2023-01-02T00:00:00+00:00"
}
},
"window": {
"pipeline": "housepricepipe",
"model": "housepricemodel",
"width": "24 hours",
"start": null,
"interval": null
},
"summarizer": {
"type": "UnivariateContinuous",
"bin_mode": "Quantile",
"aggregation": "Density",
"metric": "PSI",
"num_bins": 5,
"bin_weights": null,
"bin_width": null,
"provided_edges": null,
"add_outlier_edges": true
},
"warning_threshold": null,
"alert_threshold": 0.25,
"run_until": "2023-02-01T00:00:00+00:00",
"workspace_id": 5
}
Defaults
We can run the assay interactively and review the first analysis. The method compare_basic_stats
gives us a dataframe with basic stats for the baseline and window data.
assay_results = assay_builder.build().interactive_run()
ar = assay_results[0]
ar.compare_basic_stats()
Baseline | Window | diff | pct_diff | |
---|---|---|---|---|
count | 182.00 | 181.00 | -1.00 | -0.55 |
min | 12.00 | 12.05 | 0.04 | 0.36 |
max | 14.97 | 14.71 | -0.26 | -1.71 |
mean | 12.94 | 12.97 | 0.03 | 0.22 |
median | 12.88 | 12.90 | 0.01 | 0.12 |
std | 0.45 | 0.48 | 0.03 | 5.68 |
start | 2023-01-01T00:00:00+00:00 | 2023-01-02T00:00:00+00:00 | NaN | NaN |
end | 2023-01-02T00:00:00+00:00 | 2023-01-03T00:00:00+00:00 | NaN | NaN |
The method compare_bins
gives us a dataframe with the bin information. Such as the number of bins, the right edges, suggested bin/edge names and the values for each bin in the baseline and the window.
assay_bins = ar.compare_bins()
display(assay_bins.loc[:, assay_bins.columns!='w_aggregation'])
b_edges | b_edge_names | b_aggregated_values | b_aggregation | w_edges | w_edge_names | w_aggregated_values | diff_in_pcts | |
---|---|---|---|---|---|---|---|---|
0 | 12.00 | left_outlier | 0.00 | Density | 12.00 | left_outlier | 0.00 | 0.00 |
1 | 12.55 | q_20 | 0.20 | Density | 12.55 | e_1.26e1 | 0.19 | -0.01 |
2 | 12.81 | q_40 | 0.20 | Density | 12.81 | e_1.28e1 | 0.21 | 0.01 |
3 | 12.98 | q_60 | 0.20 | Density | 12.98 | e_1.30e1 | 0.18 | -0.02 |
4 | 13.33 | q_80 | 0.20 | Density | 13.33 | e_1.33e1 | 0.21 | 0.01 |
5 | 14.97 | q_100 | 0.20 | Density | 14.97 | e_1.50e1 | 0.21 | 0.01 |
6 | NaN | right_outlier | 0.00 | Density | NaN | right_outlier | 0.00 | 0.00 |
We can also plot the chart to visualize the values of the bins.
ar.chart()
baseline mean = 12.940910643273655
window mean = 12.969964654406132
baseline median = 12.884286880493164
window median = 12.899214744567873
bin_mode = Quantile
aggregation = Density
metric = PSI
weighted = False
score = 0.0029273068646199748
scores = [0.0, 0.000514261205558409, 0.0002139202456922972, 0.0012617897456473992, 0.0002139202456922972, 0.0007234154220295724, 0.0]
index = None
Binning Mode
We can change the bin mode algorithm to equal and see that the bins/edges are partitioned at different points and the bins have different values.
prefix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
assay_name = f"{prefix}example assay"
assay_builder = wl.build_assay(assay_name, pipeline, model_name, baseline_start, baseline_end).add_run_until(last_day)
assay_builder.summarizer_builder.add_bin_mode(BinMode.EQUAL)
assay_results = assay_builder.build().interactive_run()
assay_results_df = assay_results[0].compare_bins()
display(assay_results_df.loc[:, ~assay_results_df.columns.isin(['b_aggregation', 'w_aggregation'])])
assay_results[0].chart()
b_edges | b_edge_names | b_aggregated_values | w_edges | w_edge_names | w_aggregated_values | diff_in_pcts | |
---|---|---|---|---|---|---|---|
0 | 12.00 | left_outlier | 0.00 | 12.00 | left_outlier | 0.00 | 0.00 |
1 | 12.60 | p_1.26e1 | 0.24 | 12.60 | e_1.26e1 | 0.24 | 0.00 |
2 | 13.19 | p_1.32e1 | 0.49 | 13.19 | e_1.32e1 | 0.48 | -0.02 |
3 | 13.78 | p_1.38e1 | 0.22 | 13.78 | e_1.38e1 | 0.22 | -0.00 |
4 | 14.38 | p_1.44e1 | 0.04 | 14.38 | e_1.44e1 | 0.06 | 0.02 |
5 | 14.97 | p_1.50e1 | 0.01 | 14.97 | e_1.50e1 | 0.01 | 0.00 |
6 | NaN | right_outlier | 0.00 | NaN | right_outlier | 0.00 | 0.00 |
baseline mean = 12.940910643273655
window mean = 12.969964654406132
baseline median = 12.884286880493164
window median = 12.899214744567873
bin_mode = Equal
aggregation = Density
metric = PSI
weighted = False
score = 0.011074287819376092
scores = [0.0, 7.3591419975306595e-06, 0.000773779195360713, 8.538514991838585e-05, 0.010207597078872246, 1.6725322721660374e-07, 0.0]
index = None
User Provided Bin Edges
The values in this dataset run from ~11.6 to ~15.81. And lets say we had a business reason to use specific bin edges. We can specify them with the BinMode.PROVIDED and specifying a list of floats with the right hand / upper edge of each bin and optionally the lower edge of the smallest bin. If the lowest edge is not specified the threshold for left outliers is taken from the smallest value in the baseline dataset.
edges = [11.0, 12.0, 13.0, 14.0, 15.0, 16.0]
assay_builder = wl.build_assay(assay_name, pipeline, model_name, baseline_start, baseline_end).add_run_until(last_day)
assay_builder.summarizer_builder.add_bin_mode(BinMode.PROVIDED, edges)
assay_results = assay_builder.build().interactive_run()
assay_results_df = assay_results[0].compare_bins()
display(assay_results_df.loc[:, ~assay_results_df.columns.isin(['b_aggregation', 'w_aggregation'])])
assay_results[0].chart()
b_edges | b_edge_names | b_aggregated_values | w_edges | w_edge_names | w_aggregated_values | diff_in_pcts | |
---|---|---|---|---|---|---|---|
0 | 11.00 | left_outlier | 0.00 | 11.00 | left_outlier | 0.00 | 0.00 |
1 | 12.00 | e_1.20e1 | 0.00 | 12.00 | e_1.20e1 | 0.00 | 0.00 |
2 | 13.00 | e_1.30e1 | 0.62 | 13.00 | e_1.30e1 | 0.59 | -0.03 |
3 | 14.00 | e_1.40e1 | 0.36 | 14.00 | e_1.40e1 | 0.35 | -0.00 |
4 | 15.00 | e_1.50e1 | 0.02 | 15.00 | e_1.50e1 | 0.06 | 0.03 |
5 | 16.00 | e_1.60e1 | 0.00 | 16.00 | e_1.60e1 | 0.00 | 0.00 |
6 | NaN | right_outlier | 0.00 | NaN | right_outlier | 0.00 | 0.00 |
baseline mean = 12.940910643273655
window mean = 12.969964654406132
baseline median = 12.884286880493164
window median = 12.899214744567873
bin_mode = Provided
aggregation = Density
metric = PSI
weighted = False
score = 0.0321620386600679
scores = [0.0, 0.0, 0.0014576920813015586, 3.549754401142936e-05, 0.030668849034754912, 0.0, 0.0]
index = None
Number of Bins
We could also choose to a different number of bins, lets say 10, which can be evenly spaced or based on the quantiles (deciles).
assay_builder = wl.build_assay(assay_name, pipeline, model_name, baseline_start, baseline_end).add_run_until(last_day)
assay_builder.summarizer_builder.add_bin_mode(BinMode.QUANTILE).add_num_bins(10)
assay_results = assay_builder.build().interactive_run()
assay_results_df = assay_results[1].compare_bins()
display(assay_results_df.loc[:, ~assay_results_df.columns.isin(['b_aggregation', 'w_aggregation'])])
assay_results[1].chart()
b_edges | b_edge_names | b_aggregated_values | w_edges | w_edge_names | w_aggregated_values | diff_in_pcts | |
---|---|---|---|---|---|---|---|
0 | 12.00 | left_outlier | 0.00 | 12.00 | left_outlier | 0.00 | 0.00 |
1 | 12.41 | q_10 | 0.10 | 12.41 | e_1.24e1 | 0.09 | -0.00 |
2 | 12.55 | q_20 | 0.10 | 12.55 | e_1.26e1 | 0.04 | -0.05 |
3 | 12.72 | q_30 | 0.10 | 12.72 | e_1.27e1 | 0.14 | 0.03 |
4 | 12.81 | q_40 | 0.10 | 12.81 | e_1.28e1 | 0.05 | -0.05 |
5 | 12.88 | q_50 | 0.10 | 12.88 | e_1.29e1 | 0.12 | 0.02 |
6 | 12.98 | q_60 | 0.10 | 12.98 | e_1.30e1 | 0.09 | -0.01 |
7 | 13.15 | q_70 | 0.10 | 13.15 | e_1.32e1 | 0.18 | 0.08 |
8 | 13.33 | q_80 | 0.10 | 13.33 | e_1.33e1 | 0.14 | 0.03 |
9 | 13.47 | q_90 | 0.10 | 13.47 | e_1.35e1 | 0.07 | -0.03 |
10 | 14.97 | q_100 | 0.10 | 14.97 | e_1.50e1 | 0.08 | -0.02 |
11 | NaN | right_outlier | 0.00 | NaN | right_outlier | 0.00 | 0.00 |
baseline mean = 12.940910643273655
window mean = 12.956829186961135
baseline median = 12.884286880493164
window median = 12.929338455200195
bin_mode = Quantile
aggregation = Density
metric = PSI
weighted = False
score = 0.16591076620684958
scores = [0.0, 0.0002571306027792045, 0.044058279699182114, 0.009441459631493015, 0.03381618572319047, 0.0027335446937028877, 0.0011792419836838435, 0.051023062424253904, 0.009441459631493015, 0.008662563542113508, 0.0052978382749576496, 0.0]
index = None
Bin Weights
Now lets say we only care about differences at the higher end of the range. We can use weights to specify that difference in the lower bins should not be counted in the score.
If we stick with 10 bins we can provide 10 a vector of 12 weights. One weight each for the original bins plus one at the front for the left outlier bin and one at the end for the right outlier bin.
Note we still show the values for the bins but the scores for the lower 5 and left outlier are 0 and only the right half is counted and reflected in the score.
weights = [0] * 6
weights.extend([1] * 6)
print("Using weights: ", weights)
assay_builder = wl.build_assay(assay_name, pipeline, model_name, baseline_start, baseline_end).add_run_until(last_day)
assay_builder.summarizer_builder.add_bin_mode(BinMode.QUANTILE).add_num_bins(10).add_bin_weights(weights)
assay_results = assay_builder.build().interactive_run()
assay_results_df = assay_results[1].compare_bins()
display(assay_results_df.loc[:, ~assay_results_df.columns.isin(['b_aggregation', 'w_aggregation'])])
assay_results[1].chart()
Using weights: [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1]
b_edges | b_edge_names | b_aggregated_values | w_edges | w_edge_names | w_aggregated_values | diff_in_pcts | |
---|---|---|---|---|---|---|---|
0 | 12.00 | left_outlier | 0.00 | 12.00 | left_outlier | 0.00 | 0.00 |
1 | 12.41 | q_10 | 0.10 | 12.41 | e_1.24e1 | 0.09 | -0.00 |
2 | 12.55 | q_20 | 0.10 | 12.55 | e_1.26e1 | 0.04 | -0.05 |
3 | 12.72 | q_30 | 0.10 | 12.72 | e_1.27e1 | 0.14 | 0.03 |
4 | 12.81 | q_40 | 0.10 | 12.81 | e_1.28e1 | 0.05 | -0.05 |
5 | 12.88 | q_50 | 0.10 | 12.88 | e_1.29e1 | 0.12 | 0.02 |
6 | 12.98 | q_60 | 0.10 | 12.98 | e_1.30e1 | 0.09 | -0.01 |
7 | 13.15 | q_70 | 0.10 | 13.15 | e_1.32e1 | 0.18 | 0.08 |
8 | 13.33 | q_80 | 0.10 | 13.33 | e_1.33e1 | 0.14 | 0.03 |
9 | 13.47 | q_90 | 0.10 | 13.47 | e_1.35e1 | 0.07 | -0.03 |
10 | 14.97 | q_100 | 0.10 | 14.97 | e_1.50e1 | 0.08 | -0.02 |
11 | NaN | right_outlier | 0.00 | NaN | right_outlier | 0.00 | 0.00 |
baseline mean = 12.940910643273655
window mean = 12.956829186961135
baseline median = 12.884286880493164
window median = 12.929338455200195
bin_mode = Quantile
aggregation = Density
metric = PSI
weighted = True
score = 0.012600694309416988
scores = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.00019654033061397393, 0.00850384373737565, 0.0015735766052488358, 0.0014437605903522511, 0.000882973045826275, 0.0]
index = None
Metrics
The score
is a distance or dis-similarity measure. The larger it is the less similar the two distributions are. We currently support
summing the differences of each individual bin, taking the maximum difference and a modified Population Stability Index (PSI).
The following three charts use each of the metrics. Note how the scores change. The best one will depend on your particular use case.
assay_builder = wl.build_assay(assay_name, pipeline, model_name, baseline_start, baseline_end).add_run_until(last_day)
assay_results = assay_builder.build().interactive_run()
assay_results[0].chart()
baseline mean = 12.940910643273655
window mean = 12.969964654406132
baseline median = 12.884286880493164
window median = 12.899214744567873
bin_mode = Quantile
aggregation = Density
metric = PSI
weighted = False
score = 0.0029273068646199748
scores = [0.0, 0.000514261205558409, 0.0002139202456922972, 0.0012617897456473992, 0.0002139202456922972, 0.0007234154220295724, 0.0]
index = None
assay_builder = wl.build_assay(assay_name, pipeline, model_name, baseline_start, baseline_end).add_run_until(last_day)
assay_builder.summarizer_builder.add_metric(Metric.SUMDIFF)
assay_results = assay_builder.build().interactive_run()
assay_results[0].chart()
baseline mean = 12.940910643273655
window mean = 12.969964654406132
baseline median = 12.884286880493164
window median = 12.899214744567873
bin_mode = Quantile
aggregation = Density
metric = SumDiff
weighted = False
score = 0.025438649748041997
scores = [0.0, 0.009956893934794486, 0.006648048084512165, 0.01548175581324751, 0.006648048084512165, 0.012142553579017668, 0.0]
index = None
assay_builder = wl.build_assay(assay_name, pipeline, model_name, baseline_start, baseline_end).add_run_until(last_day)
assay_builder.summarizer_builder.add_metric(Metric.MAXDIFF)
assay_results = assay_builder.build().interactive_run()
assay_results[0].chart()
baseline mean = 12.940910643273655
window mean = 12.969964654406132
baseline median = 12.884286880493164
window median = 12.899214744567873
bin_mode = Quantile
aggregation = Density
metric = MaxDiff
weighted = False
score = 0.01548175581324751
scores = [0.0, 0.009956893934794486, 0.006648048084512165, 0.01548175581324751, 0.006648048084512165, 0.012142553579017668, 0.0]
index = 3
Aggregation Options
Also, bin aggregation can be done in histogram Aggregation.DENSITY
style (the default) where we count the number/percentage of values that fall in each bin or Empirical Cumulative Density Function style Aggregation.CUMULATIVE
where we keep a cumulative count of the values/percentages that fall in each bin.
assay_builder = wl.build_assay(assay_name, pipeline, model_name, baseline_start, baseline_end).add_run_until(last_day)
assay_builder.summarizer_builder.add_aggregation(Aggregation.DENSITY)
assay_results = assay_builder.build().interactive_run()
assay_results[0].chart()
baseline mean = 12.940910643273655
window mean = 12.969964654406132
baseline median = 12.884286880493164
window median = 12.899214744567873
bin_mode = Quantile
aggregation = Density
metric = PSI
weighted = False
score = 0.0029273068646199748
scores = [0.0, 0.000514261205558409, 0.0002139202456922972, 0.0012617897456473992, 0.0002139202456922972, 0.0007234154220295724, 0.0]
index = None
assay_builder = wl.build_assay(assay_name, pipeline, model_name, baseline_start, baseline_end).add_run_until(last_day)
assay_builder.summarizer_builder.add_aggregation(Aggregation.CUMULATIVE)
assay_results = assay_builder.build().interactive_run()
assay_results[0].chart()
baseline mean = 12.940910643273655
window mean = 12.969964654406132
baseline median = 12.884286880493164
window median = 12.899214744567873
bin_mode = Quantile
aggregation = Cumulative
metric = PSI
weighted = False
score = 0.04419889502762442
scores = [0.0, 0.009956893934794486, 0.0033088458502823492, 0.01879060166352986, 0.012142553579017725, 0.0, 0.0]
index = None
4.5 - Pipeline Logs Tutorial
This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.
Pipeline Log Tutorial
This tutorial demonstrates Wallaroo Pipeline logs and
This tutorial will demonstrate how to:
- Select or create a workspace, pipeline and upload the control model, then additional models for A/B Testing and Shadow Deploy.
- Add a pipeline step with the champion model, then deploy the pipeline and perform sample inferences.
- Display the various log types for a standard deployed pipeline.
- Swap out the pipeline step with the champion model with a shadow deploy step that compares the champion model against two competitors.
- Perform sample inferences with a shadow deployed step, then display the log files for a shadow deployed pipeline.
- Swap out the shadow deployed pipeline step with an A/B pipeline step.
- Perform sample inferences with a A/B pipeline step, then display the log files for an A/B pipeline step.
- Undeploy the pipeline.
This tutorial provides the following:
- Models:
models/rf_model.onnx
: The champion model that has been used in this environment for some time.models/xgb_model.onnx
andmodels/gbr_model.onnx
: Rival models that will be tested against the champion.
- Data:
data/xtest-1.df.json
anddata/xtest-1k.df.json
: DataFrame JSON inference inputs with 1 input and 1,000 inputs.data/xtest-1k.arrow
: Apache Arrow inference inputs with 1 input and 1,000 inputs.
Prerequisites
- A deployed Wallaroo instance
- The following Python libraries installed:
Initial Steps
Import libraries
The first step is to import the libraries needed for this notebook.
import wallaroo
from wallaroo.object import EntityNotFoundError
import pyarrow as pa
from IPython.display import display
# used to display DataFrame information without truncating
from IPython.display import display
import pandas as pd
pd.set_option('display.max_colwidth', None)
import datetime
import os
Connect to the Wallaroo Instance
The first step is to connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. For more information on Wallaroo Client settings, see the Client Connection guide.
# Login through local Wallaroo instance
wl = wallaroo.Client()
Create Workspace
We will create a workspace to manage our pipeline and models. The following variables will set the name of our sample workspace then set it as the current workspace.
workspace_name = 'logworkspace'
main_pipeline_name = 'logpipeline'
model_name_control = 'logcontrol'
model_file_name_control = './models/rf_model.onnx'
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
{'name': 'logworkspace', 'id': 26, 'archived': False, 'created_by': '4e296632-35b3-460e-85fe-565e311bc566', 'created_at': '2023-07-14T15:49:20.890382+00:00', 'models': [], 'pipelines': []}
Standard Pipeline
Upload The Champion Model
For our example, we will upload the champion model that has been trained to derive house prices from a variety of inputs. The model file is rf_model.onnx
, and is uploaded with the name housingcontrol
.
housing_model_control = wl.upload_model(model_name_control, model_file_name_control, framework=wallaroo.framework.Framework.ONNX).configure()
Build the Pipeline
This pipeline is made to be an example of an existing situation where a model is deployed and being used for inferences in a production environment. We’ll call it housepricepipeline
, set housingcontrol
as a pipeline step, then run a few sample inferences.
mainpipeline = wl.build_pipeline(main_pipeline_name)
mainpipeline.undeploy()
# in case this pipeline was run before
mainpipeline.clear()
mainpipeline.add_model_step(housing_model_control).deploy()
name | logpipeline |
---|---|
created | 2023-07-14 15:49:23.959261+00:00 |
last_updated | 2023-07-14 15:49:24.981192+00:00 |
deployed | True |
tags | |
versions | 48ec856b-9640-4a04-83f6-77a9bd205a44, 8a9d1d69-d71d-4c0a-9c95-99b3f86dcbc7 |
steps | logcontrol |
Testing
We’ll use two inferences as a quick sample test - one that has a house that should be determined around $700k, the other with a house determined to be around $1.5 million. We’ll also save the start and end periods for these events to for later log functionality.
dataframe_start = datetime.datetime.now()
normal_input = pd.DataFrame.from_records({"tensor": [[4.0, 2.5, 2900.0, 5505.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2900.0, 0.0, 47.6063, -122.02, 2970.0, 5251.0, 12.0, 0.0, 0.0]]})
result = mainpipeline.infer(normal_input)
display(result)
time | in.tensor | out.variable | check_failures | |
---|---|---|---|---|
0 | 2023-07-14 15:49:36.579 | [4.0, 2.5, 2900.0, 5505.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2900.0, 0.0, 47.6063, -122.02, 2970.0, 5251.0, 12.0, 0.0, 0.0] | [718013.7] | 0 |
large_house_input = pd.DataFrame.from_records({'tensor': [[4.0, 3.0, 3710.0, 20000.0, 2.0, 0.0, 2.0, 5.0, 10.0, 2760.0, 950.0, 47.6696, -122.261, 3970.0, 20000.0, 79.0, 0.0, 0.0]]})
large_house_result = mainpipeline.infer(large_house_input)
display(large_house_result)
import time
time.sleep(10)
dataframe_end = datetime.datetime.now()
time | in.tensor | out.variable | check_failures | |
---|---|---|---|---|
0 | 2023-07-14 15:49:36.996 | [4.0, 3.0, 3710.0, 20000.0, 2.0, 0.0, 2.0, 5.0, 10.0, 2760.0, 950.0, 47.6696, -122.261, 3970.0, 20000.0, 79.0, 0.0, 0.0] | [1514079.4] | 0 |
As one last sample, we’ll run through roughly 1,000 inferences at once and show a few of the results. For this example we’ll use an Apache Arrow table, which has a smaller file size compared to uploading a pandas DataFrame JSON file. The inference result is returned as an arrow table, which we’ll convert into a pandas DataFrame to display the first 20 results.
batch_inferences = mainpipeline.infer_from_file('./data/xtest-1k.arrow')
large_inference_result = batch_inferences.to_pandas()
display(large_inference_result.head(20))
time | in.tensor | out.variable | check_failures | |
---|---|---|---|---|
0 | 2023-07-14 15:49:47.621 | [4.0, 2.5, 2900.0, 5505.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2900.0, 0.0, 47.6063, -122.02, 2970.0, 5251.0, 12.0, 0.0, 0.0] | [718013.75] | 0 |
1 | 2023-07-14 15:49:47.621 | [2.0, 2.5, 2170.0, 6361.0, 1.0, 0.0, 2.0, 3.0, 8.0, 2170.0, 0.0, 47.7109, -122.017, 2310.0, 7419.0, 6.0, 0.0, 0.0] | [615094.56] | 0 |
2 | 2023-07-14 15:49:47.621 | [3.0, 2.5, 1300.0, 812.0, 2.0, 0.0, 0.0, 3.0, 8.0, 880.0, 420.0, 47.5893, -122.317, 1300.0, 824.0, 6.0, 0.0, 0.0] | [448627.72] | 0 |
3 | 2023-07-14 15:49:47.621 | [4.0, 2.5, 2500.0, 8540.0, 2.0, 0.0, 0.0, 3.0, 9.0, 2500.0, 0.0, 47.5759, -121.994, 2560.0, 8475.0, 24.0, 0.0, 0.0] | [758714.2] | 0 |
4 | 2023-07-14 15:49:47.621 | [3.0, 1.75, 2200.0, 11520.0, 1.0, 0.0, 0.0, 4.0, 7.0, 2200.0, 0.0, 47.7659, -122.341, 1690.0, 8038.0, 62.0, 0.0, 0.0] | [513264.7] | 0 |
5 | 2023-07-14 15:49:47.621 | [3.0, 2.0, 2140.0, 4923.0, 1.0, 0.0, 0.0, 4.0, 8.0, 1070.0, 1070.0, 47.6902, -122.339, 1470.0, 4923.0, 86.0, 0.0, 0.0] | [668288.0] | 0 |
6 | 2023-07-14 15:49:47.621 | [4.0, 3.5, 3590.0, 5334.0, 2.0, 0.0, 2.0, 3.0, 9.0, 3140.0, 450.0, 47.6763, -122.267, 2100.0, 6250.0, 9.0, 0.0, 0.0] | [1004846.5] | 0 |
7 | 2023-07-14 15:49:47.621 | [3.0, 2.0, 1280.0, 960.0, 2.0, 0.0, 0.0, 3.0, 9.0, 1040.0, 240.0, 47.602, -122.311, 1280.0, 1173.0, 0.0, 0.0, 0.0] | [684577.2] | 0 |
8 | 2023-07-14 15:49:47.621 | [4.0, 2.5, 2820.0, 15000.0, 2.0, 0.0, 0.0, 4.0, 9.0, 2820.0, 0.0, 47.7255, -122.101, 2440.0, 15000.0, 29.0, 0.0, 0.0] | [727898.1] | 0 |
9 | 2023-07-14 15:49:47.621 | [3.0, 2.25, 1790.0, 11393.0, 1.0, 0.0, 0.0, 3.0, 8.0, 1790.0, 0.0, 47.6297, -122.099, 2290.0, 11894.0, 36.0, 0.0, 0.0] | [559631.1] | 0 |
10 | 2023-07-14 15:49:47.621 | [3.0, 1.5, 1010.0, 7683.0, 1.5, 0.0, 0.0, 5.0, 7.0, 1010.0, 0.0, 47.72, -122.318, 1550.0, 7271.0, 61.0, 0.0, 0.0] | [340764.53] | 0 |
11 | 2023-07-14 15:49:47.621 | [3.0, 2.0, 1270.0, 1323.0, 3.0, 0.0, 0.0, 3.0, 8.0, 1270.0, 0.0, 47.6934, -122.342, 1330.0, 1323.0, 8.0, 0.0, 0.0] | [442168.06] | 0 |
12 | 2023-07-14 15:49:47.621 | [4.0, 1.75, 2070.0, 9120.0, 1.0, 0.0, 0.0, 4.0, 7.0, 1250.0, 820.0, 47.6045, -122.123, 1650.0, 8400.0, 57.0, 0.0, 0.0] | [630865.6] | 0 |
13 | 2023-07-14 15:49:47.621 | [4.0, 1.0, 1620.0, 4080.0, 1.5, 0.0, 0.0, 3.0, 7.0, 1620.0, 0.0, 47.6696, -122.324, 1760.0, 4080.0, 91.0, 0.0, 0.0] | [559631.1] | 0 |
14 | 2023-07-14 15:49:47.621 | [4.0, 3.25, 3990.0, 9786.0, 2.0, 0.0, 0.0, 3.0, 9.0, 3990.0, 0.0, 47.6784, -122.026, 3920.0, 8200.0, 10.0, 0.0, 0.0] | [909441.1] | 0 |
15 | 2023-07-14 15:49:47.621 | [4.0, 2.0, 1780.0, 19843.0, 1.0, 0.0, 0.0, 3.0, 7.0, 1780.0, 0.0, 47.4414, -122.154, 2210.0, 13500.0, 52.0, 0.0, 0.0] | [313096.0] | 0 |
16 | 2023-07-14 15:49:47.621 | [4.0, 2.5, 2130.0, 6003.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2130.0, 0.0, 47.4518, -122.12, 1940.0, 4529.0, 11.0, 0.0, 0.0] | [404040.8] | 0 |
17 | 2023-07-14 15:49:47.621 | [3.0, 1.75, 1660.0, 10440.0, 1.0, 0.0, 0.0, 3.0, 7.0, 1040.0, 620.0, 47.4448, -121.77, 1240.0, 10380.0, 36.0, 0.0, 0.0] | [292859.5] | 0 |
18 | 2023-07-14 15:49:47.621 | [3.0, 2.5, 2110.0, 4118.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2110.0, 0.0, 47.3878, -122.153, 2110.0, 4044.0, 25.0, 0.0, 0.0] | [338357.88] | 0 |
19 | 2023-07-14 15:49:47.621 | [4.0, 2.25, 2200.0, 11250.0, 1.5, 0.0, 0.0, 5.0, 7.0, 1300.0, 900.0, 47.6845, -122.201, 2320.0, 10814.0, 94.0, 0.0, 0.0] | [682284.6] | 0 |
Standard Pipeline Logs
Pipeline logs with standard pipeline steps are retrieved either with:
- Pipeline
logs
which returns either a pandas DataFrame or Apache Arrow table. - Pipeline
export_logs
which saves the logs either a pandas DataFrame JSON file or Apache Arrow table.
For full details, see the Wallaroo Documentation Pipeline Log Management guide.
Pipeline Log Method
The Pipeline logs
method includes the following parameters. For a complete list, see the Wallaroo SDK Essentials Guide: Pipeline Log Management.
Parameter | Type | Description |
---|---|---|
limit | Int (Optional) | Limits how many log records to display. Defaults to 100 . If there are more pipeline logs than are being displayed, the Warning message Pipeline log record limit exceeded will be displayed. For example, if 100 log files were requested and there are a total of 1,000, the warning message will be displayed. |
start_datetime and end_datetime | DateTime (Optional) | Limits logs to all logs between the start and end DateTime parameters. Both parameters must be provided. Submitting a logs() request with only start_datetime or end_datetime will generate an exception.If start_datetime and end_datetime are provided as parameters, then the records are returned in chronological order, with the oldest record displayed first. |
dataset | List (OPTIONAL) | The datasets to be returned. The datasets available are:
metadata.elapsed : IMPORTANT NOTE: See Metadata Requests Restrictionsfor specifications on how this dataset can be used with other datasets.
|
arrow | Boolean (Optional) | Defaults to False. If arrow is set to True , then the logs are returned as an Apache Arrow table. If arrow=False , then the logs are returned as a pandas DataFrame. |
Pipeline Log Warnings
If the total number of logs the either the set limit or 10 MB in file size, the following warning is returned:
Warning: There are more logs available. Please set a larger limit or request a file using export_logs.
If the total number of logs requested either through the limit or through the start_datetime
and end_datetime
request is greater than 10 MB in size, the following error is displayed:
Warning: Pipeline log size limit exceeded. Only displaying 509 log messages. Please request a file using export_logs.
The following examples demonstrate displaying the logs, then displaying the logs between the control_model_start
and control_model_end
periods, then again retrieved as an Arrow table with the logs limited to only 5 entries.
# pipeline log retrieval - reverse chronological order
regular_logs = mainpipeline.logs()
display("Standard Logs")
display(len(regular_logs))
display(regular_logs)
# Display metadata
metadatalogs = mainpipeline.logs(dataset=["time", "out.variable", "metadata"])
display("Metadata Logs")
# Only showing the pipeline version for space reasons
display(metadatalogs.loc[:, ["time", "out.variable", "metadata.pipeline_version"]])
# Display logs restricted by date and limit
display("Logs restricted by date")
arrow_logs = mainpipeline.logs(start_datetime=dataframe_start, end_datetime=dataframe_end, limit=50)
display(len(arrow_logs))
display(arrow_logs)
# # pipeline log retrieval limited to the last 5 an an arrow table
display("Arrow logs by limit")
display(mainpipeline.logs(arrow=True))
Warning: There are more logs available. Please set a larger limit or request a file using export_logs.
‘Standard Logs’
100
time | in.tensor | out.variable | check_failures | |
---|---|---|---|---|
0 | 2023-07-14 15:49:47.621 | [3.0, 2.0, 2005.0, 7000.0, 1.0, 0.0, 0.0, 3.0, 7.0, 1605.0, 400.0, 47.6039, -122.298, 1750.0, 4500.0, 34.0, 0.0, 0.0] | [581003.0] | 0 |
1 | 2023-07-14 15:49:47.621 | [3.0, 1.75, 2910.0, 37461.0, 1.0, 0.0, 0.0, 4.0, 7.0, 1530.0, 1380.0, 47.7015, -122.164, 2520.0, 18295.0, 47.0, 0.0, 0.0] | [706823.56] | 0 |
2 | 2023-07-14 15:49:47.621 | [4.0, 3.25, 2910.0, 1880.0, 2.0, 0.0, 3.0, 5.0, 9.0, 1830.0, 1080.0, 47.616, -122.282, 3100.0, 8200.0, 100.0, 0.0, 0.0] | [1060847.5] | 0 |
3 | 2023-07-14 15:49:47.621 | [4.0, 1.75, 2700.0, 7875.0, 1.5, 0.0, 0.0, 4.0, 8.0, 2700.0, 0.0, 47.454, -122.144, 2220.0, 7875.0, 46.0, 0.0, 0.0] | [441960.38] | 0 |
4 | 2023-07-14 15:49:47.621 | [3.0, 2.5, 2900.0, 23550.0, 1.0, 0.0, 0.0, 3.0, 10.0, 1490.0, 1410.0, 47.5708, -122.153, 2900.0, 19604.0, 27.0, 0.0, 0.0] | [827411.0] | 0 |
... | ... | ... | ... | ... |
95 | 2023-07-14 15:49:47.621 | [2.0, 1.5, 1070.0, 1236.0, 2.0, 0.0, 0.0, 3.0, 8.0, 1000.0, 70.0, 47.5619, -122.382, 1170.0, 1888.0, 10.0, 0.0, 0.0] | [435628.56] | 0 |
96 | 2023-07-14 15:49:47.621 | [3.0, 2.5, 2830.0, 6000.0, 1.0, 0.0, 3.0, 3.0, 9.0, 1730.0, 1100.0, 47.5751, -122.378, 2040.0, 5300.0, 60.0, 0.0, 0.0] | [981676.6] | 0 |
97 | 2023-07-14 15:49:47.621 | [4.0, 1.75, 1720.0, 8750.0, 1.0, 0.0, 0.0, 3.0, 7.0, 860.0, 860.0, 47.726, -122.21, 1790.0, 8750.0, 43.0, 0.0, 0.0] | [437177.84] | 0 |
98 | 2023-07-14 15:49:47.621 | [4.0, 2.25, 4470.0, 60373.0, 2.0, 0.0, 0.0, 3.0, 11.0, 4470.0, 0.0, 47.7289, -122.127, 3210.0, 40450.0, 26.0, 0.0, 0.0] | [1208638.0] | 0 |
99 | 2023-07-14 15:49:47.621 | [3.0, 1.0, 1150.0, 3000.0, 1.0, 0.0, 0.0, 5.0, 6.0, 1150.0, 0.0, 47.6867, -122.345, 1460.0, 3200.0, 108.0, 0.0, 0.0] | [448627.72] | 0 |
100 rows × 4 columns
Warning: There are more logs available. Please set a larger limit or request a file using export_logs.
‘Metadata Logs’
time | out.variable | metadata.pipeline_version | |
---|---|---|---|
0 | 2023-07-14 15:49:47.621 | [581003.0] | |
1 | 2023-07-14 15:49:47.621 | [706823.56] | |
2 | 2023-07-14 15:49:47.621 | [1060847.5] | |
3 | 2023-07-14 15:49:47.621 | [441960.38] | |
4 | 2023-07-14 15:49:47.621 | [827411.0] | |
... | ... | ... | ... |
95 | 2023-07-14 15:49:47.621 | [435628.56] | |
96 | 2023-07-14 15:49:47.621 | [981676.6] | |
97 | 2023-07-14 15:49:47.621 | [437177.84] | |
98 | 2023-07-14 15:49:47.621 | [1208638.0] | |
99 | 2023-07-14 15:49:47.621 | [448627.72] |
100 rows × 3 columns
'Logs restricted by date'
2
time | in.tensor | out.variable | check_failures | |
---|---|---|---|---|
0 | 2023-07-14 15:49:36.579 | [4.0, 2.5, 2900.0, 5505.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2900.0, 0.0, 47.6063, -122.02, 2970.0, 5251.0, 12.0, 0.0, 0.0] | [718013.7] | 0 |
1 | 2023-07-14 15:49:36.996 | [4.0, 3.0, 3710.0, 20000.0, 2.0, 0.0, 2.0, 5.0, 10.0, 2760.0, 950.0, 47.6696, -122.261, 3970.0, 20000.0, 79.0, 0.0, 0.0] | [1514079.4] | 0 |
'Arrow logs by limit'
Warning: There are more logs available. Please set a larger limit or request a file using export_logs.
pyarrow.Table
time: timestamp[ms]
in.tensor: list<item: float> not null
child 0, item: float
out.variable: list<inner: float not null> not null
child 0, inner: float not null
check_failures: int8
----
time: [[2023-07-14 15:49:47.621,2023-07-14 15:49:47.621,2023-07-14 15:49:47.621,2023-07-14 15:49:47.621,2023-07-14 15:49:47.621,...,2023-07-14 15:49:47.621,2023-07-14 15:49:47.621,2023-07-14 15:49:47.621,2023-07-14 15:49:47.621,2023-07-14 15:49:47.621]]
in.tensor: [[[3,2,2005,7000,1,...,1750,4500,34,0,0],[3,1.75,2910,37461,1,...,2520,18295,47,0,0],...,[4,2.25,4470,60373,2,...,3210,40450,26,0,0],[3,1,1150,3000,1,...,1460,3200,108,0,0]]]
out.variable: [[[581003],[706823.56],...,[1208638],[448627.72]]]
check_failures: [[0,0,0,0,0,...,0,0,0,0,0]]
Pipeline Limits
In a previous step we performed 10,000 inferences at once. If we attempt to pull them at once, we’ll likely run into the size limit for this pipeline and receive the following warning message indicating that the pipeline size limits were exceeded and we should use export_logs
instead.
Warning: Pipeline log size limit exceeded. Only displaying 1000 log messages (of 10000 requested). Please request a file using export_logs.
logs = mainpipeline.logs(limit=10000)
display(logs)
Warning: Pipeline log size limit exceeded. Please request logs using export_logs
time | in.tensor | out.variable | check_failures | |
---|---|---|---|---|
0 | 2023-07-14 15:49:47.621 | [3.0, 2.0, 2005.0, 7000.0, 1.0, 0.0, 0.0, 3.0, 7.0, 1605.0, 400.0, 47.6039, -122.298, 1750.0, 4500.0, 34.0, 0.0, 0.0] | [581003.0] | 0 |
1 | 2023-07-14 15:49:47.621 | [3.0, 1.75, 2910.0, 37461.0, 1.0, 0.0, 0.0, 4.0, 7.0, 1530.0, 1380.0, 47.7015, -122.164, 2520.0, 18295.0, 47.0, 0.0, 0.0] | [706823.56] | 0 |
2 | 2023-07-14 15:49:47.621 | [4.0, 3.25, 2910.0, 1880.0, 2.0, 0.0, 3.0, 5.0, 9.0, 1830.0, 1080.0, 47.616, -122.282, 3100.0, 8200.0, 100.0, 0.0, 0.0] | [1060847.5] | 0 |
3 | 2023-07-14 15:49:47.621 | [4.0, 1.75, 2700.0, 7875.0, 1.5, 0.0, 0.0, 4.0, 8.0, 2700.0, 0.0, 47.454, -122.144, 2220.0, 7875.0, 46.0, 0.0, 0.0] | [441960.38] | 0 |
4 | 2023-07-14 15:49:47.621 | [3.0, 2.5, 2900.0, 23550.0, 1.0, 0.0, 0.0, 3.0, 10.0, 1490.0, 1410.0, 47.5708, -122.153, 2900.0, 19604.0, 27.0, 0.0, 0.0] | [827411.0] | 0 |
... | ... | ... | ... | ... |
661 | 2023-07-14 15:49:47.621 | [5.0, 3.25, 3160.0, 10587.0, 1.0, 0.0, 0.0, 5.0, 7.0, 2190.0, 970.0, 47.7238, -122.165, 2200.0, 7761.0, 55.0, 0.0, 0.0] | [573403.1] | 0 |
662 | 2023-07-14 15:49:47.621 | [3.0, 2.5, 2210.0, 7620.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2210.0, 0.0, 47.6938, -122.13, 1920.0, 7440.0, 20.0, 0.0, 0.0] | [677870.9] | 0 |
663 | 2023-07-14 15:49:47.621 | [3.0, 1.75, 1960.0, 8136.0, 1.0, 0.0, 0.0, 3.0, 7.0, 980.0, 980.0, 47.5208, -122.364, 1070.0, 7480.0, 66.0, 0.0, 0.0] | [365436.25] | 0 |
664 | 2023-07-14 15:49:47.621 | [3.0, 2.0, 1260.0, 8092.0, 1.0, 0.0, 0.0, 3.0, 7.0, 1260.0, 0.0, 47.3635, -122.054, 1950.0, 8092.0, 28.0, 0.0, 0.0] | [253958.75] | 0 |
665 | 2023-07-14 15:49:47.621 | [4.0, 2.5, 2650.0, 18295.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2650.0, 0.0, 47.6075, -122.154, 2230.0, 19856.0, 28.0, 0.0, 0.0] | [706407.4] | 0 |
666 rows × 4 columns
Pipeline export_logs Method
The Pipeline method export_logs
returns the Pipeline records as either a DataFrame JSON file, or an Apache Arrow table file. For a complete list, see the Wallaroo SDK Essentials Guide: Pipeline Log Management.
The export_logs
method takes the following parameters:
Parameter | Type | Description |
---|---|---|
directory | String (Optional) (Default: logs ) | Logs are exported to a file from current working directory to directory . |
data_size_limit | String (Optional) ((Default: 100MB ) | The maximum size for the exported data in bytes. Note that file size is approximate to the request; a request of 10MiB may return 10.3MB of data. The fields are in the format “{size as number} {unit value}”, and can include a space so “10 MiB” and “10MiB” are the same. The accepted unit values are:
|
file_prefix | String (Optional) (Default: The name of the pipeline) | The name of the exported files. By default, this will the name of the pipeline and is segmented by pipeline version between the limits or the start and end period. For example: ’logpipeline-1.json`, etc. |
limit | Int (Optional) | Limits how many log records to display. Defaults to 100 . If there are more pipeline logs than are being displayed, the Warning message Pipeline log record limit exceeded will be displayed. For example, if 100 log files were requested and there are a total of 1,000, the warning message will be displayed. |
start and end | DateTime (Optional) | Limits logs to all logs between the start and end DateTime parameters. Both parameters must be provided. Submitting a logs() request with only start or end will generate an exception.If start and end are provided as parameters, then the records are returned in chronological order, with the oldest record displayed first. |
dataset | List (OPTIONAL) | The datasets to be returned. The datasets available are:
metadata.elapsed : IMPORTANT NOTE: See Metadata Requests Restrictionsfor specifications on how this dataset can be used with other datasets.
|
arrow | Boolean (Optional) | Defaults to False. If arrow is set to True , then the logs are returned as an Apache Arrow table. If arrow=False , then the logs are returned as JSON in pandas DataFrame format. |
The following examples demonstrate saving a DataFrame version of the mainpipeline
logs, then an Arrow version.
# Save the DataFrame version of the log file
mainpipeline.export_logs()
display(os.listdir('./logs'))
mainpipeline.export_logs(arrow=True)
display(os.listdir('./logs'))
Warning: There are more logs available. Please set a larger limit to export more data.
[’logpipeline-1.json']
Warning: There are more logs available. Please set a larger limit to export more data.
[’logpipeline-1.arrow’, ’logpipeline-1.json’]
Shadow Deploy Pipelines
Let’s assume that after analyzing the assay information we want to test two challenger models to our control. We do that with the Shadow Deploy pipeline step.
In Shadow Deploy, the pipeline step is added with the add_shadow_deploy
method, with the champion model listed first, then an array of challenger models after. All inference data is fed to all models, with the champion results displayed in the out.variable
column, and the shadow results in the format out_{model name}.variable
. For example, since we named our challenger models housingchallenger01
and housingchallenger02
, the columns out_housingchallenger01.variable
and out_housingchallenger02.variable
have the shadow deployed model results.
For this example, we will remove the previous pipeline step, then replace it with a shadow deploy step with rf_model.onnx
as our champion, and models xgb_model.onnx
and gbr_model.onnx
as the challengers. We’ll deploy the pipeline and prepare it for sample inferences.
# Upload the challenger models
model_name_challenger01 = 'logcontrolchallenger01'
model_file_name_challenger01 = './models/xgb_model.onnx'
model_name_challenger02 = 'logcontrolchallenger02'
model_file_name_challenger02 = './models/gbr_model.onnx'
housing_model_challenger01 = wl.upload_model(model_name_challenger01, model_file_name_challenger01, framework=wallaroo.framework.Framework.ONNX).configure()
housing_model_challenger02 = wl.upload_model(model_name_challenger02, model_file_name_challenger02, framework=wallaroo.framework.Framework.ONNX).configure()
# Undeploy the pipeline
mainpipeline.undeploy()
mainpipeline.clear()
# Add the new shadow deploy step with our challenger models
mainpipeline.add_shadow_deploy(housing_model_control, [housing_model_challenger01, housing_model_challenger02])
# Deploy the pipeline with the new shadow step
mainpipeline.deploy()
name | logpipeline |
---|---|
created | 2023-07-14 15:49:23.959261+00:00 |
last_updated | 2023-07-14 15:50:33.900128+00:00 |
deployed | True |
tags | |
versions | f5b3e05b-297d-44a0-8645-86897ded3031, 48ec856b-9640-4a04-83f6-77a9bd205a44, 8a9d1d69-d71d-4c0a-9c95-99b3f86dcbc7 |
steps | logcontrol |
Shadow Deploy Sample Inference
We’ll now use our same sample data for an inference to our shadow deployed pipeline, then display the first 20 results with just the comparative outputs.
shadow_date_start = datetime.datetime.now()
shadow_result = mainpipeline.infer_from_file('./data/xtest-1k.arrow')
shadow_outputs = shadow_result.to_pandas()
display(shadow_outputs.loc[0:20,['out.variable','out_logcontrolchallenger01.variable','out_logcontrolchallenger02.variable']])
shadow_date_end = datetime.datetime.now()
out.variable | out_logcontrolchallenger01.variable | out_logcontrolchallenger02.variable | |
---|---|---|---|
0 | [718013.75] | [659806.0] | [704901.9] |
1 | [615094.56] | [732883.5] | [695994.44] |
2 | [448627.72] | [419508.84] | [416164.8] |
3 | [758714.2] | [634028.8] | [655277.2] |
4 | [513264.7] | [427209.44] | [426854.66] |
5 | [668288.0] | [615501.9] | [632556.1] |
6 | [1004846.5] | [1139732.5] | [1100465.2] |
7 | [684577.2] | [498328.88] | [528278.06] |
8 | [727898.1] | [722664.4] | [659439.94] |
9 | [559631.1] | [525746.44] | [534331.44] |
10 | [340764.53] | [376337.1] | [377187.2] |
11 | [442168.06] | [382053.12] | [403964.3] |
12 | [630865.6] | [505608.97] | [528991.3] |
13 | [559631.1] | [603260.5] | [612201.75] |
14 | [909441.1] | [969585.4] | [893874.7] |
15 | [313096.0] | [313633.75] | [318054.94] |
16 | [404040.8] | [360413.56] | [357816.75] |
17 | [292859.5] | [316674.94] | [294034.7] |
18 | [338357.88] | [299907.44] | [323254.3] |
19 | [682284.6] | [811896.75] | [770916.7] |
20 | [583765.94] | [573618.5] | [549141.4] |
Shadow Deploy Logs
Pipelines with a shadow deployed step include the shadow inference result in the same format as the inference result: inference results from shadow deployed models are displayed as out_{model name}.{output variable}
.
# display logs with shadow deployed steps
display(mainpipeline.logs(start_datetime=shadow_date_start, end_datetime=shadow_date_end).loc[:, ["time", "out.variable", "out_logcontrolchallenger01.variable", "out_logcontrolchallenger02.variable"]])
Warning: Pipeline log size limit exceeded. Please request logs using export_logs
time | out.variable | out_logcontrolchallenger01.variable | out_logcontrolchallenger02.variable | |
---|---|---|---|---|
0 | 2023-07-14 15:50:45.925 | [718013.75] | [659806.0] | [704901.9] |
1 | 2023-07-14 15:50:45.925 | [615094.56] | [732883.5] | [695994.44] |
2 | 2023-07-14 15:50:45.925 | [448627.72] | [419508.84] | [416164.8] |
3 | 2023-07-14 15:50:45.925 | [758714.2] | [634028.8] | [655277.2] |
4 | 2023-07-14 15:50:45.925 | [513264.7] | [427209.44] | [426854.66] |
... | ... | ... | ... | ... |
663 | 2023-07-14 15:50:45.925 | [642519.75] | [390891.06] | [481425.8] |
664 | 2023-07-14 15:50:45.925 | [301714.75] | [406503.62] | [374509.53] |
665 | 2023-07-14 15:50:45.925 | [448627.72] | [473771.0] | [478128.03] |
666 | 2023-07-14 15:50:45.925 | [544392.1] | [428174.9] | [442408.25] |
667 | 2023-07-14 15:50:45.925 | [944006.75] | [902058.6] | [866622.25] |
668 rows × 4 columns
# Save shadow deployed log files as pandas DataFrame
mainpipeline.export_logs(directory="shadow", file_prefix="shadowdeploylogs")
display(os.listdir('./shadow'))
Warning: There are more logs available. Please set a larger limit to export more data.
[‘shadowdeploylogs-1.json’]
A/B Testing Pipeline
A/B testing allows inference requests to be split between a control model and one or more challenger models. For full details, see the Pipeline Management Guide: A/B Testing.
When the inference results and log entries are displayed, they include the column out._model_split
which displays:
Field | Type | Description |
---|---|---|
name | String | The model name used for the inference. |
version | String | The version of the model. |
sha | String | The sha hash of the model version. |
For this example, the shadow deployed step will be removed and replaced with an A/B Testing step with the ratio 1:1:1, so the control and each of the challenger models will be split randomly between inference requests. A set of sample inferences will be run, then the pipeline logs displayed.
pipeline = (wl.build_pipeline(“randomsplitpipeline-demo”)
.add_random_split([(2, control), (1, challenger)], “session_id”))
mainpipeline.undeploy()
# remove the shadow deploy steps
mainpipeline.clear()
# Add the a/b test step to the pipeline
mainpipeline.add_random_split([(1, housing_model_control), (1, housing_model_challenger01), (1, housing_model_challenger02)], "session_id")
mainpipeline.deploy()
# Perform sample inferences of 20 rows and display the results
ab_date_start = datetime.datetime.now()
abtesting_inputs = pd.read_json('./data/xtest-1k.df.json')
for index, row in abtesting_inputs.sample(20).iterrows():
display(mainpipeline.infer(row.to_frame('tensor').reset_index()).loc[:,["out._model_split", "out.variable"]])
ab_date_end = datetime.datetime.now()
out._model_split | out.variable | |
---|---|---|
0 | [{"name":"logcontrolchallenger01","version":"87fc1e4e-308a-4e15-824f-dd9e237d00e8","sha":"31e92d6ccb27b041a324a7ac22cf95d9d6cc3aa7e8263a229f7c4aec4938657c"}] | [703914.5] |
out._model_split | out.variable | |
---|---|---|
0 | [{"name":"logcontrol","version":"9d3e0500-0272-4337-863b-539657d74aaa","sha":"e22a0831aafd9917f3cc87a15ed267797f80e2afa12ad7d8810ca58f173b8cc6"}] | [1108000.0] |
out._model_split | out.variable | |
---|---|---|
0 | [{"name":"logcontrol","version":"9d3e0500-0272-4337-863b-539657d74aaa","sha":"e22a0831aafd9917f3cc87a15ed267797f80e2afa12ad7d8810ca58f173b8cc6"}] | [296411.7] |
out._model_split | out.variable | |
---|---|---|
0 | [{"name":"logcontrolchallenger01","version":"87fc1e4e-308a-4e15-824f-dd9e237d00e8","sha":"31e92d6ccb27b041a324a7ac22cf95d9d6cc3aa7e8263a229f7c4aec4938657c"}] | [1596398.5] |
out._model_split | out.variable | |
---|---|---|
0 | [{"name":"logcontrolchallenger02","version":"e4c298b0-fbf5-42e1-b352-4f752935830e","sha":"ed6065a79d841f7e96307bb20d5ef22840f15da0b587efb51425c7ad60589d6a"}] | [612753.3] |
out._model_split | out.variable | |
---|---|---|
0 | [{"name":"logcontrolchallenger02","version":"e4c298b0-fbf5-42e1-b352-4f752935830e","sha":"ed6065a79d841f7e96307bb20d5ef22840f15da0b587efb51425c7ad60589d6a"}] | [1066417.5] |
out._model_split | out.variable | |
---|---|---|
0 | [{"name":"logcontrolchallenger02","version":"e4c298b0-fbf5-42e1-b352-4f752935830e","sha":"ed6065a79d841f7e96307bb20d5ef22840f15da0b587efb51425c7ad60589d6a"}] | [933591.5] |
out._model_split | out.variable | |
---|---|---|
0 | [{"name":"logcontrol","version":"9d3e0500-0272-4337-863b-539657d74aaa","sha":"e22a0831aafd9917f3cc87a15ed267797f80e2afa12ad7d8810ca58f173b8cc6"}] | [342604.47] |
out._model_split | out.variable | |
---|---|---|
0 | [{"name":"logcontrolchallenger02","version":"e4c298b0-fbf5-42e1-b352-4f752935830e","sha":"ed6065a79d841f7e96307bb20d5ef22840f15da0b587efb51425c7ad60589d6a"}] | [514748.5] |
out._model_split | out.variable | |
---|---|---|
0 | [{"name":"logcontrolchallenger02","version":"e4c298b0-fbf5-42e1-b352-4f752935830e","sha":"ed6065a79d841f7e96307bb20d5ef22840f15da0b587efb51425c7ad60589d6a"}] | [244174.22] |
out._model_split | out.variable | |
---|---|---|
0 | [{"name":"logcontrol","version":"9d3e0500-0272-4337-863b-539657d74aaa","sha":"e22a0831aafd9917f3cc87a15ed267797f80e2afa12ad7d8810ca58f173b8cc6"}] | [701940.7] |
out._model_split | out.variable | |
---|---|---|
0 | [{"name":"logcontrolchallenger01","version":"87fc1e4e-308a-4e15-824f-dd9e237d00e8","sha":"31e92d6ccb27b041a324a7ac22cf95d9d6cc3aa7e8263a229f7c4aec4938657c"}] | [310098.3] |
out._model_split | out.variable | |
---|---|---|
0 | [{"name":"logcontrol","version":"9d3e0500-0272-4337-863b-539657d74aaa","sha":"e22a0831aafd9917f3cc87a15ed267797f80e2afa12ad7d8810ca58f173b8cc6"}] | [277145.63] |
out._model_split | out.variable | |
---|---|---|
0 | [{"name":"logcontrolchallenger02","version":"e4c298b0-fbf5-42e1-b352-4f752935830e","sha":"ed6065a79d841f7e96307bb20d5ef22840f15da0b587efb51425c7ad60589d6a"}] | [348536.3] |
out._model_split | out.variable | |
---|---|---|
0 | [{"name":"logcontrol","version":"9d3e0500-0272-4337-863b-539657d74aaa","sha":"e22a0831aafd9917f3cc87a15ed267797f80e2afa12ad7d8810ca58f173b8cc6"}] | [509102.53] |
out._model_split | out.variable | |
---|---|---|
0 | [{"name":"logcontrolchallenger02","version":"e4c298b0-fbf5-42e1-b352-4f752935830e","sha":"ed6065a79d841f7e96307bb20d5ef22840f15da0b587efb51425c7ad60589d6a"}] | [445993.63] |
out._model_split | out.variable | |
---|---|---|
0 | [{"name":"logcontrolchallenger01","version":"87fc1e4e-308a-4e15-824f-dd9e237d00e8","sha":"31e92d6ccb27b041a324a7ac22cf95d9d6cc3aa7e8263a229f7c4aec4938657c"}] | [377534.8] |
out._model_split | out.variable | |
---|---|---|
0 | [{"name":"logcontrol","version":"9d3e0500-0272-4337-863b-539657d74aaa","sha":"e22a0831aafd9917f3cc87a15ed267797f80e2afa12ad7d8810ca58f173b8cc6"}] | [444141.88] |
out._model_split | out.variable | |
---|---|---|
0 | [{"name":"logcontrolchallenger01","version":"87fc1e4e-308a-4e15-824f-dd9e237d00e8","sha":"31e92d6ccb27b041a324a7ac22cf95d9d6cc3aa7e8263a229f7c4aec4938657c"}] | [270508.1] |
out._model_split | out.variable | |
---|---|---|
0 | [{"name":"logcontrol","version":"9d3e0500-0272-4337-863b-539657d74aaa","sha":"e22a0831aafd9917f3cc87a15ed267797f80e2afa12ad7d8810ca58f173b8cc6"}] | [245070.95] |
## Get the logs with the a/b testing information
display(mainpipeline.logs(start_datetime=ab_date_start, end_datetime=ab_date_end).loc[:, ["time", "out._model_split", "out.variable"]])
time | out._model_split | out.variable | |
---|---|---|---|
0 | 2023-07-14 15:51:42.630 | [{"name":"logcontrolchallenger01","version":"87fc1e4e-308a-4e15-824f-dd9e237d00e8","sha":"31e92d6ccb27b041a324a7ac22cf95d9d6cc3aa7e8263a229f7c4aec4938657c"}] | [703914.5] |
1 | 2023-07-14 15:51:43.024 | [{"name":"logcontrol","version":"9d3e0500-0272-4337-863b-539657d74aaa","sha":"e22a0831aafd9917f3cc87a15ed267797f80e2afa12ad7d8810ca58f173b8cc6"}] | [1108000.0] |
2 | 2023-07-14 15:51:43.426 | [{"name":"logcontrol","version":"9d3e0500-0272-4337-863b-539657d74aaa","sha":"e22a0831aafd9917f3cc87a15ed267797f80e2afa12ad7d8810ca58f173b8cc6"}] | [296411.7] |
3 | 2023-07-14 15:51:43.834 | [{"name":"logcontrolchallenger01","version":"87fc1e4e-308a-4e15-824f-dd9e237d00e8","sha":"31e92d6ccb27b041a324a7ac22cf95d9d6cc3aa7e8263a229f7c4aec4938657c"}] | [1596398.5] |
4 | 2023-07-14 15:51:44.224 | [{"name":"logcontrolchallenger02","version":"e4c298b0-fbf5-42e1-b352-4f752935830e","sha":"ed6065a79d841f7e96307bb20d5ef22840f15da0b587efb51425c7ad60589d6a"}] | [612753.3] |
5 | 2023-07-14 15:51:44.641 | [{"name":"logcontrolchallenger02","version":"e4c298b0-fbf5-42e1-b352-4f752935830e","sha":"ed6065a79d841f7e96307bb20d5ef22840f15da0b587efb51425c7ad60589d6a"}] | [1066417.5] |
6 | 2023-07-14 15:51:45.046 | [{"name":"logcontrolchallenger02","version":"e4c298b0-fbf5-42e1-b352-4f752935830e","sha":"ed6065a79d841f7e96307bb20d5ef22840f15da0b587efb51425c7ad60589d6a"}] | [933591.5] |
7 | 2023-07-14 15:51:45.453 | [{"name":"logcontrol","version":"9d3e0500-0272-4337-863b-539657d74aaa","sha":"e22a0831aafd9917f3cc87a15ed267797f80e2afa12ad7d8810ca58f173b8cc6"}] | [342604.47] |
8 | 2023-07-14 15:51:45.846 | [{"name":"logcontrolchallenger02","version":"e4c298b0-fbf5-42e1-b352-4f752935830e","sha":"ed6065a79d841f7e96307bb20d5ef22840f15da0b587efb51425c7ad60589d6a"}] | [514748.5] |
9 | 2023-07-14 15:51:46.235 | [{"name":"logcontrolchallenger02","version":"e4c298b0-fbf5-42e1-b352-4f752935830e","sha":"ed6065a79d841f7e96307bb20d5ef22840f15da0b587efb51425c7ad60589d6a"}] | [244174.22] |
10 | 2023-07-14 15:51:46.644 | [{"name":"logcontrol","version":"9d3e0500-0272-4337-863b-539657d74aaa","sha":"e22a0831aafd9917f3cc87a15ed267797f80e2afa12ad7d8810ca58f173b8cc6"}] | [701940.7] |
11 | 2023-07-14 15:51:47.504 | [{"name":"logcontrolchallenger01","version":"87fc1e4e-308a-4e15-824f-dd9e237d00e8","sha":"31e92d6ccb27b041a324a7ac22cf95d9d6cc3aa7e8263a229f7c4aec4938657c"}] | [310098.3] |
12 | 2023-07-14 15:51:47.944 | [{"name":"logcontrol","version":"9d3e0500-0272-4337-863b-539657d74aaa","sha":"e22a0831aafd9917f3cc87a15ed267797f80e2afa12ad7d8810ca58f173b8cc6"}] | [277145.62] |
13 | 2023-07-14 15:51:48.377 | [{"name":"logcontrolchallenger02","version":"e4c298b0-fbf5-42e1-b352-4f752935830e","sha":"ed6065a79d841f7e96307bb20d5ef22840f15da0b587efb51425c7ad60589d6a"}] | [348536.3] |
14 | 2023-07-14 15:51:48.783 | [{"name":"logcontrol","version":"9d3e0500-0272-4337-863b-539657d74aaa","sha":"e22a0831aafd9917f3cc87a15ed267797f80e2afa12ad7d8810ca58f173b8cc6"}] | [509102.53] |
15 | 2023-07-14 15:51:49.196 | [{"name":"logcontrolchallenger02","version":"e4c298b0-fbf5-42e1-b352-4f752935830e","sha":"ed6065a79d841f7e96307bb20d5ef22840f15da0b587efb51425c7ad60589d6a"}] | [445993.62] |
16 | 2023-07-14 15:51:49.618 | [{"name":"logcontrolchallenger01","version":"87fc1e4e-308a-4e15-824f-dd9e237d00e8","sha":"31e92d6ccb27b041a324a7ac22cf95d9d6cc3aa7e8263a229f7c4aec4938657c"}] | [377534.8] |
17 | 2023-07-14 15:51:50.096 | [{"name":"logcontrol","version":"9d3e0500-0272-4337-863b-539657d74aaa","sha":"e22a0831aafd9917f3cc87a15ed267797f80e2afa12ad7d8810ca58f173b8cc6"}] | [444141.88] |
18 | 2023-07-14 15:51:50.491 | [{"name":"logcontrolchallenger01","version":"87fc1e4e-308a-4e15-824f-dd9e237d00e8","sha":"31e92d6ccb27b041a324a7ac22cf95d9d6cc3aa7e8263a229f7c4aec4938657c"}] | [270508.1] |
# Save a/b testing log files as DataFrame
mainpipeline.export_logs(limit=1000,directory="abtesting", file_prefix="abtests")
display(os.listdir('./abtesting'))
Note: The logs with different schemas are written to separate files in the provided directory.
[‘abtests-3.json’, ‘abtests-2.json’, ‘abtests-1.json’]
Undeploy Main Pipeline
With the examples and tutorial complete, we will undeploy the main pipeline and return the resources back to the Wallaroo instance.
mainpipeline.undeploy()
name | logpipeline |
---|---|
created | 2023-07-14 15:49:23.959261+00:00 |
last_updated | 2023-07-14 15:51:27.112803+00:00 |
deployed | False |
tags | |
versions | 76a98987-721e-4ea7-8dc2-4380ad06d6a8, f5b3e05b-297d-44a0-8645-86897ded3031, 48ec856b-9640-4a04-83f6-77a9bd205a44, 8a9d1d69-d71d-4c0a-9c95-99b3f86dcbc7 |
steps | logcontrol |
4.6 - Pipeline Logs MLOps API Tutorial
This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.
Pipeline API Log Tutorial
This tutorial demonstrates Wallaroo Pipeline MLOps API for pipeline log retrieval.
This tutorial will demonstrate how to:
- Select or create a workspace, pipeline and upload the control model, and additional testing models.
- Add a pipeline step with the champion model, then deploy the pipeline and perform sample inferences.
- Retrieve the logs via the Wallaroo MLOps API. These steps will be simplified to only show the API log retrieval method. See the Wallaroo Documentation site for full details.
- Swap out the pipeline step with the champion model with a shadow deploy step that compares the champion model against two competitors.
- Perform sample inferences with a shadow deployed step, then display the log files through the MLOps API for a shadow deployed pipeline.
- Swap out the shadow deployed pipeline step with an A/B pipeline step.
- Perform sample inferences with a A/B pipeline step, then display the log files through the MLOps API for an A/B pipeline step.
- Undeploy the pipeline.
This tutorial provides the following:
- Models:
models/rf_model.onnx
: The champion model that has been used in this environment for some time.models/xgb_model.onnx
andmodels/gbr_model.onnx
: Rival models that will be tested against the champion.
- Data:
data/xtest-1.df.json
anddata/xtest-1k.df.json
: DataFrame JSON inference inputs with 1 input and 1,000 inputs.data/xtest-1k.arrow
: Apache Arrow inference inputs with 1 input and 1,000 inputs.
Prerequisites
- A deployed Wallaroo instance
- The following Python libraries installed:
Initial Steps
Import libraries
The first step is to import the libraries needed for this notebook.
import wallaroo
from wallaroo.object import EntityNotFoundError
import pyarrow as pa
from IPython.display import display
# used to display DataFrame information without truncating
from IPython.display import display
import pandas as pd
pd.set_option('display.max_colwidth', None)
import datetime
import requests
Connect to the Wallaroo Instance
The first step is to connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. If logging in externally, update the wallarooPrefix
and wallarooSuffix
variables with the proper DNS information. For more information on Wallaroo Client settings, see the Client Connection guide.
# Login through local Wallaroo instance
wl = wallaroo.Client()
Wallaroo MLOps API URL
API URL
The variable APIURL
is used to specify the connection to the Wallaroo instance’s MLOps API URL, and is composed of the Wallaroo DNS prefix and suffix. For full details, see the Wallaroo API Connection Guide
.
The variables wallarooPrefix.
and wallarooSuffix
variables will be used to derive the API url. For example, if the Wallaroo Prefix is doc-test.
and the url is example.com
, then the MLOps API URL would be doc-test.api.example.com/v1/api/{request}
. Note that the .
is part of the prefix; if there is no prefix, then wallarooPrefix = ""
.
Set the Wallaroo Prefix and Suffix in the code segment below based on your Wallaroo instance.
wallarooPrefix = "YOUR PREFIX."
wallarooSuffix = "YOUR SUFFIX"
APIURL = f"https://{wallarooPrefix}api.{wallarooSuffix}"
Create Workspace
We will create a workspace to manage our pipeline and models. The following variables will set the name of our sample workspace then set it as the current workspace.
workspace_name = 'logapiworkspace'
main_pipeline_name = 'logapipipeline'
model_name_control = 'logapicontrol'
model_file_name_control = './models/rf_model.onnx'
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
workspace_id = workspace.id()
Standard Pipeline
Upload The Champion Model
For our example, we will upload the champion model that has been trained to derive house prices from a variety of inputs. The model file is rf_model.onnx
, and is uploaded with the name housingcontrol
.
housing_model_control = wl.upload_model(model_name_control, model_file_name_control, framework=wallaroo.framework.Framework.ONNX).configure()
Build the Pipeline
This pipeline is made to be an example of an existing situation where a model is deployed and being used for inferences in a production environment. We’ll call it housepricepipeline
, set housingcontrol
as a pipeline step, then run a few sample inferences.
mainpipeline = wl.build_pipeline(main_pipeline_name)
mainpipeline.undeploy()
# in case this pipeline was run before
mainpipeline.clear()
mainpipeline.add_model_step(housing_model_control).deploy()
name | logapipipeline |
---|---|
created | 2023-07-14 15:43:25.566285+00:00 |
last_updated | 2023-07-14 15:43:29.948989+00:00 |
deployed | True |
tags | |
versions | 762a7f50-d1cc-4912-ab1d-5ed87b985797, 29b7109c-1467-40e1-aa11-dcb96959bb3e |
steps | logapicontrol |
Testing
We’ll pass in two DataFrame formatted inference requests which are returned as a pandas DataFrame. Then roughly 1,000 inferences as a batch as an Apache Arrow table, which is returned as an arrow table, which we’ll convert into a pandas DataFrame to display the first 20 results.
dataframe_start = datetime.datetime.now(datetime.timezone.utc)
normal_input = pd.DataFrame.from_records({"tensor": [[4.0, 2.5, 2900.0, 5505.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2900.0, 0.0, 47.6063, -122.02, 2970.0, 5251.0, 12.0, 0.0, 0.0]]})
result = mainpipeline.infer(normal_input)
display(result)
large_house_input = pd.DataFrame.from_records({'tensor': [[4.0, 3.0, 3710.0, 20000.0, 2.0, 0.0, 2.0, 5.0, 10.0, 2760.0, 950.0, 47.6696, -122.261, 3970.0, 20000.0, 79.0, 0.0, 0.0]]})
large_house_result = mainpipeline.infer(large_house_input)
display(large_house_result)
import time
time.sleep(10)
dataframe_end = datetime.datetime.now(datetime.timezone.utc)
# generating multiple log entries
batch_inferences = mainpipeline.infer_from_file('./data/xtest-1k.arrow')
batch_inferences = mainpipeline.infer_from_file('./data/xtest-1k.arrow')
batch_inferences = mainpipeline.infer_from_file('./data/xtest-1k.arrow')
large_inference_result = batch_inferences.to_pandas()
display(large_inference_result.head(20))
time | in.tensor | out.variable | check_failures | |
---|---|---|---|---|
0 | 2023-07-14 15:43:45.872 | [4.0, 2.5, 2900.0, 5505.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2900.0, 0.0, 47.6063, -122.02, 2970.0, 5251.0, 12.0, 0.0, 0.0] | [718013.7] | 0 |
time | in.tensor | out.variable | check_failures | |
---|---|---|---|---|
0 | 2023-07-14 15:43:46.983 | [4.0, 3.0, 3710.0, 20000.0, 2.0, 0.0, 2.0, 5.0, 10.0, 2760.0, 950.0, 47.6696, -122.261, 3970.0, 20000.0, 79.0, 0.0, 0.0] | [1514079.4] | 0 |
time | in.tensor | out.variable | check_failures | |
---|---|---|---|---|
0 | 2023-07-14 15:43:58.844 | [4.0, 2.5, 2900.0, 5505.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2900.0, 0.0, 47.6063, -122.02, 2970.0, 5251.0, 12.0, 0.0, 0.0] | [718013.75] | 0 |
1 | 2023-07-14 15:43:58.844 | [2.0, 2.5, 2170.0, 6361.0, 1.0, 0.0, 2.0, 3.0, 8.0, 2170.0, 0.0, 47.7109, -122.017, 2310.0, 7419.0, 6.0, 0.0, 0.0] | [615094.56] | 0 |
2 | 2023-07-14 15:43:58.844 | [3.0, 2.5, 1300.0, 812.0, 2.0, 0.0, 0.0, 3.0, 8.0, 880.0, 420.0, 47.5893, -122.317, 1300.0, 824.0, 6.0, 0.0, 0.0] | [448627.72] | 0 |
3 | 2023-07-14 15:43:58.844 | [4.0, 2.5, 2500.0, 8540.0, 2.0, 0.0, 0.0, 3.0, 9.0, 2500.0, 0.0, 47.5759, -121.994, 2560.0, 8475.0, 24.0, 0.0, 0.0] | [758714.2] | 0 |
4 | 2023-07-14 15:43:58.844 | [3.0, 1.75, 2200.0, 11520.0, 1.0, 0.0, 0.0, 4.0, 7.0, 2200.0, 0.0, 47.7659, -122.341, 1690.0, 8038.0, 62.0, 0.0, 0.0] | [513264.7] | 0 |
5 | 2023-07-14 15:43:58.844 | [3.0, 2.0, 2140.0, 4923.0, 1.0, 0.0, 0.0, 4.0, 8.0, 1070.0, 1070.0, 47.6902, -122.339, 1470.0, 4923.0, 86.0, 0.0, 0.0] | [668288.0] | 0 |
6 | 2023-07-14 15:43:58.844 | [4.0, 3.5, 3590.0, 5334.0, 2.0, 0.0, 2.0, 3.0, 9.0, 3140.0, 450.0, 47.6763, -122.267, 2100.0, 6250.0, 9.0, 0.0, 0.0] | [1004846.5] | 0 |
7 | 2023-07-14 15:43:58.844 | [3.0, 2.0, 1280.0, 960.0, 2.0, 0.0, 0.0, 3.0, 9.0, 1040.0, 240.0, 47.602, -122.311, 1280.0, 1173.0, 0.0, 0.0, 0.0] | [684577.2] | 0 |
8 | 2023-07-14 15:43:58.844 | [4.0, 2.5, 2820.0, 15000.0, 2.0, 0.0, 0.0, 4.0, 9.0, 2820.0, 0.0, 47.7255, -122.101, 2440.0, 15000.0, 29.0, 0.0, 0.0] | [727898.1] | 0 |
9 | 2023-07-14 15:43:58.844 | [3.0, 2.25, 1790.0, 11393.0, 1.0, 0.0, 0.0, 3.0, 8.0, 1790.0, 0.0, 47.6297, -122.099, 2290.0, 11894.0, 36.0, 0.0, 0.0] | [559631.1] | 0 |
10 | 2023-07-14 15:43:58.844 | [3.0, 1.5, 1010.0, 7683.0, 1.5, 0.0, 0.0, 5.0, 7.0, 1010.0, 0.0, 47.72, -122.318, 1550.0, 7271.0, 61.0, 0.0, 0.0] | [340764.53] | 0 |
11 | 2023-07-14 15:43:58.844 | [3.0, 2.0, 1270.0, 1323.0, 3.0, 0.0, 0.0, 3.0, 8.0, 1270.0, 0.0, 47.6934, -122.342, 1330.0, 1323.0, 8.0, 0.0, 0.0] | [442168.06] | 0 |
12 | 2023-07-14 15:43:58.844 | [4.0, 1.75, 2070.0, 9120.0, 1.0, 0.0, 0.0, 4.0, 7.0, 1250.0, 820.0, 47.6045, -122.123, 1650.0, 8400.0, 57.0, 0.0, 0.0] | [630865.6] | 0 |
13 | 2023-07-14 15:43:58.844 | [4.0, 1.0, 1620.0, 4080.0, 1.5, 0.0, 0.0, 3.0, 7.0, 1620.0, 0.0, 47.6696, -122.324, 1760.0, 4080.0, 91.0, 0.0, 0.0] | [559631.1] | 0 |
14 | 2023-07-14 15:43:58.844 | [4.0, 3.25, 3990.0, 9786.0, 2.0, 0.0, 0.0, 3.0, 9.0, 3990.0, 0.0, 47.6784, -122.026, 3920.0, 8200.0, 10.0, 0.0, 0.0] | [909441.1] | 0 |
15 | 2023-07-14 15:43:58.844 | [4.0, 2.0, 1780.0, 19843.0, 1.0, 0.0, 0.0, 3.0, 7.0, 1780.0, 0.0, 47.4414, -122.154, 2210.0, 13500.0, 52.0, 0.0, 0.0] | [313096.0] | 0 |
16 | 2023-07-14 15:43:58.844 | [4.0, 2.5, 2130.0, 6003.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2130.0, 0.0, 47.4518, -122.12, 1940.0, 4529.0, 11.0, 0.0, 0.0] | [404040.8] | 0 |
17 | 2023-07-14 15:43:58.844 | [3.0, 1.75, 1660.0, 10440.0, 1.0, 0.0, 0.0, 3.0, 7.0, 1040.0, 620.0, 47.4448, -121.77, 1240.0, 10380.0, 36.0, 0.0, 0.0] | [292859.5] | 0 |
18 | 2023-07-14 15:43:58.844 | [3.0, 2.5, 2110.0, 4118.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2110.0, 0.0, 47.3878, -122.153, 2110.0, 4044.0, 25.0, 0.0, 0.0] | [338357.88] | 0 |
19 | 2023-07-14 15:43:58.844 | [4.0, 2.25, 2200.0, 11250.0, 1.5, 0.0, 0.0, 5.0, 7.0, 1300.0, 900.0, 47.6845, -122.201, 2320.0, 10814.0, 94.0, 0.0, 0.0] | [682284.6] | 0 |
Standard Pipeline Logs
Pipeline logs are retrieved through the Wallaroo MLOps API with the following request.
- REQUEST URL
v1/api/pipelines/get_logs
- Headers
- Accept:
application/json; format=pandas-records
: For the logs returned as pandas DataFrameapplication/vnd.apache.arrow.file
: for the logs returned as Apache Arrow
- Accept:
- PARAMETERS
- pipeline_id (String Required): The name of the pipeline.
- workspace_id (Integer Required): The numerical identifier of the workspace.
- cursor (String Optional): Cursor returned with a previous page of results from a pipeline log request, used to retrieve the next page of information.
- order (String Optional Default:
Desc
): The order for log inserts returned. Valid values are:Asc
: In chronological order of inserts.Desc
: In reverse chronological order of inserts.
- page_size (Integer Optional Default:
1000
.): Max records per page. - start_time (String Optional): The start time of the period to retrieve logs for in RFC 3339 format for DateTime. Must be combined with
end_time
. - end_time (String Optional): The end time of the period to retrieve logs for in RFC 3339 format for DateTime. Must be combined with
start_time
.
- RETURNS
- The logs are returned by default as
'application/json; format=pandas-records'
format. To request the logs as Apache Arrow tables, set the submission headerAccept
toapplication/vnd.apache.arrow.file
. - Headers:
- x-iteration-cursor: Used to retrieve the next page of results. This is not included if
x-iteration-status
isAll
. - x-iteration-status: Informs whether there are more records available outside of this log request parameters.
- All: This page includes all logs available from this request. If
x-iteration-status
isAll
, thenx-iteration-cursor
is not provided. - SchemaChange: A change in the log schema caused by actions such as pipeline version, etc.
- RecordLimited: The number of records exceeded from the page size, more records can be requested as the next page. There may be more records available to retrieve OR the record limit was reached for this request even if no more records are available in next cursor request.
- ByteLimited: The number of records exceeded the pipeline log limit which is around 100K.
- All: This page includes all logs available from this request. If
- x-iteration-cursor: Used to retrieve the next page of results. This is not included if
- The logs are returned by default as
# retrieve the authorization token
headers = wl.auth.auth_header()
url = f"{APIURL}/v1/api/pipelines/get_logs"
# Standard log retrieval
data = {
'pipeline_id': main_pipeline_name,
'workspace_id': workspace_id
}
response = requests.post(url, headers=headers, json=data)
standard_logs = pd.DataFrame.from_records(response.json())
display(len(standard_logs))
display(standard_logs.head(5).loc[:, ["time", "in", "out"]])
cursor = response.headers['x-iteration-cursor']
2
time | in | out | |
---|---|---|---|
0 | 1689349425872 | {'tensor': [4.0, 2.5, 2900.0, 5505.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2900.0, 0.0, 47.6063, -122.02, 2970.0, 5251.0, 12.0, 0.0, 0.0]} | {'variable': [718013.7]} |
1 | 1689349426983 | {'tensor': [4.0, 3.0, 3710.0, 20000.0, 2.0, 0.0, 2.0, 5.0, 10.0, 2760.0, 950.0, 47.6696, -122.261, 3970.0, 20000.0, 79.0, 0.0, 0.0]} | {'variable': [1514079.4]} |
# Get next page of results as an arrow table
# retrieve the authorization token
headers = wl.auth.auth_header()
headers['Accept']="application/vnd.apache.arrow.file"
url = f"{APIURL}/v1/api/pipelines/get_logs"
# Standard log retrieval
data = {
'pipeline_id': main_pipeline_name,
'workspace_id': workspace_id,
'cursor': cursor
}
response = requests.post(url, headers=headers, json=data)
# Arrow table is retrieved
with pa.ipc.open_file(response.content) as reader:
arrow_table = reader.read_all()
# convert to Polars DataFrame and display the first 5 rows
display(arrow_table.to_pandas().head(5).loc[:,["time", "out"]])
time | out | |
---|---|---|
0 | 1689349437595 | {'variable': [718013.75]} |
1 | 1689349437595 | {'variable': [615094.56]} |
2 | 1689349437595 | {'variable': [448627.72]} |
3 | 1689349437595 | {'variable': [758714.2]} |
4 | 1689349437595 | {'variable': [513264.7]} |
# Retrieve logs from specific date/time to only get the two DataFrame input inferences in ascending format
# retrieve the authorization token
headers = wl.auth.auth_header()
url = f"{APIURL}/v1/api/pipelines/get_logs"
# Standard log retrieval
data = {
'pipeline_id': main_pipeline_name,
'workspace_id': workspace_id,
'order': 'Asc',
'start_time': f'{dataframe_start.isoformat()}',
'end_time': f'{dataframe_end.isoformat()}'
}
response = requests.post(url, headers=headers, json=data)
standard_logs = pd.DataFrame.from_records(response.json())
display(standard_logs.head(5).loc[:, ["time", "in", "out"]])
display(response.headers)
time | in | out | |
---|---|---|---|
0 | 1689349425872 | {'tensor': [4.0, 2.5, 2900.0, 5505.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2900.0, 0.0, 47.6063, -122.02, 2970.0, 5251.0, 12.0, 0.0, 0.0]} | {'variable': [718013.7]} |
1 | 1689349426983 | {'tensor': [4.0, 3.0, 3710.0, 20000.0, 2.0, 0.0, 2.0, 5.0, 10.0, 2760.0, 950.0, 47.6696, -122.261, 3970.0, 20000.0, 79.0, 0.0, 0.0]} | {'variable': [1514079.4]} |
{'content-type': 'application/json; format=pandas-records', 'x-iteration-status': 'All', 'content-length': '867', 'date': 'Fri, 14 Jul 2023 15:44:37 GMT', 'x-envoy-upstream-service-time': '2', 'server': 'envoy'}
Shadow Deploy Pipelines
Let’s assume that after analyzing the assay information we want to test two challenger models to our control. We do that with the Shadow Deploy pipeline step.
In Shadow Deploy, the pipeline step is added with the add_shadow_deploy
method, with the champion model listed first, then an array of challenger models after. All inference data is fed to all models, with the champion results displayed in the out.variable
column, and the shadow results in the format out_{model name}.variable
. For example, since we named our challenger models housingchallenger01
and housingchallenger02
, the columns out_housingchallenger01.variable
and out_housingchallenger02.variable
have the shadow deployed model results.
For this example, we will remove the previous pipeline step, then replace it with a shadow deploy step with rf_model.onnx
as our champion, and models xgb_model.onnx
and gbr_model.onnx
as the challengers. We’ll deploy the pipeline and prepare it for sample inferences.
# Upload the challenger models
model_name_challenger01 = 'logcontrolchallenger01'
model_file_name_challenger01 = './models/xgb_model.onnx'
model_name_challenger02 = 'logcontrolchallenger02'
model_file_name_challenger02 = './models/gbr_model.onnx'
housing_model_challenger01 = wl.upload_model(model_name_challenger01, model_file_name_challenger01, framework=wallaroo.framework.Framework.ONNX).configure()
housing_model_challenger02 = wl.upload_model(model_name_challenger02, model_file_name_challenger02, framework=wallaroo.framework.Framework.ONNX).configure()
# Undeploy the pipeline
mainpipeline.undeploy()
mainpipeline.clear()
# Add the new shadow deploy step with our challenger models
mainpipeline.add_shadow_deploy(housing_model_control, [housing_model_challenger01, housing_model_challenger02])
# Deploy the pipeline with the new shadow step
mainpipeline.deploy()
name | logapipipeline |
---|---|
created | 2023-07-14 15:43:25.566285+00:00 |
last_updated | 2023-07-14 15:45:23.038631+00:00 |
deployed | True |
tags | |
versions | f2022a9f-1b94-4e23-9d19-05577f3d7010, 762a7f50-d1cc-4912-ab1d-5ed87b985797, 29b7109c-1467-40e1-aa11-dcb96959bb3e |
steps | logapicontrol |
Shadow Deploy Sample Inference
We’ll now use our same sample data for an inference to our shadow deployed pipeline, then display the first 20 results with just the comparative outputs.
shadow_date_start = datetime.datetime.now(datetime.timezone.utc)
shadow_result = mainpipeline.infer_from_file('./data/xtest-1k.arrow')
shadow_outputs = shadow_result.to_pandas()
display(shadow_outputs.loc[0:20,['out.variable','out_logcontrolchallenger01.variable','out_logcontrolchallenger02.variable']])
shadow_date_end = datetime.datetime.now(datetime.timezone.utc)
out.variable | out_logcontrolchallenger01.variable | out_logcontrolchallenger02.variable | |
---|---|---|---|
0 | [718013.75] | [659806.0] | [704901.9] |
1 | [615094.56] | [732883.5] | [695994.44] |
2 | [448627.72] | [419508.84] | [416164.8] |
3 | [758714.2] | [634028.8] | [655277.2] |
4 | [513264.7] | [427209.44] | [426854.66] |
5 | [668288.0] | [615501.9] | [632556.1] |
6 | [1004846.5] | [1139732.5] | [1100465.2] |
7 | [684577.2] | [498328.88] | [528278.06] |
8 | [727898.1] | [722664.4] | [659439.94] |
9 | [559631.1] | [525746.44] | [534331.44] |
10 | [340764.53] | [376337.1] | [377187.2] |
11 | [442168.06] | [382053.12] | [403964.3] |
12 | [630865.6] | [505608.97] | [528991.3] |
13 | [559631.1] | [603260.5] | [612201.75] |
14 | [909441.1] | [969585.4] | [893874.7] |
15 | [313096.0] | [313633.75] | [318054.94] |
16 | [404040.8] | [360413.56] | [357816.75] |
17 | [292859.5] | [316674.94] | [294034.7] |
18 | [338357.88] | [299907.44] | [323254.3] |
19 | [682284.6] | [811896.75] | [770916.7] |
20 | [583765.94] | [573618.5] | [549141.4] |
Shadow Deploy Logs
Pipelines with a shadow deployed step include the shadow inference result in the same format as the inference result: inference results from shadow deployed models are displayed as out_{model name}.{output variable}
.
# Retrieve logs from specific date/time to only get the two DataFrame input inferences in ascending format
# retrieve the authorization token
headers = wl.auth.auth_header()
url = f"{APIURL}/v1/api/pipelines/get_logs"
# Standard log retrieval
data = {
'pipeline_id': main_pipeline_name,
'workspace_id': workspace_id,
'order': 'Asc',
'start_time': f'{shadow_date_start.isoformat()}',
'end_time': f'{shadow_date_end.isoformat()}'
}
response = requests.post(url, headers=headers, json=data)
standard_logs = pd.DataFrame.from_records(response.json())
display(standard_logs.head(5).loc[:, ["time", "out", "out_logcontrolchallenger01", "out_logcontrolchallenger02"]])
time | out | out_logcontrolchallenger01 | out_logcontrolchallenger02 | |
---|---|---|---|---|
0 | 1689349535135 | {'variable': [718013.75]} | {'variable': [659806.0]} | {'variable': [704901.9]} |
1 | 1689349535135 | {'variable': [615094.56]} | {'variable': [732883.5]} | {'variable': [695994.44]} |
2 | 1689349535135 | {'variable': [448627.72]} | {'variable': [419508.84]} | {'variable': [416164.8]} |
3 | 1689349535135 | {'variable': [758714.2]} | {'variable': [634028.8]} | {'variable': [655277.2]} |
4 | 1689349535135 | {'variable': [513264.7]} | {'variable': [427209.44]} | {'variable': [426854.66]} |
A/B Testing Pipeline
A/B testing allows inference requests to be split between a control model and one or more challenger models. For full details, see the Pipeline Management Guide: A/B Testing.
When the inference results and log entries are displayed, they include the column out._model_split
which displays:
Field | Type | Description |
---|---|---|
name | String | The model name used for the inference. |
version | String | The version of the model. |
sha | String | The sha hash of the model version. |
For this example, the shadow deployed step will be removed and replaced with an A/B Testing step with the ratio 1:1:1, so the control and each of the challenger models will be split randomly between inference requests. A set of sample inferences will be run, then the pipeline logs displayed.
pipeline = (wl.build_pipeline(“randomsplitpipeline-demo”)
.add_random_split([(2, control), (1, challenger)], “session_id”))
ab_date_start = datetime.datetime.now(datetime.timezone.utc)
mainpipeline.undeploy()
# remove the shadow deploy steps
mainpipeline.clear()
# Add the a/b test step to the pipeline
mainpipeline.add_random_split([(1, housing_model_control), (1, housing_model_challenger01), (1, housing_model_challenger02)], "session_id")
mainpipeline.deploy()
# Perform sample inferences of 20 rows and display the results
abtesting_inputs = pd.read_json('./data/xtest-1k.df.json')
for index, row in abtesting_inputs.sample(20).iterrows():
display(mainpipeline.infer(row.to_frame('tensor').reset_index()).loc[:,["out._model_split", "out.variable"]])
ab_date_end = datetime.datetime.now(datetime.timezone.utc)
out._model_split | out.variable | |
---|---|---|
0 | [{"name":"logapicontrol","version":"448634a1-6f2b-438c-98fa-68268f151462","sha":"e22a0831aafd9917f3cc87a15ed267797f80e2afa12ad7d8810ca58f173b8cc6"}] | [718013.7] |
out._model_split | out.variable | |
---|---|---|
0 | [{"name":"logcontrolchallenger01","version":"bde52213-3828-4fd7-b286-09d2149d8a10","sha":"31e92d6ccb27b041a324a7ac22cf95d9d6cc3aa7e8263a229f7c4aec4938657c"}] | [550902.5] |
out._model_split | out.variable | |
---|---|---|
0 | [{"name":"logcontrolchallenger02","version":"52fdf218-5e90-457a-a956-d07d741d6dae","sha":"ed6065a79d841f7e96307bb20d5ef22840f15da0b587efb51425c7ad60589d6a"}] | [329266.97] |
out._model_split | out.variable | |
---|---|---|
0 | [{"name":"logapicontrol","version":"448634a1-6f2b-438c-98fa-68268f151462","sha":"e22a0831aafd9917f3cc87a15ed267797f80e2afa12ad7d8810ca58f173b8cc6"}] | [450867.7] |
out._model_split | out.variable | |
---|---|---|
0 | [{"name":"logapicontrol","version":"448634a1-6f2b-438c-98fa-68268f151462","sha":"e22a0831aafd9917f3cc87a15ed267797f80e2afa12ad7d8810ca58f173b8cc6"}] | [499651.56] |
out._model_split | out.variable | |
---|---|---|
0 | [{"name":"logcontrolchallenger02","version":"52fdf218-5e90-457a-a956-d07d741d6dae","sha":"ed6065a79d841f7e96307bb20d5ef22840f15da0b587efb51425c7ad60589d6a"}] | [294921.5] |
out._model_split | out.variable | |
---|---|---|
0 | [{"name":"logcontrolchallenger01","version":"bde52213-3828-4fd7-b286-09d2149d8a10","sha":"31e92d6ccb27b041a324a7ac22cf95d9d6cc3aa7e8263a229f7c4aec4938657c"}] | [420434.13] |
out._model_split | out.variable | |
---|---|---|
0 | [{"name":"logapicontrol","version":"448634a1-6f2b-438c-98fa-68268f151462","sha":"e22a0831aafd9917f3cc87a15ed267797f80e2afa12ad7d8810ca58f173b8cc6"}] | [381737.6] |
out._model_split | out.variable | |
---|---|---|
0 | [{"name":"logcontrolchallenger01","version":"bde52213-3828-4fd7-b286-09d2149d8a10","sha":"31e92d6ccb27b041a324a7ac22cf95d9d6cc3aa7e8263a229f7c4aec4938657c"}] | [299659.7] |
out._model_split | out.variable | |
---|---|---|
0 | [{"name":"logcontrolchallenger02","version":"52fdf218-5e90-457a-a956-d07d741d6dae","sha":"ed6065a79d841f7e96307bb20d5ef22840f15da0b587efb51425c7ad60589d6a"}] | [349665.53] |
out._model_split | out.variable | |
---|---|---|
0 | [{"name":"logapicontrol","version":"448634a1-6f2b-438c-98fa-68268f151462","sha":"e22a0831aafd9917f3cc87a15ed267797f80e2afa12ad7d8810ca58f173b8cc6"}] | [293808.03] |
out._model_split | out.variable | |
---|---|---|
0 | [{"name":"logcontrolchallenger01","version":"bde52213-3828-4fd7-b286-09d2149d8a10","sha":"31e92d6ccb27b041a324a7ac22cf95d9d6cc3aa7e8263a229f7c4aec4938657c"}] | [186544.78] |
out._model_split | out.variable | |
---|---|---|
0 | [{"name":"logapicontrol","version":"448634a1-6f2b-438c-98fa-68268f151462","sha":"e22a0831aafd9917f3cc87a15ed267797f80e2afa12ad7d8810ca58f173b8cc6"}] | [294203.53] |
out._model_split | out.variable | |
---|---|---|
0 | [{"name":"logapicontrol","version":"448634a1-6f2b-438c-98fa-68268f151462","sha":"e22a0831aafd9917f3cc87a15ed267797f80e2afa12ad7d8810ca58f173b8cc6"}] | [289359.47] |
out._model_split | out.variable | |
---|---|---|
0 | [{"name":"logcontrolchallenger01","version":"bde52213-3828-4fd7-b286-09d2149d8a10","sha":"31e92d6ccb27b041a324a7ac22cf95d9d6cc3aa7e8263a229f7c4aec4938657c"}] | [589324.8] |
out._model_split | out.variable | |
---|---|---|
0 | [{"name":"logapicontrol","version":"448634a1-6f2b-438c-98fa-68268f151462","sha":"e22a0831aafd9917f3cc87a15ed267797f80e2afa12ad7d8810ca58f173b8cc6"}] | [271309.13] |
out._model_split | out.variable | |
---|---|---|
0 | [{"name":"logapicontrol","version":"448634a1-6f2b-438c-98fa-68268f151462","sha":"e22a0831aafd9917f3cc87a15ed267797f80e2afa12ad7d8810ca58f173b8cc6"}] | [465299.9] |
out._model_split | out.variable | |
---|---|---|
0 | [{"name":"logapicontrol","version":"448634a1-6f2b-438c-98fa-68268f151462","sha":"e22a0831aafd9917f3cc87a15ed267797f80e2afa12ad7d8810ca58f173b8cc6"}] | [247792.75] |
out._model_split | out.variable | |
---|---|---|
0 | [{"name":"logcontrolchallenger01","version":"bde52213-3828-4fd7-b286-09d2149d8a10","sha":"31e92d6ccb27b041a324a7ac22cf95d9d6cc3aa7e8263a229f7c4aec4938657c"}] | [413473.8] |
out._model_split | out.variable | |
---|---|---|
0 | [{"name":"logcontrolchallenger01","version":"bde52213-3828-4fd7-b286-09d2149d8a10","sha":"31e92d6ccb27b041a324a7ac22cf95d9d6cc3aa7e8263a229f7c4aec4938657c"}] | [442778.22] |
Retrieve A/B Testing Log Files through API
The log files for A/B Testing pipeline inference results contain the model information with the model outputs in the out
field.
# Retrieve logs from specific date/time to only get the two DataFrame input inferences in ascending format
# retrieve the authorization token
headers = wl.auth.auth_header()
url = f"{APIURL}/v1/api/pipelines/get_logs"
# Standard log retrieval
data = {
'pipeline_id': main_pipeline_name,
'workspace_id': workspace_id,
'order': 'Asc',
'start_time': f'{ab_date_start.isoformat()}',
'end_time': f'{ab_date_end.isoformat()}'
}
response = requests.post(url, headers=headers, json=data)
standard_logs = pd.DataFrame.from_records(response.json())
display(standard_logs.head(5).loc[:, ["time", "out"]])
time | out | |
---|---|---|
0 | 1689349586459 | {'_model_split': ['{"name":"logapicontrol","version":"448634a1-6f2b-438c-98fa-68268f151462","sha":"e22a0831aafd9917f3cc87a15ed267797f80e2afa12ad7d8810ca58f173b8cc6"}'], 'variable': [718013.7]} |
1 | 1689349586894 | {'_model_split': ['{"name":"logcontrolchallenger01","version":"bde52213-3828-4fd7-b286-09d2149d8a10","sha":"31e92d6ccb27b041a324a7ac22cf95d9d6cc3aa7e8263a229f7c4aec4938657c"}'], 'variable': [550902.5]} |
2 | 1689349587285 | {'_model_split': ['{"name":"logcontrolchallenger02","version":"52fdf218-5e90-457a-a956-d07d741d6dae","sha":"ed6065a79d841f7e96307bb20d5ef22840f15da0b587efb51425c7ad60589d6a"}'], 'variable': [329266.97]} |
3 | 1689349587672 | {'_model_split': ['{"name":"logapicontrol","version":"448634a1-6f2b-438c-98fa-68268f151462","sha":"e22a0831aafd9917f3cc87a15ed267797f80e2afa12ad7d8810ca58f173b8cc6"}'], 'variable': [450867.7]} |
4 | 1689349588092 | {'_model_split': ['{"name":"logapicontrol","version":"448634a1-6f2b-438c-98fa-68268f151462","sha":"e22a0831aafd9917f3cc87a15ed267797f80e2afa12ad7d8810ca58f173b8cc6"}'], 'variable': [499651.56]} |
Undeploy Main Pipeline
With the examples and tutorial complete, we will undeploy the main pipeline and return the resources back to the Wallaroo instance.
mainpipeline.undeploy()
name | logapipipeline |
---|---|
created | 2023-07-14 15:43:25.566285+00:00 |
last_updated | 2023-07-14 15:46:15.685023+00:00 |
deployed | False |
tags | |
versions | 43cbb475-5eaf-4aaf-a6b1-63edc77f44a8, f2022a9f-1b94-4e23-9d19-05577f3d7010, 762a7f50-d1cc-4912-ab1d-5ed87b985797, 29b7109c-1467-40e1-aa11-dcb96959bb3e |
steps | logapicontrol |
4.7 - Statsmodel Forecast with Wallaroo Features
This tutorial series demonstrates how to use Wallaroo to create a Statsmodel forecasting model based on bike rentals. This tutorial series is broken down into the following:
- Create and Train the Model: This first notebook shows how the model is trained from existing data.
- Deploy and Sample Inference: With the model developed, we will deploy it into Wallaroo and perform a sample inference.
- Parallel Infer: A sample of multiple weeks of data will be retrieved and submitted as an asynchronous parallel inference. The results will be collected and uploaded to a sample database.
- External Connection: A sample data connection to Google BigQuery to retrieve input data and store the results in a table.
- ML Workload Orchestration: Take all of the previous steps and automate the request into a single Wallaroo ML Workload Orchestration.
4.7.1 - Statsmodel Forecast with Wallaroo Features: Model Creation
This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.
Statsmodel Forecast with Wallaroo Features: Model Creation
This tutorial series demonstrates how to use Wallaroo to create a Statsmodel forecasting model based on bike rentals. This tutorial series is broken down into the following:
- Create and Train the Model: This first notebook shows how the model is trained from existing data.
- Deploy and Sample Inference: With the model developed, we will deploy it into Wallaroo and perform a sample inference.
- Parallel Infer: A sample of multiple weeks of data will be retrieved and submitted as an asynchronous parallel inference. The results will be collected and uploaded to a sample database.
- External Connection: A sample data connection to Google BigQuery to retrieve input data and store the results in a table.
- ML Workload Orchestration: Take all of the previous steps and automate the request into a single Wallaroo ML Workload Orchestration.
Prerequisites
- A Wallaroo instance version 2023.2.1 or greater.
References
- Wallaroo SDK Essentials Guide: Model Uploads and Registrations: Python Models
- Wallaroo SDK Essentials Guide: Pipeline Management
- Wallaroo SDK Essentials: Inference Guide: Parallel Inferences
import pandas as pd
import datetime
import os
from statsmodels.tsa.arima.model import ARIMA
from resources import simdb as simdb
Train the Model
The resources to train the model will start with the local file day.csv
. This data is load and prepared for use in training the model.
For this example, the simulated database is controled by the resources simbdb
.
def mk_dt_range_query(*, tablename: str, seed_day: str) -> str:
assert isinstance(tablename, str)
assert isinstance(seed_day, str)
query = f"select cnt from {tablename} where date > DATE(DATE('{seed_day}'), '-1 month') AND date <= DATE('{seed_day}')"
return query
conn = simdb.get_db_connection()
# create the query
query = mk_dt_range_query(tablename=simdb.tablename, seed_day='2011-03-01')
print(query)
# read in the data
training_frame = pd.read_sql_query(query, conn)
training_frame
select cnt from bikerentals where date > DATE(DATE('2011-03-01'), '-1 month') AND date <= DATE('2011-03-01')
cnt | |
---|---|
0 | 1526 |
1 | 1550 |
2 | 1708 |
3 | 1005 |
4 | 1623 |
5 | 1712 |
6 | 1530 |
7 | 1605 |
8 | 1538 |
9 | 1746 |
10 | 1472 |
11 | 1589 |
12 | 1913 |
13 | 1815 |
14 | 2115 |
15 | 2475 |
16 | 2927 |
17 | 1635 |
18 | 1812 |
19 | 1107 |
20 | 1450 |
21 | 1917 |
22 | 1807 |
23 | 1461 |
24 | 1969 |
25 | 2402 |
26 | 1446 |
27 | 1851 |
Test the Forecast
The training frame is then loaded, and tested against our forecast
model.
# test
import forecast
import json
# create the appropriate json
jsonstr = json.dumps(training_frame.to_dict(orient='list'))
print(jsonstr)
forecast.wallaroo_json(jsonstr)
{"cnt": [1526, 1550, 1708, 1005, 1623, 1712, 1530, 1605, 1538, 1746, 1472, 1589, 1913, 1815, 2115, 2475, 2927, 1635, 1812, 1107, 1450, 1917, 1807, 1461, 1969, 2402, 1446, 1851]}
{‘forecast’: [1764, 1749, 1743, 1741, 1740, 1740, 1740]}
Reload New Model
The forecast
model is reloaded in preparation of creating the evaluation data.
import importlib
importlib.reload(forecast)
<module 'forecast' from '/home/jovyan/pipeline_multiple_replicas_forecast_tutorial/forecast.py'>
Prepare evaluation data
For ease of inference, we save off the evaluation data to a separate json file.
# save off the evaluation frame json, too
import json
with open("./data/testdata_dict.json", "w") as f:
json.dump(training_frame.to_dict(orient='list'), f)
4.7.2 - Statsmodel Forecast with Wallaroo Features: Deploy and Test Infer
This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.
Statsmodel Forecast with Wallaroo Features: Deploy and Test Infer
This tutorial series demonstrates how to use Wallaroo to create a Statsmodel forecasting model based on bike rentals. This tutorial series is broken down into the following:
- Create and Train the Model: This first notebook shows how the model is trained from existing data.
- Deploy and Sample Inference: With the model developed, we will deploy it into Wallaroo and perform a sample inference.
- Parallel Infer: A sample of multiple weeks of data will be retrieved and submitted as an asynchronous parallel inference. The results will be collected and uploaded to a sample database.
- External Connection: A sample data connection to Google BigQuery to retrieve input data and store the results in a table.
- ML Workload Orchestration: Take all of the previous steps and automate the request into a single Wallaroo ML Workload Orchestration.
In the previous step “Statsmodel Forecast with Wallaroo Features: Model Creation”, the statsmodel was trained and saved to the Python file forecast.py
. This file will now be uploaded to a Wallaroo instance as a Python model, then used for sample inferences.
Prerequisites
- A Wallaroo instance version 2023.2.1 or greater.
References
- Wallaroo SDK Essentials Guide: Model Uploads and Registrations: Python Models
- Wallaroo SDK Essentials Guide: Pipeline Management
- Wallaroo SDK Essentials: Inference Guide: Parallel Inferences
Tutorial Steps
Import Libraries
The first step is to import the libraries that we will need.
import json
import os
import datetime
import wallaroo
from wallaroo.object import EntityNotFoundError
from wallaroo.framework import Framework
# used to display dataframe information without truncating
from IPython.display import display
import pandas as pd
pd.set_option('display.max_colwidth', None)
wallaroo.__version__
'2023.2.1rc2'
Initialize connection
Start a connect to the Wallaroo instance and save the connection into the variable wl
.
# Login through local Wallaroo instance
wl = wallaroo.Client()
Set Configurations
The following will set the workspace, model name, and pipeline that will be used for this example. If the workspace or pipeline already exist, then they will assigned for use in this example. If they do not exist, they will be created based on the names listed below.
Workspace names must be unique. To allow this tutorial to run in the same Wallaroo instance for multiple users, the suffix
variable is generated from a random set of 4 ASCII characters. To use the same workspace each time, hard code suffix
and verify the workspace name created is is unique across the Wallaroo instance.
# used for unique connection names
import string
import random
suffix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
workspace_name = f'multiple-replica-forecast-tutorial-{suffix}'
pipeline_name = 'bikedaypipe'
model_name = 'bikedaymodel'
Set the Workspace and Pipeline
The workspace will be either used or created if it does not exist, along with the pipeline.
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
pipeline = wl.build_pipeline(name)
return pipeline
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
pipeline = get_pipeline(pipeline_name)
Upload Model
The Python model created in “Forecast and Parallel Infer with Statsmodel: Model Creation” will now be uploaded. Note that the Framework and runtime are set to python
.
model_file_name = 'forecast.py'
bike_day_model = wl.upload_model(model_name, model_file_name, Framework.PYTHON).configure(runtime="python")
Deploy the Pipeline
We will now add the uploaded model as a step for the pipeline, then deploy it. The pipeline configuration will allow for multiple replicas of the pipeline to be deployed and spooled up in the cluster. Each pipeline replica will use 0.25 cpu and 512 Gi RAM.
# Set the deployment to allow for additional engines to run
deploy_config = (wallaroo.DeploymentConfigBuilder()
.replica_count(1)
.replica_autoscale_min_max(minimum=2, maximum=5)
.cpus(0.25)
.memory("512Mi")
.build()
)
pipeline.add_model_step(bike_day_model).deploy(deployment_config = deploy_config)
name | bikedaypipe |
---|---|
created | 2023-07-14 15:50:50.014326+00:00 |
last_updated | 2023-07-14 15:50:52.029628+00:00 |
deployed | True |
tags | |
versions | 7aae4653-9e9f-468c-b266-4433be652313, 48983f9b-7c43-41fe-9688-df72a6aa55e9 |
steps | bikedaymodel |
Run Inference
Run a test inference to verify the pipeline is operational from the sample test data stored in ./data/testdata_dict.json
.
inferencedata = json.load(open("./data/testdata_dict.json"))
results = pipeline.infer(inferencedata)
display(results)
[{'forecast': [1764, 1749, 1743, 1741, 1740, 1740, 1740]}]
Undeploy the Pipeline
Undeploy the pipeline and return the resources back to the Wallaroo instance.
pipeline.undeploy()
name | bikedaypipe |
---|---|
created | 2023-07-14 15:50:50.014326+00:00 |
last_updated | 2023-07-14 15:50:52.029628+00:00 |
deployed | False |
tags | |
versions | 7aae4653-9e9f-468c-b266-4433be652313, 48983f9b-7c43-41fe-9688-df72a6aa55e9 |
steps | bikedaymodel |
4.7.3 - Statsmodel Forecast with Wallaroo Features: Parallel Inference
This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.
Statsmodel Forecast with Wallaroo Features: Parallel Inference
This tutorial series demonstrates how to use Wallaroo to create a Statsmodel forecasting model based on bike rentals. This tutorial series is broken down into the following:
- Create and Train the Model: This first notebook shows how the model is trained from existing data.
- Deploy and Sample Inference: With the model developed, we will deploy it into Wallaroo and perform a sample inference.
- Parallel Infer: A sample of multiple weeks of data will be retrieved and submitted as an asynchronous parallel inference. The results will be collected and uploaded to a sample database.
- External Connection: A sample data connection to Google BigQuery to retrieve input data and store the results in a table.
- ML Workload Orchestration: Take all of the previous steps and automate the request into a single Wallaroo ML Workload Orchestration.
This step will use the simulated database simdb
to gather 4 weeks of inference data, then submit the inference request through the asynchronous Pipeline method parallel_infer
. This receives a List of inference data, submits it to the Wallaroo pipeline, then receives the results as a separate list with each inference matched to the input submitted.
The results are then compared against the actual data to see if the model was accurate.
Prerequisites
- A Wallaroo instance version 2023.2.1 or greater.
References
- Wallaroo SDK Essentials Guide: Model Uploads and Registrations: Python Models
- Wallaroo SDK Essentials Guide: Pipeline Management
- Wallaroo SDK Essentials: Inference Guide: Parallel Inferences
Parallel Infer Steps
Import Libraries
The first step is to import the libraries that we will need.
import json
import os
import datetime
import wallaroo
from wallaroo.object import EntityNotFoundError
from wallaroo.framework import Framework
# used to display dataframe information without truncating
from IPython.display import display
import pandas as pd
import numpy as np
from resources import simdb
from resources import util
pd.set_option('display.max_colwidth', None)
display(wallaroo.__version__)
'2023.2.1rc2'
Initialize connection
Start a connect to the Wallaroo instance and save the connection into the variable wl
.
# Login through local Wallaroo instance
wl = wallaroo.Client()
Set Configurations
The following will set the workspace, model name, and pipeline that will be used for this example. If the workspace or pipeline already exist, then they will assigned for use in this example. If they do not exist, they will be created based on the names listed below.
Workspace names must be unique. To allow this tutorial to run in the same Wallaroo instance for multiple users, the suffix
variable is generated from a random set of 4 ASCII characters. To use the same workspace across the tutorial notebooks, hard code suffix
and verify the workspace name created is is unique across the Wallaroo instance.
# used for unique connection names
import string
import random
suffix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
workspace_name = f'multiple-replica-forecast-tutorial-{suffix}'
pipeline_name = 'bikedaypipe'
model_name = 'bikedaymodel'
Set the Workspace and Pipeline
The workspace will be either used or created if it does not exist, along with the pipeline.
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
pipeline = wl.build_pipeline(name)
return pipeline
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
pipeline = get_pipeline(pipeline_name)
model_file_name = 'forecast.py'
bike_day_model = wl.upload_model(model_name, model_file_name, Framework.PYTHON).configure(runtime="python")
Upload Model
The Python model created in “Forecast and Parallel Infer with Statsmodel: Model Creation” will now be uploaded. Note that the Framework and runtime are set to python
.
pipeline.add_model_step(bike_day_model)
name | bikedaypipe |
---|---|
created | 2023-07-14 15:50:50.014326+00:00 |
last_updated | 2023-07-14 15:50:52.029628+00:00 |
deployed | False |
tags | |
versions | 7aae4653-9e9f-468c-b266-4433be652313, 48983f9b-7c43-41fe-9688-df72a6aa55e9 |
steps | bikedaymodel |
Deploy the Pipeline
We will now add the uploaded model as a step for the pipeline, then deploy it. The pipeline configuration will allow for multiple replicas of the pipeline to be deployed and spooled up in the cluster. Each pipeline replica will use 0.25 cpu and 512 Gi RAM.
# Set the deployment to allow for additional engines to run
deploy_config = (wallaroo.DeploymentConfigBuilder()
.replica_count(1)
.replica_autoscale_min_max(minimum=2, maximum=5)
.cpus(0.25)
.memory("512Mi")
.build()
)
pipeline.deploy(deployment_config = deploy_config)
name | bikedaypipe |
---|---|
created | 2023-07-14 15:53:07.284131+00:00 |
last_updated | 2023-07-14 15:56:07.413409+00:00 |
deployed | True |
tags | |
versions | 9c67dd93-014c-4cc9-9b44-549829e613ad, 258dafaf-c272-4bda-881b-5998a4a9be26 |
steps | bikedaymodel |
Run Inference
For this example, we will forecast bike rentals by looking back one month from “today” which will be set as 2011-02-22. The data from 2011-01-23 to 2011-01-27 (the 5 days starting from one month back) are used to generate a forecast for what bike sales will be over the next week from “today”, which will be 2011-02-23 to 2011-03-01.
# retrieve forecast schedule
first_day, analysis_days = util.get_forecast_days()
print(f'Running analysis on {first_day}')
Running analysis on 2011-02-22
# connect to SQL data base
conn = simdb.get_db_connection()
print(f'Bike rentals table: {simdb.tablename}')
# create the query and retrieve data
query = util.mk_dt_range_query(tablename=simdb.tablename, forecast_day=first_day)
print(query)
data = pd.read_sql_query(query, conn)
data.head()
Bike rentals table: bikerentals
select cnt from bikerentals where date > DATE(DATE('2011-02-22'), '-1 month') AND date <= DATE('2011-02-22')
cnt | |
---|---|
0 | 986 |
1 | 1416 |
2 | 1985 |
3 | 506 |
4 | 431 |
pd.read_sql_query("select date, cnt from bikerentals where date > DATE(DATE('2011-02-22'), '-1 month') AND date <= DATE('2011-02-22') LIMIT 5", conn)
date | cnt | |
---|---|---|
0 | 2011-01-23 | 986 |
1 | 2011-01-24 | 1416 |
2 | 2011-01-25 | 1985 |
3 | 2011-01-26 | 506 |
4 | 2011-01-27 | 431 |
# send data to model for forecast
results = pipeline.infer(data.to_dict(orient='list'))[0]
results
{'forecast': [1462, 1483, 1497, 1507, 1513, 1518, 1521]}
# annotate with the appropriate dates (the next seven days)
resultframe = pd.DataFrame({
'date' : util.get_forecast_dates(first_day),
'forecast' : results['forecast']
})
# write the new data to the db table "bikeforecast"
resultframe.to_sql('bikeforecast', conn, index=False, if_exists='append')
# display the db table
query = "select date, forecast from bikeforecast"
pd.read_sql_query(query, conn)
date | forecast | |
---|---|---|
0 | 2011-02-23 | 1462 |
1 | 2011-02-24 | 1483 |
2 | 2011-02-25 | 1497 |
3 | 2011-02-26 | 1507 |
4 | 2011-02-27 | 1513 |
5 | 2011-02-28 | 1518 |
6 | 2011-03-01 | 1521 |
Four Weeks of Inference Data
Now we’ll go back staring at the “current data” of 2011-03-01, and fetch each week’s data across the month. This will be used to submit 5 inference requests through the Pipeline parallel_infer
method.
The inference data is saved into the inference_data
List - each element in the list will be a separate inference request.
# get our list of items to run through
inference_data = []
content_type = "application/json"
days = []
for day in analysis_days:
print(f"Current date: {day}")
days.append(day)
query = util.mk_dt_range_query(tablename=simdb.tablename, forecast_day=day)
print(query)
data = pd.read_sql_query(query, conn)
inference_data.append(data.to_dict(orient='list'))
Current date: 2011-03-01
select cnt from bikerentals where date > DATE(DATE('2011-03-01'), '-1 month') AND date <= DATE('2011-03-01')
Current date: 2011-03-08
select cnt from bikerentals where date > DATE(DATE('2011-03-08'), '-1 month') AND date <= DATE('2011-03-08')
Current date: 2011-03-15
select cnt from bikerentals where date > DATE(DATE('2011-03-15'), '-1 month') AND date <= DATE('2011-03-15')
Current date: 2011-03-22
select cnt from bikerentals where date > DATE(DATE('2011-03-22'), '-1 month') AND date <= DATE('2011-03-22')
Current date: 2011-03-29
select cnt from bikerentals where date > DATE(DATE('2011-03-29'), '-1 month') AND date <= DATE('2011-03-29')
Parallel Inference Request
The List inference_data
will be submitted. Recall that the pipeline deployment can spool up to 5 replicas.
The pipeline parallel_infer(tensor_list, timeout, num_parallel, retries)
asynchronous method performs an inference as defined by the pipeline steps and takes the following arguments:
- tensor_list (REQUIRED List): The data submitted to the pipeline for inference as a List of the supported data types:
- pandas.DataFrame: Data submitted as a pandas DataFrame are returned as a pandas DataFrame. For models that output one column based on the models outputs.
- Apache Arrow (Preferred): Data submitted as an Apache Arrow are returned as an Apache Arrow.
- timeout (OPTIONAL int): A timeout in seconds before the inference throws an exception. The default is 15 second per call to accommodate large, complex models. Note that for a batch inference, this is per list item - with 10 inference requests, each would have a default timeout of 15 seconds.
- num_parallel (OPTIONAL int): The number of parallel threads used for the submission. This should be no more than four times the number of pipeline replicas.
- retries (OPTIONAL int): The number of retries per inference request submitted.
parallel_infer
is an asynchronous method that returns the Python callback list of tasks. Calling parallel_infer
should be called with the await
keyword to retrieve the callback results.
For more details, see the Wallaroo parallel inferences guide.
parallel_results = await pipeline.parallel_infer(tensor_list=inference_data, timeout=20, num_parallel=16, retries=2)
display(parallel_results)
[[{'forecast': [1764, 1749, 1743, 1741, 1740, 1740, 1740]}],
[{'forecast': [1735, 1858, 1755, 1841, 1770, 1829, 1780]}],
[{'forecast': [1878, 1851, 1858, 1856, 1857, 1856, 1856]}],
[{'forecast': [2363, 2316, 2277, 2243, 2215, 2192, 2172]}],
[{'forecast': [2225, 2133, 2113, 2109, 2108, 2108, 2108]}]]
Upload into DataBase
With our results, we’ll merge the results we have into the days we were looking to analyze. Then we can upload the results into the sample database and display the results.
# merge the days and the results
days_results = list(zip(days, parallel_results))
# upload to the database
for day_result in days_results:
resultframe = pd.DataFrame({
'date' : util.get_forecast_dates(day_result[0]),
'forecast' : day_result[1][0]['forecast']
})
resultframe.to_sql('bikeforecast', conn, index=False, if_exists='append')
On April 1st, we can compare March forecasts to actuals
query = f'''SELECT bikeforecast.date AS date, forecast, cnt AS actual
FROM bikeforecast LEFT JOIN bikerentals
ON bikeforecast.date = bikerentals.date
WHERE bikeforecast.date >= DATE('2011-03-01')
AND bikeforecast.date < DATE('2011-04-01')
ORDER BY 1'''
print(query)
comparison = pd.read_sql_query(query, conn)
comparison
SELECT bikeforecast.date AS date, forecast, cnt AS actual
FROM bikeforecast LEFT JOIN bikerentals
ON bikeforecast.date = bikerentals.date
WHERE bikeforecast.date >= DATE('2011-03-01')
AND bikeforecast.date < DATE('2011-04-01')
ORDER BY 1
date | forecast | actual | |
---|---|---|---|
0 | 2011-03-02 | 1764 | 2134 |
1 | 2011-03-03 | 1749 | 1685 |
2 | 2011-03-04 | 1743 | 1944 |
3 | 2011-03-05 | 1741 | 2077 |
4 | 2011-03-06 | 1740 | 605 |
5 | 2011-03-07 | 1740 | 1872 |
6 | 2011-03-08 | 1740 | 2133 |
7 | 2011-03-09 | 1735 | 1891 |
8 | 2011-03-10 | 1858 | 623 |
9 | 2011-03-11 | 1755 | 1977 |
10 | 2011-03-12 | 1841 | 2132 |
11 | 2011-03-13 | 1770 | 2417 |
12 | 2011-03-14 | 1829 | 2046 |
13 | 2011-03-15 | 1780 | 2056 |
14 | 2011-03-16 | 1878 | 2192 |
15 | 2011-03-17 | 1851 | 2744 |
16 | 2011-03-18 | 1858 | 3239 |
17 | 2011-03-19 | 1856 | 3117 |
18 | 2011-03-20 | 1857 | 2471 |
19 | 2011-03-21 | 1856 | 2077 |
20 | 2011-03-22 | 1856 | 2703 |
21 | 2011-03-23 | 2363 | 2121 |
22 | 2011-03-24 | 2316 | 1865 |
23 | 2011-03-25 | 2277 | 2210 |
24 | 2011-03-26 | 2243 | 2496 |
25 | 2011-03-27 | 2215 | 1693 |
26 | 2011-03-28 | 2192 | 2028 |
27 | 2011-03-29 | 2172 | 2425 |
28 | 2011-03-30 | 2225 | 1536 |
29 | 2011-03-31 | 2133 | 1685 |
Undeploy the Pipeline
Undeploy the pipeline and return the resources back to the Wallaroo instance.
conn.close()
pipeline.undeploy()
name | bikedaypipe |
---|---|
created | 2023-07-14 15:53:07.284131+00:00 |
last_updated | 2023-07-14 15:56:07.413409+00:00 |
deployed | False |
tags | |
versions | 9c67dd93-014c-4cc9-9b44-549829e613ad, 258dafaf-c272-4bda-881b-5998a4a9be26 |
steps | bikedaymodel |
4.7.4 - Statsmodel Forecast with Wallaroo Features: Data Connection
This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.
Statsmodel Forecast with Wallaroo Features: Data Connection
This tutorial series demonstrates how to use Wallaroo to create a Statsmodel forecasting model based on bike rentals. This tutorial series is broken down into the following:
- Create and Train the Model: This first notebook shows how the model is trained from existing data.
- Deploy and Sample Inference: With the model developed, we will deploy it into Wallaroo and perform a sample inference.
- Parallel Infer: A sample of multiple weeks of data will be retrieved and submitted as an asynchronous parallel inference. The results will be collected and uploaded to a sample database.
- External Connection: A sample data connection to Google BigQuery to retrieve input data and store the results in a table.
- ML Workload Orchestration: Take all of the previous steps and automate the request into a single Wallaroo ML Workload Orchestration.
For this step, we will use a Google BigQuery dataset to retrieve the inference information, predict the next month of sales, then store those predictions into another table. This will use the Wallaroo Connection feature to create a Connection, assign it to our workspace, then perform our inferences by using the Connection details to connect to the BigQuery dataset and tables.
Prerequisites
- A Wallaroo instance version 2023.2.1 or greater.
- Install the libraries from
./resources/requirements.txt
that include the following:- google-cloud-bigquery==3.10.0
- google-auth==2.17.3
- db-dtypes==1.1.1
References
- Wallaroo SDK Essentials Guide: Model Uploads and Registrations: Python Models
- Wallaroo SDK Essentials Guide: Pipeline Management
- Wallaroo SDK Essentials: Inference Guide: Parallel Inferences
Statsmodel Forecast Connection Steps
Import Libraries
The first step is to import the libraries that we will need.
import json
import os
import datetime
import wallaroo
from wallaroo.object import EntityNotFoundError
from wallaroo.framework import Framework
# used to display dataframe information without truncating
from IPython.display import display
import pandas as pd
import numpy as np
from resources import simdb
from resources import util
pd.set_option('display.max_colwidth', None)
# for Big Query connections
from google.cloud import bigquery
from google.oauth2 import service_account
import db_dtypes
import time
display(wallaroo.__version__)
'2023.3.0+65834aca6'
Initialize connection
Start a connect to the Wallaroo instance and save the connection into the variable wl
.
# Login through local Wallaroo instance
wl = wallaroo.Client()
Set Configurations
The following will set the workspace, model name, and pipeline that will be used for this example. If the workspace or pipeline already exist, then they will assigned for use in this example. If they do not exist, they will be created based on the names listed below.
Workspace names must be unique. To allow this tutorial to run in the same Wallaroo instance for multiple users, the suffix
variable is generated from a random set of 4 ASCII characters. To use the same workspace across the tutorial notebooks, hard code suffix
and verify the workspace name created is is unique across the Wallaroo instance.
# used for unique connection names
import string
import random
suffix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
workspace_name = f'multiple-replica-forecast-tutorial-{suffix}'
pipeline_name = 'bikedaypipe'
model_name = 'bikedaymodel'
Set the Workspace and Pipeline
The workspace will be either used or created if it does not exist, along with the pipeline.
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
pipeline = wl.build_pipeline(name)
return pipeline
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
pipeline = get_pipeline(pipeline_name)
Upload Model
The Python model created in “Forecast and Parallel Infer with Statsmodel: Model Creation” will now be uploaded. Note that the Framework and runtime are set to python
.
model_file_name = 'forecast.py'
bike_day_model = wl.upload_model(model_name, model_file_name, Framework.PYTHON).configure(runtime="python")
pipeline.add_model_step(bike_day_model)
name | bikedaypipe |
---|---|
created | 2023-06-28 20:11:58.734248+00:00 |
last_updated | 2023-06-29 21:10:19.250680+00:00 |
deployed | True |
tags | |
versions | 93b113a2-f31a-4e05-883e-66a3d1fa10fb, 7d687c43-a833-4585-b607-7085eff16e9d, 504bb140-d9e2-4964-8f82-27b1d234f7f2, db1a14ad-c40c-41ac-82db-0cdd372172f3, 01d60d1c-7834-4d1f-b9a8-8ad569e114b6, a165cbbb-84d9-42e7-99ec-aa8e244aeb55, 0fefef8b-105e-4a6e-9193-d2e6d61248a1 |
steps | bikedaymodel |
Deploy the Pipeline
We will now add the uploaded model as a step for the pipeline, then deploy it. The pipeline configuration will allow for multiple replicas of the pipeline to be deployed and spooled up in the cluster. Each pipeline replica will use 0.25 cpu and 512 Gi RAM.
# Set the deployment to allow for additional engines to run
deploy_config = (wallaroo.DeploymentConfigBuilder()
.replica_count(4)
.cpus(0.25)
.memory("512Mi")
.build()
)
pipeline.deploy(deployment_config = deploy_config)
ok
name | bikedaypipe |
---|---|
created | 2023-06-28 20:11:58.734248+00:00 |
last_updated | 2023-06-29 21:12:00.676013+00:00 |
deployed | True |
tags | |
versions | f5051ddf-1111-49e6-b914-f8d24f1f6a8a, 93b113a2-f31a-4e05-883e-66a3d1fa10fb, 7d687c43-a833-4585-b607-7085eff16e9d, 504bb140-d9e2-4964-8f82-27b1d234f7f2, db1a14ad-c40c-41ac-82db-0cdd372172f3, 01d60d1c-7834-4d1f-b9a8-8ad569e114b6, a165cbbb-84d9-42e7-99ec-aa8e244aeb55, 0fefef8b-105e-4a6e-9193-d2e6d61248a1 |
steps | bikedaymodel |
Create the Connection
We have already demonstrated through the other notebooks in this series that we can use the statsmodel forecast model to perform an inference through a simulated database. Now we’ll create a Wallaroo connection that will store the credentials to a Google BigQuery database containining the information we’re looking for.
The details of the connection are stored in the file ./resources/bigquery_service_account_statsmodel.json
that include the service account key file(SAK) information, as well as the dataset and table used. The details on how to generate the table and data for the sample bike_rentals
table are stored in the file ./resources/create_bike_rentals.table
, with the data used stored in ./resources/bike_rentals.csv
.
Wallaroo connections are created through the Wallaroo Client create_connection(name, type, details)
method. See the Wallaroo SDK Essentials Guide: Data Connections Management guide for full details.
With the credentials are three other important fields:
dataset
: The BigQuery dataset from the project specified in the service account credentials file.input_table
: The table used for inference inputs.output_table
: The table used to store results.
We’ll add the helper method get_connection
. If the connection already exists, then Wallaroo will return an error. If the connection with the same name already exists, it will retrieve it. Verify that the connection does not already exist in the Wallaroo instance for proper functioning of this tutorial.
forecast_connection_input_name = f'statsmodel-bike-rentals-{suffix}'
forecast_connection_input_type = "BIGQUERY"
forecast_connection_input_argument = json.load(open('./resources/bigquery_service_account_statsmodel.json'))
statsmodel_connection = wl.create_connection(forecast_connection_input_name,
forecast_connection_input_type,
forecast_connection_input_argument)
display(statsmodel_connection)
Field | Value |
---|---|
Name | statsmodel-bike-rentals-jch |
Connection Type | BIGQUERY |
Details | ***** |
Created At | 2023-06-29T19:55:17.866728+00:00 |
Linked Workspaces | ['multiple-replica-forecast-tutorial-jch'] |
Add Connection to Workspace
We’ll now add the connection to our workspace so it can be retrieved by other workspace users. The method Workspace add_connection(connection_name)
adds a Data Connection to a workspace.
workspace.add_connection(forecast_connection_input_name)
Retrieve Connection from Workspace
To simulate a data scientist’s procedural flow, we’ll now retrieve the connection from the workspace.
The method Workspace list_connections()
displays a list of connections attached to the workspace. By default the details field is obfuscated. Specific connections are retrieved by specifying their position in the returned list.
forecast_connection = workspace.list_connections()[0]
display(forecast_connection)
Field | Value |
---|---|
Name | statsmodel-bike-rentals-jch |
Connection Type | BIGQUERY |
Details | ***** |
Created At | 2023-06-29T19:55:17.866728+00:00 |
Linked Workspaces | ['multiple-replica-forecast-tutorial-jch'] |
Run Inference from BigQuery Table
We’ll now retrieve sample data through the Wallaroo connection, and perform a sample inference. The connection details are retrieved through the Connection details()
method.
The process is:
- Create the BigQuery credentials.
- Connect to the BigQuery dataset.
- Retrieve the inference data.
bigquery_statsmodel_credentials = service_account.Credentials.from_service_account_info(
forecast_connection.details())
bigquery_statsmodel_client = bigquery.Client(
credentials=bigquery_statsmodel_credentials,
project=forecast_connection.details()['project_id']
)
inference_inputs = bigquery_statsmodel_client.query(
f"""
select dteday as date, cnt FROM {forecast_connection.details()['dataset']}.{forecast_connection.details()['input_table']}
where dteday > DATE_SUB(DATE('2011-02-22'),
INTERVAL 1 month) AND dteday <= DATE('2011-02-22')
ORDER BY dteday
LIMIT 5
"""
).to_dataframe().apply({"date":str, "cnt":int}).to_dict(orient='list')
# the original table sends back the date schema as a date, not text. We'll convert it here.
# inference_inputs = inference_inputs.apply({"date":str, "cnt":int})
display(inference_inputs)
{'date': ['2011-01-23',
'2011-01-24',
'2011-01-25',
'2011-01-26',
'2011-01-27'],
'cnt': [986, 1416, 1985, 506, 431]}
Perform Inference from BigQuery Connection Data
With the data retrieved, we’ll perform an inference through it and display the result.
results = pipeline.infer(inference_inputs)
results
[{'forecast': [1177, 1023, 1082, 1060, 1068, 1065, 1066]}]
Four Weeks of Inference Data
Now we’ll go back staring at the “current data” of the next month in 2011, and fetch the previous month to that date, then use that to predict what sales will be over the next 7 days.
The inference data is saved into the inference_data
List - each element in the list will be a separate inference request.
# Start by getting the current month - we'll alway assume we're in 2011 to match the data store
month = datetime.datetime.now().month
month=5
start_date = f"{month+1}-1-2011"
display(start_date)
'6-1-2011'
def get_forecast_days(firstdate) :
days = [i*7 for i in [-1,0,1,2,3,4]]
deltadays = pd.to_timedelta(pd.Series(days), unit='D')
analysis_days = (pd.to_datetime(firstdate) + deltadays).dt.date
analysis_days = [str(day) for day in analysis_days]
analysis_days
seed_day = analysis_days.pop(0)
return analysis_days
forecast_dates = get_forecast_days(start_date)
display(forecast_dates)
['2011-06-01', '2011-06-08', '2011-06-15', '2011-06-22', '2011-06-29']
# get our list of items to run through
inference_data = []
days = []
# get the days from the start date to the end date
def get_forecast_dates(forecast_day: str, nforecast=7):
days = [i for i in range(nforecast)]
deltadays = pd.to_timedelta(pd.Series(days), unit='D')
last_day = pd.to_datetime(forecast_day)
dates = last_day + deltadays
datestr = dates.dt.date.astype(str)
return datestr
# used to generate our queries
def mk_dt_range_query(*, tablename: str, forecast_day: str) -> str:
assert isinstance(tablename, str)
assert isinstance(forecast_day, str)
query = f"""
select cnt from {tablename} where
dteday >= DATE_SUB(DATE('{forecast_day}'), INTERVAL 1 month)
AND dteday < DATE('{forecast_day}')
ORDER BY dteday
"""
return query
for day in forecast_dates:
print(f"Current date: {day}")
day_range=get_forecast_dates(day)
days.append({"date": day_range})
query = mk_dt_range_query(tablename=f"{forecast_connection.details()['dataset']}.{forecast_connection.details()['input_table']}", forecast_day=day)
print(query)
data = bigquery_statsmodel_client.query(query).to_dataframe().apply({"cnt":int}).to_dict(orient='list')
# add the date into the list
inference_data.append(data)
Current date: 2011-06-01
select cnt from release_testing_2023_2.bike_rentals where
dteday >= DATE_SUB(DATE('2011-06-01'), INTERVAL 1 month)
AND dteday < DATE('2011-06-01')
ORDER BY dteday
Current date: 2011-06-08
select cnt from release_testing_2023_2.bike_rentals where
dteday >= DATE_SUB(DATE('2011-06-08'), INTERVAL 1 month)
AND dteday < DATE('2011-06-08')
ORDER BY dteday
Current date: 2011-06-15
select cnt from release_testing_2023_2.bike_rentals where
dteday >= DATE_SUB(DATE('2011-06-15'), INTERVAL 1 month)
AND dteday < DATE('2011-06-15')
ORDER BY dteday
Current date: 2011-06-22
select cnt from release_testing_2023_2.bike_rentals where
dteday >= DATE_SUB(DATE('2011-06-22'), INTERVAL 1 month)
AND dteday < DATE('2011-06-22')
ORDER BY dteday
Current date: 2011-06-29
select cnt from release_testing_2023_2.bike_rentals where
dteday >= DATE_SUB(DATE('2011-06-29'), INTERVAL 1 month)
AND dteday < DATE('2011-06-29')
ORDER BY dteday
parallel_results = await pipeline.parallel_infer(tensor_list=inference_data, timeout=20, num_parallel=16, retries=2)
display(parallel_results)
[[{'forecast': [4373, 4385, 4379, 4382, 4380, 4381, 4380]}],
[{'forecast': [4666, 4582, 4560, 4555, 4553, 4553, 4552]}],
[{'forecast': [4683, 4634, 4625, 4623, 4622, 4622, 4622]}],
[{'forecast': [4732, 4637, 4648, 4646, 4647, 4647, 4647]}],
[{'forecast': [4692, 4698, 4699, 4699, 4699, 4699, 4699]}]]
days_results = list(zip(days, parallel_results))
# merge our parallel results into the predicted date sales
# results_table = pd.DataFrame(list(zip(days, parallel_results)),
# columns=["date", "forecast"])
results_table = pd.DataFrame(columns=["date", "forecast"])
# display(days_results)
for date in days_results:
# display(date)
new_days = date[0]['date'].tolist()
new_forecast = date[1][0]['forecast']
new_results = list(zip(new_days, new_forecast))
results_table = results_table.append(pd.DataFrame(list(zip(new_days, new_forecast)), columns=['date','forecast']))
Based on all of the predictions, here are the results for the next month.
results_table
date | forecast | |
---|---|---|
0 | 2011-06-01 | 4373 |
1 | 2011-06-02 | 4385 |
2 | 2011-06-03 | 4379 |
3 | 2011-06-04 | 4382 |
4 | 2011-06-05 | 4380 |
5 | 2011-06-06 | 4381 |
6 | 2011-06-07 | 4380 |
0 | 2011-06-08 | 4666 |
1 | 2011-06-09 | 4582 |
2 | 2011-06-10 | 4560 |
3 | 2011-06-11 | 4555 |
4 | 2011-06-12 | 4553 |
5 | 2011-06-13 | 4553 |
6 | 2011-06-14 | 4552 |
0 | 2011-06-15 | 4683 |
1 | 2011-06-16 | 4634 |
2 | 2011-06-17 | 4625 |
3 | 2011-06-18 | 4623 |
4 | 2011-06-19 | 4622 |
5 | 2011-06-20 | 4622 |
6 | 2011-06-21 | 4622 |
0 | 2011-06-22 | 4732 |
1 | 2011-06-23 | 4637 |
2 | 2011-06-24 | 4648 |
3 | 2011-06-25 | 4646 |
4 | 2011-06-26 | 4647 |
5 | 2011-06-27 | 4647 |
6 | 2011-06-28 | 4647 |
0 | 2011-06-29 | 4692 |
1 | 2011-06-30 | 4698 |
2 | 2011-07-01 | 4699 |
3 | 2011-07-02 | 4699 |
4 | 2011-07-03 | 4699 |
5 | 2011-07-04 | 4699 |
6 | 2011-07-05 | 4699 |
Upload into DataBase
With our results, we’ll upload the results into the table listed in our connection as the results_table
. To save time, we’ll just upload the dataframe directly with the Google Query insert_rows_from_dataframe
method.
output_table = bigquery_statsmodel_client.get_table(f"{forecast_connection.details()['dataset']}.{forecast_connection.details()['results_table']}")
bigquery_statsmodel_client.insert_rows_from_dataframe(
output_table,
dataframe=results_table
)
[[]]
We’ll grab the last 5 results from our results table to verify the data was inserted.
# Get the last insert to the output table to verify
# wait 10 seconds for the insert to finish
time.sleep(10)
task_inference_results = bigquery_statsmodel_client.query(
f"""
SELECT *
FROM {forecast_connection.details()['dataset']}.{forecast_connection.details()['results_table']}
ORDER BY date DESC
LIMIT 5
"""
).to_dataframe()
display(task_inference_results)
date | forecast | |
---|---|---|
0 | 2011-07-05 | 4699 |
1 | 2011-07-05 | 4699 |
2 | 2011-07-04 | 4699 |
3 | 2011-07-04 | 4699 |
4 | 2011-07-03 | 4699 |
Undeploy the Pipeline
Undeploy the pipeline and return the resources back to the Wallaroo instance.
pipeline.undeploy()
Waiting for undeployment - this will take up to 45s ..................................... ok
name | bikedaypipe |
---|---|
created | 2023-06-28 20:11:58.734248+00:00 |
last_updated | 2023-06-29 21:12:00.676013+00:00 |
deployed | False |
tags | |
versions | f5051ddf-1111-49e6-b914-f8d24f1f6a8a, 93b113a2-f31a-4e05-883e-66a3d1fa10fb, 7d687c43-a833-4585-b607-7085eff16e9d, 504bb140-d9e2-4964-8f82-27b1d234f7f2, db1a14ad-c40c-41ac-82db-0cdd372172f3, 01d60d1c-7834-4d1f-b9a8-8ad569e114b6, a165cbbb-84d9-42e7-99ec-aa8e244aeb55, 0fefef8b-105e-4a6e-9193-d2e6d61248a1 |
steps | bikedaymodel |
4.7.5 - Statsmodel Forecast with Wallaroo Features: ML Workload Orchestration
This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.
Statsmodel Forecast with Wallaroo Features: ML Workload Orchestration
This tutorial series demonstrates how to use Wallaroo to create a Statsmodel forecasting model based on bike rentals. This tutorial series is broken down into the following:
- Create and Train the Model: This first notebook shows how the model is trained from existing data.
- Deploy and Sample Inference: With the model developed, we will deploy it into Wallaroo and perform a sample inference.
- Parallel Infer: A sample of multiple weeks of data will be retrieved and submitted as an asynchronous parallel inference. The results will be collected and uploaded to a sample database.
- External Connection: A sample data connection to Google BigQuery to retrieve input data and store the results in a table.
- ML Workload Orchestration: Take all of the previous steps and automate the request into a single Wallaroo ML Workload Orchestration.
This step will expand upon using the Connection and create a ML Workload Orchestration that automates requesting the inference data, submitting it in parallel, and storing the results into a database table.
Prerequisites
- A Wallaroo instance version 2023.2.1 or greater.
- Install the libraries from
./resources/requirements.txt
that include the following:- google-cloud-bigquery==3.10.0
- google-auth==2.17.3
- db-dtypes==1.1.1
References
- Wallaroo SDK Essentials Guide: Model Uploads and Registrations: Python Models
- Wallaroo SDK Essentials Guide: Pipeline Management
- Wallaroo SDK Essentials: Inference Guide: Parallel Inferences
Orchestrations, Taks, and Tasks Runs
We’ve details how Wallaroo Connections work. Now we’ll use Orchestrations, Tasks, and Task Runs.
Item | Description |
---|---|
Orchestration | ML Workload orchestration allows data scientists and ML Engineers to automate and scale production ML workflows in Wallaroo to ensure a tight feedback loop and continuous tuning of models from training to production. Wallaroo platform users (data scientists or ML Engineers) have the ability to deploy, automate and scale recurring batch production ML workloads that can ingest data from predefined data sources to run inferences in Wallaroo, chain pipelines, and send inference results to predefined destinations to analyze model insights and assess business outcomes. |
Task | An implementation of an Orchestration. Tasks can be either Run Once : They run once and upon completion, stop. Run Scheduled : The task runs whenever a specific cron like schedule is reached. Scheduled tasks will run until the kill command is issued. |
Task Run | The execusion of a task. For Run Once tasks, there will be only one Run Task . A Run Scheduled tasks will have multiple tasks, one for every time the schedule parameter is met. Task Runs have their own log files that can be examined to track progress and results. |
Statsmodel Forecast Connection Steps
Import Libraries
The first step is to import the libraries that we will need.
import json
import os
import datetime
import wallaroo
from wallaroo.object import EntityNotFoundError
from wallaroo.framework import Framework
# used to display dataframe information without truncating
from IPython.display import display
import pandas as pd
import numpy as np
from resources import simdb
from resources import util
pd.set_option('display.max_colwidth', None)
# for Big Query connections
from google.cloud import bigquery
from google.oauth2 import service_account
import db_dtypes
import time
display(wallaroo.__version__)
'2023.3.0+785595cda'
Initialize connection
Start a connect to the Wallaroo instance and save the connection into the variable wl
.
# Login through local Wallaroo instance
wl = wallaroo.Client()
Set Configurations
The following will set the workspace, model name, and pipeline that will be used for this example. If the workspace or pipeline already exist, then they will assigned for use in this example. If they do not exist, they will be created based on the names listed below.
Workspace names must be unique. To allow this tutorial to run in the same Wallaroo instance for multiple users, the suffix
variable is generated from a random set of 4 ASCII characters. To use the same workspace across the tutorial notebooks, hard code suffix
and verify the workspace name created is is unique across the Wallaroo instance.
# used for unique connection names
import string
import random
suffix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
workspace_name = f'multiple-replica-forecast-tutorial-{suffix}'
pipeline_name = 'bikedaypipe'
connection_name = f'statsmodel-bike-rentals-{suffix}'
Set the Workspace and Pipeline
The workspace will be either used or created if it does not exist, along with the pipeline.
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
pipeline = wl.build_pipeline(name)
return pipeline
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
pipeline = get_pipeline(pipeline_name)
Deploy Pipeline
The pipeline is already set witht the model. For our demo we’ll verify that it’s deployed.
# Set the deployment to allow for additional engines to run
deploy_config = (wallaroo.DeploymentConfigBuilder()
.replica_count(4)
.cpus(0.25)
.memory("512Mi")
.build()
)
pipeline.deploy(deployment_config = deploy_config)
Waiting for deployment - this will take up to 45s .................... ok
name | bikedaypipe |
---|---|
created | 2023-06-30 15:42:56.781150+00:00 |
last_updated | 2023-06-30 15:45:23.267621+00:00 |
deployed | True |
tags | |
versions | 6552b04e-d074-4773-982b-a2885ce6f9bf, b884c20c-c491-46ec-b438-74384a963acc, 4e8d2a88-1a41-482c-831d-f057a48e18c1 |
steps | bikedaymodel |
BigQuery Sample Orchestration
The orchestration that will automate this process is ./resources/forecast-bigquer-orchestration.zip
. The files used are stored in the directory forecast-bigquery-orchestration
, created with the command:
zip -r forecast-bigquery-connection.zip main.py requirements.txt
.
This contains the following:
requirements.txt
: The Python requirements file to specify the following libraries used:
google-cloud-bigquery==3.10.0
google-auth==2.17.3
db-dtypes==1.1.1
main.py
: The entry file that takes the previous statsmodel BigQuery connection and statsmodel Forecast model and uses it to predict the next month’s sales based on the previous month’s performance. The details are listed below. Since we are using the asyncparallel_infer
, we’ll use theasyncio
library to run our samplemain
method.
import json
import os
import datetime
import asyncio
import wallaroo
from wallaroo.object import EntityNotFoundError
from wallaroo.framework import Framework
import pandas as pd
import numpy as np
pd.set_option('display.max_colwidth', None)
# for Big Query connections
from google.cloud import bigquery
from google.oauth2 import service_account
import db_dtypes
import time
async def main():
wl = wallaroo.Client()
# get the arguments
arguments = wl.task_args()
if "workspace_name" in arguments:
workspace_name = arguments['workspace_name']
else:
workspace_name="multiple-replica-forecast-tutorial"
if "pipeline_name" in arguments:
pipeline_name = arguments['pipeline_name']
else:
pipeline_name="bikedaypipe"
if "bigquery_connection_input_name" in arguments:
bigquery_connection_name = arguments['bigquery_connection_input_name']
else:
bigquery_connection_name = "statsmodel-bike-rentals"
print(bigquery_connection_name)
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
print(f"Pipeline not found:{name}")
return pipeline
print(f"BigQuery Connection: {bigquery_connection_name}")
forecast_connection = wl.get_connection(bigquery_connection_name)
print(f"Workspace: {workspace_name}")
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
print(workspace)
# the pipeline is assumed to be deployed
print(f"Pipeline: {pipeline_name}")
pipeline = get_pipeline(pipeline_name)
print(pipeline)
print("Getting date and input query.")
bigquery_statsmodel_credentials = service_account.Credentials.from_service_account_info(
forecast_connection.details())
bigquery_statsmodel_client = bigquery.Client(
credentials=bigquery_statsmodel_credentials,
project=forecast_connection.details()['project_id']
)
print("Get the current month and retrieve next month's forecasts")
month = datetime.datetime.now().month
start_date = f"{month+1}-1-2011"
print(f"Start date: {start_date}")
def get_forecast_days(firstdate) :
days = [i*7 for i in [-1,0,1,2,3,4]]
deltadays = pd.to_timedelta(pd.Series(days), unit='D')
analysis_days = (pd.to_datetime(firstdate) + deltadays).dt.date
analysis_days = [str(day) for day in analysis_days]
analysis_days
seed_day = analysis_days.pop(0)
return analysis_days
forecast_dates = get_forecast_days(start_date)
print(f"Forecast dates: {forecast_dates}")
# get our list of items to run through
inference_data = []
days = []
# get the days from the start date to the end date
def get_forecast_dates(forecast_day: str, nforecast=7):
days = [i for i in range(nforecast)]
deltadays = pd.to_timedelta(pd.Series(days), unit='D')
last_day = pd.to_datetime(forecast_day)
dates = last_day + deltadays
datestr = dates.dt.date.astype(str)
return datestr
# used to generate our queries
def mk_dt_range_query(*, tablename: str, forecast_day: str) -> str:
assert isinstance(tablename, str)
assert isinstance(forecast_day, str)
query = f"""
select cnt from {tablename} where
dteday >= DATE_SUB(DATE('{forecast_day}'), INTERVAL 1 month)
AND dteday < DATE('{forecast_day}')
ORDER BY dteday
"""
return query
for day in forecast_dates:
print(f"Current date: {day}")
day_range=get_forecast_dates(day)
days.append({"date": day_range})
query = mk_dt_range_query(tablename=f"{forecast_connection.details()['dataset']}.{forecast_connection.details()['input_table']}", forecast_day=day)
print(query)
data = bigquery_statsmodel_client.query(query).to_dataframe().apply({"cnt":int}).to_dict(orient='list')
# add the date into the list
inference_data.append(data)
print(inference_data)
parallel_results = await pipeline.parallel_infer(tensor_list=inference_data, timeout=20, num_parallel=16, retries=2)
days_results = list(zip(days, parallel_results))
print(days_results)
# merge our parallel results into the predicted date sales
results_table = pd.DataFrame(columns=["date", "forecast"])
# match the dates to predictions
# display(days_results)
for date in days_results:
# display(date)
new_days = date[0]['date'].tolist()
new_forecast = date[1][0]['forecast']
new_results = list(zip(new_days, new_forecast))
results_table = results_table.append(pd.DataFrame(list(zip(new_days, new_forecast)), columns=['date','forecast']))
print("Uploading results to results table.")
output_table = bigquery_statsmodel_client.get_table(f"{forecast_connection.details()['dataset']}.{forecast_connection.details()['results_table']}")
bigquery_statsmodel_client.insert_rows_from_dataframe(
output_table,
dataframe=results_table
)
asyncio.run(main())
This orchestration allows a user to specify the workspace, pipeline, and data connection. As long as they all match the previous conditions, then the orchestration will run successfully.
Upload the Orchestration
Orchestrations are uploaded with the Wallaroo client upload_orchestration(path)
method with the following parameters.
Parameter | Type | Description |
---|---|---|
path | string (Required) | The path to the ZIP file to be uploaded. |
Once uploaded, the deployment will be prepared and any requirements will be downloaded and installed.
For this example, the orchestration ./bigquery_remote_inference/bigquery_remote_inference.zip
will be uploaded and saved to the variable orchestration
. Then we will loop until the uploaded orchestration’s status
displays ready
.
orchestration = wl.upload_orchestration(name="statsmodel-orchestration", path="./resources/forecast-bigquery-orchestration.zip")
while orchestration.status() != 'ready':
print(orchestration.status())
time.sleep(5)
pending_packaging
pending_packaging
packaging
packaging
packaging
packaging
packaging
packaging
packaging
packaging
packaging
wl.list_orchestrations()
id | name | status | filename | sha | created at | updated at |
---|---|---|---|---|---|---|
8211497d-292a-4145-b28b-f6364e12544e | statsmodel-orchestration | packaging | forecast-bigquery-orchestration.zip | 44f591...1fa8d6 | 2023-30-Jun 15:45:48 | 2023-30-Jun 15:45:58 |
f8f31494-41c4-4336-bfd6-5b3b1607dedc | statsmodel-orchestration | ready | forecast-bigquery-orchestration.zip | 27ad14...306ad1 | 2023-30-Jun 15:51:08 | 2023-30-Jun 15:51:57 |
fd776f89-ea63-45e9-b8d6-a749074fd579 | statsmodel-orchestration | ready | forecast-bigquery-orchestration.zip | bd6a0e...3a6a09 | 2023-30-Jun 16:45:50 | 2023-30-Jun 16:46:39 |
8200995b-3e33-49f4-ac4f-98ea2b1330db | statsmodel-orchestration | ready | forecast-bigquery-orchestration.zip | 8d0c2f...a3c89f | 2023-30-Jun 15:54:14 | 2023-30-Jun 15:55:07 |
5449a104-abc5-423d-a973-31a3cfdf8b55 | statsmodel-orchestration | ready | forecast-bigquery-orchestration.zip | e00646...45d2a7 | 2023-30-Jun 16:12:39 | 2023-30-Jun 16:13:29 |
9fd1e58c-942d-495b-b3bd-d51f5c03b5ed | statsmodel-orchestration | ready | forecast-bigquery-orchestration.zip | bd6a0e...3a6a09 | 2023-30-Jun 16:48:53 | 2023-30-Jun 16:49:44 |
73f2e90a-13ab-4182-bde1-0fe55c4446cf | statsmodel-orchestration | ready | forecast-bigquery-orchestration.zip | f78c26...f494d9 | 2023-30-Jun 16:27:37 | 2023-30-Jun 16:28:31 |
64b085c7-5317-4152-81c3-c0c77b4f683b | statsmodel-orchestration | ready | forecast-bigquery-orchestration.zip | 37257f...4b4547 | 2023-30-Jun 16:39:49 | 2023-30-Jun 16:40:38 |
4a3a73ab-014c-4aa4-9896-44c313d80daa | statsmodel-orchestration | ready | forecast-bigquery-orchestration.zip | 23bf29...17b780 | 2023-30-Jun 16:52:45 | 2023-30-Jun 16:53:38 |
b4ef4449-9afe-4fba-aaa0-b7fd49687443 | statsmodel-orchestration | ready | forecast-bigquery-orchestration.zip | d4f02b...0e6c5d | 2023-30-Jun 16:42:29 | 2023-30-Jun 16:43:26 |
Create the Task
The orchestration is now ready to be implemented as a Wallaroo Task. We’ll just run it once as an example. This specific Orchestration that creates the Task assumes that the pipeline is deployed, and accepts the arguments:
- workspace_name
- pipeline_name
- bigquery_connection_name
We’ll supply the workspaces, pipeline and connection created in previous steps and stored in the initial variables above. Verify these exist and match the existing workspace, pipeline and connection used in the previous notebooks in this series.
Tasks are generated and run once with the Orchestration run_once(name, json_args, timeout)
method. Any arguments for the orchestration are passed in as a Dict
. If there are no arguments, then an empty set {}
is passed.
task = orchestration.run_once(name="statsmodel single run", json_args={"workspace_name":workspace_name, "pipeline_name": pipeline_name, "bigquery_connection_input_name":connection_name})
Monitor Run with Task Status
We’ll monitor the run first with it’s status.
For this example, the status of the previously created task will be generated, then looped until it has reached status started
.
while task.status() != "started":
display(task.status())
time.sleep(5)
'pending'
‘pending’
display(connection_name)
'statsmodel-bike-rentals-jch'
List Tasks
We’ll use the Wallaroo client list_tasks
method to view the tasks currently running.
wl.list_tasks()
id | name | last run status | type | active | schedule | created at | updated at |
---|---|---|---|---|---|---|---|
c7279e5e-e162-42f8-90ce-b7c0c0bb30f8 | statsmodel single run | running | Temporary Run | True | - | 2023-30-Jun 16:53:41 | 2023-30-Jun 16:53:47 |
a47dbca0-e568-44d3-9715-1fed0f17b9a7 | statsmodel single run | failure | Temporary Run | True | - | 2023-30-Jun 16:49:44 | 2023-30-Jun 16:49:54 |
15c80ad0-537f-4e6a-84c6-6c2f35b5f441 | statsmodel single run | failure | Temporary Run | True | - | 2023-30-Jun 16:46:41 | 2023-30-Jun 16:46:51 |
d0935da6-480a-420d-a70c-570160b0b6b3 | statsmodel single run | failure | Temporary Run | True | - | 2023-30-Jun 16:44:50 | 2023-30-Jun 16:44:56 |
e510e8c5-048b-43b1-9524-974934a9e4f5 | statsmodel single run | failure | Temporary Run | True | - | 2023-30-Jun 16:43:30 | 2023-30-Jun 16:43:35 |
0f62befb-c788-4779-bcfb-0595e3ca6f24 | statsmodel single run | failure | Temporary Run | True | - | 2023-30-Jun 16:40:39 | 2023-30-Jun 16:40:50 |
f00c6a97-32f9-4124-bf86-34a0068c1314 | statsmodel single run | failure | Temporary Run | True | - | 2023-30-Jun 16:28:32 | 2023-30-Jun 16:28:38 |
10c8af33-8ff4-4aae-b08d-89665bcb0481 | statsmodel single run | failure | Temporary Run | True | - | 2023-30-Jun 16:13:30 | 2023-30-Jun 16:13:35 |
9ae4e6e6-3849-4039-acfe-6810699edef8 | statsmodel single run | failure | Temporary Run | True | - | 2023-30-Jun 16:00:05 | 2023-30-Jun 16:00:15 |
Display Task Run Results
The Task Run is the implementation of the task - the actual running of the script and it’s results. Tasks that are Run Once will only have one Task Run, while a Task set to Run Scheduled will have a Task Run for each time the task is executed. Each Task Run has its own set of logs and results that are monitoried through the Task Run logs()
method.
We’ll wait 30 seconds, then retrieve the task run for our generated task, then start checking the logs for our task run. It may take longer than 30 seconds to launch the task, so be prepared to run the .logs()
method again to view the logs.
#wait 30 seconds for the task to finish
time.sleep(30)
statsmodel_task_run = task.last_runs()[0]
statsmodel_task_run.logs()
2023-30-Jun 16:53:57 statsmodel-bike-rentals-jch
2023-30-Jun 16:53:57 BigQuery Connection: statsmodel-bike-rentals-jch
2023-30-Jun 16:53:57 Workspace: multiple-replica-forecast-tutorial-jch
2023-30-Jun 16:53:57 {'name': 'multiple-replica-forecast-tutorial-jch', 'id': 7, 'archived': False, 'created_by': '34b86cac-021e-4cf0-aa30-40da7db5a77f', 'created_at': '2023-06-30T15:42:56.551195+00:00', 'models': [{'name': 'bikedaymodel', 'versions': 1, 'owner_id': '""', 'last_update_time': datetime.datetime(2023, 6, 30, 15, 42, 56, 979723, tzinfo=tzutc()), 'created_at': datetime.datetime(2023, 6, 30, 15, 42, 56, 979723, tzinfo=tzutc())}], 'pipelines': [{'name': 'bikedaypipe', 'create_time': datetime.datetime(2023, 6, 30, 15, 42, 56, 781150, tzinfo=tzutc()), 'definition': '[]'}]}
2023-30-Jun 16:53:57 Pipeline: bikedaypipe
2023-30-Jun 16:53:57 {'name': 'bikedaypipe', 'create_time': datetime.datetime(2023, 6, 30, 15, 42, 56, 781150, tzinfo=tzutc()), 'definition': '[]'}
2023-30-Jun 16:53:57 Getting date and input query.
2023-30-Jun 16:53:57 Get the current month and retrieve next month's forecasts
2023-30-Jun 16:53:57 Start date: 7-1-2011
2023-30-Jun 16:53:57 Forecast dates: ['2011-07-01', '2011-07-08', '2011-07-15', '2011-07-22', '2011-07-29']
2023-30-Jun 16:53:57 Current date: 2011-07-01
2023-30-Jun 16:53:57
2023-30-Jun 16:53:57 select cnt from release_testing_2023_2.bike_rentals where
2023-30-Jun 16:53:57 dteday >= DATE_SUB(DATE('2011-07-01'), INTERVAL 1 month)
2023-30-Jun 16:53:57 AND dteday < DATE('2011-07-01')
2023-30-Jun 16:53:57 ORDER BY dteday
2023-30-Jun 16:53:57 Current date: 2011-07-08
2023-30-Jun 16:53:57
2023-30-Jun 16:53:57
2023-30-Jun 16:53:57 select cnt from release_testing_2023_2.bike_rentals where
2023-30-Jun 16:53:57 dteday >= DATE_SUB(DATE('2011-07-08'), INTERVAL 1 month)
2023-30-Jun 16:53:57 ORDER BY dteday
2023-30-Jun 16:53:57 AND dteday < DATE('2011-07-08')
2023-30-Jun 16:53:57
2023-30-Jun 16:53:57 Current date: 2011-07-15
2023-30-Jun 16:53:57
2023-30-Jun 16:53:57 select cnt from release_testing_2023_2.bike_rentals where
2023-30-Jun 16:53:57 dteday >= DATE_SUB(DATE('2011-07-15'), INTERVAL 1 month)
2023-30-Jun 16:53:57 ORDER BY dteday
2023-30-Jun 16:53:57 AND dteday < DATE('2011-07-15')
2023-30-Jun 16:53:57
2023-30-Jun 16:53:57 Current date: 2011-07-22
2023-30-Jun 16:53:57
2023-30-Jun 16:53:57 select cnt from release_testing_2023_2.bike_rentals where
2023-30-Jun 16:53:57 dteday >= DATE_SUB(DATE('2011-07-22'), INTERVAL 1 month)
2023-30-Jun 16:53:57 AND dteday < DATE('2011-07-22')
2023-30-Jun 16:53:57 ORDER BY dteday
2023-30-Jun 16:53:57
2023-30-Jun 16:53:57 Current date: 2011-07-29
2023-30-Jun 16:53:57 select cnt from release_testing_2023_2.bike_rentals where
2023-30-Jun 16:53:57
2023-30-Jun 16:53:57 dteday >= DATE_SUB(DATE('2011-07-29'), INTERVAL 1 month)
2023-30-Jun 16:53:57 ORDER BY dteday
2023-30-Jun 16:53:57 AND dteday < DATE('2011-07-29')
2023-30-Jun 16:53:57
2023-30-Jun 16:53:57 [({'date': 0 2011-07-01
2023-30-Jun 16:53:57 [{'cnt': [3974, 4968, 5312, 5342, 4906, 4548, 4833, 4401, 3915, 4586, 4966, 4460, 5020, 4891, 5180, 3767, 4844, 5119, 4744, 4010, 4835, 4507, 4790, 4991, 5202, 5305, 4708, 4648, 5225, 5515]}, {'cnt': [4401, 3915, 4586, 4966, 4460, 5020, 4891, 5180, 3767, 4844, 5119, 4744, 4010, 4835, 4507, 4790, 4991, 5202, 5305, 4708, 4648, 5225, 5515, 5362, 5119, 4649, 6043, 4665, 4629, 4592]}, {'cnt': [5180, 3767, 4844, 5119, 4744, 4010, 4835, 4507, 4790, 4991, 5202, 5305, 4708, 4648, 5225, 5515, 5362, 5119, 4649, 6043, 4665, 4629, 4592, 4040, 5336, 4881, 4086, 4258, 4342, 5084]}, {'cnt': [4507, 4790, 4991, 5202, 5305, 4708, 4648, 5225, 5515, 5362, 5119, 4649, 6043, 4665, 4629, 4592, 4040, 5336, 4881, 4086, 4258, 4342, 5084, 5538, 5923, 5302, 4458, 4541, 4332, 3784]}, {'cnt': [5225, 5515, 5362, 5119, 4649, 6043, 4665, 4629, 4592, 4040, 5336, 4881, 4086, 4258, 4342, 5084, 5538, 5923, 5302, 4458, 4541, 4332, 3784, 3387, 3285, 3606, 3840, 4590, 4656, 4390]}]
2023-30-Jun 16:53:57 1 2011-07-02
2023-30-Jun 16:53:57 2 2011-07-03
2023-30-Jun 16:53:57 3 2011-07-04
2023-30-Jun 16:53:57 4 2011-07-05
2023-30-Jun 16:53:57 5 2011-07-06
2023-30-Jun 16:53:57 6 2011-07-07
2023-30-Jun 16:53:57 dtype: object}, [{'forecast': [4894, 4767, 4786, 4783, 4783, 4783, 4783]}]), ({'date': 0 2011-07-08
2023-30-Jun 16:53:57 2 2011-07-10
2023-30-Jun 16:53:57 1 2011-07-09
2023-30-Jun 16:53:57 4 2011-07-12
2023-30-Jun 16:53:57 3 2011-07-11
2023-30-Jun 16:53:57 5 2011-07-13
2023-30-Jun 16:53:57 6 2011-07-14
2023-30-Jun 16:53:57 dtype: object}, [{'forecast': [4842, 4839, 4836, 4833, 4831, 4830, 4828]}]), ({'date': 0 2011-07-15
2023-30-Jun 16:53:57 1 2011-07-16
2023-30-Jun 16:53:57 2 2011-07-17
2023-30-Jun 16:53:57 3 2011-07-18
2023-30-Jun 16:53:57 4 2011-07-19
2023-30-Jun 16:53:57 5 2011-07-20
2023-30-Jun 16:53:57 6 2011-07-21
2023-30-Jun 16:53:57 dtype: object}, [{'forecast': [4895, 4759, 4873, 4777, 4858, 4789, 4848]}]), ({'date': 0 2011-07-22
2023-30-Jun 16:53:57 1 2011-07-23
2023-30-Jun 16:53:57 2 2011-07-24
2023-30-Jun 16:53:57 3 2011-07-25
2023-30-Jun 16:53:57 5 2011-07-27
2023-30-Jun 16:53:57 4 2011-07-26
2023-30-Jun 16:53:57 6 2011-07-28
2023-30-Jun 16:53:57 dtype: object}, [{'forecast': [4559, 4953, 4829, 4868, 4856, 4860, 4858]}]), ({'date': 0 2011-07-29
2023-30-Jun 16:53:57 1 2011-07-30
2023-30-Jun 16:53:57 3 2011-08-01
2023-30-Jun 16:53:57 2 2011-07-31
2023-30-Jun 16:53:57 5 2011-08-03
2023-30-Jun 16:53:57 4 2011-08-02
2023-30-Jun 16:53:57 6 2011-08-04
2023-30-Jun 16:53:57 dtype: object}, [{'forecast': [4490, 4549, 4586, 4610, 4624, 4634, 4640]}])]
2023-30-Jun 16:53:57 Uploading results to results table.
Undeploy the Pipeline
Undeploy the pipeline and return the resources back to the Wallaroo instance.
pipeline.undeploy()
Waiting for undeployment - this will take up to 45s ..................................... ok
name | bikedaypipe |
---|---|
created | 2023-06-30 15:42:56.781150+00:00 |
last_updated | 2023-06-30 15:45:23.267621+00:00 |
deployed | False |
tags | |
versions | 6552b04e-d074-4773-982b-a2885ce6f9bf, b884c20c-c491-46ec-b438-74384a963acc, 4e8d2a88-1a41-482c-831d-f057a48e18c1 |
steps | bikedaymodel |
4.8 - Tags Tutorial
This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.
Wallaroo SDK Tag Tutorial
The following tutorial demonstrates how to use Wallaroo Tags. Tags are applied to either model versions or pipelines. This allows organizations to track different versions of models, and search for what pipelines have been used for specific purposes such as testing versus production use.
The following will be demonstrated:
- List all tags in a Wallaroo instance.
- List all tags applied to a model.
- List all tags applied to a pipeline.
- Apply a tag to a model.
- Remove a tag from a model.
- Apply a tag to a pipeline.
- Remove a tag from a pipeline.
- Search for a model version by a tag.
- Search for a pipeline by a tag.
This demonstration provides the following through the Wallaroo Tutorials Github Repository:
models/ccfraud.onnx
: a sample model used as part of the Wallaroo 101 Tutorials.
Prerequisites
- A deployed Wallaroo instance
- The following Python libraries installed:
os
string
random
wallaroo
: The Wallaroo SDK. Included with the Wallaroo JupyterHub service by default.
Steps
The following steps are performed use to connect to a Wallaroo instance and demonstrate how to use tags with models and pipelines.
Load Libraries
The first step is to load the libraries used to connect and use a Wallaroo instance.
import wallaroo
from wallaroo.object import EntityNotFoundError
import pandas as pd
# used to display dataframe information without truncating
from IPython.display import display
pd.set_option('display.max_colwidth', None)
Connect to the Wallaroo Instance
The first step is to connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. For more information on Wallaroo Client settings, see the Client Connection guide.
# Client connection from local Wallaroo instance
wl = wallaroo.Client()
Set Variables
The following variables are used to create or connect to existing workspace and pipeline. The model name and model file are set as well. Adjust as required for your organization’s needs.
The methods get_workspace
and get_pipeline
are used to either create a new workspace and pipeline based on the variables below, or connect to an existing workspace and pipeline with the same name. Once complete, the workspace will be set as the current workspace where pipelines and models are used.
To allow this tutorial to be run multiple times or by multiple users in the same Wallaroo instance, a random 4 character prefix will be added to the workspace, pipeline, and model.
import string
import random
# make a random 4 character prefix
prefix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
workspace_name = f'{prefix}tagtestworkspace'
pipeline_name = f'{prefix}tagtestpipeline'
model_name = f'{prefix}tagtestmodel'
model_file_name = './models/ccfraud.onnx'
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
pipeline = wl.build_pipeline(name)
return pipeline
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
{'name': 'rehqtagtestworkspace', 'id': 24, 'archived': False, 'created_by': '028c8b48-c39b-4578-9110-0b5bdd3824da', 'created_at': '2023-05-17T21:56:18.63721+00:00', 'models': [], 'pipelines': []}
Upload Model and Create Pipeline
The tagtest_model
and tagtest_pipeline
will be created (or connected if already existing) based on the variables set earlier.
tagtest_model = wl.upload_model(model_name, model_file_name, framework=wallaroo.framework.Framework.ONNX).configure()
tagtest_model
{'name': 'rehqtagtestmodel', 'version': '53febe9a-bb4b-4a01-a6a2-a17f943d6652', 'file_name': 'ccfraud.onnx', 'image_path': None, 'last_update_time': datetime.datetime(2023, 5, 17, 21, 56, 20, 208454, tzinfo=tzutc())}
tagtest_pipeline = get_pipeline(pipeline_name)
tagtest_pipeline
name | rehqtagtestpipeline |
---|---|
created | 2023-05-17 21:56:21.405556+00:00 |
last_updated | 2023-05-17 21:56:21.405556+00:00 |
deployed | (none) |
tags | |
versions | e259f6db-8ce2-45f1-b2d7-a719fde3b18f |
steps |
List Pipeline and Model Tags
This tutorial assumes that no tags are currently existing, but that can be verified through the Wallaroo client list_pipelines
and list_models
commands. For this demonstration, it is recommended to use unique tags to verify each example.
wl.list_pipelines()
name | created | last_updated | deployed | tags | versions | steps |
---|---|---|---|---|---|---|
rehqtagtestpipeline | 2023-17-May 21:56:21 | 2023-17-May 21:56:21 | (unknown) | e259f6db-8ce2-45f1-b2d7-a719fde3b18f | ||
osysapiinferenceexamplepipeline | 2023-17-May 21:54:56 | 2023-17-May 21:54:56 | False | 8f244f23-73f9-4af2-a95e-2a03214dca63 | osysccfraud | |
fvqusdkinferenceexamplepipeline | 2023-17-May 21:53:14 | 2023-17-May 21:53:15 | False | a987e13f-ffbe-4826-a6f5-9fd8de9f47fa, 0966d243-ce76-4132-aa69-0d287ae9a572 | fvquccfraud | |
gobtedgepipelineexample | 2023-17-May 21:50:13 | 2023-17-May 21:51:06 | False | dc0238e7-f3e3-4579-9a63-24902cb3e3bd, 5cf788a6-50ff-471f-a3ee-4bfdc24def34, 9efda57b-c18b-4ebb-9681-33647e7d7e66 | gobtalohamodel | |
logpipeline | 2023-17-May 21:41:06 | 2023-17-May 21:46:51 | False | 66fb765b-d46c-4472-9976-dba2eac5b8ce, 328b2b59-7a57-403b-abd5-70708a67674e, 18eb212d-0af5-4c0b-8bdb-3abbc4907a3e, c39b5215-0535-4006-a26a-d78b1866435b | logcontrol | |
btffhotswappipeline | 2023-17-May 21:37:16 | 2023-17-May 21:37:39 | False | 438796a3-e320-4a51-9e64-35eb32d57b49, 4fc11650-1003-43c2-bd3a-96b9cdacbb6d, e4b8d7ca-00fa-4e31-8671-3d0a3bf4c16e, 3c5f951b-e815-4bc7-93bf-84de3d46718d | btffhousingmodelcontrol | |
qjjoccfraudpipeline | 2023-17-May 21:32:06 | 2023-17-May 21:32:08 | False | 89b634d6-f538-4ac6-98a2-fbb9883fdeb6, c0f8551d-cefe-49c8-8701-c2a307c0ad99 | qjjoccfraudmodel | |
housing-pipe | 2023-17-May 21:26:56 | 2023-17-May 21:29:05 | False | 34e75a0c-01bd-4ca2-a6e8-ebdd25473aab, b7dbd380-e48c-487c-8f23-398a2ba558c3, 5ea6f182-5764-4377-9f83-d363e349ef32 | preprocess | |
xgboost-regression-autoconvert-pipeline | 2023-17-May 21:21:56 | 2023-17-May 21:21:59 | False | f5337089-2756-469a-871a-1cb9e3416847, 324433ae-db9a-4d43-9563-ff76df59953d | xgb-regression-model | |
xgboost-classification-autoconvert-pipeline | 2023-17-May 21:21:19 | 2023-17-May 21:21:22 | False | 5f7bb0cc-f60d-4cee-8425-c5e85331ae2f, bbe4dce4-f62a-4f4f-a45c-aebbfce23304 | xgb-class-model | |
statsmodelpipeline | 2023-17-May 21:19:52 | 2023-17-May 21:19:55 | False | 4af264e3-f427-4b02-b5ad-4f6690b0ee06, 5456dd2a-3167-4b3c-ad3a-85544292a230 | bikedaymodel | |
isoletpipeline | 2023-17-May 21:17:33 | 2023-17-May 21:17:44 | False | c129b33c-cefc-4873-ad2c-d186fe2b8228, 145b768e-79f2-44fd-ab6b-14d675501b83 | isolettest | |
externalkerasautoconvertpipeline | 2023-17-May 21:13:27 | 2023-17-May 21:13:30 | False | 7be0dd01-ef82-4335-b60d-6f1cd5287e5b, 3948e0dc-d591-4ff5-a48f-b8d17195a806 | externalsimple-sentiment-model | |
gcpsdkpipeline | 2023-17-May 21:03:44 | 2023-17-May 21:03:49 | False | 6398cafc-50c4-49e3-9499-6025b7808245, 7c043d3c-c894-4ae9-9ec1-c35518130b90 | gcpsdkmodel | |
databricksazuresdkpipeline | 2023-17-May 21:02:55 | 2023-17-May 21:02:59 | False | f125dc67-f690-4011-986a-8f6a9a23c48a, 8c4a15b4-2ef0-4da1-8e2d-38088fde8c56 | ccfraudmodel | |
azuremlsdkpipeline | 2023-17-May 21:01:46 | 2023-17-May 21:01:51 | False | 28a7a5aa-5359-4320-842b-bad84258f7e4, e011272d-c22c-4b2d-ab9f-b17c60099434 | azuremlsdkmodel | |
copiedmodelpipeline | 2023-17-May 20:54:01 | 2023-17-May 20:54:01 | (unknown) | bcf5994f-1729-4036-a910-00b662946801 | ||
pipelinemodels | 2023-17-May 20:52:06 | 2023-17-May 20:52:06 | False | 55f45c16-591e-4a16-8082-3ab6d843b484 | apimodel | |
pipelinenomodel | 2023-17-May 20:52:04 | 2023-17-May 20:52:04 | (unknown) | a6dd2cee-58d6-4d24-9e25-f531dbbb95ad | ||
sdkquickpipeline | 2023-17-May 20:43:38 | 2023-17-May 20:46:02 | False | 961c909d-f5ae-472a-b8ae-1e6a00fbc36e, bf7c2146-ed14-430b-bf96-1e8b1047eb2e, 2bd5c838-f7cc-4f48-91ea-28a9ce0f7ed8, d72c468a-a0e2-4189-aa7a-4e27127a2f2b | sdkquickmodel | |
housepricepipe | 2023-17-May 20:41:50 | 2023-17-May 20:41:50 | False | 4d9dfb3b-c9ae-402a-96fc-20ae0a2b2279, fc68f5f2-7bbf-435e-b434-e0c89c28c6a9 | housepricemodel |
wl.list_models()
Name | # of Versions | Owner ID | Last Updated | Created At |
---|---|---|---|---|
rehqtagtestmodel | 1 | "" | 2023-05-17 21:56:20.208454+00:00 | 2023-05-17 21:56:20.208454+00:00 |
Create Tag
Tags are created with the Wallaroo client command create_tag(String tagname)
. This creates the tag and makes it available for use.
The tag will be saved to the variable currentTag
to be used in the rest of these examples.
# Now we create our tag
currentTag = wl.create_tag("My Great Tag")
List Tags
Tags are listed with the Wallaroo client command list_tags()
, which shows all tags and what models and pipelines they have been assigned to. Note that if a tag has not been assigned, it will not be displayed.
# List all tags
wl.list_tags()
(no tags)
Assign Tag to a Model
Tags are assigned to a model through the Wallaroo Tag add_to_model(model_id)
command, where model_id
is the model’s numerical ID number. The tag is applied to the most current version of the model.
For this example, the currentTag
will be applied to the tagtest_model
. All tags will then be listed to show it has been assigned to this model.
# add tag to model
currentTag.add_to_model(tagtest_model.id())
{'model_id': 29, 'tag_id': 1}
# list all tags to verify
wl.list_tags()
id | tag | models | pipelines |
---|---|---|---|
1 | My Great Tag | [('rehqtagtestmodel', ['53febe9a-bb4b-4a01-a6a2-a17f943d6652'])] | [] |
Search Models by Tag
Model versions can be searched via tags using the Wallaroo Client method search_models(search_term)
, where search_term
is a string value. All models versions containing the tag will be displayed. In this example, we will be using the text from our tag to list all models that have the text from currentTag
in them.
# Search models by tag
wl.search_models('My Great Tag')
name | version | file_name | image_path | last_update_time |
---|---|---|---|---|
rehqtagtestmodel | 53febe9a-bb4b-4a01-a6a2-a17f943d6652 | ccfraud.onnx | None | 2023-05-17 21:56:20.208454+00:00 |
Remove Tag from Model
Tags are removed from models using the Wallaroo Tag remove_from_model(model_id)
command.
In this example, the currentTag
will be removed from tagtest_model
. A list of all tags will be shown with the list_tags
command, followed by searching the models for the tag to verify it has been removed.
### remove tag from model
currentTag.remove_from_model(tagtest_model.id())
{'model_id': 29, 'tag_id': 1}
# list all tags to verify it has been removed from `tagtest_model`.
wl.list_tags()
(no tags)
# search models for currentTag to verify it has been removed from `tagtest_model`.
wl.search_models('My Great Tag')
(no model versions)
Add Tag to Pipeline
Tags are added to a pipeline through the Wallaroo Tag add_to_pipeline(pipeline_id)
method, where pipeline_id
is the pipeline’s integer id.
For this example, we will add currentTag
to testtest_pipeline
, then verify it has been added through the list_tags
command and list_pipelines
command.
# add this tag to the pipeline
currentTag.add_to_pipeline(tagtest_pipeline.id())
{'pipeline_pk_id': 45, 'tag_pk_id': 1}
# list tags to verify it was added to tagtest_pipeline
wl.list_tags()
id | tag | models | pipelines |
---|---|---|---|
1 | My Great Tag | [] | [('rehqtagtestpipeline', ['e259f6db-8ce2-45f1-b2d7-a719fde3b18f'])] |
# get all of the pipelines to show the tag was added to tagtest-pipeline
wl.list_pipelines()
name | created | last_updated | deployed | tags | versions | steps |
---|---|---|---|---|---|---|
rehqtagtestpipeline | 2023-17-May 21:56:21 | 2023-17-May 21:56:21 | (unknown) | My Great Tag | e259f6db-8ce2-45f1-b2d7-a719fde3b18f | |
osysapiinferenceexamplepipeline | 2023-17-May 21:54:56 | 2023-17-May 21:54:56 | False | 8f244f23-73f9-4af2-a95e-2a03214dca63 | osysccfraud | |
fvqusdkinferenceexamplepipeline | 2023-17-May 21:53:14 | 2023-17-May 21:53:15 | False | a987e13f-ffbe-4826-a6f5-9fd8de9f47fa, 0966d243-ce76-4132-aa69-0d287ae9a572 | fvquccfraud | |
gobtedgepipelineexample | 2023-17-May 21:50:13 | 2023-17-May 21:51:06 | False | dc0238e7-f3e3-4579-9a63-24902cb3e3bd, 5cf788a6-50ff-471f-a3ee-4bfdc24def34, 9efda57b-c18b-4ebb-9681-33647e7d7e66 | gobtalohamodel | |
logpipeline | 2023-17-May 21:41:06 | 2023-17-May 21:46:51 | False | 66fb765b-d46c-4472-9976-dba2eac5b8ce, 328b2b59-7a57-403b-abd5-70708a67674e, 18eb212d-0af5-4c0b-8bdb-3abbc4907a3e, c39b5215-0535-4006-a26a-d78b1866435b | logcontrol | |
btffhotswappipeline | 2023-17-May 21:37:16 | 2023-17-May 21:37:39 | False | 438796a3-e320-4a51-9e64-35eb32d57b49, 4fc11650-1003-43c2-bd3a-96b9cdacbb6d, e4b8d7ca-00fa-4e31-8671-3d0a3bf4c16e, 3c5f951b-e815-4bc7-93bf-84de3d46718d | btffhousingmodelcontrol | |
qjjoccfraudpipeline | 2023-17-May 21:32:06 | 2023-17-May 21:32:08 | False | 89b634d6-f538-4ac6-98a2-fbb9883fdeb6, c0f8551d-cefe-49c8-8701-c2a307c0ad99 | qjjoccfraudmodel | |
housing-pipe | 2023-17-May 21:26:56 | 2023-17-May 21:29:05 | False | 34e75a0c-01bd-4ca2-a6e8-ebdd25473aab, b7dbd380-e48c-487c-8f23-398a2ba558c3, 5ea6f182-5764-4377-9f83-d363e349ef32 | preprocess | |
xgboost-regression-autoconvert-pipeline | 2023-17-May 21:21:56 | 2023-17-May 21:21:59 | False | f5337089-2756-469a-871a-1cb9e3416847, 324433ae-db9a-4d43-9563-ff76df59953d | xgb-regression-model | |
xgboost-classification-autoconvert-pipeline | 2023-17-May 21:21:19 | 2023-17-May 21:21:22 | False | 5f7bb0cc-f60d-4cee-8425-c5e85331ae2f, bbe4dce4-f62a-4f4f-a45c-aebbfce23304 | xgb-class-model | |
statsmodelpipeline | 2023-17-May 21:19:52 | 2023-17-May 21:19:55 | False | 4af264e3-f427-4b02-b5ad-4f6690b0ee06, 5456dd2a-3167-4b3c-ad3a-85544292a230 | bikedaymodel | |
isoletpipeline | 2023-17-May 21:17:33 | 2023-17-May 21:17:44 | False | c129b33c-cefc-4873-ad2c-d186fe2b8228, 145b768e-79f2-44fd-ab6b-14d675501b83 | isolettest | |
externalkerasautoconvertpipeline | 2023-17-May 21:13:27 | 2023-17-May 21:13:30 | False | 7be0dd01-ef82-4335-b60d-6f1cd5287e5b, 3948e0dc-d591-4ff5-a48f-b8d17195a806 | externalsimple-sentiment-model | |
gcpsdkpipeline | 2023-17-May 21:03:44 | 2023-17-May 21:03:49 | False | 6398cafc-50c4-49e3-9499-6025b7808245, 7c043d3c-c894-4ae9-9ec1-c35518130b90 | gcpsdkmodel | |
databricksazuresdkpipeline | 2023-17-May 21:02:55 | 2023-17-May 21:02:59 | False | f125dc67-f690-4011-986a-8f6a9a23c48a, 8c4a15b4-2ef0-4da1-8e2d-38088fde8c56 | ccfraudmodel | |
azuremlsdkpipeline | 2023-17-May 21:01:46 | 2023-17-May 21:01:51 | False | 28a7a5aa-5359-4320-842b-bad84258f7e4, e011272d-c22c-4b2d-ab9f-b17c60099434 | azuremlsdkmodel | |
copiedmodelpipeline | 2023-17-May 20:54:01 | 2023-17-May 20:54:01 | (unknown) | bcf5994f-1729-4036-a910-00b662946801 | ||
pipelinemodels | 2023-17-May 20:52:06 | 2023-17-May 20:52:06 | False | 55f45c16-591e-4a16-8082-3ab6d843b484 | apimodel | |
pipelinenomodel | 2023-17-May 20:52:04 | 2023-17-May 20:52:04 | (unknown) | a6dd2cee-58d6-4d24-9e25-f531dbbb95ad | ||
sdkquickpipeline | 2023-17-May 20:43:38 | 2023-17-May 20:46:02 | False | 961c909d-f5ae-472a-b8ae-1e6a00fbc36e, bf7c2146-ed14-430b-bf96-1e8b1047eb2e, 2bd5c838-f7cc-4f48-91ea-28a9ce0f7ed8, d72c468a-a0e2-4189-aa7a-4e27127a2f2b | sdkquickmodel | |
housepricepipe | 2023-17-May 20:41:50 | 2023-17-May 20:41:50 | False | 4d9dfb3b-c9ae-402a-96fc-20ae0a2b2279, fc68f5f2-7bbf-435e-b434-e0c89c28c6a9 | housepricemodel |
Search Pipelines by Tag
Pipelines can be searched through the Wallaroo Client search_pipelines(search_term)
method, where search_term
is a string value for tags assigned to the pipelines.
In this example, the text “My Great Tag” that corresponds to currentTag
will be searched for and displayed.
wl.search_pipelines('My Great Tag')
name | version | creation_time | last_updated_time | deployed | tags | steps |
---|---|---|---|---|---|---|
rehqtagtestpipeline | e259f6db-8ce2-45f1-b2d7-a719fde3b18f | 2023-17-May 21:56:21 | 2023-17-May 21:56:21 | (unknown) | My Great Tag |
Remove Tag from Pipeline
Tags are removed from a pipeline with the Wallaroo Tag remove_from_pipeline(pipeline_id)
command, where pipeline_id
is the integer value of the pipeline’s id.
For this example, currentTag
will be removed from tagtest_pipeline
. This will be verified through the list_tags
and search_pipelines
command.
## remove from pipeline
currentTag.remove_from_pipeline(tagtest_pipeline.id())
{'pipeline_pk_id': 45, 'tag_pk_id': 1}
wl.list_tags()
(no tags)
## Verify it was removed
wl.search_pipelines('My Great Tag')
(no pipelines)
4.9 - Large Language Model with GPU Pipeline Deployment in Wallaroo Demonstration
This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.
Large Language Model with GPU Pipeline Deployment in Wallaroo Demonstration
Wallaroo supports the use of GPUs for model deployment and inferences. This demonstration demonstrates using a Hugging Face Large Language Model (LLM) stored in a registry service that creates summaries of larger text strings.
Tutorial Goals
For this demonstration, a cluster with GPU resources will be hosting the Wallaroo instance.
- The containerized model
hf-bart-summarizer3
will be registered to a Wallaroo workspace. - The model will be added as a step to a Wallaroo pipeline.
- When the pipeline is deployed, the deployment configuration will specify the allocation of a GPU to the pipeline.
- A sample inference summarizing a set of text is used as an inference input, and the sample results and time period displayed.
Prerequisites
The following is required for this tutorial:
- A Wallaroo Enterprise version 2023.2.1 or greater instance installed into a GPU enabled Kubernetes cluster as described in the Wallaroo Create GPU Nodepools Kubernetes Clusters guide.
- The Wallaroo SDK version 2023.2.1 or greater.
References
- Wallaroo SDK Essentials Guide: Pipeline Deployment Configuration
- Wallaroo SDK Reference wallaroo.deployment_config
Tutorial Steps
Import Libraries
The first step is to import the libraries we’ll be using. These are included by default in the Wallaroo instance’s JupyterHub service.
import json
import os
import pickle
import wallaroo
from wallaroo.pipeline import Pipeline
from wallaroo.deployment_config import DeploymentConfigBuilder
from wallaroo.framework import Framework
import pyarrow as pa
import numpy as np
import pandas as pd
from sklearn.datasets import load_iris
from sklearn.cluster import KMeans
Connect to the Wallaroo Instance through the User Interface
The next step is to connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. For more information on Wallaroo Client settings, see the Client Connection guide.
wl = wallaroo.Client()
Register MLFlow Model in Wallaroo
MLFlow Containerized Model require the input and output schemas be defined in Apache Arrow format. Both the input and output schema is a string.
Once complete, the MLFlow containerized model is registered to the Wallaroo workspace.
input_schema = pa.schema([
pa.field('inputs', pa.string())
])
output_schema = pa.schema([
pa.field('summary_text', pa.string()),
])
model = wl.register_model_image(
name="hf-bart-summarizer3",
image=f"sampleregistry.com/gpu-hf-summ-official2:1.30"
).configure("mlflow", input_schema=input_schema, output_schema=output_schema)
model
Name | hf-bart-summarizer3 |
Version | d511a20c-9612-4112-9368-2d79ae764dec |
File Name | none |
SHA | 360dcd343a593e87639106757bad58a7d960899c915bbc9787e7601073bc1121 |
Status | ready |
Image Path | proxy.replicated.com/proxy/wallaroo/ghcr.io/wallaroolabs/gpu-hf-summ-official2:1.30 |
Updated At | 2023-11-Jul 19:23:57 |
Pipeline Deployment With GPU
The registered model will be added to our sample pipeline as a pipeline step. When the pipeline is deployed, a specific resource configuration is applied that allocated a GPU to our MLFlow containerized model.
MLFlow models are run in the Containerized Runtime in the pipeline. As such, the DeploymentConfigBuilder
method .sidekick_gpus(model: wallaroo.model.Model, core_count: int)
is used to allocate 1 GPU to our model.
The pipeline is then deployed with our deployment configuration, and a GPU from the cluster is allocated for use by this model.
If DeploymentConfigBuilder.gpus
or DeploymentConfigBuilder.gpus
are called, then DeploymentConfigBuilder().deployment_label(label:string)
must be called with the label set to the same name as the GPU nodepool. See Create GPU Nodepools for Kubernetes Clusters for more details on GPU nodepools with Wallaroo.
pipeline_name = f"test-gpu7"
pipeline = wl.build_pipeline(pipeline_name)
pipeline.add_model_step(model)
deployment_config = DeploymentConfigBuilder() \
.cpus(0.25).memory('1Gi').gpus(0) \
.sidekick_gpus(model, 1) \
.sidekick_env(model, {"GUNICORN_CMD_ARGS": "--timeout=180 --workers=1"}) \
.image("proxy.replicated.com/proxy/wallaroo/ghcr.io/wallaroolabs/fitzroy-mini-cuda:v2023.3.0-josh-fitzroy-gpu-3374") \
.deployment_label('doc-gpu-label:true') \
.build()
deployment_config
{'engine': {'cpu': 0.25,
'resources': {'limits': {'cpu': 0.25, 'memory': '1Gi', 'nvidia.com/gpu': 0},
'requests': {'cpu': 0.25, 'memory': '1Gi', 'nvidia.com/gpu': 0}},
'gpu': 0,
'deployment_label': 'doc-gpu-label:true',
'image': 'proxy.replicated.com/proxy/wallaroo/ghcr.io/wallaroolabs/fitzroy-mini-cuda:v2023.3.0-josh-fitzroy-gpu-3374'},
'enginelb': {},
'engineAux': {'images': {'hf-bart-summarizer3-28': {'resources': {'limits': {'nvidia.com/gpu': 1},
'requests': {'nvidia.com/gpu': 1}},
'env': [{'name': 'GUNICORN_CMD_ARGS',
'value': '--timeout=180 --workers=1'}]}}},
'node_selector': {}}
pipeline.deploy(deployment_config=deployment_config)
pipeline.status()
Waiting for deployment - this will take up to 90s ................ ok
{‘status’: ‘Running’,
‘details’: [],
’engines’: [{‘ip’: ‘10.244.38.26’,
’name’: ’engine-7457c88db4-42ww6’,
‘status’: ‘Running’,
‘reason’: None,
‘details’: [],
‘pipeline_statuses’: {‘pipelines’: [{‘id’: ’test-gpu7’,
‘status’: ‘Running’}]},
‘model_statuses’: {‘models’: [{’name’: ‘hf-bart-summarizer3’,
‘version’: ‘d511a20c-9612-4112-9368-2d79ae764dec’,
‘sha’: ‘360dcd343a593e87639106757bad58a7d960899c915bbc9787e7601073bc1121’,
‘status’: ‘Running’}]}}],
’engine_lbs’: [{‘ip’: ‘10.244.0.113’,
’name’: ’engine-lb-584f54c899-ht5cd’,
‘status’: ‘Running’,
‘reason’: None,
‘details’: []}],
‘sidekicks’: [{‘ip’: ‘10.244.41.21’,
’name’: ’engine-sidekick-hf-bart-summarizer3-28-f5f8d6567-zzh62’,
‘status’: ‘Running’,
‘reason’: None,
‘details’: [],
‘statuses’: ‘\n’}]}
pipeline.status()
{'status': 'Running',
'details': [],
'engines': [{'ip': '10.244.38.26',
'name': 'engine-7457c88db4-42ww6',
'status': 'Running',
'reason': None,
'details': [],
'pipeline_statuses': {'pipelines': [{'id': 'test-gpu7',
'status': 'Running'}]},
'model_statuses': {'models': [{'name': 'hf-bart-summarizer3',
'version': 'd511a20c-9612-4112-9368-2d79ae764dec',
'sha': '360dcd343a593e87639106757bad58a7d960899c915bbc9787e7601073bc1121',
'status': 'Running'}]}}],
'engine_lbs': [{'ip': '10.244.0.113',
'name': 'engine-lb-584f54c899-ht5cd',
'status': 'Running',
'reason': None,
'details': []}],
'sidekicks': [{'ip': '10.244.41.21',
'name': 'engine-sidekick-hf-bart-summarizer3-28-f5f8d6567-zzh62',
'status': 'Running',
'reason': None,
'details': [],
'statuses': '\n'}]}
Sample Text Inference
A sample inference is performed 10 times using the definition of LinkedIn, and the time to completion displayed. In this case, the total time to create a summary of the text multiple times is around 2 seconds per inference request.
input_data = {
"inputs": ["LinkedIn (/lɪŋktˈɪn/) is a business and employment-focused social media platform that works through websites and mobile apps. It launched on May 5, 2003. It is now owned by Microsoft. The platform is primarily used for professional networking and career development, and allows jobseekers to post their CVs and employers to post jobs. From 2015 most of the company's revenue came from selling access to information about its members to recruiters and sales professionals. Since December 2016, it has been a wholly owned subsidiary of Microsoft. As of March 2023, LinkedIn has more than 900 million registered members from over 200 countries and territories. LinkedIn allows members (both workers and employers) to create profiles and connect with each other in an online social network which may represent real-world professional relationships. Members can invite anyone (whether an existing member or not) to become a connection. LinkedIn can also be used to organize offline events, join groups, write articles, publish job postings, post photos and videos, and more."]
}
dataframe = pd.DataFrame(input_data)
dataframe.to_json('test_data.json', orient='records')
dataframe
inputs | |
---|---|
0 | LinkedIn (/lɪŋktˈɪn/) is a business and employ... |
import time
start = time.time()
end = time.time()
end - start
2.765655517578125e-05
start = time.time()
elapsed_time = 0
for i in range(10):
s = time.time()
res = pipeline.infer_from_file('test_data.json', timeout=120)
print(res)
e = time.time()
el = e-s
print(el)
end = time.time()
elapsed_time += end - start
print('Execution time:', elapsed_time, 'seconds')
time in.inputs \
0 2023-07-11 19:27:50.806 LinkedIn (/lɪŋktˈɪn/) is a business and employ...
out.summary_text check_failures
0 LinkedIn is a business and employment-focused … 0
2.616016387939453
time in.inputs
0 2023-07-11 19:27:53.421 LinkedIn (/lɪŋktˈɪn/) is a business and employ…
out.summary_text check_failures
0 LinkedIn is a business and employment-focused … 0
2.478372097015381
time in.inputs
0 2023-07-11 19:27:55.901 LinkedIn (/lɪŋktˈɪn/) is a business and employ…
out.summary_text check_failures
0 LinkedIn is a business and employment-focused … 0
2.453855514526367
time in.inputs
0 2023-07-11 19:27:58.365 LinkedIn (/lɪŋktˈɪn/) is a business and employ…
out.summary_text check_failures
0 LinkedIn is a business and employment-focused … 0
2.4600493907928467
time in.inputs
0 2023-07-11 19:28:00.819 LinkedIn (/lɪŋktˈɪn/) is a business and employ…
out.summary_text check_failures
0 LinkedIn is a business and employment-focused … 0
2.461345672607422
time in.inputs
0 2023-07-11 19:28:03.273 LinkedIn (/lɪŋktˈɪn/) is a business and employ…
out.summary_text check_failures
0 LinkedIn is a business and employment-focused … 0
2.4581406116485596
time in.inputs
0 2023-07-11 19:28:05.732 LinkedIn (/lɪŋktˈɪn/) is a business and employ…
out.summary_text check_failures
0 LinkedIn is a business and employment-focused … 0
2.4555394649505615
time in.inputs
0 2023-07-11 19:28:08.192 LinkedIn (/lɪŋktˈɪn/) is a business and employ…
out.summary_text check_failures
0 LinkedIn is a business and employment-focused … 0
2.4681003093719482
time in.inputs
0 2023-07-11 19:28:10.657 LinkedIn (/lɪŋktˈɪn/) is a business and employ…
out.summary_text check_failures
0 LinkedIn is a business and employment-focused … 0
2.4639062881469727
time in.inputs
0 2023-07-11 19:28:13.120 LinkedIn (/lɪŋktˈɪn/) is a business and employ…
out.summary_text check_failures
0 LinkedIn is a business and employment-focused … 0
2.4664926528930664
Execution time: 24.782114267349243 seconds
elapsed_time / 10
2.4782114267349242
Undeploy the Pipeline
With the inferences completed, the pipeline is undeployed. This returns the resources back to the cluster for use by other pipeline.
pipeline.undeploy()
Waiting for undeployment - this will take up to 45s ..............
4.10 - Simulated Edge Tutorial
This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.
Simulated Edge Demo
This notebook will explore “Edge ML”, meaning deploying a model intended to be run on “the edge”. What is “the edge”? This is typically defined as a resource (CPU, memory, and/or bandwidth) constrained environment or where a combination of latency requirements and bandwidth available requires the models to run locally.
Wallaroo provides two key capabilities when it comes to deploying models to edge devices:
- Since the same engine is used in both environments, the model behavior can often be simulated accurately using Wallaroo in a data center for testing prior to deployment.
- Wallaroo makes edge deployments “observable” so the same tools used to monitor model performance can be used in both kinds of deployments.
This notebook closely parallels the Aloha tutorial. The primary difference is instead of provide ample resources to a pipeline to allow high-throughput operation we will specify a resource budget matching what is expected in the final deployment. Then we can apply the expected load to the model and observe how it behaves given the available resources.
This example uses the open source Aloha CNN LSTM model for classifying Domain names as being either legitimate or being used for nefarious purposes such as malware distribution. This could be deployed on a network router to detect suspicious domains in real-time. Of course, it is important to monitor the behavior of the model across all of the deployments so we can see if the detect rate starts to drift over time.
Note that this example is not intended for production use and is meant of an example of running Wallaroo in a restrained environment. The environment is based on the Wallaroo AWS EC2 Setup guide.
Full details on how to configure a deployment through the SDK, see the Wallaroo SDK guides.
For our example, we will perform the following:
- Create a workspace for our work.
- Upload the Aloha model.
- Define a resource budget for our inference pipeline.
- Create a pipeline that can ingest our submitted data, submit it to the model, and export the results
- Run a sample inference through our pipeline by loading a file
- Run a batch inference through our pipeline’s URL and store the results in a file and find that the original memory
allocation is too small. - Redeploy the pipeline with a larger memory budget and attempt sending the same batch of requests through again.
All sample data and models are available through the Wallaroo Quick Start Guide Samples repository.
Prerequisites
- A deployed Wallaroo instance
- The following Python libraries installed:
import wallaroo
from wallaroo.object import EntityNotFoundError
# to display dataframe tables
from IPython.display import display
# used to display dataframe information without truncating
import pandas as pd
pd.set_option('display.max_colwidth', None)
import pyarrow as pa
Connect to the Wallaroo Instance
The first step is to connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. For more information on Wallaroo Client settings, see the Client Connection guide.
# Login through local Wallaroo instance
wl = wallaroo.Client()
Useful variables
The following variables and methods are used to create a workspace, the pipeline in the example workspace and upload models into it.
To allow this tutorial to be run multiple times or by multiple users in the same Wallaroo instance, a random 4 character prefix will be added to the workspace, pipeline, and model.
import string
import random
# make a random 4 character prefix
prefix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
pipeline_name = f'{prefix}edgepipelineexample'
workspace_name = f'{prefix}edgeworkspaceexample'
model_name = f'{prefix}alohamodel'
model_file_name = './alohacnnlstm.zip'
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
pipeline = wl.build_pipeline(name)
return pipeline
Create or Set the Workspace
Create the workspace and set it as our default workspace. If a workspace by the same name already exists, then that workspace will be used.
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
workspace
{'name': 'gobtedgeworkspaceexample', 'id': 21, 'archived': False, 'created_by': '028c8b48-c39b-4578-9110-0b5bdd3824da', 'created_at': '2023-05-17T21:50:10.05059+00:00', 'models': [], 'pipelines': []}
Upload the Models
Now we will upload our models. Note that for this example we are applying the model from a .ZIP file. The Aloha model is a protobuf file that has been defined for evaluating web pages, and we will configure it to use data in the tensorflow
format.
model = wl.upload_model(model_name, model_file_name, framework=wallaroo.framework.Framework.TENSORFLOW).configure("tensorflow")
Define the resource budget
The DeploymentConfig object specifies the resources to allocate for a model pipeline. In this case, we’re going to set a very small budget, one that is too small for this model and then expand it based on testing. To start with, we’ll use 1 CPU and 250 MB of RAM.
deployment_config = wallaroo.DeploymentConfigBuilder().replica_count(1).cpus(1).memory("250Mi").build()
Deploy a model
Now that we have a model that we want to use we will create a deployment for it using the resource limits defined above.
We will tell the deployment we are using a tensorflow model and give the deployment name and the configuration we want for the deployment.
To do this, we’ll create our pipeline that can ingest the data, pass the data to our Aloha model, and give us a final output. We’ll call our pipeline edgepipeline
, then deploy it so it’s ready to receive data. The deployment process usually takes about 45 seconds.
- Note: If you receive an error that the pipeline could not be deployed because there are not enough resources, undeploy any other pipelines and deploy this one again. This command can quickly undeploy all pipelines to regain resources. We recommend not running this command in a production environment since it will cancel any running pipelines:
for p in wl.list_pipelines(): p.undeploy()
pipeline = get_pipeline(pipeline_name)
pipeline.add_model_step(model)
name | gobtedgepipelineexample |
---|---|
created | 2023-05-17 21:50:13.166628+00:00 |
last_updated | 2023-05-17 21:50:13.166628+00:00 |
deployed | (none) |
tags | |
versions | 9efda57b-c18b-4ebb-9681-33647e7d7e66 |
steps |
pipeline.deploy(deployment_config=deployment_config)
name | gobtedgepipelineexample |
---|---|
created | 2023-05-17 21:50:13.166628+00:00 |
last_updated | 2023-05-17 21:50:14.868118+00:00 |
deployed | True |
tags | |
versions | 5cf788a6-50ff-471f-a3ee-4bfdc24def34, 9efda57b-c18b-4ebb-9681-33647e7d7e66 |
steps | gobtalohamodel |
We can verify that the pipeline is running and list what models are associated with it.
pipeline.status()
{'status': 'Running',
'details': [],
'engines': [{'ip': '10.244.3.150',
'name': 'engine-7c78c78bb8-lrhb9',
'status': 'Running',
'reason': None,
'details': [],
'pipeline_statuses': {'pipelines': [{'id': 'gobtedgepipelineexample',
'status': 'Running'}]},
'model_statuses': {'models': [{'name': 'gobtalohamodel',
'version': '969b91cb-1cef-49c5-9292-36af48e494b5',
'sha': 'd71d9ffc61aaac58c2b1ed70a2db13d1416fb9d3f5b891e5e4e2e97180fe22f8',
'status': 'Running'}]}}],
'engine_lbs': [{'ip': '10.244.4.182',
'name': 'engine-lb-584f54c899-757hh',
'status': 'Running',
'reason': None,
'details': []}],
'sidekicks': []}
Inferences
Infer 1 row
Now that the pipeline is deployed and our model is in place, we’ll perform a smoke test to verify the pipeline is up and running properly. We’ll use the infer_from_file
command to load a single encoded URL into the inference engine and print the results back out.
The result should tell us that the tokenized URL is legitimate (0) or fraud (1). This sample data should return close to 1.
smoke_test = pd.DataFrame.from_records(
[
{
"text_input":[
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
28,
16,
32,
23,
29,
32,
30,
19,
26,
17
]
}
]
)
result = pipeline.infer(smoke_test)
display(result.loc[:, ["time","out.main"]])
time | out.main | |
---|---|---|
0 | 2023-05-17 21:50:26.790 | [0.997564] |
- IMPORTANT NOTE: The
_deployment._url()
method will return an internal URL when using Python commands from within the Wallaroo instance - for example, the Wallaroo JupyterHub service. When connecting via an external connection,_deployment._url()
returns an external URL. External URL connections requires the authentication be included in the HTTP request, and that Model Endpoints Guide external endpoints are enabled in the Wallaroo configuration options.
inference_url = pipeline._deployment._url()
display(inference_url)
connection =wl.mlops().__dict__
token = connection['token']
'https://doc-test.api.wallarooexample.ai/v1/api/pipelines/infer/gobtedgepipelineexample-23/gobtedgepipelineexample'
dataFile="./data/data_1k.arrow"
contentType="application/vnd.apache.arrow.file"
!curl -X POST {inference_url} -H "Authorization: Bearer {token}" -H "Content-Type:{contentType}" --data-binary @{dataFile} > curl_response.df
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 196k 100 95 100 196k 90 187k 0:00:01 0:00:01 --:--:-- 190k
Redeploy with a little larger budget
If you look in the file curl_response.df, you will see that the inference failed:
upstream connect error or disconnect/reset before headers. reset reason: connection termination
Even though a single inference passed, submitted a larger batch of work did not. If this is an expected usage case for
this model, we need to add more memory. Let’s do that now.
The following DeploymentConfig is the same as the original, but increases the memory from 300MB to 600MB. This sort
of budget would be available on some network routers.
pipeline.undeploy()
deployment_config = wallaroo.DeploymentConfigBuilder().replica_count(1).cpus(1).memory("600Mi").build()
pipeline.deploy(deployment_config=deployment_config)
name | gobtedgepipelineexample |
---|---|
created | 2023-05-17 21:50:13.166628+00:00 |
last_updated | 2023-05-17 21:51:06.928374+00:00 |
deployed | True |
tags | |
versions | dc0238e7-f3e3-4579-9a63-24902cb3e3bd, 5cf788a6-50ff-471f-a3ee-4bfdc24def34, 9efda57b-c18b-4ebb-9681-33647e7d7e66 |
steps | gobtalohamodel |
Re-run inference
Running the same curl command again should now produce a curl_response.txt file containing the expected results.
connection =wl.mlops().__dict__
token = connection['token']
print(f'curl -X POST {inference_url} -H "Authorization: Bearer {token}" -H "Content-Type:{contentType}" --data-binary @{dataFile} > curl_response.df')
curl -X POST https://doc-test.api.wallarooexample.ai/v1/api/pipelines/infer/gobtedgepipelineexample-23/gobtedgepipelineexample -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJDYkFqN19QY0xCWTFkWmJiUDZ6Q3BsbkNBYTd6US0tRHlyNy0yLXlQb25nIn0.eyJleHAiOjE2ODQzNjAzMTksImlhdCI6MTY4NDM2MDI1OSwiYXV0aF90aW1lIjoxNjg0MzU1OTU5LCJqdGkiOiI1ZjU4NTQ2Yy1lOTVlLTQ5YjktODgyYS0zYWMxMzgxYzdkODYiLCJpc3MiOiJodHRwczovL2RvYy10ZXN0LmtleWNsb2FrLndhbGxhcm9vY29tbXVuaXR5Lm5pbmphL2F1dGgvcmVhbG1zL21hc3RlciIsImF1ZCI6WyJtYXN0ZXItcmVhbG0iLCJhY2NvdW50Il0sInN1YiI6IjAyOGM4YjQ4LWMzOWItNDU3OC05MTEwLTBiNWJkZDM4MjRkYSIsInR5cCI6IkJlYXJlciIsImF6cCI6InNkay1jbGllbnQiLCJzZXNzaW9uX3N0YXRlIjoiMGJlODJjN2ItNzg1My00ZjVkLWJiNWEtOTlkYjUwYjhiNDVmIiwiYWNyIjoiMCIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJkZWZhdWx0LXJvbGVzLW1hc3RlciIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJtYXN0ZXItcmVhbG0iOnsicm9sZXMiOlsibWFuYWdlLXVzZXJzIiwidmlldy11c2VycyIsInF1ZXJ5LWdyb3VwcyIsInF1ZXJ5LXVzZXJzIl19LCJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6InByb2ZpbGUgZW1haWwiLCJzaWQiOiIwYmU4MmM3Yi03ODUzLTRmNWQtYmI1YS05OWRiNTBiOGI0NWYiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImh0dHBzOi8vaGFzdXJhLmlvL2p3dC9jbGFpbXMiOnsieC1oYXN1cmEtdXNlci1pZCI6IjAyOGM4YjQ4LWMzOWItNDU3OC05MTEwLTBiNWJkZDM4MjRkYSIsIngtaGFzdXJhLWRlZmF1bHQtcm9sZSI6InVzZXIiLCJ4LWhhc3VyYS1hbGxvd2VkLXJvbGVzIjpbInVzZXIiXSwieC1oYXN1cmEtdXNlci1ncm91cHMiOiJ7fSJ9LCJuYW1lIjoiSm9obiBIYW5zYXJpY2siLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJqb2huLmh1bW1lbEB3YWxsYXJvby5haSIsImdpdmVuX25hbWUiOiJKb2huIiwiZmFtaWx5X25hbWUiOiJIYW5zYXJpY2siLCJlbWFpbCI6ImpvaG4uaHVtbWVsQHdhbGxhcm9vLmFpIn0.j86GU-Zi07DvuMnOi1iz8G7ySEL_GeC0A-ol0oI1-X_OdncCpuYBcJWBnf6w66xWkl3oi3-1eHWFcQkPG7W-pNaYW00oYR2o5vBd18_iHWeMTSOeW6ooooseDeGzmk88j9Z02C517fFjHPG1WB_EB1L12cB0PzBOWjoQu9o2tXpSDx8zjP0A-AQZWx5_itrOrMcSwffq3KNgzIscrVjSY4rcin_c5bdZkTvrKeW8uG9wHGyVN_BSVyceTeXqD21oDUmIvnYVDZyx9gmDytWtp43ahX_qHaV7chWOfnaTcd4e4_mAotcLP_PjfptushhanhSfWty1z1b5xv0ut3SxUQ" -H "Content-Type:application/vnd.apache.arrow.file" --data-binary @./data/data_1k.arrow > curl_response.df
!curl -X POST {inference_url} -H "Authorization: Bearer {token}" -H "Content-Type:{contentType}" --data-binary @{dataFile} > curl_response.df
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 1045k 100 849k 100 196k 411k 97565 0:00:02 0:00:02 --:--:-- 512k
It is important to note that increasing the memory was necessary to run a batch of 1,000 inferences at once. If this is not a design
use case for your system, running with the smaller memory budget may be acceptable. Wallaroo allows you to easily test difference
loading patterns to get a sense for what resources are required with sufficient buffer to allow for robust operation of your system
while not over-provisioning scarce resources.
Undeploy Pipeline
When finished with our tests, we will undeploy the pipeline so we have the Kubernetes resources back for other tasks. Note that if the deployment variable is unchanged aloha_pipeline.deploy() will restart the inference engine in the same configuration as before.
pipeline.undeploy()
name | gobtedgepipelineexample |
---|---|
created | 2023-05-17 21:50:13.166628+00:00 |
last_updated | 2023-05-17 21:51:06.928374+00:00 |
deployed | False |
tags | |
versions | dc0238e7-f3e3-4579-9a63-24902cb3e3bd, 5cf788a6-50ff-471f-a3ee-4bfdc24def34, 9efda57b-c18b-4ebb-9681-33647e7d7e66 |
steps | gobtalohamodel |
5 - Model Validation and Testing in Wallaroo
The following tutorials demonstrate how to use Wallaroo to test and validation models in production against their target outcomes.
5.1 - A/B Testing Tutorial
This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.
A/B Testing
A/B testing is a method that provides the ability to test out ML models for performance, accuracy or other useful benchmarks. A/B testing is contrasted with the Wallaroo Shadow Deployment feature. In both cases, two sets of models are added to a pipeline step:
- Control or Champion model: The model currently used for inferences.
- Challenger model(s): One or more models that are to be compared to the champion model.
The two feature are different in this way:
Feature | Description |
---|---|
A/B Testing | A subset of inferences are submitted to either the champion ML model or a challenger ML model. |
Shadow Deploy | All inferences are submitted to the champion model and one or more challenger models. |
So to repeat: A/B testing submits some of the inference requests to the champion model, some to the challenger model with one set of outputs, while shadow testing submits all of the inference requests to champion and shadow models, and has separate outputs.
This tutorial demonstrate how to conduct A/B testing in Wallaroo. For this example we will be using an open source model that uses an Aloha CNN LSTM model for classifying Domain names as being either legitimate or being used for nefarious purposes such as malware distribution.
For our example, we will perform the following:
- Create a workspace for our work.
- Upload the Aloha model and a challenger model.
- Create a pipeline that can ingest our submitted data with the champion model and the challenger model set into a A/B step.
- Run a series of sample inferences to display inferences that are run through the champion model versus the challenger model, then determine which is more efficient.
All sample data and models are available through the Wallaroo Quick Start Guide Samples repository.
Prerequisites
- A deployed Wallaroo instance
- The following Python libraries installed:
Steps
Import libraries
Here we will import the libraries needed for this notebook.
import wallaroo
from wallaroo.object import EntityNotFoundError
import os
import pandas as pd
import json
from IPython.display import display
# used to display dataframe information without truncating
from IPython.display import display
pd.set_option('display.max_colwidth', None)
wallaroo.__version__
'2023.2.0rc3'
Connect to the Wallaroo Instance
The first step is to connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. For more information on Wallaroo Client settings, see the Client Connection guide.
# Login through local Wallaroo instance
wl = wallaroo.Client()
Create Workspace
We will create a workspace to manage our pipeline and models. The following variables will set the name of our sample workspace then set it as the current workspace for all other commands.
To allow this tutorial to be run multiple times or by multiple users in the same Wallaroo instance, a random 4 character prefix will be added to the workspace, pipeline, and model.
workspace_name = 'abhousetesting'
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
{'name': 'abtesting', 'id': 33, 'archived': False, 'created_by': '028c8b48-c39b-4578-9110-0b5bdd3824da', 'created_at': '2023-05-18T13:55:21.887136+00:00', 'models': [], 'pipelines': []}
Set Up the Champion and Challenger Models
Now we upload the Champion and Challenger models to our workspace. We will use two models:
aloha-cnn-lstm
model.aloha-cnn-lstm-new
(a retrained version)
Set the Champion Model
We upload our champion model, labeled as control
.
#control = wl.upload_model("aloha-control", 'models/aloha-cnn-lstm.zip').configure('tensorflow')
control = wl.upload_model("houseprice-control",'models/housing_control.zip', framework=wallaroo.framework.Framework.TENSORFLOW).configure('tensorflow')
Set the Challenger Model
Now we upload the Challenger model, labeled as challenger
.
#challenger = wl.upload_model("aloha-challenger", 'models/aloha-cnn-lstm-new.zip').configure('tensorflow')
challenger = wl.upload_model("houseprice-challenger",'models/housing_challenger.zip', framework=wallaroo.framework.Framework.TENSORFLOW).configure('tensorflow')
Define The Pipeline
Here we will configure a pipeline with two models and set the control model with a random split chance of receiving 2/3 of the data. Because this is a random split, it is possible for one model or the other to receive more inferences than a strict 2:1 ratio, but the more inferences are run, the more likely it is for the proper ratio split.
pipeline = (wl.build_pipeline("randomsplitpipeline-demo")
.add_random_split([(2, control), (1, challenger)], "session_id"))
Deploy the pipeline
Now we deploy the pipeline so we can run our inference through it.
experiment_pipeline = pipeline.deploy()
experiment_pipeline.status()
{'status': 'Running',
'details': [],
'engines': [{'ip': '10.244.3.161',
'name': 'engine-66cbb56b67-4j46k',
'status': 'Running',
'reason': None,
'details': [],
'pipeline_statuses': {'pipelines': [{'id': 'randomsplitpipeline-demo',
'status': 'Running'}]},
'model_statuses': {'models': [{'name': 'aloha-control',
'version': '7e5d3218-f7ad-4f08-9984-e1a459f6bc1c',
'sha': 'fd998cd5e4964bbbb4f8d29d245a8ac67df81b62be767afbceb96a03d1a01520',
'status': 'Running'},
{'name': 'aloha-challenger',
'version': 'dcdd8ef9-e30a-4785-ac91-06bc396487ec',
'sha': '223d26869d24976942f53ccb40b432e8b7c39f9ffcf1f719f3929d7595bceaf3',
'status': 'Running'}]}}],
'engine_lbs': [{'ip': '10.244.4.194',
'name': 'engine-lb-584f54c899-ks6s8',
'status': 'Running',
'reason': None,
'details': []}],
'sidekicks': []}
Run a single inference
Now we have our deployment set up let’s run a single inference. In the results we will be able to see the inference results as well as which model the inference went to under model_id. We’ll run the inference request 5 times, with the odds are that the challenger model being run at least once.
# use dataframe JSON files
for x in range(5):
result = experiment_pipeline.infer_from_file("data/data-1.df.json")
value = result.loc[0]["out.dense_19"]
model = json.loads(result.loc[0]["out._model_split"][0])['name']
df = pd.DataFrame({'model': model, 'value': value})
display(df)
out._model_split | out.dense_19 | |
---|---|---|
0 | [{"name":"aloha-control","version":"7e5d3218-f7ad-4f08-9984-e1a459f6bc1c","sha":"fd998cd5e4964bbbb4f8d29d245a8ac67df81b62be767afbceb96a03d1a01520"}] | [0.997564] |
out._model_split | out.dense_19 | |
---|---|---|
0 | [{"name":"aloha-control","version":"7e5d3218-f7ad-4f08-9984-e1a459f6bc1c","sha":"fd998cd5e4964bbbb4f8d29d245a8ac67df81b62be767afbceb96a03d1a01520"}] | [0.997564] |
out._model_split | out.dense_19 | |
---|---|---|
0 | [{"name":"aloha-challenger","version":"dcdd8ef9-e30a-4785-ac91-06bc396487ec","sha":"223d26869d24976942f53ccb40b432e8b7c39f9ffcf1f719f3929d7595bceaf3"}] | [0.997564] |
out._model_split | out.dense_19 | |
---|---|---|
0 | [{"name":"aloha-control","version":"7e5d3218-f7ad-4f08-9984-e1a459f6bc1c","sha":"fd998cd5e4964bbbb4f8d29d245a8ac67df81b62be767afbceb96a03d1a01520"}] | [0.997564] |
out._model_split | out.main | |
---|---|---|
0 | [{"name":"aloha-challenger","version":"dcdd8ef9-e30a-4785-ac91-06bc396487ec","sha":"223d26869d24976942f53ccb40b432e8b7c39f9ffcf1f719f3929d7595bceaf3"}] | [0.997564] |
Run Inference Batch
We will submit 1000 rows of test data through the pipeline, then loop through the responses and display which model each inference was performed in. The results between the control and challenger should be approximately 2:1.
responses = []
test_data = pd.read_json('data/data-1k.df.json')
# For each row, submit that row as a separate dataframe
# Add the results to the responses array
for index, row in test_data.head(1000).iterrows():
responses.append(experiment_pipeline.infer(row.to_frame('text_input').reset_index()))
#now get our responses for each row
l = [json.loads(r.loc[0]["out._model_split"][0])["name"] for r in responses]
df = pd.DataFrame({'model': l})
display(df.model.value_counts())
aloha-control 666
aloha-challenger 334
Name: model, dtype: int64
Test Challenger
Now we have run a large amount of data we can compare the results.
For this experiment we are looking for a significant change in the fraction of inferences that predicted a probability of the seventh category being high than 0.5 so we can determine whether our challenger model is more “successful” than the champion model at identifying category 7.
control_count = 0
challenger_count = 0
control_success = 0
challenger_success = 0
for r in responses:
if json.loads(r.loc[0]["out._model_split"][0])["name"] == "aloha-control":
control_count += 1
if(r.loc[0]["out.main"][0] > .5):
control_success += 1
else:
challenger_count += 1
if(r.loc[0]["out.main"][0] > .5):
challenger_success += 1
print("control class 7 prediction rate: " + str(control_success/control_count))
print("challenger class 7 prediction rate: " + str(challenger_success/challenger_count))
control class 7 prediction rate: 0.972972972972973
challenger class 7 prediction rate: 0.9850299401197605
Logs
Logs can be viewed with the Pipeline method logs()
. For this example, only the first 5 logs will be shown. For Arrow enabled environments, the model type can be found in the column out._model_split
.
logs = experiment_pipeline.logs(limit=5)
display(logs.loc[:,['time', 'out._model_split', 'out.main']])
Warning: There are more logs available. Please set a larger limit or request a file using export_logs.
time | out._model_split | out.dense_19 | |
---|---|---|---|
0 | 2023-05-18 14:02:08.525 | [{"name":"aloha-challenger","version":"dcdd8ef9-e30a-4785-ac91-06bc396487ec","sha":"223d26869d24976942f53ccb40b432e8b7c39f9ffcf1f719f3929d7595bceaf3"}] | [0.99999803] |
1 | 2023-05-18 14:02:08.141 | [{"name":"aloha-control","version":"7e5d3218-f7ad-4f08-9984-e1a459f6bc1c","sha":"fd998cd5e4964bbbb4f8d29d245a8ac67df81b62be767afbceb96a03d1a01520"}] | [0.9998954] |
2 | 2023-05-18 14:02:07.758 | [{"name":"aloha-control","version":"7e5d3218-f7ad-4f08-9984-e1a459f6bc1c","sha":"fd998cd5e4964bbbb4f8d29d245a8ac67df81b62be767afbceb96a03d1a01520"}] | [0.66066873] |
3 | 2023-05-18 14:02:07.374 | [{"name":"aloha-control","version":"7e5d3218-f7ad-4f08-9984-e1a459f6bc1c","sha":"fd998cd5e4964bbbb4f8d29d245a8ac67df81b62be767afbceb96a03d1a01520"}] | [0.9999727] |
4 | 2023-05-18 14:02:07.021 | [{"name":"aloha-challenger","version":"dcdd8ef9-e30a-4785-ac91-06bc396487ec","sha":"223d26869d24976942f53ccb40b432e8b7c39f9ffcf1f719f3929d7595bceaf3"}] | [0.9999754] |
Undeploy Pipeline
With the testing complete, we undeploy the pipeline to return the resources back to the environment.
experiment_pipeline.undeploy()
name | randomsplitpipeline-demo |
---|---|
created | 2023-05-18 13:55:25.914690+00:00 |
last_updated | 2023-05-18 13:55:27.144796+00:00 |
deployed | False |
tags | |
versions | 6350d3ee-8b11-4eac-a8f5-e32659ea0dd2, 170fb233-5b26-492a-ba86-e2ee72129d16 |
steps | aloha-control |
5.2 - Anomaly Testing Tutorial
This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.
Anomaly Detection
Wallaroo provides multiple methods of analytical analysis to verify that the data received and generated during an inference is accurate. This tutorial will demonstrate how to use anomaly detection to track the outputs from a sample model to verify that the model is outputting acceptable results.
Anomaly detection allows organizations to set validation parameters in a pipeline. A validation is added to a pipeline to test data based on an expression, and flag any inferences where the validation failed inference result and the pipeline logs.
This tutorial will follow this process in setting up a validation to a pipeline and examining the results:
- Create a workspace and upload the sample model.
- Establish a pipeline and add the model as a step.
- Add a validation to the pipeline.
- Perform inferences and display anomalies through the
InferenceResult
object and the pipeline log files.
This tutorial provides the following:
- Housing model:
./models/housingprice.onnx
- a pretrained model used to determine standard home prices. - Test Data:
./data
- sample data.
Prerequisites
- A deployed Wallaroo instance
- The following Python libraries installed:
Steps
Import libraries
The first step is to import the libraries needed for this notebook.
import wallaroo
from wallaroo.object import EntityNotFoundError
import os
import json
from IPython.display import display
# used to display dataframe information without truncating
from IPython.display import display
import pandas as pd
pd.set_option('display.max_colwidth', None)
import datetime
Connect to the Wallaroo Instance
The first step is to connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. For more information on Wallaroo Client settings, see the Client Connection guide.
# Login through local Wallaroo instance
wl = wallaroo.Client()
Create Workspace
We will create a workspace to manage our pipeline and models. The following variables will set the name of our sample workspace then set it as the current workspace.
workspace_name = 'anomalytesting'
pipeline_name = 'anomalytestexample'
model_name = 'anomaly-housing-model'
model_file_name = './models/house_price_keras.onnx'
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
{'name': 'anomalytesting', 'id': 145, 'archived': False, 'created_by': '138bd7e6-4dc8-4dc1-a760-c9e721ef3c37', 'created_at': '2023-03-06T19:27:47.219395+00:00', 'models': [{'name': 'anomaly-housing-model', 'versions': 1, 'owner_id': '""', 'last_update_time': datetime.datetime(2023, 3, 13, 16, 39, 41, 683686, tzinfo=tzutc()), 'created_at': datetime.datetime(2023, 3, 13, 16, 39, 41, 683686, tzinfo=tzutc())}], 'pipelines': [{'name': 'anomalyhousing', 'create_time': datetime.datetime(2023, 3, 6, 19, 37, 23, 71334, tzinfo=tzutc()), 'definition': '[]'}]}
Upload The Model
The housing model will be uploaded for use in our pipeline.
housing_model = wl.upload_model(model_name, model_file_name, framework=wallaroo.framework.Framework.ONNX).configure()
Build the Pipeline and Validation
The pipeline anomaly-housing-pipeline
will be created and the anomaly-housing-model
added as a step. A validation will be created for outputs greater 100.0. This is interpreted as houses with a value greater than $350 thousand with the add_validation
method. When houses greater than this value are detected, the InferenceObject
will add it in the check_failures
array with the message “price too high”.
Once complete, the pipeline will be deployed and ready for inferences.
p = wl.build_pipeline(pipeline_name)
p = p.add_model_step(housing_model)
p = p.add_validation('price too high', housing_model.outputs[0][0] < 35.0)
pipeline = p.deploy()
Testing
Two data points will be fed used for an inference.
The first, labeled response_normal
, will not trigger an anomaly detection. The other, labeled response_trigger
, will trigger the anomaly detection, which will be shown in the InferenceResult check_failures
array.
Note that multiple validations can be created to allow for multiple anomalies detected.
if arrowEnabled is True:
normal_input = pd.DataFrame.from_records({"tensor": [
[
0.6752651953165153,
0.49993424710692347,
0.7386510547400537,
1.4527294113261855,
-0.08666382440547035,
-0.0713079330077084,
1.8870291307801872,
0.9294639723887077,
-0.305653139057544,
-0.6285378875598833,
0.29288456205300767,
1.181109967163617,
-0.65605032361317,
1.1203567680905366,
-0.20817781526102327,
0.9695503533113344,
2.823342771358126
]
]
})
else:
normal_input = {"tensor": [
[
0.6752651953165153,
0.49993424710692347,
0.7386510547400537,
1.4527294113261855,
-0.08666382440547035,
-0.0713079330077084,
1.8870291307801872,
0.9294639723887077,
-0.305653139057544,
-0.6285378875598833,
0.29288456205300767,
1.181109967163617,
-0.65605032361317,
1.1203567680905366,
-0.20817781526102327,
0.9695503533113344,
2.823342771358126
]
]
}
result = pipeline.infer(normal_input)
display(result)
time | in.tensor | out.dense_2 | check_failures | |
---|---|---|---|---|
0 | 2023-03-13 20:18:34.677 | [0.6752651953, 0.4999342471, 0.7386510547, 1.4527294113, -0.0866638244, -0.071307933, 1.8870291308, 0.9294639724, -0.3056531391, -0.6285378876, 0.2928845621, 1.1811099672, -0.6560503236, 1.1203567681, -0.2081778153, 0.9695503533, 2.8233427714] | [13.12781] | 0 |
if arrowEnabled is True:
trigger_input= pd.DataFrame.from_records({"tensor": [
[0.6752651953165153, -1.4463372267359147, 0.8592227450151407, -1.336883943861539, -0.08666382440547035, 372.11547809844996, -0.26674056237955046, 0.005746226275241667, 2.308796820400806, -0.6285378875598833, -0.5584151415472702, -0.08354305857288258, -0.65605032361317, -1.4648287573778653, -0.20817781526102327, 0.22552571571180896, -0.30338131340656516]
]
}
)
else:
trigger_input= {"tensor": [
[0.6752651953165153, -1.4463372267359147, 0.8592227450151407, -1.336883943861539, -0.08666382440547035, 372.11547809844996, -0.26674056237955046, 0.005746226275241667, 2.308796820400806, -0.6285378875598833, -0.5584151415472702, -0.08354305857288258, -0.65605032361317, -1.4648287573778653, -0.20817781526102327, 0.22552571571180896, -0.30338131340656516]
]
}
trigger_result = pipeline.infer(trigger_input)
display(trigger_result)
time | in.tensor | out.dense_2 | check_failures | |
---|---|---|---|---|
0 | 2023-03-13 18:53:50.585 | [0.6752651953, -1.4463372267, 0.859222745, -1.3368839439, -0.0866638244, 372.1154780984, -0.2667405624, 0.0057462263, 2.3087968204, -0.6285378876, -0.5584151415, -0.0835430586, -0.6560503236, -1.4648287574, -0.2081778153, 0.2255257157, -0.3033813134] | [39.575085] | 1 |
Multiple Tests
With the initial tests run, we can run the inferences against a larger set of data and identify anomalies that appear versus the expected results. These will be displayed into a graph so we can see where the anomalies occur. In this case with the house that came in at $350 million - outside of our validation range.
Note: Because this is splitting one batch inference into 500 separate inferences for this example, it may take longer to run.
Notice the one result that is outside the normal range - the one lonely result on the far right.
if arrowEnabled is True:
test_data = pd.read_json("./data/houseprice_inputs_500.json", orient="records")
responses_anomaly = pd.DataFrame()
# For the first 1000 rows, submit that row as a separate DataFrame
# Add the results to the responses_anomaly dataframe
for index, row in test_data.head(500).iterrows():
responses_anomaly = responses_anomaly.append(pipeline.infer(row.to_frame('tensor').reset_index()))
else:
test_data = json.load("./data/houseprice_inputs_500.json")
responses_anomaly =[]
for nth in range(500):
responses_anomaly.extend(pipeline.infer({ "tensor": [test_data['tensor'][0][nth]]}))
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
if arrowEnabled is True:
houseprices = pd.DataFrame({'sell_price': responses_anomaly['out.dense_2'].apply(lambda x: x[0])})
else:
houseprices = pd.DataFrame({'sell_price': [r.raw['outputs'][0]['Float']['data'][0] for r in responses_anomaly]})
houseprices.hist(column='sell_price', bins=75, grid=False, figsize=(12,8))
plt.axvline(x=40, color='gray', ls='--')
_ = plt.title('Distribution of predicted home sales price')
How To Check For Anomalies
There are two primary methods for detecting anomalies with Wallaroo:
- As demonstrated in the example above, from the
InferenceObject
check_failures
array in the output of each inference to see if anything has happened. - The other method is to view pipeline’s logs and see what anomalies have been detected.
View Logs
Anomalies can be displayed through the pipeline logs()
method.
For Arrow enabled Wallaroo instances, the logs are returned as a dataframe. Filtering by the column check_failures
greater than 0 displays any inferences that had an anomaly triggered.
For Arrow disabled Wallaroo instances, the parameter valid=False
will show any validations that were flagged as False
- in this case, houses that were above 350 thousand in value.
if arrowEnabled is True:
logs = pipeline.logs()
logs = logs.loc[logs['check_failures'] > 0]
else:
logs = pipeline.logs(valid=False)
display(logs)
Warning: Pipeline log size limit exceeded. Please request logs using export_logs
time | in.index | in.tensor | out.dense_2 | check_failures | |
---|---|---|---|---|---|
32 | 2023-03-13 19:41:03.596 | tensor | [0.6752651953, -1.4463372267, 0.859222745, -1.3368839439, -0.0866638244, 372.1154780984, -0.2667405624, 0.0057462263, 2.3087968204, -0.6285378876, -0.5584151415, -0.0835430586, -0.6560503236, -1.4648287574, -0.2081778153, 0.2255257157, -0.3033813134] | [39.575085] | 1 |
Undeploy The Pipeline
With the example complete, we undeploy the pipeline to return the resources back to the Wallaroo instance.
pipeline.undeploy()
name | anomalytestexample |
---|---|
created | 2023-03-13 20:18:16.622828+00:00 |
last_updated | 2023-03-13 20:18:18.995804+00:00 |
deployed | False |
tags | |
versions | dec18ab4-8b71-44c9-a507-c9763803153f, 64246a8b-61a8-4ead-94aa-00f4cf571f74 |
steps | anomaly-housing-model |
5.3 - House Price Testing Life Cycle
This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.
House Price Testing Life Cycle Comprehensive Tutorial
This tutorial simulates using Wallaroo for testing a model for inference outliers, potential model drift, and methods to test competitive models against each other and deploy the final version to use. This demonstrates using assays to detect model or data drift, then Wallaroo Shadow Deploy to compare different models to determine which one is most fit for an organization’s needs. These features allow organizations to monitor model performance and accuracy then swap out models as needed.
- IMPORTANT NOTE: This tutorial assumes that the House Price Model Life Cycle Preparation notebook was run before this notebook, and that the workspace, pipeline and models used are the same. This is critical for the section on Assays below. If the preparation notebook has not been run, skip the Assays section as there will be no historical data for the assays to function on.
This tutorial will demonstrate how to:
- Select or create a workspace, pipeline and upload the champion model.
- Add a pipeline step with the champion model, then deploy the pipeline and perform sample inferences.
- Create an assay and set a baseline, then demonstrate inferences that trigger the assay alert threshold.
- Swap out the pipeline step with the champion model with a shadow deploy step that compares the champion model against two competitors.
- Evaluate the results of the champion versus competitor models.
- Change the pipeline step from a shadow deploy step to an A/B testing step, and show the different results.
- Change the A/B testing step back to standard pipeline step with the original control model, then demonstrate hot swapping the control model with a challenger model without undeploying the pipeline.
- Undeploy the pipeline.
This tutorial provides the following:
- Models:
models/rf_model.onnx
: The champion model that has been used in this environment for some time.models/xgb_model.onnx
andmodels/gbr_model.onnx
: Rival models that will be tested against the champion.
- Data:
data/xtest-1.df.json
anddata/xtest-1k.df.json
: DataFrame JSON inference inputs with 1 input and 1,000 inputs.data/xtest-1k.arrow
: Apache Arrow inference inputs with 1 input and 1,000 inputs.
Prerequisites
- A deployed Wallaroo instance
- The following Python libraries installed:
Initial Steps
Import libraries
The first step is to import the libraries needed for this notebook.
import wallaroo
from wallaroo.object import EntityNotFoundError
from wallaroo.framework import Framework
from IPython.display import display
# used to display DataFrame information without truncating
from IPython.display import display
import pandas as pd
pd.set_option('display.max_colwidth', None)
import datetime
import time
# used for unique connection names
import string
import random
suffix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
import json
Connect to the Wallaroo Instance
The first step is to connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. For more information on Wallaroo Client settings, see the Client Connection guide.
# Login through local Wallaroo instance
wl = wallaroo.Client()
Create Workspace
We will create a workspace to manage our pipeline and models. The following variables will set the name of our sample workspace then set it as the current workspace.
Workspace, pipeline, and model names should be unique to each user, so we’ll add in a randomly generated suffix so multiple people can run this tutorial in a Wallaroo instance without effecting each other.
workspace_name = f'housepricesagaworkspace'
main_pipeline_name = f'housepricesagapipeline'
model_name_control = f'housepricesagacontrol'
model_file_name_control = './models/rf_model.onnx'
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name, workspace):
pipelines = workspace.pipelines()
pipe_filter = filter(lambda x: x.name() == name, pipelines)
pipes = list(pipe_filter)
# we can't have a pipe in the workspace with the same name, so it's always the first
if pipes:
pipeline = pipes[0]
else:
pipeline = wl.build_pipeline(name)
return pipeline
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
{'name': 'housepricesagaworkspace', 'id': 8, 'archived': False, 'created_by': 'c3a45eb6-37ff-4020-8d59-7166c3e153d0', 'created_at': '2023-07-19T19:39:41.220639+00:00', 'models': [{'name': 'housepricesagacontrol', 'versions': 1, 'owner_id': '""', 'last_update_time': datetime.datetime(2023, 7, 19, 19, 40, 12, 107050, tzinfo=tzutc()), 'created_at': datetime.datetime(2023, 7, 19, 19, 40, 12, 107050, tzinfo=tzutc())}, {'name': 'housingchallenger01', 'versions': 1, 'owner_id': '""', 'last_update_time': datetime.datetime(2023, 7, 19, 19, 49, 16, 74913, tzinfo=tzutc()), 'created_at': datetime.datetime(2023, 7, 19, 19, 49, 16, 74913, tzinfo=tzutc())}, {'name': 'housingchallenger02', 'versions': 1, 'owner_id': '""', 'last_update_time': datetime.datetime(2023, 7, 19, 19, 49, 17, 406154, tzinfo=tzutc()), 'created_at': datetime.datetime(2023, 7, 19, 19, 49, 17, 406154, tzinfo=tzutc())}], 'pipelines': [{'name': 'housepricesagapipeline', 'create_time': datetime.datetime(2023, 7, 19, 19, 40, 14, 556167, tzinfo=tzutc()), 'definition': '[]'}]}
Upload The Champion Model
For our example, we will upload the champion model that has been trained to derive house prices from a variety of inputs. The model file is rf_model.onnx
, and is uploaded with the name housingcontrol
.
housing_model_control = (wl.upload_model(model_name_control,
model_file_name_control,
framework=Framework.ONNX)
.configure()
)
Standard Pipeline Steps
Build the Pipeline
This pipeline is made to be an example of an existing situation where a model is deployed and being used for inferences in a production environment. We’ll call it housepricepipeline
, set housingcontrol
as a pipeline step, then run a few sample inferences.
This pipeline will be a simple one - just a single pipeline step.
mainpipeline = get_pipeline(main_pipeline_name, workspace)
# clearing from previous runs and verifying it is undeployed
mainpipeline.clear()
mainpipeline.undeploy()
mainpipeline.add_model_step(housing_model_control).deploy()
name | housepricesagapipeline |
---|---|
created | 2023-07-19 19:40:14.556167+00:00 |
last_updated | 2023-07-19 19:52:22.971291+00:00 |
deployed | True |
tags | |
versions | 34fbdbd2-37b6-4c87-a73b-2bc37356f107, 85f43a06-6a2d-445e-9ba7-6511a7c8300c, 6f21cab0-689d-4c7a-be9a-317786a1173d, ee06f9e2-56d8-413d-8245-0fb37dc377ed, ccad17d0-8121-42ac-9311-14777667ae61, a77de963-1df0-4f2f-92d7-cc010277e726, 44f668f8-938c-4542-8c79-13128891171c, 423875eb-d31a-48fa-bcc5-a3fe4d45e4d9 |
steps | housepricesagacontrol |
Testing
We’ll use two inferences as a quick sample test - one that has a house that should be determined around $700k, the other with a house determined to be around $1.5 million. We’ll also save the start and end periods for these events to for later log functionality.
normal_input = pd.DataFrame.from_records({"tensor": [[4.0, 2.5, 2900.0, 5505.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2900.0, 0.0, 47.6063, -122.02, 2970.0, 5251.0, 12.0, 0.0, 0.0]]})
result = mainpipeline.infer(normal_input)
display(result)
time | in.tensor | out.variable | check_failures | |
---|---|---|---|---|
0 | 2023-07-19 19:52:42.586 | [4.0, 2.5, 2900.0, 5505.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2900.0, 0.0, 47.6063, -122.02, 2970.0, 5251.0, 12.0, 0.0, 0.0] | [718013.7] | 0 |
large_house_input = pd.DataFrame.from_records({'tensor': [[4.0, 3.0, 3710.0, 20000.0, 2.0, 0.0, 2.0, 5.0, 10.0, 2760.0, 950.0, 47.6696, -122.261, 3970.0, 20000.0, 79.0, 0.0, 0.0]]})
large_house_result = mainpipeline.infer(large_house_input)
display(large_house_result)
time | in.tensor | out.variable | check_failures | |
---|---|---|---|---|
0 | 2023-07-19 19:52:43.403 | [4.0, 3.0, 3710.0, 20000.0, 2.0, 0.0, 2.0, 5.0, 10.0, 2760.0, 950.0, 47.6696, -122.261, 3970.0, 20000.0, 79.0, 0.0, 0.0] | [1514079.4] | 0 |
As one last sample, we’ll run through roughly 1,000 inferences at once and show a few of the results. For this example we’ll use an Apache Arrow table, which has a smaller file size compared to uploading a pandas DataFrame JSON file. The inference result is returned as an arrow table, which we’ll convert into a pandas DataFrame to display the first 20 results.
time.sleep(5)
control_model_start = datetime.datetime.now()
batch_inferences = mainpipeline.infer_from_file('./data/xtest-1k.arrow')
large_inference_result = batch_inferences.to_pandas()
display(large_inference_result.head(20))
time | in.tensor | out.variable | check_failures | |
---|---|---|---|---|
0 | 2023-07-19 19:52:49.125 | [4.0, 2.5, 2900.0, 5505.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2900.0, 0.0, 47.6063, -122.02, 2970.0, 5251.0, 12.0, 0.0, 0.0] | [718013.75] | 0 |
1 | 2023-07-19 19:52:49.125 | [2.0, 2.5, 2170.0, 6361.0, 1.0, 0.0, 2.0, 3.0, 8.0, 2170.0, 0.0, 47.7109, -122.017, 2310.0, 7419.0, 6.0, 0.0, 0.0] | [615094.56] | 0 |
2 | 2023-07-19 19:52:49.125 | [3.0, 2.5, 1300.0, 812.0, 2.0, 0.0, 0.0, 3.0, 8.0, 880.0, 420.0, 47.5893, -122.317, 1300.0, 824.0, 6.0, 0.0, 0.0] | [448627.72] | 0 |
3 | 2023-07-19 19:52:49.125 | [4.0, 2.5, 2500.0, 8540.0, 2.0, 0.0, 0.0, 3.0, 9.0, 2500.0, 0.0, 47.5759, -121.994, 2560.0, 8475.0, 24.0, 0.0, 0.0] | [758714.2] | 0 |
4 | 2023-07-19 19:52:49.125 | [3.0, 1.75, 2200.0, 11520.0, 1.0, 0.0, 0.0, 4.0, 7.0, 2200.0, 0.0, 47.7659, -122.341, 1690.0, 8038.0, 62.0, 0.0, 0.0] | [513264.7] | 0 |
5 | 2023-07-19 19:52:49.125 | [3.0, 2.0, 2140.0, 4923.0, 1.0, 0.0, 0.0, 4.0, 8.0, 1070.0, 1070.0, 47.6902, -122.339, 1470.0, 4923.0, 86.0, 0.0, 0.0] | [668288.0] | 0 |
6 | 2023-07-19 19:52:49.125 | [4.0, 3.5, 3590.0, 5334.0, 2.0, 0.0, 2.0, 3.0, 9.0, 3140.0, 450.0, 47.6763, -122.267, 2100.0, 6250.0, 9.0, 0.0, 0.0] | [1004846.5] | 0 |
7 | 2023-07-19 19:52:49.125 | [3.0, 2.0, 1280.0, 960.0, 2.0, 0.0, 0.0, 3.0, 9.0, 1040.0, 240.0, 47.602, -122.311, 1280.0, 1173.0, 0.0, 0.0, 0.0] | [684577.2] | 0 |
8 | 2023-07-19 19:52:49.125 | [4.0, 2.5, 2820.0, 15000.0, 2.0, 0.0, 0.0, 4.0, 9.0, 2820.0, 0.0, 47.7255, -122.101, 2440.0, 15000.0, 29.0, 0.0, 0.0] | [727898.1] | 0 |
9 | 2023-07-19 19:52:49.125 | [3.0, 2.25, 1790.0, 11393.0, 1.0, 0.0, 0.0, 3.0, 8.0, 1790.0, 0.0, 47.6297, -122.099, 2290.0, 11894.0, 36.0, 0.0, 0.0] | [559631.1] | 0 |
10 | 2023-07-19 19:52:49.125 | [3.0, 1.5, 1010.0, 7683.0, 1.5, 0.0, 0.0, 5.0, 7.0, 1010.0, 0.0, 47.72, -122.318, 1550.0, 7271.0, 61.0, 0.0, 0.0] | [340764.53] | 0 |
11 | 2023-07-19 19:52:49.125 | [3.0, 2.0, 1270.0, 1323.0, 3.0, 0.0, 0.0, 3.0, 8.0, 1270.0, 0.0, 47.6934, -122.342, 1330.0, 1323.0, 8.0, 0.0, 0.0] | [442168.06] | 0 |
12 | 2023-07-19 19:52:49.125 | [4.0, 1.75, 2070.0, 9120.0, 1.0, 0.0, 0.0, 4.0, 7.0, 1250.0, 820.0, 47.6045, -122.123, 1650.0, 8400.0, 57.0, 0.0, 0.0] | [630865.6] | 0 |
13 | 2023-07-19 19:52:49.125 | [4.0, 1.0, 1620.0, 4080.0, 1.5, 0.0, 0.0, 3.0, 7.0, 1620.0, 0.0, 47.6696, -122.324, 1760.0, 4080.0, 91.0, 0.0, 0.0] | [559631.1] | 0 |
14 | 2023-07-19 19:52:49.125 | [4.0, 3.25, 3990.0, 9786.0, 2.0, 0.0, 0.0, 3.0, 9.0, 3990.0, 0.0, 47.6784, -122.026, 3920.0, 8200.0, 10.0, 0.0, 0.0] | [909441.1] | 0 |
15 | 2023-07-19 19:52:49.125 | [4.0, 2.0, 1780.0, 19843.0, 1.0, 0.0, 0.0, 3.0, 7.0, 1780.0, 0.0, 47.4414, -122.154, 2210.0, 13500.0, 52.0, 0.0, 0.0] | [313096.0] | 0 |
16 | 2023-07-19 19:52:49.125 | [4.0, 2.5, 2130.0, 6003.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2130.0, 0.0, 47.4518, -122.12, 1940.0, 4529.0, 11.0, 0.0, 0.0] | [404040.8] | 0 |
17 | 2023-07-19 19:52:49.125 | [3.0, 1.75, 1660.0, 10440.0, 1.0, 0.0, 0.0, 3.0, 7.0, 1040.0, 620.0, 47.4448, -121.77, 1240.0, 10380.0, 36.0, 0.0, 0.0] | [292859.5] | 0 |
18 | 2023-07-19 19:52:49.125 | [3.0, 2.5, 2110.0, 4118.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2110.0, 0.0, 47.3878, -122.153, 2110.0, 4044.0, 25.0, 0.0, 0.0] | [338357.88] | 0 |
19 | 2023-07-19 19:52:49.125 | [4.0, 2.25, 2200.0, 11250.0, 1.5, 0.0, 0.0, 5.0, 7.0, 1300.0, 900.0, 47.6845, -122.201, 2320.0, 10814.0, 94.0, 0.0, 0.0] | [682284.6] | 0 |
Graph of Prices
Here’s a distribution plot of the inferences to view the values, with the X axis being the house price in millions, and the Y axis the number of houses fitting in a bin grouping. The majority of houses are in the $250,000 to $500,000 range, with some outliers in the far end.
import matplotlib.pyplot as plt
houseprices = pd.DataFrame({'sell_price': large_inference_result['out.variable'].apply(lambda x: x[0])})
houseprices.hist(column='sell_price', bins=75, grid=False, figsize=(12,8))
plt.axvline(x=0, color='gray', ls='--')
_ = plt.title('Distribution of predicted home sales price')
time.sleep(5)
control_model_end = datetime.datetime.now()
Pipeline Logs
Pipeline logs with standard pipeline steps are retrieved either with:
- Pipeline
logs
which returns either a pandas DataFrame or Apache Arrow table. - Pipeline
export_logs
which saves the logs either a pandas DataFrame JSON file or Apache Arrow table.
For full details, see the Wallaroo Documentation Pipeline Log Management guide.
Pipeline Log Methods
The Pipeline logs
method accepts the following parameters.
Parameter | Type | Description |
---|---|---|
limit | Int (Optional) | Limits how many log records to display. Defaults to 100 . If there are more pipeline logs than are being displayed, the Warning message Pipeline log record limit exceeded will be displayed. For example, if 100 log files were requested and there are a total of 1,000, the warning message will be displayed. |
start_datetimert and end_datetime | DateTime (Optional) | Limits logs to all logs between the start_datetime and end_datetime DateTime parameters. Both parameters must be provided. Submitting a logs() request with only start_datetime or end_datetime will generate an exception.If start_datetime and end_datetime are provided as parameters, then the records are returned in chronological order, with the oldest record displayed first. |
arrow | Boolean (Optional) | Defaults to False. If arrow is set to True , then the logs are returned as an Apache Arrow table. If arrow=False , then the logs are returned as a pandas DataFrame. |
The following examples demonstrate displaying the logs, then displaying the logs between the control_model_start
and control_model_end
periods, then again retrieved as an Arrow table.
# pipeline log retrieval - reverse chronological order
display(mainpipeline.logs())
# pipeline log retrieval between two dates - chronological order
display(mainpipeline.logs(start_datetime=control_model_start, end_datetime=control_model_end))
# pipeline log retrieval limited to the last 5 an an arrow table
display(mainpipeline.logs(arrow=True))
Warning: There are more logs available. Please set a larger limit or request a file using export_logs.
time | in.tensor | out.variable | check_failures | |
---|---|---|---|---|
0 | 2023-07-19 19:44:05.909 | [3.0, 3.25, 4560.0, 13363.0, 1.0, 0.0, 4.0, 3.0, 11.0, 2760.0, 1800.0, 47.6204986572, -122.2139968872, 4060.0, 13362.0, 20.0, 0.0, 0.0] | [2005883.1] | 0 |
1 | 2023-07-19 19:44:05.909 | [6.0, 4.0, 5310.0, 12741.0, 2.0, 0.0, 2.0, 3.0, 10.0, 3600.0, 1710.0, 47.5695991516, -122.2129974365, 4190.0, 12632.0, 48.0, 0.0, 0.0] | [2016006.0] | 0 |
2 | 2023-07-19 19:44:05.909 | [4.0, 3.5, 4285.0, 9567.0, 2.0, 0.0, 1.0, 5.0, 10.0, 3485.0, 800.0, 47.6433982849, -122.408996582, 2960.0, 6902.0, 68.0, 0.0, 0.0] | [1886959.4] | 0 |
3 | 2023-07-19 19:44:05.909 | [3.0, 3.25, 4560.0, 13363.0, 1.0, 0.0, 4.0, 3.0, 11.0, 2760.0, 1800.0, 47.6204986572, -122.2139968872, 4060.0, 13362.0, 20.0, 0.0, 0.0] | [2005883.1] | 0 |
4 | 2023-07-19 19:44:05.909 | [4.0, 3.0, 3710.0, 20000.0, 2.0, 0.0, 2.0, 5.0, 10.0, 2760.0, 950.0, 47.6696014404, -122.2610015869, 3970.0, 20000.0, 79.0, 0.0, 0.0] | [1514079.8] | 0 |
... | ... | ... | ... | ... |
95 | 2023-07-19 19:44:05.909 | [4.0, 3.0, 4750.0, 21701.0, 1.5, 0.0, 0.0, 5.0, 11.0, 4750.0, 0.0, 47.645401001, -122.2180023193, 3120.0, 18551.0, 38.0, 0.0, 0.0] | [2002393.5] | 0 |
96 | 2023-07-19 19:44:05.909 | [3.0, 2.5, 5403.0, 24069.0, 2.0, 1.0, 4.0, 4.0, 12.0, 5403.0, 0.0, 47.4169006348, -122.3479995728, 3980.0, 104374.0, 39.0, 0.0, 0.0] | [1946437.2] | 0 |
97 | 2023-07-19 19:44:05.909 | [4.0, 3.0, 3710.0, 20000.0, 2.0, 0.0, 2.0, 5.0, 10.0, 2760.0, 950.0, 47.6696014404, -122.2610015869, 3970.0, 20000.0, 79.0, 0.0, 0.0] | [1514079.8] | 0 |
98 | 2023-07-19 19:44:05.909 | [5.0, 4.25, 4860.0, 9453.0, 1.5, 0.0, 1.0, 5.0, 10.0, 3100.0, 1760.0, 47.6195983887, -122.2860031128, 3150.0, 8557.0, 109.0, 0.0, 0.0] | [1910823.8] | 0 |
99 | 2023-07-19 19:44:05.909 | [3.0, 3.25, 4560.0, 13363.0, 1.0, 0.0, 4.0, 3.0, 11.0, 2760.0, 1800.0, 47.6204986572, -122.2139968872, 4060.0, 13362.0, 20.0, 0.0, 0.0] | [2005883.1] | 0 |
100 rows × 4 columns
Warning: Pipeline log size limit exceeded. Please request logs using export_logs
time | in.tensor | out.variable | check_failures | |
---|---|---|---|---|
0 | 2023-07-19 19:52:49.125 | [4.0, 2.5, 2900.0, 5505.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2900.0, 0.0, 47.6063, -122.02, 2970.0, 5251.0, 12.0, 0.0, 0.0] | [718013.75] | 0 |
1 | 2023-07-19 19:52:49.125 | [2.0, 2.5, 2170.0, 6361.0, 1.0, 0.0, 2.0, 3.0, 8.0, 2170.0, 0.0, 47.7109, -122.017, 2310.0, 7419.0, 6.0, 0.0, 0.0] | [615094.56] | 0 |
2 | 2023-07-19 19:52:49.125 | [3.0, 2.5, 1300.0, 812.0, 2.0, 0.0, 0.0, 3.0, 8.0, 880.0, 420.0, 47.5893, -122.317, 1300.0, 824.0, 6.0, 0.0, 0.0] | [448627.72] | 0 |
3 | 2023-07-19 19:52:49.125 | [4.0, 2.5, 2500.0, 8540.0, 2.0, 0.0, 0.0, 3.0, 9.0, 2500.0, 0.0, 47.5759, -121.994, 2560.0, 8475.0, 24.0, 0.0, 0.0] | [758714.2] | 0 |
4 | 2023-07-19 19:52:49.125 | [3.0, 1.75, 2200.0, 11520.0, 1.0, 0.0, 0.0, 4.0, 7.0, 2200.0, 0.0, 47.7659, -122.341, 1690.0, 8038.0, 62.0, 0.0, 0.0] | [513264.7] | 0 |
... | ... | ... | ... | ... |
663 | 2023-07-19 19:52:49.125 | [3.0, 2.5, 2100.0, 5060.0, 2.0, 0.0, 0.0, 3.0, 7.0, 2100.0, 0.0, 47.563, -122.298, 1520.0, 2468.0, 8.0, 0.0, 0.0] | [642519.75] | 0 |
664 | 2023-07-19 19:52:49.125 | [4.0, 1.0, 1540.0, 115434.0, 1.5, 0.0, 0.0, 4.0, 7.0, 1540.0, 0.0, 47.4163, -122.22, 2027.0, 23522.0, 91.0, 0.0, 0.0] | [301714.75] | 0 |
665 | 2023-07-19 19:52:49.125 | [3.0, 2.0, 940.0, 5000.0, 1.0, 0.0, 0.0, 3.0, 7.0, 880.0, 60.0, 47.6771, -122.398, 1420.0, 5000.0, 74.0, 0.0, 0.0] | [448627.72] | 0 |
666 | 2023-07-19 19:52:49.125 | [2.0, 2.25, 1620.0, 1841.0, 2.0, 0.0, 0.0, 3.0, 8.0, 1540.0, 80.0, 47.5483, -122.004, 1530.0, 1831.0, 10.0, 0.0, 0.0] | [544392.1] | 0 |
667 | 2023-07-19 19:52:49.125 | [4.0, 3.5, 3180.0, 12528.0, 2.0, 0.0, 1.0, 4.0, 9.0, 2060.0, 1120.0, 47.7058, -122.379, 2850.0, 11410.0, 36.0, 0.0, 0.0] | [944006.75] | 0 |
668 rows × 4 columns
Warning: There are more logs available. Please set a larger limit or request a file using export_logs.
pyarrow.Table
time: timestamp[ms]
in.tensor: list<item: double> not null
child 0, item: double
out.variable: list<inner: float not null> not null
child 0, inner: float not null
check_failures: int8
time: [[2023-07-19 19:44:05.909,2023-07-19 19:44:05.909,2023-07-19 19:44:05.909,2023-07-19 19:44:05.909,2023-07-19 19:44:05.909,…,2023-07-19 19:44:05.909,2023-07-19 19:44:05.909,2023-07-19 19:44:05.909,2023-07-19 19:44:05.909,2023-07-19 19:44:05.909]]
in.tensor: [[[3,3.25,4560,13363,1,…,4060,13362,20,0,0],[6,4,5310,12741,2,…,4190,12632,48,0,0],…,[5,4.25,4860,9453,1.5,…,3150,8557,109,0,0],[3,3.25,4560,13363,1,…,4060,13362,20,0,0]]]
out.variable: [[[2005883.1],[2016006],…,[1910823.8],[2005883.1]]]
check_failures: [[0,0,0,0,0,…,0,0,0,0,0]]
Anomaly Detection through Validations
Anomaly detection allows organizations to set validation parameters in a pipeline. A validation is added to a pipeline to test data based on an expression, and flag any inferences where the validation failed inference result and the pipeline logs.
Validations are added through the Pipeline add_validation(name, validation)
command which uses the following parameters:
Parameter | Type | Description |
---|---|---|
name | String (Required) | The name of the validation. |
Validation | Expression (Required) | The validation test command in the format model_name.outputs][field][index] {Operation} {Value} . |
For this example, we want to detect the outputs of housing_model_control
and validate that values are less than 1,500,000
. Any outputs greater than that will trigger a check_failure
which is shown in the output.
## Add the validation to the pipeline
mainpipeline = mainpipeline.add_validation('price too high', housing_model_control.outputs[0][0] < 1500000.0)
mainpipeline.deploy()
name | housepricesagapipeline |
---|---|
created | 2023-07-19 19:40:14.556167+00:00 |
last_updated | 2023-07-19 19:53:11.562402+00:00 |
deployed | True |
tags | |
versions | 2513fed6-ad9e-48da-9830-716d81bd2f42, 34fbdbd2-37b6-4c87-a73b-2bc37356f107, 85f43a06-6a2d-445e-9ba7-6511a7c8300c, 6f21cab0-689d-4c7a-be9a-317786a1173d, ee06f9e2-56d8-413d-8245-0fb37dc377ed, ccad17d0-8121-42ac-9311-14777667ae61, a77de963-1df0-4f2f-92d7-cc010277e726, 44f668f8-938c-4542-8c79-13128891171c, 423875eb-d31a-48fa-bcc5-a3fe4d45e4d9 |
steps | housepricesagacontrol |
Validation Testing
Two validations will be tested:
- One that should return a house value lower than 1,500,000. The validation will pass so
check_failure
will be 0. - The other than should return a house value greater than 1,500,000. The validation will fail, so
check_failure
will be 1.
validation_start = datetime.datetime.now()
# Small value home
normal_input = pd.DataFrame.from_records({
"tensor": [[
3.0,
2.25,
1620.0,
997.0,
2.5,
0.0,
0.0,
3.0,
8.0,
1540.0,
80.0,
47.5400009155,
-122.0260009766,
1620.0,
1068.0,
4.0,
0.0,
0.0
]]
}
)
small_result = mainpipeline.infer(normal_input)
display(small_result.loc[:,["time", "out.variable", "check_failures"]])
time | out.variable | check_failures | |
---|---|---|---|
0 | 2023-07-19 19:53:17.034 | [544392.06] | 0 |
# Big value home
big_input = pd.DataFrame.from_records({
"tensor": [[
4.0,
4.5,
5770.0,
10050.0,
1.0,
0.0,
3.0,
5.0,
9.0,
3160.0,
2610.0,
47.6769981384,
-122.2750015259,
2950.0,
6700.0,
65.0,
0.0,
0.0
]]
}
)
big_result = mainpipeline.infer(big_input)
display(big_result.loc[:,["time", "out.variable", "check_failures"]])
time | out.variable | check_failures | |
---|---|---|---|
0 | 2023-07-19 19:53:17.439 | [1689843.1] | 1 |
Anomaly Results
We’ll run through our previous batch, this time showing only those results outside of the validation, and a graph showing where the anomalies are against the other results.
batch_inferences = mainpipeline.infer_from_file('./data/xtest-1k.arrow')
large_inference_result = batch_inferences.to_pandas()
# Display only the anomalous results
display(large_inference_result[large_inference_result["check_failures"] > 0].loc[:,["time", "out.variable", "check_failures"]])
time | out.variable | check_failures | |
---|---|---|---|
30 | 2023-07-19 19:53:18.004 | [1514079.8] | 1 |
248 | 2023-07-19 19:53:18.004 | [1967344.1] | 1 |
255 | 2023-07-19 19:53:18.004 | [2002393.5] | 1 |
556 | 2023-07-19 19:53:18.004 | [1886959.4] | 1 |
698 | 2023-07-19 19:53:18.004 | [1689843.2] | 1 |
711 | 2023-07-19 19:53:18.004 | [1946437.2] | 1 |
722 | 2023-07-19 19:53:18.004 | [2005883.1] | 1 |
782 | 2023-07-19 19:53:18.004 | [1910823.8] | 1 |
965 | 2023-07-19 19:53:18.004 | [2016006.0] | 1 |
import matplotlib.pyplot as plt
houseprices = pd.DataFrame({'sell_price': large_inference_result['out.variable'].apply(lambda x: x[0])})
houseprices.hist(column='sell_price', bins=75, grid=False, figsize=(12,8))
plt.axvline(x=1500000, color='red', ls='--')
_ = plt.title('Distribution of predicted home sales price')
Assays
Wallaroo assays provide a method for detecting input or model drift. These can be triggered either when unexpected input is provided for the inference, or when the model needs to be retrained from changing environment conditions.
Wallaroo assays can track either an input field and its index, or an output field and its index. For full details, see the Wallaroo Assays Management Guide.
For this example, we will:
- Perform sample inferences based on lower priced houses.
- Create an assay with the baseline set off those lower priced houses.
- Generate inferences spread across all house values, plus specific set of high priced houses to trigger the assay alert.
- Run an interactive assay to show the detection of values outside the established baseline.
Assay Generation
To start the demonstration, we’ll create a baseline of values from houses with small estimated prices and set that as our baseline. Assays are typically run on a 24 hours interval based on a 24 hour window of data, but we’ll bypass that by setting our baseline time even shorter.
small_houses_inputs = pd.read_json('./data/smallinputs.df.json')
baseline_size = 500
# Where the baseline data will start
baseline_start = datetime.datetime.now()
# These inputs will be random samples of small priced houses. Around 30,000 is a good number
small_houses = small_houses_inputs.sample(baseline_size, replace=True).reset_index(drop=True)
# Wait 30 seconds to set this data apart from the rest
time.sleep(30)
mainpipeline.infer(small_houses)
# Set the baseline end
baseline_end = datetime.datetime.now()
# Set the name of the assay
assay_name=f"small houses test {suffix}"
# Now build the assay, based on the start and end of our baseline time,
# and tracking the output variable index 0
assay_builder = wl.build_assay(assay_name, mainpipeline, model_name_control, baseline_start, baseline_end
).add_iopath("output variable 0")
# Perform an interactive baseline run to set out baseline, then show the baseline statistics
baseline_run = assay_builder.build().interactive_baseline_run()
baseline_run.baseline_stats()
Baseline | |
---|---|
count | 500 |
min | 236238.65625 |
max | 1489624.5 |
mean | 517129.749094 |
median | 448627.71875 |
std | 233679.013558 |
start | 2023-07-19T19:58:51.399138Z |
end | 2023-07-19T19:59:22.406972Z |
display(assay_builder.baseline_dataframe().loc[:, ["time", "output_variable_0"]])
time | output_variable_0 | |
---|---|---|
0 | 1689796762275 | 701940.68750 |
1 | 1689796762275 | 442168.06250 |
2 | 1689796762275 | 725184.25000 |
3 | 1689796762275 | 642519.75000 |
4 | 1689796762275 | 450867.56250 |
... | ... | ... |
495 | 1689796762275 | 363491.68750 |
496 | 1689796762275 | 705013.43750 |
497 | 1689796762275 | 559631.12500 |
498 | 1689796762275 | 340764.53125 |
499 | 1689796762275 | 320863.75000 |
500 rows × 2 columns
Now we’ll perform some inferences with a spread of values, then a larger set with a set of larger house values to trigger our assay alert.
Because our assay windows are 1 minutes, we’ll need to stagger our inference values to be set into the proper windows. This will take about 4 minutes.
# set the number of inferences to use
inference_size = 1000
# Get a spread of house values
regular_houses_inputs = pd.read_json('./data/xtest-1k.df.json', orient="records")
regular_houses = regular_houses_inputs.sample(inference_size, replace=True).reset_index(drop=True)
# And a spread of large house values
big_houses_inputs = pd.read_json('./data/biginputs.df.json', orient="records")
big_houses = big_houses_inputs.sample(inference_size, replace=True).reset_index(drop=True)
# Set the start for our assay window period. Adjust date for the historical data used
assay_window_start = assay_window_start = datetime.datetime.now()
# Run a set of regular house values, spread across 90 seconds
# Use to generate inferences now if historical data doesn't exit
for x in range(3):
mainpipeline.infer(regular_houses)
time.sleep(35)
mainpipeline.infer(big_houses)
time.sleep(35)
# End our assay window period
assay_window_end = datetime.datetime.now()
# now set up our interactive assay based on the window set above.
assay_builder = assay_builder.add_run_until(assay_window_end)
# We don't have many records at the moment, so set the width to 1 minute so it'll slice each
# one minute interval into a window to analyze
assay_builder.window_builder().add_width(minutes=1)
# Build the assay and then do an interactive run rather than waiting for the next interval
assay_config = assay_builder.build()
assay_results = assay_config.interactive_run()
# Show how many assay windows were analyzed, then show the chart
print(f"Generated {len(assay_results)} analyses")
assay_results.chart_scores()
Generated 8 analyses
# Display the results as a DataFrame - we're mainly interested in the score and whether the
# alert threshold was triggered
display(assay_results.to_dataframe().loc[:, ["score", "start", "alert_threshold", "status"]])
score | start | alert_threshold | status | |
---|---|---|---|---|
0 | 2.677746 | 2023-07-19T19:59:22.406972+00:00 | 0.25 | Alert |
1 | 2.677746 | 2023-07-19T20:00:22.406972+00:00 | 0.25 | Alert |
2 | 0.027453 | 2023-07-19T20:01:22.406972+00:00 | 0.25 | Ok |
3 | 8.868504 | 2023-07-19T20:02:22.406972+00:00 | 0.25 | Alert |
4 | 0.036661 | 2023-07-19T20:07:22.406972+00:00 | 0.25 | Ok |
5 | 2.660477 | 2023-07-19T20:08:22.406972+00:00 | 0.25 | Alert |
6 | 2.660477 | 2023-07-19T20:09:22.406972+00:00 | 0.25 | Alert |
7 | 0.026399 | 2023-07-19T20:10:22.406972+00:00 | 0.25 | Ok |
display(assay_name)
'small houses test'
assay_builder.upload()
2
The assay is now visible through the Wallaroo UI by selecting the workspace, then the pipeline, then Insights.
Shadow Deploy
Let’s assume that after analyzing the assay information we want to test two challenger models to our control. We do that with the Shadow Deploy pipeline step.
In Shadow Deploy, the pipeline step is added with the add_shadow_deploy
method, with the champion model listed first, then an array of challenger models after. All inference data is fed to all models, with the champion results displayed in the out.variable
column, and the shadow results in the format out_{model name}.variable
. For example, since we named our challenger models housingchallenger01
and housingchallenger02
, the columns out_housingchallenger01.variable
and out_housingchallenger02.variable
have the shadow deployed model results.
For this example, we will remove the previous pipeline step, then replace it with a shadow deploy step with rf_model.onnx
as our champion, and models xgb_model.onnx
and gbr_model.onnx
as the challengers. We’ll deploy the pipeline and prepare it for sample inferences.
# Upload the challenger models
model_name_challenger01 = 'housingchallenger01'
model_file_name_challenger01 = './models/xgb_model.onnx'
model_name_challenger02 = 'housingchallenger02'
model_file_name_challenger02 = './models/gbr_model.onnx'
housing_model_challenger01 = (wl.upload_model(model_name_challenger01,
model_file_name_challenger01,
framework=Framework.ONNX)
.configure()
)
housing_model_challenger02 = (wl.upload_model(model_name_challenger02,
model_file_name_challenger02,
framework=Framework.ONNX)
.configure()
)
# Undeploy the pipeline
mainpipeline.clear()
# Add the new shadow deploy step with our challenger models
mainpipeline.add_shadow_deploy(housing_model_control, [housing_model_challenger01, housing_model_challenger02])
# Deploy the pipeline with the new shadow step
mainpipeline.deploy()
name | housepricesagapipeline |
---|---|
created | 2023-07-19 19:40:14.556167+00:00 |
last_updated | 2023-07-19 20:12:24.670888+00:00 |
deployed | True |
tags | |
versions | 57cd5d2b-316a-4ed6-8730-4a88ee251d93, 2513fed6-ad9e-48da-9830-716d81bd2f42, 34fbdbd2-37b6-4c87-a73b-2bc37356f107, 85f43a06-6a2d-445e-9ba7-6511a7c8300c, 6f21cab0-689d-4c7a-be9a-317786a1173d, ee06f9e2-56d8-413d-8245-0fb37dc377ed, ccad17d0-8121-42ac-9311-14777667ae61, a77de963-1df0-4f2f-92d7-cc010277e726, 44f668f8-938c-4542-8c79-13128891171c, 423875eb-d31a-48fa-bcc5-a3fe4d45e4d9 |
steps | housepricesagacontrol |
Shadow Deploy Sample Inference
We’ll now use our same sample data for an inference to our shadow deployed pipeline, then display the first 20 results with just the comparative outputs.
shadow_result = mainpipeline.infer_from_file('./data/xtest-1k.arrow')
shadow_outputs = shadow_result.to_pandas()
display(shadow_outputs.loc[0:20,['out.variable','out_housingchallenger01.variable','out_housingchallenger02.variable']])
out.variable | out_housingchallenger01.variable | out_housingchallenger02.variable | |
---|---|---|---|
0 | [718013.75] | [659806.0] | [704901.9] |
1 | [615094.56] | [732883.5] | [695994.44] |
2 | [448627.72] | [419508.84] | [416164.8] |
3 | [758714.2] | [634028.8] | [655277.2] |
4 | [513264.7] | [427209.44] | [426854.66] |
5 | [668288.0] | [615501.9] | [632556.1] |
6 | [1004846.5] | [1139732.5] | [1100465.2] |
7 | [684577.2] | [498328.88] | [528278.06] |
8 | [727898.1] | [722664.4] | [659439.94] |
9 | [559631.1] | [525746.44] | [534331.44] |
10 | [340764.53] | [376337.1] | [377187.2] |
11 | [442168.06] | [382053.12] | [403964.3] |
12 | [630865.6] | [505608.97] | [528991.3] |
13 | [559631.1] | [603260.5] | [612201.75] |
14 | [909441.1] | [969585.4] | [893874.7] |
15 | [313096.0] | [313633.75] | [318054.94] |
16 | [404040.8] | [360413.56] | [357816.75] |
17 | [292859.5] | [316674.94] | [294034.7] |
18 | [338357.88] | [299907.44] | [323254.3] |
19 | [682284.6] | [811896.75] | [770916.7] |
20 | [583765.94] | [573618.5] | [549141.4] |
A/B Testing
A/B Testing is another method of comparing and testing models. Like shadow deploy, multiple models are compared against the champion or control models. The difference is that instead of submitting the inference data to all models, then tracking the outputs of all of the models, the inference inputs are off of a ratio and other conditions.
For this example, we’ll be using a 1:1:1 ratio with a random split between the champion model and the two challenger models. Each time an inference request is made, there is a random equal chance of any one of them being selected.
When the inference results and log entries are displayed, they include the column out._model_split
which displays:
Field | Type | Description |
---|---|---|
name | String | The model name used for the inference. |
version | String | The version of the model. |
sha | String | The sha hash of the model version. |
This is used to determine which model was used for the inference request.
# remove the shadow deploy steps
mainpipeline.clear()
# Add the a/b test step to the pipeline
mainpipeline.add_random_split([(1, housing_model_control), (1, housing_model_challenger01), (1, housing_model_challenger02)], "session_id")
mainpipeline.deploy()
# Perform sample inferences of 20 rows and display the results
ab_date_start = datetime.datetime.now()
abtesting_inputs = pd.read_json('./data/xtest-1k.df.json')
df = pd.DataFrame(columns=["model", "value"])
for index, row in abtesting_inputs.sample(20).iterrows():
result = mainpipeline.infer(row.to_frame('tensor').reset_index())
value = result.loc[0]["out.variable"]
model = json.loads(result.loc[0]["out._model_split"][0])['name']
df = df.append({'model': model, 'value': value}, ignore_index=True)
display(df)
ab_date_end = datetime.datetime.now()
model | value | |
---|---|---|
0 | housepricesagacontrol | [435628.72] |
1 | housingchallenger02 | [331885.06] |
2 | housepricesagacontrol | [987974.8] |
3 | housingchallenger01 | [418809.63] |
4 | housepricesagacontrol | [765468.9] |
5 | housingchallenger01 | [1497691.9] |
6 | housepricesagacontrol | [342604.47] |
7 | housingchallenger02 | [280919.44] |
8 | housingchallenger01 | [391736.7] |
9 | housingchallenger01 | [420496.0] |
10 | housingchallenger01 | [663045.3] |
11 | housingchallenger02 | [617112.0] |
12 | housingchallenger01 | [686057.06] |
13 | housepricesagacontrol | [712309.9] |
14 | housingchallenger02 | [817921.4] |
15 | housepricesagacontrol | [642519.7] |
16 | housingchallenger02 | [423905.3] |
17 | housingchallenger02 | [731539.0] |
18 | housingchallenger01 | [442630.16] |
19 | housingchallenger02 | [719696.1] |
Model Swap
Now that we’ve completed our testing, we can swap our deployed model in the original housepricingpipeline
with one we feel works better.
We’ll start by removing the A/B Testing pipeline step, then going back to the single pipeline step with the champion model and perform a test inference.
When going from a testing step such as A/B Testing or Shadow Deploy, it is best to undeploy the pipeline, change the steps, then deploy the pipeline. In a production environment, there should be two pipelines: One for production, the other for testing models. Since this example uses one pipeline for simplicity, we will undeploy our main pipeline and reset it back to a one-step pipeline with the current champion model as our pipeline step.
Once done, we’ll perform the hot swap with the model gbr_model.onnx
, which was labeled housing_model_challenger02
in a previous step. We’ll do an inference with the same data as used with the challenger model. Note that previously, the inference through the original model returned [718013.7]
.
mainpipeline.undeploy()
# remove the shadow deploy steps
mainpipeline.clear()
mainpipeline.add_model_step(housing_model_control).deploy()
# Inference test
normal_input = pd.DataFrame.from_records({"tensor": [[4.0,
2.25,
2200.0,
11250.0,
1.5,
0.0,
0.0,
5.0,
7.0,
1300.0,
900.0,
47.6845,
-122.201,
2320.0,
10814.0,
94.0,
0.0,
0.0]]})
controlresult = mainpipeline.infer(normal_input)
display(controlresult)
time | in.tensor | out.variable | check_failures | |
---|---|---|---|---|
0 | 2023-07-19 20:13:34.669 | [4.0, 2.25, 2200.0, 11250.0, 1.5, 0.0, 0.0, 5.0, 7.0, 1300.0, 900.0, 47.6845, -122.201, 2320.0, 10814.0, 94.0, 0.0, 0.0] | [682284.56] | 0 |
Now we’ll “hot swap” the control model. We don’t have to deploy the pipeline - we can just swap the model out in that pipeline step and continue with only a millisecond or two lost while the swap was performed.
# Perform hot swap
mainpipeline.replace_with_model_step(0, housing_model_challenger02).deploy()
# inference after model swap
normal_input = pd.DataFrame.from_records({"tensor": [[4.0,
2.25,
2200.0,
11250.0,
1.5,
0.0,
0.0,
5.0,
7.0,
1300.0,
900.0,
47.6845,
-122.201,
2320.0,
10814.0,
94.0,
0.0,
0.0]]})
challengerresult = mainpipeline.infer(normal_input)
display(challengerresult)
time | in.tensor | out.variable | check_failures | |
---|---|---|---|---|
0 | 2023-07-19 20:13:37.309 | [4.0, 2.25, 2200.0, 11250.0, 1.5, 0.0, 0.0, 5.0, 7.0, 1300.0, 900.0, 47.6845, -122.201, 2320.0, 10814.0, 94.0, 0.0, 0.0] | [770916.6] | 0 |
# Display the difference between the two
display(f'Original model output: {controlresult.loc[0]["out.variable"]}')
display(f'Hot swapped model output: {challengerresult.loc[0]["out.variable"]}')
'Original model output: [682284.56]'
‘Hot swapped model output: [770916.6]’
Undeploy Main Pipeline
With the examples and tutorial complete, we will undeploy the main pipeline and return the resources back to the Wallaroo instance.
mainpipeline.undeploy()
name | housepricesagapipeline |
---|---|
created | 2023-07-19 19:40:14.556167+00:00 |
last_updated | 2023-07-19 20:13:35.089207+00:00 |
deployed | False |
tags | |
versions | 3170fb20-b0e4-4fdf-89d2-ca1444c1bcf8, 75ca4917-af81-4b47-884a-98ad67fa138f, 23f98d7c-27d7-456d-b85f-b69be85b6ebe, 57cd5d2b-316a-4ed6-8730-4a88ee251d93, 2513fed6-ad9e-48da-9830-716d81bd2f42, 34fbdbd2-37b6-4c87-a73b-2bc37356f107, 85f43a06-6a2d-445e-9ba7-6511a7c8300c, 6f21cab0-689d-4c7a-be9a-317786a1173d, ee06f9e2-56d8-413d-8245-0fb37dc377ed, ccad17d0-8121-42ac-9311-14777667ae61, a77de963-1df0-4f2f-92d7-cc010277e726, 44f668f8-938c-4542-8c79-13128891171c, 423875eb-d31a-48fa-bcc5-a3fe4d45e4d9 |
steps | housepricesagacontrol |
5.4 - Shadow Deployment Tutorial
This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.
Shadow Deployment Tutorial
Wallaroo provides a method of testing the same data against two different models or sets of models at the same time through shadow deployments otherwise known as parallel deployments. This allows data to be submitted to a pipeline with inferences running on two different sets of models. Typically this is performed on a model that is known to provide accurate results - the champion - and a model that is being tested to see if it provides more accurate or faster responses depending on the criteria known as the challengers. Multiple challengers can be tested against a single champion.
As described in the Wallaroo blog post The What, Why, and How of Model A/B Testing:
In data science, A/B tests can also be used to choose between two models in production, by measuring which model performs better in the real world. In this formulation, the control is often an existing model that is currently in production, sometimes called the champion. The treatment is a new model being considered to replace the old one. This new model is sometimes called the challenger….
Keep in mind that in machine learning, the terms experiments and trials also often refer to the process of finding a training configuration that works best for the problem at hand (this is sometimes called hyperparameter optimization).
When a shadow deployment is created, only the inference from the champion is returned in the InferenceResult Object data
, while the result data for the shadow deployments is stored in the InferenceResult Object shadow_data
.
The following tutorial will demonstrate how:
- Upload champion and challenger models into a Wallaroo instance.
- Create a shadow deployment in a Wallaroo pipeline.
- Perform an inference through a pipeline with a shadow deployment.
- View the
data
andshadow_data
results from the InferenceResult Object. - View the pipeline logs and pipeline shadow logs.
This tutorial provides the following:
dev_smoke_test.json
: Sample test data used for the inference testing.models/keras_ccfraud.onnx
: The champion model.models/modelA.onnx
: A challenger model.models/xgboost_ccfraud.onnx
: A challenger model.
All models are similar to the ones used for the Wallaroo-101 example included in the Wallaroo Tutorials repository.
Prerequisites
- A deployed Wallaroo instance
- The following Python libraries installed:
Steps
Import libraries
The first step is to import the libraries required.
import wallaroo
from wallaroo.object import EntityNotFoundError
# used to display dataframe information without truncating
from IPython.display import display
import pandas as pd
pd.set_option('display.max_colwidth', None)
Connect to the Wallaroo Instance
The first step is to connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. For more information on Wallaroo Client settings, see the Client Connection guide.
# Login through local Wallaroo instance
wl = wallaroo.Client()
Set Variables
The following variables are used to create or use existing workspaces, pipelines, and upload the models. Adjust them based on your Wallaroo instance and organization requirements.
import string
import random
suffix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
workspace_name = f'ccfraudcomparisondemo{suffix}'
pipeline_name = f'ccshadow{suffix}'
champion_model_name = f'ccfraud-lstm{suffix}'
champion_model_file = 'models/keras_ccfraud.onnx'
shadow_model_01_name = f'ccfraud-xgb{suffix}'
shadow_model_01_file = 'models/xgboost_ccfraud.onnx'
shadow_model_02_name = f'ccfraud-rf{suffix}'
shadow_model_02_file = 'models/modelA.onnx'
Workspace and Pipeline
The following creates or connects to an existing workspace based on the variable workspace_name
, and creates or connects to a pipeline based on the variable pipeline_name
.
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
pipeline = wl.build_pipeline(name)
return pipeline
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
pipeline = get_pipeline(pipeline_name)
pipeline
name | ccshadoweonn |
---|---|
created | 2023-05-19 15:13:48.963815+00:00 |
last_updated | 2023-05-19 15:13:48.963815+00:00 |
deployed | (none) |
tags | |
versions | 08f4c75f-3e61-48d6-ac76-38e6dddcfaf6 |
steps |
Load the Models
The models will be uploaded into the current workspace based on the variable names set earlier and listed as the champion
, model2
and model3
.
champion = wl.upload_model(champion_model_name, champion_model_file, framework=wallaroo.framework.Framework.ONNX).configure()
model2 = wl.upload_model(shadow_model_01_name, shadow_model_01_file, framework=wallaroo.framework.Framework.ONNX).configure()
model3 = wl.upload_model(shadow_model_02_name, shadow_model_02_file, framework=wallaroo.framework.Framework.ONNX).configure()
Create Shadow Deployment
A shadow deployment is created using the add_shadow_deploy(champion, challengers[])
method where:
champion
: The model that will be primarily used for inferences run through the pipeline. Inference results will be returned through the Inference Object’sdata
element.challengers[]
: An array of models that will be used for inferences iteratively. Inference results will be returned through the Inference Object’sshadow_data
element.
pipeline.add_shadow_deploy(champion, [model2, model3])
pipeline.deploy()
name | ccshadoweonn |
---|---|
created | 2023-05-19 15:13:48.963815+00:00 |
last_updated | 2023-05-19 15:13:53.060392+00:00 |
deployed | True |
tags | |
versions | c6c28139-dd9d-4fdf-b7fc-d391bae58bc8, 08f4c75f-3e61-48d6-ac76-38e6dddcfaf6 |
steps | ccfraud-lstmeonn |
Run Test Inference
Using the data from sample_data_file
, a test inference will be made.
For Arrow enabled Wallaroo instances the model outputs are listed by column. The output data is set by the term out
, followed by the name of the model. For the default model, this is out.dense_1
, while the shadow deployed models are in the format out_{model name}.variable
, where {model name}
is the name of the shadow deployed model.
For Arrow disabled environments, the output is from the Wallaroo InferenceResult object.### Run Test Inference
Using the data from sample_data_file
, a test inference will be made. As mentioned earlier, the inference results from the champion
model will be available in the returned InferenceResult Object’s data
element, while inference results from each of the challenger
models will be in the returned InferenceResult Object’s shadow_data
element.
sample_data_file = './smoke_test.df.json'
response = pipeline.infer_from_file(sample_data_file)
display(response)
time | in.tensor | out.dense_1 | check_failures | out_ccfraud-rfeonn.variable | out_ccfraud-xgbeonn.variable | |
---|---|---|---|---|---|---|
0 | 2023-05-19 15:14:08.898 | [1.0678324729, 0.2177810266, -1.7115145262, 0.682285721, 1.0138553067, -0.4335000013, 0.7395859437, -0.2882839595, -0.447262688, 0.5146124988, 0.3791316964, 0.5190619748, -0.4904593222, 1.1656456469, -0.9776307444, -0.6322198963, -0.6891477694, 0.1783317857, 0.1397992467, -0.3554220649, 0.4394217877, 1.4588397512, -0.3886829615, 0.4353492889, 1.7420053483, -0.4434654615, -0.1515747891, -0.2668451725, -1.4549617756] | [0.0014974177] | 0 | [1.0] | [0.0005066991] |
View Pipeline Logs
With the inferences complete, we can retrieve the log data from the pipeline with the pipeline logs
method. Note that for each inference request, the logs return one entry per model. For this example, for one inference request three log entries will be created.
pipeline.logs()
time | in.tensor | out.dense_1 | check_failures | out_ccfraud-rfeonn.variable | out_ccfraud-xgbeonn.variable | |
---|---|---|---|---|---|---|
0 | 2023-05-19 15:14:08.898 | [1.0678324729, 0.2177810266, -1.7115145262, 0.682285721, 1.0138553067, -0.4335000013, 0.7395859437, -0.2882839595, -0.447262688, 0.5146124988, 0.3791316964, 0.5190619748, -0.4904593222, 1.1656456469, -0.9776307444, -0.6322198963, -0.6891477694, 0.1783317857, 0.1397992467, -0.3554220649, 0.4394217877, 1.4588397512, -0.3886829615, 0.4353492889, 1.7420053483, -0.4434654615, -0.1515747891, -0.2668451725, -1.4549617756] | [0.0014974177] | 0 | [1.0] | [0.0005066991] |
View Logs Per Model
Another way of displaying the logs would be to specify the model.
For Arrow enabled Wallaroo instances the model outputs are listed by column. The output data is set by the term out
, followed by the name of the model. For the default model, this is out.dense_1
, while the shadow deployed models are in the format out_{model name}.variable
, where {model name}
is the name of the shadow deployed model.
For arrow disabled Wallaroo instances, to view the inputs and results for the shadow deployed models, use the pipeline logs_shadow_deploy()
method. The results will be grouped by the inputs.
logs = pipeline.logs()
display(logs)
time | in.tensor | out.dense_1 | check_failures | out_ccfraud-rfeonn.variable | out_ccfraud-xgbeonn.variable | |
---|---|---|---|---|---|---|
0 | 2023-05-19 15:14:08.898 | [1.0678324729, 0.2177810266, -1.7115145262, 0.682285721, 1.0138553067, -0.4335000013, 0.7395859437, -0.2882839595, -0.447262688, 0.5146124988, 0.3791316964, 0.5190619748, -0.4904593222, 1.1656456469, -0.9776307444, -0.6322198963, -0.6891477694, 0.1783317857, 0.1397992467, -0.3554220649, 0.4394217877, 1.4588397512, -0.3886829615, 0.4353492889, 1.7420053483, -0.4434654615, -0.1515747891, -0.2668451725, -1.4549617756] | [0.0014974177] | 0 | [1.0] | [0.0005066991] |
Undeploy the Pipeline
With the tutorial complete, we undeploy the pipeline and return the resources back to the system.
pipeline.undeploy()
name | ccshadoweonn |
---|---|
created | 2023-05-19 15:13:48.963815+00:00 |
last_updated | 2023-05-19 15:13:53.060392+00:00 |
deployed | False |
tags | |
versions | c6c28139-dd9d-4fdf-b7fc-d391bae58bc8, 08f4c75f-3e61-48d6-ac76-38e6dddcfaf6 |
steps | ccfraud-lstmeonn |
6 - Using Jupyter Notebooks in Production
Using Jupyter Notebooks in Production
The following tutorials are available from the Wallaroo Tutorials Repository.
The following tutorials provide an example of an organization moving from experimentation to deployment in production using Jupyter Notebooks as the basis for code research and use. For this example, we can assume to main actors performing the following tasks.
Number | Notebook Sample | Task | Actor | Description |
---|---|---|---|---|
01 | 01_explore_and_train.ipynb | Data Exploration and Model Selection | Data Scientist | The data scientist evaluates the data and determines the best model to use to solve the proposed problems. |
02 | 02_automated_training_process.ipynd | Training Process Automation Setup | Data Scientist | The data scientist has selected the model and tested how to train it. In this phase, the data scientist tests automating the training process based on a data store. |
03 | 03_deploy_model.ipynb | Deploy the Model in Wallaroo | MLOps Engineer | The MLOps takes the trained model and deploys a Wallaroo pipeline with it to perform inferences on by feeding it data from a data store. |
04 | 04_regular_batch_inferences.ipynb | Regular Batch Inference | MLOps Engineer | With the pipeline deployed, regular inferences can be made and the results reported to a data store. |
Each Jupyter Notebook is arranged to demonstrate each step of the process.
Resources
The following resources are provided as part of this tutorial:
- data
data/seattle_housing_col_description.txt
: Describes the columns used as part data analysis.data/seattle_housing.csv
: Sample data of the Seattle, Washington housing market between 2014 and 2015.
- code
preprocess.py
: Formats the incoming data for the model.simdb.py
: A simulated database to demonstrate sending and receiving queries.wallaroo_client.py
: Additional methods used with the Wallaroo instance to create workspaces, etc.
6.1 - Data Exploration And Model Selection
The following tutorials are available from the Wallaroo Tutorials Repository.
Stage 1: Data Exploration And Model Selection
When starting a project, the data scientist focuses on exploration and experimentation, rather than turning the process into an immediate production system. This notebook presents a simplified view of this stage.
Resources
The following resources are used as part of this tutorial:
- data
data/seattle_housing_col_description.txt
: Describes the columns used as part data analysis.data/seattle_housing.csv
: Sample data of the Seattle, Washington housing market between 2014 and 2015.
- code
postprocess.py
: Formats the data after inference by the model is complete.preprocess.py
: Formats the incoming data for the model.simdb.py
: A simulated database to demonstrate sending and receiving queries.wallaroo_client.py
: Additional methods used with the Wallaroo instance to create workspaces, etc.
Steps
The following steps are part of this process:
- Retrieve Training Data: Connect to the data store and retrieve the training data.
- Data Transformations: Evaluate the data and train the model.
- Model Testing: Evaluate different models and determine which is best suited for the problem.
Import Libraries
First we’ll import the libraries we’ll be using to evaluate the data and test different models.
import numpy as np
import pandas as pd
import sklearn
import sklearn.ensemble
import xgboost as xgb
import seaborn
import matplotlib
import matplotlib.pyplot as plt
import simdb # module for the purpose of this demo to simulate pulling data from a database
matplotlib.rcParams["figure.figsize"] = (12,6)
# ignoring warnings for demonstration
import warnings
warnings.filterwarnings('ignore')
Retrieve Training Data
For training, we will use the data on all houses sold in this market with the last two years. As a reminder, this data pulled from a simulated database as an example of how to pull from an existing data store.
Only a few columns will be shown for display purposes.
conn = simdb.simulate_db_connection()
tablename = simdb.tablename
query = f"select * from {tablename} where date > DATE(DATE(), '-24 month') AND sale_price is not NULL"
print(query)
# read in the data
housing_data = pd.read_sql_query(query, conn)
conn.close()
housing_data.loc[:, ["id", "date", "list_price", "bedrooms", "bathrooms", "sqft_living", "sqft_lot"]]
select * from house_listings where date > DATE(DATE(), '-24 month') AND sale_price is not NULL
id | date | list_price | bedrooms | bathrooms | sqft_living | sqft_lot | |
---|---|---|---|---|---|---|---|
0 | 7129300520 | 2022-10-05 | 221900.0 | 3 | 1.00 | 1180 | 5650 |
1 | 6414100192 | 2022-12-01 | 538000.0 | 3 | 2.25 | 2570 | 7242 |
2 | 5631500400 | 2023-02-17 | 180000.0 | 2 | 1.00 | 770 | 10000 |
3 | 2487200875 | 2022-12-01 | 604000.0 | 4 | 3.00 | 1960 | 5000 |
4 | 1954400510 | 2023-02-10 | 510000.0 | 3 | 2.00 | 1680 | 8080 |
... | ... | ... | ... | ... | ... | ... | ... |
20518 | 263000018 | 2022-05-13 | 360000.0 | 3 | 2.50 | 1530 | 1131 |
20519 | 6600060120 | 2023-02-15 | 400000.0 | 4 | 2.50 | 2310 | 5813 |
20520 | 1523300141 | 2022-06-15 | 402101.0 | 2 | 0.75 | 1020 | 1350 |
20521 | 291310100 | 2023-01-08 | 400000.0 | 3 | 2.50 | 1600 | 2388 |
20522 | 1523300157 | 2022-10-07 | 325000.0 | 2 | 0.75 | 1020 | 1076 |
20523 rows × 7 columns
Data transformations
To improve relative error performance, we will predict on log10
of the sale price.
Predict on log10 price to try to improve relative error performance
housing_data['logprice'] = np.log10(housing_data.sale_price)
From the data, we will create the following features to evaluate:
house_age
: How old the house is.renovated
: Whether the house has been renovated or not.yrs_since_reno
: If the house has been renovated, how long has it been.
import datetime
thisyear = datetime.datetime.now().year
housing_data['house_age'] = thisyear - housing_data['yr_built']
housing_data['renovated'] = np.where((housing_data['yr_renovated'] > 0), 1, 0)
housing_data['yrs_since_reno'] = np.where(housing_data['renovated'], housing_data['yr_renovated'] - housing_data['yr_built'], 0)
housing_data.loc[:, ['yr_built', 'yr_renovated', 'house_age', 'renovated', 'yrs_since_reno']]
yr_built | yr_renovated | house_age | renovated | yrs_since_reno | |
---|---|---|---|---|---|
0 | 1955 | 0 | 68 | 0 | 0 |
1 | 1951 | 1991 | 72 | 1 | 40 |
2 | 1933 | 0 | 90 | 0 | 0 |
3 | 1965 | 0 | 58 | 0 | 0 |
4 | 1987 | 0 | 36 | 0 | 0 |
... | ... | ... | ... | ... | ... |
20518 | 2009 | 0 | 14 | 0 | 0 |
20519 | 2014 | 0 | 9 | 0 | 0 |
20520 | 2009 | 0 | 14 | 0 | 0 |
20521 | 2004 | 0 | 19 | 0 | 0 |
20522 | 2008 | 0 | 15 | 0 | 0 |
20523 rows × 5 columns
Now we pick variables and split training data into training and holdout (test).
vars = ['bedrooms', 'bathrooms', 'sqft_living', 'sqft_lot', 'floors', 'waterfront', 'view',
'condition', 'grade', 'sqft_above', 'sqft_basement', 'lat', 'long', 'sqft_living15', 'sqft_lot15', 'house_age', 'renovated', 'yrs_since_reno']
outcome = 'logprice'
runif = np.random.default_rng(2206222).uniform(0, 1, housing_data.shape[0])
gp = np.where(runif < 0.2, 'test', 'training')
hd_train = housing_data.loc[gp=='training', :].reset_index(drop=True, inplace=False)
hd_test = housing_data.loc[gp=='test', :].reset_index(drop=True, inplace=False)
# split the training into training and val for xgboost
runif = np.random.default_rng(123).uniform(0, 1, hd_train.shape[0])
xgb_gp = np.where(runif < 0.2, 'val', 'train')
# for xgboost, further split into train and val
train_features = np.array(hd_train.loc[xgb_gp=='train', vars])
train_labels = np.array(hd_train.loc[xgb_gp=='train', outcome])
val_features = np.array(hd_train.loc[xgb_gp=='val', vars])
val_labels = np.array(hd_train.loc[xgb_gp=='val', outcome])
Postprocessing
Since we are fitting a model to predict log10
price, we need to convert predictions back into price units. We also want to round to the nearest dollar.
def postprocess(log10price):
return np.rint(np.power(10, log10price))
Model testing
For the purposes of this demo, let’s say that we require a mean absolute percent error (MAPE) of 15% or less, and the we want to try a few models to decide which model we want to use.
One could also hyperparameter tune at this stage; for brevity, we’ll omit that in this demo.
XGBoost
First we will test out using a XGBoost model.
xgb_model = xgb.XGBRegressor(
objective = 'reg:squarederror',
max_depth=5,
base_score = np.mean(hd_train[outcome])
)
xgb_model.fit(
train_features,
train_labels,
eval_set=[(train_features, train_labels), (val_features, val_labels)],
verbose=False,
early_stopping_rounds=35
)
XGBRegressor(base_score=5.666446833601829, booster='gbtree', callbacks=None, colsample_bylevel=1, colsample_bynode=1, colsample_bytree=1, early_stopping_rounds=None, enable_categorical=False, eval_metric=None, gamma=0, gpu_id=-1, grow_policy='depthwise', importance_type=None, interaction_constraints='', learning_rate=0.300000012, max_bin=256, max_cat_to_onehot=4, max_delta_step=0, max_depth=5, max_leaves=0, min_child_weight=1, missing=nan, monotone_constraints='()', n_estimators=100, n_jobs=0, num_parallel_tree=1, predictor='auto', random_state=0, reg_alpha=0, reg_lambda=1, ...)In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
XGBRegressor(base_score=5.666446833601829, booster='gbtree', callbacks=None,colsample_bylevel=1, colsample_bynode=1, colsample_bytree=1, early_stopping_rounds=None, enable_categorical=False, eval_metric=None, gamma=0, gpu_id=-1, grow_policy='depthwise', importance_type=None, interaction_constraints='', learning_rate=0.300000012, max_bin=256, max_cat_to_onehot=4, max_delta_step=0, max_depth=5, max_leaves=0, min_child_weight=1, missing=nan, monotone_constraints='()', n_estimators=100, n_jobs=0, num_parallel_tree=1, predictor='auto', random_state=0, reg_alpha=0, reg_lambda=1, ...)</pre>
print(xgb_model.best_score) print(xgb_model.best_iteration) print(xgb_model.best_ntree_limit)
0.07793614689092423 99 100
XGBoost Evaluate on holdout
With the sample model created, we will test it against the holdout data. Note that we are calling the
postprocess
function on the data.test_features = np.array(hd_test.loc[:, vars]) test_labels = np.array(hd_test.loc[:, outcome]) pframe = pd.DataFrame({ 'pred' : postprocess(xgb_model.predict(test_features)), 'actual' : postprocess(test_labels) }) ax = seaborn.scatterplot( data=pframe, x='pred', y='actual', alpha=0.2 ) matplotlib.pyplot.plot(pframe.pred, pframe.pred, color='DarkGreen') matplotlib.pyplot.title("test") plt.show()
pframe['se'] = (pframe.pred - pframe.actual)**2 pframe['pct_err'] = 100*np.abs(pframe.pred - pframe.actual)/pframe.actual pframe.describe()
pred | actual | se | pct_err | |
---|---|---|---|---|
count | 4.094000e+03 | 4.094000e+03 | 4.094000e+03 | 4094.000000 |
mean | 5.340824e+05 | 5.396937e+05 | 1.657722e+10 | 12.857674 |
std | 3.413714e+05 | 3.761666e+05 | 1.276017e+11 | 13.512028 |
min | 1.216140e+05 | 8.200000e+04 | 1.000000e+00 | 0.000500 |
25% | 3.167628e+05 | 3.200000e+05 | 3.245312e+08 | 4.252492 |
50% | 4.568700e+05 | 4.500000e+05 | 1.602001e+09 | 9.101485 |
75% | 6.310372e+05 | 6.355250e+05 | 6.575385e+09 | 17.041227 |
max | 5.126706e+06 | 7.700000e+06 | 6.637466e+12 | 252.097895 |
rmse = np.sqrt(np.mean(pframe.se))
mape = np.mean(pframe.pct_err)
print(f'rmse = {rmse}, mape = {mape}')
rmse = 128752.54982046234, mape = 12.857674005250548
Random Forest
The next model to test is Random Forest.
model_rf = sklearn.ensemble.RandomForestRegressor(n_estimators=100, max_depth=5, n_jobs=2, max_samples=0.8)
train_features = np.array(hd_train.loc[:, vars])
train_labels = np.array(hd_train.loc[:, outcome])
model_rf.fit(train_features, train_labels)
RandomForestRegressor(max_depth=5, max_samples=0.8, n_jobs=2)In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
RandomForestRegressor(max_depth=5, max_samples=0.8, n_jobs=2)
Random Forest Evaluate on holdout
With the Random Forest sample model created, now we can test it against the holdout data.
pframe = pd.DataFrame({
'pred' : postprocess(model_rf.predict(test_features)),
'actual' : postprocess(test_labels)
})
ax = seaborn.scatterplot(
data=pframe,
x='pred',
y='actual',
alpha=0.2
)
matplotlib.pyplot.plot(pframe.pred, pframe.pred, color='DarkGreen')
matplotlib.pyplot.title("random forest")
plt.show()
pframe['se'] = (pframe.pred - pframe.actual)**2
pframe['pct_err'] = 100*np.abs(pframe.pred - pframe.actual)/pframe.actual
pframe.describe()
pred | actual | se | pct_err | |
---|---|---|---|---|
count | 4.094000e+03 | 4.094000e+03 | 4.094000e+03 | 4094.000000 |
mean | 5.194535e+05 | 5.396937e+05 | 3.875433e+10 | 18.188652 |
std | 2.797001e+05 | 3.761666e+05 | 4.054895e+11 | 17.634478 |
min | 2.039200e+05 | 8.200000e+04 | 1.444000e+03 | 0.014729 |
25% | 3.291252e+05 | 3.200000e+05 | 6.686879e+08 | 6.156760 |
50% | 4.621880e+05 | 4.500000e+05 | 3.321332e+09 | 13.148593 |
75% | 5.851052e+05 | 6.355250e+05 | 1.367023e+10 | 24.630187 |
max | 2.888692e+06 | 7.700000e+06 | 2.314868e+13 | 175.444819 |
rmse = np.sqrt(np.mean(pframe.se))
mape = np.mean(pframe.pct_err)
print(f'rmse = {rmse}, mape = {mape}')
rmse = 196861.19318381665, mape = 18.188652142429135
Final Decision
At this stage, we decide to go with the xgboost model, with the variables/settings above.
With this stage complete, we can move on to Stage 2: Training Process Automation Setup.
6.2 - From Jupyter to Production
The following tutorials are available from the Wallaroo Tutorials Repository.
Stage 2: Training Process Automation Setup
Now that we have decided on the type and structure of the model from Stage 1: Data Exploration And Model Selection, this notebook modularizes the various steps of the process in a structure that is compatible with production and with Wallaroo.
We have pulled the preprocessing and postprocessing steps out of the training notebook into individual scripts that can also be used when the model is deployed.
Assuming no changes are made to the structure of the model, this notebook, or a script based on this notebook, can then be scheduled to run on a regular basis, to refresh the model with more recent training data. We’d expect to run this notebook in conjunction with the Stage 3 notebook, 03_deploy_model.ipynb
. For clarity in this demo, we have split the training/upload task into two notebooks, 02_automated_training_process.ipynb
and 03_deploy_model.ipynb
.
Resources
The following resources are used as part of this tutorial:
- data
data/seattle_housing_col_description.txt
: Describes the columns used as part data analysis.data/seattle_housing.csv
: Sample data of the Seattle, Washington housing market between 2014 and 2015.
- code
postprocess.py
: Formats the data after inference by the model is complete.preprocess.py
: Formats the incoming data for the model.simdb.py
: A simulated database to demonstrate sending and receiving queries.wallaroo_client.py
: Additional methods used with the Wallaroo instance to create workspaces, etc.
Steps
The following steps are part of this process:
- Retrieve Training Data: Connect to the data store and retrieve the training data.
- Data Transformations: Evaluate the data and train the model.
- Generate and Test the Model: Create the model and verify it against the sample test data.
- Pickle The Model: Prepare the model to be uploaded to Wallaroo.
Retrieve Training Data
Note that this connection is simulated to demonstrate how data would be retrieved from an existing data store. For training, we will use the data on all houses sold in this market with the last two years.
import numpy as np
import pandas as pd
import sklearn
import xgboost as xgb
import seaborn
import matplotlib
import matplotlib.pyplot as plt
import pickle
import simdb # module for the purpose of this demo to simulate pulling data from a database
from preprocess import create_features # our custom preprocessing
from postprocess import postprocess # our custom postprocessing
matplotlib.rcParams["figure.figsize"] = (12,6)
# ignoring warnings for demonstration
import warnings
warnings.filterwarnings('ignore')
conn = simdb.simulate_db_connection()
tablename = simdb.tablename
query = f"select * from {tablename} where date > DATE(DATE(), '-24 month') AND sale_price is not NULL"
print(query)
# read in the data
housing_data = pd.read_sql_query(query, conn)
conn.close()
housing_data.loc[:, ["id", "date", "list_price", "bedrooms", "bathrooms", "sqft_living", "sqft_lot"]]
select * from house_listings where date > DATE(DATE(), '-24 month') AND sale_price is not NULL
id | date | list_price | bedrooms | bathrooms | sqft_living | sqft_lot | |
---|---|---|---|---|---|---|---|
0 | 7129300520 | 2022-10-05 | 221900.0 | 3 | 1.00 | 1180 | 5650 |
1 | 6414100192 | 2022-12-01 | 538000.0 | 3 | 2.25 | 2570 | 7242 |
2 | 5631500400 | 2023-02-17 | 180000.0 | 2 | 1.00 | 770 | 10000 |
3 | 2487200875 | 2022-12-01 | 604000.0 | 4 | 3.00 | 1960 | 5000 |
4 | 1954400510 | 2023-02-10 | 510000.0 | 3 | 2.00 | 1680 | 8080 |
... | ... | ... | ... | ... | ... | ... | ... |
20518 | 263000018 | 2022-05-13 | 360000.0 | 3 | 2.50 | 1530 | 1131 |
20519 | 6600060120 | 2023-02-15 | 400000.0 | 4 | 2.50 | 2310 | 5813 |
20520 | 1523300141 | 2022-06-15 | 402101.0 | 2 | 0.75 | 1020 | 1350 |
20521 | 291310100 | 2023-01-08 | 400000.0 | 3 | 2.50 | 1600 | 2388 |
20522 | 1523300157 | 2022-10-07 | 325000.0 | 2 | 0.75 | 1020 | 1076 |
20523 rows × 7 columns
Data transformations
To improve relative error performance, we will predict on log10
of the sale price.
Predict on log10 price to try to improve relative error performance
housing_data['logprice'] = np.log10(housing_data.list_price)
# split data into training and test
outcome = 'logprice'
runif = np.random.default_rng(2206222).uniform(0, 1, housing_data.shape[0])
gp = np.where(runif < 0.2, 'test', 'training')
hd_train = housing_data.loc[gp=='training', :].reset_index(drop=True, inplace=False)
hd_test = housing_data.loc[gp=='test', :].reset_index(drop=True, inplace=False)
# split the training into training and val for xgboost
runif = np.random.default_rng(123).uniform(0, 1, hd_train.shape[0])
xgb_gp = np.where(runif < 0.2, 'val', 'train')
# for xgboost
train_features = hd_train.loc[xgb_gp=='train', :].reset_index(drop=True, inplace=False)
train_features = np.array(create_features(train_features))
train_labels = np.array(hd_train.loc[xgb_gp=='train', outcome])
val_features = hd_train.loc[xgb_gp=='val', :].reset_index(drop=True, inplace=False)
val_features = np.array(create_features(val_features))
val_labels = np.array(hd_train.loc[xgb_gp=='val', outcome])
print(f'train_features: {train_features.shape}, train_labels: {len(train_labels)}')
print(f'val_features: {val_features.shape}, val_labels: {len(val_labels)}')
train_features: (13129, 18), train_labels: 13129
val_features: (3300, 18), val_labels: 3300
Generate and Test the Model
Based on the experimentation and testing performed in Stage 1: Data Exploration And Model Selection, XGBoost was selected as the ML model and the variables for training were selected. The model will be generated and tested against sample data.
xgb_model = xgb.XGBRegressor(
objective = 'reg:squarederror',
max_depth=5,
base_score = np.mean(hd_train[outcome])
)
xgb_model.fit(
train_features,
train_labels,
eval_set=[(train_features, train_labels), (val_features, val_labels)],
verbose=False,
early_stopping_rounds=35
)
XGBRegressor(base_score=5.666446833601829, booster='gbtree', callbacks=None, colsample_bylevel=1, colsample_bynode=1, colsample_bytree=1, early_stopping_rounds=None, enable_categorical=False, eval_metric=None, gamma=0, gpu_id=-1, grow_policy='depthwise', importance_type=None, interaction_constraints='', learning_rate=0.300000012, max_bin=256, max_cat_to_onehot=4, max_delta_step=0, max_depth=5, max_leaves=0, min_child_weight=1, missing=nan, monotone_constraints='()', n_estimators=100, n_jobs=0, num_parallel_tree=1, predictor='auto', random_state=0, reg_alpha=0, reg_lambda=1, ...)In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
XGBRegressor(base_score=5.666446833601829, booster='gbtree', callbacks=None,colsample_bylevel=1, colsample_bynode=1, colsample_bytree=1, early_stopping_rounds=None, enable_categorical=False, eval_metric=None, gamma=0, gpu_id=-1, grow_policy='depthwise', importance_type=None, interaction_constraints='', learning_rate=0.300000012, max_bin=256, max_cat_to_onehot=4, max_delta_step=0, max_depth=5, max_leaves=0, min_child_weight=1, missing=nan, monotone_constraints='()', n_estimators=100, n_jobs=0, num_parallel_tree=1, predictor='auto', random_state=0, reg_alpha=0, reg_lambda=1, ...)</pre>
print(xgb_model.best_score) print(xgb_model.best_iteration) print(xgb_model.best_ntree_limit)
0.07793614689092423 99 100
test_features = np.array(create_features(hd_test.copy())) test_labels = np.array(hd_test.loc[:, outcome]) pframe = pd.DataFrame({ 'pred' : postprocess(xgb_model.predict(test_features)), 'actual' : postprocess(test_labels) }) ax = seaborn.scatterplot( data=pframe, x='pred', y='actual', alpha=0.2 ) matplotlib.pyplot.plot(pframe.pred, pframe.pred, color='DarkGreen') matplotlib.pyplot.title("test") plt.show()
pframe['se'] = (pframe.pred - pframe.actual)**2 pframe['pct_err'] = 100*np.abs(pframe.pred - pframe.actual)/pframe.actual pframe.describe()
pred | actual | se | pct_err | |
---|---|---|---|---|
count | 4.094000e+03 | 4.094000e+03 | 4.094000e+03 | 4094.000000 |
mean | 5.340824e+05 | 5.396937e+05 | 1.657722e+10 | 12.857674 |
std | 3.413714e+05 | 3.761666e+05 | 1.276017e+11 | 13.512028 |
min | 1.216140e+05 | 8.200000e+04 | 1.000000e+00 | 0.000500 |
25% | 3.167628e+05 | 3.200000e+05 | 3.245312e+08 | 4.252492 |
50% | 4.568700e+05 | 4.500000e+05 | 1.602001e+09 | 9.101485 |
75% | 6.310372e+05 | 6.355250e+05 | 6.575385e+09 | 17.041227 |
max | 5.126706e+06 | 7.700000e+06 | 6.637466e+12 | 252.097895 |
rmse = np.sqrt(np.mean(pframe.se))
mape = np.mean(pframe.pct_err)
print(f'rmse = {rmse}, mape = {mape}')
rmse = 128752.54982046234, mape = 12.857674005250548
Convert the Model to Onnx
This step converts the model to onnx for easy import into Wallaroo.
import onnx
from onnxmltools.convert import convert_xgboost
from skl2onnx.common.data_types import FloatTensorType, DoubleTensorType
import preprocess
# set the number of columns
ncols = len(preprocess._vars)
# derive the opset value
from onnx.defs import onnx_opset_version
from onnxconverter_common.onnx_ex import DEFAULT_OPSET_NUMBER
TARGET_OPSET = min(DEFAULT_OPSET_NUMBER, onnx_opset_version())
# Convert the model to onnx
onnx_model_converted = convert_xgboost(xgb_model, 'tree-based classifier',
[('input', FloatTensorType([None, ncols]))],
target_opset=TARGET_OPSET)
# Save the model
onnx.save_model(onnx_model_converted, "housing_model_xgb.onnx")
With the model trained and ready, we can now go to Stage 3: Deploy the Model in Wallaroo.
6.3 - Deploy the Model in Wallaroo
The following tutorials are available from the Wallaroo Tutorials Repository.
Stage 3: Deploy the Model in Wallaroo
In this stage, we upload the trained model and the processing steps to Wallaroo, then set up and deploy the inference pipeline.
Once deployed we can feed the newest batch of data to the pipeline, do the inferences and write the results to a results table.
For clarity in this demo, we have split the training/upload task into two notebooks:
02_automated_training_process.ipynb
: Train and pickle ML model.03_deploy_model.ipynb
: Upload the model to Wallaroo and deploy into a pipeline.
Assuming no changes are made to the structure of the model, these two notebooks, or a script based on them, can then be scheduled to run on a regular basis, to refresh the model with more recent training data and update the inference pipeline.
This notebook is expected to run within the Wallaroo instance’s Jupyter Hub service to provide access to all required Wallaroo libraries and functionality.
Resources
The following resources are used as part of this tutorial:
- data
data/seattle_housing_col_description.txt
: Describes the columns used as part data analysis.data/seattle_housing.csv
: Sample data of the Seattle, Washington housing market between 2014 and 2015.
- code
postprocess.py
: Formats the data after inference by the model is complete.simdb.py
: A simulated database to demonstrate sending and receiving queries.wallaroo_client.py
: Additional methods used with the Wallaroo instance to create workspaces, etc.
- models
housing_model_xgb.onnx
: Model created in Stage 2: Training Process Automation Setup.
Steps
The process of uploading the model to Wallaroo follows these steps:
- Connect to Wallaroo: Connect to the Wallaroo instance and set up the workspace.
- Upload The Model: Upload the model and autoconvert for use in the Wallaroo engine.
- Upload the Processing Modules: Upload the processing modules.
- Create and Deploy the Pipeline: Create the pipeline with the model and processing modules as steps, then deploy it.
- Test the Pipeline: Verify that the pipeline works with the sample data.
Connect to Wallaroo
First we import the required libraries to connect to the Wallaroo instance, then connect to the Wallaroo instance.
import json
import pickle
import pandas as pd
import numpy as np
import pyarrow as pa
import simdb # module for the purpose of this demo to simulate pulling data from a database
# from wallaroo.ModelConversion import ConvertXGBoostArgs, ModelConversionSource, ModelConversionInputType
import wallaroo
from wallaroo.object import EntityNotFoundError
# used to display dataframe information without truncating
from IPython.display import display
import pandas as pd
pd.set_option('display.max_colwidth', None)
import datetime
Connect to the Wallaroo Instance
The first step is to connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. For more information on Wallaroo Client settings, see the Client Connection guide.
# Login through local Wallaroo instance
wl = wallaroo.Client()
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
pipeline = wl.build_pipeline(name)
return pipeline
workspace_name = 'housepricing'
model_name = "housepricemodel"
model_file = "./housing_model_xgb.onnx"
pipeline_name = "housing-pipe"
The workspace housepricing
will either be created or, if already existing, used and set to the current workspace.
new_workspace = get_workspace(workspace_name)
new_workspace
{'name': 'housepricing', 'id': 16, 'archived': False, 'created_by': 'aa707604-ec80-495a-a9a1-87774c8086d5', 'created_at': '2023-09-12T17:35:46.994384+00:00', 'models': [{'name': 'housepricemodel', 'versions': 1, 'owner_id': '""', 'last_update_time': datetime.datetime(2023, 9, 12, 17, 35, 49, 181499, tzinfo=tzutc()), 'created_at': datetime.datetime(2023, 9, 12, 17, 35, 49, 181499, tzinfo=tzutc())}, {'name': 'preprocess', 'versions': 1, 'owner_id': '""', 'last_update_time': datetime.datetime(2023, 9, 12, 17, 35, 50, 150472, tzinfo=tzutc()), 'created_at': datetime.datetime(2023, 9, 12, 17, 35, 50, 150472, tzinfo=tzutc())}, {'name': 'postprocess', 'versions': 1, 'owner_id': '""', 'last_update_time': datetime.datetime(2023, 9, 12, 17, 35, 51, 80789, tzinfo=tzutc()), 'created_at': datetime.datetime(2023, 9, 12, 17, 35, 51, 80789, tzinfo=tzutc())}], 'pipelines': [{'name': 'housing-pipe', 'create_time': datetime.datetime(2023, 9, 12, 17, 35, 52, 273091, tzinfo=tzutc()), 'definition': '[]'}]}
_ = wl.set_current_workspace(new_workspace)
Upload The Model
With the connection set and workspace prepared, upload the model created in 02_automated_training_process.ipynb
into the current workspace.
hpmodel = wl.upload_model(model_name, model_file, framework=wallaroo.framework.Framework.ONNX).configure()
Upload the Processing Modules
Upload the postprocess.py
modules as models to be added to the pipeline.
# load the postprocess module
preprocess_input_schema = pa.schema([
pa.field('id', pa.int64()),
pa.field('date', pa.string()),
pa.field('list_price', pa.float64()),
pa.field('bedrooms', pa.int64()),
pa.field('bathrooms', pa.float64()),
pa.field('sqft_living', pa.int64()),
pa.field('sqft_lot', pa.int64()),
pa.field('floors', pa.float64()),
pa.field('waterfront', pa.int64()),
pa.field('view', pa.int64()),
pa.field('condition', pa.int64()),
pa.field('grade', pa.int64()),
pa.field('sqft_above', pa.int64()),
pa.field('sqft_basement', pa.int64()),
pa.field('yr_built', pa.int64()),
pa.field('yr_renovated', pa.int64()),
pa.field('zipcode', pa.int64()),
pa.field('lat', pa.float64()),
pa.field('long', pa.float64()),
pa.field('sqft_living15', pa.int64()),
pa.field('sqft_lot15', pa.int64()),
pa.field('sale_price', pa.float64())
])
preprocess_output_schema = pa.schema([
pa.field('tensor', pa.list_(pa.float32()))
])
module_pre = (wl.upload_model("preprocess",
"./preprocess.py",
framework=wallaroo.framework.Framework.PYTHON)
.configure('python',
input_schema=preprocess_input_schema,
output_schema=preprocess_output_schema)
)
# load the postprocess module
input_schema = pa.schema([
pa.field('variable', pa.list_(pa.float64()))
])
output_schema = pa.schema([
pa.field('variable', pa.list_(pa.float64()))
])
module_post = (wl.upload_model("postprocess",
"./postprocess.py",
framework=wallaroo.framework.Framework.PYTHON)
.configure('python',
input_schema=input_schema,
output_schema=output_schema)
)
Create and Deploy the Pipeline
Create the pipeline with the preprocess module, housing model, and postprocess module as pipeline steps, then deploy the newpipeline.
pipeline = get_pipeline(pipeline_name)
# clear if the tutorial was run before
pipeline.clear()
pipeline.add_model_step(module_pre)
pipeline.add_model_step(hpmodel)
pipeline.add_model_step(module_post)
pipeline.deploy()
name | housing-pipe |
---|---|
created | 2023-09-12 17:35:52.273091+00:00 |
last_updated | 2023-09-12 17:40:44.630596+00:00 |
deployed | True |
tags | |
versions | 05d941bb-6547-4608-be5d-4515388d205c, d957ce8d-9d70-477e-bc03-d58b70cd047a, ba8a411e-9318-4ba5-95f5-22c22be8c064, ab42a8de-3551-4551-bc36-9a71d323f81c |
steps | preprocess |
published | False |
Test the Pipeline
We will use a single query from the simulated housing_price
table and infer. When successful, we will undeploy the pipeline to restore the resources back to the Kubernetes environment.
conn = simdb.simulate_db_connection()
# create the query
query = f"select * from {simdb.tablename} limit 1"
print(query)
# read in the data
singleton = pd.read_sql_query(query, conn)
conn.close()
display(singleton.loc[:, ["id", "date", "list_price", "bedrooms", "bathrooms", "sqft_living", "sqft_lot"]])
select * from house_listings limit 1
id | date | list_price | bedrooms | bathrooms | sqft_living | sqft_lot | |
---|---|---|---|---|---|---|---|
0 | 7129300520 | 2023-01-29 | 221900.0 | 3 | 1.0 | 1180 | 5650 |
result = pipeline.infer(singleton)
display(result.loc[:, ['time', 'out.variable']])
time | out.variable | |
---|---|---|
0 | 2023-09-12 17:41:00.319 | [224852.0] |
When finished, we undeploy the pipeline to return the resources back to the environment.
pipeline.undeploy()
name | housing-pipe |
---|---|
created | 2023-09-12 17:35:52.273091+00:00 |
last_updated | 2023-09-12 17:40:44.630596+00:00 |
deployed | False |
tags | |
versions | 05d941bb-6547-4608-be5d-4515388d205c, d957ce8d-9d70-477e-bc03-d58b70cd047a, ba8a411e-9318-4ba5-95f5-22c22be8c064, ab42a8de-3551-4551-bc36-9a71d323f81c |
steps | preprocess |
published | False |
With this stage complete, we can proceed to Stage 4: Regular Batch Inference.
6.4 - Regular Batch Inference
The following tutorials are available from the Wallaroo Tutorials Repository.
Stage 4: Regular Batch Inference
In Stage 3: Deploy the Model in Wallaroo, the housing model created and tested in Stage 2: Training Process Automation Setup was uploaded to a Wallaroo instance and added to the pipeline housing-pipe
in the workspace housepricing
. This pipeline can be deployed at any point and time and used with new inferences.
For the purposes of this demo, let’s say that every month we find the newly entered and still-unsold houses and predict their sale price.
The predictions are entered into a staging table, for further inspection before being joined to the primary housing data table.
We show this as a notebook, but this can also be scripted and scheduled, using CRON or some other process.
Resources
The following resources are used as part of this tutorial:
- data
data/seattle_housing_col_description.txt
: Describes the columns used as part data analysis.data/seattle_housing.csv
: Sample data of the Seattle, Washington housing market between 2014 and 2015.
- code
postprocess.py
: Formats the data after inference by the model is complete.simdb.py
: A simulated database to demonstrate sending and receiving queries.wallaroo_client.py
: Additional methods used with the Wallaroo instance to create workspaces, etc.
- models
housing_model_xgb.onnx
: Model created in Stage 2: Training Process Automation Setup.
Steps
This process will use the following steps:
- Connect to Wallaroo: Connect to the Wallaroo instance and the
housepricing
workspace. - Deploy the Pipeline: Deploy the pipeline to prepare it to run inferences.
- Read In New House Listings: Read in the previous month’s house listings and submit them to the pipeline for inference.
- Send Predictions to Results Staging Table: Add the inference results to the results staging table.
Connect to Wallaroo
Connect to the Wallaroo instance and set the housepricing
workspace as the current workspace.
import json
import pickle
import wallaroo
import pandas as pd
import numpy as np
import pyarrow as pa
import datetime
import simdb # module for the purpose of this demo to simulate pulling data from a database
from wallaroo_client import get_workspace
# used to display dataframe information without truncating
from IPython.display import display
pd.set_option('display.max_colwidth', None)
Connect to the Wallaroo Instance
The first step is to connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. For more information on Wallaroo Client settings, see the Client Connection guide.
# Login through local Wallaroo instance
wl = wallaroo.Client()
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
pipeline = wl.build_pipeline(name)
return pipeline
workspace_name = 'housepricing'
model_name = "housepricemodel"
model_file = "./housing_model_xgb.onnx"
pipeline_name = "housing-pipe"
new_workspace = get_workspace(workspace_name)
_ = wl.set_current_workspace(new_workspace)
Deploy the Pipeline
Deploy the housing-pipe
workspace established in Stage 3: Deploy the Model in Wallaroo (03_deploy_model.ipynb
).
pipeline = get_pipeline(pipeline_name)
pipeline.deploy()
name | housing-pipe |
---|---|
created | 2023-09-12 17:35:52.273091+00:00 |
last_updated | 2023-09-12 17:37:27.074611+00:00 |
deployed | True |
tags | |
versions | d957ce8d-9d70-477e-bc03-d58b70cd047a, ba8a411e-9318-4ba5-95f5-22c22be8c064, ab42a8de-3551-4551-bc36-9a71d323f81c |
steps | preprocess |
published | False |
Read In New House Listings
From the data store, load the previous month’s house listing, prepare it as a DataFrame, then submit it for inferencing.
conn = simdb.simulate_db_connection()
# create the query
query = f"select * from {simdb.tablename} where date > DATE(DATE(), '-1 month') AND sale_price is NULL"
print(query)
# read in the data
# can't have null values - turn them into 0
newbatch = pd.read_sql_query(query, conn)
newbatch['sale_price'] = newbatch.sale_price.apply(lambda x: 0)
display(newbatch.shape)
display(newbatch.head(10).loc[:, ["id", "date", "list_price", "bedrooms", "bathrooms", "sqft_living", "sqft_lot"]])
select * from house_listings where date > DATE(DATE(), '-1 month') AND sale_price is NULL
(1090, 22)
id | date | list_price | bedrooms | bathrooms | sqft_living | sqft_lot | |
---|---|---|---|---|---|---|---|
0 | 9215400105 | 2023-08-14 | 450000.0 | 3 | 1.75 | 1250 | 5963 |
1 | 1695900060 | 2023-08-27 | 535000.0 | 4 | 1.00 | 1610 | 2982 |
2 | 9545240070 | 2023-08-14 | 660500.0 | 4 | 2.25 | 2010 | 9603 |
3 | 1432900240 | 2023-08-24 | 205000.0 | 3 | 1.00 | 1610 | 8579 |
4 | 6131600075 | 2023-08-13 | 225000.0 | 3 | 1.00 | 1300 | 8316 |
5 | 1400300055 | 2023-08-14 | 425000.0 | 2 | 1.00 | 770 | 5040 |
6 | 7960900060 | 2023-08-20 | 2900000.0 | 4 | 3.25 | 5050 | 20100 |
7 | 6378500125 | 2023-08-17 | 436000.0 | 2 | 1.00 | 1040 | 7538 |
8 | 2022069200 | 2023-08-21 | 455000.0 | 4 | 2.50 | 2210 | 49375 |
9 | 9412900055 | 2023-08-21 | 405000.0 | 3 | 1.75 | 2390 | 6000 |
# query = {'query': newbatch.to_json()}
result = pipeline.infer(newbatch)
# display(result)
predicted_prices = pd.DataFrame(result['out.variable'].apply(lambda x: x[0])).rename(columns={'out.variable':'prediction'})
display(predicted_prices[0:5])
prediction | |
---|---|
0 | 508255.0 |
1 | 500198.0 |
2 | 539598.0 |
3 | 270739.0 |
4 | 191304.0 |
Send Predictions to Results Staging Table
Take the predicted prices based on the inference results so they can be joined into the house_listings
table.
Once complete, undeploy the pipeline to return the resources back to the Kubernetes environment.
result_table = pd.DataFrame({
'id': newbatch['id'],
'saleprice_estimate': predicted_prices['prediction']
})
display(result_table)
result_table.to_sql('results_table', conn, index=False, if_exists='append')
id | saleprice_estimate | |
---|---|---|
0 | 9215400105 | 508255.0 |
1 | 1695900060 | 500198.0 |
2 | 9545240070 | 539598.0 |
3 | 1432900240 | 270739.0 |
4 | 6131600075 | 191304.0 |
... | ... | ... |
1085 | 3304300300 | 577492.0 |
1086 | 6453550090 | 882930.0 |
1087 | 1760650820 | 271484.0 |
1088 | 3345700207 | 537434.0 |
1089 | 7853420110 | 634226.0 |
1090 rows × 2 columns
# Display the top of the table for confirmation
pd.read_sql_query("select * from results_table limit 5", conn)
id | saleprice_estimate | |
---|---|---|
0 | 9215400105 | 508255.0 |
1 | 1695900060 | 500198.0 |
2 | 9545240070 | 539598.0 |
3 | 1432900240 | 270739.0 |
4 | 6131600075 | 191304.0 |
conn.close()
pipeline.undeploy()
name | housing-pipe |
---|---|
created | 2023-09-12 17:35:52.273091+00:00 |
last_updated | 2023-09-12 17:37:27.074611+00:00 |
deployed | False |
tags | |
versions | d957ce8d-9d70-477e-bc03-d58b70cd047a, ba8a411e-9318-4ba5-95f5-22c22be8c064, ab42a8de-3551-4551-bc36-9a71d323f81c |
steps | preprocess |
published | False |
From here, organizations can automate this process. Other features could be used such as data analysis using Wallaroo assays, or other features such as shadow deployments to test champion and challenger models to find which models provide the best results.
7 - ML Workload Orchestration Tutorials
The following tutorials show how to use the Wallaroo ML Workload Orchestration feature with different data connections and situations.
7.1 - Wallaroo ML Workload Orchestration API Tutorial
This can be downloaded as part of the Wallaroo Tutorials repository.
Wallaroo Connection and ML Workload Orchestration API Tutorial
This tutorial provides a quick set of methods and examples regarding Wallaroo Connections and Wallaroo ML Workload Orchestration. For full details, see the Wallaroo Documentation site.
Wallaroo provides Data Connections and ML Workload Orchestrations to provide organizations with a method of creating and managing automated tasks that can either be run on demand or a regular schedule.
Definitions
- Orchestration: A set of instructions written as a python script with a requirements library. Orchestrations are uploaded to the Wallaroo instance as a .zip file.
- Task: An implementation of an orchestration. Tasks are run either once when requested, on a repeating schedule, or as a service.
- Connection: Definitions set by MLOps engineers that are used by other Wallaroo users for connection information to a data source. Usually paired with orchestrations.
Tutorial Goals
The tutorial will demonstrate the following:
- Create a workspace and pipeline with a sample model.
- Upload Wallaroo ML Workload Orchestration through the Wallaroo MLOps API.
- List available orchestrations through the Wallaroo MLOps API.
- Run the orchestration once as a Run Once Task through the Wallaroo MLOps API and verify that the information was saved the pipeline logs.
Prerequisites
- An installed Wallaroo instance.
- The following Python libraries installed. These are included by default in a Wallaroo instance’s JupyterHub service.
Initial Steps
For this tutorial, we’ll create a workspace, upload our sample model and deploy a pipeline. We’ll perform some quick sample inferences to verify that everything it working.
Load Libraries
Here we’ll import the various libraries we’ll use for the tutorial.
import wallaroo
from wallaroo.object import EntityNotFoundError, RequiredAttributeMissing
# to display dataframe tables
from IPython.display import display
# used to display dataframe information without truncating
import pandas as pd
pd.set_option('display.max_colwidth', None)
import pyarrow as pa
import time
import requests
# Used to create unique workspace and pipeline names
import string
import random
# make a random 4 character suffix
suffix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
display(suffix)
'gsze'
Connect to the Wallaroo Instance
The first step is to connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. If logging in externally, update the wallarooPrefix
and wallarooSuffix
variables with the proper DNS information. For more information on Wallaroo Client settings, see the Client Connection guide.
# Login through local Wallaroo instance
wl = wallaroo.Client()
API URL
The variable APIURL
is used to specify the connection to the Wallaroo instance’s MLOps API URL, and is composed of the Wallaroo DNS prefix and suffix. For full details, see the Wallaroo API Connection Guide
.
The variables wallarooPrefix
and wallarooSuffix
variables will be used to derive the API url. For example, if the Wallaroo Prefix is doc-test
and the url is example.com
, then the MLOps API URL would be doc-test.api.example.com/v1/api/{request}
.
Note the .
is part of the prefix. If there is no prefix, then wallarooPrefix = ""
Set the Wallaroo Prefix and Suffix in the code segment below based on your Wallaroo instance.
# Setting variables for later steps
wallarooPrefix = "YOUR PREFIX."
wallarooSuffix = "YOUR SUFFIX"
APIURL = f"https://{wallarooPrefix}api.{wallarooSuffix}"
workspace_name = f'apiorchestrationworkspace{suffix}'
pipeline_name = f'apipipeline{suffix}'
model_name = f'apiorchestrationmodel{suffix}'
model_file_name = './models/rf_model.onnx'
Helper Methods
The following helper methods are used to either create or get workspaces, pipelines, and connections.
# helper methods to retrieve workspaces and pipelines
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
pipeline = wl.build_pipeline(name)
return pipeline
def get_connection(name, connection_type, connection_arguments):
try:
connection = wl.get_connection(name)
except RequiredAttributeMissing:
connection =wl.create_connection(name,
connection_type,
connection_arguments)
return connection
Create the Workspace and Pipeline
We’ll now create our workspace and pipeline for the tutorial. If this tutorial has been run previously, then this will retrieve the existing ones with the assumption they’re for us with this tutorial.
We’ll set the retrieved workspace as the current workspace in the SDK, so all commands will default to that workspace.
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
workspace_id = workspace.id()
pipeline = get_pipeline(pipeline_name)
Upload the Model and Deploy Pipeline
We’ll upload our model into our sample workspace, then add it as a pipeline step before deploying the pipeline to it’s ready to accept inference requests.
# Upload the model
housing_model_control = wl.upload_model(model_name, model_file_name, framework=wallaroo.framework.Framework.ONNX).configure()
# Add the model as a pipeline step
pipeline.add_model_step(housing_model_control)
name | apipipelinegsze |
---|---|
created | 2023-05-22 20:48:30.700499+00:00 |
last_updated | 2023-05-22 20:48:30.700499+00:00 |
deployed | (none) |
tags | |
versions | 101b252a-623c-4185-a24d-ec00593dda79 |
steps |
#deploy the pipeline
pipeline.deploy()
Waiting for deployment - this will take up to 45s ......... ok
name | apipipelinegsze |
---|---|
created | 2023-05-22 20:48:30.700499+00:00 |
last_updated | 2023-05-22 20:48:31.357336+00:00 |
deployed | True |
tags | |
versions | aac61b5a-e4f4-4ea3-9347-6482c330b5f5, 101b252a-623c-4185-a24d-ec00593dda79 |
steps | apiorchestrationmodelgsze |
Wallaroo ML Workload Orchestration Example
With the pipeline deployed and our connections set, we will now generate our ML Workload Orchestration. See the Wallaroo ML Workload Orchestrations guide for full details.
Orchestrations are uploaded to the Wallaroo instance as a ZIP file with the following requirements:
Parameter | Type | Description |
---|---|---|
User Code | (Required) Python script as .py files | If main.py exists, then that will be used as the task entrypoint. Otherwise, the first main.py found in any subdirectory will be used as the entrypoint. |
Python Library Requirements | (Optional) requirements.txt file in the requirements file format. A standard Python requirements.txt for any dependencies to be provided in the task environment. The Wallaroo SDK will already be present and should not be included in the requirements.txt. Multiple requirements.txt files are not allowed. | |
Other artifacts | Â | Other artifacts such as files, data, or code to support the orchestration. |
For our example, our orchestration will:
- Use the
inference_results_connection
to open a HTTP Get connection to the inference data file and use it in an inference request in the deployed pipeline. - Submit the inference results to the location specified in the
external_inference_connection
.
This sample script is stored in remote_inference/main.py
with an empty requirements.txt
file, and packaged into the orchestration as ./remote_inference/remote_inference.zip
. We’ll display the steps in uploading the orchestration to the Wallaroo instance.
Note that the orchestration assumes the pipeline is already deployed.
API Upload the Orchestration
Orchestrations are uploaded via POST as a application/octet-stream
with MLOps API route:
- REQUEST
- POST
/v1/api/orchestration/upload
- Content-Type
multipart/form-data
- POST
- PARAMETERS
file
: The file uploaded as Content-Type asapplication/octet-stream
.metadata
: Included as Content-Type asapplication/json
with:- workspace_id: The numerical id of the workspace to upload the orchestration to.
- name: The name of the orchestration.
- The metadata specifying the workspace id and Content-Type as
application/json
.
Once uploaded, the deployment will be prepared and any requirements will be downloaded and installed.
# retrieve the authorization token
headers = wl.auth.auth_header()
url = f"{APIURL}/v1/api/orchestration/upload"
fp = open("./api_inference_orchestration.zip", "rb")
metadata = f'{{"workspace_id": {workspace_id},"name": "apiorchestrationsample"}}'
response = requests.post(
url,
headers=headers,
files=[
("file",
("api_inference_orchestration.zip", fp, "application/octet-stream")
),
("metadata",
("metadata", metadata, "application/json")
)
],
).json()
display(response)
orchestration_id = response['id']
{'id': 'b951f7b8-0690-4004-86bf-cc9802359313'}
API List Orchestrations
A list of orchestrations retrieved via POST MLOps API route:
- REQUEST
- POST
/v1/api/orchestration/list
- POST
- PARAMETERS
- workspace_id: The numerical identifier of the workspace associated with the orchestration.
# retrieve the authorization token
headers = wl.auth.auth_header()
url = f"{APIURL}/v1/api/orchestration/list"
data = {
'workspace_id': workspace_id
}
response=requests.post(url, headers=headers, json=data)
display(response.json())
[{'id': 'b951f7b8-0690-4004-86bf-cc9802359313',
'workspace_id': 8,
'sha': 'd3b93c9f280734106376e684792aa8b4285d527092fe87d89c74ec804f8e169e',
'name': 'apiorchestrationsample',
'file_name': 'api_inference_orchestration.zip',
'task_id': 'c9622cf8-cbe5-4e3c-b64c-dabd6c5b7fef',
'owner_id': 'a6857628-b0aa-451e-a0a0-bbc1d6eea6e0',
'status': 'pending_packaging',
'created_at': '2023-05-22T20:48:41.233482+00:00',
'updated_at': '2023-05-22T20:48:41.233482+00:00'}]
API Get Orchestration
A list of orchestrations retrieved via POST MLOps API route:
- REQUEST
- POST
/v1/api/orchestration/get_by_id
- POST
- PARAMETERS
- id: The UUID of the orchestration being retrieved.
- RETURNS
- id: The ID of the orchestration in UUID format.
- workspace_id: Numerical value of the workspace the orchestration was uploaded to.
- sha: The SHA hash value of the orchestration.
- file_name: The file name the orchestration was uploaded as.
- task_id: The task id in UUID format for unpacking and preparing the orchestration.
- owner_id: The Keycloak ID of the user that uploaded the orchestration.
- status: The status of the orchestration. Status values are:
packing
: Preparing the orchestration to be used as a task.ready
: The orchestration is ready to be deployed as a task.
# retrieve the authorization token
headers = wl.auth.auth_header()
url = f"{APIURL}/v1/api/orchestration/get_by_id"
data = {
'id': orchestration_id
}
# loop until status is ready
status = None
while status != 'ready':
response=requests.post(url, headers=headers, json=data).json()
display(response)
status = response['status']
time.sleep(10)
orchestration_sha = response['sha']
{'id': 'b951f7b8-0690-4004-86bf-cc9802359313',
'workspace_id': 8,
'sha': 'd3b93c9f280734106376e684792aa8b4285d527092fe87d89c74ec804f8e169e',
'file_name': 'api_inference_orchestration.zip',
'task_id': 'c9622cf8-cbe5-4e3c-b64c-dabd6c5b7fef',
'owner_id': 'a6857628-b0aa-451e-a0a0-bbc1d6eea6e0',
'status': 'packaging'}
{‘id’: ‘b951f7b8-0690-4004-86bf-cc9802359313’,
‘workspace_id’: 8,
‘sha’: ‘d3b93c9f280734106376e684792aa8b4285d527092fe87d89c74ec804f8e169e’,
‘file_name’: ‘api_inference_orchestration.zip’,
’task_id’: ‘c9622cf8-cbe5-4e3c-b64c-dabd6c5b7fef’,
‘owner_id’: ‘a6857628-b0aa-451e-a0a0-bbc1d6eea6e0’,
‘status’: ‘packaging’}
{‘id’: ‘b951f7b8-0690-4004-86bf-cc9802359313’,
‘workspace_id’: 8,
‘sha’: ‘d3b93c9f280734106376e684792aa8b4285d527092fe87d89c74ec804f8e169e’,
‘file_name’: ‘api_inference_orchestration.zip’,
’task_id’: ‘c9622cf8-cbe5-4e3c-b64c-dabd6c5b7fef’,
‘owner_id’: ‘a6857628-b0aa-451e-a0a0-bbc1d6eea6e0’,
‘status’: ‘packaging’}
{‘id’: ‘b951f7b8-0690-4004-86bf-cc9802359313’,
‘workspace_id’: 8,
‘sha’: ‘d3b93c9f280734106376e684792aa8b4285d527092fe87d89c74ec804f8e169e’,
‘file_name’: ‘api_inference_orchestration.zip’,
’task_id’: ‘c9622cf8-cbe5-4e3c-b64c-dabd6c5b7fef’,
‘owner_id’: ‘a6857628-b0aa-451e-a0a0-bbc1d6eea6e0’,
‘status’: ‘packaging’}
{‘id’: ‘b951f7b8-0690-4004-86bf-cc9802359313’,
‘workspace_id’: 8,
‘sha’: ‘d3b93c9f280734106376e684792aa8b4285d527092fe87d89c74ec804f8e169e’,
‘file_name’: ‘api_inference_orchestration.zip’,
’task_id’: ‘c9622cf8-cbe5-4e3c-b64c-dabd6c5b7fef’,
‘owner_id’: ‘a6857628-b0aa-451e-a0a0-bbc1d6eea6e0’,
‘status’: ‘packaging’}
{‘id’: ‘b951f7b8-0690-4004-86bf-cc9802359313’,
‘workspace_id’: 8,
‘sha’: ‘d3b93c9f280734106376e684792aa8b4285d527092fe87d89c74ec804f8e169e’,
‘file_name’: ‘api_inference_orchestration.zip’,
’task_id’: ‘c9622cf8-cbe5-4e3c-b64c-dabd6c5b7fef’,
‘owner_id’: ‘a6857628-b0aa-451e-a0a0-bbc1d6eea6e0’,
‘status’: ‘packaging’}
{‘id’: ‘b951f7b8-0690-4004-86bf-cc9802359313’,
‘workspace_id’: 8,
‘sha’: ‘d3b93c9f280734106376e684792aa8b4285d527092fe87d89c74ec804f8e169e’,
‘file_name’: ‘api_inference_orchestration.zip’,
’task_id’: ‘c9622cf8-cbe5-4e3c-b64c-dabd6c5b7fef’,
‘owner_id’: ‘a6857628-b0aa-451e-a0a0-bbc1d6eea6e0’,
‘status’: ‘packaging’}
{‘id’: ‘b951f7b8-0690-4004-86bf-cc9802359313’,
‘workspace_id’: 8,
‘sha’: ‘d3b93c9f280734106376e684792aa8b4285d527092fe87d89c74ec804f8e169e’,
‘file_name’: ‘api_inference_orchestration.zip’,
’task_id’: ‘c9622cf8-cbe5-4e3c-b64c-dabd6c5b7fef’,
‘owner_id’: ‘a6857628-b0aa-451e-a0a0-bbc1d6eea6e0’,
‘status’: ‘packaging’}
{‘id’: ‘b951f7b8-0690-4004-86bf-cc9802359313’,
‘workspace_id’: 8,
‘sha’: ‘d3b93c9f280734106376e684792aa8b4285d527092fe87d89c74ec804f8e169e’,
‘file_name’: ‘api_inference_orchestration.zip’,
’task_id’: ‘c9622cf8-cbe5-4e3c-b64c-dabd6c5b7fef’,
‘owner_id’: ‘a6857628-b0aa-451e-a0a0-bbc1d6eea6e0’,
‘status’: ‘ready’}
Task Management Tutorial
Once an Orchestration has the status ready
, it can be run as a task. Tasks have three run options.
Type | SDK Call | How triggered |
---|---|---|
Once | orchestration.run_once(name, json_args, timeout) | Task runs once and exits. |
Scheduled | orchestration.run_scheduled(name, schedule, timeout, json_args) | User provides schedule. Task runs exits whenever schedule dictates. |
Run Task Once via API
We’ll do both a Run Once task and generate our Run Once Task from our orchestration. Orchestrations are started as a run once task with the following request:
- REQUEST
- POST
/v1/api/orchestration/task/run_once
- POST
- PARAMETERS
- name (String Required): The name to assign to the task.
- workspace_id (Integer Required): The numerical identifier of the workspace associated with the orchestration.
- orch_id(String Required): The orchestration ID represented by a UUID.
- json(Dict Required): The parameters to pass to the task.
Tasks are generated and run once with the Orchestration run_once
method. Any arguments for the orchestration are passed in as a Dict
. If there are no arguments, then an empty set {}
is passed.
# retrieve the authorization token
headers = wl.auth.auth_header()
data = {
"name": "api run once task",
"workspace_id": workspace_id,
"orch_id": orchestration_id,
"json": {
"workspace_name": workspace_name,
"pipeline_name": pipeline_name
}
}
import datetime
task_start = datetime.datetime.now()
url=f"{APIURL}/v1/api/task/run_once"
response=requests.post(url, headers=headers, json=data).json()
display(response)
task_id = response['id']
{'id': 'c868aa44-f7fe-4e3d-b11d-e1e6af3ec150'}
Task Status via API
The list of tasks in the Wallaroo instance is retrieves through the Wallaroo MLOPs API request:
- REQUEST
- POST
/v1/api/task/get_by_id
- POST
- PARAMETERS
- task: The numerical identifier of the workspace associated with the orchestration.
- orch_id: The orchestration ID represented by a UUID.
- json: The parameters to pass to the task.
For this example, the status of the previously created task will be generated, then looped until it has reached status started
.
# retrieve the authorization token
headers = wl.auth.auth_header()
url=f"{APIURL}/v1/api/task/get_by_id"
data = {
"id": task_id
}
status = None
while status != 'started':
response=requests.post(url, headers=headers, json=data).json()
display(response)
status = response['status']
time.sleep(10)
{'name': 'api run once task',
'id': 'c868aa44-f7fe-4e3d-b11d-e1e6af3ec150',
'image': 'proxy.replicated.com/proxy/wallaroo/ghcr.io/wallaroolabs/arbex-orch-deploy',
'image_tag': 'v2023.2.0-main-3271',
'bind_secrets': ['minio'],
'extra_env_vars': {'MINIO_URL': 'http://minio.wallaroo.svc.cluster.local:9000',
'ORCH_OWNER_ID': 'a6857628-b0aa-451e-a0a0-bbc1d6eea6e0',
'ORCH_SHA': 'd3b93c9f280734106376e684792aa8b4285d527092fe87d89c74ec804f8e169e',
'TASK_DEBUG': 'false',
'TASK_ID': 'c868aa44-f7fe-4e3d-b11d-e1e6af3ec150'},
'auth_init': True,
'workspace_id': 8,
'flavor': 'exec_orch_oneshot',
'reap_threshold_secs': 900,
'exec_type': 'job',
'status': 'pending',
'input_data': {'pipeline_name': 'apipipelinegsze',
'workspace_name': 'apiorchestrationworkspacegsze'},
'killed': False,
'created_at': '2023-05-22T21:08:31.099447+00:00',
'updated_at': '2023-05-22T21:08:31.105312+00:00',
'last_runs': []}
{’name’: ‘api run once task’,
‘id’: ‘c868aa44-f7fe-4e3d-b11d-e1e6af3ec150’,
‘image’: ‘proxy.replicated.com/proxy/wallaroo/ghcr.io/wallaroolabs/arbex-orch-deploy’,
‘image_tag’: ‘v2023.2.0-main-3271’,
‘bind_secrets’: [‘minio’],
’extra_env_vars’: {‘MINIO_URL’: ‘http://minio.wallaroo.svc.cluster.local:9000’,
‘ORCH_OWNER_ID’: ‘a6857628-b0aa-451e-a0a0-bbc1d6eea6e0’,
‘ORCH_SHA’: ‘d3b93c9f280734106376e684792aa8b4285d527092fe87d89c74ec804f8e169e’,
‘TASK_DEBUG’: ‘false’,
‘TASK_ID’: ‘c868aa44-f7fe-4e3d-b11d-e1e6af3ec150’},
‘auth_init’: True,
‘workspace_id’: 8,
‘flavor’: ’exec_orch_oneshot’,
‘reap_threshold_secs’: 900,
’exec_type’: ‘job’,
‘status’: ‘started’,
‘input_data’: {‘pipeline_name’: ‘apipipelinegsze’,
‘workspace_name’: ‘apiorchestrationworkspacegsze’},
‘killed’: False,
‘created_at’: ‘2023-05-22T21:08:31.099447+00:00’,
‘updated_at’: ‘2023-05-22T21:08:36.585775+00:00’,
’last_runs’: [{‘run_id’: ‘96a7f85f-e30c-40b5-9185-0dee5bd1a15e’,
‘status’: ‘running’,
‘created_at’: ‘2023-05-22T21:08:33.112805+00:00’,
‘updated_at’: ‘2023-05-22T21:08:33.112805+00:00’}]}
Task Results
We can view the inferences from our logs and verify that new entries were added from our task. In our case, we’ll assume the task once started takes about 1 minute to run (deploy the pipeline, run the inference, undeploy the pipeline). We’ll add in a wait of 1 minute, then display the logs during the time period the task was running.
time.sleep(30)
task_end = datetime.datetime.now()
display(task_end)
pipeline.logs(start_datetime = task_start, end_datetime = task_end)
datetime.datetime(2023, 5, 22, 21, 9, 32, 447321)
time | in.tensor | out.variable | check_failures | |
---|---|---|---|---|
0 | 2023-05-22 21:08:37.779 | [4.0, 2.5, 2900.0, 5505.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2900.0, 0.0, 47.6063, -122.02, 2970.0, 5251.0, 12.0, 0.0, 0.0] | [718013.7] | 0 |
Get Tasks by Orchestration SHA
Tasks tied to the same orchestration are retrieved through the following request.
- REQUEST
- POST
/v1/api/task/get_tasks_by_orch_sha
- POST
- PARAMETERS
- sha: The orchestrations SHA hash.
- RETURNS
ids
: List[string] List of tasks by UUID.
# retrieve the authorization token
headers = wl.auth.auth_header()
url=f"{APIURL}/v1/api/task/get_tasks_by_orch_sha"
data = {
"sha": orchestration_sha
}
response=requests.post(url, headers=headers, json=data).json()
display(response)
{'ids': ['2424a9a7-2331-42f6-bd84-90643386b130',
'c868aa44-f7fe-4e3d-b11d-e1e6af3ec150',
'a41fe4ae-b8a4-4e1f-a45a-114df64ae2bc']}
Task Last Runs History
The history of a task, which each deployment of the task is known as a task run is retrieved with the Task last_runs
method that takes the following arguments. It returns the reverse chronological order of tasks runs listed by updated_at
.
- REQUEST
- POST
/v1/api/task/list_task_runs
- POST
- PARAMETERS
- task_id: The numerical identifier of the task.
- status: Filters the task history by the
status
. Ifall
, returns all statuses. Status values are:running
: The task has started.failure
: The task failed.success
: The task completed.
- limit: The number of tasks runs to display.
- RETURNS
- ids: List of task runs ids in UUID.
# retrieve the authorization token
headers = wl.auth.auth_header()
url=f"{APIURL}/v1/api/task/list_task_runs"
data = {
"task_id": task_id
}
response=requests.post(url, headers=headers, json=data).json()
task_run_id = response[0]['run_id']
display(response)
[{'task': 'c868aa44-f7fe-4e3d-b11d-e1e6af3ec150',
'run_id': '96a7f85f-e30c-40b5-9185-0dee5bd1a15e',
'status': 'success',
'created_at': '2023-05-22T21:08:33.112805+00:00',
'updated_at': '2023-05-22T21:08:33.112805+00:00'}]
Get Task Run Logs
Logs for a task run are retrieved through the following process.
- REQUEST
- POST
/v1/api/task/get_logs_for_run
- POST
- PARAMETERS
- id: The numerical identifier of the task run associated with the orchestration.
- lines: The number of log lines to retrieve starting from the end of the log.
- RETURNS
- logs: Array of log entries.
# retrieve the authorization token
headers = wl.auth.auth_header()
url=f"{APIURL}/v1/api/task/get_logs_for_run"
data = {
"id": task_run_id
}
response=requests.post(url, headers=headers, json=data).json()
display(response)
{'logs': ["2023-05-22T21:09:17.683428502Z stdout F {'pipeline_name': 'apipipelinegsze', 'workspace_name': 'apiorchestrationworkspacegsze'}",
'2023-05-22T21:09:17.683489102Z stdout F Getting the workspace apiorchestrationworkspacegsze',
'2023-05-22T21:09:17.683497403Z stdout F Getting the pipeline apipipelinegsze',
'2023-05-22T21:09:17.683504003Z stdout F Deploying the pipeline.',
'2023-05-22T21:09:17.683510203Z stdout F Performing sample inference.',
'2023-05-22T21:09:17.683516203Z stdout F time ... check_failures',
'2023-05-22T21:09:17.683521903Z stdout F 0 2023-05-22 21:08:37.779 ... 0',
'2023-05-22T21:09:17.683527803Z stdout F ',
'2023-05-22T21:09:17.683533603Z stdout F [1 rows x 4 columns]',
'2023-05-22T21:09:17.683540103Z stdout F Undeploying the pipeline']}
Run Task Scheduled via API
The other method of using tasks is as a scheduled run through the Orchestration run_scheduled(name, schedule, timeout, json_args)
. This sets up a task to run on an regular schedule as defined by the schedule
parameter in the cron
service format. For example:
schedule={'42 * * * *'}
Runs on the 42nd minute of every hour.
The following schedule runs every day at 12 noon from February 1 to February 15 2024 - and then ends.
schedule={'0 0 12 1-15 2 2024'}
The Run Scheduled Task request is available at the following address:
/v1/api/task/run_scheduled
And takes the following parameters.
- name (String Required): The name to assign to the task.
- orch_id (String Required): The UUID orchestration ID to create the task from.
- workspace_id (Integer Required): The numberical identifier for the workspace.
- schedule (String Required): The schedule as a single string in
cron
format. - timeout(Integer Optional): The timeout to complete the task in seconds.
- json (String Required): The arguments to pass to the task.
For our example, we will create a scheduled task to run every 5 minutes, display the inference results, then use the Orchestration kill
task to keep the task from running any further.
It is recommended that orchestrations that have pipeline deploy or undeploy commands be spaced out no less than 5 minutes to prevent colliding with other tasks that use the same pipeline.
# retrieve the authorization token
headers = wl.auth.auth_header()
data = {
"name": "scheduled api task",
"workspace_id": workspace_id,
"orch_id": orchestration_id,
"schedule": "*/5 * * * *",
"json": {
"workspace_name": workspace_name,
"pipeline_name": pipeline_name
}
}
import datetime
task_start = datetime.datetime.now()
url=f"{APIURL}/v1/api/task/run_scheduled"
response=requests.post(url, headers=headers, json=data).json()
display(response)
scheduled_task_id = response['id']
{'id': '87c2c7c0-e17e-4c00-9407-bfc44d632910'}
# loop until the task is started
# retrieve the authorization token
headers = wl.auth.auth_header()
url=f"{APIURL}/v1/api/task/get_by_id"
data = {
"id": scheduled_task_id
}
status = None
while status != 'started':
response=requests.post(url, headers=headers, json=data).json()
display(response)
status = response['status']
time.sleep(10)
{'name': 'scheduled api task',
'id': '87c2c7c0-e17e-4c00-9407-bfc44d632910',
'image': 'proxy.replicated.com/proxy/wallaroo/ghcr.io/wallaroolabs/arbex-orch-deploy',
'image_tag': 'v2023.2.0-main-3271',
'bind_secrets': ['minio'],
'extra_env_vars': {'MINIO_URL': 'http://minio.wallaroo.svc.cluster.local:9000',
'ORCH_OWNER_ID': 'a6857628-b0aa-451e-a0a0-bbc1d6eea6e0',
'ORCH_SHA': 'd3b93c9f280734106376e684792aa8b4285d527092fe87d89c74ec804f8e169e',
'TASK_DEBUG': 'false',
'TASK_ID': '87c2c7c0-e17e-4c00-9407-bfc44d632910'},
'auth_init': True,
'workspace_id': 8,
'schedule': '*/5 * * * *',
'reap_threshold_secs': 900,
'status': 'started',
'input_data': {'pipeline_name': 'apipipelinegsze',
'workspace_name': 'apiorchestrationworkspacegsze'},
'killed': False,
'created_at': '2023-05-22T21:10:22.43957+00:00',
'updated_at': '2023-05-22T21:10:22.871615+00:00',
'last_runs': []}
# retrieve the authorization token
headers = wl.auth.auth_header()
url=f"{APIURL}/v1/api/task/list_task_runs"
data = {
"task_id": scheduled_task_id
}
# loop until we have a single run task
length = 0
while length == 0:
response=requests.post(url, headers=headers, json=data).json()
display(response)
length = len(response)
time.sleep(10)
task_run_id = response[0]['run_id']
[{'task': '87c2c7c0-e17e-4c00-9407-bfc44d632910',
'run_id': 'ad3d427a-3643-4f10-9b94-116688b32355',
'status': 'running',
'created_at': '2023-05-22T21:15:02.148274+00:00',
'updated_at': '2023-05-22T21:15:02.148274+00:00'}]
# retrieve the authorization token
headers = wl.auth.auth_header()
url=f"{APIURL}/v1/api/task/get_logs_for_run"
data = {
"id": task_run_id
}
# loop until we get a log
response=requests.post(url, headers=headers, json=data).json()
display(response)
{'logs': ["2023-05-22T21:15:55.548754679Z stdout F {'pipeline_name': 'apipipelinegsze', 'workspace_name': 'apiorchestrationworkspacegsze'}",
'2023-05-22T21:15:55.54880928Z stdout F Getting the workspace apiorchestrationworkspacegsze',
'2023-05-22T21:15:55.54881708Z stdout F Getting the pipeline apipipelinegsze',
'2023-05-22T21:15:55.54882328Z stdout F Deploying the pipeline.',
'2023-05-22T21:15:55.54883088Z stdout F Performing sample inference.',
'2023-05-22T21:15:55.54883628Z stdout F time ... check_failures',
'2023-05-22T21:15:55.54884248Z stdout F 0 2023-05-22 21:15:17.420 ... 0',
'2023-05-22T21:15:55.54884788Z stdout F ',
'2023-05-22T21:15:55.54885348Z stdout F [1 rows x 4 columns]',
'2023-05-22T21:15:55.54885938Z stdout F Undeploying the pipeline']}
Cleanup
With the tutorial complete, we can undeploy the pipeline and return the resources back to the Wallaroo instance.
Kill Task via API
Started tasks are killed through the following request:
/v1/api/task/kill
And takes the following parameters.
- orch_id (String Required): The UUID orchestration ID to create the task from.
# retrieve the authorization token
headers = wl.auth.auth_header()
url=f"{APIURL}/v1/api/task/kill"
data = {
"id": scheduled_task_id
}
response=requests.post(url, headers=headers, json=data).json()
display(response)
{'name': 'scheduled api task',
'id': '87c2c7c0-e17e-4c00-9407-bfc44d632910',
'image': 'proxy.replicated.com/proxy/wallaroo/ghcr.io/wallaroolabs/arbex-orch-deploy',
'image_tag': 'v2023.2.0-main-3271',
'bind_secrets': ['minio'],
'extra_env_vars': {'MINIO_URL': 'http://minio.wallaroo.svc.cluster.local:9000',
'ORCH_OWNER_ID': 'a6857628-b0aa-451e-a0a0-bbc1d6eea6e0',
'ORCH_SHA': 'd3b93c9f280734106376e684792aa8b4285d527092fe87d89c74ec804f8e169e',
'TASK_DEBUG': 'false',
'TASK_ID': '87c2c7c0-e17e-4c00-9407-bfc44d632910'},
'auth_init': True,
'workspace_id': 8,
'schedule': '*/5 * * * *',
'reap_threshold_secs': 900,
'status': 'pending_kill',
'input_data': {'pipeline_name': 'apipipelinegsze',
'workspace_name': 'apiorchestrationworkspacegsze'},
'killed': False,
'created_at': '2023-05-22T21:10:22.43957+00:00',
'updated_at': '2023-05-22T21:10:22.871615+00:00',
'last_runs': [{'run_id': 'ad3d427a-3643-4f10-9b94-116688b32355',
'status': 'success',
'created_at': '2023-05-22T21:15:02.148274+00:00',
'updated_at': '2023-05-22T21:15:02.148274+00:00'}]}
Close Resources
With the tutorial complete, we’ll verify the pipeline is closed so the resources are assigned back to the Wallaroo instance.
pipeline.undeploy()
ok
name | apipipelinegsze |
---|---|
created | 2023-05-22 20:48:30.700499+00:00 |
last_updated | 2023-05-22 20:48:31.357336+00:00 |
deployed | False |
tags | |
versions | aac61b5a-e4f4-4ea3-9347-6482c330b5f5, 101b252a-623c-4185-a24d-ec00593dda79 |
steps | apiorchestrationmodelgsze |
7.2 - Wallaroo Connection API with Google BigQuery Tutorial
This can be downloaded as part of the Wallaroo Tutorials repository.
Wallaroo Connection and ML Workload Orchestration with BigQuery House Price Model Tutorial
This tutorial provides a quick set of methods and examples regarding Wallaroo Connections. For full details, see the Wallaroo Documentation site.
Wallaroo provides Data Connections to organizations with a method of creating and managing automated tasks that can either be run on demand or a regular schedule.
Definitions
- Orchestration: A set of instructions written as a python script with a requirements library. Orchestrations are uploaded to the Wallaroo instance as a .zip file.
- Task: An implementation of an orchestration. Tasks are run either once when requested, on a repeating schedule, or as a service.
- Connection: Definitions set by MLOps engineers that are used by other Wallaroo users for connection information to a data source. Usually paired with orchestrations.
This tutorial will focus on using Google BigQuery as the data source.
Tutorial Goals
The tutorial will demonstrate the following:
- Create a Wallaroo connection to retrieving information from a Google BigQuery source table.
- Create a Wallaroo connection to store inference results into a Google BigQuery destination table.
- Upload Wallaroo ML Workload Orchestration that supports BigQuery connections with the connection details.
- Run the orchestration once as a Run Once Task and verify that the inference request succeeded and the inference results were saved to the external data store.
- Schedule the orchestration as a Scheduled Task and verify that the inference request succeeded and the inference results were saved to the external data store.
Prerequisites
- An installed Wallaroo instance.
- The following Python libraries installed. These are included by default in a Wallaroo instance’s JupyterHub service.
- The following Python libraries. These are not included in a Wallaroo instance’s JupyterHub service.
google-cloud-bigquery
: Specifically for its support for Google BigQuery.google-auth
: Used to authenticate for bigquery.db-dtypes
: Converts the BigQuery results to Apache Arrow table or pandas DataFrame.
Tutorial Resources
- Models:
models/rf_model.onnx
: A model that predicts house price values.
- Data:
data/xtest-1.df.json
anddata/xtest-1k.df.json
: DataFrame JSON inference inputs with 1 input and 1,000 inputs.data/xtest-1k.arrow
: Apache Arrow inference inputs with 1 input and 1,000 inputs.- Sample inference inputs in
CSV
that can be imported into Google BigQuery.data/xtest-1k.df.json
: Random sample housing prices.data/smallinputs.df.json
: Sample housing prices that return results lower than $1.5 million.data/biginputs.df.json
: Sample housing prices that return results higher than $1.5 million.
- SQL queries to create the inputs/outputs tables with schema.
./resources/create_inputs_table.sql
: Inputs table with schema../resources/create_outputs_table.sql
: Outputs table with schema../resources/housrpricesga_inputs.avro
: Avro container of inputs table.
Initial Steps
For this tutorial, we’ll create a workspace, upload our sample model and deploy a pipeline. We’ll perform some quick sample inferences to verify that everything it working.
Load Libraries
Here we’ll import the various libraries we’ll use for the tutorial.
import wallaroo
from wallaroo.object import EntityNotFoundError, RequiredAttributeMissing
# to display dataframe tables
from IPython.display import display
# used to display dataframe information without truncating
import pandas as pd
pd.set_option('display.max_colwidth', None)
import pyarrow as pa
import time
import json
# for Big Query connections
from google.cloud import bigquery
from google.oauth2 import service_account
import db_dtypes
import requests
# used for unique connection names
import string
import random
suffix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
wallaroo.__version__
'2023.2.0rc3'
Connect to the Wallaroo Instance
The first step is to connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. If logging in externally, update the wallarooPrefix
and wallarooSuffix
variables with the proper DNS information. For more information on Wallaroo Client settings, see the Client Connection guide.
# Login through local Wallaroo instance
wl = wallaroo.Client()
API URL
The variable APIURL
is used to specify the connection to the Wallaroo instance’s MLOps API URL, and is composed of the Wallaroo DNS prefix and suffix. For full details, see the Wallaroo API Connection Guide
.
The variables wallarooPrefix
and wallarooSuffix
variables will be used to derive the API url. For example, if the Wallaroo Prefix is doc-test.
and the url is example.com
, then the MLOps API URL would be doc-test.api.example.com/v1/api/{request}
.
Note the .
is part of the prefix. If there is no prefix, then wallarooPrefix = ""
Set the Wallaroo Prefix and Suffix in the code segment below based on your Wallaroo instance.
Variable Declaration
The following variables will be used for our big query testing.
We’ll use two connections:
- bigquery_input_connection: The connection that will draw inference input data from a BigQuery table.
- bigquery_output_connection: The connection that will upload inference results into a BigQuery table.
Not that for the connection arguments, we’ll retrieve the information from the files ./bigquery_service_account_input_key.json
and ./bigquery_service_account_output_key.json
that include the service account key file(SAK) information, as well as the dataset and table used.
Field | Included in SAK |
---|---|
type | √ |
project_id | √ |
private_key_id | √ |
private_key | √ |
client_email | √ |
auth_uri | √ |
token_uri | √ |
auth_provider_x509_cert_url | √ |
client_x509_cert_url | √ |
database | 🚫 |
table | 🚫 |
# Setting variables for later steps
wallarooPrefix = "YOUR PREFIX."
wallarooSuffix = "YOUR SUFFIX"
APIURL = f"https://{wallarooPrefix}api.{wallarooSuffix}"
# Setting variables for later steps
workspace_name = 'bigqueryapiworkspace'
pipeline_name = 'bigqueryapipipeline'
model_name = 'bigqueryapimodel'
model_file_name = './models/rf_model.onnx'
bigquery_connection_input_name = f"bigqueryhouseapiinput{suffix}"
bigquery_connection_input_type = "BIGQUERY"
bigquery_connection_input_argument = json.load(open('./bigquery_service_account_input_key.json'))
bigquery_connection_output_name = f"bigqueryhouseapioutputs{suffix}"
bigquery_connection_output_type = "BIGQUERY"
bigquery_connection_output_argument = json.load(open('./bigquery_service_account_output_key.json'))
Helper Methods
The following helper methods are used to either create or get workspaces, pipelines, and connections.
# helper methods to retrieve workspaces and pipelines
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
pipeline = wl.build_pipeline(name)
return pipeline
def get_connection(name, connection_type, connection_arguments):
try:
connection = wl.get_connection(name)
except RequiredAttributeMissing:
connection =wl.create_connection(name,
connection_type,
connection_arguments)
return connection
Create the Workspace and Pipeline
We’ll now create our workspace and pipeline for the tutorial. If this tutorial has been run previously, then this will retrieve the existing ones with the assumption they’re for us with this tutorial.
We’ll set the retrieved workspace as the current workspace in the SDK, so all commands will default to that workspace.
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
workspace_id = workspace.id()
pipeline = get_pipeline(pipeline_name)
Upload the Model and Deploy Pipeline
We’ll upload our model into our sample workspace, then add it as a pipeline step before deploying the pipeline to it’s ready to accept inference requests.
# Upload the model
housing_model_control = wl.upload_model(model_name, model_file_name, framework=wallaroo.framework.Framework.ONNX).configure()
# Add the model as a pipeline step
pipeline.add_model_step(housing_model_control)
name | bigqueryapipipeline |
---|---|
created | 2023-05-17 16:06:45.400005+00:00 |
last_updated | 2023-05-17 16:06:48.150211+00:00 |
deployed | True |
tags | |
versions | 5e78de4f-51f1-43ce-8c2c-d724a05f856a, d2f03a9c-dfbb-4a03-a014-e70aa80902e3 |
steps | bigqueryapimodel |
#deploy the pipeline
pipeline.deploy()
name | bigqueryapipipeline |
---|---|
created | 2023-05-17 16:06:45.400005+00:00 |
last_updated | 2023-05-17 16:11:23.317215+00:00 |
deployed | True |
tags | |
versions | 153a11e5-7968-450c-aef5-d1be17c6b173, 5e78de4f-51f1-43ce-8c2c-d724a05f856a, d2f03a9c-dfbb-4a03-a014-e70aa80902e3 |
steps | bigqueryapimodel |
Connection Management via the Wallaroo MLOps API
The following steps will demonstration using the Wallaroo MLOps API to:
- Create the BigQuery connections
- Add the connections to the targeted workspace
- Use the connections for inference requests and uploading the results to a BigQuery dataset table.
Create Connections via API
We will create the data source connection via the Wallaroo api request:
/v1/api/connections/create
This takes the following parameters:
name (String Required): The name of the connection.
type (String Required): The user defined type of connection.
details (String Required): User defined configuration details for the data connection. These can be
{'username':'dataperson', 'password':'datapassword', 'port': 3339}
, or{'token':'abcde123==', 'host':'example.com', 'port:1234'}
, or other user defined combinations.IMPORTANT NOTE: Data connections names must be unique. Attempting to create a data connection with the same
name
as an existing data connection will result in an error.
# retrieve the authorization token
headers = wl.auth.auth_header()
url = f"{APIURL}/v1/api/connections/create"
# input connection
data = {
'name': bigquery_connection_input_name,
'type' : bigquery_connection_input_type,
'details': bigquery_connection_input_argument
}
response=requests.post(url, headers=headers, json=data).json()
display(response)
# saved for later steps
connection_input_id = response['id']
{'id': '839779d7-d4b3-4e1c-953a-2f5ef55f1bb2'}
# retrieve the authorization token
headers = wl.auth.auth_header()
url = f"{APIURL}/v1/api/connections/create"
# output connection
data = {
'name': bigquery_connection_output_name,
'type' : bigquery_connection_output_type,
'details': bigquery_connection_output_argument
}
response=requests.post(url, headers=headers, json=data).json()
display(response)
# saved for later steps
connection_output_id = response['id']
{'id': 'ef0c62e7-e59f-4c43-bdff-05ed0976cffa'}
Add Connections to Workspace via API
The connections will be added to the sample workspace with the MLOps API request:
/v1/api/connections/add_to_workspace
This takes the following parameters:
- workspace_id (String Required): The name of the connection.
- connection_id (String Required): The UUID connection ID
# retrieve the authorization token
headers = wl.auth.auth_header()
url = f"{APIURL}/v1/api/connections/add_to_workspace"
data = {
'workspace_id': workspace_id,
'connection_id': connection_input_id
}
response=requests.post(url, headers=headers, json=data)
display(response.json())
data = {
'workspace_id': workspace_id,
'connection_id': connection_output_id
}
response=requests.post(url, headers=headers, json=data)
display(response.json())
{'id': '3877da9d-e450-4b9a-85dd-7f68127b1313'}
{‘id’: ‘15f247ab-833f-468a-b7c5-9b50831052e6’}
Connect to Google BigQuery
With our connections set, we’ll now use them for an inference request through the following steps:
- Retrieve the input data from a BigQuery request from the input connection details.
- Perform the inference.
- Upload the inference results into another BigQuery table from the output connection details.
Create Google Credentials
From our BigQuery request, we’ll create the credentials for our BigQuery connection.
We will use the MLOps API call:
/v1/api/connections/get
to retrieve the connection. This request takes the following parameters:
- name (String Required): The name of the connection.
# get the connection input details
# retrieve the authorization token
headers = wl.auth.auth_header()
url = f"{APIURL}/v1/api/connections/get"
data = {
'name': bigquery_connection_input_name
}
connection_input_details=requests.post(url, headers=headers, json=data).json()['details']
# get the connection output details
# retrieve the authorization token
headers = wl.auth.auth_header()
url = f"{APIURL}/v1/api/connections/get"
data = {
'name': bigquery_connection_output_name
}
connection_output_details=requests.post(url, headers=headers, json=data).json()['details']
# Set the bigquery credentials
bigquery_input_credentials = service_account.Credentials.from_service_account_info(
connection_input_details)
bigquery_output_credentials = service_account.Credentials.from_service_account_info(
connection_output_details)
Connect to Google BigQuery
We can now generate a client from our connection details, specifying the project that was included in the big_query_connection
details.
bigqueryinputclient = bigquery.Client(
credentials=bigquery_input_credentials,
project=connection_input_details['project_id']
)
bigqueryoutputclient = bigquery.Client(
credentials=bigquery_output_credentials,
project=connection_output_details['project_id']
)
Query Data
Now we’ll create our query and retrieve information from out dataset and table as defined in the file bigquery_service_account_key.json
. The table is expected to be in the format of the file ./data/xtest-1k.df.json
.
inference_dataframe_input = bigqueryinputclient.query(
f"""
SELECT tensor
FROM {connection_input_details['dataset']}.{connection_input_details['table']}"""
).to_dataframe()
inference_dataframe_input.head(5)
tensor | |
---|---|
0 | [4.0, 2.5, 2900.0, 5505.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2900.0, 0.0, 47.6063, -122.02, 2970.0, 5251.0, 12.0, 0.0, 0.0] |
1 | [2.0, 2.5, 2170.0, 6361.0, 1.0, 0.0, 2.0, 3.0, 8.0, 2170.0, 0.0, 47.7109, -122.017, 2310.0, 7419.0, 6.0, 0.0, 0.0] |
2 | [3.0, 2.5, 1300.0, 812.0, 2.0, 0.0, 0.0, 3.0, 8.0, 880.0, 420.0, 47.5893, -122.317, 1300.0, 824.0, 6.0, 0.0, 0.0] |
3 | [4.0, 2.5, 2500.0, 8540.0, 2.0, 0.0, 0.0, 3.0, 9.0, 2500.0, 0.0, 47.5759, -121.994, 2560.0, 8475.0, 24.0, 0.0, 0.0] |
4 | [3.0, 1.75, 2200.0, 11520.0, 1.0, 0.0, 0.0, 4.0, 7.0, 2200.0, 0.0, 47.7659, -122.341, 1690.0, 8038.0, 62.0, 0.0, 0.0] |
Sample Inference
With our data retrieved, we’ll perform an inference and display the results.
result = pipeline.infer(inference_dataframe_input)
display(result.head(5))
time | in.tensor | out.variable | check_failures | |
---|---|---|---|---|
0 | 2023-05-17 16:12:53.970 | [4.0, 2.5, 2900.0, 5505.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2900.0, 0.0, 47.6063, -122.02, 2970.0, 5251.0, 12.0, 0.0, 0.0] | [718013.75] | 0 |
1 | 2023-05-17 16:12:53.970 | [2.0, 2.5, 2170.0, 6361.0, 1.0, 0.0, 2.0, 3.0, 8.0, 2170.0, 0.0, 47.7109, -122.017, 2310.0, 7419.0, 6.0, 0.0, 0.0] | [615094.56] | 0 |
2 | 2023-05-17 16:12:53.970 | [3.0, 2.5, 1300.0, 812.0, 2.0, 0.0, 0.0, 3.0, 8.0, 880.0, 420.0, 47.5893, -122.317, 1300.0, 824.0, 6.0, 0.0, 0.0] | [448627.72] | 0 |
3 | 2023-05-17 16:12:53.970 | [4.0, 2.5, 2500.0, 8540.0, 2.0, 0.0, 0.0, 3.0, 9.0, 2500.0, 0.0, 47.5759, -121.994, 2560.0, 8475.0, 24.0, 0.0, 0.0] | [758714.2] | 0 |
4 | 2023-05-17 16:12:53.970 | [3.0, 1.75, 2200.0, 11520.0, 1.0, 0.0, 0.0, 4.0, 7.0, 2200.0, 0.0, 47.7659, -122.341, 1690.0, 8038.0, 62.0, 0.0, 0.0] | [513264.7] | 0 |
Upload the Results
With the query complete, we’ll upload the results back to the BigQuery dataset.
output_table = bigqueryoutputclient.get_table(f"{connection_output_details['dataset']}.{connection_output_details['table']}")
bigqueryoutputclient.insert_rows_from_dataframe(
output_table,
dataframe=result.rename(columns={"in.tensor":"in_tensor", "out.variable":"out_variable"})
)
[[], []]
Verify the Upload
We can verify the upload by requesting the last few rows of the output table.
task_inference_results = bigqueryoutputclient.query(
f"""
SELECT *
FROM {connection_output_details['dataset']}.{connection_output_details['table']}
ORDER BY time DESC
LIMIT 5
"""
).to_dataframe()
display(task_inference_results)
time | in_tensor | out_variable | check_failures | |
---|---|---|---|---|
0 | 2023-05-17 16:12:53.970 | [4.0, 2.5, 2500.0, 8540.0, 2.0, 0.0, 0.0, 3.0, 9.0, 2500.0, 0.0, 47.5759, -121.994, 2560.0, 8475.0, 24.0, 0.0, 0.0] | [758714.2] | 0 |
1 | 2023-05-17 16:12:53.970 | [4.0, 2.5, 2900.0, 5505.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2900.0, 0.0, 47.6063, -122.02, 2970.0, 5251.0, 12.0, 0.0, 0.0] | [718013.75] | 0 |
2 | 2023-05-17 16:12:53.970 | [2.0, 2.5, 2170.0, 6361.0, 1.0, 0.0, 2.0, 3.0, 8.0, 2170.0, 0.0, 47.7109, -122.017, 2310.0, 7419.0, 6.0, 0.0, 0.0] | [615094.56] | 0 |
3 | 2023-05-17 16:12:53.970 | [3.0, 1.75, 2200.0, 11520.0, 1.0, 0.0, 0.0, 4.0, 7.0, 2200.0, 0.0, 47.7659, -122.341, 1690.0, 8038.0, 62.0, 0.0, 0.0] | [513264.7] | 0 |
4 | 2023-05-17 16:12:53.970 | [3.0, 2.5, 1300.0, 812.0, 2.0, 0.0, 0.0, 3.0, 8.0, 880.0, 420.0, 47.5893, -122.317, 1300.0, 824.0, 6.0, 0.0, 0.0] | [448627.72] | 0 |
Cleanup
With the tutorial complete, we can undeploy the pipeline and return the resources back to the Wallaroo instance.
pipeline.undeploy()
name | bigqueryapipipeline |
---|---|
created | 2023-05-17 16:06:45.400005+00:00 |
last_updated | 2023-05-17 16:11:23.317215+00:00 |
deployed | False |
tags | |
versions | 153a11e5-7968-450c-aef5-d1be17c6b173, 5e78de4f-51f1-43ce-8c2c-d724a05f856a, d2f03a9c-dfbb-4a03-a014-e70aa80902e3 |
steps | bigqueryapimodel |
7.3 - Wallaroo ML Workload Orchestration Simple Tutorial
This can be downloaded as part of the Wallaroo Tutorials repository.
Wallaroo Connection and ML Workload Orchestration Simple Tutorial
This tutorial provides a quick set of methods and examples regarding Wallaroo Connections and Wallaroo ML Workload Orchestration. For full details, see the Wallaroo Documentation site.
Wallaroo provides Data Connections and ML Workload Orchestrations to provide organizations with a method of creating and managing automated tasks that can either be run on demand or a regular schedule.
Definitions
- Orchestration: A set of instructions written as a python script with a requirements library. Orchestrations are uploaded to the Wallaroo instance as a .zip file.
- Task: An implementation of an orchestration. Tasks are run either once when requested, on a repeating schedule, or as a service.
- Connection: Definitions set by MLOps engineers that are used by other Wallaroo users for connection information to a data source. Usually paired with orchestrations.
Tutorial Goals
The tutorial will demonstrate the following:
- Create a Wallaroo connection to retrieving information from an external source.
- Upload Wallaroo ML Workload Orchestration.
- Run the orchestration once as a Run Once Task and verify that the information was saved the pipeline logs.
- Schedule the orchestration as a Scheduled Task and verify that the information was saved to the pipeline logs.
Prerequisites
- An installed Wallaroo instance.
- The following Python libraries installed. These are included by default in a Wallaroo instance’s JupyterHub service.
Initial Steps
For this tutorial, we’ll create a workspace, upload our sample model and deploy a pipeline. We’ll perform some quick sample inferences to verify that everything it working.
Load Libraries
Here we’ll import the various libraries we’ll use for the tutorial.
import wallaroo
from wallaroo.object import EntityNotFoundError, RequiredAttributeMissing
# to display dataframe tables
from IPython.display import display
# used to display dataframe information without truncating
import pandas as pd
pd.set_option('display.max_colwidth', None)
import pyarrow as pa
import time
# Used to create unique workspace and pipeline names
import string
import random
# make a random 4 character suffix
suffix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
display(suffix)
'dtzw'
Connect to the Wallaroo Instance
The first step is to connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. For more information on Wallaroo Client settings, see the Client Connection guide.
# Login through local Wallaroo instance
wl = wallaroo.Client()
# Setting variables for later steps
workspace_name = f'simpleorchestrationworkspace{suffix}'
pipeline_name = f'simpleorchestrationpipeline{suffix}'
model_name = f'simpleorchestrationmodel{suffix}'
model_file_name = './models/rf_model.onnx'
inference_connection_name = f'external_inference_connection{suffix}'
inference_connection_type = "HTTP"
inference_connection_argument = {'host':'https://github.com/WallarooLabs/Wallaroo_Tutorials/raw/main/wallaroo-testing-tutorials/houseprice-saga/data/xtest-1k.arrow'}
Helper Methods
The following helper methods are used to either create or get workspaces, pipelines, and connections.
# helper methods to retrieve workspaces and pipelines
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
pipeline = wl.build_pipeline(name)
return pipeline
Create the Workspace and Pipeline
We’ll now create our workspace and pipeline for the tutorial. If this tutorial has been run previously, then this will retrieve the existing ones with the assumption they’re for us with this tutorial.
We’ll set the retrieved workspace as the current workspace in the SDK, so all commands will default to that workspace.
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
pipeline = get_pipeline(pipeline_name)
Upload the Model and Deploy Pipeline
We’ll upload our model into our sample workspace, then add it as a pipeline step before deploying the pipeline to it’s ready to accept inference requests.
# Upload the model
housing_model_control = wl.upload_model(model_name, model_file_name, framework=wallaroo.framework.Framework.ONNX).configure()
# Add the model as a pipeline step
pipeline.add_model_step(housing_model_control)
name | simpleorchestrationpipelinedtzw |
---|---|
created | 2023-05-23 15:26:00.268667+00:00 |
last_updated | 2023-05-23 15:26:00.268667+00:00 |
deployed | (none) |
tags | |
versions | 9a5afba3-c664-4d57-8c08-fc072d3f549c |
steps |
#deploy the pipeline
pipeline.deploy()
Waiting for deployment - this will take up to 45s ....................... ok
name | simpleorchestrationpipelinedtzw |
---|---|
created | 2023-05-23 15:26:00.268667+00:00 |
last_updated | 2023-05-23 15:26:00.568944+00:00 |
deployed | True |
tags | |
versions | 24f5d5e9-59fe-4440-9e08-78c0003226df, 9a5afba3-c664-4d57-8c08-fc072d3f549c |
steps | simpleorchestrationmodeldtzw |
Create Connections
We will create the data source connection via the Wallaroo client command create_connection
.
Connections are created with the Wallaroo client command create_connection
with the following parameters.
Parameter | Type | Description |
---|---|---|
name | string (Required) | The name of the connection. This must be unique - if submitting the name of an existing connection it will return an error. |
type | string (Required) | The user defined type of connection. |
details | Dict (Required) | User defined configuration details for the data connection. These can be {'username':'dataperson', 'password':'datapassword', 'port': 3339} , or {'token':'abcde123==', 'host':'example.com', 'port:1234'} , or other user defined combinations. |
- IMPORTANT NOTE: Data connections names must be unique. Attempting to create a data connection with the same
name
as an existing data connection will result in an error.
We’ll also create a data connection named inference_results_connection
with our helper function get_connection
that will either create or retrieve a connection if it already exists. From there we’ll create out connections:
houseprice_arrow_table
: An Apache Arrow file stored on GitHub that will be used for our inference input.
wl.create_connection(inference_connection_name, inference_connection_type, inference_connection_argument)
Field | Value |
---|---|
Name | external_inference_connectiondtzw |
Connection Type | HTTP |
Details | ***** |
Created At | 2023-05-23T15:26:24.613152+00:00 |
Linked Workspaces | [] |
Get Connection by Name
The Wallaroo client method get_connection(name)
retrieves the connection that matches the name
parameter. We’ll retrieve our connection and store it as inference_source_connection
.
inference_source_connection = wl.get_connection(name=inference_connection_name)
display(inference_source_connection)
Field | Value |
---|---|
Name | external_inference_connectiondtzw |
Connection Type | HTTP |
Details | ***** |
Created At | 2023-05-23T15:26:24.613152+00:00 |
Linked Workspaces | [] |
Add Connection to Workspace
The method Workspace add_connection(connection_name)
adds a Data Connection to a workspace, and takes the following parameters.
Parameter | Type | Description |
---|---|---|
name | string (Required) | The name of the Data Connection |
We’ll add both connections to our sample workspace, then list the connections available to the workspace to confirm.
workspace.add_connection(inference_connection_name)
workspace.list_connections()
name | connection type | details | created at | linked workspaces |
---|---|---|---|---|
external_inference_connectiondtzw | HTTP | ***** | 2023-05-23T15:26:24.613152+00:00 | ['simpleorchestrationworkspacedtzw'] |
Wallaroo ML Workload Orchestration Example
With the pipeline deployed and our connections set, we will now generate our ML Workload Orchestration. See the Wallaroo ML Workload Orchestrations guide for full details.
Orchestrations are uploaded to the Wallaroo instance as a ZIP file with the following requirements:
Parameter | Type | Description |
---|---|---|
User Code | (Required) Python script as .py files | If main.py exists, then that will be used as the task entrypoint. Otherwise, the first main.py found in any subdirectory will be used as the entrypoint. |
Python Library Requirements | (Optional) requirements.txt file in the requirements file format. A standard Python requirements.txt for any dependencies to be provided in the task environment. The Wallaroo SDK will already be present and should not be included in the requirements.txt. Multiple requirements.txt files are not allowed. | |
Other artifacts | Â | Other artifacts such as files, data, or code to support the orchestration. |
For our example, our orchestration will:
- Use the
inference_results_connection
to open a HTTP Get connection to the inference data file and use it in an inference request in the deployed pipeline. - Submit the inference results to the location specified in the
external_inference_connection
.
This sample script is stored in remote_inference/main.py
with an empty requirements.txt
file, and packaged into the orchestration as ./remote_inference/remote_inference.zip
. We’ll display the steps in uploading the orchestration to the Wallaroo instance.
Note that the orchestration assumes the pipeline is already deployed.
Upload the Orchestration
Orchestrations are uploaded with the Wallaroo client upload_orchestration(path)
method with the following parameters.
Parameter | Type | Description |
---|---|---|
path | string (Required) | The path to the ZIP file to be uploaded. |
Once uploaded, the deployment will be prepared and any requirements will be downloaded and installed.
For this example, the orchestration ./remote_inference/remote_inference.zip
will be uploaded and saved to the variable orchestration
.
Orchestration Status
We will loop until the uploaded orchestration’s status
displays ready
.
orchestration = wl.upload_orchestration(path="./remote_inference/remote_inference.zip")
while orchestration.status() != 'ready':
print(orchestration.status())
time.sleep(5)
pending_packaging
pending_packaging
packaging
packaging
packaging
packaging
packaging
packaging
packaging
packaging
wl.list_orchestrations()
id | name | status | filename | sha | created at | updated at |
---|---|---|---|---|---|---|
701c991a-a716-4bca-aa69-afd957ff189e | None | ready | remote_inference.zip | b4593d...d5a86f | 2023-23-May 15:26:24 | 2023-23-May 15:27:13 |
Upload Orchestration via File Object
Another method to upload the orchestration is as a file object. For that, we will open the zip file as a binary, then upload it using the bytes_buffer
parameter to specify the file object, and the file_name
to give it a new name.
zipfile = open("./remote_inference/remote_inference.zip", "rb").read()
wl.upload_orchestration(bytes_buffer=zipfile, file_name="inferencetest.zip", name="uploadedbytesdemo")
Field | Value |
---|---|
ID | 7683f0f9-13fa-4257-840a-8e1cf8b12089 |
Name | uploadedbytesdemo |
File Name | inferencetest.zip |
SHA | b4593d6084e07e9ad1b57367258ca425d7f290540ab4378b8cba168b91d5a86f |
Status | pending_packaging |
Created At | 2023-23-May 15:27:15 |
Updated At | 2023-23-May 15:27:15 |
wl.list_orchestrations()
id | name | status | filename | sha | created at | updated at |
---|---|---|---|---|---|---|
701c991a-a716-4bca-aa69-afd957ff189e | None | ready | remote_inference.zip | b4593d...d5a86f | 2023-23-May 15:26:24 | 2023-23-May 15:27:13 |
7683f0f9-13fa-4257-840a-8e1cf8b12089 | uploadedbytesdemo | pending_packaging | inferencetest.zip | b4593d...d5a86f | 2023-23-May 15:27:15 | 2023-23-May 15:27:15 |
Task Management Tutorial
Once an Orchestration has the status ready
, it can be run as a task. Tasks have three run options.
Type | SDK Call | How triggered |
---|---|---|
Once | orchestration.run_once(name, json_args, timeout) | Task runs once and exits. |
Scheduled | orchestration.run_scheduled(name, schedule, timeout, json_args) | User provides schedule. Task runs exits whenever schedule dictates. |
Run Task Once
We’ll do both a Run Once task and generate our Run Once Task from our orchestration.
Tasks are generated and run once with the Orchestration run_once(name, json_args, timeout)
method. Any arguments for the orchestration are passed in as a Dict
. If there are no arguments, then an empty set {}
is passed.
# Example: run once
import datetime
task_start = datetime.datetime.now()
task = orchestration.run_once(name="simpletaskdemo",
json_args={"workspace_name": workspace_name,
"pipeline_name": pipeline_name,
"connection_name": inference_connection_name
})
Task Status
The list of tasks in the Wallaroo instance is retrieves through the Wallaroo Client list_tasks()
method. This returns an array list of the following.
Parameter | Type | Description |
---|---|---|
id | string | The UUID identifier for the task. |
last run status | string | The last reported status the task. Values are:
|
type | string | The type of the task. Values are:
|
created at | DateTime | The date and time the task was started. |
updated at | DateTime | The date and time the task was updated. |
For this example, the status of the previously created task will be generated, then looped until it has reached status started
.
while task.status() != "started":
display(task.status())
time.sleep(5)
'pending'
‘pending’
‘pending’
Task Results
We can view the inferences from our logs and verify that new entries were added from our task. We can do that with the task logs()
method.
In our case, we’ll assume the task once started takes about 1 minute to run (deploy the pipeline, run the inference, undeploy the pipeline). We’ll add in a wait of 1 minute, then display the logs during the time period the task was running.
time.sleep(60)
task_end = datetime.datetime.now()
display(task_end)
pipeline.logs(start_datetime = task_start, end_datetime = task_end)
datetime.datetime(2023, 5, 23, 15, 28, 30, 718361)
Warning: Pipeline log size limit exceeded. Please request logs using export_logs
time | in.tensor | out.variable | check_failures | |
---|---|---|---|---|
0 | 2023-05-23 15:27:28.001 | [4.0, 2.5, 2900.0, 5505.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2900.0, 0.0, 47.6063, -122.02, 2970.0, 5251.0, 12.0, 0.0, 0.0] | [718013.75] | 0 |
1 | 2023-05-23 15:27:28.001 | [2.0, 2.5, 2170.0, 6361.0, 1.0, 0.0, 2.0, 3.0, 8.0, 2170.0, 0.0, 47.7109, -122.017, 2310.0, 7419.0, 6.0, 0.0, 0.0] | [615094.56] | 0 |
2 | 2023-05-23 15:27:28.001 | [3.0, 2.5, 1300.0, 812.0, 2.0, 0.0, 0.0, 3.0, 8.0, 880.0, 420.0, 47.5893, -122.317, 1300.0, 824.0, 6.0, 0.0, 0.0] | [448627.72] | 0 |
3 | 2023-05-23 15:27:28.001 | [4.0, 2.5, 2500.0, 8540.0, 2.0, 0.0, 0.0, 3.0, 9.0, 2500.0, 0.0, 47.5759, -121.994, 2560.0, 8475.0, 24.0, 0.0, 0.0] | [758714.2] | 0 |
4 | 2023-05-23 15:27:28.001 | [3.0, 1.75, 2200.0, 11520.0, 1.0, 0.0, 0.0, 4.0, 7.0, 2200.0, 0.0, 47.7659, -122.341, 1690.0, 8038.0, 62.0, 0.0, 0.0] | [513264.7] | 0 |
... | ... | ... | ... | ... |
487 | 2023-05-23 15:27:28.001 | [3.0, 1.5, 1030.0, 8414.0, 1.0, 0.0, 0.0, 4.0, 7.0, 1030.0, 0.0, 47.7654, -122.297, 1750.0, 8414.0, 47.0, 0.0, 0.0] | [340764.53] | 0 |
488 | 2023-05-23 15:27:28.001 | [4.0, 2.75, 2450.0, 15002.0, 1.0, 0.0, 0.0, 5.0, 9.0, 2450.0, 0.0, 47.4268, -122.343, 2650.0, 15055.0, 40.0, 0.0, 0.0] | [508746.75] | 0 |
489 | 2023-05-23 15:27:28.001 | [2.0, 1.0, 1010.0, 4000.0, 1.0, 0.0, 0.0, 3.0, 6.0, 1010.0, 0.0, 47.5536, -122.267, 1040.0, 4000.0, 103.0, 0.0, 0.0] | [435628.56] | 0 |
490 | 2023-05-23 15:27:28.001 | [3.0, 2.5, 1330.0, 1200.0, 3.0, 0.0, 0.0, 3.0, 7.0, 1330.0, 0.0, 47.7034, -122.344, 1330.0, 1206.0, 12.0, 0.0, 0.0] | [342604.4] | 0 |
491 | 2023-05-23 15:27:28.001 | [2.0, 1.75, 2770.0, 19700.0, 2.0, 0.0, 0.0, 3.0, 8.0, 1780.0, 990.0, 47.7581, -122.365, 2360.0, 9700.0, 31.0, 0.0, 0.0] | [536371.25] | 0 |
492 rows × 4 columns
Scheduled Run Task Example
The other method of using tasks is as a scheduled run through the Orchestration run_scheduled(name, schedule, timeout, json_args)
. This sets up a task to run on an regular schedule as defined by the schedule
parameter in the cron
service format. For example:
schedule={'42 * * * *'}
Runs on the 42nd minute of every hour.
The following schedule runs every day at 12 noon from February 1 to February 15 2024 - and then ends.
schedule={'0 0 12 1-15 2 2024'}
For our example, we will create a scheduled task to run every 5 minutes, display the inference results, then use the Orchestration kill
task to keep the task from running any further.
It is recommended that orchestrations that have pipeline deploy or undeploy commands be spaced out no less than 5 minutes to prevent colliding with other tasks that use the same pipeline.
scheduled_task_start = datetime.datetime.now()
scheduled_task = orchestration.run_scheduled(name="simple_inference_schedule",
schedule="*/5 * * * *",
timeout=120,
json_args={"workspace_name": workspace_name,
"pipeline_name": pipeline_name,
"connection_name": inference_connection_name
})
while scheduled_task.status() != "started":
display(scheduled_task.status())
time.sleep(5)
'pending'
#wait 420 seconds to give the scheduled event time to finish
time.sleep(420)
scheduled_task_end = datetime.datetime.now()
pipeline.logs(start_datetime = scheduled_task_start, end_datetime = scheduled_task_end)
Warning: Pipeline log size limit exceeded. Please request logs using export_logs
time | in.tensor | out.variable | check_failures | |
---|---|---|---|---|
0 | 2023-05-23 15:30:08.633 | [4.0, 2.5, 2900.0, 5505.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2900.0, 0.0, 47.6063, -122.02, 2970.0, 5251.0, 12.0, 0.0, 0.0] | [718013.75] | 0 |
1 | 2023-05-23 15:30:08.633 | [2.0, 2.5, 2170.0, 6361.0, 1.0, 0.0, 2.0, 3.0, 8.0, 2170.0, 0.0, 47.7109, -122.017, 2310.0, 7419.0, 6.0, 0.0, 0.0] | [615094.56] | 0 |
2 | 2023-05-23 15:30:08.633 | [3.0, 2.5, 1300.0, 812.0, 2.0, 0.0, 0.0, 3.0, 8.0, 880.0, 420.0, 47.5893, -122.317, 1300.0, 824.0, 6.0, 0.0, 0.0] | [448627.72] | 0 |
3 | 2023-05-23 15:30:08.633 | [4.0, 2.5, 2500.0, 8540.0, 2.0, 0.0, 0.0, 3.0, 9.0, 2500.0, 0.0, 47.5759, -121.994, 2560.0, 8475.0, 24.0, 0.0, 0.0] | [758714.2] | 0 |
4 | 2023-05-23 15:30:08.633 | [3.0, 1.75, 2200.0, 11520.0, 1.0, 0.0, 0.0, 4.0, 7.0, 2200.0, 0.0, 47.7659, -122.341, 1690.0, 8038.0, 62.0, 0.0, 0.0] | [513264.7] | 0 |
... | ... | ... | ... | ... |
487 | 2023-05-23 15:30:08.633 | [3.0, 1.5, 1030.0, 8414.0, 1.0, 0.0, 0.0, 4.0, 7.0, 1030.0, 0.0, 47.7654, -122.297, 1750.0, 8414.0, 47.0, 0.0, 0.0] | [340764.53] | 0 |
488 | 2023-05-23 15:30:08.633 | [4.0, 2.75, 2450.0, 15002.0, 1.0, 0.0, 0.0, 5.0, 9.0, 2450.0, 0.0, 47.4268, -122.343, 2650.0, 15055.0, 40.0, 0.0, 0.0] | [508746.75] | 0 |
489 | 2023-05-23 15:30:08.633 | [2.0, 1.0, 1010.0, 4000.0, 1.0, 0.0, 0.0, 3.0, 6.0, 1010.0, 0.0, 47.5536, -122.267, 1040.0, 4000.0, 103.0, 0.0, 0.0] | [435628.56] | 0 |
490 | 2023-05-23 15:30:08.633 | [3.0, 2.5, 1330.0, 1200.0, 3.0, 0.0, 0.0, 3.0, 7.0, 1330.0, 0.0, 47.7034, -122.344, 1330.0, 1206.0, 12.0, 0.0, 0.0] | [342604.4] | 0 |
491 | 2023-05-23 15:30:08.633 | [2.0, 1.75, 2770.0, 19700.0, 2.0, 0.0, 0.0, 3.0, 8.0, 1780.0, 990.0, 47.7581, -122.365, 2360.0, 9700.0, 31.0, 0.0, 0.0] | [536371.25] | 0 |
492 rows × 4 columns
Kill Task
With our testing complete, we will kill the scheduled task so it will not run again. First we’ll show all the tasks to verify that our task is there, then issue it the kill command.
wl.list_tasks()
id | name | last run status | type | active | schedule | created at | updated at |
---|---|---|---|---|---|---|---|
d0b6a83c-a3a1-41f0-98c9-92422d2544c4 | simple_inference_schedule | success | Scheduled Run | True | */5 * * * * | 2023-23-May 15:28:30 | 2023-23-May 15:28:31 |
74046e01-68ab-42a0-bcd2-3493cbc66576 | simpletaskdemo | success | Temporary Run | True | - | 2023-23-May 15:27:15 | 2023-23-May 15:27:26 |
scheduled_task.kill()
<ArbexStatus.PENDING_KILL: 'pending_kill'>
wl.list_tasks()
id | name | last run status | type | active | schedule | created at | updated at |
---|---|---|---|---|---|---|---|
74046e01-68ab-42a0-bcd2-3493cbc66576 | simpletaskdemo | success | Temporary Run | True | - | 2023-23-May 15:27:15 | 2023-23-May 15:27:26 |
Cleanup
With the tutorial complete, we can undeploy the pipeline and return the resources back to the Wallaroo instance.
pipeline.undeploy()
Waiting for undeployment - this will take up to 45s ..................................... ok
name | simpleorchestrationpipelinedtzw |
---|---|
created | 2023-05-23 15:26:00.268667+00:00 |
last_updated | 2023-05-23 15:26:00.568944+00:00 |
deployed | False |
tags | |
versions | 24f5d5e9-59fe-4440-9e08-78c0003226df, 9a5afba3-c664-4d57-8c08-fc072d3f549c |
steps | simpleorchestrationmodeldtzw |
7.4 - Wallaroo ML Workload Orchestration Google BigQuery with House Price Model Tutorial
This can be downloaded as part of the Wallaroo Tutorials repository.
Wallaroo ML Workload Orchestration House Price Model Tutorial
This tutorial provides a quick set of methods and examples regarding Wallaroo Connections and Wallaroo ML Workload Orchestration. For full details, see the Wallaroo Documentation site.
Wallaroo provides Data Connections and ML Workload Orchestrations to provide organizations with a method of creating and managing automated tasks that can either be run on demand or a regular schedule.
Definitions
- Orchestration: A set of instructions written as a python script with a requirements library. Orchestrations are uploaded to the Wallaroo instance as a .zip file.
- Task: An implementation of an orchestration. Tasks are run either once when requested, on a repeating schedule, or as a service.
- Connection: Definitions set by MLOps engineers that are used by other Wallaroo users for connection information to a data source. Usually paired with orchestrations.
This tutorial will focus on using Google BigQuery as the data source.
Tutorial Goals
The tutorial will demonstrate the following:
- Create a Wallaroo connection to retrieving information from a Google BigQuery source table.
- Create a Wallaroo connection to store inference results into a Google BigQuery destination table.
- Upload Wallaroo ML Workload Orchestration that supports BigQuery connections with the connection details.
- Run the orchestration once as a Run Once Task and verify that the inference request succeeded and the inference results were saved to the external data store.
- Schedule the orchestration as a Scheduled Task and verify that the inference request succeeded and the inference results were saved to the external data store.
Prerequisites
- An installed Wallaroo instance.
- The following Python libraries installed. These are included by default in a Wallaroo instance’s JupyterHub service.
- The following Python libraries. These are not included in a Wallaroo instance’s JupyterHub service.
google-cloud-bigquery
: Specifically for its support for Google BigQuery.google-auth
: Used to authenticate for bigquery.db-dtypes
: Converts the BigQuery results to Apache Arrow table or pandas DataFrame.
Tutorial Resources
- Models:
models/rf_model.onnx
: A model that predicts house price values.
- Data:
data/xtest-1.df.json
anddata/xtest-1k.df.json
: DataFrame JSON inference inputs with 1 input and 1,000 inputs.data/xtest-1k.arrow
: Apache Arrow inference inputs with 1 input and 1,000 inputs.- Sample inference inputs in
CSV
that can be imported into Google BigQuery.data/xtest-1k.df.json
: Random sample housing prices.data/smallinputs.df.json
: Sample housing prices that return results lower than $1.5 million.data/biginputs.df.json
: Sample housing prices that return results higher than $1.5 million.
- SQL queries to create the inputs/outputs tables with schema.
./resources/create_inputs_table.sql
: Inputs table with schema../resources/create_outputs_table.sql
: Outputs table with schema../resources/housrpricesga_inputs.avro
: Avro container of inputs table.
Initial Steps
For this tutorial, we’ll create a workspace, upload our sample model and deploy a pipeline. We’ll perform some quick sample inferences to verify that everything it working.
Load Libraries
Here we’ll import the various libraries we’ll use for the tutorial.
import wallaroo
from wallaroo.object import EntityNotFoundError, RequiredAttributeMissing
# to display dataframe tables
from IPython.display import display
# used to display dataframe information without truncating
import pandas as pd
pd.set_option('display.max_colwidth', None)
import pyarrow as pa
import time
import json
# for Big Query connections
from google.cloud import bigquery
from google.oauth2 import service_account
import db_dtypes
# used for unique connection names
import string
import random
suffix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
wallaroo.__version__
'2023.2.0+dfca0605e'
Connect to the Wallaroo Instance
The first step is to connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. For more information on Wallaroo Client settings, see the Client Connection guide.
# Login through local Wallaroo instance
wl = wallaroo.Client()
Variable Declaration
The following variables will be used for our big query testing.
We’ll use two connections:
- bigquery_input_connection: The connection that will draw inference input data from a BigQuery table.
- bigquery_output_connection: The connection that will upload inference results into a BigQuery table.
Not that for the connection arguments, we’ll retrieve the information from the files ./bigquery_service_account_input_key.json
and ./bigquery_service_account_output_key.json
that include the service account key file(SAK) information, as well as the dataset and table used.
Field | Included in SAK |
---|---|
type | √ |
project_id | √ |
private_key_id | √ |
private_key | √ |
client_email | √ |
auth_uri | √ |
token_uri | √ |
auth_provider_x509_cert_url | √ |
client_x509_cert_url | √ |
database | 🚫 |
table | 🚫 |
# Setting variables for later steps
workspace_name = f'bigqueryworkspace{suffix}'
pipeline_name = f'bigquerypipeline{suffix}'
model_name = f'bigquerymodel{suffix}'
model_file_name = './models/rf_model.onnx'
bigquery_connection_input_name = 'bigqueryhouseinputs{suffix}'
bigquery_connection_input_type = "BIGQUERY"
bigquery_connection_input_argument = json.load(open("./bigquery_service_account_input_key.json"))
bigquery_connection_output_name = 'bigqueryhouseoutputs{suffix}'
bigquery_connection_output_type = "BIGQUERY"
bigquery_connection_output_argument = json.load(open("./bigquery_service_account_output_key.json"))
Helper Methods
The following helper methods are used to either create or get workspaces, pipelines, and connections.
# helper methods to retrieve workspaces and pipelines
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
pipeline = wl.build_pipeline(name)
return pipeline
Create the Workspace and Pipeline
We’ll now create our workspace and pipeline for the tutorial. If this tutorial has been run previously, then this will retrieve the existing ones with the assumption they’re for us with this tutorial.
We’ll set the retrieved workspace as the current workspace in the SDK, so all commands will default to that workspace.
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
pipeline = get_pipeline(pipeline_name)
Upload the Model and Deploy Pipeline
We’ll upload our model into our sample workspace, then add it as a pipeline step before deploying the pipeline to it’s ready to accept inference requests.
# Upload the model
housing_model_control = wl.upload_model(model_name, model_file_name, framework=wallaroo.framework.Framework.ONNX).configure()
# Add the model as a pipeline step
pipeline.add_model_step(housing_model_control)
name | bigquerypipelinechbp |
---|---|
created | 2023-05-23 15:17:30.123708+00:00 |
last_updated | 2023-05-23 15:17:30.123708+00:00 |
deployed | (none) |
tags | |
versions | fefa4278-04e0-4d1a-8d5b-9cc9c5275832 |
steps |
#deploy the pipeline to set the pipeline steps
pipeline.deploy()
Waiting for deployment - this will take up to 45s ............ ok
name | bigquerypipelinechbp |
---|---|
created | 2023-05-23 15:17:30.123708+00:00 |
last_updated | 2023-05-23 15:17:30.428928+00:00 |
deployed | True |
tags | |
versions | 3dd0653a-12b0-4298-b91b-1e0b712716c5, fefa4278-04e0-4d1a-8d5b-9cc9c5275832 |
steps | bigquerymodelchbp |
Create Connections
We will create the data source connection via the Wallaroo client command create_connection
.
Connections are created with the Wallaroo client command create_connection
with the following parameters.
Parameter | Type | Description |
---|---|---|
name | string (Required) | The name of the connection. This must be unique - if submitting the name of an existing connection it will return an error. |
type | string (Required) | The user defined type of connection. |
details | Dict (Required) | User defined configuration details for the data connection. These can be {'username':'dataperson', 'password':'datapassword', 'port': 3339} , or {'token':'abcde123==', 'host':'example.com', 'port:1234'} , or other user defined combinations. |
- IMPORTANT NOTE: Data connections names must be unique. Attempting to create a data connection with the same
name
as an existing data connection will result in an error.
connection_input = wl.create_connection(bigquery_connection_input_name, bigquery_connection_input_type, bigquery_connection_input_argument)
connection_output = wl.create_connection(bigquery_connection_output_name, bigquery_connection_output_type, bigquery_connection_output_argument)
wl.list_connections()
name | connection type | details | created at | linked workspaces |
---|---|---|---|---|
bigqueryhouseinputs | BIGQUERY | ***** | 2023-05-23T14:35:26.896064+00:00 | ['bigqueryworkspace'] |
bigqueryhouseoutputs | BIGQUERY | ***** | 2023-05-23T14:35:26.932685+00:00 | ['bigqueryworkspace'] |
bigqueryhouseinputs-jcw | BIGQUERY | ***** | 2023-05-23T14:37:22.103147+00:00 | ['bigqueryworkspace-jcw'] |
bigqueryhouseoutputs-jcw | BIGQUERY | ***** | 2023-05-23T14:37:22.141179+00:00 | ['bigqueryworkspace-jcw'] |
bigqueryhouseinputs{suffix} | BIGQUERY | ***** | 2023-05-23T15:05:29.850628+00:00 | ['bigqueryworkspacekbcy'] |
bigqueryhouseoutputs{suffix} | BIGQUERY | ***** | 2023-05-23T15:05:30.298941+00:00 | ['bigqueryworkspacekbcy'] |
bigqueryforecastinputsrklr | BIGQUERY | ***** | 2023-05-23T15:05:58.206726+00:00 | ['bigquerystatsmodelworkspacerklr'] |
bigqueryforecastoutputsrklr | BIGQUERY | ***** | 2023-05-23T15:05:58.673528+00:00 | ['bigquerystatsmodelworkspacerklr'] |
Get Connection by Name
The Wallaroo client method get_connection(name)
retrieves the connection that matches the name
parameter. We’ll retrieve our connection and store it as inference_source_connection
.
big_query_input_connection = wl.get_connection(name=bigquery_connection_input_name)
big_query_output_connection = wl.get_connection(name=bigquery_connection_output_name)
display(big_query_input_connection)
display(big_query_output_connection)
Field | Value |
---|---|
Name | bigqueryhouseinputs{suffix} |
Connection Type | BIGQUERY |
Details | ***** |
Created At | 2023-05-23T15:05:29.850628+00:00 |
Linked Workspaces | ['bigqueryworkspacekbcy'] |
Field | Value |
---|---|
Name | bigqueryhouseoutputs{suffix} |
Connection Type | BIGQUERY |
Details | ***** |
Created At | 2023-05-23T15:05:30.298941+00:00 |
Linked Workspaces | ['bigqueryworkspacekbcy'] |
Add Connection to Workspace
The method Workspace add_connection(connection_name)
adds a Data Connection to a workspace, and takes the following parameters.
Parameter | Type | Description |
---|---|---|
name | string (Required) | The name of the Data Connection |
We’ll add both connections to our sample workspace, then list the connections available to the workspace to confirm.
workspace.add_connection(bigquery_connection_input_name)
workspace.add_connection(bigquery_connection_output_name)
workspace.list_connections()
name | connection type | details | created at | linked workspaces |
---|---|---|---|---|
bigqueryhouseinputs{suffix} | BIGQUERY | ***** | 2023-05-23T15:05:29.850628+00:00 | ['bigqueryworkspacekbcy', 'bigqueryworkspacechbp'] |
bigqueryhouseoutputs{suffix} | BIGQUERY | ***** | 2023-05-23T15:05:30.298941+00:00 | ['bigqueryworkspacekbcy', 'bigqueryworkspacechbp'] |
Big Query Connection Inference Example
We can test the BigQuery connection with a simple inference to our deployed pipeline. We’ll request the data, format the table into a pandas DataFrame, then submit it for an inference request.
Create Google Credentials
From our BigQuery request, we’ll create the credentials for our BigQuery connection.
bigquery_input_credentials = service_account.Credentials.from_service_account_info(
big_query_input_connection.details())
bigquery_output_credentials = service_account.Credentials.from_service_account_info(
big_query_output_connection.details())
Connect to Google BigQuery
We can now generate a client from our connection details, specifying the project that was included in the big_query_connection
details.
bigqueryinputclient = bigquery.Client(
credentials=bigquery_input_credentials,
project=big_query_input_connection.details()['project_id']
)
bigqueryoutputclient = bigquery.Client(
credentials=bigquery_output_credentials,
project=big_query_output_connection.details()['project_id']
)
Query Data
Now we’ll create our query and retrieve information from out dataset and table as defined in the file bigquery_service_account_key.json
. The table is expected to be in the format of the file ./data/xtest-1k.df.json
.
inference_dataframe_input = bigqueryinputclient.query(
f"""
SELECT tensor
FROM {big_query_input_connection.details()['dataset']}.{big_query_input_connection.details()['table']}"""
).to_dataframe()
inference_dataframe_input.head(5)
tensor | |
---|---|
0 | [4.0, 2.5, 2900.0, 5505.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2900.0, 0.0, 47.6063, -122.02, 2970.0, 5251.0, 12.0, 0.0, 0.0] |
1 | [2.0, 2.5, 2170.0, 6361.0, 1.0, 0.0, 2.0, 3.0, 8.0, 2170.0, 0.0, 47.7109, -122.017, 2310.0, 7419.0, 6.0, 0.0, 0.0] |
2 | [3.0, 2.5, 1300.0, 812.0, 2.0, 0.0, 0.0, 3.0, 8.0, 880.0, 420.0, 47.5893, -122.317, 1300.0, 824.0, 6.0, 0.0, 0.0] |
3 | [4.0, 2.5, 2500.0, 8540.0, 2.0, 0.0, 0.0, 3.0, 9.0, 2500.0, 0.0, 47.5759, -121.994, 2560.0, 8475.0, 24.0, 0.0, 0.0] |
4 | [3.0, 1.75, 2200.0, 11520.0, 1.0, 0.0, 0.0, 4.0, 7.0, 2200.0, 0.0, 47.7659, -122.341, 1690.0, 8038.0, 62.0, 0.0, 0.0] |
Sample Inference
With our data retrieved, we’ll perform an inference and display the results.
result = pipeline.infer(inference_dataframe_input)
display(result.head(5))
time | in.tensor | out.variable | check_failures | |
---|---|---|---|---|
0 | 2023-05-23 15:17:47.498 | [4.0, 2.5, 2900.0, 5505.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2900.0, 0.0, 47.6063, -122.02, 2970.0, 5251.0, 12.0, 0.0, 0.0] | [718013.75] | 0 |
1 | 2023-05-23 15:17:47.498 | [2.0, 2.5, 2170.0, 6361.0, 1.0, 0.0, 2.0, 3.0, 8.0, 2170.0, 0.0, 47.7109, -122.017, 2310.0, 7419.0, 6.0, 0.0, 0.0] | [615094.56] | 0 |
2 | 2023-05-23 15:17:47.498 | [3.0, 2.5, 1300.0, 812.0, 2.0, 0.0, 0.0, 3.0, 8.0, 880.0, 420.0, 47.5893, -122.317, 1300.0, 824.0, 6.0, 0.0, 0.0] | [448627.72] | 0 |
3 | 2023-05-23 15:17:47.498 | [4.0, 2.5, 2500.0, 8540.0, 2.0, 0.0, 0.0, 3.0, 9.0, 2500.0, 0.0, 47.5759, -121.994, 2560.0, 8475.0, 24.0, 0.0, 0.0] | [758714.2] | 0 |
4 | 2023-05-23 15:17:47.498 | [3.0, 1.75, 2200.0, 11520.0, 1.0, 0.0, 0.0, 4.0, 7.0, 2200.0, 0.0, 47.7659, -122.341, 1690.0, 8038.0, 62.0, 0.0, 0.0] | [513264.7] | 0 |
Upload the Results
With the query complete, we’ll upload the results back to the BigQuery dataset.
output_table = bigqueryoutputclient.get_table(f"{big_query_output_connection.details()['dataset']}.{big_query_output_connection.details()['table']}")
bigqueryoutputclient.insert_rows_from_dataframe(
output_table,
dataframe=result.rename(columns={"in.tensor":"in_tensor", "out.variable":"out_variable"})
)
[[], []]
Wallaroo ML Workload Orchestration Example
With the pipeline deployed and our connections set, we will now generate our ML Workload Orchestration. See the Wallaroo ML Workload Orchestrations guide for full details.
Orchestrations are uploaded to the Wallaroo instance as a ZIP file with the following requirements:
Parameter | Type | Description |
---|---|---|
User Code | (Required) Python script as .py files | If main.py exists, then that will be used as the task entrypoint. Otherwise, the first main.py found in any subdirectory will be used as the entrypoint. |
Python Library Requirements | (Optional) requirements.txt file in the requirements file format. A standard Python requirements.txt for any dependencies to be provided in the task environment. The Wallaroo SDK will already be present and should not be included in the requirements.txt. Multiple requirements.txt files are not allowed. | |
Other artifacts | Â | Other artifacts such as files, data, or code to support the orchestration. |
For our example, our orchestration will:
- Use the
bigquery_remote_inference
to open a connection to the input and output tables. - Deploy the pipeline.
- Perform an inference with the input data.
- Save the inference results to the output table.
- Undeploy the pipeline.
This sample script is stored in bigquery_remote_inference/main.py
with an requirements.txt
file having the specific libraries for the Google BigQuery connection., and packaged into the orchestration as ./bigquery_remote_inference/bigquery_remote_inference.zip
. We’ll display the steps in uploading the orchestration to the Wallaroo instance.
Note that the orchestration assumes the pipeline is already deployed.
Upload the Orchestration
Orchestrations are uploaded with the Wallaroo client upload_orchestration(path)
method with the following parameters.
Parameter | Type | Description |
---|---|---|
path | string (Required) | The path to the ZIP file to be uploaded. |
Once uploaded, the deployment will be prepared and any requirements will be downloaded and installed.
For this example, the orchestration ./bigquery_remote_inference/bigquery_remote_inference.zip
will be uploaded and saved to the variable orchestration
. Then we will loop until the uploaded orchestration’s status
displays ready
.
orchestration = wl.upload_orchestration(path="./bigquery_remote_inference/bigquery_remote_inference.zip")
while orchestration.status() != 'ready':
print(orchestration.status())
time.sleep(5)
pending_packaging
pending_packaging
packaging
packaging
packaging
packaging
packaging
packaging
packaging
packaging
packaging
wl.list_orchestrations()
id | name | status | filename | sha | created at | updated at |
---|---|---|---|---|---|---|
78ea6222-c102-4eb1-9705-166233a12257 | None | ready | bigquery_remote_inference.zip | 582f33...a6957c | 2023-23-May 15:17:48 | 2023-23-May 15:18:39 |
Task Management Tutorial
Once an Orchestration has the status ready
, it can be run as a task. Tasks have three run options.
Type | SDK Call | How triggered |
---|---|---|
Once | orchestration.run_once(name, json_args, timeout) | Task runs once and exits. |
Scheduled | orchestration.run_scheduled(name, schedule, timeout, json_args) | User provides schedule. Task runs exits whenever schedule dictates. |
Run Task Once
We’ll do both a Run Once task and generate our Run Once Task from our orchestration.
Tasks are generated and run once with the Orchestration run_once(name, json_args, timeout)
method. Any arguments for the orchestration are passed in as a Dict
. If there are no arguments, then an empty set {}
is passed.
We’ll display the last 5 rows of our BigQuery output table, then start the task that will perform the same inference we did above.
task_inference_results = bigqueryoutputclient.query(
f"""
SELECT *
FROM {big_query_output_connection.details()['dataset']}.{big_query_output_connection.details()['table']}
ORDER BY time DESC
LIMIT 5
"""
).to_dataframe()
display(task_inference_results)
time | in_tensor | out_variable | check_failures | |
---|---|---|---|---|
0 | 2023-05-23 15:17:47.498 | [4.0, 2.5, 2500.0, 8540.0, 2.0, 0.0, 0.0, 3.0, 9.0, 2500.0, 0.0, 47.5759, -121.994, 2560.0, 8475.0, 24.0, 0.0, 0.0] | [758714.2] | 0 |
1 | 2023-05-23 15:17:47.498 | [4.0, 2.5, 2900.0, 5505.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2900.0, 0.0, 47.6063, -122.02, 2970.0, 5251.0, 12.0, 0.0, 0.0] | [718013.75] | 0 |
2 | 2023-05-23 15:17:47.498 | [2.0, 2.5, 2170.0, 6361.0, 1.0, 0.0, 2.0, 3.0, 8.0, 2170.0, 0.0, 47.7109, -122.017, 2310.0, 7419.0, 6.0, 0.0, 0.0] | [615094.56] | 0 |
3 | 2023-05-23 15:17:47.498 | [3.0, 1.75, 2200.0, 11520.0, 1.0, 0.0, 0.0, 4.0, 7.0, 2200.0, 0.0, 47.7659, -122.341, 1690.0, 8038.0, 62.0, 0.0, 0.0] | [513264.7] | 0 |
4 | 2023-05-23 15:17:47.498 | [3.0, 2.5, 1300.0, 812.0, 2.0, 0.0, 0.0, 3.0, 8.0, 880.0, 420.0, 47.5893, -122.317, 1300.0, 824.0, 6.0, 0.0, 0.0] | [448627.72] | 0 |
# Example: run once
import datetime
task_start = datetime.datetime.now()
task = orchestration.run_once(name="big query single run", json_args={})
task
Field | Value |
---|---|
ID | 59b20af0-4f2e-4371-9175-e0b69a94fb91 |
Name | big query single run |
Last Run Status | unknown |
Type | Temporary Run |
Active | True |
Schedule | - |
Created At | 2023-23-May 15:18:46 |
Updated At | 2023-23-May 15:18:46 |
Task Status
The list of tasks in the Wallaroo instance is retrieves through the Wallaroo Client list_tasks()
method. This returns an array list of the following.
Parameter | Type | Description |
---|---|---|
id | string | The UUID identifier for the task. |
last run status | string | The last reported status the task. Values are:
|
type | string | The type of the task. Values are:
|
schedule | string | The schedule for the task. If a run once task, the schedule will be - . |
created at | DateTime | The date and time the task was started. |
updated at | DateTime | The date and time the task was updated. |
For this example, the status of the previously created task will be generated, then looped until it has reached status started
.
while task.status() != "started":
display(task.status())
time.sleep(5)
'pending'
‘pending’
wl.list_tasks()
id | name | last run status | type | active | schedule | created at | updated at |
---|---|---|---|---|---|---|---|
59b20af0-4f2e-4371-9175-e0b69a94fb91 | big query single run | failure | Temporary Run | True | - | 2023-23-May 15:18:46 | 2023-23-May 15:18:51 |
Task Results
We can view the inferences from our logs and verify that new entries were added from our task. We’ll query the last 5 rows of our inference output table after a wait of 60 seconds.
time.sleep(60)
task_inference_results = bigqueryoutputclient.query(
f"""
SELECT *
FROM {big_query_output_connection.details()['dataset']}.{big_query_output_connection.details()['table']}
ORDER BY time DESC LIMIT 5"""
).to_dataframe()
display(task_inference_results)
time | in_tensor | out_variable | check_failures | |
---|---|---|---|---|
0 | 2023-05-23 15:17:47.498 | [4.0, 2.5, 2500.0, 8540.0, 2.0, 0.0, 0.0, 3.0, 9.0, 2500.0, 0.0, 47.5759, -121.994, 2560.0, 8475.0, 24.0, 0.0, 0.0] | [758714.2] | 0 |
1 | 2023-05-23 15:17:47.498 | [4.0, 2.5, 2900.0, 5505.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2900.0, 0.0, 47.6063, -122.02, 2970.0, 5251.0, 12.0, 0.0, 0.0] | [718013.75] | 0 |
2 | 2023-05-23 15:17:47.498 | [2.0, 2.5, 2170.0, 6361.0, 1.0, 0.0, 2.0, 3.0, 8.0, 2170.0, 0.0, 47.7109, -122.017, 2310.0, 7419.0, 6.0, 0.0, 0.0] | [615094.56] | 0 |
3 | 2023-05-23 15:17:47.498 | [3.0, 1.75, 2200.0, 11520.0, 1.0, 0.0, 0.0, 4.0, 7.0, 2200.0, 0.0, 47.7659, -122.341, 1690.0, 8038.0, 62.0, 0.0, 0.0] | [513264.7] | 0 |
4 | 2023-05-23 15:17:47.498 | [3.0, 2.5, 1300.0, 812.0, 2.0, 0.0, 0.0, 3.0, 8.0, 880.0, 420.0, 47.5893, -122.317, 1300.0, 824.0, 6.0, 0.0, 0.0] | [448627.72] | 0 |
Scheduled Run Task Example
The other method of using tasks is as a scheduled run through the Orchestration run_scheduled(name, schedule, timeout, json_args)
. This sets up a task to run on an regular schedule as defined by the schedule
parameter in the cron
service format. For example:
schedule={'42 * * * *'}
Runs on the 42nd minute of every hour.
The following schedule runs every day at 12 noon from February 1 to February 15 2024 - and then ends.
schedule={'0 0 12 1-15 2 2024'}
For our example, we will create a scheduled task to run every 5 minutes, display the inference results, then use the Orchestration kill
task to keep the task from running any further.
It is recommended that orchestrations that have pipeline deploy or undeploy commands be spaced out no less than 5 minutes to prevent colliding with other tasks that use the same pipeline.
task_inference_results = bigqueryoutputclient.query(
f"""
SELECT *
FROM {big_query_output_connection.details()['dataset']}.{big_query_output_connection.details()['table']}
ORDER BY time DESC LIMIT 5"""
).to_dataframe()
display(task_inference_results.tail(5))
scheduled_task = orchestration.run_scheduled(name="simple_inference_schedule", schedule="*/5 * * * *", timeout=120, json_args={})
time | in_tensor | out_variable | check_failures | |
---|---|---|---|---|
0 | 2023-05-23 15:17:47.498 | [4.0, 2.5, 2500.0, 8540.0, 2.0, 0.0, 0.0, 3.0, 9.0, 2500.0, 0.0, 47.5759, -121.994, 2560.0, 8475.0, 24.0, 0.0, 0.0] | [758714.2] | 0 |
1 | 2023-05-23 15:17:47.498 | [4.0, 2.5, 2900.0, 5505.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2900.0, 0.0, 47.6063, -122.02, 2970.0, 5251.0, 12.0, 0.0, 0.0] | [718013.75] | 0 |
2 | 2023-05-23 15:17:47.498 | [2.0, 2.5, 2170.0, 6361.0, 1.0, 0.0, 2.0, 3.0, 8.0, 2170.0, 0.0, 47.7109, -122.017, 2310.0, 7419.0, 6.0, 0.0, 0.0] | [615094.56] | 0 |
3 | 2023-05-23 15:17:47.498 | [3.0, 1.75, 2200.0, 11520.0, 1.0, 0.0, 0.0, 4.0, 7.0, 2200.0, 0.0, 47.7659, -122.341, 1690.0, 8038.0, 62.0, 0.0, 0.0] | [513264.7] | 0 |
4 | 2023-05-23 15:17:47.498 | [3.0, 2.5, 1300.0, 812.0, 2.0, 0.0, 0.0, 3.0, 8.0, 880.0, 420.0, 47.5893, -122.317, 1300.0, 824.0, 6.0, 0.0, 0.0] | [448627.72] | 0 |
while scheduled_task.status() != "started":
display(scheduled_task.status())
time.sleep(5)
'pending'
#wait 420 seconds to give the scheduled event time to finish
time.sleep(420)
task_inference_results = bigqueryoutputclient.query(
f"""
SELECT *
FROM {big_query_output_connection.details()['dataset']}.{big_query_output_connection.details()['table']}
ORDER BY time DESC LIMIT 5"""
).to_dataframe()
display(task_inference_results.tail(5))
time | in_tensor | out_variable | check_failures | |
---|---|---|---|---|
0 | 2023-05-23 15:17:47.498 | [4.0, 2.5, 2500.0, 8540.0, 2.0, 0.0, 0.0, 3.0, 9.0, 2500.0, 0.0, 47.5759, -121.994, 2560.0, 8475.0, 24.0, 0.0, 0.0] | [758714.2] | 0 |
1 | 2023-05-23 15:17:47.498 | [4.0, 2.5, 2900.0, 5505.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2900.0, 0.0, 47.6063, -122.02, 2970.0, 5251.0, 12.0, 0.0, 0.0] | [718013.75] | 0 |
2 | 2023-05-23 15:17:47.498 | [2.0, 2.5, 2170.0, 6361.0, 1.0, 0.0, 2.0, 3.0, 8.0, 2170.0, 0.0, 47.7109, -122.017, 2310.0, 7419.0, 6.0, 0.0, 0.0] | [615094.56] | 0 |
3 | 2023-05-23 15:17:47.498 | [3.0, 1.75, 2200.0, 11520.0, 1.0, 0.0, 0.0, 4.0, 7.0, 2200.0, 0.0, 47.7659, -122.341, 1690.0, 8038.0, 62.0, 0.0, 0.0] | [513264.7] | 0 |
4 | 2023-05-23 15:17:47.498 | [3.0, 2.5, 1300.0, 812.0, 2.0, 0.0, 0.0, 3.0, 8.0, 880.0, 420.0, 47.5893, -122.317, 1300.0, 824.0, 6.0, 0.0, 0.0] | [448627.72] | 0 |
Kill Task
With our testing complete, we will kill the scheduled task so it will not run again. First we’ll show all the tasks to verify that our task is there, then issue it the kill command.
scheduled_task.kill()
<ArbexStatus.PENDING_KILL: 'pending_kill'>
Cleanup
With the tutorial complete, we can undeploy the pipeline and return the resources back to the Wallaroo instance.
pipeline.undeploy()
Waiting for undeployment - this will take up to 45s ..................................... ok
name | bigquerypipelinechbp |
---|---|
created | 2023-05-23 15:17:30.123708+00:00 |
last_updated | 2023-05-23 15:17:30.428928+00:00 |
deployed | False |
tags | |
versions | 3dd0653a-12b0-4298-b91b-1e0b712716c5, fefa4278-04e0-4d1a-8d5b-9cc9c5275832 |
steps | bigquerymodelchbp |
7.5 - Wallaroo ML Workload Orchestration Google BigQuery with Statsmodel Forecast Tutorial
This can be downloaded as part of the Wallaroo Tutorials repository.
Wallaroo ML Workload Orchestrations and BigQuery Connections with Statsmodel Tutorial
This tutorial provides a quick set of methods and examples regarding Wallaroo Connections and Wallaroo ML Workload Orchestration. For full details, see the Wallaroo Documentation site.
Wallaroo provides Data Connections and ML Workload Orchestrations to provide organizations with a method of creating and managing automated tasks that can either be run on demand or a regular schedule.
Definitions
- Orchestration: A set of instructions written as a python script with a requirements library. Orchestrations are uploaded to the Wallaroo instance as a .zip file.
- Task: An implementation of an orchestration. Tasks are run either once when requested, on a repeating schedule, or as a service.
- Connection: Definitions set by MLOps engineers that are used by other Wallaroo users for connection information to a data source. Usually paired with orchestrations.
This tutorial will focus on using Google BigQuery as the data source for supplying the inference data to perform inferences through a Statsmodel ML model.
Tutorial Goals
The tutorial will demonstrate the following:
- Create a Wallaroo connection to retrieving information from a Google BigQuery source table.
- Create a Wallaroo connection to store inference results into a Google BigQuery destination table.
- Upload Wallaroo ML Workload Orchestration that supports BigQuery connections with the connection details.
- Run the orchestration once as a Run Once Task and verify that the inference request succeeded and the inference results were saved to the external data store.
- Schedule the orchestration as a Scheduled Task and verify that the inference request succeeded and the inference results were saved to the external data store.
Prerequisites
- An installed Wallaroo instance.
- The following Python libraries installed. These are included by default in a Wallaroo instance’s JupyterHub service.
- The following Python libraries. These are not included in a Wallaroo instance’s JupyterHub service.
google-cloud-bigquery
: Specifically for its support for Google BigQuery.google-auth
: Used to authenticate for bigquery.db-dtypes
: Converts the BigQuery results to Apache Arrow table or pandas DataFrame.
Tutorial Resources
- Models:
models/bike_day_model.pkl
: The statsmodel model predicts how many bikes will be rented on each of the next 7 days, based on the previous 7 days’ bike rentals, temperature, and wind speed. This model only accepts shapes (7,4) - 7 rows (representing the last 7 days) and 4 columns (representing the fieldstemp
,holiday
,workingday
,windspeed
). Additional files to support this example are:infer.py
: The inference script that is part of thestatsmodel
.
- Data:
data/day.csv
: Data used to train the samplestatsmodel
example.data/bike_day_eval.json
: Data used for inferences. This will be transferred to a BigQuery table as shown in the demonstration.
- Resources:
resources/bigquery_service_account_input_key.json
: Example service key to authenticate to a Google BigQuery project with the dataset and table used for the inference data.resources/bigquery_service_account_output_key.json
: Example service key to authenticate to a Google BigQuery project with the dataset and table used for the inference result data.resources/statsmodel_forecast_inputs.sql
: SQL script to create the inputs table schema, populated with the values from./data/day.csv
.resources/statsmodel_forecast_outputs.sql
: SQL script to create the outputs table schema.
Initial Steps
For this tutorial, we’ll create a workspace, upload our sample model and deploy a pipeline. We’ll perform some quick sample inferences to verify that everything it working.
Load Libraries
Here we’ll import the various libraries we’ll use for the tutorial.
import wallaroo
from wallaroo.object import EntityNotFoundError, RequiredAttributeMissing
# to display dataframe tables
from IPython.display import display
# used to display dataframe information without truncating
import pandas as pd
pd.set_option('display.max_colwidth', None)
import pyarrow as pa
import time
import json
# for Big Query connections
from google.cloud import bigquery
from google.oauth2 import service_account
import db_dtypes
import string
import random
suffix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
wallaroo.__version__
'2023.2.0+dfca0605e'
Connect to the Wallaroo Instance
The first step is to connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. For more information on Wallaroo Client settings, see the Client Connection guide.
# Login through local Wallaroo instance
wl = wallaroo.Client()
Variable Declaration
The following variables will be used for our big query testing.
We’ll use two connections:
- bigquery_input_connection: The connection that will draw inference input data from a BigQuery table.
- bigquery_output_connection: The connection that will upload inference results into a BigQuery table.
Not that for the connection arguments, we’ll retrieve the information from the files ./bigquery_service_account_input_key.json
and ./bigquery_service_account_output_key.json
that include the service account key file(SAK) information, as well as the dataset and table used.
Field | Included in SAK |
---|---|
type | √ |
project_id | √ |
private_key_id | √ |
private_key | √ |
client_email | √ |
auth_uri | √ |
token_uri | √ |
auth_provider_x509_cert_url | √ |
client_x509_cert_url | √ |
database | 🚫 |
table | 🚫 |
# Setting variables for later steps
workspace_name = f'bigquerystatsmodelworkspace{suffix}'
pipeline_name = f'bigquerystatsmodelpipeline{suffix}'
model_name = f'bigquerystatsmodelmodel{suffix}'
model_file_name = './models/bike_day_model.pkl'
bigquery_connection_input_name = f'bigqueryforecastinputs{suffix}'
bigquery_connection_input_type = "BIGQUERY"
bigquery_connection_input_argument = json.load(open('./resources/bigquery_service_account_input_key.json'))
bigquery_connection_output_name = f'bigqueryforecastoutputs{suffix}'
bigquery_connection_output_type = "BIGQUERY"
bigquery_connection_output_argument = json.load(open('./resources/bigquery_service_account_output_key.json'))
Helper Methods
The following helper methods are used to either create or get workspaces, pipelines, and connections.
# helper methods to retrieve workspaces and pipelines
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
pipeline = wl.build_pipeline(name)
return pipeline
Create the Workspace and Pipeline
We’ll now create our workspace and pipeline for the tutorial. If this tutorial has been run previously, then this will retrieve the existing ones with the assumption they’re for us with this tutorial.
We’ll set the retrieved workspace as the current workspace in the SDK, so all commands will default to that workspace.
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
pipeline = get_pipeline(pipeline_name)
Upload the Model and Deploy Pipeline
We’ll upload our model into our sample workspace, then add it as a pipeline step before deploying the pipeline to it’s ready to accept inference requests.
# Upload the model
bike_day_model = wl.upload_model(model_name, model_file_name, framework=wallaroo.framework.Framework.ONNX).configure(runtime="python")
# Add the model as a pipeline step
pipeline.add_model_step(bike_day_model)
name | bigquerystatsmodelpipelinegztp |
---|---|
created | 2023-05-23 15:19:43.097185+00:00 |
last_updated | 2023-05-23 15:19:43.097185+00:00 |
deployed | (none) |
tags | |
versions | fa33af33-3cf3-43c9-8e2a-4f0b549d84bf |
steps |
#deploy the pipeline
pipeline.deploy()
Waiting for deployment - this will take up to 45s ............. ok
name | bigquerystatsmodelpipelinegztp |
---|---|
created | 2023-05-23 15:19:43.097185+00:00 |
last_updated | 2023-05-23 15:19:43.390612+00:00 |
deployed | True |
tags | |
versions | ee07358d-b6d5-46f3-a5bc-f98baae7ddca, fa33af33-3cf3-43c9-8e2a-4f0b549d84bf |
steps | bigquerystatsmodelmodelgztp |
Sample Inferences
We’ll perform some quick sample inferences with the local file data to verity the pipeline deployed and is ready for inferences. Once done, we’ll undeploy the pipeline.
## perform inferences
results = pipeline.infer_from_file('./data/bike_day_eval.json', data_format="custom-json")
print(results)
[{'forecast': [1882.3784555157672, 2130.607915701861, 2340.84005381799, 2895.754978552066, 2163.657515565616, 1509.1792126509536, 2431.1838923957016]}]
Create Connections
We will create the data source connection via the Wallaroo client command create_connection
.
Connections are created with the Wallaroo client command create_connection
with the following parameters.
Parameter | Type | Description |
---|---|---|
name | string (Required) | The name of the connection. This must be unique - if submitting the name of an existing connection it will return an error. |
type | string (Required) | The user defined type of connection. |
details | Dict (Required) | User defined configuration details for the data connection. These can be {'username':'dataperson', 'password':'datapassword', 'port': 3339} , or {'token':'abcde123==', 'host':'example.com', 'port:1234'} , or other user defined combinations. |
- IMPORTANT NOTE: Data connections names must be unique. Attempting to create a data connection with the same
name
as an existing data connection will result in an error.
See the statsmodel_forecast_inputs
and statsmodel_forecast_outputs
details listed above for the table schema used for our example.
connection_input = wl.create_connection(bigquery_connection_input_name, bigquery_connection_input_type, bigquery_connection_input_argument)
connection_output = wl.create_connection(bigquery_connection_output_name, bigquery_connection_output_type, bigquery_connection_output_argument)
display(connection_input)
display(connection_output)
Field | Value |
---|---|
Name | bigqueryforecastinputsgztp |
Connection Type | BIGQUERY |
Details | ***** |
Created At | 2023-05-23T15:19:58.159691+00:00 |
Linked Workspaces | [] |
Field | Value |
---|---|
Name | bigqueryforecastoutputsgztp |
Connection Type | BIGQUERY |
Details | ***** |
Created At | 2023-05-23T15:19:58.195628+00:00 |
Linked Workspaces | [] |
Get Connection by Name
The Wallaroo client method get_connection(name)
retrieves the connection that matches the name
parameter. We’ll retrieve our connection and store it as inference_source_connection
.
big_query_input_connection = wl.get_connection(name=bigquery_connection_input_name)
big_query_output_connection = wl.get_connection(name=bigquery_connection_output_name)
display(big_query_input_connection)
display(big_query_output_connection)
Field | Value |
---|---|
Name | bigqueryforecastinputsgztp |
Connection Type | BIGQUERY |
Details | ***** |
Created At | 2023-05-23T15:19:58.159691+00:00 |
Linked Workspaces | [] |
Field | Value |
---|---|
Name | bigqueryforecastoutputsgztp |
Connection Type | BIGQUERY |
Details | ***** |
Created At | 2023-05-23T15:19:58.195628+00:00 |
Linked Workspaces | [] |
Add Connection to Workspace
The method Workspace add_connection(connection_name)
adds a Data Connection to a workspace, and takes the following parameters.
Parameter | Type | Description |
---|---|---|
name | string (Required) | The name of the Data Connection |
We’ll add both connections to our sample workspace, then list the connections available to the workspace to confirm.
workspace.add_connection(bigquery_connection_input_name)
workspace.add_connection(bigquery_connection_output_name)
workspace.list_connections()
name | connection type | details | created at | linked workspaces |
---|---|---|---|---|
bigqueryforecastinputsgztp | BIGQUERY | ***** | 2023-05-23T15:19:58.159691+00:00 | ['bigquerystatsmodelworkspacegztp'] |
bigqueryforecastoutputsgztp | BIGQUERY | ***** | 2023-05-23T15:19:58.195628+00:00 | ['bigquerystatsmodelworkspacegztp'] |
Big Query Connection Inference Example
We can test the BigQuery connection with a simple inference to our deployed pipeline. We’ll request the data, format the table into a pandas DataFrame, then submit it for an inference request.
Create Google Credentials
From our BigQuery request, we’ll create the credentials for our BigQuery connection.
bigquery_input_credentials = service_account.Credentials.from_service_account_info(
big_query_input_connection.details())
bigquery_output_credentials = service_account.Credentials.from_service_account_info(
big_query_output_connection.details())
Connect to Google BigQuery
We can now generate a client from our connection details, specifying the project that was included in the big_query_connection
details.
bigqueryinputclient = bigquery.Client(
credentials=bigquery_input_credentials,
project=big_query_input_connection.details()['project_id']
)
bigqueryoutputclient = bigquery.Client(
credentials=bigquery_output_credentials,
project=big_query_output_connection.details()['project_id']
)
Query Data
Now we’ll create our query and retrieve information from out dataset and table as defined in the file bigquery_service_account_key.json
.
We’ll grab the last 7 days of data - with every record assumed to be one day - and use that for our inference request.
inference_dataframe_input = bigqueryinputclient.query(
f"""
(select dteday, temp, holiday, workingday, windspeed
FROM {big_query_input_connection.details()['dataset']}.{big_query_input_connection.details()['table']}
ORDER BY dteday DESC LIMIT 7)
ORDER BY dteday
"""
).to_dataframe().drop(columns=['dteday'])
# convert to a dict, show the first 7 rows
display(inference_dataframe_input.to_dict())
{'temp': {0: 0.291304,
1: 0.243333,
2: 0.254167,
3: 0.253333,
4: 0.253333,
5: 0.255833,
6: 0.215833},
'holiday': {0: 1, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0},
'workingday': {0: 0, 1: 1, 2: 1, 3: 1, 4: 0, 5: 0, 6: 1},
'windspeed': {0: 0.168726,
1: 0.316546,
2: 0.350133,
3: 0.155471,
4: 0.124383,
5: 0.350754,
6: 0.154846}}
Sample Inference
With our data retrieved, we’ll perform an inference and display the results.
#deploy the pipeline
pipeline.deploy()
ok
name | bigquerystatsmodelpipelinegztp |
---|---|
created | 2023-05-23 15:19:43.097185+00:00 |
last_updated | 2023-05-23 15:20:00.500498+00:00 |
deployed | True |
tags | |
versions | f0c22a0a-7e6a-4d49-91ba-e93daf575e6b, ee07358d-b6d5-46f3-a5bc-f98baae7ddca, fa33af33-3cf3-43c9-8e2a-4f0b549d84bf |
steps | bigquerystatsmodelmodelgztp |
results = pipeline.infer(inference_dataframe_input.to_dict())
display(results[0]['forecast'])
[1231.2556997246595,
1627.3643469089343,
1674.3769827243134,
1621.9273295873882,
1140.7465817903185,
1211.5223974364667,
1457.1896450382922]
Upload the Results
With the query complete, we’ll upload the results back to the BigQuery dataset.
output_table = bigqueryoutputclient.get_table(f"{big_query_output_connection.details()['dataset']}.{big_query_output_connection.details()['table']}")
job = bigqueryoutputclient.query(
f"""
INSERT {big_query_output_connection.details()['dataset']}.{big_query_output_connection.details()['table']}
VALUES
(current_timestamp(), "{results[0]['forecast']}")
"""
)
# Get the last insert to the output table to verify
# wait 10 seconds for the insert to finish
time.sleep(10)
task_inference_results = bigqueryoutputclient.query(
f"""
SELECT *
FROM {big_query_output_connection.details()['dataset']}.{big_query_output_connection.details()['table']}
ORDER BY date DESC
LIMIT 5
"""
).to_dataframe()
display(task_inference_results)
date | forecast | |
---|---|---|
0 | 2023-05-23 15:20:01.622083+00:00 | [1231.2556997246595, 1627.3643469089343, 1674.3769827243134, 1621.9273295873882, 1140.7465817903185, 1211.5223974364667, 1457.1896450382922] |
1 | 2023-05-23 15:06:06.678170+00:00 | [1231.2556997246595, 1627.3643469089343, 1674.3769827243134, 1621.9273295873882, 1140.7465817903185, 1211.5223974364667, 1457.1896450382922] |
2 | 2023-05-23 14:57:00.200596+00:00 | [1231.2556997246595, 1627.3643469089343, 1674.3769827243134, 1621.9273295873882, 1140.7465817903185, 1211.5223974364667, 1457.1896450382922] |
3 | 2023-05-19 22:39:01.932839+00:00 | [1231.2556997246595, 1627.3643469089343, 1674.3769827243134, 1621.9273295873882, 1140.7465817903185, 1211.5223974364667, 1457.1896450382922] |
4 | 2023-05-19 22:33:27.655703+00:00 | [1231.2556997246595, 1627.3643469089343, 1674.3769827243134, 1621.9273295873882, 1140.7465817903185, 1211.5223974364667, 1457.1896450382922] |
Wallaroo ML Workload Orchestration Example
With the pipeline deployed and our connections set, we will now generate our ML Workload Orchestration. See the Wallaroo ML Workload Orchestrations guide for full details.
Orchestrations are uploaded to the Wallaroo instance as a ZIP file with the following requirements:
Parameter | Type | Description |
---|---|---|
User Code | (Required) Python script as .py files | If main.py exists, then that will be used as the task entrypoint. Otherwise, the first main.py found in any subdirectory will be used as the entrypoint. |
Python Library Requirements | (Optional) requirements.txt file in the requirements file format. A standard Python requirements.txt for any dependencies to be provided in the task environment. The Wallaroo SDK will already be present and should not be included in the requirements.txt. Multiple requirements.txt files are not allowed. | |
Other artifacts | Â | Other artifacts such as files, data, or code to support the orchestration. |
For our example, our orchestration will:
- Use the
bigquery_remote_inference
to open a connection to the input and output tables. - Deploy the pipeline.
- Perform an inference with the input data.
- Save the inference results to the output table.
- Undeploy the pipeline.
This sample script is stored in bigquery_statsmodel_remote_inference/main.py
with an requirements.txt
file having the specific libraries for the Google BigQuery connection., and packaged into the orchestration as ./bigquery_statsmodel_remote_inference/bigquery_statsmodel_remote_inference.zip
. We’ll display the steps in uploading the orchestration to the Wallaroo instance.
Note that the orchestration assumes the pipeline is already deployed.
Upload the Orchestration
Orchestrations are uploaded with the Wallaroo client upload_orchestration(path)
method with the following parameters.
Parameter | Type | Description |
---|---|---|
path | string (Required) | The path to the ZIP file to be uploaded. |
Once uploaded, the deployment will be prepared and any requirements will be downloaded and installed.
For this example, the orchestration ./bigquery_remote_inference/bigquery_remote_inference.zip
will be uploaded and saved to the variable orchestration
. Then we will loop until the orchestration status is ready
.
pipeline.deploy()
ok
name | bigquerystatsmodelpipelinegztp |
---|---|
created | 2023-05-23 15:19:43.097185+00:00 |
last_updated | 2023-05-23 15:20:13.581488+00:00 |
deployed | True |
tags | |
versions | 2af13cbf-2dae-4dc9-85fa-ae06c04b6a54, f0c22a0a-7e6a-4d49-91ba-e93daf575e6b, ee07358d-b6d5-46f3-a5bc-f98baae7ddca, fa33af33-3cf3-43c9-8e2a-4f0b549d84bf |
steps | bigquerystatsmodelmodelgztp |
orchestration = wl.upload_orchestration(path="./bigquery_remote_inference/bigquery_remote_inference.zip")
while orchestration.status() != 'ready':
print(orchestration.status())
time.sleep(5)
pending_packaging
pending_packaging
packaging
packaging
packaging
packaging
packaging
packaging
packaging
packaging
packaging
wl.list_orchestrations()
id | name | status | filename | sha | created at | updated at |
---|---|---|---|---|---|---|
2186543f-f7f6-46b2-8334-63d73ddb0204 | None | ready | bigquery_remote_inference.zip | 66945c...e28259 | 2023-23-May 15:20:13 | 2023-23-May 15:21:06 |
Task Management Tutorial
Once an Orchestration has the status ready
, it can be run as a task. Tasks have three run options.
Type | SDK Call | How triggered |
---|---|---|
Once | orchestration.run_once(name, json_args, timeout) | Task runs once and exits. |
Scheduled | orchestration.run_scheduled(name, schedule, timeout, json_args) | User provides schedule. Task runs exits whenever schedule dictates. |
Run Task Once
We’ll do both a Run Once task and generate our Run Once Task from our orchestration.
Tasks are generated and run once with the Orchestration run_once(name, json_args, timeout)
method. Any arguments for the orchestration are passed in as a Dict
. If there are no arguments, then an empty set {}
is passed.
We’ll display the last 5 rows of our BigQuery output table, then start the task that will perform the same inference we did above.
# Get the last insert to the output table to verify
task_inference_results = bigqueryoutputclient.query(
f"""
SELECT *
FROM {big_query_output_connection.details()['dataset']}.{big_query_output_connection.details()['table']}
ORDER BY date DESC
LIMIT 5
"""
).to_dataframe()
display(task_inference_results)
date | forecast | |
---|---|---|
0 | 2023-05-23 15:20:01.622083+00:00 | [1231.2556997246595, 1627.3643469089343, 1674.3769827243134, 1621.9273295873882, 1140.7465817903185, 1211.5223974364667, 1457.1896450382922] |
1 | 2023-05-23 15:06:06.678170+00:00 | [1231.2556997246595, 1627.3643469089343, 1674.3769827243134, 1621.9273295873882, 1140.7465817903185, 1211.5223974364667, 1457.1896450382922] |
2 | 2023-05-23 14:57:00.200596+00:00 | [1231.2556997246595, 1627.3643469089343, 1674.3769827243134, 1621.9273295873882, 1140.7465817903185, 1211.5223974364667, 1457.1896450382922] |
3 | 2023-05-19 22:39:01.932839+00:00 | [1231.2556997246595, 1627.3643469089343, 1674.3769827243134, 1621.9273295873882, 1140.7465817903185, 1211.5223974364667, 1457.1896450382922] |
4 | 2023-05-19 22:33:27.655703+00:00 | [1231.2556997246595, 1627.3643469089343, 1674.3769827243134, 1621.9273295873882, 1140.7465817903185, 1211.5223974364667, 1457.1896450382922] |
# Example: run once
task = orchestration.run_once(name="big query statsmodel run once", json_args={})
task
Field | Value |
---|---|
ID | edf924ae-5a17-4596-adbe-b888b7102d33 |
Name | big query statsmodel run once |
Last Run Status | unknown |
Type | Temporary Run |
Active | True |
Schedule | - |
Created At | 2023-23-May 15:21:11 |
Updated At | 2023-23-May 15:21:11 |
Task Status
The list of tasks in the Wallaroo instance is retrieves through the Wallaroo Client list_tasks()
method. This returns an array list of the following.
Parameter | Type | Description |
---|---|---|
id | string | The UUID identifier for the task. |
last run status | string | The last reported status the task. Values are:
|
type | string | The type of the task. Values are:
|
created at | DateTime | The date and time the task was started. |
updated at | DateTime | The date and time the task was updated. |
For this example, the status of the previously created task will be generated, then looped until it has reached status started
.
while task.status() != "started":
display(task.status())
time.sleep(5)
'pending'
‘pending’
wl.list_tasks()
id | name | last run status | type | active | schedule | created at | updated at |
---|---|---|---|---|---|---|---|
edf924ae-5a17-4596-adbe-b888b7102d33 | big query statsmodel run once | failure | Temporary Run | True | - | 2023-23-May 15:21:11 | 2023-23-May 15:21:16 |
Task Results
We can view the inferences from our logs and verify that new entries were added from our task. We’ll query the last 5 rows of our inference output table after a wait of 60 seconds.
time.sleep(30)
# Get the last insert to the output table to verify
task_inference_results = bigqueryoutputclient.query(
f"""
SELECT *
FROM {big_query_output_connection.details()['dataset']}.{big_query_output_connection.details()['table']}
ORDER BY date DESC
LIMIT 5
"""
).to_dataframe()
display(task_inference_results)
date | forecast | |
---|---|---|
0 | 2023-05-23 15:20:01.622083+00:00 | [1231.2556997246595, 1627.3643469089343, 1674.3769827243134, 1621.9273295873882, 1140.7465817903185, 1211.5223974364667, 1457.1896450382922] |
1 | 2023-05-23 15:06:06.678170+00:00 | [1231.2556997246595, 1627.3643469089343, 1674.3769827243134, 1621.9273295873882, 1140.7465817903185, 1211.5223974364667, 1457.1896450382922] |
2 | 2023-05-23 14:57:00.200596+00:00 | [1231.2556997246595, 1627.3643469089343, 1674.3769827243134, 1621.9273295873882, 1140.7465817903185, 1211.5223974364667, 1457.1896450382922] |
3 | 2023-05-19 22:39:01.932839+00:00 | [1231.2556997246595, 1627.3643469089343, 1674.3769827243134, 1621.9273295873882, 1140.7465817903185, 1211.5223974364667, 1457.1896450382922] |
4 | 2023-05-19 22:33:27.655703+00:00 | [1231.2556997246595, 1627.3643469089343, 1674.3769827243134, 1621.9273295873882, 1140.7465817903185, 1211.5223974364667, 1457.1896450382922] |
Scheduled Run Task Example
The other method of using tasks is as a scheduled run through the Orchestration run_scheduled(name, schedule, timeout, json_args)
. This sets up a task to run on an regular schedule as defined by the schedule
parameter in the cron
service format. For example:
schedule={'42 * * * *'}
Runs on the 42nd minute of every hour.
The following schedule runs every day at 12 noon from February 1 to February 15 2024 - and then ends.
schedule={'0 0 12 1-15 2 2024'}
For our example, we will create a scheduled task to run every 1 minute, display the inference results, then use the Orchestration kill
task to keep the task from running any further.
task_inference_results = bigqueryoutputclient.query(
f"""
SELECT *
FROM {big_query_output_connection.details()['dataset']}.{big_query_output_connection.details()['table']}
ORDER BY date DESC
LIMIT 5
"""
).to_dataframe()
display(task_inference_results)
scheduled_task = orchestration.run_scheduled(name="simple_statsmodel_inference_schedule", schedule="*/1 * * * *", timeout=120, json_args={})
date | forecast | |
---|---|---|
0 | 2023-05-23 15:20:01.622083+00:00 | [1231.2556997246595, 1627.3643469089343, 1674.3769827243134, 1621.9273295873882, 1140.7465817903185, 1211.5223974364667, 1457.1896450382922] |
1 | 2023-05-23 15:06:06.678170+00:00 | [1231.2556997246595, 1627.3643469089343, 1674.3769827243134, 1621.9273295873882, 1140.7465817903185, 1211.5223974364667, 1457.1896450382922] |
2 | 2023-05-23 14:57:00.200596+00:00 | [1231.2556997246595, 1627.3643469089343, 1674.3769827243134, 1621.9273295873882, 1140.7465817903185, 1211.5223974364667, 1457.1896450382922] |
3 | 2023-05-19 22:39:01.932839+00:00 | [1231.2556997246595, 1627.3643469089343, 1674.3769827243134, 1621.9273295873882, 1140.7465817903185, 1211.5223974364667, 1457.1896450382922] |
4 | 2023-05-19 22:33:27.655703+00:00 | [1231.2556997246595, 1627.3643469089343, 1674.3769827243134, 1621.9273295873882, 1140.7465817903185, 1211.5223974364667, 1457.1896450382922] |
while scheduled_task.status() != "started":
display(scheduled_task.status())
time.sleep(5)
'pending'
#wait 120 seconds to give the scheduled event time to finish
time.sleep(60)
task_inference_results = bigqueryoutputclient.query(
f"""
SELECT *
FROM {big_query_output_connection.details()['dataset']}.{big_query_output_connection.details()['table']}
ORDER BY date DESC
LIMIT 5
"""
).to_dataframe()
display(task_inference_results)
date | forecast | |
---|---|---|
0 | 2023-05-23 15:20:01.622083+00:00 | [1231.2556997246595, 1627.3643469089343, 1674.3769827243134, 1621.9273295873882, 1140.7465817903185, 1211.5223974364667, 1457.1896450382922] |
1 | 2023-05-23 15:06:06.678170+00:00 | [1231.2556997246595, 1627.3643469089343, 1674.3769827243134, 1621.9273295873882, 1140.7465817903185, 1211.5223974364667, 1457.1896450382922] |
2 | 2023-05-23 14:57:00.200596+00:00 | [1231.2556997246595, 1627.3643469089343, 1674.3769827243134, 1621.9273295873882, 1140.7465817903185, 1211.5223974364667, 1457.1896450382922] |
3 | 2023-05-19 22:39:01.932839+00:00 | [1231.2556997246595, 1627.3643469089343, 1674.3769827243134, 1621.9273295873882, 1140.7465817903185, 1211.5223974364667, 1457.1896450382922] |
4 | 2023-05-19 22:33:27.655703+00:00 | [1231.2556997246595, 1627.3643469089343, 1674.3769827243134, 1621.9273295873882, 1140.7465817903185, 1211.5223974364667, 1457.1896450382922] |
Kill Task
With our testing complete, we will kill the scheduled task so it will not run again. First we’ll show all the tasks to verify that our task is there, then issue it the kill command.
scheduled_task.kill()
<ArbexStatus.PENDING_KILL: 'pending_kill'>
Running Task with Custom Parameters
Right now, our task assumes the workspace, pipeline, and connections all have the names we defined above. For this example, we’ll set up a new pipeline with the same pipeline step, but name it bigquerystatsmodelpipeline02
.
When we create our task, we’ll add that pipeline name as an argument to our task. Within our orchestrations main.py
there is a code block that takes in the task arguments, then sets the pipeline name:
arguments = wl.task_args()
if "pipeline_name" in arguments:
pipeline_name = arguments['pipeline_name']
else:
pipeline_name="bigquerystatsmodelpipeline"
We’ll pass along our new pipeline name as { "pipeline_name": "bigquerystatsmodelpipeline02" }
and track the task progress as before.
newpipeline_name = 'bigquerystatsmodelpipeline02'
pipeline02 = get_pipeline(newpipeline_name)
# add the model as the pipeline step
pipeline02.add_model_step(bike_day_model)
name | bigquerystatsmodelpipeline02 |
---|---|
created | 2023-05-23 15:23:02.136077+00:00 |
last_updated | 2023-05-23 15:23:02.136077+00:00 |
deployed | (none) |
tags | |
versions | b2c56497-7a50-434a-8973-6d91af80f143 |
steps |
# required to set the pipeline steps
pipeline02.deploy()
Waiting for deployment - this will take up to 45s ........ ok
name | bigquerystatsmodelpipeline02 |
---|---|
created | 2023-05-23 15:23:02.136077+00:00 |
last_updated | 2023-05-23 15:24:48.381545+00:00 |
deployed | True |
tags | |
versions | 9dc1f8c5-e9ab-48e7-9b54-290fed5bb28c, 1b44f058-9ab6-44dd-8de6-c52acfdf4fc5, b2c56497-7a50-434a-8973-6d91af80f143 |
steps | bigquerystatsmodelmodelgztp |
# Get the last insert to the output table to verify
task_inference_results = bigqueryoutputclient.query(
f"""
SELECT *
FROM {big_query_output_connection.details()['dataset']}.{big_query_output_connection.details()['table']}
ORDER BY date DESC
LIMIT 5
"""
).to_dataframe()
display(task_inference_results)
# Generate the run once task with the new parameter
task = orchestration.run_once(name="parameter sample", json_args={ "pipeline_name": newpipeline_name })
display(task)
# wait for the task to run
while task.status() != "started":
display(task.status())
time.sleep(5)
date | forecast | |
---|---|---|
0 | 2023-05-23 15:20:01.622083+00:00 | [1231.2556997246595, 1627.3643469089343, 1674.3769827243134, 1621.9273295873882, 1140.7465817903185, 1211.5223974364667, 1457.1896450382922] |
1 | 2023-05-23 15:06:06.678170+00:00 | [1231.2556997246595, 1627.3643469089343, 1674.3769827243134, 1621.9273295873882, 1140.7465817903185, 1211.5223974364667, 1457.1896450382922] |
2 | 2023-05-23 14:57:00.200596+00:00 | [1231.2556997246595, 1627.3643469089343, 1674.3769827243134, 1621.9273295873882, 1140.7465817903185, 1211.5223974364667, 1457.1896450382922] |
3 | 2023-05-19 22:39:01.932839+00:00 | [1231.2556997246595, 1627.3643469089343, 1674.3769827243134, 1621.9273295873882, 1140.7465817903185, 1211.5223974364667, 1457.1896450382922] |
4 | 2023-05-19 22:33:27.655703+00:00 | [1231.2556997246595, 1627.3643469089343, 1674.3769827243134, 1621.9273295873882, 1140.7465817903185, 1211.5223974364667, 1457.1896450382922] |
Field | Value |
---|---|
ID | 2fb3cba9-d515-44da-bd9e-59661b4372ef |
Name | parameter sample |
Last Run Status | unknown |
Type | Temporary Run |
Active | True |
Schedule | - |
Created At | 2023-23-May 15:24:59 |
Updated At | 2023-23-May 15:24:59 |
'pending'
‘pending’
# wait 30 seconds then display the results
time.sleep(30)
task_inference_results = bigqueryoutputclient.query(
f"""
SELECT *
FROM {big_query_output_connection.details()['dataset']}.{big_query_output_connection.details()['table']}
ORDER BY date DESC
LIMIT 5
"""
).to_dataframe()
display(task_inference_results)
date | forecast | |
---|---|---|
0 | 2023-05-23 15:20:01.622083+00:00 | [1231.2556997246595, 1627.3643469089343, 1674.3769827243134, 1621.9273295873882, 1140.7465817903185, 1211.5223974364667, 1457.1896450382922] |
1 | 2023-05-23 15:06:06.678170+00:00 | [1231.2556997246595, 1627.3643469089343, 1674.3769827243134, 1621.9273295873882, 1140.7465817903185, 1211.5223974364667, 1457.1896450382922] |
2 | 2023-05-23 14:57:00.200596+00:00 | [1231.2556997246595, 1627.3643469089343, 1674.3769827243134, 1621.9273295873882, 1140.7465817903185, 1211.5223974364667, 1457.1896450382922] |
3 | 2023-05-19 22:39:01.932839+00:00 | [1231.2556997246595, 1627.3643469089343, 1674.3769827243134, 1621.9273295873882, 1140.7465817903185, 1211.5223974364667, 1457.1896450382922] |
4 | 2023-05-19 22:33:27.655703+00:00 | [1231.2556997246595, 1627.3643469089343, 1674.3769827243134, 1621.9273295873882, 1140.7465817903185, 1211.5223974364667, 1457.1896450382922] |
Conclusion
With that, our tutorial is over. Please feel free to use this tutorial code in your own Wallaroo related projects. Our last task will be to undeploy our pipelines to restore the resources back to the Wallaroo instance.
pipeline.undeploy()
pipeline02.undeploy()
ok
Waiting for undeployment - this will take up to 45s ..................................... ok
name | bigquerystatsmodelpipeline02 |
---|---|
created | 2023-05-23 15:23:02.136077+00:00 |
last_updated | 2023-05-23 15:24:48.381545+00:00 |
deployed | False |
tags | |
versions | 9dc1f8c5-e9ab-48e7-9b54-290fed5bb28c, 1b44f058-9ab6-44dd-8de6-c52acfdf4fc5, b2c56497-7a50-434a-8973-6d91af80f143 |
steps | bigquerystatsmodelmodelgztp |
7.6 - Wallaroo ML Workload Orchestration Comprehensive Tutorial
This can be downloaded as part of the Wallaroo Tutorials repository.
ML Workload Orchestration Comprehensive Tutorial
This tutorial provides a complete set of methods and examples regarding Wallaroo Connections and Wallaroo ML Workload Orchestration.
Wallaroo provides data connections, orchestrations, and tasks to provide organizations with a method of creating and managing automated tasks that can either be run on demand, on a regular schedule, or as a service so they respond to requests.
Object | Description |
---|---|
Orchestration | A set of instructions written as a python script with a requirements library. Orchestrations are uploaded to the Wallaroo instance |
Task | An implementation of an orchestration. Tasks are run either once when requested, on a repeating schedule, or as a service. |
Connection | Definitions set by MLOps engineers that are used by other Wallaroo users for connection information to a data source. Usually paired with orchestrations. |
A typical flow in the orchestration, task and connection life cycle is:
- (Optional) A connection is defined with information such as username, connection URL, tokens, etc.
- One or more connections are applied to a workspace for users to implement in their code or orchestrations.
- An orchestration is created to perform some set instructions. For example:
- Deploy a pipeline, request data from an external service, store the results in an external database, then undeploy the pipeline.
- Download a ML Model then replace a current pipeline step with the new version.
- Collect log files from a deployed pipeline once every hour and submit it to a Kafka or other service.
- A task is created that specifies the orchestration to perform and the schedule:
- Run once.
- Run on a schedule (based on
cron
like settings). - Run as a service to be run whenever requested.
- Once the use for a task is complete, it is killed and its schedule or service removed.
Tutorial Goals
The tutorial will demonstrate the following:
- Create a simple connection to retrieve an Apache Arrow table file from a GitHub registry.
- Create an orchestration that retrieves the Apache Arrow table file from the location defined by the connection, deploy a pipeline, perform an inference, then undeploys the pipeline.
- Implement the orchestration as a task that runs every minute.
- Display the logs from the pipeline after 5 minutes to verify the task is running.
Tutorial Required Libraries
The following libraries are required for this tutorial, and included by default in a Wallaroo instance’s JupyterHub service.
IMPORTANT NOTE: These libraries are already installed in the Wallaroo JupyterHub service. Do not uninstall and reinstall the Wallaroo SDK with the command below.
wallaroo: The Wallaroo SDK.
pandas: The pandas data analysis library.
pyarrow: The Apache Arrow Python library.
The specific versions used are set in the file ./resources/requirements.txt
. Supported libraries are automatically installed with the pypi
or conda
commands. For example, from the root of this tutorials folder:
pip install -r ./resources/requirements.txt
Initialization
The first step is to connect to a Wallaroo instance. We’ll load the libraries and set our client connection settings
Workspace, Model and Pipeline Setup
For this tutorial, we’ll create a workspace, upload our sample model and deploy a pipeline. We’ll perform some quick sample inferences to verify that everything it working.
import wallaroo
from wallaroo.object import EntityNotFoundError
# to display dataframe tables
from IPython.display import display
# used to display dataframe information without truncating
import pandas as pd
pd.set_option('display.max_colwidth', None)
import pyarrow as pa
import requests
# Used to create unique workspace and pipeline names
import string
import random
# make a random 4 character suffix
suffix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
display(suffix)
'tgiq'
Connect to the Wallaroo Instance
The first step is to connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. For more information on Wallaroo Client settings, see the Client Connection guide.
# Login through local Wallaroo instance
wl = wallaroo.Client()
# Setting variables for later steps
workspace_name = f'orchestrationworkspace{suffix}'
pipeline_name = f'orchestrationpipeline{suffix}'
model_name = f'orchestrationmodel{suffix}'
model_file_name = './models/rf_model.onnx'
connection_name = f'houseprice_arrow_table{suffix}'
Helper Methods
The following helper methods are used to either create or get workspaces and pipelines.
# helper methods to retrieve workspaces and pipelines
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
pipeline = wl.build_pipeline(name)
return pipeline
Create the Workspace and Pipeline
We’ll now create our workspace and pipeline for the tutorial. If this tutorial has been run previously, then this will retrieve the existing ones with the assumption they’re for us with this tutorial.
We’ll set the retrieved workspace as the current workspace in the SDK, so all commands will default to that workspace.
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
pipeline = get_pipeline(pipeline_name)
Upload the Model and Deploy Pipeline
We’ll upload our model into our sample workspace, then add it as a pipeline step before deploying the pipeline to it’s ready to accept inference requests.
# Upload the model
housing_model_control = wl.upload_model(model_name, model_file_name, framework=wallaroo.framework.Framework.ONNX).configure()
# Add the model as a pipeline step
pipeline.add_model_step(housing_model_control)
name | orchestrationpipelinetgiq |
---|---|
created | 2023-05-22 19:54:06.933674+00:00 |
last_updated | 2023-05-22 19:54:06.933674+00:00 |
deployed | (none) |
tags | |
versions | ed5bf4b1-1d5d-4ff9-8a23-2c1e44a8e672 |
steps |
#deploy the pipeline
pipeline.deploy()
Waiting for deployment - this will take up to 45s ........................ ok
name | orchestrationpipelinetgiq |
---|---|
created | 2023-05-22 19:54:06.933674+00:00 |
last_updated | 2023-05-22 19:54:08.008312+00:00 |
deployed | True |
tags | |
versions | a7336408-20ef-4b65-8167-c2f80c968a21, ed5bf4b1-1d5d-4ff9-8a23-2c1e44a8e672 |
steps | orchestrationmodeltgiq |
Sample Inferences
We’ll perform some quick sample inferences using an Apache Arrow table as the input. Once that’s finished, we’ll undeploy the pipeline and return the resources back to the Wallaroo instance.
# sample inferences
batch_inferences = pipeline.infer_from_file('./data/xtest-1k.arrow')
large_inference_result = batch_inferences.to_pandas()
display(large_inference_result.head(20))
time | in.tensor | out.variable | check_failures | |
---|---|---|---|---|
0 | 2023-05-22 19:54:33.671 | [4.0, 2.5, 2900.0, 5505.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2900.0, 0.0, 47.6063, -122.02, 2970.0, 5251.0, 12.0, 0.0, 0.0] | [718013.75] | 0 |
1 | 2023-05-22 19:54:33.671 | [2.0, 2.5, 2170.0, 6361.0, 1.0, 0.0, 2.0, 3.0, 8.0, 2170.0, 0.0, 47.7109, -122.017, 2310.0, 7419.0, 6.0, 0.0, 0.0] | [615094.56] | 0 |
2 | 2023-05-22 19:54:33.671 | [3.0, 2.5, 1300.0, 812.0, 2.0, 0.0, 0.0, 3.0, 8.0, 880.0, 420.0, 47.5893, -122.317, 1300.0, 824.0, 6.0, 0.0, 0.0] | [448627.72] | 0 |
3 | 2023-05-22 19:54:33.671 | [4.0, 2.5, 2500.0, 8540.0, 2.0, 0.0, 0.0, 3.0, 9.0, 2500.0, 0.0, 47.5759, -121.994, 2560.0, 8475.0, 24.0, 0.0, 0.0] | [758714.2] | 0 |
4 | 2023-05-22 19:54:33.671 | [3.0, 1.75, 2200.0, 11520.0, 1.0, 0.0, 0.0, 4.0, 7.0, 2200.0, 0.0, 47.7659, -122.341, 1690.0, 8038.0, 62.0, 0.0, 0.0] | [513264.7] | 0 |
5 | 2023-05-22 19:54:33.671 | [3.0, 2.0, 2140.0, 4923.0, 1.0, 0.0, 0.0, 4.0, 8.0, 1070.0, 1070.0, 47.6902, -122.339, 1470.0, 4923.0, 86.0, 0.0, 0.0] | [668288.0] | 0 |
6 | 2023-05-22 19:54:33.671 | [4.0, 3.5, 3590.0, 5334.0, 2.0, 0.0, 2.0, 3.0, 9.0, 3140.0, 450.0, 47.6763, -122.267, 2100.0, 6250.0, 9.0, 0.0, 0.0] | [1004846.5] | 0 |
7 | 2023-05-22 19:54:33.671 | [3.0, 2.0, 1280.0, 960.0, 2.0, 0.0, 0.0, 3.0, 9.0, 1040.0, 240.0, 47.602, -122.311, 1280.0, 1173.0, 0.0, 0.0, 0.0] | [684577.2] | 0 |
8 | 2023-05-22 19:54:33.671 | [4.0, 2.5, 2820.0, 15000.0, 2.0, 0.0, 0.0, 4.0, 9.0, 2820.0, 0.0, 47.7255, -122.101, 2440.0, 15000.0, 29.0, 0.0, 0.0] | [727898.1] | 0 |
9 | 2023-05-22 19:54:33.671 | [3.0, 2.25, 1790.0, 11393.0, 1.0, 0.0, 0.0, 3.0, 8.0, 1790.0, 0.0, 47.6297, -122.099, 2290.0, 11894.0, 36.0, 0.0, 0.0] | [559631.1] | 0 |
10 | 2023-05-22 19:54:33.671 | [3.0, 1.5, 1010.0, 7683.0, 1.5, 0.0, 0.0, 5.0, 7.0, 1010.0, 0.0, 47.72, -122.318, 1550.0, 7271.0, 61.0, 0.0, 0.0] | [340764.53] | 0 |
11 | 2023-05-22 19:54:33.671 | [3.0, 2.0, 1270.0, 1323.0, 3.0, 0.0, 0.0, 3.0, 8.0, 1270.0, 0.0, 47.6934, -122.342, 1330.0, 1323.0, 8.0, 0.0, 0.0] | [442168.06] | 0 |
12 | 2023-05-22 19:54:33.671 | [4.0, 1.75, 2070.0, 9120.0, 1.0, 0.0, 0.0, 4.0, 7.0, 1250.0, 820.0, 47.6045, -122.123, 1650.0, 8400.0, 57.0, 0.0, 0.0] | [630865.6] | 0 |
13 | 2023-05-22 19:54:33.671 | [4.0, 1.0, 1620.0, 4080.0, 1.5, 0.0, 0.0, 3.0, 7.0, 1620.0, 0.0, 47.6696, -122.324, 1760.0, 4080.0, 91.0, 0.0, 0.0] | [559631.1] | 0 |
14 | 2023-05-22 19:54:33.671 | [4.0, 3.25, 3990.0, 9786.0, 2.0, 0.0, 0.0, 3.0, 9.0, 3990.0, 0.0, 47.6784, -122.026, 3920.0, 8200.0, 10.0, 0.0, 0.0] | [909441.1] | 0 |
15 | 2023-05-22 19:54:33.671 | [4.0, 2.0, 1780.0, 19843.0, 1.0, 0.0, 0.0, 3.0, 7.0, 1780.0, 0.0, 47.4414, -122.154, 2210.0, 13500.0, 52.0, 0.0, 0.0] | [313096.0] | 0 |
16 | 2023-05-22 19:54:33.671 | [4.0, 2.5, 2130.0, 6003.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2130.0, 0.0, 47.4518, -122.12, 1940.0, 4529.0, 11.0, 0.0, 0.0] | [404040.8] | 0 |
17 | 2023-05-22 19:54:33.671 | [3.0, 1.75, 1660.0, 10440.0, 1.0, 0.0, 0.0, 3.0, 7.0, 1040.0, 620.0, 47.4448, -121.77, 1240.0, 10380.0, 36.0, 0.0, 0.0] | [292859.5] | 0 |
18 | 2023-05-22 19:54:33.671 | [3.0, 2.5, 2110.0, 4118.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2110.0, 0.0, 47.3878, -122.153, 2110.0, 4044.0, 25.0, 0.0, 0.0] | [338357.88] | 0 |
19 | 2023-05-22 19:54:33.671 | [4.0, 2.25, 2200.0, 11250.0, 1.5, 0.0, 0.0, 5.0, 7.0, 1300.0, 900.0, 47.6845, -122.201, 2320.0, 10814.0, 94.0, 0.0, 0.0] | [682284.6] | 0 |
Create Wallaroo Connection
Connections are created at the Wallaroo instance level, typically by a MLOps or DevOps engineer, then applied to a workspace.
For this section:
- We will create a sample connection that just has a URL to the same Arrow table file we used in the previous step.
- We’ll apply the data connection to the workspace above.
- For a quick demonstration, we’ll use the connection to retrieve the Arrow table file and use it for a quick sample inference.
Create Connection
Connections are created with the Wallaroo client command create_connection
with the following parameters.
Parameter | Type | Description |
---|---|---|
name | string (Required) | The name of the connection. This must be unique - if submitting the name of an existing connection it will return an error. |
type | string (Required) | The user defined type of connection. |
details | Dict (Requires) | User defined configuration details for the data connection. These can be {'username':'dataperson', 'password':'datapassword', 'port': 3339} , or {'token':'abcde123==', 'host':'example.com', 'port:1234'} , or other user defined combinations. |
We’ll create the connection named houseprice_arrow_table
, set it to the type HTTPFILE
, and provide the details as 'host':'https://github.com/WallarooLabs/Wallaroo_Tutorials/raw/main/wallaroo-testing-tutorials/houseprice-saga/data/xtest-1k.arrow'
- the location for our sample Arrow table inference input.
wl.create_connection(connection_name,
"HTTPFILE",
{'host':'https://github.com/WallarooLabs/Wallaroo_Tutorials/raw/main/wallaroo-testing-tutorials/houseprice-saga/data/xtest-1k.arrow'}
)
Field | Value |
---|---|
Name | houseprice_arrow_tabletgiq |
Connection Type | HTTPFILE |
Details | ***** |
Created At | 2023-05-22T19:54:33.723860+00:00 |
Linked Workspaces | [] |
List Data Connections
The Wallaroo Client list_connections()
method lists all connections for the Wallaroo instance.
wl.list_connections()
name | connection type | details | created at | linked workspaces |
---|---|---|---|---|
houseprice_arrow_tabletgiq | HTTPFILE | ***** | 2023-05-22T19:54:33.723860+00:00 | [] |
Add Connection to Workspace
The method Workspace add_connection(connection_name)
adds a Data Connection to a workspace, and takes the following parameters.
Parameter | Type | Description |
---|---|---|
name | string (Required) | The name of the Data Connection |
We’ll add this connection to our sample workspace.
workspace.add_connection(connection_name)
Get Connection
Connections are retrieved by the Wallaroo Client get_connection(name)
method.
connection = wl.get_connection(connection_name)
Connection Details
The Connection method details()
retrieves a the connection details()
as a dict
.
display(connection.details())
{'host': 'https://github.com/WallarooLabs/Wallaroo_Tutorials/raw/main/wallaroo-testing-tutorials/houseprice-saga/data/xtest-1k.arrow'}
Using a Connection Example
For this example, the connection will be used to retrieve the Apache Arrow file referenced in the connection, and use that to turn it into an Apache Arrow table, then use that for a sample inference.
# Deploy the pipeline
pipeline.deploy()
# Retrieve the file
# set accept as apache arrow table
headers = {
'Accept': 'application/vnd.apache.arrow.file'
}
response = requests.get(
connection.details()['host'],
headers=headers
)
# Arrow table is retrieved
with pa.ipc.open_file(response.content) as reader:
arrow_table = reader.read_all()
results = pipeline.infer(arrow_table)
result_table = results.to_pandas()
display(result_table.head(20))
ok
time | in.tensor | out.variable | check_failures | |
---|---|---|---|---|
0 | 2023-05-22 19:54:34.320 | [4.0, 2.5, 2900.0, 5505.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2900.0, 0.0, 47.6063, -122.02, 2970.0, 5251.0, 12.0, 0.0, 0.0] | [718013.75] | 0 |
1 | 2023-05-22 19:54:34.320 | [2.0, 2.5, 2170.0, 6361.0, 1.0, 0.0, 2.0, 3.0, 8.0, 2170.0, 0.0, 47.7109, -122.017, 2310.0, 7419.0, 6.0, 0.0, 0.0] | [615094.56] | 0 |
2 | 2023-05-22 19:54:34.320 | [3.0, 2.5, 1300.0, 812.0, 2.0, 0.0, 0.0, 3.0, 8.0, 880.0, 420.0, 47.5893, -122.317, 1300.0, 824.0, 6.0, 0.0, 0.0] | [448627.72] | 0 |
3 | 2023-05-22 19:54:34.320 | [4.0, 2.5, 2500.0, 8540.0, 2.0, 0.0, 0.0, 3.0, 9.0, 2500.0, 0.0, 47.5759, -121.994, 2560.0, 8475.0, 24.0, 0.0, 0.0] | [758714.2] | 0 |
4 | 2023-05-22 19:54:34.320 | [3.0, 1.75, 2200.0, 11520.0, 1.0, 0.0, 0.0, 4.0, 7.0, 2200.0, 0.0, 47.7659, -122.341, 1690.0, 8038.0, 62.0, 0.0, 0.0] | [513264.7] | 0 |
5 | 2023-05-22 19:54:34.320 | [3.0, 2.0, 2140.0, 4923.0, 1.0, 0.0, 0.0, 4.0, 8.0, 1070.0, 1070.0, 47.6902, -122.339, 1470.0, 4923.0, 86.0, 0.0, 0.0] | [668288.0] | 0 |
6 | 2023-05-22 19:54:34.320 | [4.0, 3.5, 3590.0, 5334.0, 2.0, 0.0, 2.0, 3.0, 9.0, 3140.0, 450.0, 47.6763, -122.267, 2100.0, 6250.0, 9.0, 0.0, 0.0] | [1004846.5] | 0 |
7 | 2023-05-22 19:54:34.320 | [3.0, 2.0, 1280.0, 960.0, 2.0, 0.0, 0.0, 3.0, 9.0, 1040.0, 240.0, 47.602, -122.311, 1280.0, 1173.0, 0.0, 0.0, 0.0] | [684577.2] | 0 |
8 | 2023-05-22 19:54:34.320 | [4.0, 2.5, 2820.0, 15000.0, 2.0, 0.0, 0.0, 4.0, 9.0, 2820.0, 0.0, 47.7255, -122.101, 2440.0, 15000.0, 29.0, 0.0, 0.0] | [727898.1] | 0 |
9 | 2023-05-22 19:54:34.320 | [3.0, 2.25, 1790.0, 11393.0, 1.0, 0.0, 0.0, 3.0, 8.0, 1790.0, 0.0, 47.6297, -122.099, 2290.0, 11894.0, 36.0, 0.0, 0.0] | [559631.1] | 0 |
10 | 2023-05-22 19:54:34.320 | [3.0, 1.5, 1010.0, 7683.0, 1.5, 0.0, 0.0, 5.0, 7.0, 1010.0, 0.0, 47.72, -122.318, 1550.0, 7271.0, 61.0, 0.0, 0.0] | [340764.53] | 0 |
11 | 2023-05-22 19:54:34.320 | [3.0, 2.0, 1270.0, 1323.0, 3.0, 0.0, 0.0, 3.0, 8.0, 1270.0, 0.0, 47.6934, -122.342, 1330.0, 1323.0, 8.0, 0.0, 0.0] | [442168.06] | 0 |
12 | 2023-05-22 19:54:34.320 | [4.0, 1.75, 2070.0, 9120.0, 1.0, 0.0, 0.0, 4.0, 7.0, 1250.0, 820.0, 47.6045, -122.123, 1650.0, 8400.0, 57.0, 0.0, 0.0] | [630865.6] | 0 |
13 | 2023-05-22 19:54:34.320 | [4.0, 1.0, 1620.0, 4080.0, 1.5, 0.0, 0.0, 3.0, 7.0, 1620.0, 0.0, 47.6696, -122.324, 1760.0, 4080.0, 91.0, 0.0, 0.0] | [559631.1] | 0 |
14 | 2023-05-22 19:54:34.320 | [4.0, 3.25, 3990.0, 9786.0, 2.0, 0.0, 0.0, 3.0, 9.0, 3990.0, 0.0, 47.6784, -122.026, 3920.0, 8200.0, 10.0, 0.0, 0.0] | [909441.1] | 0 |
15 | 2023-05-22 19:54:34.320 | [4.0, 2.0, 1780.0, 19843.0, 1.0, 0.0, 0.0, 3.0, 7.0, 1780.0, 0.0, 47.4414, -122.154, 2210.0, 13500.0, 52.0, 0.0, 0.0] | [313096.0] | 0 |
16 | 2023-05-22 19:54:34.320 | [4.0, 2.5, 2130.0, 6003.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2130.0, 0.0, 47.4518, -122.12, 1940.0, 4529.0, 11.0, 0.0, 0.0] | [404040.8] | 0 |
17 | 2023-05-22 19:54:34.320 | [3.0, 1.75, 1660.0, 10440.0, 1.0, 0.0, 0.0, 3.0, 7.0, 1040.0, 620.0, 47.4448, -121.77, 1240.0, 10380.0, 36.0, 0.0, 0.0] | [292859.5] | 0 |
18 | 2023-05-22 19:54:34.320 | [3.0, 2.5, 2110.0, 4118.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2110.0, 0.0, 47.3878, -122.153, 2110.0, 4044.0, 25.0, 0.0, 0.0] | [338357.88] | 0 |
19 | 2023-05-22 19:54:34.320 | [4.0, 2.25, 2200.0, 11250.0, 1.5, 0.0, 0.0, 5.0, 7.0, 1300.0, 900.0, 47.6845, -122.201, 2320.0, 10814.0, 94.0, 0.0, 0.0] | [682284.6] | 0 |
Remove Connection from Workspace
The Workspace method remove_connection(connection_name)
removes the connection from the workspace, but does not delete the connection from the Wallaroo instance. This method takes the following parameters.
Parameter | Type | Description |
---|---|---|
name | String (Required) | The name of the connection to be removed |
The previous connection will be removed from the workspace, then the workspace connections displayed to verify it has been removed.
workspace.remove_connection(connection_name)
display(workspace.list_connections())
(no connections)
Delete Connection
The Connection method delete_connection()
removes the connection from the Wallaroo instance, and all attachments in workspaces they were connected to.
connection.delete_connection()
wl.list_connections()
(no connections)
Orchestration Tutorial
The next series of examples will build on what we just did. So far we have:
- Deployed a pipeline, performed sample inferences with a local Apache Arrow file, displayed the results, then undeployed the pipeline.
- Deployed a pipeline, use a Wallaroo connection details to retrieve a remote Apache Arrow file, performed inferences and displayed the results, then undeployed the pipeline.
For the orchestration tutorial, we’ll do the same thing only package it into a separate python script and upload it to the Wallaroo instance, then create a task from that orchestration and perform our sample inferences again.
Orchestration Requirements
Orchestrations are uploaded to the Wallaroo instance as a ZIP file with the following requirements:
- The ZIP file should not contain any directories - only files at the top level.
Parameter | Type | Description |
---|---|---|
User Code | (Required) Python script as .py files | Python scripts for the orchestration to run. If the file main.py exists, that will be the entrypoint. Otherwise, if only one .py exists, then that will be the entrypoint. |
Python Library Requirements | (Required) requirements.txt file in the requirements file format. This is in the root of the zip file, and there can only be one requirements.txt file for the orchestration. | |
Other artifacts | Â | Other artifacts such as files, data, or code to support the orchestration. |
Zip Instructions
In a terminal with the zip
command, assemble artifacts as above and then create the archive. The zip
command is included by default with the Wallaroo JupyterHub service.
zip
commands take the following format, with {zipfilename}.zip
as the zip file to save the artifacts to, and each file thereafter as the files to add to the archive.
zip {zipfilename}.zip file1, file2, file3....
For example, the following command will add the files main.py
and requirements.txt
into the file hello.zip
.
$ zip hello.zip main.py requirements.txt
adding: main.py (deflated 47%)
adding: requirements.txt (deflated 52%)
Orchestration Recommendations
The following recommendations will make using Wallaroo orchestrations
- The version of Python used should match the same version as in the Wallaroo JupyterHub service.
- The same version of the Wallaroo SDK should match the server. For a 2023.2 Wallaroo instance, use the Wallaroo SDK version 2023.2.
- Specify the version of
pip
dependencies. - The
wallaroo.Client
constructorauth_type
argument is ignored. Usingwallaroo.Client()
is sufficient. - The following methods will assist with orchestrations:
wallaroo.in_task()
: ReturnsTrue
if the code is running within an Orchestrator task.wallaroo.task_args()
: Returns aDict
of invocation-specific arguments passed to therun_
calls.
- Use
print
commands so outputs are saved to the task’s log files.
Example requirements.txt file
dbt-bigquery==1.4.3
dbt-core==1.4.5
dbt-extractor==0.4.1
dbt-postgres==1.4.5
google-api-core==2.8.2
google-auth==2.11.0
google-auth-oauthlib==0.4.6
google-cloud-bigquery==3.3.2
google-cloud-bigquery-storage==2.15.0
google-cloud-core==2.3.2
google-cloud-storage==2.5.0
google-crc32c==1.5.0
google-pasta==0.2.0
google-resumable-media==2.3.3
googleapis-common-protos==1.56.4
Sample Orchestrator
The following orchestrator artifacts are in the directory ./remote_inference
and includes the file main.py
with the following code:
import wallaroo
from wallaroo.object import EntityNotFoundError
import pandas as pd
import pyarrow as pa
import requests
wl = wallaroo.Client()
# Setting variables for later steps
# get the arguments
arguments = wl.task_args()
if "workspace_name" in arguments:
workspace_name = arguments['workspace_name']
else:
workspace_name="orchestrationworkspace"
if "pipeline_name" in arguments:
pipeline_name = arguments['pipeline_name']
else:
pipeline_name="orchestrationpipeline"
if "connection_name" in arguments:
connection_name = arguments['connection_name']
else:
connection_name = "houseprice_arrow_table"
# helper methods to retrieve workspaces and pipelines
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
pipeline = wl.build_pipeline(name)
return pipeline
print(f"Getting the workspace {workspace_name}")
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
print(f"Getting the pipeline {pipeline_name}")
pipeline = get_pipeline(pipeline_name)
pipeline.deploy()
# Get the connection - assuming it will be the only one
inference_source_connection = wl.get_connection(name=connection_name)
print(f"Getting arrow table file")
# Retrieve the file
# set accept as apache arrow table
headers = {
'Accept': 'application/vnd.apache.arrow.file'
}
response = requests.get(
inference_source_connection.details()['host'],
headers=headers
)
# Arrow table is retrieved
with pa.ipc.open_file(response.content) as reader:
arrow_table = reader.read_all()
print("Inference time. Displaying results after.")
# Perform the inference
result = pipeline.infer(arrow_table)
print(result)
pipeline.undeploy()
This is saved to the file ./remote_inference/remote_inference.zip
.
Preparing the Wallaroo Instance
To prepare the Wallaroo instance, we’ll once again create the Wallaroo connection houseprice_arrow_table
and apply it to the workspace.
wl.create_connection(connection_name,
"HTTPFILE",
{'host':'https://github.com/WallarooLabs/Wallaroo_Tutorials/raw/main/wallaroo-testing-tutorials/houseprice-saga/data/xtest-1k.arrow'}
)
workspace.add_connection(connection_name)
Upload the Orchestration
Orchestrations are uploaded with the Wallaroo client upload_orchestration(path)
method with the following parameters.
Parameter | Type | Description |
---|---|---|
path | string (Required) | The path to the ZIP file to be uploaded. |
Once uploaded, the deployment will be prepared and any requirements will be downloaded and installed.
For this example, the orchestration ./remote_inference/remote_inference.zip
will be uploaded and saved to the variable orchestration
.
orchestration = wl.upload_orchestration(name="comprehensive sample", path="./remote_inference/remote_inference.zip")
Orchestration Status
The Orchestration method status()
displays the current status of the uploaded orchestration.
Status | Description |
---|---|
— | — |
pending_packaging | The orchestration is uploaded, but packaging hasn’t started yet. |
packaging | The orchestration is being packaged for use with the Wallaroo instance. |
ready | The orchestration is ready for use. |
For this example, the status of the orchestration will be displayed then looped until it has reached status ready
.
import time
while orchestration.status() != 'ready':
print(orchestration.status())
time.sleep(5)
pending_packaging
pending_packaging
packaging
packaging
packaging
packaging
packaging
packaging
packaging
packaging
packaging
List Orchestrations
Orchestrations are listed with the Wallaroo Client list_orchestrations()
which returns a list of available orchestrations.
wl.list_orchestrations()
id | name | status | filename | sha | created at | updated at |
---|---|---|---|---|---|---|
0f90e606-09f8-409b-a306-cb04ec4c011a | comprehensive sample | ready | remote_inference.zip | b88e93...2396fb | 2023-22-May 19:55:15 | 2023-22-May 19:56:09 |
Task Management Tutorial
Once an Orchestration has the status ready
, it can be run as a task. Tasks have three run options.
Type | SDK Call | How triggered | Purpose |
---|---|---|---|
Once | orchestration.run_once(name, json_args, timeout) | Task runs once and exits. | Single batch, experimentation |
Scheduled | orchestration.run_scheduled() | User provides schedule. Task runs exits whenever schedule dictates. | Recurrent batch ETL. |
Task Run Once
Tasks are generated and run once with the Orchestration run_once(name, json_args, timeout)
method. Any arguments for the orchestration are passed in as a Dict
. If there are no arguments, then an empty set {}
is passed.
For our example, we will pass the workspace, pipeline, and connection into our task.
# Example: run once
import datetime
task_start = datetime.datetime.now()
task = orchestration.run_once(name="house price run once 2", json_args={"workspace_name": workspace_name,
"pipeline_name":pipeline_name,
"connection_name": connection_name
}
)
task
Field | Value |
---|---|
ID | f0e27d6a-6a98-4d26-b240-266f08560c48 |
Name | house price run once 2 |
Last Run Status | unknown |
Type | Temporary Run |
Active | True |
Schedule | - |
Created At | 2023-22-May 19:58:32 |
Updated At | 2023-22-May 19:58:32 |
taskfail.last_runs()
task id | pod id | status | created at | updated at |
---|---|---|---|---|
5ee51c78-a1c6-41e4-86a6-77110ce26161 | 844902e0-5ff3-4c15-b497-e173aa3ce0d5 | running | 2023-22-May 20:15:08 | 2023-22-May 20:15:08 |
List Tasks
The list of tasks in the Wallaroo instance is retrieved through the Wallaroo Client list_tasks()
method that accepts the following parameters.
Parameter | Type | Description |
---|---|---|
killed | Boolean (Optional Default: False ) | Returns tasks depending on whether they have been issued the kill command. False returns all tasks whether killed or not. True only returns killed tasks. |
This returns an array list of the following in reverse chronological order from updated at
.
Parameter | Type | Description |
---|---|---|
id | string | The UUID identifier for the task. |
last run status | string | The last reported status the task. Values are:
|
type | string | The type of the task. Values are:
|
active | Boolean | True : The task is scheduled or running. False : The task has completed or has been issued the kill command. |
schedule | string | The cron style schedule for the task. If the task is not a scheduled one, then the schedule will be - . |
created at | DateTime | The date and time the task was started. |
updated at | DateTime | The date and time the task was updated. |
wl.list_tasks()
id | name | last run status | type | active | schedule | created at | updated at |
---|---|---|---|---|---|---|---|
f0e27d6a-6a98-4d26-b240-266f08560c48 | house price run once 2 | running | Temporary Run | True | - | 2023-22-May 19:58:32 | 2023-22-May 19:58:38 |
36509ef8-98da-42a0-913f-e6e929dedb15 | house price run once | success | Temporary Run | True | - | 2023-22-May 19:56:37 | 2023-22-May 19:56:48 |
Task Status
The status of the task is returned with the Task status()
method that returned the tasks status. Tasks can have the following status.
pending
: The task has not been started or is being prepared.started
: The task has started to execute.
while task.status() != "started":
display(task.status())
time.sleep(5)
Task Last Runs History
The history of a task, which each deployment of the task is known as a task run is retrieved with the Task last_runs
method that takes the following arguments.
Parameter | Type | Description |
---|---|---|
status | String (Optional *Default: all ) | Filters the task history by the status . If all , returns all statuses. Status values are:
|
limit | Integer (Optional) | Limits the number of task runs returned. |
This returns the following in reverse chronological order by updated at
.
Parameter | Type | Description |
---|---|---|
task id | string | Task id in UUID format. |
pod id | string | Pod id in UUID format. |
status | string | Status of the task. Status values are:
|
created at | DateTime | Date and time the task was created at. |
updated at | DateTime | Date and time the task was updated. |
task.last_runs()
task id | pod id | status | created at | updated at |
---|---|---|---|---|
f0e27d6a-6a98-4d26-b240-266f08560c48 | 7d9d73d5-df11-44ed-90c1-db0e64c7f9b8 | success | 2023-22-May 19:58:35 | 2023-22-May 19:58:35 |
Task Run Logs
The output of a task is displayed with the Task Run logs()
method that takes the following parameters.
Parameter | Type | Description |
---|---|---|
limit | Integer (Optional) | Limits the lines returned from the task run log. The limit parameter is based on the log tail - starting from the last line of the log file, then working up until the limit of lines is reached. This is useful for viewing final outputs, exceptions, etc. |
The Task Run logs()
returns the log entries as a string list, with each entry as an item in the list.
- IMPORTANT NOTE: It may take around a minute for task run logs to be integrated into the Wallaroo log database.
# give time for the task to complete and the log files entered
time.sleep(60)
recent_run = task.last_runs()[0]
display(recent_run.logs())
2023-22-May 19:59:29 Getting the workspace orchestrationworkspacetgiq
2023-22-May 19:59:29 Getting the pipeline orchestrationpipelinetgiq
2023-22-May 19:59:29 Getting arrow table file
2023-22-May 19:59:29 Inference time. Displaying results after.
2023-22-May 19:59:29 pyarrow.Table
2023-22-May 19:59:29 time: timestamp[ms]
2023-22-May 19:59:29 in.tensor: list not null
2023-22-May 19:59:29 child 0, item: float
2023-22-May 19:59:29 out.variable: list not null
2023-22-May 19:59:29 check_failures: int8
2023-22-May 19:59:29 child 0, inner: float not null
2023-22-May 19:59:29 ----
2023-22-May 19:59:29 time: [[2023-05-22 19:58:49.767,2023-05-22 19:58:49.767,2023-05-22 19:58:49.767,2023-05-22 19:58:49.767,2023-05-22 19:58:49.767,...,2023-05-22 19:58:49.767,2023-05-22 19:58:49.767,2023-05-22 19:58:49.767,2023-05-22 19:58:49.767,2023-05-22 19:58:49.767]]
2023-22-May 19:59:29 in.tensor: [[[4,2.5,2900,5505,2,...,2970,5251,12,0,0],[2,2.5,2170,6361,1,...,2310,7419,6,0,0],...,[3,1.75,2910,37461,1,...,2520,18295,47,0,0],[3,2,2005,7000,1,...,1750,4500,34,0,0]]]
2023-22-May 19:59:29 check_failures: [[0,0,0,0,0,...,0,0,0,0,0]]
2023-22-May 19:59:29 out.variable: [[[718013.75],[615094.56],...,[706823.56],[581003]]]
Failed Task Logs
We can create a task that fails and show it in the last_runs
list, then retrieve the logs to display why it failed.
# Example: run once
import datetime
task_start = datetime.datetime.now()
taskfail = orchestration.run_once(name="house price run once 2", json_args={"workspace_name": "bob",
"pipeline_name":"does not exist",
"connection_name": connection_name
}
)
while taskfail.status() != "started":
display(taskfail.status())
time.sleep(5)
Field | Value |
---|---|
ID | 54229bd2-c388-4196-9ce2-f76503a27f99 |
Name | house price run once 2 |
Last Run Status | unknown |
Type | Temporary Run |
Active | True |
Schedule | - |
Created At | 2023-22-May 20:17:16 |
Updated At | 2023-22-May 20:17:16 |
# time.sleep(60)
taskfail.last_runs()
task id | pod id | status | created at | updated at |
---|---|---|---|---|
54229bd2-c388-4196-9ce2-f76503a27f99 | 79d7fe3e-ac8c-4f9c-8288-c9c207fb0a5e | failure | 2023-22-May 20:17:18 | 2023-22-May 20:17:18 |
# time.sleep(60)
taskfaillogs = taskfail.last_runs()[0].logs()
display(taskfaillogs)
2023-22-May 20:17:22 Getting the workspace bob
2023-22-May 20:17:22 File "/home/jovyan/main.py", line 43, in get_pipeline
2023-22-May 20:17:22 Traceback (most recent call last):
2023-22-May 20:17:22 Getting the pipeline does not exist
2023-22-May 20:17:22 pipeline = wl.pipelines_by_name(name)[0]
2023-22-May 20:17:22 File "/home/jovyan/venv/lib/python3.9/site-packages/wallaroo/client.py", line 1064, in pipelines_by_name
2023-22-May 20:17:22 raise EntityNotFoundError("Pipeline", {"pipeline_name": pipeline_name})
2023-22-May 20:17:22 wallaroo.object.EntityNotFoundError: Pipeline not found: {'pipeline_name': 'does not exist'}
2023-22-May 20:17:22
2023-22-May 20:17:22 During handling of the above exception, another exception occurred:
2023-22-May 20:17:22 Traceback (most recent call last):
2023-22-May 20:17:22
2023-22-May 20:17:22 File "/home/jovyan/main.py", line 54, in
2023-22-May 20:17:22 pipeline = get_pipeline(pipeline_name)
2023-22-May 20:17:22 pipeline = wl.build_pipeline(name)
2023-22-May 20:17:22 File "/home/jovyan/main.py", line 45, in get_pipeline
2023-22-May 20:17:22 File "/home/jovyan/venv/lib/python3.9/site-packages/wallaroo/client.py", line 1102, in build_pipeline
2023-22-May 20:17:22 require_dns_compliance(pipeline_name)
2023-22-May 20:17:22 File "/home/jovyan/venv/lib/python3.9/site-packages/wallaroo/checks.py", line 274, in require_dns_compliance
2023-22-May 20:17:22 wallaroo.object.InvalidNameError: Name 'does not exist is invalid: must be DNS-compatible (ASCII alpha-numeric plus dash (-))
2023-22-May 20:17:22 raise InvalidNameError(
Task Results
We can view the inferences from our logs and verify that new entries were added from our task. In our case, we’ll assume the task once started takes about 1 minute to run (deploy the pipeline, run the inference, undeploy the pipeline). We’ll add in a wait of 1 minute, then display the logs during the time period the task was running.
task_end = datetime.datetime.now()
display(task_end)
pipeline.logs(start_datetime = task_start, end_datetime = task_end)
datetime.datetime(2023, 5, 22, 20, 1, 25, 418564)
Warning: Pipeline log size limit exceeded. Please request logs using export_logs
time | in.tensor | out.variable | check_failures | |
---|---|---|---|---|
0 | 2023-05-22 19:58:49.767 | [4.0, 2.5, 2900.0, 5505.0, 2.0, 0.0, 0.0, 3.0, 8.0, 2900.0, 0.0, 47.6063, -122.02, 2970.0, 5251.0, 12.0, 0.0, 0.0] | [718013.75] | 0 |
1 | 2023-05-22 19:58:49.767 | [2.0, 2.5, 2170.0, 6361.0, 1.0, 0.0, 2.0, 3.0, 8.0, 2170.0, 0.0, 47.7109, -122.017, 2310.0, 7419.0, 6.0, 0.0, 0.0] | [615094.56] | 0 |
2 | 2023-05-22 19:58:49.767 | [3.0, 2.5, 1300.0, 812.0, 2.0, 0.0, 0.0, 3.0, 8.0, 880.0, 420.0, 47.5893, -122.317, 1300.0, 824.0, 6.0, 0.0, 0.0] | [448627.72] | 0 |
3 | 2023-05-22 19:58:49.767 | [4.0, 2.5, 2500.0, 8540.0, 2.0, 0.0, 0.0, 3.0, 9.0, 2500.0, 0.0, 47.5759, -121.994, 2560.0, 8475.0, 24.0, 0.0, 0.0] | [758714.2] | 0 |
4 | 2023-05-22 19:58:49.767 | [3.0, 1.75, 2200.0, 11520.0, 1.0, 0.0, 0.0, 4.0, 7.0, 2200.0, 0.0, 47.7659, -122.341, 1690.0, 8038.0, 62.0, 0.0, 0.0] | [513264.7] | 0 |
... | ... | ... | ... | ... |
501 | 2023-05-22 19:58:49.767 | [3.0, 2.5, 1570.0, 1433.0, 3.0, 0.0, 0.0, 3.0, 8.0, 1570.0, 0.0, 47.6858, -122.336, 1570.0, 2652.0, 4.0, 0.0, 0.0] | [557391.25] | 0 |
502 | 2023-05-22 19:58:49.767 | [3.0, 2.5, 2390.0, 15669.0, 2.0, 0.0, 0.0, 3.0, 9.0, 2390.0, 0.0, 47.7446, -122.193, 2640.0, 12500.0, 24.0, 0.0, 0.0] | [741973.6] | 0 |
503 | 2023-05-22 19:58:49.767 | [3.0, 0.75, 920.0, 20412.0, 1.0, 1.0, 2.0, 5.0, 6.0, 920.0, 0.0, 47.4781, -122.49, 1162.0, 54705.0, 64.0, 0.0, 0.0] | [338418.8] | 0 |
504 | 2023-05-22 19:58:49.767 | [4.0, 2.5, 2800.0, 246114.0, 2.0, 0.0, 0.0, 3.0, 9.0, 2800.0, 0.0, 47.6586, -121.962, 2750.0, 60351.0, 15.0, 0.0, 0.0] | [765468.75] | 0 |
505 | 2023-05-22 19:58:49.767 | [2.0, 1.0, 1120.0, 9912.0, 1.0, 0.0, 0.0, 4.0, 6.0, 1120.0, 0.0, 47.3735, -122.43, 1540.0, 9750.0, 34.0, 0.0, 0.0] | [309800.75] | 0 |
506 rows × 4 columns
Scheduled Tasks
Scheduled tasks are run with the Orchestration run_scheduled
method. We’ll set it up to run every 5 minutes, then check the results.
It is recommended that orchestrations that have pipeline deploy or undeploy commands be spaced out no less than 5 minutes to prevent colliding with other tasks that use the same pipeline.
task_start = datetime.datetime.now()
schedule = "*/5 * * * *"
task_scheduled = orchestration.run_scheduled(name="schedule example",
timeout=600,
schedule=schedule,
json_args={"workspace_name": workspace_name,
"pipeline_name": pipeline_name,
"connection_name": connection_name
})
while task_scheduled.status() != "started":
display(task_scheduled.status())
time.sleep(5)
task_scheduled
'pending'
Field | Value |
---|---|
ID | 4af57c61-dfa9-43eb-944e-559135495df4 |
Name | schedule example |
Last Run Status | unknown |
Type | Scheduled Run |
Active | True |
Schedule | */5 * * * * |
Created At | 2023-22-May 20:08:25 |
Updated At | 2023-22-May 20:08:25 |
time.sleep(420)
recent_run = task_scheduled.last_runs()[0]
display(recent_run.logs())
2023-22-May 20:11:02 Getting the workspace orchestrationworkspacetgiq
2023-22-May 20:11:02 Getting the pipeline orchestrationpipelinetgiq
2023-22-May 20:11:02 Inference time. Displaying results after.
2023-22-May 20:11:02 Getting arrow table file
2023-22-May 20:11:02 pyarrow.Table
2023-22-May 20:11:02 time: timestamp[ms]
2023-22-May 20:11:02 in.tensor: list not null
2023-22-May 20:11:02 child 0, item: float
2023-22-May 20:11:02 out.variable: list not null
2023-22-May 20:11:02 child 0, inner: float not null
2023-22-May 20:11:02 check_failures: int8
2023-22-May 20:11:02 ----
2023-22-May 20:11:02 time: [[2023-05-22 20:10:23.271,2023-05-22 20:10:23.271,2023-05-22 20:10:23.271,2023-05-22 20:10:23.271,2023-05-22 20:10:23.271,...,2023-05-22 20:10:23.271,2023-05-22 20:10:23.271,2023-05-22 20:10:23.271,2023-05-22 20:10:23.271,2023-05-22 20:10:23.271]]
2023-22-May 20:11:02 in.tensor: [[[4,2.5,2900,5505,2,...,2970,5251,12,0,0],[2,2.5,2170,6361,1,...,2310,7419,6,0,0],...,[3,1.75,2910,37461,1,...,2520,18295,47,0,0],[3,2,2005,7000,1,...,1750,4500,34,0,0]]]
2023-22-May 20:11:02 check_failures: [[0,0,0,0,0,...,0,0,0,0,0]]
2023-22-May 20:11:02 out.variable: [[[718013.75],[615094.56],...,[706823.56],[581003]]]
Kill a Task
Killing a task removes the schedule or removes it from a service. Tasks are killed with the Task kill()
method, and returns a message with the status of the kill procedure.
If necessary, all tasks can be killed through the following script.
- IMPORTANT NOTE: This command will kill all running tasks - scheduled or otherwise. Only use this if required.
# Kill all tasks
for t in wl.list_tasks(): t.kill()
When listed with Wallaroo client task_list(killed=True)
, the field active
displays tasks that are killed (False
) or either completed running or still scheduled to run (True
).
task_scheduled.kill()
<ArbexStatus.PENDING_KILL: 'pending_kill'>
wl.list_tasks(killed=True)
id | name | last run status | type | active | schedule | created at | updated at |
---|---|---|---|---|---|---|---|
4af57c61-dfa9-43eb-944e-559135495df4 | schedule example | success | Scheduled Run | False | */5 * * * * | 2023-22-May 20:08:25 | 2023-22-May 20:13:12 |
dc185e24-cf89-4a97-b6f0-33fc3d67da72 | schedule example | unknown | Scheduled Run | False | */5 * * * * | 2023-22-May 20:05:47 | 2023-22-May 20:06:22 |
f0e27d6a-6a98-4d26-b240-266f08560c48 | house price run once 2 | success | Temporary Run | True | - | 2023-22-May 19:58:32 | 2023-22-May 19:58:38 |
36509ef8-98da-42a0-913f-e6e929dedb15 | house price run once | success | Temporary Run | True | - | 2023-22-May 19:56:37 | 2023-22-May 19:56:48 |
Cleaning Up
With the tutorial complete we will undeploy the pipeline and ensure the resources are returned back to the Wallaroo instance.
pipeline.undeploy()
8 - Model Conversion Tutorials
Wallaroo pipelines support the ONNX standard. The following guides offer tips on converting a ML model to ONNX.
See the Wallaroo ML Models Upload and Registrations tutorials for guides on uploading different model frameworks into a Wallaroo instance.
These sample guides and their machine language models can be downloaded from the Wallaroo Tutorials Repository.
8.1 - PyTorch to ONNX Outside Wallaroo
This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.
How to Convert PyTorch to ONNX
The following tutorial is a brief example of how to convert a PyTorth (aka sk-learn) ML model to ONNX. This allows organizations that have trained sk-learn models to convert them and use them with Wallaroo.
This tutorial assumes that you have a Wallaroo instance and are running this Notebook from the Wallaroo Jupyter Hub service. This sample code is based on the guide Convert your PyTorch model to ONNX.
This tutorial provides the following:
pytorchbikeshare.pt
: a RandomForestRegressor PyTorch model. This model has a total of 58 inputs, and uses the classBikeShareRegressor
.
Prerequisites
- An installed Wallaroo instance.
- The following Python libraries installed:
os
torch
Conversion Process
Libraries
The first step is to import our libraries we will be using. For this example, the PyTorth torch
library will be imported into this kernel.
# the Pytorch libraries
# Import into this kernel
import torch
import torch.onnx
Load the Model
To load a PyTorch model into a variable, the model’s class
has to be defined. For out example we are using the BikeShareRegressor
class as defined below.
class BikeShareRegressor(torch.nn.Module):
def __init__(self):
super(BikeShareRegressor, self).__init__()
self.net = nn.Sequential(nn.Linear(input_size, l1),
torch.nn.ReLU(),
torch.nn.Dropout(p=dropout),
nn.BatchNorm1d(l1),
nn.Linear(l1, l2),
torch.nn.ReLU(),
torch.nn.Dropout(p=dropout),
nn.BatchNorm1d(l2),
nn.Linear(l2, output_size))
def forward(self, x):
return self.net(x)
Now we will load the model into the variable pytorch_tobe_converted
.
# load the Pytorch model
model = torch.load("./pytorch_bikesharingmodel.pt")
Convert_ONNX Inputs
Now we will define our method Convert_ONNX()
which has the following inputs:
PyTorchModel: the PyTorch we are converting.
modelInputs: the model input or tuple for multiple inputs.
onnxPath: The location to save the onnx file.
opset_version: The ONNX version to export to.
input_names: Array of the model’s input names.
output_names: Array of the model’s output names.
dynamic_axes: Sets variable length axes in the format, replacing the
batch_size
as necessary:{'modelInput' : { 0 : 'batch_size'}, 'modelOutput' : {0 : 'batch_size'}}
export_params: Whether to store the trained parameter weight inside the model file. Defaults to
True
.do_constant_folding: Sets whether to execute constant folding for optimization. Defaults to
True
.
#Function to Convert to ONNX
def Convert_ONNX():
# set the model to inference mode
model.eval()
# Export the model
torch.onnx.export(model, # model being run
dummy_input, # model input (or a tuple for multiple inputs)
pypath, # where to save the model
export_params=True, # store the trained parameter weights inside the model file
opset_version=15, # the ONNX version to export the model to
do_constant_folding=True, # whether to execute constant folding for optimization
input_names = ['modelInput'], # the model's input names
output_names = ['modelOutput'], # the model's output names
dynamic_axes = {'modelInput' : {0 : 'batch_size'}, 'modelOutput' : {0 : 'batch_size'}} # variable length axes
)
print(" ")
print('Model has been converted to ONNX')
Convert the Model
We’ll now set our variables and run our conversion. For out example, the input_size
is known to be 58, and the device
value we’ll derive from torch.cuda
. We’ll also set the ONNX version for exporting to 15.
pypath = "pytorchbikeshare.onnx"
input_size = 58
if torch.cuda.is_available():
device = 'cuda'
else:
device = 'cpu'
onnx_opset_version = 15
# Set up some dummy input tensor for the model
dummy_input = torch.randn(1, input_size, requires_grad=True).to(device)
Convert_ONNX()
================ Diagnostic Run torch.onnx.export version 2.0.0 ================
verbose: False, log level: Level.ERROR
======================= 0 NONE 0 NOTE 0 WARNING 0 ERROR ========================
Model has been converted to ONNX
Conclusion
And now our conversion is complete. Please feel free to use this sample code in your own projects.
8.2 - XGBoost Convert to ONNX
This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.
How to Convert XGBoost to ONNX
The following tutorial is a brief example of how to convert a XGBoost ML model to the ONNX standard. This allows organizations that have trained XGBoost models to convert them and use them with Wallaroo.
This tutorial assumes that you have a Wallaroo instance and are running this Notebook from the Wallaroo Jupyter Hub service.
This tutorial provides the following:
housing_model_xgb.pkl
: A pretrained model used as part of the Notebooks in Production tutorial. This model has a total of 18 columns.
Conversion Process
Libraries
The first step is to import our libraries we will be using.
import onnx
import pickle
from onnxmltools.convert import convert_xgboost
from skl2onnx.common.data_types import FloatTensorType, DoubleTensorType
Set Variables
The following variables are required to be known before the process can be started:
- number of columns: The number of columns used by the model.
- TARGET_OPSET: Verify the TARGET_OPSET value taht will be used in the conversion process matches the current Wallaroo model uploads requirements.
# set the number of columns
ncols = 18
TARGET_OPSET = 15
Load the XGBoost Model
Next we will load our model that has been saved in the pickle
format and unpickle it.
# load the xgboost model
with open("housing_model_xgb.pkl", "rb") as f:
xgboost_model = pickle.load(f)
Conversion Inputs
The convert_xgboost
method has the following format and requires the following inputs:
convert_xgboost({XGBoost Model},
{XGBoost Model Type},
[
('input',
{Tensor Data Type}([None, {ncols}]))
],
target_opset={TARGET_OPSET})
- XGBoost Model: The XGBoost Model to convert.
- XGBoost Model Type: The type of XGBoost model. In this example is it a
tree-based classifier
. - Tensor Data Type: Either
FloatTensorType
orDoubleTensorType
from theskl2onnx.common.data_types
library. - ncols: Number of columns in the model.
- TARGET_OPSET: The target opset which can be derived in code showed below.
Convert the Model
With all of our data in place we can now convert our XBBoost model to ONNX using the convert_xgboost
method.
onnx_model_converted = convert_xgboost(xgboost_model, 'tree-based classifier',
[('input', FloatTensorType([None, ncols]))],
target_opset=TARGET_OPSET)
Save the Model
With the model converted to ONNX, we can now save it and use it in a Wallaroo pipeline.
onnx.save_model(onnx_model_converted, "housing_model_xgb.onnx")
9 - Wallaroo Tools Tutorials
The following tutorials are provided by Wallaroo to demonstrate different tools and techniques for working with MLModels and data with Wallaroo.
9.1 - Wallaroo JSON Inference Data to DataFrame and Arrow Tutorials
The following guide on using inference data inputs from Wallaroo proprietary JSON to either Pandas DataFrame or Apache Arrow downloaded as part of the Wallaroo Tutorials repository.
Introduction
The following guide is to help users transition from using Wallaroo Proprietary JSON to Pandas DataFrame and Apache Arrow. The latter two formats allow data scientists to work natively with DataFrames, and when ready convert those into Arrow table files which provides greater file size efficiency and overall speed.
Using Pandas DataFrames for inference inputs requires typecasting into the Wallaroo inference engine. For models that are sensitive to data types, Arrow is the preferred format.
This guide will demonstrate the following:
- Convert Wallaroo Proprietary JSON to Pandas DataFrame: Converting from Wallaroo Proprietary JSON to Pandas DataFrame used for inferences in the Wallaroo Engine.
- Convert Wallaroo JSON File to Pandas DataFrame: Converting from Wallaroo JSON files or DataFrame JSON files to Pandas DataFrame for Wallaroo Engine inferences.
- Convert Pandas DataFrame to Arrow Table: Converting from Pandas DataFrame to Apache Arrow used for inferences in the Wallaroo Engine.
- Read Arrow File to DataFrame: How to convert from an Arrow binary file to an Apache Arrow object or DataFrame object.
- Convert Flattened Arrow Table to Multi-Dimensional Pandas DataFrame: How to convert from a one dimensional arrow table to a Multi-Dimensional Pandas DataFrame.
Prerequisites
The demonstration code assumes a Wallaroo instance with Arrow support enabled and provides the following:
ccfraud.onnx
: Sample trained ML Model trained to detect credit card fraud transactions.data/high_fraud.json
: Sample inference input formatted in the Wallaroo proprietary JSON format.
The following demonstrates how to convert Wallaroo Proprietary JSON to Pandas DataFrame. This example data and models are taken from the Wallaroo 101, which uses the CCFraud model examples.
Initialization
Connect to the Wallaroo Instance
Connect to the Wallaroo Instance
The first step is to connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.
This is accomplished using the wallaroo.Client()
command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.
If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client()
. For more information on Wallaroo Client settings, see the Client Connection guide.
import wallaroo
from wallaroo.object import EntityNotFoundError
from IPython.display import display
# used to display dataframe information without truncating
from IPython.display import display
import pandas as pd
pd.set_option('display.max_colwidth', None)
# Login through local Wallaroo instance
wl = wallaroo.Client()
workspace_name = 'inferencedataexamplesworkspace'
pipeline_name = 'inferencedataexamplespipeline'
model_name = 'ccfraud'
model_file_name = './ccfraud.onnx'
def get_workspace(name):
workspace = None
for ws in wl.list_workspaces():
if ws.name() == name:
workspace= ws
if(workspace == None):
workspace = wl.create_workspace(name)
return workspace
def get_pipeline(name):
try:
pipeline = wl.pipelines_by_name(name)[0]
except EntityNotFoundError:
pipeline = wl.build_pipeline(name)
return pipeline
# Create the workspace
workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
# upload the model
model = wl.upload_model(model_name, model_file_name, framework=wallaroo.framework.Framework.ONNX).configure()
# Create the pipeline then deploy it
pipeline = get_pipeline(pipeline_name)
pipeline.add_model_step(model).deploy()
name | inferencedataexamplespipeline |
---|---|
created | 2023-05-18 14:06:03.460084+00:00 |
last_updated | 2023-05-18 14:06:04.140319+00:00 |
deployed | True |
tags | |
versions | ad9c68c5-c692-486f-8796-a8e8561ea320, 65b69beb-a214-44ea-99fb-d60bca8d5804 |
steps | ccfraud |
Convert Wallaroo Proprietary JSON to Pandas DataFrame
The following demonstrates how to convert Wallaroo Proprietary JSON to Pandas DataFrame.
Load Libraries
The following libraries are used as part of the conversion process.
import pandas as pd
import pyarrow as pa
import json
import datetime
import numpy as np
Load Wallaroo Data
The Wallaroo data will be saved to a variable. This sample input when run through the trained model as an inference returns a high probability of fraud.
# Start with the single example
high_fraud_data = {
"tensor": [
[1.0678324729342086,
18.155556397512136,
-1.658955105843852,
5.2111788045436445,
2.345247064454334,
10.467083577773014,
5.0925820522419745,
12.829515363712181,
4.953677046849403,
2.3934736228338225,
23.912131817957253,
1.7599568310350207,
0.8561037518143335,
1.1656456468728567,
0.5395988813934498,
0.7784221343010385,
6.75806107274245,
3.927411847659908,
12.462178276650056,
12.307538216518655,
13.787951906620115,
1.4588397511627804,
3.681834686805714,
1.7539143660379741,
8.484355003656184,
14.6454097666836,
26.852377436250144,
2.716529237720336,
3.061195706890285]
]
}
Convert to DataFrame
The Wallaroo proprietary JSON file will now be converted into Pandas DataFrame.
high_fraud_dataframe = pd.DataFrame.from_records(high_fraud_data)
display(high_fraud_dataframe)
tensor | |
---|---|
0 | [1.0678324729342086, 18.155556397512136, -1.658955105843852, 5.2111788045436445, 2.345247064454334, 10.467083577773014, 5.0925820522419745, 12.829515363712181, 4.953677046849403, 2.3934736228338225, 23.912131817957253, 1.7599568310350207, 0.8561037518143335, 1.1656456468728567, 0.5395988813934498, 0.7784221343010385, 6.75806107274245, 3.927411847659908, 12.462178276650056, 12.307538216518655, 13.787951906620115, 1.4588397511627804, 3.681834686805714, 1.7539143660379741, 8.484355003656184, 14.6454097666836, 26.852377436250144, 2.716529237720336, 3.061195706890285] |
DataFrame for Inferences
Once converted, the DataFrame version of the data can be used for inferences in an Arrow enabled Wallaroo instance.
# Use this dataframe to infer
result = pipeline.infer(high_fraud_dataframe)
display(result)
time | in.tensor | out.dense_1 | check_failures | |
---|---|---|---|---|
0 | 2023-05-18 14:06:20.449 | [1.0678324729, 18.1555563975, -1.6589551058, 5.2111788045, 2.3452470645, 10.4670835778, 5.0925820522, 12.8295153637, 4.9536770468, 2.3934736228, 23.912131818, 1.759956831, 0.8561037518, 1.1656456469, 0.5395988814, 0.7784221343, 6.7580610727, 3.9274118477, 12.4621782767, 12.3075382165, 13.7879519066, 1.4588397512, 3.6818346868, 1.753914366, 8.4843550037, 14.6454097667, 26.8523774363, 2.7165292377, 3.0611957069] | [0.981199] | 0 |
Pandas JSON to Pandas DataFrame
For JSON data that is in the Pandas DataFrame format, the data can be turned into a Pandas DataFrame object through the same method. Note that the original variable is JSON, which could have come from a file, to a DataFrame object.
high_fraud_dataframe_json = [
{
"tensor":[
1.0678324729,
18.1555563975,
-1.6589551058,
5.2111788045,
2.3452470645,
10.4670835778,
5.0925820522,
12.8295153637,
4.9536770468,
2.3934736228,
23.912131818,
1.759956831,
0.8561037518,
1.1656456469,
0.5395988814,
0.7784221343,
6.7580610727,
3.9274118477,
12.4621782767,
12.3075382165,
13.7879519066,
1.4588397512,
3.6818346868,
1.753914366,
8.4843550037,
14.6454097667,
26.8523774363,
2.7165292377,
3.0611957069
]
}
]
# Infer from the JSON
high_fraud_from_dataframe_json = pd.DataFrame.from_records(high_fraud_dataframe_json)
display(high_fraud_from_dataframe_json)
tensor | |
---|---|
0 | [1.0678324729, 18.1555563975, -1.6589551058, 5.2111788045, 2.3452470645, 10.4670835778, 5.0925820522, 12.8295153637, 4.9536770468, 2.3934736228, 23.912131818, 1.759956831, 0.8561037518, 1.1656456469, 0.5395988814, 0.7784221343, 6.7580610727, 3.9274118477, 12.4621782767, 12.3075382165, 13.7879519066, 1.4588397512, 3.6818346868, 1.753914366, 8.4843550037, 14.6454097667, 26.8523774363, 2.7165292377, 3.0611957069] |
# Use this dataframe to infer
results = pipeline.infer(high_fraud_from_dataframe_json)
display(results)
time | in.tensor | out.dense_1 | check_failures | |
---|---|---|---|---|
0 | 2023-05-18 14:06:20.877 | [1.0678324729, 18.1555563975, -1.6589551058, 5.2111788045, 2.3452470645, 10.4670835778, 5.0925820522, 12.8295153637, 4.9536770468, 2.3934736228, 23.912131818, 1.759956831, 0.8561037518, 1.1656456469, 0.5395988814, 0.7784221343, 6.7580610727, 3.9274118477, 12.4621782767, 12.3075382165, 13.7879519066, 1.4588397512, 3.6818346868, 1.753914366, 8.4843550037, 14.6454097667, 26.8523774363, 2.7165292377, 3.0611957069] | [0.981199] | 0 |
Convert Wallaroo JSON File to Pandas DataFrame
When working with files containing Wallaroo JSON data, these can be imported from their original JSON, then converted to a Pandas DataFrame object with the pandas method read_json
.
high_fraud_filename = "./data/high_fraud.json"
high_fraud_data_from_file = pd.read_json(high_fraud_filename, orient="records")
display(high_fraud_data_from_file)
tensor | |
---|---|
0 | [1.067832472934208, 18.155556397512136, -1.658955105843852, 5.2111788045436445, 2.345247064454334, 10.467083577773014, 5.092582052241974, 12.829515363712181, 4.953677046849403, 2.393473622833822, 23.912131817957253, 1.7599568310350202, 0.8561037518143331, 1.165645646872856, 0.539598881393449, 0.778422134301038, 6.75806107274245, 3.927411847659908, 12.462178276650056, 12.307538216518655, 13.787951906620115, 1.45883975116278, 3.681834686805714, 1.7539143660379741, 8.484355003656184, 14.6454097666836, 26.852377436250144, 2.7165292377203363, 3.061195706890285] |
The data can be used in an inference either with the infer
method on the DataFrame object, or directly from the file. Note that in either case, the returned object is a DataFrame.
# Use this dataframe to infer
result = pipeline.infer(high_fraud_data_from_file)
display(result)
time | in.tensor | out.dense_1 | check_failures | |
---|---|---|---|---|
0 | 2023-05-18 14:06:21.259 | [1.0678324729, 18.1555563975, -1.6589551058, 5.2111788045, 2.3452470645, 10.4670835778, 5.0925820522, 12.8295153637, 4.9536770468, 2.3934736228, 23.912131818, 1.759956831, 0.8561037518, 1.1656456469, 0.5395988814, 0.7784221343, 6.7580610727, 3.9274118477, 12.4621782767, 12.3075382165, 13.7879519066, 1.4588397512, 3.6818346868, 1.753914366, 8.4843550037, 14.6454097667, 26.8523774363, 2.7165292377, 3.0611957069] | [0.981199] | 0 |
# Infer from file - it is read as a Pandas DataFrame object from the DataFrame JSON
result = pipeline.infer_from_file(high_fraud_filename)
display(result)
time | in.tensor | out.dense_1 | check_failures | |
---|---|---|---|---|
0 | 2023-05-18 14:06:21.652 | [1.0678324729, 18.1555563975, -1.6589551058, 5.2111788045, 2.3452470645, 10.4670835778, 5.0925820522, 12.8295153637, 4.9536770468, 2.3934736228, 23.912131818, 1.759956831, 0.8561037518, 1.1656456469, 0.5395988814, 0.7784221343, 6.7580610727, 3.9274118477, 12.4621782767, 12.3075382165, 13.7879519066, 1.4588397512, 3.6818346868, 1.753914366, 8.4843550037, 14.6454097667, 26.8523774363, 2.7165292377, 3.0611957069] | [0.981199] | 0 |
Convert Pandas DataFrame to Arrow Table
The helper file convert_wallaroo_data.py
is used to convert from Pandas DataFrame to an Arrow Table with the following caveats:
Arrow requires the user to specify the exact datatypes of the array elements before passing the data to the engine. If you are aware of what data type the model expects, create a dictionary with column names as key and data type as the value and pass it as a param in place of data_type_dict
. If not, the convert_to_pa_dtype
function will try and guess the equivalent pyarrow data type and use it (this may or may not work as intended).
import convert_wallaroo_data
data_type_dict = {"tensor": pa.float32()}
pa_table = convert_wallaroo_data.convert_pandas_to_arrow(high_fraud_dataframe, data_type_dict)
pa_table
pyarrow.Table
tensor: fixed_size_list<item: float>[29]
child 0, item: float
----
tensor: [[[1.0678325,18.155556,-1.6589551,5.211179,2.345247,...,8.484355,14.64541,26.852377,2.7165291,3.0611956]]]
An inference can be done using the arrow table. The following shows the code sample and result. Note that when submitting an Arrow table to infer
, that the returned object is an Arrow table.
# use the arrow table for infer:
result = pipeline.infer(pa_table)
display(result)
pyarrow.Table
time: timestamp[ms]
in.tensor: list<item: float> not null
child 0, item: float
out.dense_1: list<inner: float not null> not null
child 0, inner: float not null
check_failures: int8
----
time: [[2023-05-18 14:06:22.107]]
in.tensor: [[[1.0678325,18.155556,-1.6589551,5.211179,2.345247,...,8.484355,14.64541,26.852377,2.7165291,3.0611956]]]
out.dense_1: [[[0.981199]]]
check_failures: [[0]]
Save Arrow Table to Arrow File
The converted Arrow table can be saved using the pyarrow
library.
arrow_file_name = "./data/high_fraud.arrow"
with pa.OSFile(arrow_file_name, 'wb') as sink:
with pa.ipc.new_file(sink, pa_table.schema) as arrow_ipc:
arrow_ipc.write(pa_table)
arrow_ipc.close()
infer_from_file
can be performed using the new arrow
file. Note again that when submitting an inference with an Arrow object, the returning value is an Arrow object.
result = pipeline.infer_from_file(arrow_file_name)
display(result)
pyarrow.Table
time: timestamp[ms]
in.tensor: list<item: float> not null
child 0, item: float
out.dense_1: list<inner: float not null> not null
child 0, inner: float not null
check_failures: int8
----
time: [[2023-05-18 14:06:22.522]]
in.tensor: [[[1.0678325,18.155556,-1.6589551,5.211179,2.345247,...,8.484355,14.64541,26.852377,2.7165291,3.0611956]]]
out.dense_1: [[[0.981199]]]
check_failures: [[0]]
Read Arrow File to DataFrame
The data can go the opposite direction - reading from an Arrow binary file, and turning the data into either an Arrow table with the Arrow read_all
method, or just the data into a DataFrame with the Arrow read_pandas
method.
with pa.ipc.open_file(arrow_file_name) as source:
table = source.read_all() # to get pyarrow table
table_df = source.read_pandas() # to get pandas dataframe
display(table)
display(table_df)
pyarrow.Table
tensor: fixed_size_list<item: float>[29]
child 0, item: float
----
tensor: [[[1.0678325,18.155556,-1.6589551,5.211179,2.345247,...,8.484355,14.64541,26.852377,2.7165291,3.0611956]]]
tensor | |
---|---|
0 | [1.0678325, 18.155556, -1.6589551, 5.211179, 2.345247, 10.467084, 5.092582, 12.829515, 4.953677, 2.3934736, 23.912132, 1.7599568, 0.8561038, 1.1656456, 0.5395989, 0.7784221, 6.758061, 3.9274118, 12.462178, 12.307538, 13.787951, 1.4588398, 3.6818347, 1.7539144, 8.484355, 14.64541, 26.852377, 2.7165291, 3.0611956] |
Convert Arrow Infer to DataFrame
When an infer result is returned as an Arrow object, it can be converted to a DataFrame for easy viewing.
result = pipeline.infer_from_file(arrow_file_name)
display(result.to_pandas())
time | in.tensor | out.dense_1 | check_failures | |
---|---|---|---|---|
0 | 2023-05-18 14:06:22.910 | [1.0678325, 18.155556, -1.6589551, 5.211179, 2.345247, 10.467084, 5.092582, 12.829515, 4.953677, 2.3934736, 23.912132, 1.7599568, 0.8561038, 1.1656456, 0.5395989, 0.7784221, 6.758061, 3.9274118, 12.462178, 12.307538, 13.787951, 1.4588398, 3.6818347, 1.7539144, 8.484355, 14.64541, 26.852377, 2.7165291, 3.0611956] | [0.981199] | 0 |
Convert Flattened Arrow Table to Multi-Dimensional Pandas DataFrame
Some ML models use multi-dimensional DataFrames. Currently Wallaroo supports and outputs flattened tables for inferences.
For situations where the original data was in a multi-dimensional DataFrame, the following procedure will convert the flattened Arrow table back into a desired multi-dimensional pandas DataFrame.
Here is a sample infer result data in Arrow Table format.
time_array = pa.array([datetime.datetime(2023, 2 , 22, 22, 14)])
in_tensor_array = pa.array([[1.5997242884551583,-0.72885535293112,-0.8464381472712799,-0.48041787186839674,0.8211244519635765,0.999086254697715,-1.365979802921807,0.36611200379560294,1.27093766309002,0.4895466723195178]])
out_array = pa.array([[1.8749652,-0.94025564,-1.0790397,-0.72123086,0.90895796,1.092086,-1.2834015,0.340406,1.2441622,0.57471186]])
check_failures_array = pa.array([0])
names = ["time", "in.tensor", "out.reshape", "check_failures"]
flattened_2d_table = pa.Table.from_arrays([time_array, in_tensor_array, out_array, check_failures_array], names = names)
flattened_2d_table
pyarrow.Table
time: timestamp[us]
in.tensor: list<item: double>
child 0, item: double
out.reshape: list<item: double>
child 0, item: double
check_failures: int64
----
time: [[2023-02-22 22:14:00.000000]]
in.tensor: [[[1.5997242884551583,-0.72885535293112,-0.8464381472712799,-0.48041787186839674,0.8211244519635765,0.999086254697715,-1.365979802921807,0.36611200379560294,1.27093766309002,0.4895466723195178]]]
out.reshape: [[[1.8749652,-0.94025564,-1.0790397,-0.72123086,0.90895796,1.092086,-1.2834015,0.340406,1.2441622,0.57471186]]]
check_failures: [[0]]
flattened_2d_table["out.reshape"]
<pyarrow.lib.ChunkedArray object at 0x154467a40>
[
[
[
1.8749652,
-0.94025564,
-1.0790397,
-0.72123086,
0.90895796,
1.092086,
-1.2834015,
0.340406,
1.2441622,
0.57471186
]
]
]
Verify the Shape
Let’s suppose the shape of the output that natively comes out of the model is [2,5]. We can use that to make sure the shape is correct when translating from the 1 dimensional Arrow table.
tensor_type = {"shape": [2, 5]}
output_df = flattened_2d_table.to_pandas()['out.reshape']
# numpy array, shape [N, 2, 5]
# In this case N = 1
output_list = [elt.reshape(tensor_type['shape']) for elt in output_df]
output_tensor = np.stack(output_list)
output_tensor
array([[[ 1.8749652 , -0.94025564, -1.0790397 , -0.72123086,
0.90895796],
[ 1.092086 , -1.2834015 , 0.340406 , 1.2441622 ,
0.57471186]]])
output_2d_df = pd.DataFrame(output_tensor.tolist())
output_2d_df
0 | 1 | |
---|---|---|
0 | [1.8749652, -0.94025564, -1.0790397, -0.72123086, 0.90895796] | [1.092086, -1.2834015, 0.340406, 1.2441622, 0.57471186] |
Undeploy Pipeline
The pipeline will now be undeployed to return the resources back to the Wallaroo instance.
pipeline.undeploy()
name | inferencedataexamplespipeline |
---|---|
created | 2023-05-18 14:06:03.460084+00:00 |
last_updated | 2023-05-18 14:06:04.140319+00:00 |
deployed | False |
tags | |
versions | ad9c68c5-c692-486f-8796-a8e8561ea320, 65b69beb-a214-44ea-99fb-d60bca8d5804 |
steps | ccfraud |