Files
manifoldbt/docs/gpu-sweep-coverage-plan.md
T
2026-07-18 19:04:25 +00:00

273 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Plan — Couverture GPU du sweep (2026-07-17)
Contexte : le sweep GPU fait 300k combos/s (fp64 bit-identique) / 676k (fp32 scan),
mais seules les stratégies dans un périmètre étroit en profitent. Tout le reste
retombe sur le CPU (~1-20k combos/s) **en silence**. Ce plan séquence l'élargissement
du périmètre, du moins cher au plus cher, avec un gate data-driven au milieu.
État de départ : PR #74 (18 commits GPU) en attente de merge ; branche
`perf/python-metrics-extraction` (3 commits : dicts directs, `sweep_columns`,
fix du pool VRAM) prête. Les phases ci-dessous partent au-dessus.
Log de mesures : `gpu_funding_bench.md`. Toute nouvelle phase y ajoute sa section.
---
## État au 2026-07-17 : TOUTES les phases 0-5 shippées en 0.13.0
Phases **0, 1a, 1b, 2, 3, 4, 5 LIVRÉES** (voir chaque section + `gpu_funding_bench.md`
Parts 0-17). La 0.13.0 est publiée sur PyPI. Il ne reste que le backlog Phase 6.
**Reste à faire (Phase 6), chaque cas se nomme quand il retombe sur CPU :**
- **Brackets multi-asset** : le kernel `sweep_t_multi` n'a pas de bracket (seul le
mono l'a, cf. Part 17). Raison nommée : « exit orders on a multi-asset universe
(the multi kernel has no bracket) ».
- **Frais per-venue** (2 exemples) : extension du fast path.
- **Cross-sectionnel** (1) : `sweep_t_multi`, restructuration en 2 passes.
- **MTF** (1) : grilles multiples, mapping d'index par timeframe.
- **Multi-asset + dérivées** (1) : l'exo/dérivée est globale, layout `[n_cols][n_sym][num_bars]`.
**Branches en attente (hors périmètre GPU, à trier) :**
- `feat/import-dataframe` (1 commit) : import de barres depuis un DataFrame en
mémoire, sans round-trip CSV. Feature réelle, jamais mergée.
- `fix/python-test-suite` (2 commits) : branche pytest en CI + aligne les tests
golden/sweep. Vaut le coup (la CI ne teste pas pytest aujourd'hui), MAIS
rendrait la CI rouge sur le `test_sweep_returns_one_result_per_combo` golden
préexistant tant qu'il n'est pas réglé (plancher de sortie Pro).
---
## Les trois couches qui bloquent une stratégie
1. **Ops transpileur manquants** : `Eq, Lag, Lead, RollingStd, RollingSum,
RollingMin, RollingMax, Scan` (→ Kalman et tout indicateur à état custom).
2. **Périmètre fast-path** (11 conditions) : la plus coûteuse est probablement
`orders.is_empty()` — **toute stratégie avec SL/TP retombe**. Aussi :
AtClose only, FixedBps only, frais mono-venue, pas de borrow rate, etc.
3. **Garde-fous sweep** : MTF, exogène, multi-source, cross-sectionnel,
funding multi-asset.
Tri technique de la couche 3 : funding multi = ~15 lignes (terme déjà écrit pour
le mono) ; exogène = ~30 lignes (l'exo est ASOF-joint sur la grille de barres au
chargement → colonne ordinaire pour le kernel) ; cross-sectionnel = moyen (le
kernel multi a déjà tous les symboles dans le thread, restructuration en 2
passes) ; MTF = moyen-lourd ; multi-source = lourd.
---
## Phase 0 — Rendre la retombée visible (≈1 h) — À FAIRE EN PREMIER
**Problème** : `run_sweep_lite(device="cuda")` retombe en silence. L'utilisateur
paie Pro, voit 12k combos/s, ne saura jamais que retirer son SL/TP le rendrait
25x plus rapide. Et nous, on priorise les phases suivantes à l'aveugle.
**Livrable**
- La raison de retombée (déjà construite : `gpu-sweep-unsupported: <raison>`)
remonte à l'utilisateur : `warnings.warn` une fois par sweep, et champ
`profile["gpu_fallback_reason"]` sur les résultats.
- Les raisons sont des chaînes stables (elles le sont déjà) → agrégeables plus
tard en télémétrie si souhaité.
**Validation** : sweep avec SL/TP → warning avec la raison exacte ; sweep GPU
nominal → aucun warning, champ absent ; e2e inchangés.
**Gate de décision** : après quelques jours d'usage réel (et un tour des
exemples/notebooks), la fréquence des raisons décide l'ordre des phases 2-4.
Sans données, l'ordre ci-dessous est notre meilleure estimation.
---
## Phase 1 — Quick wins périmètre (≈½ journée)
### 1a. Funding multi-asset dans `sweep_t_multi` (~15 lignes, **FAIT** `bb4396c`)
Le terme funding est déjà dans `sweep_t` (mono) : `capital += -position * close
* rate` par barre, par symbole ici. Retirer le garde-fou
`"multi-asset funding not yet on the GPU kernel"`.
- **Validation** : colonne funding synthétique injectée, e2e multi bit-identique
CPU==GPU (même harnais que le fix funding mono).
### 1b. Colonnes exogènes dans le sweep GPU (~30 lignes, **FAIT** `0a695e4`)
`load_exo_columns` ASOF-joint déjà l'exo sur `master_timestamps` → un
`Float64Array` de `num_bars`, indistinguable de `close` pour le kernel. Étendre
la résolution des `col_names` du transpileur aux colonnes exo alignées, retirer
`!config.exo_data.is_empty()` du garde-fou (garder `exo_sources`/multi-source
exclus).
- **Validation** : e2e avec exo synthétique bit-identique CPU==GPU ; l'exemple
`16_hashrate_exogene.py` passe sur GPU ; NaN de tête d'ASOF (avant le premier
point exo) couverts par le test.
- **Attention** : chemin multi-asset — un exo est global (pas par symbole), le
layout `cols [n_cols][n_sym][num_bars]` duplique par symbole ou garde-fou
multi+exo conservé en V1.
---
## Phase 2 — Ops transpileur faciles (≈½ journée) — **FAIT 2026-07-17**
**Livré** : ZScore/RollingStd/RollingSum/RollingMean/Rsi/Roc sur colonne OU
expression pointwise (séries dérivées), Eq, RollingMin/Max (hoistés, rescan
exact), scan sans param → colonne hôte. **Refusé avec raison nommée** :
Lag/Lead — mur sémantique null-vs-NaN (le CPU garde la position sur cible
null, le kernel n'a qu'un encodage) ; il faudrait un canal de validité.
Détails : Part 14 du rapport.
`ZScore`, `RollingStd`, `Eq`, `Lag`, `Lead`, `RollingSum`, `RollingMin`, `RollingMax`.
Débloque : Bollinger (rolling_std), breakouts (rolling_max/min), signaux à
retard (lag), et retire un pan de retombées silencieuses.
**`ZScore` en tête de liste** : mesuré 2026-07-17, c'est ce qui bloque
`examples/16_hashrate_exogene.py` (la Phase 1b a débloqué son exo, mais il bute
maintenant sur `indicator ZScore is not supported`). ZScore est une op à part
entière dans bt-expr, pas un `rolling_std` déguisé — je l'avais manquée dans
l'inventaire initial ; c'est le warning de la Phase 0 qui l'a nommée.
**Mise à jour 2026-07-17 (suite)** : ZScore-sur-colonne est livré (`d883edd`),
mais l'exemple 16 fait `zscore(EXPRESSION)` et retombe encore : TOUTES les ops
fenêtrées du transpileur exigent une colonne brute (limite structurelle
préexistante — l'éviction `sum -= x[jj-w]` exige un accès aléatoire que le
streaming n'a pas ; seule l'EMA, sans regard arrière, accepte une expression).
Le correctif structurel est le **plan « séries dérivées »** :
[`gpu-derived-series-plan.md`](gpu-derived-series-plan.md) (~2-2,5 j, P0 = bug
NaN latent des folds in-thread multi-asset découvert par l'analyse).
**Mise à jour 2026-07-17 (fin de journée) : plan séries dérivées LIVRÉ
(P0-P5).** Le bug NaN du fold SMA in-thread est corrigé (e2e multi NaN-volume,
rouge-puis-vert), et `zscore(EXPRESSION)` tourne sur GPU : l'entrée pointwise
est matérialisée en lignes dérivées (kernel `derived_fill` généré par le même
emit), le fold z-score lit la ligne via `HINDAT` in-thread. Exemple 16 réel :
**102x** à 30k combos (0.43 s vs 43.6 s CPU), bit-identique. Rapport : Part 11
de `gpu_funding_bench.md`. Reste de la Phase 2 : brancher `RollingMean`/`Rsi`/
`Roc` sur le même chemin dérivé, puis les ops manquantes ci-dessus.
**Trou distinct découvert au passage** : une stratégie SANS périodes dynamiques
part dans `transpile_sizing` (pointwise uniquement) et meurt sur le premier
`EwmMean`, bien avant le pipeline. L'exemple 16 tel quel tombe là-dessus. Peu
grave pour un sweep (on paramètre les fenêtres, donc on passe par
`transpile_pipeline`), mais à garder en tête pour `run_batch`.
**Règles héritées de la campagne (non négociables)**
- Chaque fold miroir **bt-expr op-pour-op**, y compris NaN : le bug des folds
NaN (Part 5) venait exactement d'un miroir non ancré au moteur.
Pièges connus : `Eq` = `(a-b).abs() < f64::EPSILON` (pas `==`) ;
`RollingStd` : même formule de Welford/somme que bt-expr, même fenêtre sale.
- Tests de parité **contre bt-expr** (pattern `gpu_fold_nan_gaps_*`), jamais
contre un miroir du même algorithme.
- Hoisting : chaque nouveau kind hoistable rejoint `hoist_fill` (kind + period),
sinon état par thread via `state_decls` — attention au **cap 48 registres**
(ptxas -v systématique, la table des résultats négatifs Parts 4-5 fait foi).
---
## Phase 3 — `scan` sur GPU → Kalman — **FAIT 2026-07-17**
**Livré** : gate 3a passé (bande Kalman dépliée + fold = 94 regs, zéro
spill) ; `lower_scan_tape` extrait dans bt-expr (une bande, deux exécuteurs) ;
traduction 1:1 ScanOp→CUDA (Eq EPSILON du VM répliqué, ≠ du == array) ;
`derived_fill` séquentiel par ligne → fenêtré-sur-scan-sweepé OK (dédup par
bits résolus) ; fix param()-dans-scan (has_dynamic_periods + collecteur
Python). Surface (q,w) 1M réelle : 25.8 s. Refus nommés : log/exp, inits
non-pointwise, colonnes à nulls. Phase 5 (dispatch auto) : device="auto",
seuil mesuré 1k combos, Part 15.
## Phase 3 (plan d'origine, conservé pour référence)
### 3a. Sonde registres (30 min, GATE)
Écrire À LA MAIN le kernel Kalman (bande ScanOp dépliée en CUDA), `ptxas -v`
sm_86. Le kernel sim est à 48/48 registres **zéro marge** : si la bande scan
spille, le gain s'évapore → **stop, on documente, on ne construit pas le
générateur**.
### 3b. Générateur ScanOp → CUDA
La bande `CompiledScan.instructions` est en SSA plat (24 opcodes scalaires) →
traduction mécanique, 1 ligne CUDA par opcode, branchée sur
`state_decls`/`state_init`/`body` existants.
Sémantiques à mirrorer exactement (source de vérité : `evaluator.rs`) :
- `Div` : dénominateur `== 0.0`**NaN** (pas l'inf IEEE)
- `Eq` : `(a-b).abs() < f64::EPSILON`
- init : `prev_state[i] = init_arr.value(0)`, null → `0.0`
- ordre d'exécution : registre = pointeur d'instruction, writeback après output
### 3c. `param()` dans un nœud scan (bug indépendant, petit)
`CompiledScan.param_names` existe déjà ; c'est la **découverte des paramètres de
stratégie** qui ne descend pas dans les nœuds scan ("uses undefined parameters:
q"). Fix côté validation/collecte → débloque le sweep de `q` (la dimension la
plus intéressante du filtre), même sur CPU.
### 3d. Acceptance
- Parité GPU vs VM bt-expr bit-identique, NaN gaps inclus.
- e2e sweep Kalman CPU==GPU bit-identique.
- **La heatmap 1M Kalman (w×en, puis q×en) sur GPU** — l'objectif d'origine.
Budget attendu : quelques secondes au lieu de 15 min CPU.
- fp32 : vérifier le ranking (le scan ajoute des Div/Sqrt → sensibilité à
mesurer, pas à supposer).
---
## Phase 4 : SL/TP sur le fast path GPU (**FAIT 2026-07-17**)
**Livré** : brackets (stop-loss / take-profit / trailing) simulés dans le kernel
CUDA `sweep_t`, transcription 1:1 des phases B1/B0 de `sim_fast_lite_core_single`
(la référence CPU, elle-même tenue à `run()` par `backtest_orders.rs`). Gate
`allow_exits` sur `fast_path_blocker` (mono-asset seulement) ; OHL packés en un
buffer (arité cudarc). **5.3x vs CPU à 24k combos**, GPU==CPU exact. Le trou le
plus gros de l'audit exemples (5/18) est bouché → **exemples 7→10/18 sur GPU**.
Deux bugs livrés trouvés en route : signe de `max_drawdown` inversé sur tout
chemin lite, et ~360 lignes de bracket lite inatteignables. Détail : Part 17.
Design d'origine (conservé) : résolution intra-bar SL/TP = high/low + sémantique
de priorité fixée à la 0.12.0 côté lite CPU.
## Phase 5 — Dispatch GPU/CPU (à faire À LA FIN)
**Mesuré 2026-07-17** : sur une grille de 100 combos, le GPU est **6x plus LENT**
que le CPU (882 vs 5 327 combos/s) — les coûts fixes (upload, launch, hoist,
compile) écrasent le gain. À 6 000 combos le GPU gagne 7x. Il existe donc un
seuil de rentabilité, et aujourd'hui l'utilisateur qui passe `device="cuda"` sur
une petite grille **paie pour être ralenti, sans rien qui le prévienne**.
Deux options, à trancher le moment venu :
1. **Bascule automatique** sous un seuil (attention : `device="cuda"` explicite
qui exécute sur CPU, c'est exactement le genre de magie silencieuse que la
Phase 0 vient de supprimer — il faudrait le dire).
2. **Warning symétrique** : « grille trop petite pour amortir le GPU (N combos,
seuil ~M) », et laisser l'utilisateur décider.
Le seuil dépend de num_bars, n_sym et de la complexité du pipeline : à calibrer
par mesure, pas à coder en dur (cf. [[feedback_portability_first]] : pas de
seuil spécifique à une machine). Placé en fin de plan volontairement : c'est du
polish, les phases 1-3 débloquent des stratégies entières.
## Phase 6 — Backlog (ordre selon Phase 0)
- Cross-sectionnel dans `sweep_t_multi` (2 ops, restructuration 2 passes).
- Exo en multi-asset (broadcast de la série globale dans chaque slab symbole).
- MTF (grilles multiples, mapping d'index par timeframe).
- Multi-source / cross-exchange (chemin général, lourd).
- Frais par venue, borrow rate, AtOpen (extensions du fast path).
- `transpile_sizing` (chemin sans params) limité au pointwise → bloque `run_batch`
sur toute stratégie à indicateurs.
---
## Invariants transverses (toutes phases)
1. **Bit-identité fp64 CPU==GPU** : chaque phase ajoute ses cas au harnais e2e
(`to_bits` par métrique). Les checksums 21-métriques du bench servent de
non-régression (`14fabff006fda0a0` fp64 / `c1be6db787e2f49f` fp32 sur la
grille de référence).
2. **Parité ancrée à bt-expr**, jamais à un miroir (leçon Part 5).
3. **ptxas -v à chaque changement de kernel** (cap 48 regs, spills interdits).
4. **Un garde-fou ne se retire qu'avec le test qui prouve le remplacement.**
5. **Chaque phase = sa branche + sa PR + sa section dans le bench log.**
Livraison incrémentale, pas de méga-branche de 18 commits une deuxième fois.
## Estimation d'ensemble
| Phase | Effort | Gain |
|---|---|---|
| 0 — retombée visible | ~1 h | priorisation data-driven + UX Pro |
| 1 — funding multi + exo | ~½ j | 2 familles débloquées, quasi sans risque |
| 2 — 7 ops faciles | ~½ j | Bollinger/breakout/lag sur GPU |
| 3 — scan → Kalman | 1-2 j (gate 30 min) | la stratégie de l'article sur GPU + échappatoire universel |
| 4 — SL/TP | gros | à chiffrer après Phase 0 |