Introduction : pourquoi un Singleton global dédié ?
Dans une architecture utilisant LlamaIndex, la gestion des modèles (LLM, embeddings, tokenizer, versions) repose sur des mécanismes internes du framework. Ces mécanismes fonctionnent correctement dans des cas simples, mais présentent plusieurs limites structurelles lorsqu’on construit une application :
- modulaire,
- multi‑thread,
- multi‑process (ex. plusieurs workers uvicorn/gunicorn),
- ou composée de plusieurs modules indépendants devant partager des modèles identiques.
Le “singleton” interne de LlamaIndex n’est ni strict, ni stable, et peut être réinitialisé ou modifié de manière implicite selon l’ordre des imports, les contextes d’exécution ou les workers.
Cela conduit à des comportements non déterministes, difficiles à diagnostiquer, et parfois dangereux (modèles différents selon les modules ou les workers).
Pour stabiliser l’ensemble, il est nécessaire d’introduire un singleton global explicite, contrôlé, verrouillé, traçable et idempotent : c’est le rôle de AppSettings et AppSettingsManager qui forment un système robuste plus strict et plus fiable que celui fourni par LlamaIndex, et adapté à une architecture modulaire, multi‑thread et multiprocess.
Ce texte documente :
- le principe du Singleton global utilisé dans nos applications d’IA,
- en quoi il diffère du “singleton” de LlamaIndex,
- la structure et le rôle de AppSettings,
- la logique et les responsabilités de AppSettingsManager,
- les mécanismes d’initialisation, de verrouillage et de traçage.
1. Principe général
AppSettings est un singleton global destiné à contenir les objets critiques de l’application :
- le modèle LLM,
- le modèle d’embedding,
- le tokenizer,
- les versions associées.
Ce singleton est conçu pour être :
- unique dans le processus,
- initialisé une seule fois,
- verrouillé après initialisation,
- idempotent (les appels suivants ne modifient rien),
- traçable (on sait qui a initialisé en premier).
Ce mécanisme garantit que toutes les parties de l’application utilisent les mêmes objets, sans risque de divergence ou de réinitialisation accidentelle.
2. Différence avec le “singleton” de LlamaIndex
LlamaIndex utilise un mécanisme interne d’initialisation (souvent via Settings ou des loaders) qui :
- n’est pas global à l’application, mais local au framework,
- peut être réinitialisé selon les contextes,
- n’est pas conçu pour être partagé entre plusieurs modules indépendants,
- ne fournit pas de verrou multi‑thread ou multi‑process,
- ne trace pas l’origine de l’initialisation.
En résumé :
| Aspect | Notre AppSettings | LlamaIndex Settings |
| Portée | Globale à l’application | Locale au framework |
| Initialisation | Unique, verrouillée | Réinitialisable |
| Idempotence | Oui | Non garanti |
| Traçage | Oui (stack + timestamp) | Non |
| Multi‑thread | Protégé | Non |
| Multi‑process | Protégé (file lock) | Non |
Notre système est donc plus strict, plus robuste et plus adapté à une application multi‑modules.
3. Structure de AppSettings
AppSettings est un BaseModel Pydantic contenant les objets critiques :
llm: Optional[Any]
embed_model: Optional[Any]
tokenizer: Optional[Any]
versions: Optional[Dict[str, Any]]
locked: bool
initialized_by: Optional[str]
initialized_at: Optional[float]
Caractéristiques :
lockedempêche toute modification après initialisation.initialized_bycontient la stack trace du premier appel.initialized_atcontient le timestamp d’initialisation.extra = "forbid"empêche l’ajout d’attributs fantômes.validate_assignment = Truegarantit un comportement fail‑closed.
4. Rôle de AppSettingsManager
AppSettingsManager est le seul point d’entrée pour modifier ou initialiser AppSettings.
Il fournit :
4.1. load_models()
- Charge les objets critiques.
- Capture la stack trace du premier appel.
- Capture le timestamp.
- Valide les objets.
- Verrouille l’état.
- Est idempotent : si déjà initialisé, ne fait rien.
4.2. validate()
- Vérifie la présence de
llm,embed_model,tokenizer. - Comporte un comportement fail‑closed.
4.3. lock()
- Marque l’état comme immuable.
4.4. describe()
- Retourne un dictionnaire d’introspection complet.
5. Sécurité multi‑thread et multi‑process
L’initialisation est protégée par deux verrous :
5.1. Verrou thread‑safe
_thread_lock = threading.Lock()
Empêche deux threads du même processus d’initialiser simultanément.
5.2. Verrou multi‑process (file lock)
_process_lock = FileLock("/tmp/appsettings_init.lock")
Empêche deux processus (ex. plusieurs workers uvicorn/gunicorn) d’initialiser simultanément.
Ce mécanisme garantit une initialisation unique, même dans un environnement multi‑workers.
6. Cycle de vie complet
- Une application appelle
load_models(). - Le verrou multi‑process est acquis.
- Le verrou multi‑thread est acquis.
- Si
locked == True→ rien n’est fait. - Sinon :
- stack trace enregistrée,
- timestamp enregistré,
- objets chargés,
- validation stricte,
- verrouillage.
- Les appels suivants sont silencieusement ignorés.

7. Avantages de cette architecture
- Initialisation unique garantie.
- Protection contre les réinitialisations accidentelles.
- Sécurité multi‑thread et multi‑process.
- Traçabilité complète.
- Idempotence.
- Isolation claire entre :
- le stockage global (
AppSettings) - la logique métier (
AppSettingsManager) - les configurateurs externes (ex.
LlamaConfigurator).
- le stockage global (
8. Résumé
AppSettings et AppSettingsManager forment un système robuste permettant :
- de centraliser les objets critiques,
- de garantir une initialisation unique,
- de sécuriser l’accès concurrent,
- de tracer l’origine de l’initialisation,
- de fournir une introspection complète.
Ce mécanisme est plus strict et plus fiable que celui fourni par LlamaIndex, et il est adapté à une architecture modulaire, multi‑thread et multiprocess.