MLOps: Model serving
Machine learning projects do not end once a model is trained. In fact, the ultimate goal is to integrate the model into a real-world application where it can make predictions on-demand. In this post, we delve into model serving—the crucial stage where models transition from offline development into systems that deliver predictions with speed, scale, and cost efficiency. Whether you are just starting or you are looking to optimize advanced ML pipelines, this guide will help you understand the various strategies and trade-offs.
What is Model Serving?
Model serving is the process of making machine learning models available for prediction requests. It involves exposing a trained model as a service that can be called—either in real time or in batch mode—to produce predictions on new input data. Essentially, model serving completes the ML lifecycle by bridging the gap between offline training and real-time inference.
A key diagram from our presentation illustrates the lifecycle:
- Request: How a request reaches the model, whether via a service endpoint in the cloud, or directly on an edge device, subject to network delays and data queuing.
- Model & Data: The model processes incoming data, producing predictions.
- Response: The output is either sent back immediately (online) or cached for batch usage later (citeturn0file0).
This end-to-end process is critical for many applications like personalized recommendations, image recognition in mobile devices, or autonomous driving, where latency and efficiency are paramount.
Types of Predictions: Batch vs. Online
Understanding the prediction use case is fundamental when designing model serving architectures. There are two main types:
Batch Predictions
- When to Use:
- Data is available in advance.
- The system can prepare predictions ahead of time.
- Advantages:
- High throughput processing; predictions can be computed en masse.
- Challenges:
- Risk of wasted compute if not all pre-computed predictions are needed.
- Latency is less critical since predictions are computed in advance.
Online Predictions
- When to Use:
- Real-time requests from users or autonomous systems.
- Advantages:
- Immediate response to requests.
- Challenges:
- Stricter requirements on latency.
- Networking conditions (e.g., cloud-based serving) can introduce delays.
Each prediction style impacts the serving design—from resource allocation to the underlying architectures that support low latency or high throughput.
Deployment Environments: Cloud and Edge
How you deploy your model serving solution significantly impacts its performance:
- Cloud Deployment:
- Centralized, scales easily, but subject to network latency.
- Ideal for complex models that require higher compute power.
- Edge Deployment:
- Runs directly on devices (smartphones, IoT devices) with reduced network latency.
- Must be optimized for limited compute resources and lower memory footprints.
In many systems, the challenge is finding the right balance: delivering the necessary computational power while minimizing delays that arise from network conditions. Our PPT clearly contrasts these deployment models and emphasizes that the design and optimizations vary based on where the model lives.
Core Goals of Model Serving
When designing any model serving system, several performance goals must be balanced:
- Low Latency:
- Crucial for real-time applications where users or systems wait for an immediate response.
- High Throughput:
- Important when processing large volumes of batch predictions or handling multiple concurrent requests.
- Low Cost:
- Efficiency in resource allocation translates directly to lower operational costs.
- Accuracy:
- The model must deliver precise and reliable predictions.
Often, improvements in one area may come at the expense of another, making trade-offs inevitable. A robust serving solution finds a sweet spot by carefully considering these objectives.
Model-Level Optimizations
Optimizing the model itself is the first step to a responsive serving system. These optimizations are typically applied after your model is trained but before it’s deployed.
Foundation Model Choice
The selection of the model itself—often referred to as the foundation model—is critical:
- Smaller Models:
- Models like MobileNet and YOLO are designed for speed and efficiency.
- They deliver faster inference times at the potential expense of some accuracy.
- Design for Speed:
- Specialized architectures inherently optimize for lower latency, making them ideal for time-sensitive applications.
This decision has the most significant impact on both latency and throughput.
Graph Compilation and Optimizations
Modern ML frameworks allow you to compile your model into an intermediate representation (IR) or computational graph, which can then be optimized:
- Eager Mode to Graph Mode Conversion:
- During development, models are often written in an “eager” mode (more readable but less efficient). For deployment, converting to a static or optimized graph helps in applying low-level optimizations.
- This conversion can be done either Just-In-Time (JIT), which incurs some delay on the first call, or Ahead-Of-Time (AOT), which aims to optimize performance from the start.
- Graph Optimizations:
-
Eliminating Operations on Constants:
Unused operations, such as those present solely for training (e.g., dropout), can be removed to streamline inference. -
Fusing Operations:
Rather than executing operations sequentially (e.g., a series of additions and multiplications), they can be “fused” into a single operation. This reduces overhead and improves memory usage (citeturn0file0). -
Transforming Primitives:
Converting operations from a general-purpose implementation to one specifically optimized for the target hardware (like GPU or specialized accelerators) is another powerful optimization. For instance, specific transforms may leverage specialized instructions in CUDA for optimized matrix multiplication. -
Memory Layout Considerations:
Optimizations that adjust memory layout to improve cache utilization and access patterns, particularly on edge devices with limited memory, can significantly improve performance.
-
These optimizations not only speed up inference but also lower the resource utilization, which is critical when deploying models in resource-constrained environments.
Quantization, Pruning, and Distillation
Beyond graph-level adjustments, further model-level improvements include:
- Quantization:
-
Post-Training Quantization:
This involves converting model weights from high precision (e.g., float32) to lower precision (e.g., int8), often with a calibration phase using representative data. Both static and dynamic quantization schemes exist. -
Quantization Aware Training:
The model is trained with quantization in mind from the beginning, leading to smoother performance transitions when the model is later deployed in a quantized form. -
Benefits include reduced memory footprint and faster inference, especially on devices that support low-precision arithmetic.
-
- Pruning:
- Techniques that remove redundant or less important parameters from the model. This not only speeds up inference but may also reduce overfitting in some cases.
- Knowledge Distillation:
- A process where a “student” model is trained to mimic the outputs of a larger, more complex “teacher” model. The result is a lighter model that retains much of the accuracy of the teacher, while being faster and easier to deploy.
Each of these techniques is a powerful tool in your optimization arsenal, especially when balancing accuracy with latency and throughput.
System-Level Optimizations
Even a highly optimized model can be bottlenecked by system-level inefficiencies. Optimizations at this level aim to improve the broader serving infrastructure.
Cold vs. Warm Starts
Model serving platforms often contend with two types of initialization:
- Cold Start:
- This involves the complete startup sequence: downloading code and models, starting containers, and initializing the runtime.
- Cold starts are more time-consuming and may be acceptable when predictions are not needed immediately.
- Warm Start:
- Models remain loaded and ready to serve subsequent requests, reducing the initialization overhead.
The choice between cold and warm starts is a classic trade-off between resource utilization (and cost) versus latency.
Concurrent Model Execution
When serving predictions at scale, it’s essential to utilize your hardware optimally:
- Multiple Models, Multiple Instances:
- Running different models concurrently or instantiating parallel instances of the same model allows for more efficient use of resources.
- Load Balancing:
- Distributing prediction requests among several instances can help reduce wait times and mitigate the impact of any one slow process.
This concurrent execution strategy is particularly important in environments with fluctuating demand.
Dynamic and Static Batching
Batching is the practice of aggregating multiple requests into a single processing unit, which can greatly enhance throughput:
- Static Batching:
- Pre-defined batch sizes are used to accumulate requests. While efficient, this approach may delay responses if the batch isn’t filled quickly enough.
- Dynamic Batching:
- Requests are batched on the fly, with algorithms dynamically adjusting the batch size based on incoming traffic. This reduces latency but may introduce variable throughput, depending on the delay set for batching (citeturn0file0).
Balancing the trade-off between waiting for a full batch and delivering timely predictions is a critical system-level challenge.
Ensembling for Improved Performance
Ensembling combines the outputs of multiple models to improve prediction robustness and accuracy:
- Model Ensembles:
- Different models, or the same model running on separate instances, can be ensembled so that their combined output is more reliable than any single prediction.
- Image and Processing Pipelines:
- For instance, one model may process an image to extract features while another performs classification. Ensembling these predictions can lead to better overall performance.
This approach is particularly useful in scenarios where multiple perspectives (or predictions) are beneficial to the end application.
Real-World Deployment: Case Study of LyftLearn Serving
An effective model serving platform must meet the needs of diverse teams and multiple machine learning frameworks. An exemplary model serving solution like LyftLearn Serving was highlighted in our presentation (citeturn0file0).
Key Features Include:
- Multi-Framework Support:
- The platform can ingest models from various ML frameworks, ensuring flexibility as teams adopt new or specialized technologies.
- Team Isolation:
- It provides a mechanism to isolate models owned by different teams, preventing resource contention and interference. This isolation is vital in large organizations where multiple models may be deployed concurrently.
- Customizable Templates:
- ML teams can customize their deployments to suit the particular characteristics of their model or use case. This customization supports different optimization strategies—whether you are focusing on low latency for a user-facing application or high throughput for batch predictions.
Overall, LyftLearn Serving represents the trend toward unified, scalable, and flexible serving platforms that accommodate the rapid evolution of machine learning models and techniques.
Final Thoughts and Best Practices
Model serving is a critical aspect of the ML lifecycle that transforms theoretical models into practical, deployable services. The design of a serving system requires a delicate balance between speed, accuracy, cost, and resource constraints.
Key Takeaways:
- Understand Your Use Case:
- Decide whether you need batch or online predictions, and where your service will run (cloud vs. edge).
- Optimize at Multiple Levels:
- Start by choosing the right foundation model. Then, apply model-level optimizations such as graph compilation, operation fusing, quantization, pruning, and knowledge distillation.
- System-Level Strategies Matter:
- Design your infrastructure to handle cold and warm starts effectively, leverage concurrent model execution, and use dynamic batching to optimize throughput and latency.
- Deploy with Flexibility:
- Use platforms like LyftLearn Serving that can support multiple frameworks and provide customized deployment options, ensuring that your model serving solution scales with your organization’s needs.
The journey from developing a high-quality model to deploying a high-performing service involves numerous trade-offs and technical challenges. By mastering both model-level and system-level optimizations, you can build a robust serving infrastructure that meets the rigorous demands of modern applications.
Whether you are a beginner exploring the basics or an advanced practitioner tuning every component of your ML pipeline, keeping these principles in mind will help you design systems that are not only efficient but also scalable and maintainable.
Happy serving!
Scaling Real-Time Decisions: Advanced Model Serving Architectures and GPU Kernel Optimizations
Part I: Building Robust Real-Time Serving Platforms with LyftLearn Serving
The Challenge of Real-Time Decisions
Modern machine learning applications make hundreds of millions of decisions every day. At Lyft, for example, these decisions drive pricing optimization, driver incentives, fraud detection, and ETA predictions—all of which have a tangible impact on customer experience and operational efficiency. To enable this level of performance, a serving system must support very low latency, handle high throughput, and offer flexibility for diverse teams with vastly different requirements.
The Architecture of LyftLearn Serving
LyftLearn Serving is a decentralized, microservices-based platform that addresses these challenges head on. Its design balances ease of deployment with performance, ensuring that each model deployed for real-time inference runs in isolation, yet seamlessly integrates with a shared infrastructure.
Key Architectural Components
-
Microservice-Based Design:
LyftLearn Serving leverages the advantages of microservices. By deploying models as independent services—powered by an optimized HTTP serving library (built on Flask and fine-tuned for integration with Envoy and Gunicorn)—the platform achieves both scalability and resilience. Each service is managed independently, allowing teams to update or roll back changes without impacting others. - Core Serving Library & Custom Interfaces:
At its heart is a robust core library managing tasks such as model versioning, request handling, shadowing, and monitoring. ML modelers only need to implement two main interface functions:-
load()
: Responsible for deserializing and loading the model into memory. -
predict()
: Handles the feature processing and inference, acting as the entry point for real-time prediction requests.
-
-
Isolation and Ownership:
Lyft’s model serving environment enforces strict repository-level isolation, so each team’s code and deployment pipelines remain independent. This minimizes the impact of integration issues and ensures that service-level performance—such as resource allocation (CPU, memory, autoscaling parameters) and container orchestration within the Envoy service mesh and Kubernetes—is tailored to individual team needs. -
Configuration Generator:
To simplify onboarding, LyftLearn Serving includes a configuration generator that automates the creation of fully functional GitHub repositories. These generated repositories come pre-populated with working examples, configuration files (in formats like YAML and Terraform), and secrets, allowing ML modelers to focus solely on their inference code without delving deep into infrastructure minutiae. - Model Self-Tests and CI/CD Integration:
Recognizing that models in production must remain reliable despite frequent changes to underlying containers or libraries, the system enforces model self-tests. These tests run both at runtime (after each model load) and as part of the CI pipeline to ensure backward compatibility and correct behavior. By logging discrepancies and performance metrics, modelers are quickly alerted to any issues, leading to robust operational practices.
Lessons Learned and Best Practices
Lyft’s experience with LyftLearn Serving offers several takeaways:
- Comprehensive Documentation:
Clear, user-focused documentation (structured around tutorials, how-to guides, technical references, and community discussions) is essential for seamless team onboarding. - Operational Flexibility:
Balancing “seamless end-to-end UX for new customers” with “composable APIs for power users” is key. This often means making difficult trade-offs between ease of use and the ability to handle bespoke ML workflows. - Enduring Inference Requests:
Once a model is deployed, it is expected to serve requests indefinitely. Migrating or upgrading such models is a non-trivial task, emphasizing the need for a stable, well-documented, and decoupled infrastructure.
This part of our journey underscores the importance of high-level system design in achieving both flexibility and performance in real-time model serving.
Part II: Accelerating Model Inference – Advanced CUDA Kernel Optimizations
The Importance of Matrix Multiplication
At the very core of many deep learning inference pipelines lies matrix multiplication (GEMM). Whether it’s for fully connected layers, convolution operations, or transformer architectures, the performance of these operations significantly impacts the overall throughput and latency of ML models. In this section, we explore an iterative approach to optimizing a CUDA matrix multiplication kernel—showing how low-level hardware insights can dramatically boost performance.
The Iterative Journey from Naïve to Near cuBLAS Performance
The process begins with a naïve CUDA SGEMM kernel—where each thread calculates a single element of the result matrix by traversing the full range of the inner dimension. From this starting point, a series of optimizations is applied, each addressing specific performance bottlenecks:
1. Naïve Implementation & Baseline Establishment
- Description:
A simple kernel assigns each thread to one output matrix element, computing the dot product of a row from matrix A and a column from matrix B. - Bottlenecks:
Poor memory access patterns, excessive global memory (GMEM) traffic, and low arithmetic intensity result in only 1-2% of peak performance.
2. Global Memory Coalescing
- Goal:
Reduce the overhead of global memory accesses by ensuring that threads in a warp (a group of 32 threads) access consecutive memory addresses. - Impact:
This optimization increases memory throughput dramatically (e.g., from 15GB/s to 110GB/s) and boosts performance closer to 8-10% of cuBLAS levels.
3. Shared Memory Caching and Blocking
- Strategy:
Utilize the GPU’s on-chip shared memory (SMEM) to cache tiles of matrices A and B. Threads in a block collaboratively load these tiles into SMEM, then reuse them for multiple computations. - Benefit:
Reduces the number of global memory accesses and leverages the low-latency, high-bandwidth characteristics of shared memory, pushing performance closer to 12-13% of cuBLAS performance.
4. 1D and 2D Block Tiling: Increasing Arithmetic Intensity
- Concept:
Instead of assigning one result per thread, the kernel is modified such that each thread computes multiple results (1D tiling) and later extended to 2D tiling. - Outcome:
Improved arithmetic intensity minimizes redundant SMEM loads per computed result, translating into significant performance gains—up to 68-78% relative performance improvements in subsequent kernel versions.
5. Vectorized Memory Accesses
- Optimization:
By using vector data types (e.g.,float4
), both GMEM and SMEM accesses are vectorized. This change promises the compiler that the data is properly aligned, enabling wider memory transactions. - Impact:
Vectorized loads (128B transactions) help bridge the performance gap with cuBLAS by improving throughput by a modest 3-5%.
6. Autotuning and Warptiling
- Autotuning:
An extensive search over parameters—such as tile sizes and the number of threads per block—identifies the optimal configuration for the target GPU. - Warptiling:
An additional tiling layer is introduced within warps. By explicitly scheduling computations at the warp level, shared memory bank conflicts are reduced, and better register cache locality is achieved. - Final Outcome:
These combined efforts push the optimized kernel to within 94% of cuBLAS performance on large matrices, achieving a peak of over 21 TFLOPs on devices like the A100.
Insights from the Iterative Process
- Incremental Improvements Matter:
Each optimization may yield modest performance gains individually, but cumulatively they lead to dramatic speed-ups. - Balancing Resource Utilization:
Considerations such as shared memory capacity, register usage, and occupancy are critical. Overloading any single resource (e.g., using too much SMEM per block) can reduce overall throughput. - Autotuning as a Necessity:
Given the variability in GPU architectures, parameter tuning is essential. The optimal settings for one GPU (e.g., A6000) might differ from another (e.g., A100), underscoring the need for an adaptive approach. - Future Work:
While significant progress is made, avenues like double buffering (for overlapping computation and memory loads) and elimination of SMEM bank conflicts remain as potential further optimizations.
This section reveals how a deep understanding of GPU architecture—from memory hierarchies to warp scheduling—can drastically impact the performance of fundamental operations in ML inference.
Conclusion: Bridging High-Level Design and Low-Level Optimization
We have now traversed the full spectrum of model serving—from the high-level design challenges of building robust platforms like LyftLearn Serving to the low-level intricacies of CUDA kernel optimization for matrix multiplication.
-
At the System Level:
Robust model serving requires a flexible, isolated, and scalable architecture. By integrating microservices, automated configuration, and rigorous testing, platforms like LyftLearn Serving ensure that real-time decisions are both reliable and performant. -
At the Hardware Level:
Achieving near-peak performance in ML inference hinges on the ability to optimize core computational kernels. Iterative approaches that address memory access patterns, leverage shared memory, and finely tune execution parameters can unlock substantial performance improvements.
Both perspectives are essential. High-level system design ensures that the right predictions are made at the right time, while low-level optimization guarantees that the underlying hardware is used to its fullest potential. Together, these approaches form the backbone of modern ML systems that power millions of real-time decisions, ultimately driving innovation and operational excellence.
By embracing these strategies and continuously iterating on both the architectural and algorithmic fronts, practitioners can build model serving pipelines that are not only efficient and scalable but also robust enough to meet the ever-evolving demands of real-world applications.
Happy serving and optimizing!
Advanced Serving: System and Model Optimizations for Efficient ML Inference
Machine learning model serving is more than simply deploying a trained model. It is a multifaceted challenge that encompasses everything from pre-processing data and optimizing model execution to managing system-level delays such as queuing, batching, and concurrent execution. In the previous sections, we introduced the concepts behind model serving and covered many preliminary optimization techniques. In this post, we continue the discussion—covering advanced coding practices, containerization strategies, and detailed explanations on system and model optimizations for both cloud and edge deployment.
1. Advanced Model-Level Optimizations
Optimizing a machine learning model for inference is critical to reducing latency and maximizing throughput. As explained in our earlier PPT and detailed guide [citeturn2file0], the model-serving lifecycle relies heavily on model-level techniques such as compiling models into graphs, fusing operations, applying quantization, pruning, and knowledge distillation.
1.1 Compiling the Model into a Graph
Most modern frameworks allow you to convert your model from an imperative “eager” mode to a graph (or static) mode using just-in-time (JIT) or ahead-of-time (AOT) compilation. This conversion makes it possible for automated optimizations such as constant folding and operation fusion. For example, when you compile a model, the system can eliminate redundant operations or fuse consecutive layers (such as a convolution followed by a batch normalization) to reduce memory footprint and improve computation speed.
Additional Explanation:
Graph compilation not only simplifies the model computation by creating a fixed data flow, it also optimizes the order of operations based on the target hardware’s memory layout and compute characteristics. This step is essential when deploying models on devices with limited resources.
1.2 Graph Optimizations: Fusing and Eliminating Operations
Once your model is in graph form, further optimizations can be applied. Operations that are not used in inference (for example, dropout layers) are removed, and similar operations can be fused together (e.g. reshaping with multiplication) [citeturn2file0]. This fusion minimizes the number of memory reads and writes during model execution.
Additional Explanation:
Fusing vertical operations (like consecutive convolution, add, and activation functions) reduces the frequency of intermediate data storage. Less memory overhead means faster inference—a critical factor especially when serving models in real time.
1.3 Quantization: Reducing Precision Without Sacrificing Accuracy
Quantization is one of the most effective techniques for speeding up inference. By converting weights and activations from 32-bit floating point representations to lower-precision formats (e.g., INT8), you reduce the model size dramatically and improve compute efficiency. Our guides [citeturn2file2] provide detailed examples of post-training quantization (both static and dynamic) as well as quantization-aware training.
Key Points:
- Dynamic Quantization: We compute activation quantization parameters on the fly. This technique adds minimal overhead and is best suited for RNNs and transformer-based models.
- Static Quantization: Calibration with a representative dataset ensures that activations and weights have pre-set quantization parameters, which is ideal for CNN models with strict latency requirements.
Additional Explanation:
The conversion process involves mapping floating point values to integers using a scale and zero-point. This mapping must be carefully computed to avoid precision loss. For instance, using calibration data, static quantization finds an optimal range (or “zero-point”) ensuring that the quantized model output closely matches the FP32 baseline. Detailed papers and frameworks (such as Intel Neural Compressor) provide further insights into achieving the desired balance between size reduction and accuracy.
1.4 Pruning and Knowledge Distillation
Pruning reduces the number of parameters by removing those that contribute less to the model’s predictive power. Combined with knowledge distillation, where a smaller student model learns to mimic a larger teacher model, these techniques can yield models that are both fast and lightweight without a significant loss in accuracy.
Additional Explanation:
Pruning often relies on iterative fine-tuning: you remove the lowest magnitude weights, then retrain the network to recover any loss in accuracy. Knowledge distillation helps further by transferring the ‘knowledge’ of a high-capacity model to a smaller one. The key is to find the right balance—too much pruning can degrade performance, while too little may not yield the desired speedup.
2. System-Level Optimizations and Serving Infrastructure
Beyond the optimizations inside the model, system-level enhancements are crucial to reduce the overall latency of a machine learning system. The overall prediction time isn’t just about inference—the routing, queuing, and response stages also matter.
2.1 Managing Request Latency: Cold vs. Warm Starts
When deploying your model in production, you have to decide between cold starts (involving container startup, code loading, and model initialization) and warm starts (where the model is preloaded in memory). Warm-starting a model serving service is critical when dealing with online predictions where latency is measured in milliseconds [citeturn2file0].
Additional Explanation:
A cold start can delay the first request significantly. By contrast, warm-starting preloads necessary resources, making the initial response times much faster. This trade-off must be carefully managed in real-world deployments where users expect near-instant feedback.
2.2 Concurrency and Batching
Concurrency management involves executing predictions from multiple requests in parallel, either by running multiple copies of the model concurrently or by batching several inputs into a single inference call. Techniques like dynamic batching allow systems such as Triton Inference Server to combine incoming requests, thus increasing throughput without proportionally increasing latency.
Detailed Example Using Triton:
The configuration file for Triton can include dynamic batching settings, where a list of preferred batch sizes (e.g., [4, 6, 8, 10]) along with a max queue delay (e.g., 100 microseconds) optimally batches requests. This reduces the number of model invocations and leverages the GPU’s parallel processing capabilities more efficiently.
Additional Explanation:
Dynamic batching is particularly useful when predictions arrive in bursts. However, it also adds complexity because the system must balance between waiting for more requests (to maximize batch size) and returning results quickly (minimizing latency). This is a classic example of a system-level trade-off that must be carefully tuned based on expected load and hardware capabilities.
2.3 Leveraging Multi-Instance and Distributed Serving
For systems that need to scale beyond one GPU or CPU, you might deploy multiple instances of your model. For instance, a FastAPI application using Docker Compose can host several containers with isolated models to ensure no single instance becomes a bottleneck. Using Kubernetes or orchestrators like Prefect or Airflow further decouples model serving from monitoring and can manage container scaling automatically.
Additional Explanation:
Running multiple instances across different nodes ensures redundancy and fault tolerance, while also allowing load balancing to distribute traffic evenly. When using Triton, you can specify in the configuration file the number of instances per GPU (or even span across GPUs) to maximize performance.
3. Serving ML Models with FastAPI, Docker, and Triton Inference Server
An end-to-end model serving pipeline involves deploying your model as a microservice with robust API endpoints. Our coding guides illustrate this process using FastAPI with Docker containers, integrating Triton Inference Server for optimized inference.
3.1 FastAPI Endpoint for Predictions
The FastAPI application exposes an endpoint (e.g., /predict
) that takes in input features, processes them, and returns predictions. Here’s a simplified version of the code from our guides:
from fastapi import FastAPI, Response, BackgroundTasks
import pandas as pd
import torch
app = FastAPI(title="ML Model Serving API")
# Example function to get predictions
def get_predictions(features, model):
# Perform preprocessing and model inference
predictions = model(torch.tensor(features, dtype=torch.float))
# Reshape predictions if required (e.g., apply softmax)
return predictions.detach().cpu().numpy()
# Background task to save predictions in a database (e.g., PostgreSQL)
def save_predictions(predictions_df: pd.DataFrame):
# Insert code to save DataFrame into a database
pass
@app.post('/predict')
def predict(response: Response, features_item: dict, background_tasks: BackgroundTasks):
try:
# Assume features_item contains a JSON representation of features
features = pd.read_json(features_item.get("features"))
predictions = get_predictions(features, MODEL)
# Add a task to save predictions without delaying the response
background_tasks.add_task(save_predictions, features.assign(predictions=predictions))
return {"predictions": predictions.tolist()}
except Exception as e:
response.status_code = 500
return {"error_msg": str(e)}
Additional Explanation:
- Background Tasks: FastAPI allows you to delegate non-critical tasks to a background thread (or process) so that user response times remain fast.
- Response Handling: The API returns predictions as JSON after converting numpy arrays to lists, ensuring compatibility with web clients.
3.2 Deploying with Docker
Containerization using Docker helps encapsulate dependencies, ensuring that your API behaves the same way in development, testing, and production. A sample docker-compose.yaml
brings together multiple services including FastAPI, Triton Inference Server, a PostgreSQL database, and even a Streamlit UI for visualization.
version: "3.9"
services:
fastapi_app:
image: fastapi_app:latest
ports:
- "5000:5000"
networks:
- monitoring
monitoring-db:
image: postgres:15.2-alpine
ports:
- "5432:5432"
networks:
- monitoring
triton_server:
image: triton_inference_server:latest
ports:
- "8000:8000" # HTTP service for inference
- "8001:8001" # gRPC service
volumes:
- ./models:/models
networks:
- monitoring
streamlit_app:
image: streamlit_app:latest
ports:
- "8501:8501"
networks:
- monitoring
networks:
monitoring:
name: monitoring
Additional Explanation:
- Isolation and Scalability: Each service is isolated in its own container, making it easier to scale individual parts (e.g., more FastAPI replicas if needed).
- Volume Mounting: The Triton server mounts a directory with models to ensure it always has access to the latest model version.
- Network Configuration: All services communicate over an isolated network, which improves security and performance.
3.3 Triton Inference Server Integration
Scaling inference performance often demands using a dedicated inference server that supports dynamic batching and multi-GPU or multi-instance deployment. NVIDIA’s Triton Inference Server meets these needs by supporting various backends (PyTorch, ONNX, TensorFlow, etc.), enabling advanced features like dynamic batching and concurrent model serving.
A. Organizing the Model Repository
Triton expects your models to be organized in a specific directory structure. For example, for our food classifier with a Python backend, the layout is:
models/
└── food_classifier
├── 1
│ ├── food11.pth
│ └── model.py
└── config.pbtxt
The config.pbtxt
file includes essential parameters:
name: "food_classifier"
backend: "python"
max_batch_size: 16
input [
{
name: "INPUT_IMAGE"
data_type: TYPE_STRING
dims: [1]
}
]
output [
{
name: "FOOD_LABEL"
data_type: TYPE_STRING
dims: [1]
},
{
name: "PROBABILITY"
data_type: TYPE_FP32
dims: [1]
}
]
instance_group [
{
count: 1
kind: KIND_GPU
gpus: [0]
}
]
This file specifies the model name, backend, batching settings, input/output definitions, and instance grouping. By adjusting parameters such as max_batch_size
and the instance_group
, you can scale inference to meet high throughput demands.
B. Dynamic Batching and Scaling
Dynamic batching is a powerful feature in Triton that automatically aggregates incoming single-sample requests into batches, thus optimizing GPU utilization. Simply adding a dynamic batching block in your config.pbtxt
can lead to reduced queuing delays:
dynamic_batching {
preferred_batch_size: [4, 6, 8, 10]
max_queue_delay_microseconds: 100
}
Furthermore, modifying the instance_group
to deploy multiple instances on a GPU or across GPUs allows you to handle higher concurrency. For example:
instance_group [
{
count: 2
kind: KIND_GPU
gpus: [0]
},
{
count: 2
kind: KIND_GPU
gpus: [1]
}
]
This multi-instance approach ensures that incoming requests are processed with minimal delay, even when the server experiences heavy load.
Additional Explanation:
The dynamic batching block instructs Triton to accumulate requests up to certain batch sizes before processing them—drastically improving throughput for bursty workloads. You can adjust these parameters based on your specific load patterns and hardware capabilities.
3.4 Serving on Edge Devices
Deploying models to edge devices (like Raspberry Pi) presents unique challenges such as limited compute, memory, and power. As detailed in our guide [citeturn2file1], the process involves ensuring that the model is lightweight (typically under 5MB) and optimizing inference (median latency below 15ms).
Key Considerations for Edge Deployment:
- Model Size: Use smaller architectures such as MobileNetV2 or Tiny YOLO. Techniques such as quantization (even more aggressive on edge) further reduce size.
- Inference Speed: With lower compute power, aim for execution times in the low double-digit milliseconds.
- Efficient Data Transfer: Edge devices require minimal I/O overhead. Pre-processing can often be done on the device to avoid network latency.
Additional Example:
Deploying an ONNX model on a Raspberry Pi 5 using ONNX Runtime, you can benchmark inference performance with code like:
import onnxruntime as ort
import numpy as np
import time
# Create an inference session using the CPUExecutionProvider
ort_session = ort.InferenceSession("models/food11.onnx", providers=['CPUExecutionProvider'])
# Generate a dummy input sample matching the model's expected shape
input_shape = ort_session.get_inputs()[0].shape
dummy_input = np.random.random_sample(input_shape).astype(np.float32)
# Warm-up run
ort_session.run(None, {ort_session.get_inputs()[0].name: dummy_input})
# Benchmarking
latencies = []
for _ in range(100):
start_time = time.time()
ort_session.run(None, {ort_session.get_inputs()[0].name: dummy_input})
latencies.append(time.time() - start_time)
median_latency = np.median(latencies)
print(f"Median inference latency: {median_latency*1000:.2f} ms")
Additional Explanation:
This benchmarking setup is crucial to ensure that the edge device can meet the stringent latency requirements. Often, further optimizations like static quantization (with calibration data) and graph optimization will be required to meet the 15ms target.
3.5 Benchmarking and Performance Analysis
Assessing performance is key to optimizing your deployment. Two common types of benchmarking are:
A. Direct Endpoint Benchmarking
A simple approach is to write a Python script that sends requests to your FastAPI endpoint and measures latency and throughput. For instance:
import time, requests, numpy as np
FASTAPI_URL = "http://<your_server_ip>:8000/predict"
payload = {"image": "<your_base64_encoded_image>"}
num_requests = 100
inference_times = []
for _ in range(num_requests):
start_time = time.time()
response = requests.post(FASTAPI_URL, json=payload)
inference_times.append(time.time() - start_time)
inference_times = np.array(inference_times)
print(f"Median Inference Time: {np.percentile(inference_times, 50) * 1000:.2f} ms")
print(f"Throughput: {num_requests / np.sum(inference_times):.2f} requests/sec")
B. Using Triton’s Perf Analyzer
Triton provides a performance analyzer tool called perf_analyzer
that can simulate concurrent requests and provide detailed metrics—including queuing delays and inference computation times. For example:
perf_analyzer -u <your_triton_server_ip>:8000 -m food_classifier --input-data input.json -b 1
This command benchmarks your Triton endpoint for the food_classifier
model. You can run tests with increased concurrency to observe how queuing delays vary with load and to fine-tune your dynamic batching parameters.
4 Converting and Optimizing Models with ONNX
While PyTorch models are excellent for research, converting them to ONNX can unlock further optimizations via graph-level transforms and hardware-specific execution providers.
A. Converting Your PyTorch Model to ONNX
Using PyTorch’s torch.onnx.export
, you can convert a model to ONNX format:
import torch
dummy_input = torch.randn(1, 3, 224, 224)
torch.onnx.export(model, dummy_input, "food11.onnx",
export_params=True, opset_version=20, do_constant_folding=True,
input_names=['input'], output_names=['output'],
dynamic_axes={"input": {0: "batch_size"}})
After conversion, validate the model with ONNX’s checker:
import onnx
onnx_model = onnx.load("food11.onnx")
onnx.checker.check_model(onnx_model)
B. Benchmarking the ONNX Model
You can use ONNX Runtime to benchmark your model’s inference time and throughput on CPU or GPU:
import onnxruntime as ort
ort_session = ort.InferenceSession("food11.onnx", providers=['CPUExecutionProvider'])
# Benchmark inference latency similar to earlier examples...
C. Graph Optimizations and Quantization
ONNX Runtime supports several optimizations:
-
Graph Optimizations: These fuse operations and remove redundant nodes to create an optimized computational graph.
session_options = ort.SessionOptions() session_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_EXTENDED session_options.optimized_model_filepath = "food11_optimized.onnx" ort_session = ort.InferenceSession("food11.onnx", sess_options=session_options)
-
Quantization: Quantizing your model (dynamic or static) can significantly reduce model size and potentially improve inference speed (while possibly trading off some accuracy). Using Intel Neural Compressor, you can apply dynamic quantization:
from neural_compressor import quantization, PostTrainingQuantConfig config_ptq = PostTrainingQuantConfig(approach="dynamic") # Load and quantize your model, then save the quantized model.
Static quantization involves a calibration phase using representative data, which can be managed similarly but with an accuracy threshold parameter to control performance degradation.
D. Experimenting with Different Execution Providers
ONNX Runtime supports multiple execution providers (CPU, CUDA, TensorRT, OpenVINO) which can be used to fine-tune performance based on your deployment environment:
# For CUDA:
ort_session = ort.InferenceSession("food11.onnx", providers=['CUDAExecutionProvider'])
# For TensorRT (if available):
ort_session = ort.InferenceSession("food11.onnx", providers=['TensorrtExecutionProvider'])
Switching providers can lead to dramatic improvements in inference latency and throughput. Comparing results across providers is recommended to choose the best configuration for your use case.
5. Serving Models on Edge Devices
For many applications, deploying models on end-user devices is paramount for reducing network latency and preserving user privacy. The tutorial on serving models on edge devices explains how to deploy and benchmark ONNX models on low-resource devices such as a Raspberry Pi 5 with an ARM Cortex-A76 processor.
A. Key Considerations for Edge Deployment
- Model Size: Your model should be as small as possible (e.g., less than 5MB) to suit storage constraints on mobile or embedded devices.
- Inference Latency: Although the latency budget on edge devices is higher (e.g., up to 15ms per sample), it is still critical to ensure that performance meets user experience expectations.
- Quantization: ONNX models that are quantized (to INT8) provide substantial size reduction and often faster inference on CPUs, which is crucial on resource-constrained devices.
B. Benchmarking on an Edge Device
Using ONNX Runtime on an edge device, you can set up a benchmark similar to the server environment:
import onnxruntime as ort
import time, numpy as np
# Create the ONNX session using CPUExecutionProvider for the ARM device.
ort_session = ort.InferenceSession("food11.onnx", providers=['CPUExecutionProvider'])
# Generate a dummy input and benchmark inference latency as shown earlier.
By comparing the baseline model with its quantized versions (dynamic and static), you can make informed decisions about the trade-offs between accuracy, model size, and inference performance on low-resource hardware.
Conclusion
In this comprehensive continuation, we covered the coding techniques that bring together system-level and model-level optimizations for efficient ML inference. We explored:
- FastAPI Endpoints: Wrapping your model as a scalable, production-ready service.
- Triton Inference Server: Deploying models with advanced features like dynamic batching and multi-instance scaling.
- ONNX Conversion and Optimizations: Transforming your model for enhanced portability, reduced computational overhead, and compatibility with multiple execution providers.
- Quantization: Utilizing both dynamic and static quantization to shrink model size and potentially boost performance.
- Edge Deployment: Strategies for running inference on low-resource devices, ensuring that models are optimized for storage and compute constraints.
Both high-level system design and low-level model optimizations are critical for achieving optimal inference performance across different deployment scenarios. By integrating these strategies, you can build serving systems that not only meet stringent latency and throughput requirements on powerful server hardware but also deliver acceptable performance on resource-constrained edge devices.
Happy serving and optimizing!
Enjoy Reading This Article?
Here are some more articles you might like to read next: