Jak policzyć parametry modelu 2,4 bln, nie pobierając ani jednej wagi
Buduję interaktywną wizualizację budowy modeli językowych: wybierasz model, klikasz warstwę i widzisz, ile parametrów siedzi w uwadze, w ekspertach MoE, w routerze, w normalizacjach. Liczby liczę wzorem z config.json, ale wzór to hipoteza. Żeby ją sprawdzić, trzeba policzyć prawdziwe tensory w checkpoincie. Dla Qwen3.8-2.4T-A95B checkpoint w bf16 waży około 4,84 TB. Nie zamierzałem go pobierać.
Format, który sam się opisuje
Plik .safetensors zaczyna się od 8 bajtów: liczby całkowitej bez znaku, little-endian, która mówi, ile bajtów ma nagłówek JSON. Zaraz za nią leży ten JSON: nazwa każdego tensora, jego dtype, shape i przesunięcie w pliku. Dopiero potem są surowe wagi. Do policzenia parametrów wystarczą kształty, więc wystarczy nagłówek.
Hugging Face serwuje pliki przez zwykłe HTTP z obsługą Range. Na każdy shard idą dwa żądania:
req = Request(base + shard, headers={'Range': 'bytes=0-7'})
n = struct.unpack('<Q', urlopen(req).read(8))[0]
req = Request(base + shard, headers={'Range': f'bytes=8-{8 + n - 1}'})
header = json.loads(urlopen(req).read())
Listę shardów daje model.safetensors.index.json, czyli mapa „tensor → plik”. Shardy pobieram równolegle w 12 wątkach, każde żądanie ma cztery próby. Cały checkpoint 2,4 bln parametrów kosztuje kilka megabajtów transferu zamiast prawie pięciu terabajtów.
Grupowanie po nazwach
Same kształty to za mało, potrzebny jest podział na grupy, które rysuję na stronie. Robię go po nazwach tensorów: .self_attn. to pełna uwaga, .linear_attn. to warstwa liniowa (Gated DeltaNet), .mlp.experts. to eksperci, .mlp.shared_expert to ekspert współdzielony, .mlp.gate. to router, embed_tokens i lm_head to brzegi modelu. Dwie gałęzie odkładam osobno: głowicę MTP (przewidywanie kilku tokenów naraz, używaną przy dekodowaniu spekulatywnym) i wieżę wizyjną. Strona pokazuje szkielet tekstowy, więc suma kontrolna to „wszystko minus MTP minus vision”.
Każda nazwa, która nie pasuje do żadnego wzorca, ląduje w kategorii other:<pełna nazwa>. To ważniejsze, niż wygląda: nowy typ tensora nie znika w worku „reszta”, tylko pojawia się na wydruku z nazwą.
Co wyszło dla 2,4 bln
Model ma 92 warstwy, d_model 8192, 512 ekspertów routowanych z k = 10 plus jeden współdzielony, każdy ekspert z wymiarem pośrednim 2048. Wzór na ekspertów to 3 × d × ff na eksperta (bramka, projekcja w górę i w dół):
3 × 8192 × 2048 × 512 × 92 = 2 370 821 947 392
To 98% całego modelu. Pozostałe grupy według nagłówków:
| grupa | parametry |
|---|---|
| eksperci routowani | 2 370 821 947 392 |
| uwaga (pełna + liniowa) | 39 895 618 944 |
| ekspert współdzielony | 4 631 265 280 |
| embedding | 2 034 237 440 |
wyjście (lm_head) |
2 034 237 440 |
| router | 385 875 968 |
| normalizacje | 1 515 520 |
| razem | 2 419 804 697 984 |
Ekspert współdzielony nie wychodzi równo z 3 × 8192 × 2048 × 92. Brakuje 753 664 parametrów, czyli dokładnie 8192 na warstwę: to skalarna bramka eksperta współdzielonego, wektor długości d_model. Takich rzeczy wzór z configu nie mówi, a nagłówek pokazuje od razu.
Druga kontrola to nazwa modelu. „A95B” obiecuje około 95 mld parametrów aktywnych na token. Liczę: 10 ekspertów na 92 warstwach (46,3 mld), ekspert współdzielony (4,63 mld), cała uwaga (39,9 mld), embedding i wyjście (4,07 mld), router i normalizacje. Wychodzi 95,29 mld. Zgadza się.
Dlaczego uwagi nie liczę wzorem
W hybrydach Qwen3.5/3.8 co czwarta warstwa ma pełną uwagę, a trzy poprzednie to Gated DeltaNet: liniowa uwaga ze stanem rekurencyjnym, krótką konwolucją i własnymi bramkami. W 92 warstwach daje to 23 warstwy pełnej uwagi i 69 liniowych. Wzór na projekcje Q/K/V/O dla GDN wymagałby odtworzenia całej implementacji z kodem konwolucji i bramek włącznie. Zamiast tego pole attn w profilu modelu przechowuje dokładną sumę wszystkich tensorów uwagi z checkpointu, a resztę liczy wzór. Ta asymetria jest celowa: liczę wzorem to, co jest regularne i dobrze opisane w configu, a to, co nieregularne, odczytuję.
To samo podejście sprawdziłem na pięciu modelach tej rodziny, od 9 mld do 2,4 bln parametrów. Suma grup na stronie zgadza się z nagłówkami co do jednego parametru.
Pułapki po drodze
- Wyświetlanie. Funkcja skracająca liczby znała progi do miliardów. 2 419 804 697 984 wyświetlało się jako „2419.80B”. Próg dla bilionów (i terabajtów przy rozmiarze w bajtach) doszedł dopiero przy tym modelu, bo wcześniej nic go nie potrzebowało.
- Model tekstowy bez wieży wizyjnej. Flagowiec 2,4 bln nie ma w checkpoincie gałęzi vision, więc odejmuję tylko MTP. Skrypt nie musi tego wiedzieć, bo vision po prostu wychodzi zerem.
- Rewizja. Repozytorium na Hugging Face może się zmienić pod tą samą nazwą. Skrypt przyjmuje rewizję jako drugi argument, a w kodzie strony zapisuję datę przeglądu źródeł.
Najważniejsze: liczby z nagłówków nie trafiają do kodu, który je potem odtwarza. Trafiają do testu jako stałe i to z nimi porównuję wynik wzoru. Dzięki temu test pada, gdy wzór się zmieni, a nie przytakuje sam sobie.