Skip to content

Plan de fix — chemin runtime base-model (#1133) + observabilité (#1134)

Dossier de plan (revu en plan_review, ADR-68) du correctif débloquant la classe runtime FTF. Fait suite à la RCA cost_scenario no_data. Le fix est porté par une Story de correctif dédiée sous l'Epic CVN-N001-EI (Q4 tranché) ; ce dossier est lié dans la nav S12 (analyse-de-record). Bugs : GH #1133 (cause/fix runtime) · #1134 (observabilité, issue séparée). État live = OpenProject (ADR-76).

0. Revue de plan — verdict comité (2026-06-08)

plan_review PASSED · code=EXECUTION_RISK — 5 experts, consensus strong (ml-eng 8.0, data-sci 7.5, architect 7.5, ops 7.0, crypto-trader 7.0). Session 9990485d · OP Meeting 264 (ADR-82).

Résolutions actées (toutes les questions §8 sont tranchées ici ; §8 ne garde plus rien de vivant) :

Sujet Résolution Où c'est porté
Q2 — peuplement return_model: bool = False ; result.model peuplé seulement pour le runtime → réduit le blocker à un seul chemin (cf. §0bis) §3 lignes 1-2
#1134 — mécanisme stdout des pods compute = primaire (scrape fiable) ; log_event()/OTLP en complément ; caractériser le volume issue séparée #1134hors gate S12 (cf. §5)
Q3 — garde de coût seuils + pause/alerte avant le premier gros sweep runtimegate d'entrée Phase 6, pas un gate de #1133/S12 (la re-validation est un petit run) issue séparée (Phase-6)
ADR-58 test e2e étendu aux 4 facteurs runtime (cost_scenario, slippage_model, funding_rate, order_type) §3 ligne 5
Obs/ADR-25 seuils d'alerte Loki + runbook sur base_model_no_model / baseline_compute_failed issue #1134
Q4 — scope Story Story de correctif dédiée sous l'Epic EI ; S12 devient blocked-by cette Story, ne la contient pas en-tête + §9

0bis. Le blocker sérialisation se réduit à UN chemin (grâce à Q2)

return_model=True uniquement sur le runtime ⟹ les 4 VariantResult du chemin training portent model=Nonesûrs même sérialisés. L'audit n'est donc pas « VariantResult jamais picklé nulle part » mais « le VariantResult runtime s'échappe-t-il du process avant sa consommation in-process à _run_runtime_variant:1444 ? » — un seul chemin à auditer.

Call-site de train_weighted_variant return_model Le VariantResult (objet) s'échappe-t-il du process ? Verdict
_run_training_variant (:1226) False (défaut) ✅ sûr par construction (model=None)
train_fold_weighted (regime_trainer:1001) False ✅ sûr (model=None)
s18_step0_replay (:355) False ✅ sûr (model=None)
tests False ✅ sûr
_train_base_model (:1548) → runtime True à auditer : pickle / XCom / valeur de retour de task ? ⚠️ le seul à vérifier

Si le blocker firait : Option 2 ne sauve pas non plus. _session_cache est un attribut de classe (cvntrade_cache_interface.py:52) = état process-local — il ne traverse pas les pods. Le seul fallback cross-process est MLflow Model Registry (stocker le modèle, passer un ID). C'est donc « Option 1 ou MLflow », jamais Option 2.

1. Contexte & problème (résumé — détail dans la RCA)

Le run de validation de l'incrément 2 est revenu vide (0 results, 0 baselines, verdict=no_data). Cause confirmée : le chemin runtime partagé _train_base_model est cassé par construction —

  1. l'objet modèle fitté n'est jamais posé sur le VariantResult (getattr(result,"model",None)None, ablation_runner.py:1582) ;
  2. le fallback session-cache est mort (mauvais nom get_from_session vs _get_from_session ; clés last_trained_* jamais écrites, :1593-1601).

Bug pré-existant (#491, 2026-04-10), #1117 exonéré, de classe (les 4 facteurs Phase 6 runtime morts à l'identique). Racine organisationnelle : absence du test d'intégration ADR-58 pour la classe runtime.

2. Approche retenue

Option 1 — exposer le modèle (conditionnellement) sur VariantResult + supprimer le fallback mort.

Le modèle existe en local dans train_weighted_variantmodel = train_result.get("model") (regime_trainer.py:840), déjà utilisé in-process à :881 — il suffit de le renvoyer. Consommé in-process par _run_runtime_variant (:1444) → aucune sérialisation/XCom. Option 2 rejetée (cf. §0bis : ne survit pas à un blocker cross-process).

3. Fichiers à modifier (design résolu — return_model)

# Fichier:ligne Changement
1 regime_trainer.py — signature train_weighted_variant (626-641) Ajouter le paramètre return_model: bool = False. Défaut False → comportement des 4 chemins training inchangé.
2 regime_trainer.py:44-77VariantResult (dataclass non-frozen) Ajouter 3 champs optionnels : model: Optional[Any] = None, feature_names: List[str] = field(default_factory=list), training_config: Dict[str, Any] = field(default_factory=dict). Ajout en fin → backward-compatible.
3 regime_trainer.py (~840 → return ~964) Population conditionnelle : if return_model: renseigner result.model/feature_names/training_config depuis les objets déjà calculés. Sinon laisser les défauts (None/[]/{}).
4 ablation_runner.py:1548 (call-site runtime) Passer return_model=True ; les 4 autres call-sites restent au défaut.
5 ablation_runner.py:1582 getattr(result,"model",None)result.model (+ feature_names, training_config).
6 ablation_runner.py:1593-1601 Supprimer le fallback session-cache mort. Garder le guard model is None → event=base_model_no_model → return None.
7 Tests (nouveaux) (a) Intégration ADR-58 : un facteur de chaque des 4 runtime e2e sur fixture minimale → base_model obtenu, results non vide, finetune_baselines peuplé. (b) Unit : train_weighted_variant(..., return_model=True).model is not None et return_model=False ⇒ model is None (verrouille la sécurité du blocker) ; _train_base_model renvoie model_dict["model"] non-None.

4. Observabilité #1134 — issue séparée, hors gate S12

Le driver réel de #1134 n'est pas cette re-validation — c'est la visibilité prod des gardes #1117 (baseline_basis_mismatch/baseline_compute_failed), permanente, et de tout pod compute (portée plus large que le runtime). Mécanisme à caractériser (contraste par classe de pod ; le pod_override STANDARD/HEAVY), fix primaire = stdout, OTLP en complément. Ship sur sa propre piste, ne gate pas S12 (cf. §5).

5. Séquencement (découplé)

  1. #1133 (Story dédiée) : fix Option 1 + tests ADR-58 → le chemin runtime produit des données.
  2. Re-validation S12 = #1133 + (si échec) logs Airflow per-pod — il vient d'être prouvé qu'ils suffisent au diagnostic. Loki n'est requis pour aucune issue.
  3. #1134 indépendant, sur sa piste (visibilité prod des gardes). Optionnel : un #1134-minimal si l'on veut cette re-run diagnosable-dans-Loki — mais jamais le #1134-complet en gate Tested.
  4. Garde de coût Phase-6 : avant le premier gros sweep runtime (pas avant la petite re-validation).

6. Analyse d'impact

Débloque : S12 → Tested ; toute la classe runtime / Phase 6 ; #1129 (slippage_model runtime → blocked-by #1133 ; confirme le report (b)).

Rayon d'explosion #1133 — contenu : 5 call-sites de train_weighted_variant, un seul (_train_base_model) passe return_model=True / lit les nouveaux champs ; les 4 autres au défaut → non impactés (cf. table §0bis).

Risques :

Risque Sévérité Mitigation
Le VariantResult runtime s'échappe du process (pickle/XCom) Blocker (cf. §0bis) Audit du seul chemin runtime ; si firait → MLflow Registry (pas Option 2).
Les facteurs runtime vont réellement tourner Moyen (coût) Garde de coût Phase-6 (issue séparée) avant tout gros sweep.
#1134 (stdout) touche le logging des pods compute Moyen Caractériser ; tester le volume (anti-duplication).

Si on ne fait pas : Phase 6 silencieusement no_data ; #1129 invalidable ; gardes #1117 invisibles.

7. Critères de succès — séparés par piste

S12 → Tested (preuve SQL seule — Loki non requis) : - Re-run cost_scenario (defi_top5) : total_runs > 0, finetune_baselines peuplé. - Preuve per-cost : (b0) COUNT(DISTINCT cost_bps) ≥ 2, (b1) 0 ligne orpheline sur le join (run_id,crypto,fold,cost_bps). - Non-régression du chemin training : métriques inchangées sur une cellule de référence à épingler à l'impl = une cellule training-factor d'un run vert récent sur un crypto defi_top5 (p.ex. calibration/UNIUSDC), run_id capturé dans la PR (sinon invérifiable).

#1133 (code) : make test-unit vert + test d'intégration ADR-58 vert (les 4 runtime e2e) + le test return_model=False ⇒ model is None.

#1134 (piste séparée, PAS un critère S12) : event=run_start/skip_fold/baseline_* visibles dans Loki (chemin stdout) sur un petit run.

8. Questions résiduelles

Toutes tranchées — voir §0 (Q2, Q3, #1134, ADR-58) et §0bis (blocker), Q4 = Story dédiée. Plus rien de vivant.

9. Scope Story (Q4 tranché)

Fix porté par une Story de correctif dédiée sous l'Epic CVN-N001-EI (3 experts + opérateur) : c'est un fix niveau-Epic / classe Phase-6 dont S12, #1129 et les 4 facteurs runtime dépendent → S12 est blocked-by cette Story, ne la contient pas (sinon S12 ne clôt pas proprement et #1129 référencerait un commit interne-S12). Scopes propres : #1134 = sa propre issue (obs. infra, tous pods compute) ; garde de coût = issue Phase-6 séparée. Emplacement-doc (nav S12) ≠ scope-Story (entité EI).

Liens