Ruler

Który model system one z rodziny basal zmieści się na karcie graficznej z 16 GB VRAM?


Dzisiaj na warsztat idzie rodzina modeli basal: basal-2.0, basal-omni-1.0 oraz basal-omni-pico. Skupiam się na wersjach GGUF uruchamianych na llama.cpp server w kontenerze docker, a natywną ścieżkę (transformers) dokładam tylko jako punkt odniesienia.

basal to otwarte (Apache-2.0) modele decyzyjne (System One) dla języka polskiego i angielskiego, projekt Remigiusza Kinasa. Model dostaje stan (tekst) oraz pytania z dozwolonymi odpowiedziami i zwraca ich prawdopodobieństwa, więc klasyfikator definiujesz w każdym zapytaniu, bez trenowania. Modele obsługuje basal-serve, który liczy albo na własnym backendzie, albo - jak tutaj - przez osobny serwer llama.cpp (--mode llamacpp). W dalszej części “System 2” oznacza zwykły, generatywny LLM, który obok szybkiej warstwy decyzyjnej może działać na tej samej karcie.

Czego tu nie ma: porównania z modelami spoza rodziny basal ani ocen jakości poza wbudowanym benchmarkiem. Największy praktyczny zysk nie wyszedł z wyboru modelu, tylko z jednej flagi basal-serve (--http-parallel, do 2x przepustowości) - szczegóły w sekcji o teście obciążeniowym.

Accuracy w tym artykule pochodzi z benchmarka uruchamianego w tym stacku. To nie jest Werdykt (5 000 decyzji PL+EN), którym projekt basal opisuje modele na swoim blogu, więc liczb z obu źródeł nie porównuj.


Technikalia

Środowisko testowe

  • GPU: RTX 4070 Ti SUPER, 16376 MiB, sterownik 580.178.04
  • Docker Engine 29.9.2
  • llama.cpp: ghcr.io/ggml-org/llama.cpp:server-cuda, build b11681 (commit 5e03a1ff)
  • pakiet basal w obrazie docker: v2.3.1, w trakcie testu zaktualizowany do v2.3.3 (kolumna pkg w tabelach). Wiersze omni-pico powstały dopiero na v2.3.3, bo według wpisu o wydaniu 2.3.2 dopiero ta wersja poprawnie obsługuje architekturę pico (Qwen3.5 z warstwami Gated DeltaNet)
  • pakiet basal natywny: v2.4.1 (projekt wydaje nowe wersje niemal codziennie)

Stack to dwa kontenery: llama-server (backend) i basal-serve (HTTP front, który renderuje i parsuje zapytania i woła backend).

# Dockerfile
ARG BASE_IMAGE=python:3.14-slim
FROM ${BASE_IMAGE}

ARG BASAL_REF=v2.3.3

ENV PIP_NO_CACHE_DIR=1 \
    PYTHONUNBUFFERED=1 \
    HF_HOME=/cache/huggingface

RUN pip install --extra-index-url https://download.pytorch.org/whl/cpu torch \
    && pip install "basal @ https://github.com/rkinas/basal/archive/refs/tags/${BASAL_REF}.tar.gz"

EXPOSE 8000

Poniżej przykładowe Docker Compose w wersji odpowiadającej konfiguracji (2.0-mini Q8_0, np 8, --http-parallel 32); pozostałe punkty pomiarowe to zmiana -m, -np, -c i --http-parallel.

# docker-compose.yml
name: basal-bench

services:
  basal-llm:
    image: ghcr.io/ggml-org/llama.cpp:server-cuda
    runtime: nvidia
    environment:
      NVIDIA_VISIBLE_DEVICES: all
      NVIDIA_DRIVER_CAPABILITIES: compute,utility
    command: >-
      -m /models/basal-2.0-mini-Q8_0.gguf
      --alias basal-2.0-mini-Q8_0
      -c 65536
      -np 8
      -ngl 99
      -ctk q8_0
      -ctv q8_0
      --host 0.0.0.0
      --port 8080
    volumes:
      - /path/to/models:/models:ro
    ports:
      - "127.0.0.1:${BASAL_BENCH_LLM_PORT:-8084}:8080"
    healthcheck:
      test: ["CMD-SHELL", "curl -sf http://127.0.0.1:8080/health || exit 1"]
      interval: 10s
      timeout: 5s
      retries: 30
      start_period: 30s

  basal-serve:
    image: basal-serve
    environment:
      HF_HOME: /cache/huggingface
    command: >-
      basal-serve
      --model Remek/basal-2.0-mini-GGUF
      --http-parallel 32
      --mode llamacpp
      --llamacpp-url http://basal-llm:8080
      --host 0.0.0.0
      --port 8000
    volumes:
      - basal_hf_cache:/cache/huggingface
    ports:
      - "127.0.0.1:${BASAL_BENCH_SERVE_PORT:-8005}:8000"
    depends_on:
      basal-llm:
        condition: service_healthy

volumes:
  basal_hf_cache:

Warianty KV cache to wyłącznie flagi backendu llama.cpp:

# f16: bez flag
# q8_0:
-ctk q8_0 -ctv q8_0
# q4_0:
-ctk q4_0 -ctv q4_0

Dwie rzeczy z compose’a, które zmieniają odczyt wyników:

  • llama.cpp -c = 8192 x -np (tu 65536 = 8192 x 8)
  • --http-parallel jest flagą basal-serve, nie llama.cpp. Przy domyślnym 1 backend z ośmioma slotami widzi jedno zapytanie naraz (tutaj 32).

W tabelach poniżej VRAM to wartość procesu llama-server z nvidia-smi (compute-apps), w MiB, nie całkowite zużycie karty. W prozie przeliczam na GiB.

Wariant CPU-only: ten sam build CPU llama.cpp bez runtime GPU, AMD Ryzen 5 5600X (6 rdzeni, 12 wątków, do 4.65 GHz), 32 GB RAM, domyślna liczba wątków (12, bez -t).

Modele basal

Rozmiary plików z ls -h, czyli w GiB/MiB:

model kwantyzacje wag w teście rozmiar pliku GGUF rozmiar pliku safetensors
basal-2.0-mini Q5_K_M, Q8_0 1,1 / 1,6 GiB 3,2 GiB
basal-2.0-4.5B Q5_K_M, Q8_0 3,2 / 4,8 GiB 9,5 GiB
basal-2.0-max Q8_0 12 GiB 22,3 GiB
basal-omni-pico Q8_0 775 MiB 1,5 GiB
basal-omni-1.0-mini Q6_K, Q8_0 5,8 / 7,5 GiB 15,9 GiB
basal-omni-1.0-max Q8_0 12 GiB 23,9 GiB
basal-omni-1.0-ultra nie testowany – 62,5 GiB

Model basal-omni-1.0-ultra jest zbyt duży, abym go mógł przetestować.

Modele omni (bazujące na Gemma 4) obsługują dodatkowo obraz i dźwięk. Ten test sprawdza tylko ścieżkę tekstową.


Sposób testowania

Plan

  1. Test w trybie natywnym.
  2. Test z użyciem modeli GGUF.

Narzędzia

Używam tylko narzędzi i danych testowych dostarczonych przez pakiet Python basal.

Benchmark basal-bench – Każda konfiguracja leci dwa razy (wcześniej wykonywałem więcej powtórzeń i wyniki były powtarzalne). Metryki: lat1 (opóźnienie jednego przejścia), lat2 (batch dwóch “orderów” w terminologii benchu) wraz z p95, dec/s (decyzje na sekundę, tak jak je liczy bench) oraz accuracy. Accuracy jest identyczne między dwoma przebiegami każdej konfiguracji, więc dwa przebiegi służą tylko do uśrednienia opóźnień.

Test obciążeniowy HTTP basal-loadtest – 32 klientów, krótkie prompty, wagi Q8_0, KV q8_0, basal v2.3.3, jeden przebieg na konfigurację. Kontekst skaluje się ze slotami: -c = 8192 x np, więc każdy slot ma okno 8192 tokenów.

Trzy zastrzeżenia, zanim przejdziemy do liczb:

  1. Bench ma 44 wbudowane elementy, więc wszystkie wartości accuracy są wielokrotnościami 1/44 (0.795 = 35/44, 0.773 = 34/44, 0.977 = 43/44), a jedno pytanie to 0.023. Różnice rzędu 0.02-0.05 to jedna-dwie odpowiedzi.
  2. Testy obciążeniowe po HTTP to pojedyncze przebiegi. Różnice kilku procent traktuję jako szum. Dwa niezależne pomiary tej samej konfiguracji (mini Q8_0, np 2, --http-parallel 1) dały 127.4 i 127.0 dec/s, a pico 52.8 i 53.2 dec/s, więc dec/s jest stabilne w granicach 1%. P95 szumi bardziej (pico: 42.3 vs 38.1 ms).
  3. Ścieżka natywna działa na pakiecie v2.4.1, a GGUF na v2.3.x, więc porównanie tych dwóch ścieżek miesza też wersje silnika.

Wyniki

Natywny basal-bench (transformers, bez llama.cpp)

basal-bench uruchomiony bezpośrednio na wagach z Hugging Face w safetensors (ścieżka referencyjna transformers), z domyślnymi trybami i na 44 wbudowanych pytaniach. Natywnie mierzyłem tylko mini i pico.

model mode lat2 [ms] lat1 [ms] dec/s agree acc pamięć torch [GB]
2.0-mini eager-fp32 52.9 35.8 19.1 – 0.750 8.1
2.0-mini fast 17.0 10.1 71.8 0.977 0.727 4.3
omni-pico eager-fp32 38.0 36.0 32.0 – 0.795 6.4
omni-pico eager 40.4 39.2 49.4 1.000 0.795 4.6

Ostatnia kolumna pochodzi z wyjścia benchu (pamięć na karcie raportowana po stronie torch), więc nie porównuję jej wprost z VRAM z nvidia-smi w tabelach llama.cpp poniżej, ale daje obraz zapotrzebowania na pamięć.

Spostrzeżenia:

  1. Tryb fast jest ok. 3.1x szybszy od eager-fp32 na mini (lat2 17.0 vs 52.9 ms), przy zgodności decyzji 97.7% i accuracy 0.727 zamiast 0.750 — jedno pytanie z 44.
  2. Pico nie ma trybu fast — domyślna para to eager-fp32 + eager. Zgodność 1.000, accuracy identyczne (0.795). Bench zgłasza dwa ostrzeżenia transformers: brak kerneli causal_conv1d i flash-linear-attention, więc referencyjny fallback PyTorcha leciał wolniej niż byłoby zoptymalizowane.
  3. omni-1.0-mini nie wstaje natywnie: 15,9 GiB safetensors nie mieści się na karcie 16 GB (15,99 GiB) razem z kontekstem CUDA i aktywacjami, więc nie ma pomiaru (oba formaty w tabeli w sekcji Modele). Zmierzone natywnie modele mają 1,5-3,2 GiB wag safetensors.
  4. dec/s z benchu nie wynika wprost z lat2. Natywne pico eager ma gorsze lat1/lat2 niż eager-fp32 (39.2/40.4 vs 36.0/38.0 ms), a lepsze dec/s (49.4 vs 32.0). Dlatego porównuję dec/s tylko w obrębie tego samego benchu.

basal-bench GPU-only llama.cpp

Parametry llama.cpp: -np 2, -c 16384, -ngl 99, a basal-serve uruchomiony z --http-parallel 1.

model wagi pkg KV lat1 [ms] lat2 [ms] p95 [ms] dec/s accuracy VRAM [MiB]
2.0-mini Q5_K_M 2.3.1 f16 9.7 12.7 14.8 52.8 0.750 1898
2.0-mini Q5_K_M 2.3.1 q8_0 10.3 13.6 15.0 49.7 0.727 1660
2.0-mini Q5_K_M 2.3.1 q4_0 ERROR bench ERROR
2.0-mini Q8_0 2.3.1 f16 10.3 14.0 15.0 48.6 0.727 2404
2.0-mini Q8_0 2.3.1 q8_0 11.6 15.5 16.8 45.2 0.773 2166
2.0-mini Q8_0 2.3.1 q4_0 11.2 15.2 16.4 46.9 0.341 2038
2.0-4.5B Q5_K_M 2.3.1 f16 20.1 26.1 28.7 24.9 0.750 4502
2.0-4.5B Q5_K_M 2.3.1 q8_0 21.8 28.3 31.3 23.7 0.773 4056
2.0-4.5B Q5_K_M 2.3.1 q4_0 21.7 28.0 30.7 24.3 0.591 3816
2.0-4.5B Q8_0 2.3.1 f16 22.1 30.9 33.2 22.7 0.795 6066
2.0-4.5B Q8_0 2.3.1 q8_0 23.9 33.1 35.9 21.6 0.795 5620
2.0-4.5B Q8_0 2.3.1 q4_0 23.7 32.8 35.0 22.2 0.659 5380
2.0-max Q8_0 2.3.1 f16 OOM OOM przy starcie
2.0-max Q8_0 2.3.1 q8_0 33.6 53.9 57.7 14.4 0.841 13266
2.0-max Q8_0 2.3.1 q4_0 33.2 53.7 56.6 14.8 0.841 12466
omni-pico Q8_0 2.3.3 f16 14.3 23.8 25.3 24.8 0.795 1294
omni-pico Q8_0 2.3.3 q8_0 14.5 24.1 26.2 24.8 0.795 1230
omni-pico Q8_0 2.3.3 q4_0 14.6 24.2 25.3 24.8 0.795 1182
omni-1.0-mini Q6_K 2.3.3 f16 39.1 57.4 64.1 12.5 0.886 4426
omni-1.0-mini Q6_K 2.3.3 q8_0 40.7 59.4 66.6 12.1 0.886 4282
omni-1.0-mini Q6_K 2.3.3 q4_0 40.5 59.1 65.8 12.3 0.909 4198
omni-1.0-mini Q8_0 2.3.1 f16 38.0 60.0 66.3 12.7 0.909 5448
omni-1.0-mini Q8_0 2.3.1 q8_0 40.0 62.3 67.8 12.3 0.909 5304
omni-1.0-mini Q8_0 2.3.1 q4_0 39.7 61.4 66.7 12.5 0.909 5220
omni-1.0-max Q8_0 2.3.1 f16 72.6 149.7 184.6 5.1 0.977 13676
omni-1.0-max Q8_0 2.3.1 q8_0 72.9 127.2 143.8 6.3 0.977 13114
omni-1.0-max Q8_0 2.3.1 q4_0 71.8 122.7 137.7 6.6 0.977 12810

Najlepsze accuracy na model:

  • 2.0-mini 0.773 (Q8_0, q8_0 KV)
  • 2.0-4.5B 0.795 (Q8_0),
  • 2.0-max 0.841,
  • omni-pico 0.795,
  • omni-1.0-mini 0.909,
  • omni-1.0-max 0.977.

Rzeczy, które widać w tabeli:

  1. KV q4_0 psuje modele 2.0 małe i średnie, ale nie max. 4.5B spada do 0.591/0.659 (Q5_K_M/Q8_0), mini Q8_0 do 0.341. Tymczasem 2.0-max na q4_0 trzyma 0.841, a modele omni nie ruszają się wcale (pico 0.795, omni-mini Q8_0 0.909, omni-max 0.977 na wszystkich trzech wariantach). Wyjątek to omni-mini Q6_K, gdzie accuracy zależy od cache (f16 0.886, q8_0 0.886, q4_0 0.909), czego na Q8_0 nie ma. Przy skali 1/44 to jedno pytanie, ale kierunek jest odwrotny do oczekiwanego (q4_0 najlepsze), więc ufałbym temu tylko jako dowodowi, że Q6_K jest czułe na konfigurację.

Accuracy dla dziewięciu konfiguracji model+wagi przy trzech wariantach KV cache: f16, q8_0, q4_0. Widoczne spadki na q4_0 dla 2.0-mini i 2.0-4.5B oraz brak reakcji modeli omni.

  1. Kwantyzacja KV głównie oszczędza VRAM, a nie przyspiesza. Na małych modelach lat2 rośnie przy przejściu z f16 na q8_0 (4.5B Q8_0: 30.9 -> 33.1 ms, mini Q8_0: 14.0 -> 15.5 ms, omni-mini Q8_0: 60.0 -> 62.3 ms). Oszczędność VRAM przy -c 16384 to 64-562 MiB (pico 64, omni-mini 144, mini 238, 4.5B 446, omni-max 562). Przy modelach 1-6 GiB nie ma to znaczenia, przy 13 GiB ma. Oszczędność rośnie liniowo z -c, więc w konfiguracjach z np 8 (-c 65536) jest 4x większa. Wyjątek: omni-1.0-max na f16 traci na batchu. lat1 zostaje przy ok. 73 ms, ale lat2 skacze do 149.7 ms, a p95 do 184.6 ms (q8_0: 127.2 i 143.8 ms, dec/s 5.1 -> 6.3). Mechanizmu nie badałem, wiem tylko, że to wynik powtarzalny w obu przebiegach. 2.0-max na f16 nie wstaje: cudaMalloc failed: out of memory. Z delt VRAM między wariantami KV wychodzi mi, że f16 potrzebowałby ok. 14 770 MiB (14,4 GiB; to moja ekstrapolacja, nie pomiar), a karta 16 GB VRAM musi jeszcze u mnie utrzymać aplikacje desktopowe. Wiersz oznaczony jako OOM. mini Q5_K_M z q4_0 wywala bench błędem KeyError: 'completion_probabilities', za każdym razem (sprawdzałem kilka razy). Nie wiem, czy to bug benchu, czy ta sama degradacja KV q4_0, która obniża mini Q8_0 do 0.341; nie badałem tego.

Zużycie VRAM (MiB) procesu llama-server dla dziewięciu konfiguracji model+wagi przy trzech wariantach KV cache: f16, q8_0, q4_0. Słupki oznaczone OOM i ERROR to konfiguracje, które nie wystartowały lub nie ukończyły benchu; przerywana linia to limit karty 16 376 MiB.

Przepustowość (dec/s) dla dziewięciu konfiguracji model+wagi przy trzech wariantach KV cache: f16, q8_0, q4_0. Kwantyzacja KV nie podnosi przepustowości; wyraźniejszy wzrost widać tylko dla omni-1.0-max.

Q6_K vs Q8_0 w omni-1.0-mini

Q6_K oszczędza dokładnie 1022 MiB (ok. 1,0 GiB) VRAM na każdym wariancie KV (4426/4198/4282 vs 5448/5220/5304 MiB) i jest przy tym odrobinę szybszy: lat2 57.4-59.4 ms vs 60.0-63.1 ms (63.1 ms to wynik Q8_0 na v2.3.3, patrz re-test poniżej). Accuracy 0.886-0.909 vs 0.909.

Re-test na v2.3.3

Konfiguracje z KV q8_0 powtórzone po aktualizacji pakietu (2 przebiegi każda):

model wagi pkg lat1 [ms] lat2 [ms] p95 [ms] dec/s accuracy VRAM [MiB]
2.0-mini Q5_K_M 2.3.3 10.6 13.7 15.0 49.5 0.727 1660
2.0-mini Q8_0 2.3.3 11.3 15.3 16.5 45.8 0.773 2166
2.0-4.5B Q5_K_M 2.3.3 22.0 28.2 30.7 23.6 0.773 4056
2.0-4.5B Q8_0 2.3.3 25.0 34.0 36.6 21.3 0.795 5620
2.0-max Q8_0 2.3.3 33.6 54.2 57.3 14.4 0.841 13266
omni-1.0-mini Q8_0 2.3.3 40.6 63.1 69.5 12.2 0.909 5304
omni-1.0-max Q8_0 2.3.3 72.7 126.7 142.9 6.3 0.977 13114

Accuracy i VRAM identyczne z v2.3.1. Największe różnice opóźnień to 1.1 ms w lat1 (4.5B Q8_0), 0.9 ms w lat2 (to samo) i 1.7 ms w p95 (omni-mini Q8_0). Aktualizacja niczego mierzalnego nie zmienia w naszym teście.


basal-bench CPU-only llama.cpp

Ryzen 5 5600X, KV q8_0, pakiet 2.3.3, 2 przebiegi.

model wagi lat1 [ms] lat2 [ms] p95 [ms] dec/s accuracy
omni-pico Q8_0 345.5 373.3 506.0 1.4 0.795
2.0-mini Q5_K_M 250.7 278.3 416.7 1.8 0.773
2.0-mini Q8_0 220.7 265.9 405.8 1.9 0.795

lat1 ląduje w przedziale 220-346 ms, czyli ok. 19-24x wolniej niż ścieżka CUDA. dec/s jest 18-28x niższe. Większych modeli na CPU nie mierzyłem.

Accuracy na CPU nie jest dokładnie takie jak na GPU: mini Q8_0 dostaje 0.795 zamiast 0.773, mini Q5_K_M 0.773 zamiast 0.727.


basal-loadtest po HTTP

Test obciążeniowy: 32 klientów, krótkie prompty, wagi Q8_0, KV q8_0, v2.3.3, 1 przebieg. Domyślne ustawienia basal-serve (--http-parallel 1).

model wagi p50 [ms] p95 [ms] dec/s błędy VRAM [MiB]
2.0-mini Q8_0 9.3 16.0 127.4 0 2166
2.0-4.5B Q8_0 18.1 31.6 57.8 0 5620
2.0-max Q8_0 31.1 52.0 34.3 0 13266
omni-pico Q8_0 21.3 42.3 52.8 0 1230
omni-1.0-mini Q8_0 32.1 44.8 32.8 0 5304
omni-1.0-max Q8_0 72.9 107.5 14.1 0 13114

Zero błędów wszędzie. VRAM równa się wartości z benchu, bo KV cache jest rezerwowany przy starcie.

Dwie uwagi do tej tabeli:

  • W przeciwieństwie do benchu (gdzie pico ma lepsze lat2 i dec/s) w tym teście 2.0-4.5B wygrywa z pico: 57.8 vs 52.8 dec/s, p50 18.1 vs 21.3 ms, p95 31.6 vs 42.3 ms. Możliwe, że to efekt hybrydowej architektury pico: według wpisu o 2.3.2 silnik dekoduje prompty takich modeli pojedynczo. To korelacja, nie zmierzona przyczyna.
  • p50 dla mini (9.3 ms) leży poniżej lat1 z benchu (11.3 ms), a 127 dec/s przy 32 klientach oznacza ok. 250 ms na decyzję (zakładam jedną decyzję na zapytanie). Te liczby nie mogą opisywać tej samej wielkości, więc p50/p95 w loadteście traktuję jako czas po stronie serwera (zakładam, że to pole usage z odpowiedzi), a nie opóźnienie widziane przez klienta. Do oceny przepustowości używam dec/s.

Wpływ liczby slotów llama.cpp przy basal-serve (--http-parallel)

Ustawienia llama.cpp: -c = 8192 x np, np od 1 do 32. np określa, ile zapytań na raz ma przyjąć llama.cpp server.

Domyślnie basal-serve ma --http-parallel 1. Otrzymujemy płaskie wyniki, co widać w tabelce poniżej.

model np kontekst p50 [ms] p95 [ms] dec/s VRAM [MiB]
2.0-mini 1 8192 13.6 16.7 130.5 2030
2.0-mini 2 16384 9.4 16.3 127.0 2166
2.0-mini 4 32768 9.5 16.0 122.2 2438
2.0-mini 8 65536 9.4 16.1 130.0 2996
2.0-mini 16 131072 9.4 16.0 124.9 4148
2.0-mini 32 262144 9.7 16.3 123.5 6452
omni-pico 1 8192 21.5 38.5 56.0 1146
omni-pico 2 16384 20.5 38.1 53.2 1224
omni-pico 4 32768 20.5 38.5 53.2 1396
omni-pico 8 65536 20.0 38.2 53.4 1742
omni-pico 16 131072 20.4 38.3 53.2 2432
omni-pico 32 262144 20.6 38.8 52.4 3812

Przepustowość jest płaska: więcej slotów nic nie daje, a VRAM rośnie tylko z preallokowanym KV cache. Powód: przy --http-parallel 1 basal-serve wysyła do backendu jedno zapytanie naraz, więc dodatkowe sloty stoją puste.

Po zmianie basal-serve na --http-parallel 32 podnoszenie liczby slotów w llama.cpp server zaczyna przynosić wymierne efekty. Moja hipoteza (nie pomiar): serve renderuje i parsuje kolejne zapytania, gdy sloty są zajęte, więc praca serve przestaje być wąskim gardłem.

model np kontekst p50 [ms] p95 [ms] dec/s VRAM [MiB]
2.0-mini 1 8192 13.6 16.3 152.4 2030
2.0-mini 2 16384 9.8 16.5 202.8 2166
2.0-mini 4 32768 9.3 16.1 253.7 2440
2.0-mini 8 65536 9.5 15.9 268.7 2996
2.0-mini 16 131072 9.5 16.1 242.9 4148
2.0-mini 32 262144 9.8 16.3 175.6 6452
omni-pico 1 8192 21.7 38.4 60.4 1146
omni-pico 2 16384 20.2 38.1 64.1 1224
omni-pico 4 32768 20.2 37.6 66.4 1396
omni-pico 8 65536 20.4 38.0 66.7 1742
omni-pico 16 131072 20.4 38.3 65.4 2434
omni-pico 32 262144 20.7 38.1 63.0 3814

Dla 2.0-mini optimum to np 4-8 (253.7-268.7 dec/s), potem przepustowość spada: przy np 32 jest tylko 175.6, co przypisuję narzutowi obsługi kolejki w serve (to hipoteza, nie pomiar). Pico osiąga szczyt przy np 8 (66.7) i od np 2 jest płaskie w granicach szumu.

W porównaniu z --http-parallel 1 te same konfiguracje są szybsze od 1.08x (pico np 1) do 2.08x (mini np 4), przy niezmienionych p50, p95 i VRAM. Najlepsza konfiguracja zmierzona na tej maszynie: 2.0-mini, np 8, hp 32, 268.7 dec/s, 2996 MiB VRAM.

Stany syntetyczne

basal-loadtest po HTTP z flagą --synthetic: syntetyczne elementy ze stanami (state) o zadanej długości w tokenach. --http-parallel 32, KV q8_0.

model stan (tokeny) np p50 [ms] p95 [ms] dec/s
2.0-mini 100 2 98.2 122.7 10.6
2.0-mini 100 8 98.9 123.8 10.4
2.0-mini 250 2 98.8 123.2 10.6
2.0-mini 250 8 99.4 123.6 10.4
2.0-mini 500 2 98.9 122.9 10.6
2.0-mini 1500 2 98.2 123.1 10.6
2.0-mini 1500 8 99.8 123.9 10.4
omni-pico 1500 2 85.5 108.8 15.4
omni-pico 1500 8 86.6 112.2 14.6

Przepustowość mini spada z 268.7 do ok. 10.5 dec/s (p50 ok. 98 ms zamiast 9.5 ms), ale jest taka sama dla stanów 100, 250, 500 i 1500 tokenów, a np 8 nie zmienia niczego względem np 2. To nie wygląda na koszt prefillu ani głębokości KV, tylko na stały koszt na zapytanie, po stronie basal-serve albo generatora obciążenia. Nie wyjaśniłem tego, więc z tej tabeli nie wyciągam wniosków o długich stanach.


Wnioski

Który model basal wypada najlepiej na karcie z 16 GB VRAM?

Cztery wykresy, zanim przejdę do rekomendacji. W motylkach lewe skrzydło to pierwsza wielkość z tytułu, prawe druga, słupki kreskowane to ścieżka natywna. Pamięć natywna (torch, GB) i llama.cpp (MiB) jest skalowana tylko poglądowo, bo to różne pomiary.

Wykres motylkowy: lewe skrzydło przepustowość (dec/s), prawe pamięć na karcie — 9 konfiguracji llama.cpp (KV q8_0) i 4 tryby natywne, posortowane rosnąco po pamięci; przerywana linia to limit karty 16 376 MiB

Wykres motylkowy: lewe skrzydło przepustowość (dec/s), prawe accuracy — te same konfiguracje llama.cpp i tryby natywne, posortowane rosnąco po accuracy

Wykres motylkowy: lewe skrzydło pamięć na karcie, prawe accuracy — te same dane, posortowane rosnąco po pamięci

Radar accuracy, dec/s i pamięć dla 2.0-mini i omni-pico: llama.cpp (KV q8_0) vs tryby natywne (eager-fp32, fast/eager)

Domyślny wybór: omni-1.0-mini Q8_0

0.909 przy 5,1-5,3 GiB — drugi wynik accuracy w teście (po omni-max) za niecałą 1/3 karty, z zapasem na desktop i inne usługi. Koszt: lat2 ok. 62 ms (ok. 2,5x więcej niż pico) i 12 dec/s w benchu. Gdy trzeba odzyskać 1 GiB: Q6_K (4,1-4,3 GiB), bez straty latencji i z accuracy 0.886-0.909.

System One + System Two?

Gdy na tej samej karcie ma jeszcze działać model Systemu 2, wybór dyktują wolne GiB, nie accuracy. KV cache jest preallokowany przy starcie, więc oba procesy muszą się zmieścić na stałe. Z 16 376 MiB (ok. 16 GiB) karty zostaje: ok. 14,8 GiB przy omni-pico, ok. 13-14 GiB przy 2.0-mini, ok. 12 GiB przy omni-1.0-mini Q6_K — na model klasy 7-14B w Q4/Q5 (zwykle 8-10 GB wag plus kontekst) wystarcza. omni-1.0-max i 2.0-max (12,2-13,4 GiB) zostawiają 2,6-3,8 GiB — współdzielenie karty i “maksymalna jakość” wykluczają się nawzajem. Rozsądne pary: omni-1.0-mini Q6_K (0.886-0.909) + mniejszy System 2, albo pico (0.795) / 2.0-mini (0.773), jeśli System 2 ma być z tych nieco większych.

Liczy się tylko jakość

Maksymalna jakość: omni-1.0-max, ale tylko z KV q8_0 lub q4_0. Jedyny model z 0.977. q4_0 daje to samo accuracy co q8_0 przy 0,3 GiB mniej (304 MiB); f16 to 13,4 GiB i najgorsze p95 (184.6 ms) bez żadnego zysku. Koszt całkowity: 12,5-13,4 GiB (po desktopie zostaje niewiele) i lat2 123-150 ms — najwolniejszy w teście.

Najlepszy VRAM/Accuracy

Najlepszy stosunek kosztu do trafności: omni-pico. 0.795 przy 1,2-1,3 GiB i lat2 ok. 24 ms. 2.0-4.5B daje w benchu to samo accuracy, ale jest gorszy na każdej osi przy 4-6 GiB VRAM. Wygrywa z pico tylko przepustowością w teście HTTP przy --http-parallel 1 (57.8 vs 52.8 dec/s), a tę przewagę ma z nawiązką tańszy 2.0-mini (127 dec/s przy 2,1 GiB), więc nie widzę dla 4.5B roli na tej karcie.

Największa prędkość

Maksymalna przepustowość: 2.0-mini — accuracy 0.727-0.773 (w granicach remisu z pico i 4.5B), ale 268.7 dec/s z np 8 i --http-parallel 32; największy zysk dała konfiguracja serve, nie model. Do zadań masowych, nie jakościowych.

KV q8_0 jako domyślne ustawienie

Accuracy identyczne z f16 niemal wszędzie, przy 64-562 MiB oszczędności (przy większym -c proporcjonalnie więcej). q4_0 bezpieczne tylko dla modeli omni i 2.0-max; na 2.0-mini i 2.0-4.5B kosztuje 0.14-0.43 accuracy, a 2.0-max na f16 w ogóle nie wstaje — przy modelach ~13 GiB kwantyzacja KV to jedyny realny zapas.

Tryb natywny vs llama.cpp

Ścieżka natywna (transformers) w tym benchu nie wygrywa z llama.cpp na tej karcie. fast na mini ma wyższe dec/s (71.8 vs 45-53 na llama.cpp w benchu), ale lat1/lat2 są porównywalne lub gorsze (17.0 vs 12.7-15.5 ms), pamięć torch to 4,3 GB vs 2,1 GiB (pomiary nie do końca porównywalne), a accuracy bez zysku. omni-1.0-mini natywnie nie wstaje (15,9 GiB safetensors). Nie mierzyłem natywnie pod obciążeniem HTTP, więc nie rozstrzygam, jak wypada produkcyjnie.

WAŻNE Wszystkie wartości accuracy w tym wpisie pochodzą z basal-bench na wbudowanych w pakiet stanach i pytaniach: 44 elementy, więc każdy wynik to wielokrotność 1/44 (0.023). Różnice 0.773 vs 0.795 czy 0.909 vs 0.977 to jedna-dwie odpowiedzi.


Jeśli masz pytania, widzisz błąd w pomiarach albo chcesz porównać wyniki na własnym sprzęcie, napisz na X: @kskrzypinski.

Photo by Linus Belanger