Skip to content

Impact analysis — s42 hang fix (ADR-0096 thread/process caps + s42/s43 hardening)

Statut : r1 (companion du plan de fix et de la RCA ; normé par ADR-0096). Objet : cartographier l'impact du fix sur tout ce qui existe déjà — modules, infra, composants, bibliothèques, process — avant de lancer le fix. Pas un plan d'action (cf. fix plan) ; une analyse de surface d'impact + de blast-radius.

0. Ce que le fix change (résumé pour l'analyse)

Levier Surface modifiée
L1 — caps env-level OMP/OPENBLAS/MKL/NUMEXPR_NUM_THREADS + LOKY_MAX_CPU_COUNT + OMP_WAIT_POLICY=passive, posés avant tout import Python, au point d'entrée partagé de l'image de compute, depuis $CVN_POD_CPU_LIMIT (Downward API).
L2 — assertion fail-loud refus de démarrage si caps unset / > limite / CVN_POD_CPU_LIMIT non déclaré (event=pod_thread_caps_unsafe).
L3 — process-parallelism pas de n_jobs=-1 sur pod de compute ; défaut n_jobs=1 ; produit n_jobs × thread_cap ≤ limit.
L4 — s42 skip_phase_a explicite par-run (défaut inchangé) + belt num_threads.
L5 — s43 bootstrap B=2000 série (déjà codé série) + test Invariant 5 (pas de n_jobs=-1).
L6 — A6 détection replay-divergence indépendante (gate du futur défaut skip).

Le blast-radius dominant = L1/L2 : le point d'entrée partagé touche tous les pods qui en héritent, pas seulement s4x.

1. Impact par catégorie

1.1 Infra / déploiement

Élément Impact Sévérité
Image de base (airflow_docker/Dockerfile.k8s) reçoit le wrapper de cap. ⚠️ entrypoint.sh actuel est cron-based (pas le chemin des pods KPO, qui lancent python … directement) → le cap doit s'insérer dans le command/args du template KPO ou un bootstrap sourcé par la commande de compute, pas dans entrypoint.sh. À cartographier explicitement (cf. fix plan pré-condition inventaire). Haute
Pod specs KPO (dags/dag_diagnostic__*.py, dags/launch__*.py) doivent déclarer le Downward API CVN_POD_CPU_LIMIT (resourceFieldRef: limits.cpu). ~20 DAGs concernés (s18/s22/s26/s27/s28/s40/s41/s42 + launch promotion/discovery/retrain/backtesting/cleanup). Si centralisé dans _common.py:STANDARD_POD → 1 point. Pods n'héritant pas de STANDARD_POD = trou (blocker committee #2). Haute
Helm / values-prod aucune valeur runtime nouvelle si le cap lit la limite via Downward API (pas de nouveau ConfigMap). Les limits.cpu doivent exister sur chaque pod sinon fail-loud cpu_limit_undeclared → audit des specs sans resources.limits. Moyenne
Autoscale Scaleway PRO2-M inchangé. Le cap réduit le CPU réellement brûlé par pod (de ~3.2 cœurs busy-wait → travail utile) → potentiellement moins de pression d'autoscale, jamais plus. Faible (positif)
Contention node-level n_pods × cap > n_node_cores (5×4 > 16) → slowdown ~25%, pas livelock (cf. ADR-0096 Consequences). Acceptable one-shot. Faible

1.2 Modules de code

Module Impact Action
src/commun/finetune/diagnostic/s43_* bootstrap série déjà codé ; ajout test Invariant 5. aligné
src/commun/finetune/diagnostic/s18_step3_parity.py:556 contient n_jobs=-1 (LightGBM) — vecteur exact de l'Invariant 5. À borner (n_jobs=1 ou produit borné) avant que ce DAG tourne sur un pod cappé, sinon node_cores × cap threads. À corriger
src/commun/finetune/diagnostic/s28_windowing.py:229 Parallel(backend="threading") → couvert par le thread-cap (pas loky). Vérifier que n_jobs ≤ cap. Vérif
src/training/** (XGBoost trainer, label_pipeline, autonomous_ensemble_trainer, TwoStage hyperopt) label_pipeline = n_jobs=1 (safe) ; TwoStage = hyperconfig.n_jobs (à auditer) ; trainers OpenMP couverts par thread-cap. Le harness FTF tourne sur pod de compute → hérite du cap. Audit n_jobs
dags/_common.py point d'injection canonique du Downward API + (option) du wrapper. Changement structurel ici = le plus sûr. central

1.3 Bibliothèques (le cap les touche toutes via env)

Lib Mécanisme capté Risque résiduel
LightGBM / XGBoost / CatBoost OpenMP (OMP_NUM_THREADS) + param num_threads. belt code-level en plus.
numpy / scipy / sklearn BLAS (OPENBLAS_/MKL_NUM_THREADS) — dominant sur le bootstrap s43. dépend du backend BLAS lié (OpenBLAS vs MKL) → threadpoolctl.threadpool_info() à confirmer une fois (ADR-0096 validation note).
joblib / loky LOKY_MAX_CPU_COUNT + interdiction n_jobs=-1. le seul vecteur process ; le -1 de s18_step3_parity est la cible.
numexpr NUMEXPR_NUM_THREADS. couvert.

1.4 Composants / services

Composant Impact
Airflow workers (KPO trigger) aucun — le cap est dans le pod de tâche, pas le scheduler.
MLflow / cache aucun (env + startup check seulement, pas de data/logique).
Loki / Grafana nouveau event=pod_thread_caps_unsafe + event=s18_replay_* → catalogue d'events à étendre ; panneau Prometheus "fits vs CPU burnt" (reco committee #4).
Console / PG ftf_config aucun nouveau param config (le cap vient de l'infra/Downward API, pas de PG) → pas de violation ADR-59, rien à éditer côté opérateur.

1.5 Process / gouvernance

Process Impact
ADR catalogue ADR-0096 ajouté (active, committee PASSED) ; ADR-0095 §A3 défère à 0096. Catalogue adr/index.md mis à jour.
CI gates nouveau gate lint/guard n_jobs=-1 interdit sur pods de compute (reco committee #3, enforce Invariant 5). Gate optionnel threadpoolctl (reco #1).
Story workflow S04 gate In testing → Tested reste bloqué tant que le fix n'a pas re-tourné un verdict. Le fix lui-même n'est pas une Story S04 — c'est un correctif infra transverse (rattaché au RCA).
Deploy sequence L1/L2 = changement de toutes les images de compute → déploiement phasé (WARN → observe → fail-loud) pour borner le blast-radius (cf. fix plan §3).
Runbooks runbook s43 inchangé fonctionnellement ; ajout note "le pod cappe ses threads, ne pas surcharger n_jobs".

2. Surface NON impactée (negative scope — important)

  • Aucun changement de logique métier / pipeline de trading : filtres, inference, labels, backtest engine — intouchés. Le fix est env + startup-check + bornage de parallélisme.
  • Aucun changement de résultat numérique : caps + série donnent des mêmes verdicts (le bootstrap s43 est déjà bit-identical via default_rng(seed), série). Le cap ne change que le temps, pas les chiffres.
  • Aucun nouveau param PG / Console (ADR-59 non sollicité).
  • Pas de migration de données / de schéma ; rollback = revert env + assertion (état pré-incident), sans impact data/logique.

3. Risques d'interaction (ce qui pourrait casser ailleurs)

Risque Probabilité Mitigation
Un pod non-diagnostic (launch promotion/discovery/retrain/backtesting) hérite du cap et ralentit Moyenne cap sur cgroup = budget garanti ; passive → slowdown borné, jamais livelock. Inventaire pré-deploy.
Un pod sans limits.cpu → fail-loud cpu_limit_undeclared au démarrage (régression de démarrage) Moyenne audit des specs sans resources.limits avant le deploy fail-loud (phase WARN d'abord).
Le BLAS lié n'est pas celui cappé (MKL vs OpenBLAS) → cap manqué Faible threadpoolctl.threadpool_info() une fois en CI/startup.
s18_step3_parity n_jobs=-1 non corrigé avant deploy → re-livelock process-level Moyenne borner ce -1 (L3) + CI guard.
Limite fractionnaire (3.5 → cap 4) sur-souscrit légèrement Faible slowdown sous passive, pas livelock ; ou floor via millicores.

4. Checklist pré-fix (dérivée de l'impact)

  • Inventaire pods héritant / n'héritant pas de STANDARD_POD (blocker committee #2).
  • Audit n_jobs : corriger s18_step3_parity:556 (-1), vérifier s28_windowing, TwoStage.
  • Confirmer le backend BLAS (threadpoolctl) une fois.
  • Auditer les specs sans resources.limits.cpu (sinon fail-loud bloque le démarrage).
  • A6 détection livrée avant de songer au défaut skip_phase_a=true.
  • Étendre le catalogue d'events Loki (pod_thread_caps_unsafe, s18_replay_*).
  • Déploiement phasé (WARN → observe → fail-loud).

5. Références