Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Arquitectura de sistemes LLM

Aquest document descriu com encaixar un LLM en una arquitectura de programari: quines propietats té com a component, com se serveix (Ollama, vLLM), quin estàndard d’API s’usa i quins requisits de maquinari imposen els pesos i la KV cache. Per als patrons de programació (prompts, RAG, eines), consulta Patrons de programació amb LLMs.

Arquitectura d’un sistema LLM

LLMs com a components de programari

Els models de llenguatge que hem estudiat a Transformers i Models de Llenguatge són eines de gran capacitat, però una capacitat en aïllament no és una aplicació. Un LLM que rep un prompt i retorna text és, des del punt de vista del programari, un component — com ho és una base de dades, una cua de missatges o una API externa.

Com a component, té propietats que l’enginyer de sistemes ha de tenir presents:

  • No determinista: la mateixa entrada pot produir sortides diferents en cada crida. Amb temperatura > 0, el model mostra distribucions de probabilitat, no funcions deterministes.
  • Sense estat: el model no recorda res entre crides. Tota la “memòria” de la conversa ha de viatjar explícitament en cada petició — l’historial de missatges, el context recuperat, les instruccions del sistema.
  • Lent i car: una crida a un LLM pot trigar entre 500 ms i diversos segons, i té un cost per token. No és l’operació adequada per a cada petició d’un sistema.
  • Pot fallar de manera silenciosa: un LLM no llança una excepció si no sap la resposta — la genera igualment, amb confiança. Detectar errors requereix validació explícita de la sortida.

Aquestes propietats no fan que els LLMs siguin inferiors a altres components; simplement requereixen patrons d’enginyeria específics, igual que una base de dades requereix transaccions o una cua de missatges requereix idempotència.

On viu la crida al model en una arquitectura web

En una arquitectura frontend-backend clàssica, la crida al model viu al backend — mai al frontend directament. Hi ha tres raons principals:

  1. Seguretat: les credencials d’accés al model no s’han d’exposar al navegador.
  2. Control: el backend pot validar, enriquir i postprocessar la resposta abans d’enviar-la al client.
  3. Cost: el backend pot evitar crides redundants, aplicar cache i controlar el pressupost de tokens.
Usuari (navegador)
        │  petició HTTP
        ▼
   Backend
        │
        ├── validació de l'entrada
        ├── recuperació de context
        ├── construcció del prompt
        │
        ▼
   Servei d'inferència (model)
        │
        ▼
   Backend
        │
        ├── validació de la sortida
        ├── postprocessament
        │
        ▼
   Usuari (resposta)

La crida al model és un pas intern del backend, no un proxy directe. El backend és responsable de construir el prompt a partir de l’entrada de l’usuari i del context addicional, assegurar que la sortida té el format esperat, i gestionar errors i fallbacks.

Les capes d’un sistema LLM

Un sistema LLM combina diverses capes funcionals. No totes són necessàries en tots els sistemes: inferència i interfície unificada són sempre presents; la resta apareixen quan el sistema les necessita.

CapaRolPresènciaExemples OSS
InferènciaServir el model i exposar una APISempreOllama, vLLM, SGLang, llama.cpp
Client d’inferènciaFer crides al model i encapsular l’accés; pot ser un SDK estàndard o una abstracció pròpia de l’aplicacióSempreOpenAI SDK, Anthropic SDK, o capa pròpia
Routing / proxyAbstraure múltiples proveïdors amb una API unificadaSi hi ha múltiples proveïdorsLiteLLM
Emmagatzematge vectorialIndexar i cercar per similitud semànticaSi cal RAGChroma, Qdrant, Weaviate
OrquestracióCoordinar passos, eines i flux del sistemaSi hi ha múltiples passos o einesLangGraph, LlamaIndex
ObservabilitatRegistrar, traçar i mesurar el comportamentRecomanat en produccióLangSmith, Langfuse
DesplegamentEmpaquetar i operar el sistemaEn producció; trivial en devDocker Compose, Kubernetes

El client d’inferència és sempre necessari; el routing és opcional. L’estàndard de facto del sector és l’API d’OpenAI: un protocol HTTP que la majoria de serveis d’inferència implementen, tant locals (Ollama, vLLM) com comercials (OpenAI, Anthropic via LiteLLM, Google via LiteLLM). Això significa que el codi que interactua amb el model és independent del proveïdor — canviar de model local a API comercial és un detall de configuració, no un canvi de codi. Quan el sistema necessita suportar múltiples proveïdors, s’afegeix una capa de routing (LiteLLM) entre el client i els proveïdors.

El servei d’inferència

El rol del servidor de models

Un LLM no és una llibreria que s’importa en el codi de l’aplicació — és un servei que s’executa per separat i s’exposa via API. Aquesta separació és intencional: carregar els pesos d’un model a memòria és una operació costosa (pot trigar desenes de segons i ocupar desenes de GB) que no pot repetir-se a cada petició. El servidor de models fa aquesta càrrega una sola vegada i manté el model en memòria per atendre múltiples peticions.

Les responsabilitats del servidor d’inferència com a component arquitectònic són:

  • Carregar i mantenir el model en memòria (GPU o CPU)
  • Rebre peticions, processar-les i retornar respostes
  • Gestionar la concurrència (múltiples peticions simultànies)
  • Exposar una API estàndard que el backend pugui consumir

Hi ha dos patrons principals de desplegament d’un servidor d’inferència, i ambdós exposen la mateixa API estàndard:

Servidor local individual (Ollama): cada desenvolupador executa el servidor a la seva pròpia màquina. És l’opció més senzilla — una sola comanda descarrega el model i el serveix. Adequat per a desenvolupament i prototipatge.

Servidor d’inferència compartit (vLLM): una màquina amb GPU (al núvol o en un servidor de laboratori) executa el model i tot l’equip hi apunta. És el patró habitual en entorns institucionals i de producció. vLLM és la referència per a aquest cas: implementa PagedAttention (gestió eficient de la KV cache per a peticions concurrents) i batching continu (agrupa peticions de múltiples clients per maximitzar l’ús de la GPU), cosa que el fa significativament més eficient que Ollama sota càrrega.

Desenvolupament              Producció / equip
─────────────────            ──────────────────────────────
Màquina del dev              Servidor GPU compartit
┌──────────────┐             ┌──────────────────────────┐
│    Ollama    │             │          vLLM            │
│  llama3.2    │             │       llama3-70B         │
└──────┬───────┘             └────────────┬─────────────┘
       │ localhost:11434                  │ gpu-server:8000
       ▼                                 ▼
  Backend local                 Backend (dev/prod)

SGLang és una alternativa a vLLM que ha guanyat adopció per a models de raonament i casos on cal control fi del procés de generació (p.ex. constrained decoding o múltiples crides encadenades). Implementa optimitzacions similars a vLLM i exposa la mateixa API compatible amb OpenAI.

Des del punt de vista del backend, les tres configuracions (Ollama, vLLM, SGLang) són intercanviables: la crida al model és idèntica, només canvia la URL del servidor.

L’estàndard de la interfície: l’API compatible amb OpenAI

L’estàndard de facto per comunicar-se amb un servidor de models és el protocol definit per OpenAI: una crida HTTP POST a /v1/chat/completions amb una llista de missatges i uns paràmetres de generació, que retorna el text generat. La majoria de servidors d’inferència implementen aquest protocol, tant locals com comercials.

Això permet que el codi del backend sigui independent del proveïdor. L’única diferència entre entorns és la configuració del client:

from openai import OpenAI

# Ollama en local (desenvolupament individual)
client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")

# vLLM en servidor compartit (laboratori / equip)
client = OpenAI(base_url="http://gpu-server:8000/v1", api_key="token-intern")

# API comercial (producció o prototipatge ràpid)
client = OpenAI(api_key="sk-...")  # base_url per defecte apunta a OpenAI

La crida al model és idèntica en tots els casos:

response = client.chat.completions.create(
    model=MODEL,
    messages=[{"role": "user", "content": "Explica en una frase què és un transformer."}]
)
print(response.choices[0].message.content)

Abstraure múltiples proveïdors: la capa de routing

Quan un sistema necessita suportar diversos proveïdors (per fallback, per A/B testing de models, o per centralitzar el control de costos), afegir una capa de routing entre el backend i els proveïdors és el patró adequat. Aquesta capa exposa una única API estàndard cap al backend i gestiona la distribució cap als proveïdors de forma transparent.

LiteLLM és l’exemple de referència: actua com a proxy local que parla OpenAI cap al backend i tradueix les crides a qualsevol proveïdor (Anthropic, Google, servidors vLLM propis, etc.) segons la configuració.

Backend
   │  API OpenAI estàndard
   ▼
LiteLLM (routing)
   ├──► Ollama (local)
   ├──► OpenAI API
   └──► Anthropic API

Quan el sistema només usa un proveïdor, aquesta capa no és necessària — afegir-la prematurament és complexitat innecessària. Val la pena introduir-la quan el sistema necessita canviar de model sense modificar el codi del backend, o quan cal consolidar observabilitat i costos de múltiples proveïdors en un sol punt.

Selecció del model

La selecció del model és una decisió arquitectònica, no un detall d’implementació. A aquest nivell només cal mirar les restriccions que imposa el sistema:

  • Latència i throughput: si la resposta ha de ser interactiva, el model ha de ser prou ràpid i suportar la càrrega esperada.
  • Context necessari: si la tasca processa documents llargs o històrics amplis, cal una finestra de context adequada.
  • Privacitat i compliance: si les dades no poden sortir del perímetre propi, la inferència local passa a ser un requisit.
  • Hardware disponible: la mida del model ha de cabre en la infraestructura que tens, inclosa la KV cache.

La regla pràctica és començar amb el model més petit que compleixi aquestes restriccions i portar la decisió fina a Selecció de model per cas d’ús, on hi ha la matriu de capacitats, els criteris per cas d’ús i els punts de partida recomanats.

Requisits de maquinari

Un model amb \(N\) paràmetres en precisió FP16 ocupa aproximadament \(2N\) bytes de memòria GPU només per emmagatzemar els pesos:

ModelParàmetresPesos (FP16)Entrenament (estimat)
Petit (BERT)110M~0.2 GB~1 GB
Mitjà (LLaMA 7B)7B~14 GB~56 GB
Gran (LLaMA 70B)70B~140 GB~560 GB

⚠️ Aquestes xifres són aproximacions dels pesos del model, no del consum real de memòria. Durant la inferència, cal afegir la KV cache (memòria que emmagatzema les keys i values calculades per a cada token generat), que creix amb la longitud de la seqüència i pot suposar diversos GB addicionals. Durant l’entrenament, els gradients, els estats de l’optimitzador i les activacions intermèdies poden multiplicar el consum per 4-6× respecte els pesos sols.

Per a més detalls sobre GPU i CUDA, consulta la secció de càlcul eficient.

La KV cache: els models decoder-only generen text un token a la vegada. A cada pas, el mecanisme d’atenció necessita les keys i values de tots els tokens anteriors. La KV cache evita recalcular-les: les emmagatzema i només calcula les del nou token. El cost de memòria creix linealment amb la longitud de la seqüència — per a seqüències llargues (32K-128K tokens), la KV cache pot ocupar més VRAM que els propis pesos quantitzats. Ollama, llama.cpp i vLLM la implementen automàticament.

Quantització: reduir la precisió dels pesos (de FP16 a INT8 o INT4) permet executar models més grans en hardware limitat, amb una pèrdua mínima de qualitat. Els pesos d’un model de 7B en INT4 ocupen ~3.5 GB; el consum real de VRAM durant la inferència serà superior (KV cache, buffers d’activació).

Optimitzacions d’inferència

Els servidors d’inferència moderns implementen tècniques que expliquen per què vLLM i SGLang es comporten molt millor que Ollama sota càrrega concurrent.

Flash Attention: reimplementació del mecanisme d’atenció que redueix el consum de VRAM, permetent processar seqüències molt més llargues amb el mateix hardware. Tots els servidors moderns l’apliquen per defecte — és transparent per al desenvolupador però explica per què les finestres de context grans (128k+) són pràctiques avui.

Batching continu (continuous batching): en lloc d’esperar que totes les peticions d’un batch acabin per acceptar-ne de noves, el servidor afegeix noves peticions tan bon punt n’acaba una d’anterior. Elimina el temps mort de la GPU i és la raó principal per la qual vLLM serveix desenes de peticions concurrents eficientment mentre Ollama en serveix una a la vegada.

Last change: , commit: 416443d