← Alla inlägg

ARKITEKTUR

Serving-lagret för lokala LLM: vLLM, SGLang, llama.cpp och Ollama

När modellen är vald är det första arkitekturbeslutet vilket serving-lager som ska köra den. Det här inlägget skiljer fyra lager åt, inte med funktionslistor utan med deras arkitektoniska angreppssätt för schemaläggning, minne, återanvändning och paketering.

När en verksamhet har valt den modell den vill köra har den en fil i handen: vikter, ett vokabulär och det promptformat modellen förväntar sig. Att göra den filen till en tjänst är en egen mjukvara. Det är det lagret som köar inkommande förfrågningar, fördelar GPU-minnet, avgör hur många användare som kan föra ett samtal samtidigt och exponerar ett API utåt.

Det här inlägget skiljer fyra namn åt — vLLM, SGLang, llama.cpp och Ollama — inte med en funktionstabell utan med deras arkitektoniska angreppssätt längs fyra axlar: schemaläggning, minne, återanvändning och paketering. Någon rangordning efter hastighet ges inte, och varje siffra nedan anges med datum och jämförelsebas från sin egen källa. Beräkningarna på modellfilens sida — kvantiseringsnivåer, VRAM-aritmetik och standardvärden för kontextfönstret — hör till modellvalet och tas inte upp här.

Vad serving-lagret gör och vad modellfilen gör

Två ansvarsområden skiljs åt. Modellfilen bär vikterna, vokabuläret och promptformatet. Serving-lagret tar hand om schemaläggning, minneshantering, samtidighet och API-gränssnittet. Uppdelningen får en konkret följd: samma modellfil kan köras på alla fyra lagren, men hur många användare som får svar samtidigt på samma hårdvara avgörs av serving-lagret och inte av filen.

I praktiken tar lagret oftast formen av en HTTP-server: serverdokumentationen för llama.cpp beskriver sitt eget verktyg som en lättviktig HTTP-server i ren C/C++ byggd på httplib och nlohmann::json, som erbjuder REST-API:er och ett webbgränssnitt. Även underlagen överlappar — den 15 maj 2025 presenterade Ollama en egen motor ovanpå tensorbiblioteket GGML för multimodala modeller och skrev i samma inlägg att man fram till dess hade stött sig på projektet llama.cpp för modellstödet.

Sätt att batcha: statiskt, dynamiskt och kontinuerligt

Serving-lagrets första beslut gäller hur förfrågningar som kommer in samtidigt ska grupperas.

  • Fast batch: förfrågningar samlas i en mängd, körs som en helhet, och nästa batch startar först när varje förfrågan i den är klar. Den benämningen är enbart beskrivande; den är inte en term från någon källa.
  • Dynamisk batchning: batchen sätts samman av servern under körning. Förfrågningar väntar en kort stund och skickas i väg när den önskade storleken har nåtts eller väntebudgeten är förbrukad.
  • Kontinuerlig batchning: innehållet i batchen bestäms om vid varje iteration. En färdig förfrågan lämnar den, och en väntande går in så snart den pågående iterationen är klar.

Den dokumenterade formen av det mellersta angreppssättet finns i NVIDIA Triton (se Triton Inference Server — Dynamic Batcher). Dynamisk batchning slår ihop enskilda förfrågningar till en enda batch vid en tidpunkt som servern själv väljer. Väntetiden begränsas av max_queue_delay_microseconds, och exempelkonfigurationen använder 100 mikrosekunder.

Den primära källan för det tredje angreppssättet är Orca (USENIX OSDI ’22, s. 521-538). Artikeln lägger fram iakttagelsen först: systemen fram till dess schemalade körningen på förfrågningsnivå och höll en fast mängd förfrågningar tills varje förfrågan i batchen var klar, så en förfrågan som blev klar tidigt kunde inte gå tillbaka till klienten, och en som kom in senare fick vänta tills den pågående batchen körts färdigt. Orcas svar är att flytta ner schemaläggningen till iterationsnivå: schemaläggaren väljer vilka förfrågningar som ska köras, anropar motorn för en enda iteration och samlar in resultaten. Det är ursprunget till det som i dag kallas continuous batching.

Artikeln definierar också en andra teknik: selektiv batchning. Nästa iteration för två förfrågningar går inte alltid att slå ihop — om båda är i initieringsfasen med olika antal indatatokens, eller om den ena är i den ena fasen och den andra i den andra, sammanfaller inte attention-tensorernas former. Batchning tillämpas därför inte på alla operationer utan på utvalda. Orca redovisar 36,9 gånger genomströmningen jämfört med NVIDIA FasterTransformer på GPT-3 175B vid samma latensnivå: en mätning från 2022, mot den tidens jämförelsebas.

Termerna dras inte lika i alla dokument. Serverdokumentationen för llama.cpp definierar flaggan --cont-batching som ”continuous batching (a.k.a dynamic batching)” och har den påslagen som standard. Den begreppsliga uppdelningen ovan hör till terminologin i vLLM och SGLang.

PagedAttention: att dela upp KV-cachen i sidor

När schemaläggningen går ner till iterationsnivå blir minneshanteringen den avgörande faktorn. Skälet är hur KV-cachen beter sig: den är stor för varje förfrågan, och den växer och krymper medan genereringen fortskrider. PagedAttention-artikeln säger det rakt ut — det beteendet bestämmer batchstorleken (se Efficient Memory Management for Large Language Model Serving with PagedAttention, arXiv:2309.06180, SOSP ’23).

Det artikeln mäter är en andel: bara 20,4 % till 38,2 % av ett i förväg reserverat sammanhängande minnesblock håller faktiska tokentillstånd, mot 96,3 % för vLLM i samma mätning. Resten går till platser som reserverats för framtida tokens och till utrymme som uppstår när reservationen görs efter den maximala sekvenslängden.

Svaret kommer från operativsystemen. PagedAttention delar upp en förfrågans KV-cache i block, där varje block håller nyckel- och värdevektorerna för ett fast antal tokens, och kräver inte att blocken ligger intill varandra i det fysiska minnet. Artikelns analogi bär avsnittet: ”one can think of blocks as pages, tokens as bytes, and requests as processes”.

Bokföringen följer av det. Varje post i blocktabellen håller ett fysiskt blocknummer och antalet fyllda platser i det blocket; en förfrågans cache är en följd av logiska block som fylls från vänster till höger, och ett nytt fysiskt block reserveras först när de föregående är fulla. Varje fysiskt block bär en referensräknare, och copy-on-write tillämpas på blocknivå: två utdata som delar samma prompt håller en enda kopia av prompttillståndet, och en skrivning reserverar ett nytt block.

Vad delandet ger är mätt: vid beam search ger blockdelning minnesbesparingar på upp till 55 %. Artikeln redovisar 2-4 gånger genomströmningen jämfört med FasterTransformer och Orca vid samma latensnivå — en mätning från 2023 mot den tidens jämförelsebaser.

RadixAttention och prefixcache: att återanvända delad kontext

Det tredje granularitetsskiftet ligger på återanvändningssidan. PagedAttention gjorde delning inom en enskild förfrågan billig; SGLang siktar på delning mellan förfrågningar (se SGLang: Efficient Execution of Structured Language Model Programs, arXiv:2312.07104).

Det som utmärker RadixAttention är enkelt: cachen kastas inte när genereringen tar slut, utan hålls kvar i ett radixträd. Ett radixträd är en utrymmeseffektiv form av prefixträdet, vars kanter kan märkas med följder av varierande längd i stället för enskilda element. Trädet mappar tokenföljder till sina KV-cachetensorer, och både prompter och genereringar cachelagras.

Det här är ett lager, inte ett alternativ. Artikeln skriver rakt ut att RadixAttention är kompatibelt med continuous batching, paged attention och tensorparallellism, och att det extra minnes- och tidspåslaget är försumbart när det inte blir någon cacheträff.

Plats frigörs enligt LRU och börjar vid löven: det löv som använts för längst sedan går först, och delade förfäder förblir återanvändbara tills de själva blir löv. Någon cachepool med fast storlek reserveras inte i förväg; cachelagrade tokens och pågående förfrågningar delar samma minnespool.

Schemaläggningen är cachemedveten den också: förfrågningar sorteras efter längden på det matchade prefixet. Teorem 3.1 säger att när cachestorleken inte är mindre än den maximala förfrågningslängden är ordningen längsta-delade-prefix-först likvärdig med en traversering av radixträdet djupet först, och ger den optimala träffprocenten.

Var det lönar sig är konkret: few-shot-prompter, chatthistorik över flera turer och den delade kontexten i en RAG-pipeline. I de här jämförelsemätningarna ligger träffprocenten mellan 50 % och 99 %, och cachemedveten schemaläggning närmar sig i genomsnitt 96 % av den optimala träffprocenten. Under en månads drift på Chatbot Arena observerades en träffprocent på 52,4 % för LLaVA-Next-34B och 74,1 % för Vicuna-33B, och den genomsnittliga tiden till första token för Vicuna-33B sjönk med en faktor 1,7 — en observation från 2024, specifik för den arbetslasten.

vLLM erbjuder samma funktion i dag; skillnaden ligger i strukturen. I vLLM är blockidentiteten hashbaserad: blockets hash bildas av föräldrablockets hash och tokens i det blocket, bara fulla block cachelagras, och ett cache-salt går in i hashen för att hålla isär cacher i multitenant-miljöer. Standardalgoritmen för hashning är sha256 sedan v0.11, och prefixcache är påslaget som standard. På SGLang-sidan byggs samma funktion av ett radixträd med LRU-utrymning av löven — samma funktion, en annan datastruktur.

GGUF och den enda binärfilen: paketering och driftmodell

Den fjärde axeln lämnar schemaläggning och minne: paketeringen. GGUF är ett filformat för att lagra modeller för inferens med GGML och GGML-baserade körmiljöer. Designmålen står uppräknade i ordning i specifikationen: leverans i en enda fil — filer kan distribueras och läsas in enkelt och kräver inga externa filer; utbyggbarhet; mmap-kompatibilitet; enkel hantering; och fullständig information — allt som behövs för att läsa in en modell finns i filen.

Det sista målet är inte abstrakt. De allmänna metadatanycklarna är definierade som general.architecture, general.quantization_version, general.file_type och general.alignment. Tokenizern följer med i filen i sin helhet: vokabulär, sammanslagningsregler, tokentyper och ID:n för specialtokens. Den mest slående nyckeln är tokenizer.chat_template — en Jinja-mall som beskriver det indataformat modellen förväntar sig, placerad i samma fil som vikterna.

I en on-premise-installation bör två olika ”enskilda filer” hållas isär. Den första är modellens enda GGUF-fil. Den andra är körmiljön själv: första punkten i funktionslistan för llama.cpp är en ren C/C++-implementation utan beroenden, och distributionsenheten är en nedladdningsbar binärfil snarare än en containerstack.

Paketeringsenheten i Ollama är Modelfile, i dokumentationen beskriven som ritningen för att skapa och dela en modell. Dess instruktioner är FROM, PARAMETER, TEMPLATE, SYSTEM, ADAPTER, LICENSE och MESSAGE; FROM tar emot ett befintligt modellnamn, en Safetensors-katalog eller sökvägen till en GGUF-fil. Kvantiseringen hänger på skapandesteget, i den dokumenterade formen ollama create --quantize q4_K_M, och modellen delas sedan med ollama push och körs på andra sidan med ollama run. Vad en kvantiseringsnivå betyder för kvalitet och minne hör till modellvalet; här handlar det om paketeringen i sig.

Där ekosystemet samlades: TGI-repot går till arkivet

Det färskaste tecknet på varför de här fyra namnen nämns tillsammans kommer från ett projekt som avslutats. Hugging Faces repo Text Generation Inference arkiverades den 21 mars 2026; datumet står i bannern på repots sida. README:n bär en notis om underhållsläge.

Det som betyder något är den andra meningen i just den notisen. Den säger att TGI satte i gång rörelsen mot att optimerade inferensmotorer förlitar sig på modellarkitekturerna i transformers, och hänvisar läsaren vid namn till vLLM, SGLang och lokalt körda motorer med inbördes kompatibilitet som llama.cpp eller MLX. Tre av de fyra lagren i det här inlägget nämns alltså i det avslutade projektets egen text.

Tidslinjen är kort: den sista taggade versionen var v3.3.7 den 19 december 2025. Repots funktionslista visar också vad som räknades som standard vid det datumet — tensorparallellism över flera GPU:er, kontinuerlig batchning av inkommande förfrågningar, ett Messages API som är kompatibelt med OpenAI Chat Completion API, och inferenskod som använder Flash Attention och Paged Attention.

Vilket lager som passar vilket scenario i en on-premise-installation

Det mest konkreta sifferparet som skiljer de här lagren åt efter driftsprofil står i dokumentationen för Ollama. OLLAMA_NUM_PARALLEL är antalet samtidiga förfrågningar per modell och har standardvärdet 1; OLLAMA_MAX_LOADED_MODELS är antalet modeller som kan vara inlästa samtidigt och har standardvärdet 3 per GPU. De standardvärdena beskriver en profil: låg samtidighet, många modeller.

Standardvärdena i vLLM och SGLang beskriver motsatsen: en modell, hög samtidighet. vLLM schemalägger efter först-till-kvarn-principen; eftersom blocken i en sekvens nås tillsammans tillämpas utrymningen allt-eller-inget. Tensorparallellism i Megatron-LM-stil stöds, och eftersom varje modell-shard behandlar samma mängd indatatokens hålls en enda KV-cachehanterare och en enda mappning från logiska till fysiska block inne i den centrala schemaläggaren.

SGLang beskriver sig självt som ett serving-ramverk som skalar från en enda GPU till stora distribuerade kluster. Standardvärdena i serverargumenten bekräftar profilen: radixcache på, lru som utrymningsprincip, sidstorlek 1, tensorparallellgrad 1.

Profilen för llama.cpp är minimal uppsättning: en backend-lista som sträcker sig från BLAS till CUDA och från Metal till Vulkan, och hybridinferens på CPU+GPU som delvis accelererar modeller som är större än den samlade VRAM-kapaciteten. På Ollama-sidan ligger modellkatalogerna på dokumenterade platser och flyttas med miljövariabeln OLLAMA_MODELS; där datalokalitet är ett skrivet krav hör även den sökvägen hemma i driftsättningsdokumentet.

API-gränssnittet standardiserar åtkomsten till modellen; lagret som standardiserar modellens åtkomst till verksamhetens system är ett annat, och det behandlas i inlägget ”Vad en MCP-server är och vad den inte är”.

Motsvarande arkitekturfråga i en delad installation — vem som får se vilken kontext — behandlas i inlägget ”Åtkomstkontroll i RAG: vem ser vad”.

Versionstakten är hög. Per den 8 augusti 2026 är den taggade versionen av vLLM v0.26.0 (27 juli 2026) och av SGLang v0.5.17 (8 augusti 2026). Flaggnamnen och standardvärdena i det här inlägget hör till samma datum; stäm av dem mot dokumentationen för din egen version innan du driftsätter.

Källor

  • Orca: A Distributed Serving System for Transformer-Based Generative Models — USENIX OSDI ’22, s. 521-538 (schemaläggning på iterationsnivå, selektiv batchning, mätningen på 36,9 gånger)
  • Efficient Memory Management for Large Language Model Serving with PagedAttention — arXiv:2309.06180, SOSP ’23 (blocktabell, copy-on-write, andelen tokentillstånd)
  • SGLang: Efficient Execution of Structured Language Model Programs — arXiv:2312.07104 (RadixAttention, cachemedveten schemaläggning, Teorem 3.1)
  • NVIDIA Triton Inference Server-dokumentationen — Dynamic Batcher (max_queue_delay_microseconds)
  • vLLM — GitHub-repo, README.md och vllm/config/cache.py (funktionslista, standardvärde för prefixcache)
  • vLLM-dokumentationen — Automatic Prefix Caching, Structured Outputs, OpenAI-Compatible Server, Parallelism and Scaling
  • SGLang — GitHub-repo och dokumentation, Server Arguments (radixcache som standard, lru, sidstorlek 1)
  • Text Generation Inference — GitHub-repo (arkiverat den 21 mars 2026; senaste versionen v3.3.7)
  • GGUF Specification — ggml-org/ggml, docs/gguf.md (designmål, tokenizer.chat_template)
  • llama.cpp — GitHub-repo och tools/server/README.md (implementation utan beroenden, backend-tabell)
  • Ollama-dokumentationen — Modelfile Reference, Import a model, OpenAI compatibility och FAQ (standardvärden för samtidighet, modellkataloger)
  • Ollama’s new engine for multimodal models — Ollama Blog, 15 maj 2025
  • GitHub Releases — vllm-project/vllm v0.26.0 och sgl-project/sglang v0.5.17

Fler inlägg

Du har ett system att bygga — eller ett som ska ta nästa steg.

No deck needed. 20 minutes. The rest is up to you.