Thegretia
Back to Articles
AI & ML
Feb 10, 2025•10 min read

Sub-100ms Deep Learning Inference: Quantization, ONNX Graph Optimization, and Async FastAPI

How to compress transformer and acoustic models for CPU/GPU serving, avoiding PyTorch GIL contention, and achieving ultra-low inference latency under heavy concurrent loads.

Patrick Thomas MBONJO ETIA

Thegretia • Data & AI Engineer

#PyTorch#ONNX#FastAPI#Machine Learning#Low Latency

The Challenge of Modern Model Serving

Modern transformer architectures (such as Whisper, BERT, RoBERTa, and LLaMA) boast remarkable accuracy, but their computational complexity presents major challenges when deployed in production: - High VRAM footprint (rendering multi-GPU cloud nodes prohibitively expensive). - Python GIL (Global Interpreter Lock) contention when handling thousands of concurrent HTTP/WebSocket requests. - Latency jitter caused by unoptimized tensor memory allocations.

Here is the blueprint for converting a raw PyTorch model into a high-throughput, low-latency production microservice.


1. Exporting PyTorch to ONNX Graph

PyTorch dynamic execution graphs are convenient during research but introduce overhead in production. Exporting to a static ONNX graph allows hardware-specific runtime compilers (ONNX Runtime, TensorRT) to fuse layers and remove unused subgraphs.

pythonsnippet
import torch

class ModelExporter: @staticmethod def export_to_onnx(model, dummy_input, output_path: str): model.eval() torch.onnx.export( model, dummy_input, output_path, export_params=True, opset_version=17, do_constant_folding=True, input_names=["input_ids", "attention_mask"], output_names=["logits"], dynamic_axes={ "input_ids": {0: "batch_size", 1: "sequence_length"}, "attention_mask": {0: "batch_size", 1: "sequence_length"}, "logits": {0: "batch_size", 1: "sequence_length"}, }, ) ```


2. 8-Bit Static & Dynamic Quantization

Quantizing 32-bit floating point weights (FP32) to 8-bit integers (INT8) decreases model size by 75% and unlocks AVX-512 / VNNI vector instructions on standard x86 CPU hardware:

pythonsnippet

quantize_dynamic( model_input="model_fp32.onnx", model_output="model_int8.onnx", weight_type=QuantType.QInt8, extra_options={"EnableQuantization": True} ) ```

Benchmark Comparison: - Model Size: 560 MB -> 142 MB (74.6% reduction) - P95 Latency (CPU): 340ms -> 92ms (3.7x speedup) - Accuracy Loss: < 0.3% degradation on validation dataset


3. Non-Blocking Async FastAPI Inference Server

Avoid executing blocking tensor computations directly inside FastAPI async route handlers! Doing so blocks the Python event loop and starves all incoming requests.

Use an asynchronous threadpool or process pool executor:

pythonsnippet
import asyncio
from concurrent.futures import ThreadPoolExecutor
import onnxruntime as ort

app = FastAPI(title="High-Throughput ML Gateway") session = ort.InferenceSession("model_int8.onnx", providers=["CPUExecutionProvider"]) executor = ThreadPoolExecutor(max_workers=8)

def run_onnx_inference(inputs): return session.run(None, inputs)

@app.post("/predict") async def predict(payload: InferenceRequest): loop = asyncio.get_running_loop() try: # Offload heavy C++ ONNX computation from main async loop result = await loop.run_in_executor( executor, run_onnx_inference, payload.to_onnx_inputs() ) return {"predictions": result[0].tolist()} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) ```


Key Takeaways - Always profile and compile models to ONNX / TensorRT prior to deployment. - Leverage INT8 quantization to serve models on cost-effective CPU clusters. - Decouple computation from the FastAPI event loop using worker pools.