You Are Not Buying a Model, You Are Running a Service

The conversation usually starts with an invoice or a compliance review. The API bill crossed some line, or legal said customer documents cannot leave the region, and someone suggests running an open-weight model on our own GPUs. Two weeks later the model answers prompts on a test box, and everyone agrees the hard part is done.

It is not. Getting a model to respond is an afternoon. Running it as a service that stays up under load, scales, gets patched and costs less than the API it replaced is the actual project. This article covers the decisions that determine whether that project succeeds: whether to self-host at all, how to size the hardware, which inference server to run, and what to watch once it is live.

Self-hosting is worth it when
  • Data cannot leave your network or region, and no provider deployment satisfies that
  • Traffic is high and steady enough to keep GPUs busy most of the day
  • You need a fine-tuned or specialised open model the APIs do not offer
  • You need latency or locality guarantees a shared endpoint cannot give

“It will be cheaper” on its own is not on this list. It is cheaper only at sustained utilisation, and spiky traffic rarely gets there.

Do the Break-Even Maths First

An API charges per token. A GPU charges per hour whether it is busy or not. The question is how many tokens an hour you can push through the GPU at the quality and latency you need, and how many of those hours are actually busy.

effective cost per 1M tokens =
    (GPU cost per hour x hours provisioned)
  / (tokens served per hour at target latency x hours provisioned x utilisation)
  x 1,000,000

Inputs you measure, not guess:
  GPU cost per hour      your cloud or hardware quote
  tokens served per hour from a benchmark with YOUR prompt and output lengths
  utilisation            share of provisioned hours spent serving real traffic

Utilisation is the term that sinks most business cases. A service that runs at full throughput during office hours and idles at night is roughly 30 to 40% utilised across a week, which can more than double the effective cost per token. Before buying anything, export a week of request timestamps and token counts from your current API usage and plot it per hour. If the shape is a sharp daytime peak, price in either autoscaling to zero (with slow cold starts, covered below) or paying for idle GPUs.

Also count the people. Someone owns driver updates, model upgrades, incident response and capacity planning. If that is a fraction of an engineer you did not budget, the comparison is not apples to apples.

Sizing: Weights Plus KV Cache

GPU memory goes on two things. The weights are fixed: parameters multiplied by bytes per parameter. The KV cache grows with every token of every request in flight, and it is what limits how many users you can serve at once.

weights   = parameters x bytes per parameter
            FP16/BF16 = 2 bytes, FP8 = 1 byte, INT4 = about 0.5 bytes

KV cache per token = 2 (K and V) x layers x KV heads x head dim x bytes

Example: an 8B model with grouped-query attention
  (32 layers, 8 KV heads, head dim 128, FP16 cache)
  weights  = 8B x 2 bytes          = 16 GB
  KV/token = 2 x 32 x 8 x 128 x 2  = 131,072 bytes = 128 KiB
  one 32k-token conversation       = 4 GiB of KV cache

On an 80 GB card with 90% of memory given to the server, the 16 GB of weights leave about 56 GB of cache: roughly a dozen full 32k-token conversations at once, or many more short ones. That is why the maximum context length you configure matters so much: allowing 128k tokens per request when your real prompts are 4k reserves capacity you will never use. Set the limit from your actual prompt distribution, with headroom, not from the model card.

For models that do not fit on one GPU, tensor parallelism splits each layer across GPUs in the same machine. It needs fast interconnect between the cards, so keep a tensor-parallel group inside one node. Quantisation is the other lever: FP8 roughly halves weight memory with a small quality cost on most tasks, and INT4 halves it again with a larger and more task-dependent cost. Measure on your own evaluation set before and after; averages from someone else’s benchmark do not tell you what happens to your extraction prompts.

Choosing an Inference Server

ServerStrengthWhere I use it
vLLMPagedAttention for KV memory, continuous batching, prefix caching, OpenAI-compatible API, wide model supportThe default for production GPU serving
SGLangRadixAttention prefix sharing and fast structured output; strong on agent-style workloads with shared prefixesWhen many requests share long prefixes or need constrained decoding at volume
llama.cpp / OllamaRuns GGUF quantised models on CPUs, laptops and small GPUs with almost no setupDevelopment, demos and low-traffic internal tools

Continuous batching is the feature that makes the economics work. Instead of waiting for a batch to finish, the server adds new requests to the running batch as slots free up, so a GPU serving many users produces far more tokens per second than one serving requests one at a time. Any server without it is a development tool.

A Production-Shaped vLLM Launch

vllm serve meta-llama/Llama-3.3-70B-Instruct \
  --tensor-parallel-size 4 \
  --max-model-len 16384 \
  --gpu-memory-utilization 0.90 \
  --api-key "$VLLM_API_KEY" \
  --port 8000
  • --tensor-parallel-size 4 splits the 70B model across four GPUs in one node.
  • --max-model-len 16384 caps context at what the application really sends, which frees KV cache for more concurrent requests.
  • --gpu-memory-utilization 0.90 lets vLLM claim 90% of each card for weights and cache, leaving headroom for the CUDA context.
  • --api-key because an inference server with no authentication on an internal network is a data-exfiltration endpoint waiting to be found. Put it behind your gateway as well.

Then benchmark with your traffic shape before you promise anyone a latency: vllm bench serve with input and output lengths taken from your real prompts, at the concurrency you expect at peak. Short synthetic prompts flatter every server.

Running It on Kubernetes

Most teams end up on a Kubernetes cluster with a GPU node pool. The parts that differ from a normal web service:

  • Cold starts are minutes, not seconds.Pulling a large image and loading tens of gigabytes of weights takes time. Cache weights on a persistent volume or node-local disk instead of downloading them on every start, and set the readiness probe on the server’s health endpoint so traffic arrives only after loading finishes.
  • Scale on queue depth, not CPU. CPU tells you nothing about a GPU server. Scale on waiting requests or KV cache usage exported to Prometheus, through KEDA or a custom metrics adapter.
  • One model per deployment. Different models have different memory profiles and upgrade cycles. Route between them in a gateway rather than packing them into one pod.

What to Watch

vLLM exposes Prometheus metrics on /metrics. The handful that tell you the most:

MetricWhat it tells you
vllm:num_requests_waitingRequests queued for a slot. Sustained growth means you need more capacity.
vllm:kv_cache_usage_percShare of KV cache in use. Near 1.0, the server starts preempting requests and latency jumps.
vllm:time_to_first_token_secondsHow long users wait before anything streams. Alert on p95, not the mean.
vllm:inter_token_latency_secondsStreaming speed once output starts. Degrades as batches grow.
vllm:prefix_cache_hits / queriesHow often shared prompt prefixes are reused. A low ratio on a chat workload usually means the system prompt changes per request.
What to take away
  • Self-host for data control, steady volume or a model you cannot get otherwise; cost alone rarely justifies it.
  • Effective cost depends on utilisation. Plot your real traffic before you buy GPUs.
  • Memory is weights plus KV cache; cap context length at what you actually send.
  • vLLM is the default server. Benchmark with your own prompt and output lengths.
  • Scale on queue depth and KV usage, cache weights locally, and put authentication in front.

Pitfalls

Benchmarking with toy prompts

Fifty-token prompts and fifty-token answers produce throughput numbers you will never see in production, where a RAG prompt is several thousand tokens. Prefill on long prompts is where latency goes. Benchmark with real lengths.

Wrong chat template

Open models expect a specific chat format. Send raw text or the wrong template and quality drops in ways that look like a bad model. Use the server’s chat completions endpoint so the model’s own template is applied, and check it whenever you change model versions.

Licence terms nobody read

“Open weights” does not mean no conditions. Some licences restrict use above a user count, require attribution or prohibit certain uses. Have someone read the licence of the exact model version before it carries production traffic.

Swapping models without an eval

A newer model with better benchmark scores can still be worse on your task. Run your evaluation set against the candidate behind the same server settings, and roll it out gradually behind the gateway, as you would any other dependency upgrade.