Architektura

Jak basal-rs liczy decyzję

Droga od żądania HTTP do odpowiedzi z prawdopodobieństwami, krok po kroku. Modele decyzyjne Basal i ich protokół, czyli reguły zadawania pytań i liczenia decyzji, opracował Remek Kinas. Silnik basal-rs odpowiada za pakowanie żądań do wspólnych obliczeń, harmonogram, Programy uruchamiane bezpośrednio na karcie graficznej. Wykonują obliczenia modelu. i serwer.

silnik basal-rs protokół i modele Basal · Remek Kinas
01 / Droga żądania

Od żądania do decyzji

Każde żądanie przechodzi przez sześć etapów. Kolor etapu pokazuje, czyj kod za nim stoi: żółty to silnik basal-rs, lawendowy to protokół Basala.

Etap 1 z 6
silnik basal-rs

Przyjęcie żądania

Serwer HTTP (biblioteki axum i tokio w Rust) przyjmuje żądanie POST na /v1/systemone albo /v1/basal. Pole model w żądaniu wskazuje model, a serwer przekazuje żądanie do wątku tego modelu.

  • gdy w toku jest ponad 1024 żądania (limit max_inflight), serwer odpowiada kodem 529 z nagłówkiem Retry-After: 1, czyli prośbą o ponowienie za sekundę
  • każdy model ma własny wątek, kolejkę i tor długich żądań
  • żądania, na które klient przestał czekać, są pomijane
silnik basal-rsprotokół Basal · Remek Kinas

Walidacja i plan

Wątek modelu sprawdza żądanie, dzieli tekst na tokeny i buduje prompty według kontraktu Basala: dla każdego pytania dwa prompty z opcjami w dwóch porządkach. Błędne żądanie wraca do klienta od razu, zanim trafi do kolejki.

  • szablon promptu, reguła rozpoznawania języka i litery opcji takie same jak w upstream (prompt.py)
  • koszt żądania to liczba tokenów do policzenia, gdy wspólne prefiksy liczą się raz
  • silnik nie załaduje modelu, który ma inny kontrakt promptu
silnik basal-rs

Kolejka HRRN

Kolejka działa według reguły HRRN (Highest Response Ratio Next). Wybiera żądanie o najwyższym stosunku (czekanie + szacowany czas) / szacowany czas. Krótkie żądania wyprzedzają więc długie, a długie przesuwają się w górę kolejki, im dłużej czekają.

  • żądania powyżej 4096 tokenów idą do osobnego toru długich żądań
  • ustawienie schedule: fifo przywraca obsługę w kolejności przyjścia
silnik basal-rs

Partia i drzewo prefiksów

Partia to grupa żądań liczonych razem na GPU: pierwsze żądanie z kolejki i każde następne, które mieści się w 8192 tokenach. Prompty wszystkich żądań partii trafiają do wierszy. Każdy wiersz to drzewo, w którym wspólny początek promptów (prefiks) liczy się raz.

  • wiersz ma do 32 768 tokenów, jeden przebieg modelu (forward) do 8192
  • głębokość drzewa do 8 bloków
  • identyczne prompty dzielą jeden odczyt wyniku
silnik basal-rs

Przebieg modelu na GPU

Model Llama liczy w precyzji f16 tokeny całej partii ułożone w jedną zwartą listę, bez paddingu, czyli bez pustych tokenów wyrównujących długość promptów. W ostatniej warstwie blok MLP i logity (surowe wyniki przed zamianą na prawdopodobieństwa) liczą się tylko dla pozycji odczytu i liter odpowiedzi.

  • mnożenie macierzy (GEMM): cuBLASLt z tabelą algorytmów na CUDA albo kod przeniesiony z biblioteki MLX na Metal
  • attention w f32 po blokach drzewa, na własnych kernelach CUDA i Metal
  • sumy pośrednie w mnożeniu macierzy (akumulacja) w f32
protokół Basal · Remek Kinassilnik basal-rs

Decyzja i odpowiedź

Z logitów liter powstają prawdopodobieństwa dla obu porządków. Po przywróceniu kolejności opcji z żądania oba rozkłady są uśredniane i kalibrowane temperaturą przypisaną do typu pytania w CALIBRATION.json. Serwer składa odpowiedź w formacie API TypeSafe System One, z pewnością (confidence) liczoną wzorami TypeSafe.

  • nagłówki odpowiedzi x-basal-queue-ms (czas w kolejce), x-basal-compute-ms (czas obliczeń) i x-basal-batch-requests (liczba żądań w partii)
  • prawdopodobieństwa w pełnej precyzji (TypeSafe zaokrągla je do dwóch miejsc po przecinku)
02 / Protokół Basala

Dwa porządki opcji i kalibracja

Model odpowiada literą opcji, a decyzja to rozkład prawdopodobieństwa po literach, liczony z ich Surowy wynik modelu dla jednej litery. Funkcja softmax zamienia logity wszystkich liter na prawdopodobieństwa, które sumują się do 1.. Każde pytanie trafia do modelu dwa razy, z opcjami ułożonymi w dwóch porządkach. Uśrednienie osłabia wpływ kolejności opcji na wynik.

Ten protokół opracował Remek Kinas. basal-rs odtwarza go bez zmian: ten sam Tekst, który trafia do modelu: szablon, stan, pytanie i opcje., te same Token to fragment tekstu, zwykle część słowa. Model dostaje tekst jako ciąg numerów tokenów., oba porządki i kalibracja z pliku CALIBRATION.json. Kalibrację opisuje druga linia wzoru pod przykładem: logarytm średniej jest dzielony przez temperaturę T przypisaną do typu pytania, a potem znów przechodzi przez softmax.

1

Porządek 1

opcje w kolejności z żądania

  • A Zwroty 0.89
    logit 3.2
  • B Wsparcie techniczne 0.09
    logit 0.9
  • C Sprzedaż 0.02
    logit -0.4
2

Porządek 2

te same opcje w odwrotnej kolejności

  • A Sprzedaż 0.08
    logit 0.4
  • B Wsparcie techniczne 0.17
    logit 1.1
  • C Zwroty 0.75
    logit 2.6
3

Średnia

po przywróceniu kolejności opcji

  • Zwroty 0.82
  • Wsparcie techniczne 0.13
  • Sprzedaż 0.05
4

Po kalibracji

temperatura T = 1.00

  • Zwroty 0.82
  • Wsparcie techniczne 0.13
  • Sprzedaż 0.05

T < 1 wyostrza rozkład, T > 1 go spłaszcza. Wybrana opcja się nie zmienia, zmienia się pewność.

p     = (softmax(l_porządek1) + odwróć(softmax(l_porządek2))) / 2
p_kal = softmax(log(max(p, 1e-12)) / T_typ_pytania)
03 / Pakowanie

Wspólny stan liczony raz

Pytania o ten sam dokument mają wspólny początek promptu, czyli wspólny Początkowy fragment promptu, taki sam w kilku promptach.. Silnik układa prompty z jednej Grupa żądań, które GPU liczy razem. w drzewo i każdy wspólny prefiks liczy raz. Wspólne są szablon, Kontekst, którego dotyczą pytania, na przykład dokument. i treść pytania, która jest taka sama w obu porządkach.

Każdy prompt osobno 1290 tokenów
  • P1 · porządek 1
  • P1 · porządek 2
  • P2 · porządek 1
  • P2 · porządek 2
  • P3 · porządek 1
  • P3 · porządek 2

Liczba promptów: 6, każdy po 215 tokenów. Stan liczy się 6 razy.

Drzewo wspólnych prefiksów 300 tokenów
  • szablon · 53 · liczony raz przy starcie
    • stan · 120
      • pytanie 1 · 24
        • porządek 1 · 18
        • porządek 2 · 18
      • pytanie 2 · 24
        • porządek 1 · 18
        • porządek 2 · 18
      • pytanie 3 · 24
        • porządek 1 · 18
        • porządek 2 · 18

4,3× mniej tokenów do policzenia. Stan liczy się raz dla wszystkich pytań.

Liczby przy blokach to długości w tokenach. Długości stanu, pytań i opcji są przykładowe. Szablon polskiego promptu ma 53 tokeny.

Attention po blokach drzewa

Przed obliczeniem drzewo zamienia się w jeden wiersz tokenów. W Część modelu, w której każdy token korzysta z informacji z wcześniejszych tokenów. token widzi wcześniejsze tokeny swojego bloku i bloków nad nim w drzewie (przodków). Pozycja tokenu dla Sposób zapisu pozycji tokenu, dzięki któremu model zna kolejność tekstu. to jego numer we własnym prompcie. Kliknięcie wiersza pokazuje, które bloki widzi dany blok.

Pakowanie promptów w drzewo prefiksów pochodzi z Referencyjny serwer basal-serve autora modeli Basal. 1.5 (engine.py). basal-rs łączy w jednym drzewie także pytania z różnych żądań tej samej partii. Attention po blokach drzewa liczy własnymi kernelami CUDA i Metal.

P1 · porządek 2 widzi: stan, pytanie 1, P1 · porządek 2. Bloki rodzeństwa (inne gałęzie tego samego rodzica) są zamaskowane, więc wynik jest taki sam jak przy liczeniu każdego pełnego promptu osobno.
04 / Harmonogram

Krótkie żądania nie czekają na długie

Dokument na 16 tys. tokenów liczy się kilka sekund. Żeby krótkie żądania nie czekały za nim, żądania powyżej 4096 tokenów trafiają do osobnego toru. Ten tor dzieli GPU z torem krótkich partii i oddaje im GPU między Model liczy wynik etapami, warstwa po warstwie. modelu.

Krótkie żądanie czeka
na koniec bieżącej warstwy dokumentu
Dokument kończy się
później o czas krótkich partii
praca nad dokumentem partia krótkich żądań czekanie w kolejce przyjście żądania

Schemat poglądowy, nie pomiar. FIFO obsługuje żądania w kolejności przyjścia. Tor długich żądań oddaje GPU po zakończeniu warstwy, jeśli pracował już co najmniej long_slice_ms (domyślnie 100 ms). Potem wraca do tej samej warstwy z tymi samymi tensorami, czyli danymi pośrednimi modelu.

  • priorytet = (czekanie + szacowany czas) / szacowany czas Highest Response Ratio Next: kolejka wybiera żądanie o najwyższym priorytecie z tego wzoru.: krótkie żądania idą pierwsze, a długie przesuwają się w górę kolejki, im dłużej czekają.
  • max_batch_tokens: 8192 Limit partii w tokenach. Partia to pierwsze żądanie z kolejki i każde następne, które się w niej zmieści.
  • long_tokens: 4096 Próg toru długich żądań: dłuższe żądania trafiają do tego toru. Wartość 0 wyłącza tor.
  • kilka modeli, jedno GPU Gdy na jednej karcie działa kilka modeli, krótkie partie każdego z nich wyprzedzają długie żądania wszystkich modeli.
05 / Powtarzalność

Ta sama odpowiedź niezależnie od ruchu

Na GPU wynik mnożenia macierzy może zależeć od liczby wierszy w partii, bo biblioteka wybiera wtedy inny algorytm. basal-rs ustala algorytmy tak, żeby wynik pytania nie zależał od tego, z jakimi innymi żądaniami trafi do partii.

  • pojedynczo
  • w partii z innymi żądaniami
  • w drzewie z innymi pytaniami
  • pod obciążeniem 32 klientów

Tabela GEMM niezależna od partii

Mnożenie macierzy, z angielskiego general matrix multiply. liczy cuBLASLt, biblioteka NVIDIA. Tabela wskazuje dla każdego kształtu wag najszybszy algorytm bez Sposób mnożenia macierzy, w którym wynik powstaje z kilku części sumowanych na końcu., wybrany spośród algorytmów, które dają bit w bit te same wyniki. Do tego attention dzielony na kafelki według pozycji W attention token (zapytanie) porównuje się z wcześniejszymi tokenami (kluczami). i odczyt liter odpowiedzi własnym kernelem.

Surowe wyniki modelu dla liter, zanim zamienią się w prawdopodobieństwa. bit w bit takie same

basal-rs: różnica odpowiedzi na to samo żądanie
0
upstream: różnica zależnie od partii
0,001–0,068
czas pojedynczej decyzji z tabelą / z automatycznym wyborem algorytmu przez cuBLASLt (max, RTX 6000 Ada)
63,1 / 68,5 ms

Na Macach (Metal) mnożenie macierzy (GEMM) liczy kod przeniesiony z biblioteki MLX. Jego wynik nie zależy od liczby wierszy. Attention po blokach drzewa daje tam ten sam wynik pojedynczo, w partii i w drzewie. Na kartach NVIDIA tabela GEMM zależy od karty, modelu, wersji cuBLASLt i precyzji. Serwer generuje ją przy pierwszym starcie albo bierze gotową, wbudowaną w plik programu.

06 / Backendy

CUDA i Metal, bez Pythona

Backend to część silnika, która liczy na konkretnym sprzęcie: CUDA na kartach NVIDIA, Metal na Macach z Apple Silicon. Oba liczą przebieg modelu Llama (Jeden przebieg danych przez model, od tokenów wejściowych do wyników.) na bibliotece candle napisanej w Rust, z własnymi kernelami. Domyślna Format liczb w obliczeniach. f16 i bf16 zapisują liczbę na 16 bitach, f32 na 32 bitach: dokładniej, ale wolniej. to f16, a sumy pośrednie w mnożeniu macierzy (akumulacja) liczą się w f32. Precyzja f32 służy jako punkt odniesienia.

CUDA

karty NVIDIA od compute capability 8.0 (numer generacji architektury karty): A100, RTX 30xx/40xx, RTX 6000 Ada, L40S, H100

  • mnożenie macierzy (GEMM) przez cuBLASLt, bibliotekę NVIDIA, z tabelą algorytmów wybranych pełnym przeszukaniem (polecenie basal gemm-search)
  • attention na tensor cores, czyli jednostkach karty do mnożenia macierzy; na H100 osobny kernel na instrukcjach wgmma tej karty, z tym samym wynikiem bit w bit
  • jeden plik programu z kernelami dla compute capability 8.0, 8.9 i 9.0; właściwe kernele są wybierane przy starcie
  • gotowe tabele GEMM dla H100 i RTX 6000 Ada wbudowane w plik programu

Metal

komputery Mac z Apple Silicon, sprawdzone na M1 Pro i M2 Max (32 GB)

  • mnożenie macierzy (GEMM) przeniesione z biblioteki MLX, wynik nie zależy od liczby wierszy
  • attention w f32 na blokach macierzy 8×8 (typ simdgroup_float8x8 w Metal)
  • do działania nie potrzebuje Pythona
Precyzja obliczeń i zgodność decyzji z FP32 upstream na 900 pytaniach
PrecyzjaDecyzje zgodne z FP32Jak liczy
f16 domyślnie899–900 / 900wagi bf16 zamieniane na f16 przy ładowaniu; główny strumień danych między warstwami (residual), normalizacje i bloki MLP w f16; sumy w mnożeniu macierzy w f32
bf16 nie mierzonota sama precyzja, w której zapisane są wagi modelu, bez konwersji
f32 referencja900 / 900wagi bf16 zamieniane na f32 przed użyciem; logity różnią się od FP32 upstream najwyżej o 0,0003; obliczenia wolniejsze
07 / Kod

Trzy pakiety Rust (crate'y)

  • basal-core kontrakt API System One, budowa promptu, tokenizer (podział tekstu na tokeny), pakowanie promptów w drzewo prefiksów, decyzje i pewność (confidence), rozszerzenia facts i evidence, pytania choice z 11–255 opcjami, silnik ze wspólnym interfejsem backendów (trait Backend), współdzielenie GPU przez tory
  • basal-gpu wczytywanie wag z plików safetensors, przebieg modelu Llama (forward) na bibliotece candle, kernele CUDA i Metal, mnożenie macierzy przez cuBLASLt albo kod przeniesiony z MLX, attention po blokach drzewa, głowica rozszerzenia evidence
  • basal-cli polecenie basal w terminalu, serwer HTTP, eksport i porównania z referencją upstream, pomiary