Benchmark lokalnog modela: koliko tokena u sekundi zaista dobijaš

„Sporo je" nije podatak. Bez broja ne znaš da li je model loše izabran, da li se deo slojeva preliva na procesor, ili je sve normalno i očekivanja su bila pogrešna.

Ovaj tekst pokazuje šta se meri, čime i kako se rezultat čita. Sve dalje u Fazi 5 — nadzor, kapacitet, trošak — počiva na brojevima odavde.

Šta se meri

Dve faze rade po različitim pravilima, kako je objašnjeno u tekstu o CPU i GPU inferenciji:

Mera Šta govori Ograničena čime
Prefill (pp) Brzina čitanja prompta Računom
Decode (tg) Brzina generisanja odgovora Propusnošću memorije
TTFT Čekanje na prvi znak Dužinom prompta
Propusnost Ukupno tokena u sekundi, svi korisnici Postavkom servisa

Jedan broj ne opisuje postavku. Model može da generiše šezdeset tokena u sekundi, a da korisnik čeka pola minuta na prvi znak jer je prompt dugačak. Za razgovor je bitan TTFT plus decode; za paketnu obradu jedino ukupna propusnost.

llama-bench

Najčistije merenje, bez servera i HTTP-a između:

$ llama-bench -m ~/models/qwen3-8b-q4_k_m.gguf -ngl 99
| model             |     size | test   |            t/s |
| ----------------- | -------: | ------ | -------------: |
| qwen3 8B Q4_K_M   | 4.68 GiB | pp512  | 3421.53 ± 24.1 |
| qwen3 8B Q4_K_M   | 4.68 GiB | tg128  |   58.34 ± 0.42 |

Oznaka pp512 je obrada prompta od 512 tokena, tg128 generisanje 128 tokena. Odnos je tipičan — prefill je pedesetak puta brži od decode.

Merenje sa dužim promptom i različitim rasporedom slojeva:

$ llama-bench -m model.gguf -p 512,2048,8192 -n 128 -ngl 99
$ llama-bench -m model.gguf -ngl 0,20,40,99

Ovo drugo je najkorisnije merenje u celom tekstu — pokazuje tačno koliko košta svaki sloj koji ostane na procesoru:

| ngl | tg128 t/s |
| --- | --------: |
|   0 |      8.12 |
|  20 |     14.73 |
|  40 |     41.20 |
|  99 |     58.34 |

Pad nije srazmeran. Kako pokazuje red sa ngl 40, poslednjih nekoliko slojeva na procesoru odnosi trećinu brzine.

Poređenje kvantizacija, uz podatak iz teksta o kvantizaciji:

$ for v in q4_k_m q5_k_m q8_0; do
    echo "=== $v ==="
    llama-bench -m ~/models/qwen3-8b-$v.gguf -ngl 99 -n 128
  done

Alat je deo llama.cpp postavke iz teksta o kompajliranju.

Merenje kroz API

llama-bench meri model. Kroz API meriš ono što korisnik zaista dobija, sa svim slojevima između.

$ curl -s -o /dev/null -w 'ukupno: %{time_total}s\n' \
  http://localhost:11434/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"model":"qwen3:8b","messages":[{"role":"user","content":"Nabroj deset Linux komandi"}],"max_tokens":200}'

Ollamin sopstveni put daje razložene brojeve, u nanosekundama:

$ curl -s http://localhost:11434/api/generate \
  -d '{"model":"qwen3:8b","prompt":"Objasni šta je inode","stream":false}' \
  | jq '{
      ucitavanje: (.load_duration/1e9),
      prefill_ts: (.prompt_eval_count / (.prompt_eval_duration/1e9)),
      decode_ts: (.eval_count / (.eval_duration/1e9)),
      ukupno: (.total_duration/1e9)
    }'
{
  "ucitavanje": 0.021,
  "prefill_ts": 2847.3,
  "decode_ts": 56.8,
  "ukupno": 3.42
}

Ako je ucitavanje veliko, model je bio izbačen iz memorije — podigni OLLAMA_KEEP_ALIVE, kako stoji u tekstu o systemd jedinici.

TTFT

Merи se samo kroz streaming:

#!/bin/bash
POCETAK=$(date +%s.%N)

curl -sN http://localhost:11434/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"model":"qwen3:8b","messages":[{"role":"user","content":"Objasni šta je inode"}],"stream":true}' \
| while IFS= read -r red; do
    case "$red" in
      data:*content*)
        PRVI=$(date +%s.%N)
        echo "TTFT: $(echo "$PRVI - $POCETAK" | bc) s"
        break
        ;;
    esac
  done

Ovaj broj je ono što korisnik doživljava kao „brzo" ili „sporo". Ispod sekunde deluje trenutno; preko tri sekunde deluje pokvareno, čak i ako je decode odličan.

Merenje pod opterećenjem

Ovde se vidi razlika između Ollame i vLLM-a, koja se sa jednim zahtevom ne primećuje.

#!/usr/bin/env python3
"""Merenje propusnosti sa N paralelnih zahteva."""

import sys, time, statistics
from concurrent.futures import ThreadPoolExecutor
import requests

URL = "http://localhost:11434/v1/chat/completions"
MODEL = "qwen3:8b"
PITANJE = "Objasni ukratko šta je systemd i čemu služi."

def jedan(_):
    t0 = time.perf_counter()
    o = requests.post(URL, json={
        "model": MODEL,
        "messages": [{"role": "user", "content": PITANJE}],
        "max_tokens": 200,
        "temperature": 0,
    }, timeout=300)
    trajanje = time.perf_counter() - t0

    if o.status_code != 200:
        return None
    return trajanje, o.json()["usage"]["completion_tokens"]

def meri(paralelno, ukupno=None):
    ukupno = ukupno or paralelno * 3

    t0 = time.perf_counter()
    with ThreadPoolExecutor(max_workers=paralelno) as izvrsilac:
        rezultati = [r for r in izvrsilac.map(jedan, range(ukupno)) if r]
    proteklo = time.perf_counter() - t0

    if not rezultati:
        print(f"{paralelno:3d} — svi zahtevi neuspeli")
        return

    trajanja = [t for t, _ in rezultati]
    tokeni = sum(n for _, n in rezultati)

    print(f"{paralelno:3d} paralelno | "
          f"ukupno {tokeni/proteklo:7.1f} t/s | "
          f"po zahtevu {tokeni/len(rezultati)/statistics.mean(trajanja):5.1f} t/s | "
          f"medijana {statistics.median(trajanja):5.2f} s | "
          f"neuspelih {ukupno-len(rezultati)}")

if __name__ == "__main__":
    for n in [1, 2, 4, 8, 16]:
        meri(n)
        time.sleep(2)
$ python3 opterecenje.py
  1 paralelno | ukupno    56.2 t/s | po zahtevu  56.2 t/s | medijana  3.56 s | neuspelih 0
  2 paralelno | ukupno    58.9 t/s | po zahtevu  29.4 t/s | medijana  6.81 s | neuspelih 0
  4 paralelno | ukupno    61.3 t/s | po zahtevu  15.3 t/s | medijana 13.07 s | neuspelih 0
  8 paralelno | ukupno    62.0 t/s | po zahtevu   7.7 t/s | medijana 25.94 s | neuspelih 0

Ovo je Ollama. Ukupna propusnost stoji, a pojedinačni korisnik dobija sve manje — zahtevi se rede, ne obrađuju uporedo.

Isti test na vLLM-u:

  1 paralelno | ukupno    52.4 t/s | po zahtevu  52.4 t/s | medijana  3.82 s | neuspelih 0
  2 paralelno | ukupno   101.7 t/s | po zahtevu  50.8 t/s | medijana  3.94 s | neuspelih 0
  4 paralelno | ukupno   196.3 t/s | po zahtevu  49.1 t/s | medijana  4.07 s | neuspelih 0
  8 paralelno | ukupno   378.9 t/s | po zahtevu  47.4 t/s | medijana  4.22 s | neuspelih 0

Propusnost raste gotovo linearno, a pojedinačni korisnik jedva primećuje da nije sam. To je ceo argument za vLLM, i on postoji samo pod opterećenjem — sa jednim zahtevom vLLM je čak nešto sporiji.

Tokom testa prati karticu u drugom terminalu:

$ watch -n 1 nvidia-smi

Potrošnja blizu granice znači da je kartica zasićena i da dalje podizanje broja zahteva ne donosi ništa. Čitanje ispisa pokriva tekst o nvidia-smi.

Poštena merenja

Nekoliko pravila bez kojih brojevi ne znače ništa.

Zagrej pre merenja. Prvi zahtev uključuje učitavanje modela. Baci ga.

Fiksiraj dužinu izlaza. Bez max_tokens model jednom napiše sto tokena, drugi put četiristo, i poređenje otpada.

Temperatura na nulu. Isti ulaz, isti izlaz, ista dužina.

Meri više puta i uzmi medijanu. Jedno merenje nije podatak.

Zapiši uslove. Model, kvantizacija, kontekst, verzija alata, temperatura kartice. Bez toga poređenje sa merenjem od pre mesec dana ne vredi.

$ cat > merenje.txt <<EOF
Datum:        $(date -Is)
Host:         $(hostname)
Kartica:      $(nvidia-smi --query-gpu=name --format=csv,noheader)
Drajver:      $(nvidia-smi --query-gpu=driver_version --format=csv,noheader)
Model:        qwen3:8b Q4_K_M
Alat:         $(ollama --version)
Kontekst:     16384
EOF

Proveri da nema termalnog usporavanja. Duži test podigne temperaturu, pa poslednja merenja budu lošija bez ikakvog drugog razloga:

$ nvidia-smi --query-gpu=clocks_throttle_reasons.active,temperature.gpu --format=csv

Čitanje rezultata

Nalaz Šta znači
Decode ispod 10 t/s na kartici Deo slojeva je na procesoru
Prefill blizu decode brzine Model uopšte ne radi na kartici
Visok TTFT, dobar decode Prompt je predugačak ili se model učitava
Propusnost ne raste sa korisnicima Ollama umesto vLLM-a, ili pun KV keš
Brzina opada kroz test Termalno usporavanje
Velik rasip između merenja Nešto drugo koristi karticu

Šta je dobar broj

Za kontekst, iz ranijeg teksta: ispod deset t/s je za obradu u pozadini, oko petnaest je granica udobnosti za razgovor, preko trideset je prijatno.

Gruba provera da li je tvoj broj razuman — propusnost memorije kartice podeljena veličinom modela daje teorijski maksimum. Realno dobijaš pedeset do sedamdeset posto toga. Ako si znatno ispod, nešto nije u redu.

Praktični scenariji

Scenario 1: llama-bench pokazuje dobro, kroz API je sporo

Problem je iznad modela — proxy, mreža, ili preveliki podrazumevani kontekst. Meri redom, sloj po sloj.

Scenario 2: brzina se razlikuje između pokretanja

Model se izbacuje iz memorije. Proveri load_duration i podigni keep-alive.

Scenario 3: prvi test brz, poslednji spor

Kartica se zagrejala. Proveri razlog usporavanja i ubaci pauzu između merenja.

Scenario 4: propusnost pada pri osam zahteva

KV keš je pun. Kod vLLM-a proveri gpu_cache_usage_perc na /metrics.

Scenario 5: rezultat ne liči na ono što drugi objavljuju

Verovatno se meri nešto drugo — druga dužina prompta, druga kvantizacija, ili propusnost umesto brzine po korisniku. Poredi samo svoje merenje sa svojim.

Scenario 6: sve deluje dobro, korisnici se žale

Meriš decode, a njima smeta TTFT. Izmeri ga posebno.

Kratka referenca

  • llama-bench -m model.gguf -ngl 99 — merenje bez servera
  • llama-bench -ngl 0,20,40,99 — cena prelivanja na procesor
  • llama-bench -p 512,2048,8192 — uticaj dužine prompta
  • pp — prefill, tg — decode
  • /api/generate + jq — razloženi brojevi kod Ollame
  • load_duration — ako je velik, model je bio izbačen
  • TTFT — meri se samo kroz streaming
  • curl -w '%{time_total}' — ukupno vreme zahteva
  • /metrics kod vLLM-a — gpu_cache_usage_perc
  • Zagrej, fiksiraj max_tokens, temperatura 0, medijana
  • clocks_throttle_reasons.active — provera usporavanja
  • Ollama i vLLM se razlikuju samo pod opterećenjem

Vežba

  1. Izmeri prefill i decode svog modela kroz llama-bench.
  2. Pokreni merenje sa -ngl 0,20,40,99 i nacrtaj kako brzina pada.
  3. Izmeri TTFT za kratak i za dugačak prompt, pa uporedi.
  4. Pokreni test opterećenja sa 1, 4 i 8 paralelnih zahteva i objasni oblik rezultata.
  5. Ako imaš i vLLM, ponovi isti test i uporedi obe kolone.
  6. Zapiši uslove merenja u fajl, pa ponovi merenje za nedelju dana i uporedi.

Sledeći tekst u serijalu: Monitoring inferencije sa Prometheusom i Grafanom

Povezano:

Comments

Popular posts from this blog

Početak u Linuxu — šta je i zašto se uči

Konverzija tipova podataka u Pythonu

groupadd