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 RCAcost_scenariono_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 #1134 — hors gate S12 (cf. §5) |
| Q3 — garde de coût | seuils + pause/alerte avant le premier gros sweep runtime — gate 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=None → sû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_cacheest 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 —
- l'objet modèle fitté n'est jamais posé sur le
VariantResult(getattr(result,"model",None)→None, ablation_runner.py:1582) ; - le fallback session-cache est mort (mauvais nom
get_from_sessionvs_get_from_session; cléslast_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_variant — model = 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-77 — VariantResult (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é)¶
- #1133 (Story dédiée) : fix Option 1 + tests ADR-58 → le chemin runtime produit des données.
- 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.
- #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.
- 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¶
- RCA :
rca-cost-scenario-no-data.md· Hub : S12 · Epic : CVN-N001-EI - Issues : GH #1133 (Story) · #1134 (obs., séparée) · dépendance #1129
- Comité : OP Meeting 264 · session
9990485d