Parallélisme de Docling

Deux paramètres déterminent combien de documents sont convertis en même temps par Docling : QANSWER_CELERY_DOCLING_POOL et QANSWER_CELERY_DOCLING_CONCURRENCY. Cette page explique comment les choisir en fonction du nombre de réplicas du backend Python et de réplicas de workers Docling que vous exploitez.

Comment un document arrive dans Docling

Chaque PDF ou document bureautique indexé passe par trois étapes :

  • Chaque réplica du backend Python exécute un worker Celery qui consomme la file docling_extraction_queue. Ces deux paramètres configurent ce worker.
  • Le serveur d'API Docling (docling, QANSWER_DOCLING_ENDPOINT) reçoit chaque requête et la place dans une file Redis (RQ).
  • Les réplicas de workers Docling (docling-worker) prennent les tâches dans cette file et les convertissent. Chaque réplica exécute un worker RQ, qui traite une tâche à la fois : il compte donc pour un convertisseur.

Une tâche Celery correspond à une requête HTTP bloquante vers le serveur d'API pour un lot de pages. Un lot compte 8 pages, sauf si vous définissez QANSWER_INDEXING_PAGE_BATCH_SIZE (vide : valeur par défaut du backend).

Ce que contrôlent ces paramètres

  • QANSWER_CELERY_DOCLING_POOL est le type de pool Celery. La valeur par défaut du bundle est prefork.
  • QANSWER_CELERY_DOCLING_CONCURRENCY est le nombre d'emplacements de ce pool. Le bundle utilise 2 par défaut.

Avec prefork, N emplacements maintiennent N conversions en cours par réplica du backend. Le nombre total de requêtes envoyées à Docling est donc le nombre de réplicas du backend multiplié par la concurrence.

La règle

Visez environ deux requêtes en cours par worker Docling : une en cours de conversion et une en attente, pour qu'un convertisseur ne reste jamais inactif entre deux tâches. Avec QANSWER_SCALING_PYTHON_REPLICAS_NUMBER réplicas du backend et QANSWER_SCALING_DOCLING_WORKER_REPLICAS_NUMBER workers Docling, on obtient :

concurrency = max(1, round(2 × docling workers ÷ backend replicas))

Arrondissez à l'entier le plus proche, avec un minimum de 1 : avec moins de workers Docling que de réplicas du backend, gardez 1 par backend.

Note
Le bundle utilise prefork avec une concurrence de 2. Le pool solo est destiné à Docling en processus (in-process).

Configurations courantes

Réplicas du backendWorkers DoclingConcurrenceRequêtes en coursCible
11222
12444
22244
24488
32134
34398
3641212
42144

Les valeurs par défaut du bundle (2 réplicas du backend, 2 workers Docling) correspondent à la troisième ligne.

Exemple concret

Vous exploitez 3 réplicas du backend et 4 workers Docling. La cible est de 2 x 4 = 8 requêtes en cours, et 8 / 3 = 2,67, ce qui donne une concurrence de 3 après arrondi. Ajoutez ceci dans .env.override :

QANSWER_SCALING_PYTHON_REPLICAS_NUMBER=3
QANSWER_SCALING_DOCLING_WORKER_REPLICAS_NUMBER=4
QANSWER_CELERY_DOCLING_POOL=prefork
QANSWER_CELERY_DOCLING_CONCURRENCY=3

Cela maintient 3 x 3 = 9 requêtes en cours pour 4 convertisseurs : proche de la cible de 8.

Pourquoi ne pas aller plus haut

Les requêtes au-delà de ce que les convertisseurs peuvent traiter ne vont pas plus vite : elles attendent dans la file de Docling. Cette file n'a pas de priorités : chaque requête supplémentaire qu'un gros envoi a en cours passe devant les documents des autres utilisateurs. QAnswer répartit les lots de chaque utilisateur sur les niveaux de priorité de son broker Celery, ce qui n'aide que tant que les lots attendent encore côté QAnswer.

Conseil
Mesuré avec 2 réplicas du backend et 2 workers Docling sur 2 028 pages : prefork avec 2 par backend a pris 159 à 162 s, avec des convertisseurs occupés 69 % du temps ; avec 4 par backend, 159 s, sans gain. Pendant un gros envoi, un nouveau document de 8 pages a pris 1,2 fois son temps sur un système au repos avec 2 par backend, et 2,8 fois avec 4 par backend.

Quand une conversion échoue

Lorsqu'une requête Docling échoue ou expire, le backend journalise un avertissement (« Docling serve conversion failed, falling back to vanilla parser ») et convertit ce lot avec son analyseur simple (OCR Tesseract si l'OCR était demandé). Les limites sont :

  • Le serveur d'API Docling attend au plus 200 secondes (DOCLING_SERVE_MAX_SYNC_WAIT) une conversion synchrone, et le backend demande à Docling d'abandonner un lot au bout de 180 secondes.
  • Une tâche Docling est arrêtée après 300 secondes (DOCLING_SERVE_ENG_RQ_JOB_TIMEOUT), quel que soit l'état de la requête qui l'a créée.

En pratique, la cause est le plus souvent un worker Docling ou le serveur d'API tué faute de mémoire, ce qui fait échouer toutes les requêtes qu'il a en cours : lors d'un de nos essais, un seul redémarrage du serveur d'API a envoyé 121 lots sur 255 vers l'analyseur simple. Dans les mesures ci-dessus, aucun lot n'est passé par l'analyseur simple, même avec 4 par backend.

Avertissement
Un lot traité par l'analyseur simple n'apparaît pas comme une erreur : le fichier est tout de même indexé, sans le résultat de Docling pour ces pages. Si l'avertissement revient souvent, vérifiez les redémarrages et la mémoire des conteneurs Docling (voir ci-dessous).

Mémoire

  • Worker Docling : QANSWER_DOCLING_WORKER_MEMORY_LIMIT, 8G par défaut.
  • Les workers Docling grossissent avec le temps (de 3 à 8 Gio en une journée lors de nos essais, ticket docling-serve 366). Lorsqu'un worker reste au-dessus de QANSWER_DOCLING_WORKER_RECYCLE_MB (6144 par défaut) pendant cinq contrôles consécutifs, son contrôle de santé échoue et autoheal le redémarre proprement, une fois la tâche en cours terminée. Gardez ce seuil sous la limite de mémoire d'une marge supérieure au pic d'une tâche.
  • Serveur d'API Docling : QANSWER_DOCLING_APISERVER_MEMORY_LIMIT, 4G par défaut. Il lui faut plus de mémoire à mesure que les requêtes en cours augmentent : avec 1 Gio, il a été tué avec quatre requêtes en cours.
  • Backend Python : QANSWER_PYTHON_BACKEND_MEMORY_LIMIT, 20G par défaut.

Avec prefork, chaque emplacement est un processus distinct dans le conteneur du backend : une concurrence plus élevée s'impute donc sur la limite du backend. Dans les mesures ci-dessus, le backend est resté sous 9,5 Gio de ses 20 Gio avec jusqu'à 4 emplacements.

Appliquer la modification

Définissez les variables dans le fichier .env.override du répertoire du bundle (créez-le s'il n'existe pas). Il est chargé après .env et .env.specs, donc il l'emporte, et il n'est pas versionné. Les nombres de réplicas se trouvent dans .env.specs et peuvent être surchargés de la même façon.

# .env.override
QANSWER_CELERY_DOCLING_POOL=prefork
QANSWER_CELERY_DOCLING_CONCURRENCY=2

Relancez ensuite le script de déploiement, qui recrée les services dont la configuration a changé :

./deploy-scripts/up.sh

Vérifier le résultat

Si vous exploitez le bundle de supervision, deux tableaux de bord Grafana montrent si la configuration fonctionne :

  • Celery Monitoring - Flower Metrics : le panneau Currently Executing Tasks (by worker type) indique combien de tâches d'extraction Docling s'exécutent en même temps ; il devrait approcher le nombre de réplicas du backend multiplié par la concurrence pendant un gros import.
  • Docling Serve - OTel Observability : Active Requests (Concurrency) et Request Duration Percentiles montrent la charge du serveur d'API, Error Rate Over Time (4xx / 5xx) les requêtes en échec, et les panneaux de santé des conteneurs la mémoire et les redémarrages des workers.

Pendant l'indexation d'un gros fichier, regardez le taux d'occupation des workers Docling. Nettement sous 80 %, il reste de la marge pour plus de concurrence ; vers 80 % ou plus, une concurrence plus élevée n'ajoute que de l'attente.

Dans les journaux du backend, recherchez « falling back to vanilla parser ». Quelques lignes isolées sont normales ; un flux continu signifie le plus souvent que des conteneurs Docling redémarrent.