Introduction
Avec l'explosion des frameworks distribués et des APIs d'inférence à la demande, on pourrait croire qu'il suffit de brancher un orchestrateur existant sur un cluster GPU pour faire tourner un service LLM à l'échelle.
Mais très vite, certains obstacles techniques et économiques deviennent évidents.
Les limites des API-as-a-Service
Les APIs cloud (OpenAI, Mistral, etc.) imposent rapidement des limitations de débit -- quotas par minute, par heure -- qui deviennent contraignantes dès qu'on veut scaler proprement, surtout dans une logique de service local ou souverain.
Et même lorsque les limites sont acceptables, la dépendance à une ressource distante et non maîtrisée pose des problèmes de latence, de résilience et de coût.
Instancier un serveur GPU : une fausse bonne idée ?
On pourrait croire qu'en provisionnant dynamiquement des machines avec GPU, on récupère de la liberté. Mais là encore, on se heurte à des limitations concrètes :
- Combien de temps faut-il vraiment pour instancier un serveur GPU a l'heure ?
- Combien de requetes simultanees peuvent-ils encaisser ?
- Comment cela se compare-t-il a des serveurs plus accessibles (comme les Apple M4) ?
- Et surtout : est-ce rentable ?
Mon constat de terrain
Je n'ai pas encore testé Kubernetes, Ray ou d'autres orchestrateurs distribués, mais j'anticipe une complexité inutile dans mon cas d'usage. Ce que je cherche, c'est un système :
- Simple à déployer,
- Optimisé pour des ressources locales, hétérogènes et dynamiques,
- Capable de gérer la résilience sans usine à gaz.
La majorité des solutions que j'ai évaluées sont surdimensionnées, trop verbeuses à déployer, peu souveraines (dépendances cloud difficiles à auditer), et surtout peu optimisées pour un modèle local, modulaire et frugal -- ce qui est précisément l'ADN de mAIstrow.
Un orchestrateur maison, écrit en Rust
C'est dans ce contexte que j'ai décidé de concevoir mon propre orchestrateur. Un système léger, faiblement couplé, asynchrone et résilient par design, pensé pour exploiter :
- Des serveurs fixes ou volatiles (avec ou sans GPU),
- Des moteurs variés (llama.cpp, vLLM, Ollama, etc.),
- Et des logiques de fallback intelligentes (streaming, timeouts, relance automatique).
L'objectif : ne plus subir les limites imposées par l'infrastructure, mais bâtir un socle logiciel qui fonctionne ici, maintenant, et demain, peu importe les moyens.
Voici les choix que j'ai faits -- et les tests que je compte réaliser pour les valider.
1. Un systeme a trois corps : Hub, Engine, Client
L'architecture repose sur trois composants :
- Le Hub-server : il centralise tout. Il reçoit les requêtes, les planifie, les dispatche et les suit. Il est maître de la base de données, des timeouts et de l'auto-scaling.
- Les Engine-services : ils exécutent les tâches (LLM, embeddings, recherche vectorielle, etc.). Ce sont des exécutants silencieux, frugaux, et désengageables à tout moment.
- Les User-clients : ils envoient des requetes. C'est tout. Le Hub se charge du reste.
Un client qui disparaît ? Pas grave, la tâche continue. Un Engine qui tombe ? Pas grave, la tâche est relancée ailleurs. Un Hub qui redémarre ? Les tâches en cours sont restaurées.
Ce découplage est la clé de la résilience. Le reste est une question d'implémentation.
2. La tâche comme première classe
Tout tourne autour d'un concept central : la Task.
C'est une structure Rust générique. Peu importe si c'est une requête LLM, un calcul d'embedding ou une recherche hybride -- ça reste une Task.
Chaque tâche contient un payload, un context optionnel pour la reprise, et une status persistée à chaque mise à jour. Le Hub est le seul à écrire en base. L'Engine streame simplement des updates.
3. Le Hub : cerveau unique, mémoire centralisée
Le Hub reçoit toutes les requêtes. Il suit chaque tâche, ping chaque Engine, mesure les temps de latence, relance si besoin. Il gère les timeouts, les priorités, et peut décider de demander plus de puissance.
Pas besoin de Kafka. Pas besoin de Ray. Tout est streame, controle et relancable via un simple timer et une table SQL.
4. L'Engine : sans mémoire, mais pas sans honneur
Les Engine-services sont conçus pour mourir proprement. Ils ne détiennent aucun état durable. Ils ne font que recevoir une Task, l'exécuter, streamer les résultats, dire "done", et potentiellement s'arrêter s'ils ne servent plus à rien.
Ce design permet une scalabilité native. Une commande du Hub suffit à instancier un nouveau process GPU sur un serveur Scaleway, OVH ou même local.
5. Resilience sans surcharge
Ce système supporte la déconnexion du client, du moteur ou même du hub -- sans jamais casser la logique métier.
La base est mise à jour au fil de l'eau, chaque Engine envoie son état, et si plus rien ne se passe, le Hub relance automatiquement. Simple, lisible, testable.
Et maintenant ?
J'ai commencé à prototyper ce système en Rust, avec une stack simple (Tokio, SQLx, Actix ou Axum). L'objectif est de le rendre auto-déployable, observé et prédictif, via logs, seuils dynamiques et métriques.
Le design est intentionnellement minimaliste. Ce n'est pas un framework générique : c'est un outil forgé pour un besoin précis, celui d'orchestrer de l'inférence LLM sur des ressources hétérogènes, avec la résilience comme contrainte première.
La suite de cette série abordera l'architecture distribuée, les comparaisons avec Kafka, Pulsar et Ray, et les détails de l'implémentation asynchrone en Rust.