Benchmarks
Les performances de la passerelle Wäg face à LiteLLM dans des conditions identiques. Cette page expose la méthode en entier et publie les mesures qu'elle a produites. Nous ne publions pas de multiplicateurs.
Comment la mesure a été faite
Tout a tourné dans Docker sur une seule machine, chaque composant épinglé à ses propres cœurs afin que rien ne se dispute le CPU.
- Les deux passerelles se placent devant le même upstream avec une configuration de modèle identique : aucune n'a le chemin le plus facile.
- Chaque passerelle tourne comme sa propre documentation le recommande : LiteLLM selon son guide de production, avec autant de workers que de vCPU ; Wäg avec ses réglages par défaut. Le cache de réponses est désactivé des deux côtés.
- Les tests en boucle fermée mesurent le débit maximal. Les tests en boucle ouverte émettent à cadence fixe et comptent la latence à partir de l'instant d'envoi prévu : une passerelle qui met en file d'attente ne peut donc pas le dissimuler.
- Chaque réponse est validée : statut, structure du corps et, pour les flux, le nombre de fragments attendu et sa terminaison. Tout le reste compte comme une erreur, jamais comme une réponse rapide.
- Chaque scénario est exécuté trois fois et les tableaux affichent la médiane. Sans passerelle dans le chemin, la même charge atteint environ 90 000 req/s, bien au-delà des deux passerelles : les résultats sont donc limités par la passerelle, pas par le harnais.
Empreinte
Ce que coûte chaque passerelle avant de servir quoi que ce soit : mémoire au repos, temps de démarrage, taille de l'image et latence ajoutée à une requête unique face à un upstream instantané.
Ce que cela signifie : La mémoire décide du nombre de réplicas qui tiennent sur un nœud, et la taille de l'image de la rapidité d'un déploiement. Avec 3,8 Mo résidents et une image de 123 Mo, Wäg tient dans un sidecar et est prêt avant l'expiration d'un health check ; 2,3 Go et 11 secondes, c'est un pod autour duquel il faut planifier.
La latence ajoutée correspond au p50 de la passerelle moins le p50 mesuré sans passerelle dans le chemin.
Avec un upstream réaliste
Un vrai modèle répond en centaines de millisecondes : des milliers de requêtes sont donc en vol simultanément. Ici l'upstream prend 300 ms et les requêtes arrivent à cadence fixe quoi que fasse la passerelle. À 2 000 req/s, environ 600 requêtes sont en cours à tout instant.
| requêtes par seconde | Wäg p50 | Wäg p99 | Wäg Erreurs | Wäg CPU | LiteLLM p50 | LiteLLM p99 | LiteLLM Erreurs | LiteLLM CPU | × |
|---|---|---|---|---|---|---|---|---|---|
| 100 | 303 ms | 305 ms | 0,00% | 10,2% | 305 ms | 308 ms | 0,00% | 38,7% | 1,0× |
| 500 | 301 ms | 303 ms | 0,00% | 15,6% | 304 ms | 321 ms | 0,00% | 140,5% | 1,1× |
| 1 000 | 301 ms | 302 ms | 0,00% | 20,2% | 317 ms | 605 ms | 0,00% | 263,7% | 2,0× |
| 2 000 | 301 ms | 302 ms | 0,00% | 31,7% | 3,26 s | 28 s | 7,99% | 391,9% | 92× |
Ce que cela signifie : C'est la forme du trafic réel : un modèle répond en centaines de millisecondes et des milliers de requêtes sont en vol. Une passerelle qui met en file d'attente transforme une réponse de 300 ms en plusieurs secondes, et personne ne le voit avant le pic. Wäg reste au plancher jusqu'à 2 000 req/s avec moins d'un tiers de son budget CPU. LiteLLM met déjà en file d'attente à 1 000 et échoue sur 8 % des requêtes à 2 000.
L'upstream seul prend 300 ms : c'est le plancher. Tout ce qui le dépasse est du temps passé en file d'attente dans la passerelle. La colonne × se lit toujours de la même façon : plus la valeur est élevée, plus Wäg est devant. C'est Wäg ÷ LiteLLM pour le débit et LiteLLM ÷ Wäg pour la latence.
Débit et latence
Boucle fermée face à un upstream instantané : le coût pur de la passerelle, sans latence de modèle derrière laquelle se cacher. Ce sont les chiffres nécessaires à un plan de capacité. Ce ne sont pas la raison de choisir un plan de contrôle.
4 vCPU, sans base de données
| Clients simultanés | Wäg req/s | Wäg p99 | Wäg CPU | LiteLLM req/s | LiteLLM p99 | LiteLLM CPU | × |
|---|---|---|---|---|---|---|---|
| 1 | 6 232 | 0,27 ms | 63,4% | 463 | 3,2 ms | 98,1% | 13× |
| 8 | 25 711 | 0,70 ms | 311,5% | 1 237 | 15 ms | 286,1% | 21× |
| 32 | 25 093 | 2,6 ms | 321,8% | 1 621 | 46 ms | 385,1% | 15× |
| 128 | 23 639 | 11 ms | 328,3% | 1 669 | 111 ms | 396,2% | 14× |
| 512 | 22 407 | 45 ms | 336,9% | 1 569 | 749 ms | 391,6% | 14× |
1 vCPU, sans base de données
| Clients simultanés | Wäg req/s | Wäg p99 | LiteLLM req/s | LiteLLM p99 | × |
|---|---|---|---|---|---|
| 1 | 6 589 | 0,35 ms | 459 | 3,2 ms | 14× |
| 8 | 16 594 | 1,3 ms | 484 | 26 ms | 34× |
| 32 | 15 226 | 3,8 ms | 506 | 85 ms | 30× |
| 128 | 13 264 | 15 ms | 505 | 438 ms | 26× |
Ce que cela signifie : Cela détermine le nombre de réplicas nécessaires à un niveau de trafic, donc le coût d'exploitation de la passerelle. Pour Kubernetes, c'est le tableau à 1 vCPU qu'il faut lire : c'est l'efficacité par cœur qu'un pod obtient réellement.
Streaming, charges utiles, embeddings, bascule
4 vCPU, sans base de données. Les flux comptent 50 tokens espacés de 5 ms ; le test d'endurance envoie 200 flux par seconde de réponses d'environ 1,2 seconde pendant trois minutes.
| Scénario | Wäg | Wäg p99 | Wäg Erreurs | LiteLLM | LiteLLM p99 | LiteLLM Erreurs | × |
|---|---|---|---|---|---|---|---|
| Streaming, 64 clients | 233 | 286 ms | 0,00% | 179 | 674 ms | 0,00% | 1,3× |
| Streaming, 256 clients | 911 | 288 ms | 0,00% | 307 | 1,09 s | 0,00% | 3,0× |
| 64 KB conversation history, 32 clients | 3 583 | 29 ms | 0,00% | 568 | 106 ms | 0,00% | 6,3× |
| Embeddings (1,536 dims), 32 clients | 13 558 | 4,3 ms | 0,00% | 740 | 69 ms | 0,00% | 18× |
| Fallback after upstream 500, 16 clients | 19 816 | 1,8 ms | 0,00% | 584 | 62 ms | 0,00% | 34× |
| Soak: 200 streams/s for 3 minutes | 200 | 1,27 s | 0,00% | 176 | 20 s | 12,09% | 1,1× |
Ce que cela signifie : Streaming, prompts longs, embeddings et bascule : c'est là qu'une charge de production passe son temps. Lisez les colonnes d'erreurs avant celles de débit : une requête échouée est une requête utilisateur échouée, pas une requête lente.
Le chiffre des charges de 64 Ko atteint le plafond du harnais : sans passerelle dans le chemin, la même charge atteint 3 711 req/s de corps de 64 Ko sur un cœur. La valeur de Wäg y est donc un minorant, pas un maximum.
Ce que ceci ne montre pas
Tout benchmark décrit un montage. Voici les limites de celui-ci.
- Les valeurs absolues dépendent de l'hôte. Les deux passerelles ont tourné sur la même machine avec des cœurs épinglés : la comparaison entre elles tient, mais attendez-vous à des valeurs absolues différentes sur votre propre matériel.
- Le harnais qui a piloté ces mesures n'est pas publié : vous ne pouvez donc pas les répéter commande par commande. La méthode ci-dessus est décrite assez précisément pour reconstruire un équivalent.
- LiteLLM publie presque chaque semaine. Ces chiffres ont été mesurés avec v1.104.2, la version stable actuelle. Dès qu'une version modifie le chemin de transmission, nous relançons la mesure et publions ce que nous trouvons.
- LiteLLM a été configuré d'après son guide de production, non par ses mainteneurs. Une meilleure configuration existe peut-être ; si vous en trouvez une, nous relancerons la mesure et republierons.
- Les deux passerelles ont tourné sur une seule machine avec des cœurs épinglés. Rien ici ne mesure le comportement à travers un réseau, sous l'ordonnancement de Kubernetes ou avec des voisins bruyants.