Project walkthrough · 12 min read

Local LLM Deployment with Python: Ollama, VRAM, and a Real Endpoint

A local model is a process with a memory budget and a latency profile, not a magic box. This walkthrough covers the decisions that decide whether it survives the afternoon: quantization, VRAM math, one HTTP endpoint, a fallback chain, and the numbers to watch.

Read the tag before you pull

The suffix on a model name carries the quantization. q4_K_M means 4-bit weights with K-grouped scales on the sensitive tensors: about 4.5 bits per parameter in practice. Fewer bits means less VRAM and a little less accuracy, so start at Q4_K_M and move up only when your own evaluation asks for it.

params_b = 7
bits_per_weight = 4.5          # Q4_K_M, not exactly 4
weights_gb = params_b * 1e9 * bits_per_weight / 8 / 1024**3
print(round(weights_gb, 2))

Weights first, opinions later

Budget the KV cache as well

Weights are only half the bill. Every token in the context keeps a key and a value vector in every layer, and at 8k context that is often another 2 to 4 GB. When weights plus cache do not fit, the model spills to CPU and throughput collapses by an order of magnitude.

def kv_cache_gb(layers, hidden, ctx, bytes_per=2):
    # two tensors, K and V, fp16 by default
    return 2 * layers * hidden * ctx * bytes_per / 1024**3

print(round(kv_cache_gb(32, 4096, 8192), 2))

Context is not free

Serve it, then call it like any API

ollama serve exposes an HTTP surface on port 11434. A blocking POST to /api/generate with stream false returns one JSON object. Set a timeout longer than the slowest generation you expect; the default of a few seconds cuts real answers in half.

import requests

payload = {
    "model": "qwen2.5:7b-instruct-q4_K_M",
    "prompt": "Say hi in five words.",
    "stream": False,
}
resp = requests.post("http://127.0.0.1:11434/api/generate", json=payload, timeout=120)
resp.raise_for_status()
print(resp.json()["response"])

One endpoint, one timeout

Stream when a human is waiting

With stream true the body is newline-delimited JSON: each line carries a fragment in response, and done flips true on the last one. Parse line by line, skip empty lines, print as you go. The user sees words in under a second instead of staring at a spinner.

import json, requests

with requests.post(url, json=payload, stream=True, timeout=120) as resp:
    for line in resp.iter_lines():
        if not line:
            continue
        chunk = json.loads(line)
        print(chunk.get("response", ""), end="", flush=True)
        if chunk.get("done"):
            break

NDJSON, one fragment per line

Degrade to a smaller model, not to a 500

Out-of-memory kills and daemon 500s are normal on a laptop. Keep an ordered list of models and fall through it, then report which one answered so the logs explain the quality drop later. A slower answer beats a stack trace.

MODELS = ["qwen2.5:7b-instruct-q4_K_M", "qwen2.5:3b-instruct-q4_K_M"]

def generate(prompt):
    for name in MODELS:
        try:
            return name, call(name, prompt)
        except (TimeoutError, RuntimeError):
            continue
    raise RuntimeError("all local models failed")

A fallback chain you can log

Watch latency, throughput, and failures

First-token latency is what users feel; tokens per second is what the hardware delivers. Track both plus the failure rate per model, and alarm on the median rather than the mean, because one 30-second outlier should not page anyone at night.

import statistics

first_token_ms = [820, 910, 780, 2400, 860]
print(round(statistics.median(first_token_ms)), max(first_token_ms))

Medians, not means

Your practice checklist

Common questions

How much VRAM does a 7B model at Q4 need?

About 4 GB of weights plus 1 to 4 GB of cache depending on context length. An 8 GB card is comfortable; 6 GB works if you shorten the context.

Can I run this on CPU only?

Yes, at a few tokens per second. That is fine for overnight batch work and painful for chat.

Ollama or llama.cpp directly?

Ollama when you want model management and a stable HTTP surface. llama.cpp when you need to tune samplers or run on unusual hardware.

Local or hosted models?

Local when data cannot leave the machine or volume is high and quality is sufficient; hosted when you need the strongest model or burst capacity.

Practice model monitoring

Questions on drift, latency, evaluation on small samples, and decision thresholds.

Open practice

Keep reading: