Köra modeller lokalt: llama.cpp, vLLM, Ollama
Kunna köra en språkmodell lokalt och mäta tokens per sekund.
Förkunskaper
- EDocker — containrarkrävs
- FKvantiseringkrävs
Intuition
Att köra modeller lokalt har blivit rimligt. En 7–8B-modell kvantiserad till 4 bitar ryms i cirka 5 GB och går att köra på en vanlig bärbar dator.
Skäl att göra det:
| Skäl | Kommentar |
|---|---|
| Datan lämnar inte huset | ofta det avgörande skälet |
| Ingen kostnad per anrop | bara hårdvara och el |
| Fungerar utan internet | |
| Full kontroll över versioner | modellen ändras inte under fötterna på dig |
| Låg latens till första token | ingen nätverksrestid |
Skäl att låta bli: de största modellerna ryms inte, kvaliteten på en 8B-modell är märkbart lägre än på en frontier-modell, och du ansvarar själv för drift, uppdateringar och säkerhet.
Tre verktyg som täcker de flesta behov:
| Verktyg | Till för |
|---|---|
| Ollama | enklast att komma igång, en användare |
| llama.cpp | CPU och blandad CPU/GPU, mest portabelt |
| vLLM | servering till många samtidiga, hög genomströmning |
Formellt
Minnesbehovet styrs av kvantiseringen:
| Kvantisering | Byte/parameter | 8B-modell | Kvalitetsförlust |
|---|---|---|---|
| fp16 | 2 | ~16 GB | ingen |
| Q8 | 1 | ~8 GB | försumbar |
| Q5_K_M | ~0,7 | ~5,6 GB | liten |
| Q4_K_M | ~0,6 | ~4,8 GB | märkbar men acceptabel |
| Q3 | ~0,45 | ~3,6 GB | tydlig |
| Q2 | ~0,3 | ~2,4 GB | ofta oanvändbar |
Plus KV-cachen, som växer med kontextlängden: ungefär bytes. För en 8B-modell med 8 k kontext är det några hundra megabyte till ett par gigabyte beroende på precision.
Tumregel: Q4_K_M är den bästa avvägningen för de flesta. Under Q4 faller kvaliteten snabbt.
Vad som faktiskt begränsar hastigheten. Generering är minnesbandbreddsbunden, inte beräkningsbunden: för varje token måste alla modellvikter läsas från minnet.
En 5 GB-modell på ett system med 100 GB/s bandbredd kan alltså i bästa fall nå cirka 20 tokens/s. Det förklarar varför en snabbare processor knappt hjälper, medan snabbare minne gör det — och varför Apples enhetliga minne (hög bandbredd) presterar oväntat bra på den här uppgiften.
Två tal att mäta, och de är olika:
| Mått | Betyder | Bunden av |
|---|---|---|
| Prefill (prompt processing) | tokens/s när prompten läses | beräkning — parallelliserbart |
| Decode (generation) | tokens/s när svaret skrivs | bandbredd — sekventiellt |
Prefill kan vara tio gånger snabbare än decode. Ett verktyg som bara rapporterar ett tal döljer vilket.
Batchning hjälper bara genomströmning, inte latens för en enskild användare — men den hjälper mycket: vikterna läses en gång för hela batchen. Det är hela poängen med vLLM och dess PagedAttention, som gör KV-cachen sidbaserad så att många samtidiga sekvenser kan dela minne effektivt.
Kod
# Ollama — enklast
ollama pull llama3.1:8b-instruct-q4_K_M
ollama run llama3.1:8b-instruct-q4_K_M "Förklara gradientnedstigning på svenska."
# Mät via API:t — Ollama rapporterar prefill och decode separat
curl -s http://localhost:11434/api/generate -d '{
"model": "llama3.1:8b-instruct-q4_K_M",
"prompt": "Skriv 200 ord om Fourieranalys.",
"stream": false
}' | python3 -c '
import json, sys
d = json.load(sys.stdin)
prefill = d["prompt_eval_count"] / (d["prompt_eval_duration"] / 1e9)
decode = d["eval_count"] / (d["eval_duration"] / 1e9)
print(f"prefill {prefill:.1f} tok/s decode {decode:.1f} tok/s")
ttft = d["prompt_eval_duration"] / 1e9
print(f"tid till första token: {ttft:.2f} s")
'
# Mät själv: latens och genomströmning skiljer sig
import time, requests
def matt(modell, prompt, n=5, url="http://localhost:11434/api/generate"):
ttft, tps = [], []
for _ in range(n):
t0 = time.perf_counter()
r = requests.post(url, json={"model": modell, "prompt": prompt, "stream": True},
stream=True)
forsta = None
tokens = 0
for rad in r.iter_lines():
if not rad:
continue
if forsta is None:
forsta = time.perf_counter() - t0
tokens += 1
total = time.perf_counter() - t0
ttft.append(forsta); tps.append(tokens / total)
ttft.sort(); tps.sort()
return {"ttft_p50": round(ttft[len(ttft)//2], 3),
"ttft_p95": round(ttft[int(0.95*len(ttft))-1], 3),
"tokens_per_s": round(sum(tps)/len(tps), 1)}
# Teoretiskt tak: bandbredden delat med modellstorleken
def tak(modell_gb, bandbredd_gb_per_s):
return round(bandbredd_gb_per_s / modell_gb, 1)
for namn, gb, bw in (("8B Q4 på laptop", 4.8, 100), ("8B Q4 på GPU", 4.8, 900),
("70B Q4 på GPU", 40.0, 900)):
print(f"{namn:<18} tak ~{tak(gb, bw):>5.1f} tokens/s")
# 8B Q4 på laptop tak ~ 20.8 tokens/s
# 8B Q4 på GPU tak ~187.5 tokens/s
# 70B Q4 på GPU tak ~ 22.5 tokens/s
De tre raderna i slutet förklarar mer om lokal inferens än någon benchmark: hastigheten följer bandbredd delat med modellstorlek, och därför är en stor modell på ett snabbt kort ungefär lika snabb som en liten på en laptop.
Behärskning innebär
- Kör en modell lokalt
- Mäter genomströmning och latens
- Väljer verktyg och kvantisering efter hårdvara
Logga in för att göra övningarna och bygga upp din behärskning.
Källor
- llama.cpp (MIT) — MIT
- vLLM — dokumentation (Apache-2.0) — Apache-2.0
- arXiv — Efficient Memory Management for Large Language Model Serving with PagedAttention — arXiv (öppen åtkomst; licens per artikel)