Från prompt till token
Från noll till att förstå hur en inference-server fungerar. En kurs om vad en språkmodell består av, hur en fråga flödar genom den som tensorer med konkreta shapes, och hur en inference-motor som vLLM kör den snabbt för många användare samtidigt.
Färgerna betyder samma sak i hela kursen. Navigera med piltangenterna, knapparna längst ner eller innehållsförteckningen.
Vad är en språkmodell?
En språkmodell gör en enda sak: den gissar vilket ord som kommer härnäst. Det är samma idé som ordförslagen i mobilen, bara enormt mycket större. Allt annat, att svara på frågor, skriva kod och resonera, byggs av att göra den gissningen om och om igen.
Det viktigaste att veta från början
- Den är gjord av tal. En modell som Llama-3-8B består av ungefär 8 miljarder tal, kallade vikter eller parametrar. Tillsammans tar de 16 GB.
- Talen är inlärda. Modellen har läst enorma mängder text och justerat sina tal tills den blev bra på att gissa nästa ord. Ingen har skrivit in regler för hand.
- Den slår inte upp svar. Det finns ingen databas med fakta inuti, bara tal som räknas ihop. Kunskapen ligger utspridd i talen.
- Den skriver ett ord i taget. Varje nytt ord läggs till texten, och modellen körs igen för att gissa nästa.
Prova till höger: välj själv nästa ord, eller låt ”modellen” välja det mest sannolika.
Modellens gissningar för nästa ord:
Sannolikheterna är påhittade för att visa idén. Resten av kursen förklarar hur en riktig modell räknar fram dem.
Kursens karta
Modulerna följer datats väg: först verktygen, sedan vad modellen är, hur den körs, och sist hur en motor kör många förfrågningar samtidigt.
Välj din väg
Varje slide har en nivå: Grund förutsätter inga förkunskaper, Fördjupning visar formler och shapes, Expert går in i kod och drift. Är du ny: välj grundspåret och läs hela kursen, gå sedan igenom den igen med fördjupning. Bläddringen hoppar över slides över vald nivå.
Varje modul börjar med mål och slutar med en sammanfattning och ett quiz. Din progress och dina quizresultat sparas i den här webbläsaren. Tangenter: ← → bläddra, T innehåll, O ordlista.
Ett exempel hela vägen
Hela kursen på en slide. Följ texten ”Katten satt på” genom modellen tills den blivit en token längre. Varje steg fördjupas i en egen modul.
Referensmodellen och notation
Vi använder Llama-3-8B som genomgående exempel. Alla shapes i kursen räknas med dessa siffror, så att du kan känna igen dem i config.json och i vLLM:s loggar.
| Symbol | Betydelse | Llama-3-8B |
|---|---|---|
| B | batch: antal sekvenser | varierar |
| T | tokens som bearbetas i steget | varierar |
| d | hidden size (d_model), residual streamens bredd | 4096 |
| L | antal lager (num_hidden_layers) | 32 |
| n_h | query heads (num_attention_heads) | 32 |
| n_kv | KV heads (num_key_value_heads) | 8 |
| d_h | head_dim = d / n_h | 128 |
| d_ff | MLP:ns inre bredd (intermediate_size) | 14336 |
| V | vokabulär (vocab_size) | 128256 |
Så läser du shapes
x [B, T, d] är en tensor med B·T·d tal. Sista axeln är den som matrismultipliceras.
W [d_in, d_out] är en viktmatris. x @ W ger [B, T, d_out].
K_cache [n_kv, ctx, d_h] per lager och sekvens.
ids [B, T] är heltal, index i vokabulären.
PyTorch lagrar nn.Linear-vikter som [d_out, d_in] och räknar x @ Wᵀ. Här skriver vi [d_in, d_out] eftersom det läses i flödesriktningen.
Modul 1 av 11, cirka 15 minuter
Grunderna
Fyra verktyg som används i varje steg av modellen: vektorer, matrismultiplikation, icke-linjäritet och softmax. Plus den viktigaste distinktionen i kursen.
När du är klar kan du
- läsa en shape som [T, 4096] och säga vad en matmul gör med den
- förklara vad en skalärprodukt mäter och var den används
- förklara varför modellen behöver icke-linjäritet
- skilja på vikter och aktiveringar
Vektorer och skalärprodukt
En vektor är bara en lista med tal, som en rad i ett kalkylark. Skalärprodukten är ett sätt att mäta hur lika två sådana listor är. Det är det vanligaste räknesteget i hela modellen.
En vektor är en lista med tal. I modellen är varje token en vektor med 4096 tal, och en sådan vektor kan ses som en pil i ett rum med 4096 dimensioner.
Skalärprodukten multiplicerar två vektorer tal för tal och summerar:
Resultatet är ett enda tal som mäter hur mycket vektorerna pekar åt samma håll, skalat med deras längder.
Nästan allt i modellen är skalärprodukter:
- attention-scores: query · key
- logits: slutvektorn · en tokens rad i LM head
- varje utvärde i en matrismultiplikation
Matris gånger vektor
En matris är en tabell med tal som gör om en lista till en annan lista. Det är modellens grundläggande räknesteg, och nästan all tid i ett grafikkort går åt till just detta.
En viktmatris W [d_in, d_out] gör om en vektor med d_in tal till en med d_out tal. Varje utvärde är skalärprodukten mellan indatavektorn och en kolumn i W:
Varje kolumn fungerar som en detektor: utvärdet blir stort när x liknar kolumnens mönster.
Med flera rader, X [T, d_in], händer samma sak för varje rad oberoende av de andra:
Det är därför 7 tokens kan bearbetas på en gång: 7 rader mot samma W.
Varför icke-linjäritet?
Om modellen bara multiplicerade med tabeller kunde den bara rita raka linjer. En liten brytare mellan stegen gör att den kan forma vilka kurvor och regler som helst.
Två matriser efter varandra är bara en matris till:
Hundra lager utan något emellan vore alltså inte mäktigare än ett enda. Modellen skulle bara kunna rita räta linjer.
En aktiveringsfunktion mellan matriserna bryter det. ReLU är den enklaste:
Negativa värden blir 0, positiva släpps igenom. Varje neuron blir en detektor som antingen är av eller ger utslag. Summan av många sådana knäckar kan forma vilken kurva som helst.
Llama använder SiLU, en mjukare variant, men idén är densamma. Softmax i attention är modellens andra icke-linjäritet.
Softmax: från tal till sannolikheter
Modellen ger ut vanliga tal, men vi vill ha sannolikheter. Softmax gör om talen till procent som summerar till 100.
Modellen producerar godtyckliga tal (logits). Softmax gör om dem till positiva tal som summerar till 1:
- Exponentialen gör alla värden positiva och förstorar skillnader: en logit som är 2 högre får ungefär 7 gånger så hög sannolikhet.
- Bara skillnaderna mellan logits spelar roll. Lägg till 100 på alla och inget ändras.
- Skala alla logits med en faktor, så blir fördelningen skarpare eller plattare. Det är precis vad temperature gör.
Softmax används på två ställen: i attention (vilka positioner ska jag lyssna på?) och i slutet (vilken token kommer härnäst?).
Vikter och aktiveringar
Vikterna är modellens kunskap: inlärda en gång och sedan fasta. Aktiveringarna är modellens tankar om just din text, och de räknas fram på nytt varje gång.
Den viktigaste distinktionen i hela kursen:
- Vikter är modellens inlärda tal. De ligger fast i GPU-minnet och är desamma för alla frågor. De är programmet.
- Aktiveringar är mellanvärdena som räknas fram när en token passerar. De är tillståndet medan programmet körs.
I en tät modell används alla viktmatriser i sin helhet för varje token. Undantaget är embedding, som i stället är ett uppslag av en rad per token, vilket modul 2 visar. Det går alltså inte att se ”vilka vikter som aktiverades” av en fråga. Det som skiljer en fråga från en annan är aktiveringarna, och det är dem man studerar när man vill förstå vad modellen gör.
I en MoE-modell väljer en router några experter per token. Där kan man faktiskt se vilka viktblock som användes.
Byt token och modelltyp till höger.
Sammanfattning av modul 1
Det viktigaste: Grunderna
- En vektor är en lista tal. Skalärprodukten mäter hur mycket två vektorer pekar åt samma håll.
- x @ W: varje utvärde är en skalärprodukt mellan x och en kolumn i W. [T, d_in] @ [d_in, d_out] → [T, d_out], rad för rad.
- Utan icke-linjäritet mellan lagren kollapsar hela modellen till en enda matris.
- Softmax gör om tal till sannolikheter. Bara skillnaderna mellan logits spelar roll.
- Vikter är fasta och används varje gång: varje viktmatris läses i sin helhet för varje token, men embedding är i stället ett uppslag av en rad per token, vilket modul 2 visar. Aktiveringar räknas fram per token och är det man studerar.
Kan du förklara varje punkt med egna ord för någon annan? Då är du redo för quizet på nästa slide.
Testa dig själv: grunderna
Modul 2 av 11, cirka 10 minuter
Text → tokens
Modellen räknar bara med tal. Här blir text till token-ids, och ids till vektorer.
När du är klar kan du
- förklara varför text delas i subword-tokens
- beskriva vad en chat template gör
- ange shape före och efter embedding
Varför tokens?
Datorer räknar med tal, inte bokstäver. Texten hackas därför först i bitar, tokens, och varje bit får ett nummer.
Modellen räknar bara med tal. Texten måste delas i bitar som var och en har ett id i en fast vokabulär.
- Tecken för tecken ger korta vokabulärer men mycket långa sekvenser, och attention kostar mer ju längre sekvensen är.
- Hela ord ger korta sekvenser men en enorm vokabulär och inget sätt att hantera nya ord.
- Subword-tokens (BPE) är en kompromiss: vanliga ord blir en token, ovanliga delas upp.
Byte Pair Encoding tränas en gång innan modellen tränas: börja med enskilda bytes, hitta det vanligaste paret i träningstexten, slå ihop det till en ny token och upprepa tills vokabulären är full. Llama 3 har 128 256 tokens.
Illustration: ordningen på merges här är påhittad. I en riktig tokenizer bestäms den av hur vanliga paren är i träningsdatat.
Tokenisering i praktiken
Mellanslag hör oftast till början av nästa token. Svenska delas i fler bitar än engelska eftersom tokenizern tränats mest på engelska.
Engelska, 6 ord
6 tokens
Svenska, 4 ord
6 tokens
Siffror
Llama 3:s tokenizer delar siffror i grupper om högst tre.
Varför det spelar roll
- Kostnad och hastighet. Varje output-token är ett forward pass. Fler tokens per ord betyder långsammare svar.
- Kontextfönstret mäts i tokens, inte ord.
- Konstiga fel. Modellen ser aldrig enskilda bokstäver i ”atten”, därför är det svårt för den att räkna bokstäver.
- Tokenizern måste matcha modellen. Fel tokenizer ger ids som pekar på fel rader i embedding-tabellen.
Uppdelningarna till vänster är illustrativa. Exakta gränser beror på tokenizerns merges.
Chat template och specialtokens
En chattmodell ser aldrig en lista med meddelanden. Den ser en enda lång textsträng med specialtokens som markerar roller. vLLM bygger strängen med modellens chat template (Jinja i tokenizer_config.json).
Strängen slutar mitt i assistentens tur. Modellen fortsätter därifrån, och genereringen stoppas när den producerar <|eot_id|>.
| Specialtoken | id | Roll |
|---|---|---|
| <|begin_of_text|> | 128000 | början på sekvensen (BOS) |
| <|start_header_id|> | 128006 | rollnamn börjar |
| <|end_header_id|> | 128007 | rollnamn slutar |
| <|eot_id|> | 128009 | slut på en tur, används som stopptoken |
Efter tokenisering har vi ids [B, T], en matris med heltal. Det är allt modellen får.
Specialtokens har egna rader i embedding-tabellen precis som vanliga tokens. Deras betydelse lärs in under finjusteringen.
Embedding: från id till vektor
Varje tokennummer slås upp i en jättetabell och byts mot en lista med 4096 tal. Det är modellens första bild av ordet.
Embedding-tabellen E [V, d] har en rad per token. Steget är en ren tabelluppslagning, ingen multiplikation:
Resultatet x är startvärdet i residual stream. Varje token är nu en punkt i ett 4096-dimensionellt rum.
Tabellen är 128 256 × 4 096 ≈ 525 miljoner parametrar, ungefär 6,5 % av modellen.
Det här är den enda platsen i en tät modell där bara en liten del av vikterna används per token: fem rader av 128 256.
Sammanfattning av modul 2
Det viktigaste: Text → tokens
- Tokenizern gör text till ids med en fast vokabulär: 128 256 tokens i Llama 3.
- Antalet tokens, inte ord, styr kostnad, hastighet och hur mycket som ryms i kontexten.
- En chat template gör en konversation till en enda sträng med specialtokens för roller.
- Embedding är en tabelluppslagning: ids [T] → x [T, 4096].
Kan du förklara varje punkt med egna ord för någon annan? Då är du redo för quizet på nästa slide.
Testa dig själv: tokens
Räkna och resonera
Varför är ett svenskt svar ofta långsammare att generera än ett engelskt med samma innehåll?
Svenska delas i fler tokens per ord, och varje output-token kräver ett eget forward pass genom hela modellen.
Vilken shape har embedding-utdata för en batch med 4 prompter om 100 tokens i Llama-3-8B?
[4, 100, 4096]. (I praktiken plattar vLLM ut batchen till [400, 4096], mer om det i modul 8.)
Vad händer om du skickar en prompt utan chat template till en instruktionstränad modell?
Modellen ser text i ett format den inte tränats på att svara på. Den fortsätter ofta texten i stället för att svara, eller slutar inte i tid eftersom den aldrig ser en assistent-header.
Modul 3 av 11, cirka 40 minuter
Blocket och residual stream
Kärnan i modellen: samma block 32 gånger, och en arbetsyta per token som aldrig byter shape.
När du är klar kan du
- förklara vad transformer, decoder-only och ett block är
- förklara residual stream och varför shapen aldrig ändras
- räkna upp vikterna i ett lager och deras shapes
- räkna parametrarna i ett lager och i hela modellen
- förklara vad attention och MLP gör och hur de skiljer sig
- följa Q, K och V genom attention med rätt shapes
- förklara causal mask, RoPE och GQA
- räkna parametrarna i ett lager
Uppgiften: från vektorer till nästa token
Ett ord vet till en början inget om orden runt omkring. Modellens jobb är att låta orden dela information, tills den sista positionen vet tillräckligt för att gissa nästa ord.
Efter modul 2 har vi en vektor per token, x [T, 4096]. Men varje vektor vet bara något om sin egen token. Vektorn för ”på” vet inte att det var en katt som satt.
För att gissa nästa token behöver sista positionen veta allt relevant om hela texten: vem som gör vad, var, och vad som brukar komma härnäst.
Det är jobbet för resten av modellen: fylla varje tokens vektor med sammanhang, steg för steg, tills den sista räcker för att förutsäga fortsättningen.
Det som gör jobbet kallas en transformer. Nästa slides förklarar ordet, sedan hur den är byggd.
Etiketterna beskriver i ord vad vektorerna ungefär kan bära. I modellen är det bara tal, och innehållet är inte så prydligt uppdelat.
Vad betyder ”transformer” och ”decoder-only”?
”Transformer” är namnet på byggritningen som alla moderna språkmodeller följer. ”Decoder-only” betyder att modellen läser texten från vänster till höger och fortsätter den.
Transformer är en arkitektur som presenterades 2017 i artikeln ”Attention Is All You Need”. Det nya var att låta attention, mekanismen där tokens hämtar information från varandra, göra nästan allt arbete. Allt i den här kursen är en transformer.
Originalet byggdes för översättning och hade två halvor:
- en encoder som läser hela källtexten, där varje ord ser alla andra
- en decoder som skriver översättningen ett ord i taget och bara ser det som redan skrivits
Decoder-only betyder att man bara behåller den andra halvan. Det räcker för att fortsätta text: prompten och svaret är en enda sekvens, och varje token ser bara tokens före sig själv. GPT- och Llama-familjerna är byggda så.
Ett block, staplat 32 gånger
Modellen är samma byggsten upprepad många gånger, som våningar i ett hus. Varje våning förfinar förståelsen lite till.
Modellen består av en enda konstruktion, ett block (kallas också lager), som upprepas. Antalet kallas L. Llama-3-8B har L = 32.
Alla block har samma ritning: först attention, sedan MLP. Men varje block har egna vikter, så de lär sig göra olika saker.
Varför stapla? Ett block kan bara göra en runda av ”hämta information från andra tokens, bearbeta den”. Många slutsatser kräver flera rundor i följd, där varje runda bygger på den förra.
Dra i reglaget och se vad fler block kostar och ger.
Residual stream: en gemensam arbetsyta
Varje ord har ett anteckningsblock som följer med genom hela modellen. Varje våning läser det och lägger till en anteckning, men suddar aldrig.
Varje token har en vektor med 4096 tal som följer med genom hela modellen. Den kallas residual stream, och vi skriver den x.
Tänk på den som ett anteckningsblock per token. Varje block i modellen läser blocket, räknar fram en kort anteckning och lägger till den. Ingen suddar ut det som redan står där.
Namnet kommer från residual connection: varje block räknar bara fram en korrigering, en rest, som läggs ovanpå. Själva vägen rakt igenom, där x passerar oförändrad, finns alltid kvar.
Det ger tre fördelar: information från tidiga block finns kvar till slutet, träning av djupa modeller blir stabil, och strömmen blir den naturliga platsen att mäta och påverka modellen.
8 dimensioner i stället för 4096. Konturen visar det senaste bidraget.
Shapen ändras aldrig, förrän i slutet
Anteckningsblocket har samma storlek hela vägen genom modellen. Först allra sist görs det om till ett tal per möjligt nästa ord.
Strömmen x har shape [T, 4096]: T rader, en per token, och 4096 tal per rad. Den shapen gäller efter embedding, efter varje block och efter final norm.
Det är ett medvetet val, och det möjliggör tre saker:
- Block kan staplas. Det som kommer ut ur ett block passar rakt in i nästa.
- Bidrag kan adderas. x + bidrag kräver att båda har samma shape.
- Strömmen kan läsas var som helst. Efter vilket block som helst är det samma sorts vektor, vilket tolkningsmetoder utnyttjar.
Först i LM head, modellens sista matris, byter den form: från 4096 tal per rad till 128 256, ett tal per token i vokabulären. Mer om det i modul 4.
Normalisering: varför och var
När man hela tiden lägger till anteckningar blir talen större och större. Normalisering skalar ner dem till en jämn nivå innan nästa steg läser dem.
Problemet. Eftersom varje block adderar till strömmen växer x i storlek ju längre in i modellen man kommer. Attention och MLP fungerar däremot bäst om deras indata alltid har ungefär samma skala.
Lösningen är normalisering: skala om en vektor så att dess storlek (RMS) blir 1, utan att ändra riktningen. Formeln kommer på RMSNorm-sliden.
Var man normaliserar finns det två varianter av:
- Post-norm (originalet 2017): strömmen själv normaliseras efter varje addition.
- Pre-norm (Llama och de flesta moderna modeller): blocket normaliserar en kopia innan det läser. Strömmen själv lämnas orörd och får växa.
Pre-norm gör djupa modeller stabilare att träna, eftersom vägen rakt genom strömmen aldrig störs. Priset är att strömmen aldrig normaliseras under vägen, så den måste normaliseras en gång i slutet, före LM head. Det är final RMSNorm.
Kurvan är en illustration. Att strömmens storlek växer med djupet i pre-norm-modeller stämmer i stort, men exakta värden varierar mellan modeller.
Nu i en bild: hela modellen
Här sätts alla delar ihop till en enda bild. Den är kartan för resten av modulen.
Bitarna från de senaste slidesen, ihop:
- Embedding gör ids till vektorer: x [T, 4096].
- Residual stream är den lodräta orange linjen: x, som följer med hela vägen.
- Ett block (den streckade rutan) läser strömmen två gånger, via en normaliserad kopia, och adderar två bidrag. Först attention, sedan MLP.
- Blocket upprepas 32 gånger med nya vikter. Shapen ändras aldrig.
- Final RMSNorm normaliserar strömmen en enda gång, och LM head gör den till ett tal per token i vokabulären.
Resten av modulen öppnar den streckade rutan och går igenom varje del.
Vad ett lager innehåller: nio delar
Ett lager i Llama består av bara nio saker: sju stora tabeller med tal (viktmatriser) och två små listor. Allt annat som händer i lagret är fasta räkneregler utan inlärda tal. Klicka på en del för att se den i bilden.
Så blir det 8 miljarder parametrar
”8 miljarder parametrar” är helt enkelt antalet tal i alla tabeller tillsammans. Det mesta sitter i de nio delarna gånger 32 lager. Resten är ordförrådet i början (embedding) och i slutet (LM head).
Blocket som ett löpande band
Inuti varje block händer sex saker i fast ordning, som stationer på ett löpande band. Inget körs samtidigt: MLP:n väntar på attention, eftersom den ska bearbeta det attention har hämtat.
Ett lager inifrån: nio vikter och några fasta räkneregler
Nu öppnar vi en våning och tittar på allt som finns i den: nio uppsättningar inlärda tal och några fasta räkneregler.
”Ett lager” i en transformer är inte en matris utan hela blocket. I Llama-3-8B innehåller varje lager sju viktmatriser och två viktvektorer, totalt drygt 218 miljoner parametrar.
Mellan vikterna finns steg som saknar vikter: RoPE, scores, softmax, SiLU och de två additionerna. De är fasta formler som är likadana i alla modeller av samma typ.
Klicka på en del i diagrammet för att se vad den gör, dess shape och vad den heter i en modellfil.
32 lager i en modellfil
En modellfil är en lista med namngivna tabeller med tal. Här ser du exakt vilka tabeller som hör till varje våning.
En modellfil (safetensors) är i grunden en ordbok: namn → tensor. Llama-3-8B innehåller 291 tensorer: 9 per lager gånger 32 lager, plus tre utanför lagren.
| Utanför lagren | Shape |
|---|---|
| model.embed_tokens.weight | [128256, 4096] |
| model.norm.weight | [4096] |
| lm_head.weight | [128256, 4096] |
Alla 32 lager har exakt samma namn och shapes, bara lagrets nummer skiljer. Värdena är helt olika: varje lager har lärt sig sin egen del av jobbet. Därför kan samma kod köras i en loop, med en ny uppsättning vikter varje varv.
PyTorch lagrar en linjär vikt som [ut, in]. Därför står det [1024, 4096] för k_proj, medan kursen skriver [4096, 1024] i flödesriktningen. Det är samma matris, transponerad.
Ett utsnitt ur q_proj.weight, 10 × 24 av 4096 × 4096 tal:
Värdena i utsnitten är påhittade. I en riktig modell ser de ut som brus för ögat, men skiljer sig på samma sätt mellan lagren.
Två rader kod, ett helt lager
Varje lager är bara de här två raderna. Stega igenom dem med en token och se exakt vad som läses, vad som räknas och vad som skrivs till strömmen.
6 av 4096 dimensioner för token ”den”. Värdena är påhittade men all aritmetik är räknad på riktigt. RMS visas till höger.
RMSNorm
Här är den exakta formeln för normaliseringen: dela varje tal med vektorns genomsnittliga storlek.
Normaliseringen skalar varje tokens vektor så att dess RMS blir 1, och multiplicerar sedan med en inlärd vikt per dimension:
Utan normalisering skulle storleken på strömmen växa lager för lager, eftersom allt adderas. Attention och MLP vill ha indata i en stabil skala.
RMSNorm är en förenklad LayerNorm: ingen medelvärdessubtraktion och ingen bias. Billigare, och fungerar lika bra.
Division med RMS bevarar riktningen och bestämmer storleken. Den inlärda vikten g skalar sedan varje dimension för sig, så riktningen förskjuts lite, men samma förskjutning gäller för varje token, och därför är riktningar i strömmen fortfarande meningsfulla och jämförbara.
Sammanfattning av modul 3
Det viktigaste: Blocket och residual stream
- En decoder-only transformer är ett block (attention + MLP) upprepat L = 32 gånger, där varje token bara ser tokens före sig.
- Residual stream x är en arbetsyta per token som varje block läser och adderar till. Dess shape [T, 4096] ändras aldrig förrän i LM head.
- Pre-norm: blocken läser en normaliserad kopia, strömmen själv får växa och normaliseras bara en gång, före LM head.
- Ett lager är 7 viktmatriser och 2 normvikter, cirka 218 M parametrar. Alla 32 lager har samma shapes men olika värden.
Kan du förklara varje punkt med egna ord för någon annan? Då är du redo för quizet på nästa slide.
Modul 4 av 11, cirka 50 minuter
Attention och MLP
Inuti blocket: attention flyttar information mellan tokens, MLP bearbetar varje token för sig. Här är varje tensor och varje vikt.
När du är klar kan du
- följa Q, K och V genom attention med rätt shapes
- förklara causal mask, RoPE och GQA
- förklara vad MLP:n gör och varför den står för 70 % av parametrarna
- läsa forward pass som kod
Attention och MLP: två helt olika jobb
De två delarna i varje våning har helt olika uppgifter. Attention låter orden prata med varandra, MLP:n låter varje ord tänka för sig.
Tänk på residual stream som en tabell x [T, 4096]: en rad per token, en kolumn per dimension.
Attention flyttar information mellan rader. Varje token hämtar en viktad blandning från tidigare tokens. Det är så ”den” kan få veta något om ”Katten”.
MLP:n bearbetar en rad i taget. Den blandar om dimensionerna inom raden, men kan aldrig se andra rader. Alla rader går genom samma MLP-vikter, var för sig.
En detalj som är lätt att missa: även inne i attention arbetar viktmatriserna Wq, Wk, Wv och Wo rad för rad. Det enda steget som korsar rader är P · V.
Attention i klartext
Attention är modellens sätt att låta ord samla information från andra ord. Det fungerar ungefär som en sökning, med en viktig skillnad: man får inte ett enda träffresultat, utan en blandning av alla, viktad efter hur bra de matchar.
2. Ordets query jämförs med varje tidigare ords key. Ju mer de pekar åt samma håll, desto högre poäng.
3. Poängen görs om till procent som summerar till 100 %.
4. Ordet tar en blandning av alla values, viktad med procenten. Inget sorteras bort helt, men det som matchar dåligt får väldigt liten vikt.
Liknelse: att söka bland videor. Välj vad ordet ”den” letar efter:
Attention-formeln bit för bit
Hela attention-steget ryms i en rad. Klicka på varje del för att se vad den gör, varför den finns och vilka tal den ger i vårt räkneexempel från ”Attention räknat för hand”.
Attention 1: Q, K och V
Varje token gör tre olika versioner av sin vektor: en fråga, en etikett och ett innehåll. Det sker med tre inlärda matriser.
Den normaliserade strömmen projiceras till tre nya tensorer:
- Query: vad den här positionen letar efter
- Key: vad den här positionen erbjuder
- Value: vad den skickar vidare om den väljs
K och V är smalare än Q eftersom Llama-3-8B använder GQA: 8 KV heads mot 32 query heads.
vLLM slår ihop de tre matriserna till en enda qkv_proj och delar resultatet efteråt. En stor matmul är snabbare än tre små.
Attention 2: dela upp i heads
I stället för en stor fråga ställer varje token 32 mindre frågor samtidigt, så att den kan leta efter flera olika saker på en gång.
Q är 32 oberoende queries om 128 dimensioner bredvid varandra. En reshape gör det explicit, utan att någon data flyttas.
Varje head kör sin egen attention parallellt och kan lära sig leta efter olika saker: föregående token, syftningar, matchande parenteser.
Attention 3: RoPE ger position
Attention vet inte i sig vilket ord som kom först. RoPE vrider frågor och etiketter efter position, så att avståndet mellan orden märks.
Attention i sig vet inget om ordning. Llama roterar Q och K beroende på position innan de jämförs (Rotary Position Embedding).
Varje head-vektor om 128 dimensioner ses som 64 par. Par i roteras med vinkeln pos · θᵢ:
Tidiga par roterar snabbt och fångar närhet. Sena par roterar långsamt och skiljer på långa avstånd.
Shapes ändras inte. RoPE appliceras på Q och K, aldrig V, och K lagras roterad i KV-cachen.
Attention 4: scores och causal mask
Här jämförs varje fråga med alla tidigare etiketter. Resultatet är en tabell som säger hur mycket varje ord ska lyssna på varje annat.
Varje query jämförs med varje key via skalärprodukt. Det ger en matris per head:
Med GQA delar 4 query heads på samma K: head 0–3 använder KV-head 0, head 4–7 KV-head 1, och så vidare.
Masken gör att position i bara ser position 0..i. Det är det som gör modellen autoregressiv, och det är därför K och V för gamla positioner aldrig ändras.
S är T × T. Vid 32 000 tokens blir det en miljard tal per head och lager. FlashAttention räknar därför i block och skriver aldrig ut hela matrisen i minnet.
Attention 5: hämta values och skriv tillbaka
Varje token tar en blandning av de innehåll den valt att lyssna på, och lägger blandningen till sin egen vektor.
Vikterna i P används för att blanda ihop values:
Varje tokens utdata är en viktad summa av tidigare tokens values. Wo blandar ihop resultaten från alla heads och skriver till strömmen.
Klicka på ord till höger. Vikterna är handplockade för att visa idén, inte tagna ur en riktig modell.
Attention räknat för hand
Ett head, tre tokens och två dimensioner i stället för 128. Vi räknar vad ”den” hämtar från ”Katten”, ”satt” och sig själv. Varje tal nedan är uträknat, inget är påhittat.
GQA: färre KV heads
Att spara alla etiketter och innehåll kostar minne. GQA låter flera frågor dela på samma etiketter, vilket sparar mycket minne med liten kvalitetsförlust.
I klassisk Multi-Head Attention har varje query head egna K och V. Grouped-Query Attention låter en grupp query heads dela på ett KV-par.
Kvaliteten påverkas lite, men KV-cachen krymper med faktorn n_h / n_kv. För Llama-3-8B är det 4 gånger mindre cache, vilket ger plats för 4 gånger fler samtidiga sekvenser.
| Variant | n_kv | KV per token och lager |
|---|---|---|
| MHA | 32 | 2·32·128 = 8192 tal |
| GQA (Llama 3) | 8 | 2·8·128 = 2048 tal |
| MQA | 1 | 2·1·128 = 256 tal |
MLP i klartext
Attention har samlat in information. MLP:n tar reda på vad den betyder. Den arbetar med ett ord i taget och fungerar ungefär som ett stort uppslagsverk: känner den igen ett mönster, lägger den till det den vet om det.
2. Aktivering. SiLU på gaten fungerar som en dimmer: neuroner vars mönster passar släpps fram, resten dämpas nästan till noll.
3. Projektion. En tredje matris, down, översätter de neuroner som tändes tillbaka till 4096 tal.
Som ett minne
En neurons mönster i W_gate är som en nyckel och dess rad i W_down som ett värde. Slå på det ordets vektor innehåller:
MLP med SwiGLU
Nu bearbetas varje token för sig: vektorn blåses upp, de mönster som passar släpps fram, och resultatet trycks ihop igen.
MLP:n jobbar på varje token oberoende. Den expanderar till 3,5 gånger bredden, låter en gate avgöra vilka av de 14 336 neuronerna som släpps fram och komprimerar tillbaka.
SiLU(z) = z · σ(z) är en mjuk ReLU. Gaten multipliceras elementvis med up, så varje neuron är ”en detektor gånger ett värde”.
vLLM slår ihop W_gate och W_up till en gate_up_proj [4096, 28672].
MLP:n som detektorer
Varje neuron i MLP:n är en liten detektor som letar efter ett visst mönster. Hittar den det, lägger den till något i ordets anteckningar.
Varje neuron i MLP:n är en kolumn i W_gate och en rad i W_down. Kolumnen i W_gate är ett mönster. Skalärprodukten med tokenens vektor avgör hur väl vektorn matchar mönstret.
En neuron som slår till skriver alltså sin egen riktning till strömmen. Med 14 336 neuroner per lager och 32 lager finns det plats för väldigt många sådana regler.
Slå på och av egenskaper i tokenens vektor till höger och se vilka neuroner som tänds.
Förenklad bild. Verkliga neuroner har sällan en ren mänsklig betydelse; de flesta reagerar på blandningar av många mönster, och ett begrepp sprids ofta över många neuroner (superposition).
Egenskaper i vektorn för ”den” efter attention:
Var parametrarna finns
Modellen har 8 miljarder inlärda tal. Här ser du var de sitter: de flesta i MLP:erna.
| Del | Shape | Parametrar |
|---|---|---|
| Embedding | [128256, 4096] | 525 M |
| Wq, Wo (per lager) | 2 × [4096, 4096] | 33,6 M |
| Wk, Wv (per lager) | 2 × [4096, 1024] | 8,4 M |
| Gate, up, down (per lager) | 3 × 4096·14336 | 176,2 M |
| Norm-vikter (per lager) | 2 × [4096] | 8 K |
| LM head | [4096, 128256] | 525 M |
| Totalt | ≈ 8,03 B |
Mer än två tredjedelar av modellen är MLP. Attention-vikterna är få, men attention-beräkningen växer med kontextlängden.
Minnet för vikterna i bf16 (2 byte per parameter): 8,03 B × 2 ≈ 16 GB. Det är talet som avgör vilken GPU du behöver, innan KV-cachen ens räknats med.
Lager för lager: hur svaret växer fram
Man kan titta i anteckningsblocket mitt i modellen och fråga: vad skulle modellen gissa om den slutade här? Svaret skärps våning för våning.
Eftersom strömmen har samma shape i alla lager kan man ta vilket mellanläge som helst och skicka det genom final norm och LM head, som om modellen slutade där. Tekniken kallas logit lens.
Resultatet visar hur förutsägelsen byggs upp:
- Tidigt liknar strömmen mest den egna tokenens embedding.
- Mitt i dyker grammatiskt rimliga fortsättningar upp.
- Sent har attention hämtat sammanhanget och MLP:erna dragit slutsatser, och rätt svar tar över.
Inget lager ”bestämmer” svaret. Varje lager knuffar fördelningen en bit, och summan av alla knuffar blir den slutliga.
Siffrorna är påhittade för att visa mönstret. I verkliga modeller är de tidiga lagren ofta svåra att läsa med en ren logit lens, och varianter som tuned lens fungerar bättre.
Prompt: Katten satt på mattan eftersom den var
Spåra tensorerna genom modellen
Klicka dig igenom ett forward pass. Byt mellan prefill (hela prompten på en gång) och decode (en ny token med KV-cache).
Hela forward pass som kod
Här är hela modellen som ett kort program. Kommentarerna till höger visar formen på datat efter varje rad.
Allt i modulen på en skärm, som PyTorch-lik pseudokod för en sekvens. Jämför rad för rad med tensorspårningen.
Samma funktion gör både prefill och decode. Enda skillnaden är T: vid prefill är T promptens längd och cachen tom, vid decode är T = 1 och cachen innehåller allt tidigare.
repeat_kv visar GQA som idé: varje KV head används av 4 query heads. Riktiga kernels kopierar inget, de indexerar bara.
Koden i vLLM (models/llama.py) har samma struktur: qkv_proj, o_proj, gate_up_proj, down_proj. Attention och cache-hanteringen ligger i en separat backend.
def forward(ids, pos, cache): # ids [T] x = E[ids] # [T, 4096] for l in range(32): h = rmsnorm(x, g1[l]) # [T, 4096] q, k, v = split(h @ W_qkv[l]) # [T,4096] [T,1024] [T,1024] q = q.view(T, 32, 128).transpose(0, 1) # [32, T, 128] k = k.view(T, 8, 128).transpose(0, 1) # [ 8, T, 128] v = v.view(T, 8, 128).transpose(0, 1) # [ 8, T, 128] q, k = rope(q, pos), rope(k, pos) K, V = cache[l].append(k, v) # [8, ctx, 128] ctx = gamla + T K, V = repeat_kv(K, 4), repeat_kv(V, 4) # [32, ctx, 128] S = q @ K.transpose(-1, -2) / sqrt(128) # [32, T, ctx] S = S + causal_mask # −inf där key ligger efter query P = softmax(S, dim=-1) # [32, T, ctx] o = (P @ V).transpose(0, 1).reshape(T, 4096) x = x + o @ W_o[l] # [T, 4096] h = rmsnorm(x, g2[l]) gate, up = split(h @ W_gate_up[l]) # [T, 14336] × 2 x = x + (silu(gate) * up) @ W_down[l] # [T, 4096] h = rmsnorm(x[-1], g_final) # [4096] bara sista positionen return h @ W_U # [128256] logits
Sammanfattning av modul 4
Det viktigaste: Attention och MLP
- Attention är det enda stället där positioner utbyter information: Q [32, T, 128], K och V [8, T, 128], scores [32, T, T].
- Causal mask: position i ser bara 0..i. Därför ändras aldrig K och V för gamla positioner.
- RoPE roterar Q och K så att bara avståndet mellan positioner spelar roll.
- MLP:n arbetar per position: [T, 4096] → [T, 14336] → [T, 4096], och står för 70 % av parametrarna.
- Totalt 8,03 B parametrar, ungefär 16 GB i bf16.
Kan du förklara varje punkt med egna ord för någon annan? Då är du redo för quizet på nästa slide.
Testa dig själv: transformerblocket
Räkna och resonera
Vilken shape har attention-scores i ett lager vid prefill av 1000 tokens i Llama-3-8B?
[32, 1000, 1000] per sekvens: en T × T-matris per query head.
Varför ändras aldrig K och V för en gammal position när nya tokens tillkommer?
Causal mask: position i ser bara 0..i. Allt som påverkar K och V för position i, inklusive alla tidigare lager, beror bara på tokens 0..i. Därför kan de sparas, vilket är hela idén med KV-cachen.
Var finns flest parametrar, attention eller MLP?
MLP: 176 M per lager mot 42 M för attention i Llama-3-8B.
Vad är skillnaden mellan n_h och n_kv, och vad påverkar den?
n_h är antalet query heads (32), n_kv antalet KV heads (8). Kvoten 4 bestämmer hur många query heads som delar på samma K och V, och hur mycket mindre KV-cachen blir jämfört med MHA.
Modul 5 av 11, cirka 10 minuter
Logits → text
Sista vektorn blir en sannolikhetsfördelning över nästa token, en token dras, och allt börjar om.
När du är klar kan du
- förklara vad logits är och varför bara sista positionen behövs
- beskriva vad temperature, top-k och top-p gör
- beskriva loopen och dess stoppvillkor
LM head: från vektor till logits
Efter sista våningen görs det sista ordets anteckningar om till ett betyg för varje ord i vokabulären: hur troligt är det att just det ordet kommer härnäst?
W_U (unembedding, lm_head) ger ett tal per token i vokabulären och per position. Logit på position t är modellens bedömning av vad som kommer på position t + 1.
Vid generering behövs bara sista raden. vLLM räknar därför bara logits för den sista positionen i varje sekvens. Att räkna alla T rader för en lång prompt skulle kosta T × 128 256 tal i onödan.
I vissa modeller delar LM head vikter med embedding-tabellen (tied embeddings). Llama-3-8B gör inte det.
Sampling: temperature, top-k och top-p
Modellen ger sannolikheter, men något måste välja ett ord. Några rattar styr hur försiktigt eller djärvt valet blir.
- Temperature < 1 skärper fördelningen, > 1 plattar till den. Temperature 0 betyder greedy: ta alltid den största.
- Top-k klipper bort allt utom de k bästa.
- Top-p (nucleus) klipper adaptivt: när modellen är säker återstår få kandidater, när den är osäker många.
Logits efter ”Katten satt på” nedan är påhittade, men mekaniken är exakt den som körs.
Den autoregressiva loopen
Modellen skriver ett ord i taget. Varje nytt ord läggs till texten, och hela proceduren körs igen.
En genererad token läggs till sekvensen och hela modellen körs igen. Svaret växer en token per varv tills något av följande händer:
- modellen producerar en stopptoken (<|eot_id|>)
- en stoppsträng matchar i den detokeniserade texten
- max_tokens för förfrågan är nådd
- sekvensen når modellens max_model_len
Utan KV-cache skulle varje varv räkna om hela sekvensen. Med cache räknas bara den nya positionen, men varje varv måste ändå läsa alla vikter. Det är grunden till allt i modul 6.
Greedy (temperature 0) är inte garanterat deterministiskt i en server. Olika batchstorlekar kan ge olika ordning på flyttalsoperationer, och små skillnader kan vända en jämn argmax.
Sammanfattning av modul 5
Det viktigaste: Logits → text
- LM head: h [4096] @ W_U → logits [128256]. Bara sista positionen behövs vid generering.
- Temperature skalar logits, top-k och top-p klipper bort svansen, sedan dras en token.
- Generering är autoregressiv: ett forward pass per token tills ett stoppvillkor nås.
- Temperature 0 är greedy men inte garanterat deterministiskt i en server.
Kan du förklara varje punkt med egna ord för någon annan? Då är du redo för quizet på nästa slide.
Testa dig själv: logits och sampling
Räkna och resonera
Hur stor är logits-tensorn om du (felaktigt) räknar logits för alla positioner i en prompt på 8 000 tokens, i fp32?
8 000 × 128 256 × 4 byte ≈ 4,1 GB. Därför räknas bara sista positionen.
Vad gör top-p = 0,9 när modellen ger 95 % till en enda token?
Bara den token återstår, eftersom den ensam når 0,9. Samplingen blir i praktiken greedy just då.
Varför kan temperature 0 ge olika svar på samma fråga i en server med många användare?
Batchsammansättningen påverkar kernelval och summeringsordning. Flyttalsaddition är inte associativ, så logits kan skilja sig i sista decimalen och vända en jämn argmax.
Modul 6 av 11, cirka 10 minuter
Träning i korthet
Var kommer vikterna ifrån? En kort bakgrund som förklarar varför inference ser ut som den gör.
När du är klar kan du
- förklara next-token loss
- säga varför träning är parallell över sekvensen men generering seriell
- förklara varför träning kräver åtta gånger mer minne per parameter
Träning: förutsäg nästa token
Modellen lär sig genom att gissa nästa ord i enorma mängder text, se hur fel den hade och justera sina tal en aning. Det upprepas biljontals gånger.
Under träning är målet för varje position helt enkelt nästa token i texten. Inputs och targets är samma sekvens, förskjuten ett steg.
Tack vare causal mask räknas alla T förutsägelser i ett enda forward pass. Det är därför träning är parallell över sekvensen medan generering är seriell: vid generering finns inte nästa token förrän den samplats.
Positionen som ser ”Katten satt på” tränas att ge hög sannolikhet åt ” mattan”.
Gradienter och minne
För varje inlärt tal räknas fram åt vilket håll det ska justeras. Att hålla reda på allt det kräver mycket mer minne än att bara använda modellen.
Backpropagation ger en gradient med exakt samma shape som varje viktmatris. Optimeraren (AdamW) uppdaterar vikterna i gradientens motsatta riktning och håller två egna tillstånd per parameter.
Med mixed precision kostar varje parameter ungefär 16 byte under träning, mot 2 byte vid inference. Därtill kommer aktiveringar som måste sparas för backward pass.
Det är därför du kan köra en 8B-modell på en GPU med 24 GB, medan full finjustering av samma modell ryms på en nod med åtta GPU:er. Med LoRA eller QLoRA räcker det med en enda GPU. Det är förträningen från grunden som kräver ett kluster, och då handlar det minst lika mycket om beräkningskraft och tid som om minne.
Från basmodell till assistent
Arkitekturen är densamma genom alla faser. Bara vikterna ändras, och därmed beteendet.
För inference är fasen ointressant: en resonerande modell körs exakt som en vanlig. Den genererar bara många fler tokens innan svaret, vilket syns i kostnad och latens.
Sammanfattning av modul 6
Det viktigaste: Träning i korthet
- Target är samma sekvens skiftad ett steg. Alla positioner tränas i ett enda forward pass.
- Gradienten har samma shape som vikten. AdamW håller extra tillstånd: cirka 16 byte per parameter mot 2 vid inference.
- Pretraining, SFT och RL ändrar bara vikterna, aldrig arkitekturen. En resonerande modell körs som vilken annan.
Kan du förklara varje punkt med egna ord för någon annan? Då är du redo för quizet på nästa slide.
Testa dig själv: träning
Modul 7 av 11, cirka 45 minuter
Inference och KV-cache
Hur modellen faktiskt körs: två faser med helt olika flaskhalsar, och cachen som gör generering praktiskt möjlig.
När du är klar kan du
- skilja prefill från decode och säga vad som begränsar vardera
- förklara exakt vad KV-cachen sparar och varför det är korrekt
- räkna KV-minne för en modell och en last
- förklara varför batching ökar genomströmningen
Vad är ett grafikkort, och varför spelar minnet roll?
Språkmodeller körs på grafikkort (GPU:er). Ett grafikkort har tusentals små räknare som arbetar samtidigt, och ett eget snabbt minne. För att förstå resten av modulen räcker det med en bild: ett kök med väldigt många kockar, men en ganska smal dörr till skafferiet.
- Kockarna är räknekraften. Ett modernt datacenterkort som H100 klarar cirka 990 TFLOPS i bf16, alltså ungefär 500 biljoner multiplikationer per sekund, eftersom varje multiplikation räknas ihop med en addition (FMA).
- Skafferiet är grafikkortets minne (VRAM), till exempel 80 GB. Där ligger modellens vikter och KV-cachen.
- Dörren är minnesbandbredden, ungefär 3,35 TB per sekund. Varje ingrediens kockarna använder måste bäras in genom den.
När modellen skriver ett ord måste alla 16 GB vikter bäras in genom dörren, men kockarna gör bara lite arbete med dem. De står mest och väntar. Det kallas att vara memory-bound, och det förklarar nästan allt i resten av kursen: varför man skriver för många användare samtidigt, varför KV-cachen är så viktig och varför man gärna kvantiserar.
Två faser: prefill och decode
Ett svar tas fram i två faser. Först läses hela frågan på en gång, vilket går fort. Sedan skrivs svaret ett ord i taget, vilket tar längre tid.
Prefill kör hela prompten på en gång. T rader multipliceras med samma vikter, så varje byte som läses från minnet används för många beräkningar. Men prefill blir compute-bound först när T är stort nog: brytpunkten är samma kvot som kommer längre fram, ungefär 300 operationer per läst byte på en H100. En kort prompt är memory-bound den också.
Decode kör en token åt gången. Alla 16 GB vikter läses för att räkna en enda rad. GPU:n väntar nästan hela tiden på minnet (memory-bound).
Prefill bestämmer hur snabbt första token kommer. Decode bestämmer hur snabbt resten strömmar ut. Nästan allt en inference-motor gör handlar om att få decode att använda GPU:n bättre.
KV-cache i klartext
Tänk dig att du skriver en uppsats och före varje nytt ord måste läsa om hela texten från början. Det blir långsammare för varje ord. KV-cachen är modellens anteckningar: den sparar det som behövs från varje tidigare ord, så att bara det nya ordet behöver bearbetas.
KV-cache steg för steg
När modellen skriver ord nummer 100 behöver den etiketterna och innehållet för de 99 tidigare. KV-cachen sparar dem, så att de inte behöver räknas om varje gång.
K och V för en position ändras aldrig efter att de räknats (causal mask). Så varför räkna om dem?
Utan cache räknar varje decode-steg om K och V för hela sekvensen. Med cache räknas bara den nya positionens K och V, som läggs till. Den nya tokenens query jämförs sedan mot alla sparade keys.
Cachen byter beräkning mot minne: varje token i varje sekvens tar plats i GPU-minnet i varje lager så länge sekvensen lever.
Bilden visar ett lager och ett KV head. I verkligheten finns cachen i alla 32 lager och för alla 8 KV heads.
KV-cachen på riktigt: en minimodell
Här visar vi att cachen inte fuskar. En riktig liten modell räknar varje steg på båda sätten, och svaren blir identiska.
Här körs en riktig transformer i din webbläsare: 2 lager, 2 heads, d = 16, med attention, causal mask, RMSNorm och MLP. Vikterna är slumpade, så texten blir nonsens. Mekaniken är däremot äkta.
Varje steg räknas på två sätt:
- Utan cache: hela sekvensen körs om från början.
- Med cache: bara den nya token körs, K och V för resten läses.
Jämför kolumnen längst till höger: skillnaden i logits är noll eller ren flyttalsavrundning. KV-cachen är ingen approximation.
Rutnätet visar den faktiska K-cachen i lager 0, head 0. En rad per position, och gamla rader ändras aldrig.
K-cache, lager 0, head 0: [positioner, 8]
Ett decode-steg med shapes
Så här ser ett enda skrivsteg ut i detalj, med cachen inräknad.
Ingen causal mask behövs i decode: den nya tokenen är sist och får se allt.
Arbetet i ett decode-steg består av två helt olika delar:
- Matmuls mot vikter (qkv, o, gate/up, down): samma kostnad oavsett kontextlängd. Delas mellan alla sekvenser i en batch.
- Attention mot cachen: läser p+1 keys och values. Växer linjärt med kontexten och är unik per sekvens, så den kan inte delas.
Därför blir långa konversationer dyrare per token: allt mer av tiden går åt till att läsa cachen.
Hur stor blir KV-cachen?
Cachen tar plats i grafikkortets minne för varje ord i varje pågående konversation. Här räknar du ut hur mycket.
En konversation på 8 000 tokens tar alltså 1 GB cache. På en H100 med 80 GB, där 16 GB går till vikter, blir ungefär 50 GB över till cache, alltså runt 390 000 tokens totalt fördelat på alla samtidiga sekvenser.
Det är den här siffran som i praktiken bestämmer hur många användare en GPU kan betjäna samtidigt.
MLA (DeepSeek) cachar en komprimerad latent vektor i stället för K och V: 576 tal per token och lager.
Vad ligger i GPU-minnet?
Grafikkortets minne delas mellan modellens inlärda tal och cachen för alla pågående konversationer. Det som blir kvar efter modellen avgör hur många som kan prata samtidigt.
När en inference-server har startat är GPU-minnet uppdelat i fyra delar med helt olika livslängd:
| Del | Lever | Storlek bestäms av |
|---|---|---|
| Vikter | hela serverns livstid | parametrar × precision |
| KV-pool | en plats: så länge en sekvens lever | allt som blir över |
| Aktiveringar | ett steg | tokens per steg |
| Overhead | hela tiden | CUDA, grafer |
vLLM allokerar hela KV-poolen direkt vid start. nvidia-smi visar därför runt 90 % upptaget minne även när servern står helt stilla. Det är avsiktligt.
En större token-budget per steg kräver mer utrymme för aktiveringar, och då blir KV-poolen mindre. Allt som inte är vikter är en avvägning mellan de två.
Varför batching räddar decode
Att läsa in modellens 16 GB tar lika lång tid oavsett om man skriver för en eller hundra användare. Därför lönar det sig att skriva för många samtidigt.
En H100 läser ungefär 3,35 TB/s och räknar ungefär 990 TFLOPS i bf16. Kvoten är cirka 300: GPU:n måste göra 300 operationer per läst byte för att inte vänta på minnet.
Decode med en sekvens gör 2 operationer per parameter och läser 2 byte per parameter: kvoten 1. GPU:n står still 99 % av tiden.
Med B sekvenser i samma steg läses vikterna en gång men används B gånger. Genomströmningen ökar nästan linjärt tills beräkningen eller KV-minnet tar slut.
Idealiserad modell för Llama-3-8B i bf16 på en H100. Verkliga siffror ligger lägre, men formen stämmer.
Latensmått: TTFT, ITL och genomströmning
Två tider spelar roll för användaren: hur länge man väntar på första ordet, och hur snabbt resten flyter.
TTFT (time to first token): kötid plus prefill. Växer med promptlängden och med hur mycket annat som körs.
ITL / TPOT (inter-token latency, time per output token): tiden för ett decode-steg. Det är det användaren upplever som skrivhastighet.
Throughput: tokens per sekund summerat över alla användare. Större batch höjer throughput men också ITL för var och en.
En server väljer alltid en punkt på kurvan mellan snabbt för en och effektivt för många. vLLM:s scheduler-parametrar är rattarna för det.
Kapacitetsplanering: räcker en GPU?
Med det du nu vet kan du räkna ut hur många användare ett grafikkort klarar, och hur snabbt det går för var och en.
Nu kan du svara på den praktiska frågan: hur många användare klarar en GPU, och hur snabbt går det för var och en?
- Får vikterna plats? Parametrar × byte per parameter.
- Hur stor blir KV-poolen? 90 % av minnet minus vikter minus ca 5 GB.
- Hur många sekvenser ryms? Poolen delat med KV per sekvens.
- Hur långt blir ett decode-steg? Bytes att läsa (vikter + all aktiv KV) delat med minnesbandbredden.
Steg 3 ger gränsen för samtidighet. Steg 4 ger ITL för alla i batchen.
Modellen är idealiserad (H100-liknande bandbredd, inga köer, alla sekvenser lika långa). Använd den för storleksordningar och för att se vilken resurs som tar slut först.
Kvantisering
Man kan lagra modellens tal med färre siffror. Det sparar minne och gör modellen snabbare, mot en liten förlust i precision.
Färre bitar per vikt betyder färre bytes att läsa per decode-steg och plats för mer KV-cache.
- Weight-only int4 (AWQ, GPTQ): vikter lagras med 4 bitar plus en skalfaktor per grupp om till exempel 128 vikter. Kerneln packar upp till bf16 i farten. Bra för memory-bound decode.
- FP8 (W8A8): både vikter och aktiveringar i 8 bitar. På H100 räknar tensorkärnorna dubbelt så snabbt i FP8, så även prefill blir snabbare.
- FP8 KV-cache: halverar cachen, alltså dubbelt så många tokens i minnet.
Priset är lite precision. Int4 har bara 16 nivåer per grupp, så skalfaktorn måste väljas klokt.
Sammanfattning av modul 7
Det viktigaste: Inference och KV-cache
- Prefill kör T rader per viktläsning och är compute-bound. Decode kör en rad och är memory-bound.
- KV-cachen sparar K och V per position, lager och KV head: 128 KiB per token i Llama-3-8B. Den är exakt, ingen approximation.
- Ett decode-steg är matmuls mot vikterna (delas av hela batchen) plus attention mot cachen (per sekvens, växer med kontexten).
- Batching: vikterna läses en gång för B sekvenser, så genomströmningen ökar nästan linjärt.
- Vikterna avgör om modellen får plats. KV-minnet avgör hur många samtidiga användare som får plats.
- TTFT ≈ kö + prefill. ITL ≈ ett decode-steg.
Kan du förklara varje punkt med egna ord för någon annan? Då är du redo för quizet på nästa slide.
Testa dig själv: inferens och KV-cache
Räkna och resonera
Hur stor är KV-cachen för 16 samtidiga sekvenser om 4 000 tokens i Llama-3-8B med bf16?
16 · 4 000 · 128 KiB ≈ 7,8 GiB.
Varför blir decode inte dubbelt så långsamt när batchen går från 1 till 2 sekvenser?
Steget domineras av att läsa vikterna, och det görs en gång oavsett batchstorlek. Den extra beräkningen och KV-läsningen för sekvens nummer två är liten i jämförelse.
Vad påverkar en lång systemprompt mest: TTFT eller ITL?
Framför allt TTFT, eftersom den ska prefillas. ITL ökar också något eftersom varje decode-steg läser fler cachade positioner. Prefix caching i modul 8 tar bort det mesta av TTFT-kostnaden vid upprepning.
Varför hjälper int4-vikter decode mer än prefill?
Decode är memory-bound, så fyra gånger färre bytes ger nästan fyra gånger snabbare viktläsning. Prefill blir compute-bound först vid långa prompter, när varje läst byte hunnit användas tillräckligt många gånger (kvoten cirka 300 operationer per byte). En kort prompt vinner därför också på int4. Vid långa prompter hjälper int4 bara indirekt, eftersom weight-only int4 fortfarande räknar i bf16 efter uppackning.
Modul 8 av 11, cirka 20 minuter
Modellvarianter
Samma grundidé, olika avvägningar. Vad skiljer modellerna åt, och vad betyder det för den som ska köra dem?
När du är klar kan du
- förklara MoE och skillnaden mellan totala och aktiva parametrar
- jämföra MHA, GQA, MQA och MLA på cachestorlek
- säga vad lång kontext, resonemang och bilder kostar vid inference
Tät modell eller Mixture of Experts
I stället för en stor MLP kan en våning ha många små och en växel som väljer några av dem för varje ord. Modellen blir större men inte långsammare per ord.
I en MoE-modell ersätts MLP:n i varje lager av många mindre MLP:er (experter) och en router. Attention är fortfarande tät.
Varje token skickas till top-k experter. Modellen har alltså mycket kunskap (många parametrar) men låg kostnad per token (få aktiva).
| Modell | Totalt | Aktivt | Experter |
|---|---|---|---|
| Mixtral 8x7B | 46,7 B | 12,9 B | 2 av 8 |
| Qwen3-30B-A3B | 30,5 B | 3,3 B | 8 av 128 |
| DeepSeek-V3 | 671 B | 37 B | 8 av 256 + 1 delad |
Minnet bestäms av totalen: alla experter måste ligga i GPU-minnet. Beräkningen per token bestäms av de aktiva.
Routern
Växeln som väljer experter är en liten inlärd tabell. Den ger varje expert ett betyg, och de bästa får jobbet.
Routern är bara en liten linjär matris, tränad tillsammans med resten. Ingen säger åt den vad experterna ska kunna.
I praktiken blir specialiseringen mest syntaktisk, som indentering, interpunktion och vissa funktionsord, snarare än ämnen som ”biologi”. En load balancing-loss under träningen ser till att experterna används ungefär lika mycket över en batch.
Varje lager har egna experter och egen router. En token tar en egen väg genom alla lager.
Klicka på en token (routervärdena är påhittade):
MoE i inferens
Alla experter måste ligga i minnet, även om bara några används åt gången. Det ger särskilda utmaningar när man kör modellen.
Små batchar: varje token läser bara sina experters vikter. Decode blir snabb, nästan som en modell i storleken av de aktiva parametrarna.
Stora batchar: olika tokens väljer olika experter, och snart används alla. Då läses alla vikter igen, och fördelen ligger i beräkningen, inte minnet.
Expert parallelism: experterna fördelas över GPU:er. Varje token skickas till rätt GPU (all-to-all dispatch) och resultaten skickas tillbaka (combine). Det är mycket kommunikation, så snabba länkar mellan GPU:erna är viktiga.
Obalans: om många tokens väljer samma expert blir en GPU flaskhals medan de andra väntar.
Attention-varianter och cachens storlek
Det finns flera sätt att bygga attention som sparar minne. Skillnaden syns framför allt i hur stor cachen blir.
- MHA: ett KV head per query head. Störst cache.
- GQA: grupper av query heads delar KV (Llama 3, Mistral, Qwen).
- MQA: ett enda KV head för alla.
- MLA (DeepSeek-V2/V3): K och V komprimeras till en latent vektor som cachas, och packas upp vid behov.
- Sliding window: vissa lager ser bara de senaste W tokens, så deras cache slutar växa. Gemma 2 och 3 varvar lokala lager med globala.
Jämförelsen till höger använder 32 lager och d = 4096 för alla, för att isolera effekten av själva attention-varianten.
Attention sinks och lokal attention
Full attention tittar på alla tidigare positioner. Det gör prefill kvadratisk i T och låter KV-cachen växa linjärt med kontexten — 128 KiB per token i Llama-3-8B.
Många nyare modeller blandar i stället: de flesta lager har ett fönster, några få är globala. Kvaliteten blir nästan densamma till en bråkdel av cachen.
Räkna på det: vid 32 000 tokens kostar Llama-3 cirka 4 GiB per sekvens, medan en modell med 4 globala lager och fönster 4 096 klarar sig på ungefär 0,9 GiB — och prefill slipper räkna 32 000² positioner i 28 av lagren.
Tre varianter man möter i praktiken:
- Lokalt fönster: varje token ser bara de senaste w tokens (Mistral, 4 096).
- Blandat lokal/global: de flesta lager lokala, några få globala (Gemma 2 och 3, gpt-oss).
- Attention sinks: de första tokens behålls alltid i cachen, eftersom modellen använder dem som avlastningsplats för uppmärksamhet (StreamingLLM).
Siffrorna i resten av kursen gäller Llama-3, som är helt global. Möter du en hybridmodell får du räkna KV-cachen lager för lager — formeln är densamma, men antalet positioner skiljer.
Lång kontext
Ju längre text modellen ska hålla i huvudet, desto mer minne och räknetid går åt.
Två saker växer med kontexten:
- KV-cachen växer linjärt. 128 000 tokens i Llama-3-8B är 16 GB, lika mycket som vikterna.
- Attention-beräkningen per token växer linjärt, och för hela prefill kvadratiskt.
Grafen visar hur stor andel av beräkningen per genererad token som går till attention mot kontexten. Vid korta prompts är det försumbart. Vid 128K dominerar det.
För att modellen ska fungera på längre kontext än den tränats på skalas RoPE-frekvenserna om (position interpolation, YaRN). Llama 3.1 gick från 8K till 128K på det sättet, kombinerat med fortsatt träning.
Resonerande modeller
Resonerande modeller är vanliga modeller som lärt sig att tänka högt innan de svarar. Det kostar många fler skrivsteg.
Arkitekturen är densamma. Skillnaden är att modellen tränats med RL att skriva ut ett långt resonemang innan svaret, ofta inom taggar som <think>…</think>.
För inference betyder det:
- Nästan all tid går till decode, som är den dyraste fasen per token.
- KV-cachen växer med resonemanget, så färre sekvenser ryms samtidigt.
- max_tokens måste räcka för både tänkande och svar.
- vLLM kan dela upp utdata i resonemang och svar med --reasoning-parser.
Det är också här loopar gör ont: en modell som upprepar sig bränner decode-steg tills taket nås.
Multimodala modeller
Bilder kan göras om till samma sorts vektorer som ord. Sedan läser modellen dem som om de vore text.
En bild delas i rutor (patches), körs genom en vision encoder (oftast en ViT) och projiceras till samma bredd d som språkmodellens residual stream. Resultatet stoppas in i sekvensen där en platshållartoken stod.
Därefter är bildtokens vanliga positioner i strömmen: de får K och V i cachen och kan attendas till som vilket ord som helst.
Exempel: LLaVA-1.5 delar en bild på 336 × 336 pixlar i rutor om 14 × 14, vilket ger 576 bildtokens. Bilder är alltså dyra i både prefill och KV-minne.
vLLM kör encodern separat och cachar dess utdata så att samma bild inte kodas två gånger.
Andra arkitekturer
Det finns släktingar till vår modell som är byggda för andra uppgifter. Här är de vanligaste.
Decoder-only (GPT, Llama): causal mask, genererar text. Den här kursens fokus.
Encoder-only (BERT): varje token ser alla andra. Ger bra vektorer för klassificering och embeddings men genererar inte.
Encoder-decoder (T5, Whisper): en encoder läser indata, en decoder genererar och läser encodern via cross-attention.
SSM och hybrider (Mamba, Jamba): ett tillstånd av fast storlek ersätter den växande KV-cachen. Linjär tid i sekvenslängden. Hybrider blandar in några attention-lager för att inte tappa precision.
Sammanfattning av modul 8
Det viktigaste: Modellvarianter
- MoE: en router väljer top-k experter per token och lager. Minnet bestäms av alla experter, beräkningen av de aktiva.
- Färre KV heads (GQA, MQA) eller en latent cache (MLA) ger mindre KV-cache och därmed fler användare per GPU.
- Lång kontext: KV-cachen växer linjärt, attention i prefill kvadratiskt.
- Resonerande modeller har samma arkitektur men gör många fler decode-steg.
- En bild blir hundratals vanliga positioner i residual stream.
Kan du förklara varje punkt med egna ord för någon annan? Då är du redo för quizet på nästa slide.
Testa dig själv: modellvarianter
Räkna och resonera
Mixtral 8x7B har 46,7 B parametrar. Hur mycket GPU-minne behöver vikterna i bf16, och hur mycket räknar varje token?
Ungefär 93 GB för vikterna, eftersom alla experter måste finnas i minnet. Beräkningen per token motsvarar en tät modell på cirka 13 B.
Varför ger MoE mindre fördel vid stora batchar än vid små?
Vid små batchar läses bara de valda experternas vikter. Vid stora batchar väljer tokens tillsammans nästan alla experter, så alla vikter läses ändå.
Vilken attention-variant ger minst KV-cache: GQA med 8 KV heads eller MLA?
MLA: 576 tal per token och lager, mot 2 · 8 · 128 = 2048 för GQA-8 med d_h 128.
Vad kostar en bild i en LLaVA-1.5-liknande modell, räknat i tokens?
576 positioner i sekvensen, med lika mycket KV-cache som 576 texttokens.
Modul 9 av 11, cirka 50 minuter
vLLM inifrån
Motorn runt modellen: hur vLLM tar emot requests, delar på GPU-minnet och fyller varje steg med så mycket nyttigt arbete som möjligt.
När du är klar kan du
- beskriva vägen från HTTP-request till strömmad token
- förklara PagedAttention, prefix caching och continuous batching
- läsa startloggen och /metrics
- välja rätt flagga för ett symptom
vLLM i klartext
Modellen själv kan bara räkna ut nästa ord för en text. vLLM är programmet runt modellen som tar emot frågor från många användare, delar upp grafikkortets minne mellan dem och ser till att kortet alltid har så mycket nyttigt arbete som möjligt. Tänk på en restaurang med ett enda, mycket snabbt kök.
| I restaurangen | I vLLM | Mer om det |
|---|---|---|
| Gästerna och deras beställningar | Användare och deras frågor (requests) | livscykel |
| Hovmästaren som bestämmer vem som får bord | Schedulern | livscykel, batching |
| Borden, uppdelade i små platser | KV-minnet, uppdelat i block om 16 tokens | PagedAttention |
| Kocken som lagar en tugga åt alla bord i taget | Grafikkortet som kör ett steg för hela batchen | model runner |
| Ett nytt bord dukas så fort någon går | Continuous batching | continuous batching |
| Sällskap med samma förrätt delar på den | Prefix caching | prefix caching |
| En jättebeställning lagas i omgångar | Chunked prefill | chunked prefill |
| Fler kök som delar på jobbet | Parallellism över flera grafikkort | tensor parallelism |
Tre saker vLLM gör bättre än en enkel lösning
- Inget minne står reserverat i onödan. Varje konversation får nya minnesblock först när den behöver dem.
- Kortet står aldrig och väntar på att en grupp ska bli klar. Varje steg fylls med de frågor som finns just då.
- Samma arbete görs inte två gånger. Gemensamma början av frågor räknas en gång och delas.
Allt det här finns för att grafikkortet, som vi såg, mest väntar på minnet. Ju fler användare som delar på varje viktläsning, desto mer svar per sekund.
Liknelsen haltar på en punkt: kocken lagar inte en hel rätt åt gången utan exakt en tugga per bord och varv. Det motsvarar en token per konversation och steg.
vLLM: arkitekturen
vLLM delar upp arbetet i en frontend som pratar HTTP och text, och en motor som bara ser token-ids och GPU-minne.
Motorn arbetar i steg: varje steg väljer schedulern en batch, workers kör ett forward pass, och varje aktiv request får (oftast) en ny token. I koden heter de tre delarna schedule(), execute_model() och update_from_output(). Sedan börjar nästa steg. Allt annat i den här modulen handlar om hur stegen fylls.
En requests livscykel
Varje fråga går igenom några tillstånd: den väntar, körs, blir ibland utkastad, och blir till sist klar.
- Waiting: tokeniserad, väntar på plats. Schedulern kollar först prefix-cachen och därefter om det finns lediga KV-block.
- Running: får tokens i varje steg. Först prefill, kanske uppdelad i chunks, sedan en token per steg.
- Preempted: om KV-blocken tar slut kastas en request ut och dess block frigörs. Den läggs tillbaka i kön och räknas om senare.
- Finished: stopptoken, stoppsträng, max_tokens eller avbruten. Blocken frigörs men ligger kvar i prefix-cachen tills de behövs till annat.
Problemet: fragmenterat minne
Om varje konversation får reservera mycket minne i förväg står det mesta tomt. Det var problemet vLLM byggdes för att lösa.
Före vLLM reserverade servrar ofta ett sammanhängande minnesområde per request, stort nog för max längd. Men ingen vet i förväg hur långt svaret blir.
Resultatet var att det mesta av KV-minnet var reserverat men tomt. Färre requests fick plats, batcharna blev små och GPU:n gick på tomgång.
PagedAttention-pappret (2023) mätte att befintliga system bara använde 20–40 % av KV-minnet till faktiska tokens.
PagedAttention: KV-cache i block
vLLM delar minnet i små lådor om 16 tokens och delar ut en låda i taget, precis när den behövs. Ingen plats står reserverad i onödan.
vLLM delar KV-minnet i block om ett fast antal tokens (16 som standard). Varje sekvens har en block table som översätter logiska block till fysiska, precis som virtuellt minne i ett operativsystem.
- Ett nytt block tilldelas först när det förra är fullt. Spillet blir högst ett halvfullt block per sekvens.
- Blocken behöver inte ligga intill varandra. Attention-kerneln slår upp dem via block table.
- Block kan delas mellan sekvenser: grunden för prefix caching.
Blockstorlek 4 här för att det ska synas. vLLM använder 16.
Prefix caching
Om två frågor börjar likadant, till exempel med samma instruktion, kan de dela på samma sparade lådor.
Varje fullt block får en hash av sitt innehåll och föregående blocks hash. Två requests med samma början får därför samma hashkedja.
När en ny request kommer slår schedulern upp hasharna i tur och ordning. Träffade block återanvänds med en referensräknare, och bara resten behöver prefill.
Systemprompter, few-shot-exempel och långa konversationer, där varje ny tur börjar med hela historiken, blir därmed nästan gratis att prefilla igen.
Bara fulla block cachas. Avslutade requests block stannar kvar i cachen och evictas (LRU) först när minnet behövs.
Continuous batching
Så fort en konversation är klar tar nästa i kön dess plats, i stället för att vänta på att hela gruppen ska bli klar.
Statisk batching: en batch körs tills den längsta sekvensen är klar. Platser för sekvenser som blivit klara står tomma.
Continuous batching: schedulern bestämmer batchen på nytt i varje steg. En plats som blir ledig fylls direkt av nästa request i kön.
Eftersom decode är memory-bound kostar ett steg nästan lika mycket oavsett hur många platser som används. Varje tom plats är därför bortkastad kapacitet.
Förenklat: 4 platser, prefill räknas som ett steg, siffran i rutan är requestens nummer.
Chunked prefill och token-budget
En väldigt lång fråga delas upp i bitar, så att den inte blockerar alla andra som väntar på sitt nästa ord.
Varje steg har en budget: max_num_batched_tokens. Schedulern fyller den i ordning:
- En token till varje request i decode.
- Återstående budget till prefill, uppdelad i chunks om prompten är för lång.
En lång prompt på 5 000 tokens tar därför tre steg med max_num_batched_tokens = 2048 (2048 + 2048 + 904) i stället för att blockera allt i ett. Decode-requests får sin token i varje steg, och ITL hålls jämn.
Prefill och decode i samma steg använder också GPU:n bättre: prefill-tokens ger räknearbete åt ett steg som annars mest läser minne.
Disaggregerad prefill och decode
Prefill och decode har olika flaskhalsar: prefill är compute-bound (när prompten är stor nog), decode är memory-bound. Körs båda på samma GPU:er stör en lång prompt alla som väntar på nästa token — ITL spikar precis när en ny request kommer in.
Disaggregering delar upp dem: några GPU:er kör bara prefill, andra bara decode, och KV-cachen flyttas mellan dem. Decode-poolen får då jämn takt, oavsett hur prompterna ser ut.
Flytten kostar tid och bandbredd. Det lönar sig bara med snabb interconnect (NVLink eller RDMA) och när ITL-kraven är hårda. I vLLM slås det på med --kv-transfer-config; det är en funktion för stora installationer, inte för en enskild GPU.
Vad man vinner och betalar:
| Del | Kör | Effekt |
|---|---|---|
| Prefill-pool | compute-bound | kan skalas med snabbare GPU:er |
| Decode-pool | memory-bound | jämn ITL, oberoende av promptlängd |
| KV-flytt | nätverk | kräver NVLink eller RDMA |
Mellanlagret kostar också: en request väntar på att hela KV-cachen är framme innan decode börjar. Är kontexten lång och länken långsam försvinner vinsten.
Ett steg i model runner
vLLM paddar inte batchen till [B, T]. Alla tokens från alla requests plattas ut till en enda lång rad, och metadata talar om var varje request börjar.
Matmuls mot vikterna ser bara en tensor [N_tokens, 4096]. Det spelar ingen roll vilken request en rad kommer från. Bara attention behöver veta, och läser det ur query_start_loc, seq_lens och block tables.
CUDA graphs: ett decode-steg består av hundratals små kernel-anrop. vLLM spelar in dem som grafer för ett antal fasta batchstorlekar och spelar upp dem, vilket tar bort det mesta av CPU-overheaden. --enforce-eager stänger av det.
torch.compile slår ihop små operationer (som RMSNorm och residual-additionen) till färre kernels.
Samplern och structured output
Sist i varje steg väljs ett ord per konversation, med just den konversationens inställningar.
Alla requests i batchen samplas samtidigt. Varje requests parametrar (temperature, top_p, seed) ligger som tensorer med shape [num_reqs], så samma kernels hanterar olika inställningar per rad.
Penalties sänker logits för tokens som redan förekommit (repetition, presence, frequency).
Structured output (JSON schema, regex, grammatik): en tillståndsmaskin vet vilka tokens som är giltiga härnäst. Alla andra får logit −∞ innan sampling. Modellen kan då inte producera ogiltig JSON, oavsett vad den ”vill”. Men kapar max_tokens genereringen mitt i ett objekt får du ändå ofullständig JSON: villkoret gäller per token, inte för längden.
Masken är en bitmask över hela vokabulären, [num_reqs, 128256], och räknas fram på CPU parallellt med forward pass.
Tensor parallelism
När en modell är för stor för ett grafikkort delas varje tabell mellan flera kort. De räknar var sin del och stämmer sedan av med varandra.
När modellen inte får plats på en GPU, eller ska gå snabbare, delas varje viktmatris mellan GPU:erna.
- Column parallel (qkv, gate_up): varje GPU får en del av utdatakolumnerna. Attention delas per head: med TP = 2 får varje GPU 16 query heads och 4 KV heads.
- Row parallel (o_proj, down): varje GPU har en del av indata och räknar en delsumma. En all-reduce summerar delsummorna.
Två all-reduce per lager, alltså 64 per forward pass i Llama-3-8B. Det kräver snabba länkar som NVLink, och TP används därför främst inom en server.
Varje GPU håller bara sina heads KV-cache. TP = 2 ger alltså dubbelt så mycket totalt KV-minne.
Pipeline, data och expert parallelism
Det finns fler sätt att dela upp arbetet på flera grafikkort: efter våning, efter kopia eller efter expert.
Pipeline parallel: lager 0–15 på en GPU, 16–31 på nästa. Lite kommunikation (bara strömmen mellan stegen), passar mellan servrar. Kräver flera batchar i flykten för att alla steg ska ha arbete.
Data parallel: hela modellen kopieras, varje kopia tar egna requests. Bäst för genomströmning när modellen får plats.
Expert parallel: för MoE. Experterna fördelas, tokens skickas dit de ska med all-to-all.
Tumregel: TP inom en server, PP mellan servrar, DP för fler användare, EP för stora MoE-modeller. Stora installationer kombinerar dem.
Speculative decoding
En liten snabb modell gissar några ord framåt, och den stora modellen kontrollerar alla gissningar på en gång.
Decode är memory-bound: att verifiera 5 tokens i ett forward pass kostar nästan lika lite som att generera 1. Speculative decoding utnyttjar det.
- Ett billigt förslag ger k tokens: en liten draft-modell, n-gram från prompten, EAGLE-huvuden eller modellens egna MTP-lager.
- Stora modellen kör alla k positioner i ett pass, som en mini-prefill.
- Förslag accepteras från vänster tills första avvikelsen. Där används stora modellens egen token vid greedy; med sampling dras ersättaren i stället ur residualfördelningen normalize(max(0, p − q)), och just det steget ger exakt stora modellens fördelning.
Med sampling används rejection sampling: acceptera med sannolikhet min(1, p/q). Utfallet har då exakt samma fördelning som stora modellen ensam.
Multi-LoRA: många modeller på en GPU
En LoRA-adapter är två små matriser per vikt: B [d, r] och A [r, d] med rang r, typiskt 8–64. Basmodellen ligger orörd och delas av alla adaptrar.
vLLM kör många adaptrar mot samma basmodell samtidigt. Requests med olika adaptrar hamnar i samma batch, och kernels applicerar rätt adapter per request — en GPU kan alltså betjäna många kunder som var och en har sin egen finjusterade variant.
Priset är litet extra arbete per token när många adaptrar blandas i samma batch, och att kvaliteten aldrig blir bättre än basmodellens. Adaptern ändrar beteende och stil, inte kunskaper.
Så startar man det:
vllm serve meta-llama/Llama-3.1-8B-Instruct \
--enable-lora \
--max-loras 16 \
--lora-modules kod=./adapter-kod \
skriv=./adapter-skriv
Varje request namnger sin adapter i model-fältet (till exempel "kod"). Servern håller bara --max-loras adaptrar aktiva åt gången och lastar in fler vid behov.
LoRA-adaptern adderas till den frysta vikten; inga shapes ändras, så en adapter kan flyttas mellan maskiner och slås ihop med basmodellen om man vill ha en egen fil.
Flaggorna och vad de styr
vLLM styrs med några inställningar. Varje inställning motsvarar ett begrepp du redan har lärt dig.
| Flagga | Koppling till kursen |
|---|---|
| --gpu-memory-utilization 0.9 | Andel av GPU-minnet vLLM tar. Efter vikter och aktiveringar blir resten KV-block. |
| --max-model-len | Max kontext per sekvens. Minst en sekvens av den längden måste få plats i KV-minnet, annars startar inte servern. |
| --max-num-seqs | Max antal requests i ett steg: batchstorleken i decode. |
| --max-num-batched-tokens | Token-budget per steg. Styr balansen mellan TTFT och ITL via chunked prefill. Högre värde reserverar mer minne för aktiveringar och ger mindre KV-pool. |
| --tensor-parallel-size | Antal GPU:er som delar varje lager. En TP som inte delar antalet heads avvisas vid start; är TP större än antalet KV heads replikeras KV-cachen i stället. |
| --pipeline-parallel-size | Antal steg som lagren delas i. |
| --kv-cache-dtype fp8 | Halverar KV-cachen. |
| --quantization | awq, gptq, fp8 med flera. Läses ofta automatiskt från modellens config. |
| --enable-prefix-caching | Prefix caching. På som standard i nyare versioner. |
| --block-size 16 | Tokens per KV-block. |
| --speculative-config | Draft-modell, n-gram, EAGLE eller MTP, och antal förslag k. |
| --enforce-eager | Stänger av CUDA graphs. För felsökning, kostar prestanda. |
Vid start loggar vLLM hur många tokens KV-cachen rymmer och hur många requests av max längd som kan köras samtidigt. Det är kalkylatorn från modul 6, räknad på riktigt.
Starta servern och läs loggen
Så här startar du vLLM och läser vad den säger om minnet.
vllm serve meta-llama/Llama-3.1-8B-Instruct \ --max-model-len 8192 \ --gpu-memory-utilization 0.90
Alla siffror i kursen är räknade för Llama-3-8B. Servern ovan startar Llama-3.1-8B-Instruct: samma shapes och samma antal lager, men 3.1 har RoPE-skalning och 128 000 tokens kontext, vilket modul 7 handlar om.
Bland startloggarna finns tre rader som sammanfattar modul 6:
Available KV cache memory: 47.50 GiB GPU KV cache size: 389,120 tokens Maximum concurrency for 8,192 tokens per request: 47.50x
Siffrorna är ett räkneexempel för en GPU med 80 GB. Dina värden beror på GPU, modell, version och flaggor.
- Available KV cache memory: det som blev över efter vikter, aktiveringar och overhead. Hela den ytan görs om till block.
- GPU KV cache size: samma yta i tokens. Här 47,5 GiB ÷ 128 KiB per token.
- Maximum concurrency: poolen delat med max-model-len. Så många sekvenser ryms om alla är maximalt långa. Är de kortare ryms fler.
Skicka sedan en request. API:t följer OpenAI-formatet:
curl localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model": "meta-llama/Llama-3.1-8B-Instruct",
"messages": [{"role": "user",
"content": "Vad är en KV-cache?"}],
"max_tokens": 200, "stream": true}'
Om loggen säger att max-model-len inte får plats i KV-cachen: sänk max-model-len, höj gpu-memory-utilization eller kvantisera.
Mät med /metrics
Servern exponerar Prometheus-mått på /metrics. De viktigaste motsvarar direkt begreppen i kursen.
| Mått | Vad det visar | Begrepp |
|---|---|---|
| vllm:time_to_first_token_seconds | Tid till första token | TTFT = kö + prefill |
| vllm:inter_token_latency_seconds | Tid mellan tokens | ITL ≈ ett decode-steg |
| vllm:request_queue_time_seconds | Tid i waiting-kön | Schedulern hade inte plats |
| vllm:request_prefill_time_seconds | Tid i prefill | Promptlängd, chunked prefill |
| vllm:request_decode_time_seconds | Tid i decode | Antal output-tokens × ITL |
| vllm:num_requests_running | Requests i batchen just nu | Batchstorlek |
| vllm:num_requests_waiting | Requests i kö | Överlast |
| vllm:kv_cache_usage_perc | Andel av KV-poolen som används (1 = full) | PagedAttention-block |
| vllm:prefix_cache_queries_total / _hits_total | Efterfrågade respektive träffade tokens | Prefix caching |
| vllm:num_preemptions_total | Antal utkastade requests | KV-poolen tog slut |
| vllm:iteration_tokens_total | Tokens per motorsteg | Token-budget |
Måttens namn har ändrats mellan versioner. Kontrollera mot dokumentationen för den version du kör.
Felsökning: symptom och rattar
Vanliga problem, deras troliga orsak och vad du kan ändra.
| Symptom | Trolig orsak | Prova |
|---|---|---|
| Servern startar inte: max-model-len får inte plats | KV-poolen rymmer inte en enda sekvens av max längd | Sänk --max-model-len, kvantisera, fp8 KV-cache eller fler GPU:er |
| Hög TTFT, kön växer | Fler requests än batchen eller KV-poolen rymmer | Fler repliker, kortare kontext, höj --max-num-seqs om KV-minne finns |
| Hög TTFT trots tom kö | Långa prompter prefillas om varje gång | Lägg det gemensamma först i prompten så att prefix-cachen träffar |
| Låg träffgrad i prefix-cachen | Något varierar tidigt i prompten, till exempel tid eller användarnamn | Flytta det som varierar till slutet |
| ITL ökar när fler ansluter | Större batch och mer KV att läsa per steg | Sänk --max-num-seqs eller skala ut |
| ITL hackar när långa prompter kommer in | Prefill tar för stor del av steget | Sänk --max-num-batched-tokens |
| Preemptions | KV-poolen blir full under körning | Sänk --max-num-seqs, fp8 KV-cache, mer minne |
| Låg genomströmning, GPU:n ser ledig ut | För få sekvenser per steg: decode är memory-bound | Mer samtidig last per GPU, speculative decoding |
| Svaren tar slut mitt i en mening | max_tokens eller max-model-len nådd, ofta på grund av långt resonemang | Höj max_tokens, kontrollera finish_reason |
Hela flödet, från HTTP till token
Hela resan för en fråga, från att den skickas till att svaret strömmar tillbaka.
Sammanfattning av modul 9
Det viktigaste: vLLM inifrån
- Frontend (HTTP, text) och EngineCore (ids, GPU) är skilda processer. Motorn går i steg: schedule, execute, update.
- KV-minnet är en pool av block om 16 tokens. En block table per sekvens översätter logiska block till fysiska.
- Prefix caching: en hashkedja över fulla block gör att gemensamma början av prompter delas.
- Continuous batching och chunked prefill: batchen bestäms om i varje steg inom en token-budget.
- Batchen plattas ut till [N_tokens, 4096]. Logits räknas bara för sista token i varje request.
- Tensor parallelism delar varje matris (två all-reduce per lager). Speculative decoding verifierar flera tokens per pass.
Kan du förklara varje punkt med egna ord för någon annan? Då är du redo för quizet på nästa slide.
Testa dig själv: vLLM
Räkna och resonera
En request har 70 tokens i kontexten. Hur många KV-block använder den med blockstorlek 16, och hur många platser står tomma?
5 block (80 platser), varav 10 tomma i det sista blocket.
Två användare skickar samma systemprompt på 1 000 tokens. Hur mycket av den andra requestens systemprompt behöver prefillas?
Bara de sista 8 tokens: 62 fulla block (992 tokens) återanvänds via prefix-cachen, och det ofullständiga sista blocket räknas om.
Varför plattar vLLM ut batchen i stället för att padda till [B, T]?
Prompter och decode-steg har helt olika längd. Padding skulle räkna på tomma positioner. Matmuls bryr sig inte om var en rad kommer ifrån, och attention-kerneln får gränserna via query_start_loc.
Varför fungerar TP = 16 dåligt för Llama-3-8B?
Modellen har bara 8 KV heads, så de kan inte delas på 16 GPU:er utan att dupliceras. Dessutom blir varje GPU:s matriser så små att all-reduce-kommunikationen dominerar.
Vad händer när KV-blocken tar slut mitt i en generering?
Schedulern preemptar en request, oftast den senast tillkomna. Dess block frigörs och den läggs tillbaka i kön för att räknas om senare.
Modul 10 av 11, cirka 10 minuter
Tolkning
Hur kan man hitta ett beteende inne i modellen och ändra det — och hur ser ingreppet ut i tensorer?
När du är klar kan du
- förklara vad en riktning i residual stream är
- skilja på steering, ablation och ändring av vikter
- beskriva ingreppet i termer av tensorer
Riktningar i residual stream
Inne i anteckningsblocket verkar begrepp ligga som riktningar. Hittar man riktningen för ett begrepp kan man mäta hur mycket av det som finns.
En vanlig och väl underbyggd hypotes är att modellen representerar begrepp som riktningar i residual stream: ”det här är kod”, ”det här är ett nekande”, ”nu upprepar jag mig”.
Man hittar en sådan riktning genom att köra två grupper av exempel, med och utan egenskapen, och ta skillnaden mellan medelvärdena av aktiveringarna i ett lager:
En linear probe är samma idé tränad som en klassificerare: om en linjär gräns skiljer grupperna åt finns informationen linjärt i strömmen.
2D-projektion av ett 4096-dimensionellt rum. Punkterna är påhittade.
Steering och ablation
När man hittat en riktning kan man förstärka eller ta bort den medan modellen körs, och då ändras modellens beteende.
När riktningen är hittad kan man ingripa i strömmen under körning, med en hook i valda lager:
Steering knuffar modellen mot eller bort från begreppet.
Ablation tar bort komponenten längs r̂ helt. Allt annat i vektorn lämnas orört, men just den signalen kan inte läsas av senare lager.
Dra i reglagen och se vad som händer med x och dess projektion på r̂.
Baka in ablation i vikterna
Ablation via hook kräver extra kod vid inference. Men samma effekt kan bakas in i vikterna. Alla matriser som skriver till strömmen, alltså o_proj och down i varje lager (och embedding), kan projiceras så att de aldrig kan skriva längs r̂:
Med vår konvention [d_in, d_out] multipliceras projektionen in från höger, på utdatasidan. Shapes ändras inte, så den modifierade modellen kan köras i vLLM som vilken modell som helst.
Ett typiskt tillvägagångssätt ser ut så här: jämför aktiveringar på svar som loopar och svar som inte gör det, hitta en riktning som växer innan upprepningen börjar, och undertryck den i de lager där den är starkast.
Metoden är etablerad och känd från arbetet med att ta bort en modells ”refusal direction”. Ingen ny arkitektur behövs — bara en projektion in i de vikter som redan finns.
Du kan nu läsa en sådan beskrivning och se exakt vilka tensorer som rörs: en vektor r̂ [4096], några [·, 4096]-matriser per lager, och inget annat.
Sammanfattning av modul 10
Det viktigaste: Tolkning
- Begrepp representeras ungefär som riktningar i residual stream och hittas som skillnaden mellan två gruppers medelaktivering.
- x · r̂ mäter, x + α r̂ styr, x − (x · r̂) r̂ tar bort.
- Samma ingrepp kan bakas in i de matriser som skriver till strömmen, utan att någon shape ändras.
Kan du förklara varje punkt med egna ord för någon annan? Då är du redo för quizet på nästa slide.
Testa dig själv: tolkning
Modul 11 av 11, cirka 20 minuter
Repetition
Knyt ihop allt: vanliga missförstånd, en fusklapp med alla formler och ett slutprov.
När du är klar kan du
- reda ut de vanligaste missförstånden
- hitta varje formel och siffra på ett ställe
- klara slutprovet
Vanliga missförstånd
Åtta påståenden som låter rimliga. Fundera på vad som är fel innan du öppnar.
”En fråga aktiverar bara en del av vikterna.”
I en tät modell används alla vikter för varje token. Det som varierar är aktiveringarna. Bara i MoE väljs delar av vikterna, och då per token och lager, inte per fråga.
”Modellen minns vår konversation.”
Vikterna ändras aldrig av att du chattar. Hela konversationen skickas in på nytt varje gång. KV-cache och prefix caching gör bara att det inte behöver räknas om.
”KV-cachen är en approximation som byter kvalitet mot fart.”
Den är exakt. Causal mask garanterar att gamla positioners K och V inte påverkas av nya tokens.
”Temperature 0 ger alltid samma svar.”
Greedy tar bort slumpen i samplingen, men flyttalsordningen kan skilja mellan batchar och vända en jämn argmax.
”Experterna i en MoE-modell är ämnesexperter.”
Uppdelningen lärs in och blir mest syntaktisk. Ingen expert är ”biologi”. Varje lager har dessutom sina egna experter.
”En resonerande modell har en annan arkitektur.”
Samma transformer. Den är tränad att skriva ut ett resonemang först, vilket kostar fler decode-steg.
”En större batch gör varje steg proportionellt långsammare.”
Decode är memory-bound. Vikterna läses en gång per steg oavsett batch, så genomströmningen ökar nästan linjärt en bra bit.
”nvidia-smi visar 90 % minne, servern är nästan full.”
vLLM allokerar hela KV-poolen vid start. Titta på vllm:kv_cache_usage_perc för att se hur mycket som faktiskt används.
Fusklapp: formler, shapes och siffror
Allt viktigt på ett ställe, att slå upp när du behöver.
| Steg | Formel | Shape (Llama-3-8B) |
|---|---|---|
| Embedding | x = E[ids] | [T, 4096] |
| RMSNorm | x / rms(x) ⊙ g | [T, 4096] |
| Q | h @ Wq | [32, T, 128] |
| K, V | h @ Wk, h @ Wv | [8, T, 128] |
| Scores | Q Kᵀ / √128 + mask | [32, T, ctx] |
| Attention ut | softmax(S) V @ Wo | [T, 4096] |
| MLP | (SiLU(h@Wg) ⊙ h@Wu) @ Wd | [T, 14336] → [T, 4096] |
| Residual | x = x + bidrag | [T, 4096] |
| Logits | RMSNorm(x[-1]) @ W_U | [128256] |
| Sampling | softmax(z / temp), top-k, top-p | ett id |
| KV per token | 2 · L · n_kv · d_h · byte | 128 KiB |
| Slot | table[pos // 16] · 16 + pos % 16 | heltal |
| Ablation | x − (x · r̂) r̂ | [4096] |
| Siffror att minnas (Llama-3-8B, bf16) | |
|---|---|
| Parametrar | 8,03 B |
| Vikter i minnet | ≈ 16 GB |
| Andel i MLP | ≈ 70 % |
| KV-cache per token | 128 KiB |
| KV-cache för 8 000 tokens | ≈ 1 GB |
| Beräkning per token | ≈ 16 GFLOP |
| Bytes lästa per decode-steg | ≈ 16 GB + KV |
| KV-block i vLLM | 16 tokens |
| All-reduce per lager vid TP | 2 |
Tre tumregler
- Prefill är compute-bound, decode är memory-bound.
- Vikterna avgör om modellen får plats. KV-cachen avgör hur många användare som får plats.
- Allt som delas av många (vikter, prefix) ska läsas eller räknas en gång. Det är vad en inference-motor gör.
Slutprov
Tolv frågor över hela kursen. Förklaringen visas direkt efter varje svar.
Vidare härifrån
Förslag på hur du går vidare: läs koden, experimentera och bygg själv.
Läs koden
- vllm/model_executor/models/llama.py: hela modellen med fused qkv och gate_up. Jämför med koden i modul 3.
- vllm/v1/engine/core.py: EngineCore och dess step(). async_llm.py i samma katalog är frontend-sidan.
- Schedulern och KV cache manager under vllm/v1/core/.
- Attention-backends under vllm/v1/attention/: där syns block tables och slot mapping.
Sökvägarna gäller vLLM:s V1-motor och kan flytta mellan versioner.
Experimentera
- Starta en liten modell, till exempel Qwen2.5-0.5B-Instruct, och läs startloggen: KV-cachens storlek i tokens och max samtidighet.
- Mät TTFT och ITL med olika --max-num-seqs och se kurvan från modul 6.
- Skicka samma långa systemprompt två gånger och jämför TTFT med och utan prefix caching.
Bygg själv
- Skriv en forward pass för en liten Llama i ren PyTorch och kontrollera varje shape mot spårningen i modul 3.
- Lägg till en KV-cache och jämför hastigheten med och utan.
- Plocka ut residual stream med en hook, beräkna en skillnadsriktning mellan två promptgrupper och testa ablation.
Sammanfattning i en mening
Tokens blir vektorer, vektorerna förfinas av 32 lager som adderar till en gemensam ström, sista vektorn blir en fördelning över nästa token, och en inference-motor handlar nästan helt om att hålla KV-cachen i minnet och läsa vikterna så få gånger som möjligt.