AI EngineeringZero to ProductionHome·About·What’s new·Contact
Local & Open Models · Chapter LM3

Production serving with vLLM

Ollama got you running; vLLM gets you serving. The IC6 production engine — batching, paging, quantization — applied to self-hosting open models behind an OpenAI-compatible API.

⏱️ ~1.5 hours🧪 2 labs🎯 Advanced
⚙️ To run this for realThe code here is complete and correct as written. To actually execute it you'll need:
  • a GPU + pip install vllm (serving engine)
Reading and learning works without any of this — run when you're ready. IDs/ARNs in examples are placeholders; swap in your own.

Learning objectives

  • Explain why production self-hosting needs vLLM, not Ollama.
  • Stand up vLLM with an OpenAI-compatible API for an open model.
  • Size the GPU and set the key serving flags.
  • Connect your app with a one-line change.
📋 Before you startThis page builds on: The KV-cache · Why containers. Jumping in? Skim those first — or open the recap.
30-second recap
  • The KV-cache: During decoding the model stores each token's attention keys and values so it doesn't recompute them; that cache is what fills GPU memory.
  • Why containers: A container packages an app with its dependencies so it runs the same everywhere; an image is the blueprint, a container is a running copy.
▶ Runnable companionThe code in this lesson is also saved under code/lm3-vllm-serving/ in the course, with a README. Run the scripts or copy the configs directly.

From laptop to server intermediate

Here's the trap that catches almost everyone the first time. You wire up Ollama on your laptop in the last chapter, the model answers, everything feels production-ready — so you copy that same setup onto a GPU box, point your app at it, and open it to a handful of colleagues. It's great for the first person. It's fine for the second. By the tenth simultaneous user it's crawling, requests are timing out, and the GPU is somehow only 20% busy while everyone waits. Nothing is broken. You just used a single-user tool for a many-user job, and those are two genuinely different problems.

The reason is worth internalizing, because it's the whole justification for this chapter. Think of Ollama as your own home kitchen: perfect for cooking one meal for yourself, on demand, right now. A production serving engine is a restaurant line — the same stoves, but organized so one crew can plate fifty orders at once without any single diner waiting fifty times as long. The bottleneck in LLM serving is almost never raw compute; it's how cleverly you share the GPU across concurrent requests. A naive server processes requests one at a time, so the expensive card sits mostly idle between tokens. The fix is to keep many requests in flight together — and that is exactly the machinery a dev tool doesn't bother building.

The common mistake is assuming "it works locally" means "it will scale," and reaching for more GPUs when throughput sags. More hardware rarely helps a single-stream server, because the problem is utilization, not capacity — you're paying for a whole card and using a sliver of it. The right move is a smarter engine on the same card. That engine is vLLM, and it earns its place through three techniques you already met in the Inference track: continuous batching (slot new requests into the running batch the instant a slot frees up, instead of waiting for a fixed batch to finish), PagedAttention (manage the KV-cache in small pages like an operating system manages RAM, so memory isn't wasted on padding and you fit far more concurrent sequences), and quantization (compress the weights so a bigger model — or more cache headroom — fits on the card you have).

Ollama got you running; vLLM gets you serving. It's the production engine from IC6 — PagedAttention, continuous batching, quantization — behind the same OpenAI-compatible API. This chapter is the self-hosting view of the inference track.

Open weights from Hugging Face vLLM server batching+paging OpenAI-compatibleAPI /v1/chat Your app+ many users unchanged client
🗺️ How to read this diagram

This pipeline shows how a raw open model becomes a real service that many people can use at once. Read it left to right — each box feeds the next.

  • The first box (Open model weights) is just the downloaded model files, usually from Hugging Face ("HF") — data, not yet a running service.
  • The second box (vLLM server) is the engine that loads those weights and runs them efficiently. "batching+paging" means it serves many requests together and manages GPU memory cleverly, so it's fast under load.
  • The third box (OpenAI-compatible API) is the doorway vLLM opens — a /v1/chat endpoint that looks exactly like OpenAI's, so existing code works against it.
  • The last box (Your app + many users) is the point: real traffic hits your app, which calls the API — and the client code didn't have to change.

In short: Weights → vLLM engine → OpenAI-style API → your app. vLLM is the middle piece that turns a model file into a fast service many users can share.

Stand it up intermediate

Brace yourself for an anticlimax: the single hardest-sounding thing in this chapter — running a production LLM server — is one command. Not a Dockerfile, not a Kubernetes manifest, not a framework you have to learn. You install vLLM, you point it at a model name, you pick a port. That's genuinely it for a first server. The complexity people expect here mostly lives in the flags, and the flags are just the IC-track levers wearing command-line clothes.

It helps to picture what that one command sets in motion. vLLM downloads the open weights from Hugging Face (once — they're cached after that), loads them onto the GPU, and then stands up a web server that speaks the OpenAI wire protocol. That last part is the quiet genius of the design: instead of inventing its own API that you'd have to write a new client for, vLLM mimics the exact HTTP shape OpenAI uses — same /v1/chat/completions path, same JSON request and response. So every tool, SDK, and snippet in the world that already talks to OpenAI can talk to your box with zero code changes beyond the address. You'll see that pay off in the very next lab.

The mistake to avoid is treating the flags as magic incantations to copy blindly. Each one is a deliberate memory-versus-throughput decision you're making on the model's behalf: --quantization trades a little quality for a much smaller memory footprint; --max-num-seqs sets how wide the continuous batch runs (bigger = more concurrent users, but more KV-cache memory); --gpu-memory-utilization hands vLLM a slice of the card and tells it "budget weights and cache inside this." Read the launch command below not as boilerplate but as four tuning knobs — and know that when something goes wrong, it's almost always one of these two: not enough memory (turn the dials down) or not enough throughput (turn them up, if memory allows). That single tension is the entire craft of serving.

the flags you set --model --quantization --max-num-seqs --gpu-memory-utilization vLLM server on one GPU more memory used more throughput turn the dials DOWN if you run out of memory · turn them UP for more concurrency Four knobs, one tension. Every flag feeds one vLLM server on one card, and each nudges the same trade-off: raise the batch/memory dials for more throughput, lower them when the GPU runs out of room.
🗺️ How to read this diagram

Read it left-to-right: the flags on the left all feed the single server in the middle, which lands you somewhere on the memory-vs-throughput trade-off on the right.

  • The blue column on the left is the four flags from the launch command — the only things you actually type.
  • The middle box is the one vLLM server running on one GPU: every flag is really an instruction to it.
  • The red and green boxes on the right are the two outcomes in tension — a bigger batch or memory budget buys more throughput (green) but costs more VRAM (red). The bottom caption is the rule of thumb: dials down when you OOM, up when you have room.

In short: The flags aren't magic — they're four dials on a single memory-versus-throughput slider.

Example code for learning — review, test, and adapt it before running against real or production systems. Commands can create, change, or delete resources. See the Terms & Disclaimer.
Lab LM3.1 · Serve for production
serve.shpip install vllm
python -m vllm.entrypoints.openai.api_server \
  --model meta-llama/Llama-3.1-8B-Instruct \
  --quantization awq \                 # 4-bit (IC2)
  --max-num-seqs 128 \                # continuous-batch width (IC4)
  --gpu-memory-utilization 0.90 \     # weights + KV-cache budget (IC3)
  --host 0.0.0.0 --port 8000
▶ How this works

This one command turns an open model into a production web service. It's a python -m ... launcher with several flags — the \ at the end of each line just means "the command continues on the next line". Read the flags top to bottom; each is one knob.

  1. pip install vllm installs the serving engine (needs a GPU).
  2. --model meta-llama/Llama-3.1-8B-Instruct names which open model to serve — this is its Hugging Face path, which vLLM downloads.
  3. --quantization awq loads a 4-bit compressed version so the model fits in less GPU memory (the idea from the quantization lesson, IC2).
  4. --max-num-seqs 128 sets how many requests it will batch together at once — higher means more throughput for many users.
  5. --gpu-memory-utilization 0.90 lets vLLM use 90% of the GPU's memory, split between the model weights and the working cache.
  6. --host 0.0.0.0 --port 8000 exposes the API on port 8000 so other machines can reach it.

What the output means: A long-running server: it prints startup logs, loads the model, then waits for requests at http://your-server:8000/v1.

Try this: If you hit an out-of-memory error, lower --gpu-memory-utilization (say to 0.80) or --max-num-seqs — those two flags are your main memory dials.

Lab LM3.2 · Same client, new base_url
client.pyfrom openai import OpenAI
client = OpenAI(base_url="http://your-server:8000/v1", api_key="none")
r = client.chat.completions.create(
    model="meta-llama/Llama-3.1-8B-Instruct",
    messages=[{"role": "user", "content": "Hello from my own server."}])
print(r.choices[0].message.content)
▶ How this works

This is the same lesson as Ollama, one level up: connecting to your production vLLM server is also just a base_url change. The code you already wrote keeps working against a model you now host yourself.

  1. OpenAI(base_url="http://your-server:8000/v1", api_key="none") points the standard client at your vLLM server (port 8000) instead of a cloud provider. No real key is needed, so "none" is fine.
  2. client.chat.completions.create(model="meta-llama/Llama-3.1-8B-Instruct", messages=[...]) sends a chat request. The model name must match what the server was started with in the previous lab.
  3. r.choices[0].message.content pulls the reply text out and prints it — identical to how you'd read any OpenAI-style response.

What the output means: The model's greeting prints — served by your own GPU box, not a paid API.

Try this: Replace your-server with the real host/IP of your vLLM machine. If it's on the same computer, use localhost.

Sizing the GPU advanced

The question that will actually decide your bill. "Will this model fit on this GPU, and how many people can it serve at once?" — you cannot answer it by eyeballing the model's parameter count, and getting it wrong is expensive in both directions. Guess too small and vLLM crashes with an out-of-memory error the moment real traffic arrives. Guess too big and you rent a monster card that runs at 15% and quietly bleeds money. Sizing is the difference between a service that survives launch day and one that either falls over or wastes your budget.

Here's the mental model that makes it click. GPU memory (VRAM) is a fixed drawer, and three things compete for the space: the weights (the model itself, loaded once), the KV-cache (scratch memory that grows with every concurrent request and every token of context), and a bit of overhead (the framework, activations, CUDA itself). Most beginners fixate on the weights because that's the number on the model card — but the weights are the easy part; they're a one-time, fixed cost. It's the KV-cache that's the surprise, because it scales with concurrency. Picture a parking lot: the building (weights) takes a fixed footprint, but every car you let in (each concurrent request) needs its own space, and the context length is how big each car is. Run out of parking and the next car — the next user — simply can't get in.

The common trap is sizing only for the weights and then being baffled when the server OOMs under load even though "the model clearly fits." It fit when idle; it didn't fit once thirty users each demanded their own slice of KV-cache. So the real formula is: weights are your entry ticket, and whatever VRAM is left over — divided by the per-request cache cost — is your concurrency ceiling. That's precisely why --max-num-seqs and --gpu-memory-utilization are the two dials you reach for: one caps how many cars you admit, the other decides how much of the drawer vLLM is allowed to claim. Work the numbers below rather than trusting a vibe — the Practice ladder turns this exact reasoning into a calculator you can reuse on any card.

Memory must hold weights + KV-cache + overhead. Use the IC1–IC3 math: a 4-bit 8B model is ~5–6 GB of weights, leaving the rest of the card for KV-cache (which sets how many concurrent requests you can serve). gpu-memory-utilization tunes that split.

one GPU · fixed VRAM (the drawer) weights fixed, loaded once over- head ? KV-cache: one green slice per concurrent request next user — no room left The weights are the easy part. Weights and overhead take a fixed footprint; the leftover VRAM is sliced into one KV-cache block per concurrent request — run out of slices and the next user simply can't get in.
🗺️ How to read this diagram

Read the whole rectangle as the GPU's fixed memory drawer, filled left-to-right until it's full.

  • The purple block (weights) and the amber block (overhead) on the left are fixed costs — they take the same space no matter how many users show up.
  • The green slices are the KV-cache: each concurrent request needs its own slice, and a longer context makes each slice bigger.
  • The dashed red slice is the request that doesn't fit — the drawer is full, so the next user is turned away (an out-of-memory error under load). That's why KV-cache, not the weights, sets your concurrency ceiling.

In short: Weights + overhead are your fixed entry ticket; whatever VRAM is left, divided by the per-request KV slice, is how many users you can serve at once.

This is literally IC6 againIf LM3 feels like a repeat of IC6, that's the point: inference optimization and self-hosting are one skill. The Inference track taught the levers; here you apply them to run open models you control.

Exercise LM3.1 — Serve and load-test

Context: The point of vLLM is throughput under load — and a load test against Ollama on the same model makes the gap unmistakable.

Your task: Serve an 8B model with vLLM (quantized, batched), point a course lab at it, then run a load test at rising concurrency and record throughput and p95 latency; compare to Ollama.

Requirements:

  • Serve the model with vLLM (quantized + batched)
  • Drive rising concurrency with a load test
  • Record throughput and p95 latency at each level
  • Run the same test against Ollama on the same model
  • Confirm vLLM wins badly under load

💡 Hint: The divergence shows up as concurrency climbs — single requests look similar, but batching pulls ahead under real load.

🪜 Practice ladder beginner → industry

Six graded exercises, easy to real-world. Try each before opening its solution.

Exercise 1 · Why vLLM, not Ollama, for productionBeginner

Context: Ollama runs; vLLM serves. The production engine adds continuous batching, PagedAttention, and quantization — exactly what a single-user tool doesn't optimize for.

Your task: Write a helper that names what vLLM adds for production versus Ollama's single-user focus.

Requirements:

  • Name continuous batching, PagedAttention, and quantization
  • Give the production benefit of each
  • Note the OpenAI-compatible API so existing clients work unchanged
  • Print the feature→benefit list

💡 Hint: These are the IC6 throughput concepts; the framing is why each matters under concurrent load, not the implementation.

Show solution

vLLM is the production engine (IC6 concepts). Runnable:

ADDS = {
    "continuous batching": "serve many requests together, high GPU utilization",
    "PagedAttention":       "manage KV-cache memory in pages, fit more concurrency",
    "quantization":         "run bigger models in less VRAM",
    "OpenAI-compatible API":"existing client code works unchanged",
}
def why_vllm():
    for feature, benefit in ADDS.items():
        print(f"- {feature}: {benefit}")

why_vllm()

These throughput features are exactly what a single-user tool like Ollama does not optimize for.

Exercise 2 · Stand up vLLM (serving config)Intermediate

Context: Standing up vLLM behind an OpenAI-compatible API comes down to a launch command with a few key flags — model, quantization, batch width, context length.

Your task: Write the vLLM launch command with those key flags, labeled as needing a GPU.

Requirements:

  • Show python -m vllm.entrypoints.openai.api_server (or equivalent)
  • Include the model, a quantization flag, a batch-width flag (--max-num-seqs), and a context-length flag
  • Include a GPU-memory-utilization flag
  • Comment what each flag controls
  • Label it as needing a GPU + pip install vllm

💡 Hint: The flags map to concepts: quantization shrinks weights, --max-num-seqs is the continuous-batch width, --max-model-len caps context — it exposes a /v1/chat/completions endpoint.

Show solution

This is serving config (needs a GPU + pip install vllm):

pip install vllm
python -m vllm.entrypoints.openai.api_server \
  --model meta-llama/Llama-3.1-8B-Instruct \
  --quantization awq \        # 4-bit weights (IC2) -> less VRAM
  --max-num-seqs 128 \        # continuous-batch width (concurrency)
  --max-model-len 8192 \      # context length ceiling
  --gpu-memory-utilization 0.90  # fraction of VRAM vLLM may use

It opens a /v1/chat/completions endpoint that looks exactly like OpenAI's — the whole point of the engine.

Exercise 3 · Size the GPU for a model + concurrencyAdvanced

Context: VRAM must hold weights plus KV cache for every concurrent sequence — and it's usually the KV cache, not the weights, that forces a concurrency cap.

Your task: Write a calculator: weights (bytes/param by quant) + per-sequence KV cache × max_num_seqs, and say whether it fits a given GPU.

Requirements:

  • Weight bytes/param vary by quant (fp16 / awq / int8)
  • Per-sequence KV cache scales with layers, hidden size, and context length (K and V)
  • Total = weights + per-seq KV × concurrency
  • Return the total and whether it fits the GPU
  • Show a realistic 8B AWQ config on a mid-size card

💡 Hint: KV per sequence is ~2 (K and V) × layers × hidden × ctx × bytes; concurrency multiplies it, which is why it dominates.

Show solution

Total VRAM = weights + KV cache * concurrency. Runnable:

BYTES = {"fp16": 2, "awq": 0.5, "int8": 1}   # ~bytes per weight param

def weights_gb(params_b, quant):
    return params_b * 1e9 * BYTES[quant] / (1024**3)

def kv_gb_per_seq(layers, hidden, ctx_len, bytes_per=2):
    # 2 (K and V) * layers * hidden * ctx * bytes, in GB
    return 2 * layers * hidden * ctx_len * bytes_per / (1024**3)

def fits(gpu_gb, params_b, quant, layers, hidden, ctx, seqs):
    total = weights_gb(params_b, quant) + kv_gb_per_seq(layers, hidden, ctx) * seqs
    return total, total <= gpu_gb

# 8B AWQ, 32 layers, hidden 4096, ctx 8192, 32 concurrent seqs, on a 24GB card
total, ok = fits(24, 8, "awq", 32, 4096, 8192, 32)
print(f"needs ~{total:.1f} GB -> {'FITS' if ok else 'TOO BIG'} on 24GB")

KV cache scales with concurrency and context — it, not the weights, is what usually forces you to cap --max-num-seqs.

Exercise 4 · Tune max-num-seqs to fit VRAMExpert

Context: You should set --max-num-seqs from the VRAM math, not by guessing — too high and vLLM OOMs under load, too low and the GPU sits idle.

Your task: Write a solver that returns the largest batch width (max_num_seqs) that fits a fixed GPU and model.

Requirements:

  • Compute the VRAM budget left after weights (with a reserve)
  • Divide the remaining budget by the per-sequence KV cost
  • Return the integer max concurrency (0 if the weights alone don't fit)
  • Show the recommended --max-num-seqs for a card

💡 Hint: It's the fit calculation solved for concurrency: (usable VRAM − weights) / KV-per-seq, floored.

Show solution

Solve for the concurrency ceiling. Runnable:

BYTES = {"fp16": 2, "awq": 0.5}
def weights_gb(pb, q): return pb * 1e9 * BYTES[q] / (1024**3)
def kv_per_seq(layers, hidden, ctx): return 2*layers*hidden*ctx*2 / (1024**3)

def max_seqs(gpu_gb, params_b, quant, layers, hidden, ctx, reserve=0.10):
    budget = gpu_gb * (1 - reserve) - weights_gb(params_b, quant)
    if budget <= 0:
        return 0                       # weights alone don't fit
    return int(budget // kv_per_seq(layers, hidden, ctx))

n = max_seqs(24, 8, "awq", 32, 4096, 8192)
print(f"set --max-num-seqs {n} to stay within 24GB")

Set --max-num-seqs from the math, not by guessing — too high and vLLM OOMs under load; too low and you waste the GPU.

Exercise 5 · Connect your app with a one-line changeProfessional

Context: Because vLLM speaks the OpenAI API, connecting an app is a base_url swap — one line.

Your task: Show the one-line change (needs the server) and validate the request payload offline.

Requirements:

  • Build/validate the chat payload with the standard library, no server
  • Show the real call pointing an OpenAI client at the vLLM host's /v1 endpoint
  • Only the base_url changes in app code
  • Label the real call as needing the vLLM server running

💡 Hint: Same OpenAI client, new base_url; the self-host swap is one line because vLLM mimics the API.

Show solution

Validate the payload offline (runnable):

def chat_payload(model, prompt):
    return {"model": model,
            "messages": [{"role": "user", "content": prompt}],
            "max_tokens": 256}
print(chat_payload("meta-llama/Llama-3.1-8B-Instruct", "hi"))

The only app change (needs the vLLM server running):

from openai import OpenAI
client = OpenAI(base_url="http://your-vllm-host:8000/v1", api_key="not-needed")
r = client.chat.completions.create(**chat_payload("meta-llama/Llama-3.1-8B-Instruct", "hi"))
print(r.choices[0].message.content)

Same OpenAI client, new base_url — the self-host swap is one line because vLLM mimics the API.

Exercise 6 · Capacity plan: GPUs for a target QPSIndustry scenario

Context: Capacity planning turns vLLM's per-GPU throughput into a replica count and a monthly bill — the input to the LM5 build-vs-buy decision.

Your task: Given a target requests/second, per-GPU throughput, and headroom, compute how many GPU replicas you need and the monthly GPU cost.

Requirements:

  • Never plan a GPU at 100% — apply a headroom factor to per-GPU QPS
  • Replicas = ceil(target QPS / effective per-GPU QPS)
  • Monthly cost = replicas × hourly rate × hours/month
  • Show a worked example with the replica count and dollar figure

💡 Hint: math.ceil the ratio after de-rating per-GPU QPS by headroom, then multiply replicas by the hourly rate over a month.

Show solution

Capacity = ceil(target QPS / per-GPU QPS), then cost it. Runnable:

import math

def plan(target_qps, per_gpu_qps, gpu_hourly_usd, headroom=0.30):
    effective = per_gpu_qps * (1 - headroom)      # never run a GPU flat out
    replicas = math.ceil(target_qps / effective)
    monthly = replicas * gpu_hourly_usd * 24 * 30
    return replicas, round(monthly, 2)

replicas, cost = plan(target_qps=40, per_gpu_qps=12, gpu_hourly_usd=1.20)
print(f"{replicas} GPU replicas, ~${cost}/mo")
# 40 / (12*0.7)=~4.76 -> 5 replicas; 5*1.20*720 = $4320/mo

vLLM's batching sets per-GPU QPS; capacity planning turns that into replica count and a bill — the input to the LM5 build-vs-buy call.

✓ Checkpoint — you can move on when you can…

  • Explain why prod self-hosting uses vLLM over Ollama.
  • Stand up vLLM with an OpenAI-compatible API.
  • Size the GPU with the IC1–IC3 memory math.
  • Connect an app with a one-line change.

Knowledge check check yourself

✓ Knowledge check

Why does production self-hosting call for vLLM rather than Ollama, and which IC-track techniques does vLLM apply?

Show answer
Ollama is built for local, low-traffic dev; vLLM is the production engine for high-throughput serving of many concurrent users. It applies the inference-track levers from IC6 — PagedAttention (KV-cache paging), continuous batching, and quantization — behind the same OpenAI-compatible API.
✓ Knowledge check

What two vLLM flags are the main memory dials when you hit an out-of-memory error, and what does each control?

Show answer
--gpu-memory-utilization (e.g. lower to 0.80) sets the fraction of GPU memory vLLM may use for weights + KV-cache, and --max-num-seqs sets the continuous-batch width (how many requests are batched at once). Lowering either reduces memory pressure.
© 2026 studybydoing.in · AI Engineering: Zero to Production · All rights reserved. · About · Privacy Policy · Terms · Contact
Educational content, provided as-is and without warranty. Code samples are examples — review, test, and adapt them before using in production. See the Terms of Use & Disclaimer. Use at your own risk.
© studybydoing.in