Search Tech Journey

Find topics, journeys and posts

back to blog
systemsadvanced 20m read

Designing for Scale · Model Serving

Why wrapping a PyTorch model in a Flask API is a prototype, not a production system. Handling GPU saturation, dynamic batching, and the difference between CPU and GPU scaling.

The problem this post solves

A machine learning model is a mathematical function that takes an input tensor and returns an output tensor. The simplest way to serve it is to wrap it in a web server (like Flask or FastAPI), accept a JSON request, pass it to the model, and return the JSON response.

This works perfectly for one request per second. At one thousand requests per second, the web server melts. Models are computationally heavy, often requiring GPUs. Web servers are designed for lightweight I/O bound tasks. When a web server waits for a GPU, it blocks; when it blocks, latency spikes; when latency spikes, the system falls over.

This post covers the architecture required to serve heavy machine learning models at scale, focusing on the critical technique of dynamic batching and the architectural split between the API tier and the inference tier.

First principles

From first principles
Start with the question
Why does standard horizontal scaling fail for GPUs?
  1. 1
    GPUs are massively parallel processors.
    forced by · They are designed to perform the same operation on thousands of data points simultaneously.
  2. 2
    Passing a single request (batch size of 1) to a GPU leaves 99% of its cores idle.
    forced by · The overhead of moving data from CPU RAM to GPU VRAM dominates the actual computation time.
  3. 3
    To utilize a GPU efficiently, you must send it a batch of requests simultaneously.
    forced by · The GPU can process 32 requests in almost the exact same time it takes to process 1 request.
  4. 4
    Standard web requests arrive individually and asynchronously.
    forced by · Users do not coordinate their clicks.
⇒ Therefore
If you pass individual web requests directly to a GPU, you are paying for a supercomputer and using it like a pocket calculator. The architecture must actively group independent requests together before inference.

Architecture: The Inference Tier

A production serving architecture decouples the API server (handling HTTP/gRPC, auth, and feature retrieval) from the Inference Server (handling the actual tensor math).

The API Server

The API server is a standard CPU-bound microservice. Its job is to:

  1. Accept the incoming request.
  2. Query the Feature Store for any missing data required by the model.
  3. Transform the raw features into the tensor format the model expects.
  4. Send the tensor to the Inference Server and wait for the response.

Dynamic Batching

Dynamic batching is the core mechanism of the Inference Server (handled by systems like Triton Inference Server, Ray Serve, or TensorFlow Serving).

When a request arrives at the Inference Server, it does not execute immediately. The server holds the request in a microscopic queue. It waits for either:

  • A specific number of milliseconds to pass (the latency SLA, e.g., 10ms).
  • A specific number of requests to accumulate (the maximum batch size, e.g., 32).

Whichever condition is met first triggers the execution. The server concatenates the individual tensors into one large batch tensor, sends it to the GPU, receives the result, splits the result back into individual responses, and returns them to the waiting API server threads.

The tradeoff
Setting the Dynamic Batching Window
Long Window (e.g., 50ms)
+ you gain High throughput. The GPU receives large batches and operates at maximum efficiency. Low cost per request.
− you pay High latency. The first request in the batch sat idle for 50ms before computation even began.
pick when For offline processing, background tasks, or high-volume asynchronous scoring.
Short Window (e.g., 2ms)
+ you gain Low latency. Users receive answers almost instantly.
− you pay Low throughput. The GPU processes small batches, wasting its parallel capacity. High cost per request.
pick when For user-facing, synchronous critical paths (e.g., ad bidding, search ranking).

The CPU vs GPU Scaling Problem

If the API server and the model run in the same container (the monolithic approach), scaling becomes economically impossible.

Assume the API server does heavy feature processing (CPU bound), and the model requires a GPU. If traffic increases and you scale the monolithic container horizontally, you are paying to provision a new GPU every time you need more CPU capacity. GPUs are vastly more expensive than CPUs.

By separating the API server from the Inference Server, they can scale independently.

Mental modelAsymmetric Scaling
A restaurant with waitstaff (API servers) and chefs with massive ovens (Inference servers with GPUs). If you get a flood of customers who need menus and drinks, you hire more waitstaff (cheap). You only buy another massive oven (expensive) if the current oven is actually full of pizzas.
  • API servers scale on CPU utilization.
  • Inference servers scale on GPU utilization and queue depth.
  • The ratio of API servers to Inference servers is rarely 1:1.
🔔 Fires when you see
Use to explain why monolithic ML deployments ruin cloud budgets.

Shadow Deployment and A/B Testing

Model serving requires a mechanism to test new models safely. A new model might perform perfectly on historical data (offline), but fail spectacularly on live user data due to shifting behavior.

Shadow Deployment (Dark Launch)

The API server sends the request to both the current production model (Model A) and the new model (Model B). It returns Model A's prediction to the user. It logs Model B's prediction to a database for offline comparison. Model B has zero impact on the user, allowing data scientists to verify its accuracy in the real world before trusting it.

A/B Testing

The API server routes 90% of users to Model A, and 10% to Model B. It returns the respective predictions to the users. Business metrics (click-through rate, conversion) are then compared between the two cohorts.

Failure modes

CPU bottlenecks on the Inference Server. Data scientists often write pre-processing code (like tokenizing text or resizing images) inside the model's predict() function. If this code runs on the Inference Server's CPU before hitting the GPU, the CPU will max out, starving the GPU of work. Pre-processing must be moved upstream to the API server or compiled directly into the model graph so it runs on the GPU.

Ignoring model size in memory. A GPU might have 16GB of VRAM. If a Large Language Model requires 30GB of VRAM just to load its weights, it cannot run on that GPU. The model must be quantized (reducing the precision of the weights, e.g., from 32-bit floats to 8-bit integers) or sharded across multiple GPUs (Tensor Parallelism), which vastly complicates the serving architecture.

What to carry forward

Key points
    You can now
    • Explain why a simple Flask wrapper around a model fails under load.
    • Design a dynamic batching configuration based on a strict latency budget.
    • Defend the architectural separation of feature processing from tensor execution.
    • Architect a shadow deployment pipeline for a high-risk model update.