Hoppa till innehållet
AI-grafen
E· Universitetinferens-optimering· ca 60 min· utvecklande· verifierad 2026-09-20

Köra modeller lokalt: llama.cpp, vLLM, Ollama

Kunna köra en språkmodell lokalt och mäta tokens per sekund.

Förkunskaper

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älKommentar
Datan lämnar inte husetofta det avgörande skälet
Ingen kostnad per anropbara hårdvara och el
Fungerar utan internet
Full kontroll över versionermodellen ändras inte under fötterna på dig
Låg latens till första tokeningen 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:

VerktygTill för
Ollamaenklast att komma igång, en användare
llama.cppCPU och blandad CPU/GPU, mest portabelt
vLLMservering till många samtidiga, hög genomströmning

Formellt

Minnesbehovet styrs av kvantiseringen:

KvantiseringByte/parameter8B-modellKvalitetsförlust
fp162~16 GBingen
Q81~8 GBförsumbar
Q5_K_M~0,7~5,6 GBliten
Q4_K_M~0,6~4,8 GBmärkbar men acceptabel
Q3~0,45~3,6 GBtydlig
Q2~0,3~2,4 GBofta oanvändbar

Plus KV-cachen, som växer med kontextlängden: ungefär 2⋅L⋅H⋅dhead⋅ntokens⋅2 \cdot L \cdot H \cdot d_{\text{head}} \cdot n_{\text{tokens}} \cdot 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.

tokens/s≲minnesbandbreddmodellstorlek i byte\text{tokens/s} \lesssim \frac{\text{minnesbandbredd}}{\text{modellstorlek i byte}}

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åttBetyderBunden av
Prefill (prompt processing)tokens/s när prompten läsesberäkning — parallelliserbart
Decode (generation)tokens/s när svaret skrivsbandbredd — 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

Alla källor och licenser