Worstel je met AI of full-stack ontwikkeling? Onze experts staan klaar om je te begeleiden: advies op maat, technische integratie en meer. Neem contact op met [email protected].

LLM Inferentie Optimalisatietechnieken

Inferentieoptimalisatie is een cruciaal onderdeel van generatieve AI-toepassingen die in de productie worden ingezet. Het efficiënt gebruiken van LLM's op schaal is een uitdaging en er zijn de laatste jaren veel technieken ontwikkeld om inferentie sneller en goedkoper te maken. In dit artikel bespreken we deze technieken.

LLM Inferentie Optimalisatietechnieken

Een focus op LLM's architectuur

Grote taalmodellen (LLM's) zijn allemaal gebaseerd op de transformatorarchitectuur die in 2017 werd uitgevonden door Vaswani et al. De transformatorarchitectuur bereikt een superieure nauwkeurigheid, weinig leerprocessen en bijna menselijke vaardigheden voor verschillende taaltaken. Deze basismodellen, die vaak tientallen tot honderden miljarden parameters bevatten, zijn echter duur om te trainen en vergen veel middelen tijdens de inferentie. De inferentiekosten nemen toe bij lange invoercontexten, die veel rekenkracht vergen vanwege de grote invoergegevens. Dit maakt efficiënte inferentie tot een kritieke uitdaging, vooral wat betreft het beheer van geheugen en rekenkracht.

De transformatorarchitectuur
De transformatorarchitectuur

Meer in het bijzonder zijn de meeste bekende LLM's alleen-decoder LLM's, zoals GPT-3, GPT-4, LLaMA, Mistral, DeepSeek, enz. Deze modellen zijn voorgetraind op een causale modelleringstaak en functioneren als voorspellers van volgende woorden. Ze verwerken een reeks tokens als invoer en produceren volgende tokens autoregressief totdat een stopconditie is bereikt.

LLM-inferentie in modellen met alleen een decoder omvat twee belangrijke fasen: de prefill-fase en de decodeer-fase. In de prefill-fase verwerkt het model invoertokens om tussenliggende toestanden (sleutels en waarden) te berekenen voor het genereren van het eerste nieuwe token. Deze fase, die lijkt op een matrix-matrix bewerking, is sterk geparallelliseerd en maakt efficiënt gebruik van de GPU mogelijkheden. Omgekeerd genereert de decoderingsfase één voor één tokens, vertrouwend op de toestanden van de vorige tokens. Deze matrix-vector bewerking is geheugengebonden, omdat de gegevensoverdracht naar de GPU, in plaats van de berekeningssnelheid, voornamelijk de latentie dicteert, wat leidt tot onderbenutting van de rekenkracht van de GPU.

Het optimaliseren van de decoderingsfase is een aandachtspunt bij het aanpakken van inferentie-uitdagingen. Oplossingen zijn onder andere het ontwikkelen van efficiënte aandachtsmechanismen en beter beheer van sleutels en waarden om geheugenknelpunten te verminderen. Dit artikel belicht praktische benaderingen om de inferentieprestaties te verbeteren, ervan uitgaande dat lezers een basiskennis hebben van de transformatorarchitectuur en de aandachtsmechanismen. Deze optimalisaties zijn cruciaal voor het verbeteren van de doorvoer en het verminderen van de latentie in echte LLM implementaties.

Een andere complicatie komt voort uit het gebruik van verschillende tokenizers in LLM's, wat de vergelijkbaarheid van tokens beïnvloedt. Tokens, ruwweg gelijk aan vier Engelse karakters, variëren in representatie afhankelijk van de tokenizer, waardoor directe vergelijkingen van inferentie doorvoer (bijv. tokens per seconde) misleidend zijn. Deze variabiliteit onderstreept de behoefte aan gestandaardiseerde evaluatiemetrieken om de prestaties van LLM's tijdens inferentie nauwkeurig te beoordelen en te vergelijken.

Batching

Batching is een belangrijke strategie voor het verbeteren van GPU-gebruik en doorvoer in grote taalmodellen (LLM's). Door meerdere verzoeken tegelijk te verwerken met hetzelfde model, verdeelt batching de geheugenkosten van modelgewichten over verzoeken, waardoor grotere batches meer GPU rekenkracht opleveren. Er is echter een limiet aan de grootte van batches, omdat te grote batches geheugenoverloop kunnen veroorzaken door de geheugeneisen van LLM's, met name gerelateerd aan key-value (KV) caching (waarover later meer).

Batchingtechnieken
Batchingtechnieken

Traditionele of statische batching heeft beperkingen omdat verzoeken binnen een batch vaak verschillende aantallen voltooiingstokens genereren, wat leidt tot verschillende uitvoeringstijden. Dit zorgt ervoor dat alle verzoeken moeten wachten tot de langzaamste is voltooid, wat problematisch kan zijn als de generatielengte aanzienlijk varieert. Om dit aan te pakken zijn geavanceerde technieken zoals in-flight batching ontwikkeld om de prestaties te optimaliseren.

In-flight batching, ook bekend als continuous batching, pakt de uitdagingen aan die het gevolg zijn van de dynamische aard van LLM workloads, die kunnen variëren van eenvoudige chatbotreacties tot complexe documentsamenvattingen of het genereren van code. Deze taken produceren uitvoer van enorm verschillende groottes, waardoor het moeilijk is om verzoeken in batch te verwerken en efficiënt parallel uit te voeren. In tegenstelling tot statische batching, stelt in-flight batching de server in staat om voltooide reeksen onmiddellijk uit de batch te verwijderen en te beginnen met het verwerken van nieuwe verzoeken terwijl andere nog bezig zijn. Deze aanpak verhoogt het GPU-gebruik aanzienlijk door zich aan te passen aan de variërende uitvoeringstijden van verzoeken in echte scenario's.

Inzet van meerdere GPU's met modelparallellisatie

Parallellisatie van modellen is een kritieke strategie voor het beheren van het geheugen en de rekenkundige eisen van grootschalige modellen voor machinaal leren door ze te verdelen over meerdere GPU's. Deze aanpak maakt het mogelijk om grotere modellen of invoerbatches te verwerken die de geheugencapaciteit van een enkel apparaat te boven gaan. Deze aanpak maakt het mogelijk om grotere modellen of invoerbatches te verwerken die de geheugencapaciteit van een enkel apparaat overschrijden, waardoor het essentieel is voor zowel training als inferentie wanneer de geheugenbeperkingen klein zijn. Er bestaan verschillende technieken voor het splitsen van modelgewichten, waaronder pijplijnparallellisme, tensorparallellisme en sequentieparallellisme, die elk verschillende aspecten van modeldistributie aanpakken. In tegenstelling tot dataparallellisme, dat zich richt op het repliceren van modelgewichten over apparaten om grotere invoerbatches te verwerken tijdens training, zijn deze methoden relevanter voor het verkleinen van de geheugenvoetafdruk tijdens zowel training als inferentie.

Meerdere NVIDIA GPU's
Meerdere NVIDIA GPU's

Pijplijnparallellisme verdeelt het model verticaal in opeenvolgende brokken, waarbij elk brok een subset van lagen bevat die aan een afzonderlijk apparaat zijn toegewezen. In een vier-weg pijplijnopstelling bijvoorbeeld, behandelt elk apparaat een kwart van de lagen van het model en geeft de uitvoer door aan het volgende apparaat in volgorde. Hoewel dit de geheugenvereisten per apparaat aanzienlijk vermindert, introduceert het inefficiënties die bekend staan als "pipeline bubbles", waarbij apparaten ongebruikt kunnen blijven terwijl ze wachten op uitvoer van vorige lagen. Microbatching, waarbij invoerbatches worden opgesplitst in kleinere subbatches voor sequentiële verwerking, kan deze bubbels verminderen, maar niet volledig elimineren, omdat inactieve tijden blijven bestaan tijdens voorwaartse en achterwaartse passes.

Tensorparallellisme daarentegen splitst individuele lagen horizontaal op in kleinere rekenblokken die onafhankelijk van elkaar op apparaten kunnen worden uitgevoerd. Dit is vooral effectief voor transformatorcomponenten zoals aandachtsblokken en meerlaagse perceptrons (MLP's), waar bijvoorbeeld verschillende aandachtskoppen kunnen worden toegewezen aan afzonderlijke apparaten voor parallelle berekening. Echter, tensorparallellisme is minder effectief voor bewerkingen zoals LayerNorm en Dropout, die niet eenvoudig kunnen worden opgedeeld en moeten worden gerepliceerd over apparaten, wat leidt tot overbodig geheugengebruik voor het opslaan van activeringen. Deze beperking benadrukt de behoefte aan aanvullende benaderingen om de geheugenefficiëntie te optimaliseren.

Sequentieparallellisme pakt de geheugeninefficiënties van bewerkingen zoals LayerNorm en Dropout aan door ze te partitioneren langs de dimensie van de inputsequentie en zo hun onafhankelijkheid over sequentie-elementen te benutten. Deze methode vermindert de geheugenvoetafdruk van overbodige activeringen, waardoor het een waardevolle aanvulling is op tensorparallellisme. Deze parallellisatietechnieken sluiten elkaar niet uit en kunnen worden gecombineerd om grote taalmodellen (LLM's) verder te optimaliseren. Daarnaast kunnen specifieke optimalisatiestrategieën voor de aandachtsmodule de schaalbaarheid verbeteren en het geheugengebruik per GPU verminderen, waardoor efficiëntere training en inferentie voor grote modellen mogelijk wordt.

Aandacht Optimalisatie

De 2017 paper *Attention Is All You Need* van Vaswani et al. introduceerde het Transformer model, met zelf-attentie als hoeksteen. Zelfattentie stelt het model in staat om de relevantie van verschillende woorden in een zin ten opzichte van elkaar te beoordelen, waardoor contextueel begrip voor taken zoals natuurlijke taalverwerking wordt verbeterd. Het artikel formaliseerde zelfattentie, met name door middel van het SDPA-mechanisme (Scaled Dot-product Attention), dat query- en sleutelwaardeparen koppelt aan een uitvoer, waardoor het een centrale component wordt in moderne neurale netwerken. Hier zijn enkele van de belangrijkste technieken om aandachtsberekeningen te optimaliseren:

Het aandachtspapier
Het aandachtspapier

Multi-head attention (MHA) bouwt voort op SDPA door meerdere aandachtsoperaties parallel uit te voeren, elk met verschillende projecties van query-, sleutel- en waardematrices. Deze parallelle bewerkingen, of "koppen", richten zich op verschillende representatieve deelruimten, waardoor het model de invoer beter begrijpt. De uitvoer van deze koppen wordt samengevoegd en lineair geprojecteerd, waardoor de computerefficiëntie vergelijkbaar blijft met die van aandacht met één kop door de dimensionaliteit van elke kop te verminderen (bijvoorbeeld door de dimensie van het model te delen door het aantal koppen, zoals 8).

Multi-query attention (MQA) optimaliseert MHA voor inferentie door sleutel- en waardeprojecties te delen over meerdere aandachtskoppen terwijl meerdere queryprojecties behouden blijven. Dit verlaagt de bandbreedte van het geheugen en de grootte van de KV-cache (key-value), waardoor grotere batches en een beter computergebruik mogelijk zijn. MQA kan echter de nauwkeurigheid iets verminderen en modellen die er gebruik van maken vereisen training of fijnafstelling met MQA ingeschakeld om de prestaties te behouden.

Gegroepeerde-query-aandacht (GQA) brengt MHA en MQA in evenwicht door queryhoofden te groeperen en key-value-projecties binnen elke groep te delen, waardoor een bijna-MHA-kwaliteit wordt bereikt met een rekenefficiëntie die dichter bij MQA ligt. Modellen zoals Llama 2 70B gebruiken GQA en modellen die getraind zijn met MHA kunnen met minimale extra training worden aangepast aan GQA. Zowel MQA als GQA verminderen de behoefte aan KV cachegeheugen, hoewel verdere optimalisaties in cachebeheer nodig blijven.

FlashAttention verbetert de aandachtsmechanismen door berekeningen te herschikken om GPU-geheugenhiërarchieën effectiever te gebruiken. In tegenstelling tot traditionele laag-voor-laag verwerking, voegt FlashAttention bewerkingen samen en maakt gebruik van "tiling" om kleine delen van de uitvoermatrix in één keer te berekenen, waardoor lees-/schrijfbewerkingen in het geheugen worden geminimaliseerd. Dit I/O-bewuste, exacte aandacht-algoritme integreert naadloos in bestaande modellen zonder aanpassingen en biedt aanzienlijke snelheidsverbeteringen door optimalisatie van gegevensbewegingen.

Sleutelwaarde caching

KV caching is een kritische optimalisatietechniek die wordt gebruikt tijdens de decoderingsfase van grote taalmodellen (LLM's) om de efficiëntie van zelfattentieberekeningen te verbeteren. In deze fase is elk gegenereerd token afhankelijk van de sleutel (K) en waarde (V) tensoren van alle voorgaande tokens, inclusief de tensoren die zijn berekend tijdens de prefill-fase en de daaropvolgende decoderingsstappen. In plaats van deze tensoren opnieuw te berekenen voor elk token bij elke tijdstap, slaat KV caching ze op in het GPU-geheugen, waarbij nieuwe tensoren aan de cache worden toegevoegd wanneer ze worden berekend. Gewoonlijk wordt voor elke laag van het model een aparte KV cache bijgehouden, waardoor overbodige berekeningen aanzienlijk worden verminderd en het decoderingsproces wordt versneld.

Sleutelwaarde caching
Sleutelwaarde caching

De geheugenvereisten voor LLM's op GPU's worden voornamelijk bepaald door twee componenten: modelgewichten en de KV cache. Modelgewichten, die bestaan uit de parameters van het model, nemen veel geheugen in beslag; een model met 7 miljard parameters zoals Llama 2 7B in 16-bits precisie heeft bijvoorbeeld ongeveer 14 GB nodig. De KV cache daarentegen slaat tensoren van zelfattentie op om herberekening te voorkomen. De grootte wordt bepaald door factoren zoals het aantal lagen, aandachtskoppen, afmetingen van de koppen en precisie. Voor elke token wordt de grootte van de cache berekend als 2 * num_layers * (num_heads * dim_head) * precision_in_bytes, waarbij de factor 2 rekening houdt met zowel K als V matrices. Voor een batch van ingangen schaalt de totale KV cache-grootte met de batch-grootte en sequentielengte, wat kan oplopen tot aanzienlijke afmetingen, zoals ~2 GB voor een Llama 2 7B model met een sequentielengte van 4.096 en een batch-grootte van 1.

Het efficiënt beheren van de KV cache vormt een uitdaging vanwege de lineaire groei van de cache met de grootte van de batch en de lengte van de sequentie, wat de doorvoer kan beperken en het verwerken van invoer met een lange context kan bemoeilijken. Een veel voorkomende inefficiëntie komt voort uit statische over-provisioning, waarbij geheugen wordt gereserveerd voor de maximaal ondersteunde sequentielengte (bijv. 2.048 tokens), ongeacht de werkelijke invoergrootte. Dit leidt tot aanzienlijke geheugenverspilling of fragmentatie, omdat een groot deel van de gereserveerde ruimte vaak ongebruikt blijft gedurende de levensduur van de aanvraag, waardoor waardevolle GPU-geheugenbronnen in beslag worden genomen.

Om deze inefficiënties aan te pakken, introduceert het PagedAttention-algoritme een nieuwe benadering die is geïnspireerd op paging in besturingssystemen. Het verdeelt de KV-cache in blokken van vaste grootte, die elk een vast aantal tokens vertegenwoordigen en die niet-contigu in het geheugen kunnen worden opgeslagen. Een blokkentabel houdt deze blokken bij en haalt ze op wanneer dat nodig is tijdens aandachtsberekeningen. Als er nieuwe tokens worden gegenereerd, worden er dynamisch extra blokken toegewezen. Deze methode minimaliseert geheugenverspilling door de noodzaak voor contigue toewijzing en over-provisioning te elimineren, waardoor grotere batches mogelijk worden en de doorvoer wordt verbeterd. Dit is dus een belangrijke vooruitgang in het beheer van KV cachegeheugen voor LLM's.

Modeloptimalisatie

In dit hoofdstuk bespreken we verschillende technieken voor het optimaliseren van grote taalmodellen (LLM's) om hun geheugengebruik te verminderen en de prestaties op GPU's te verbeteren. De belangrijkste methoden zijn kwantisatie, sparsiteit en distillatie, die elk gericht zijn op verschillende aspecten van modelefficiëntie. Deze technieken wijzigen modelgewichten, maken gebruik van GPU-hardwareversnelling en brengen kennis over naar kleinere modellen, waardoor grotere modellen kunnen draaien op beperkte hardware met behoud van prestaties. Deze methoden kunnen de nauwkeurigheid van het model verminderen, dus ze moeten met voorzichtigheid worden gebruikt.

Kwantificering vermindert de precisie van de gewichten en activeringen van een model, meestal van 32 of 16 bits naar 8 of minder bits, waardoor modellen minder geheugen innemen en gegevens efficiënter kunnen overdragen. Terwijl het kwantiseren van gewichten eenvoudig is vanwege hun vaste aard na training, is het kwantiseren van activeringen complexer vanwege uitschieters die hun dynamische bereik vergroten. Technieken zoals LLM.int8() pakken dit aan door selectief hogere precisie toe te passen op bepaalde activeringen, of door het dynamische bereik van gekwantiseerde gewichten opnieuw te gebruiken voor activeringen, hoewel GPU's mogelijk gewichten terug moeten converteren naar hogere precisie voor bewerkingen.

Sparsity houdt in dat modelwaarden dicht bij nul worden weggelaten, waardoor schaarse matrices ontstaan die minder geheugen nodig hebben. GPU's ondersteunen gestructureerde sparsiteit, zoals twee van elke vier waarden weergeven als nullen, wat berekeningen versnelt. Het combineren van sparsity met kwantisatie kan de uitvoeringssnelheid nog verder verhogen. Er wordt nog steeds onderzoek gedaan naar optimale sparse representaties voor LLM's, wat veelbelovend is voor het verbeteren van inferentiesnelheden.

Distillatie draagt kennis over van een groter "leraar"-model naar een kleiner "leerling"-model, waardoor de omvang wordt verkleind met behoud van prestaties. DistilBERT bereikt bijvoorbeeld een 40% kleinere omvang en 60% hogere snelheid in vergelijking met BERT, terwijl 97% van de mogelijkheden behouden blijven. Distillatie kan inhouden dat de uitvoer van de leraar wordt nagebootst of dat door de leraar gegenereerde gegevens worden gebruikt voor training, waarbij methoden als "Distilleren stap voor stap!" rationales bevatten voor efficiënt leren. Beperkende licenties op veel geavanceerde LLM's beperken echter de beschikbaarheid van geschikte docentmodellen voor distillatie.

Speculatieve gevolgtrekking

Speculatieve inferentie, ook bekend als speculatieve sampling of geassisteerde generatie, is een methode om de uitvoering van autoregressieve grote taalmodellen (LLM's) zoals GPT-stijl modellen, die meestal tekst token voor token genereren, parallel te laten verlopen. Bij standaard uitvoering is elk token voor context afhankelijk van alle voorafgaande tokens, waardoor parallel genereren onmogelijk is omdat het n-de token voor het (n+1)e moet worden gegenereerd. Speculative inference pakt dit aan door een "goedkoper" conceptmodel te gebruiken om meerdere toekomstige tokens tegelijk te voorspellen, die dan parallel worden geverifieerd of verworpen door het hoofdmodel, waardoor tekst sneller kan worden gegenereerd.

Het proces bestaat uit het genereren van een conceptvoortzetting van verschillende tokens met behulp van een minder middelenintensieve methode, gevolgd door parallelle verificatie door het hoofdmodel met behulp van het concept als speculatieve context. Als het verificatiemodel overeenkomt met de concepttokens, worden ze geaccepteerd; zo niet, dan worden niet-overeenkomende en volgende tokens weggegooid en wordt het proces herhaald met een nieuw concept. Ontwerp-tokens kunnen worden gegenereerd met verschillende benaderingen, zoals het trainen van meerdere modellen, het afstemmen van meerdere hoofden op een voorgetraind model om toekomstige tokens te voorspellen, of het gebruik van een kleiner ontwerpmodel naast een groter, meer capabel verificatiemodel, elk met zijn eigen afwegingen.

Gedesaggregeerde inferentie

Gedesaggregeerde inferentie is een techniek waarbij de rekentaken worden verdeeld over verschillende hardware om de prestaties, de kosten en het gebruik van bronnen te optimaliseren. Specifiek worden de prefilling en decoding fases gescheiden. Door deze fasen op te splitsen, kan elke fase worden toegewezen aan hardware die het meest geschikt is voor de computationele eisen, wat de efficiëntie en schaalbaarheid verbetert.

Gedesaggregeerde inferentie
Gedesaggregeerde inferentie

Voorvullen is rekenintensief en vereist aanzienlijke matrixvermenigvuldigingen om de hele invoerprompt te verwerken en KV-caches te produceren. Deze fase heeft baat bij krachtige hardware zoals GPU's of TPU's, die uitblinken in parallelle berekeningen. Omdat prefilling een eenmalige taak is per inferentieverzoek, kan het worden overgeheveld naar een gecentraliseerde, krachtige rekenknoop die is geoptimaliseerd voor dergelijke werklasten. Deze opzet maakt snellere verwerking van grote prompts mogelijk en vermindert de belasting op minder capabele apparaten, waardoor het ideaal is voor cloud-gebaseerde of datacenter-omgevingen waar high-throughput hardware beschikbaar is.

Decoderen daarentegen is geheugengebonden en omvat iteratieve tokengeneratie, waarbij veel gebruik wordt gemaakt van de KV-caches. Het vereist minder rekenkracht maar heeft snelle geheugentoegang nodig, waardoor het geschikt is voor minder krachtige, geheugengeoptimaliseerde hardware zoals CPU's of randapparaten. Door het decoderen naar aparte hardware te verplaatsen - mogelijk dichter bij de eindgebruiker, zoals servers op locatie of randapparatuur - wordt de latentie en de benodigde netwerkbandbreedte verlaagd. Deze scheiding maakt flexibele inzet mogelijk, waarbij prefilling wordt uitgevoerd op high-end cloudservers en decoding plaatsvindt op lokale of randapparaten, waardoor de toewijzing van bronnen wordt geoptimaliseerd en efficiënt geschaald kan worden voor toepassingen zoals realtime chatbots of interactieve AI-systemen.

Conclusie

Er zijn recent veel technieken uitgevonden om de prestaties van LLM's te verbeteren.

Het implementeren van deze technieken vereist een grondige kennis van de LLM-architectuur en de hardware die u gebruikt, dus het is over het algemeen gemakkelijker om een bestaande inferentie-engine te gebruiken die deze technieken al heeft geïmplementeerd, zoals vLLM, TensorRT-LLM, LMDeploy, enz. We hebben deze technieken geïmplementeerd in onze eigen inferentie-engine bij NLP Cloud en we hebben een blogpost geschreven over inferentie-engines als je je eigen modellen wilt implementeren: je kunt het hier lezen.

Als u uw eigen LLM's niet zelf kunt of wilt implementeren, kunt u NLP Cloud gebruiken en gebruikmaken van snelle generatieve AI-modellen op schaal in productie. Probeer nu snelle inferentie op NLP Cloud!

Als je vragen hebt over inferentie-engines in het algemeen, aarzel dan niet om het ons te vragen, het is altijd een genoegen om advies te geven!

Julien
CTO bij NLP Cloud