V37 — ML Lab : la vitesse, et le confort des longues sessions - #55
Merged
Conversation
La vague s'ouvre par une mesure, comme le PLAN l'exigeait — et la mesure a retourné la vague. Entraînement parallèle. Les familles lourdes partent sur des cœurs d'appoint. Un modèle ne traverse pas une frontière de worker (`predict` est une closure), donc chaque helper renvoie `toJSON()` SOUS FORME DE CHAÎNE JSON et l'appelant reconstruit par le chemin d'import V22. La chaîne n'est pas un détail : envoyer l'objet laissait le structured clone conserver des formes que JSON abandonne, et `load()` de ml-cart reconstruisait alors un arbre dont la première prédiction lançait `this.root.classify(...).maxRowIndex is not a function` — un défaut qui ne serait apparu que plus tard, à l'ouverture des explications. Répartition par coût mesuré (LPT glouton), k-NN reste au centre (seule famille sans `toJSON`, et seule à s'ajuster en 0 ms), tout échec retombe silencieusement sur l'entraînement séquentiel : le parallélisme change la durée d'un run, jamais les modèles qu'il produit. La mesure a ensuite trouvé le vrai goulot, qui n'était pas l'entraînement. Sur 60 000 lignes, l'inférence k-NN coûtait 59,6 s sur 68,8 s de temps de mur — 87 % du run : la recherche de voisins allouait 5 000 objets et les triait pour chaque prédiction, et le scorer redemandait les mêmes voisins pour les probabilités. Corrigé par une insertion top-k bornée sur un Float64Array plat (les égalités gardent la ligne vue en premier, exactement comme le tri stable) et un `predictWithProba` explicite. L'ancienne implémentation triée est conservée telle quelle dans les tests comme oracle : le chemin rapide prédit à l'identique, ligne par ligne. Mesuré, même machine, même fichier de 60 000 lignes, même graine : séquentiel 73 950 ms → 14 654 ms avec le correctif k-NN ; parallèle 69 738 ms → 9 509 ms. Le parallélisme seul valait 1,06× ; le correctif k-NN 5,0× ; les deux ensemble 7,8×, tous les chiffres du leaderboard identiques. Aucun seuil de taille minimale : le gain est déjà positif à 1 000 lignes. Comparer plus de deux runs. De 2 à 6 runs lus par rapport au PLUS ANCIEN de la sélection, pour que les écarts disent ce qu'ont donné les changements de la session. Pas un second moteur de diff : la matrice est le classement V35 et les colonnes sont de l'algèbre d'ensembles sur `summary.featureColumns`. Deux défauts latents corrigés au passage : la latence d'inférence, mesurée ici sur le prédicteur reconstruit (elle aurait affiché 0 ms pour toutes les familles parallèles), et l'export d'un modèle entraîné dans un helper, qui ne fonctionnait plus faute de `toJSON` sur l'objet reconstruit. Descope assumé et nommé dans le PLAN : la reprise d'un run interrompu. Elle exigerait le pipeline persisté autant que les modèles, k-NN n'a pas de `toJSON`, et elle ne marcherait que pour les datasets conservés sous le budget V19 — une fonction qui marche pour certains runs et produit silencieusement un autre leaderboard pour les autres est pire que pas de fonction. 423 tests unitaires, 71 e2e. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UKw6oNC8iZ9Kn7q6x4qom4
There was a problem hiding this comment.
🟡 Changes recommended
There is at least one confirmed correctness issue in the new parallel reporting (helper count can be overstated when workers fail to spawn), and it should be fixed before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR delivers V37 for the ML Lab, focusing on (1) faster runs via parallel training orchestration and a major k-NN inference optimization, and (2) improved long-session ergonomics by enabling comparison of 3–6 saved runs in a single “session comparison” view.
Changes:
- Add best-effort helper-worker training for heavy model families, with deterministic batching and a leaderboard announcement of parallelism.
- Rework k-NN inference to avoid per-prediction allocations/sorts, and add
predictWithProbato avoid duplicate neighbor searches in scorers. - Add “compare many runs” data model + page, plus UI/history wiring and i18n strings.
File summaries
| File | Description |
|---|---|
| src/locales/fr.json | Adds FR strings for parallel-training announcement and multi-run comparison UI. |
| src/locales/en.json | Adds EN strings for parallel-training announcement and multi-run comparison UI. |
| src/features/ml/train/v37.test.ts | Adds unit tests locking in k-NN equivalence + worker boundary serialization guarantees + helper planning. |
| src/features/ml/train/types.ts | Extends TrainSummary with optional parallel metadata. |
| src/features/ml/train/trainer.ts | Adds pretrained-family reuse, adds detectTaskType, and uses predictWithProba in scoring. |
| src/features/ml/train/score.ts | Uses predictWithProba to avoid double work when available. |
| src/features/ml/train/parallel.ts | Introduces helper-count and batching planner plus measured-cost table. |
| src/features/ml/train/parallel.test.ts | Tests helper sizing, batching, determinism, and the “k-NN never leaves” rule. |
| src/features/ml/train/parallel-run.ts | Orchestrates helper workers, rebuilds models from JSON, and returns pretrained models + report. |
| src/features/ml/train/models.ts | Adds predictWithProba to TrainedModel and implements optimized k-NN. |
| src/features/ml/train/family.worker.ts | Adds helper worker implementation for training a subset of families and returning JSON-string serialization. |
| src/features/ml/train/deserialize.ts | Exports rebuildTrainedModel to rebuild and re-attach toJSON for helper-trained models. |
| src/features/ml/projects/compare-many.ts | Implements N-run comparison (2–6), aligned to shared ranking and reference-by-oldest semantics. |
| src/features/ml/projects/compare-many.test.ts | Tests ordering, deltas vs oldest, leader selection, holes in matrices, and feature set algebra. |
| src/features/ml/pages/MlCompareManyPage.tsx | Adds the compare-many UI page showing a multi-column matrix and feature diffs. |
| src/features/ml/data/parse.worker.ts | Integrates parallel training phase ahead of sequential training and records summary.parallel. |
| src/features/ml/components/RunsHistory.tsx | Allows selecting up to MAX_RUNS and adds a CTA to open compare-many when 3+ runs selected. |
| src/features/ml/components/LeaderboardTable.tsx | Announces helper-core usage + which families trained in parallel in the leaderboard footer. |
| src/app/router.tsx | Adds route for /ml/compare-many/:ids. |
| README.md | Updates ML Lab feature list and test-count blurb to include V37 capabilities. |
| PLAN.md | Marks V37 as delivered and documents the measured results and design constraints. |
| e2e/text.spec.ts | Tightens selector to click the leaderboard row (avoids footer text collisions). |
| e2e/parallel.spec.ts | Adds e2e coverage for parallel announcement and compare-many flow. |
Review details
- Files reviewed: 23/23 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+37
to
+41
| export function helperCount(families: readonly ModelKey[], cores: number): number { | ||
| const heavy = families.filter((key) => HEAVY_FAMILIES.includes(key)).length; | ||
| if (heavy < 2) return 0; // one heavy family in parallel is just overhead | ||
| return Math.max(0, Math.min(MAX_HELPERS, heavy, Math.max(1, cores - 1))); | ||
| } |
Comment on lines
+126
to
+130
| report: { | ||
| helpers: batches.length, | ||
| families: [...pretrained.keys()], | ||
| ms: performance.now() - started, | ||
| }, |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
La vague s'ouvre par une mesure, comme le PLAN l'exigeait — et la mesure a retourné la vague.
Les chiffres, d'abord
Même machine, même fichier de 60 000 lignes, même graine 42, quatre bras mesurés :
Le parallélisme seul valait 1,06× — du vrai travail, noyé par un goulot que personne n'avait mesuré. Le correctif k-NN seul : 5,0×. Le parallélisme par-dessus : 1,54×. Ensemble : 7,8×, avec tous les chiffres du leaderboard identiques.
Aucun seuil de taille minimale ne protège les helpers, parce que ça aussi a été mesuré : le gain est déjà positif à 1 000 lignes (2 919 → 2 375 ms), à 3 000 (6 479 → 5 446 ms) et à 8 000 (7 496 → 5 937 ms).
1. Entraînement parallèle
Les familles lourdes partent sur des cœurs d'appoint, réparties par coût mesuré, la plus lourde d'abord, vers le helper le moins chargé (LPT glouton). Le run annonce combien de cœurs ont aidé et quelles familles ils ont prises — comme toute autre décision du lab, jamais en silence.
Un modèle ne traverse pas une frontière de worker :
predictest une closure et le structured clone abandonne les fonctions. Chaque helper renvoie donctoJSON()sous forme de chaîne JSON et l'appelant reconstruit par le chemin d'import V22 — ce qui rend un modèle parallèle identique, octet pour octet, à un modèle importé.La chaîne n'est pas un détail. Envoyer l'objet laissait le structured clone conserver des formes que JSON abandonne, et
load()de ml-cart reconstruisait alors un arbre dont la première prédiction lançaitthis.root.classify(...).maxRowIndex is not a function. Un test le fige explicitement, pour que personne ne « simplifie » le protocole en repostant l'objet.Trois règles tenues : k-NN ne quitte jamais le worker principal (seule famille sans
toJSON, et seule à s'ajuster en 0 ms) ; tout échec — pas de Worker, un helper qui lance, une famille non sérialisable — retombe sur l'entraînement séquentiel ; la latence d'inférence est mesurée ici, sur le prédicteur reconstruit, parce que le chronomètre d'un helper décrirait un autre cœur en contention.2. La mesure a trouvé le vrai goulot, et ce n'était pas l'entraînement
Sur 60 000 lignes, l'inférence k-NN coûtait 59,6 s sur 68,8 s de temps de mur — 87 % du run. La recherche de voisins allouait 5 000 objets et les triait pour chaque prédiction, et le scorer redemandait exactement les mêmes voisins pour obtenir les probabilités.
Corrigé par une insertion top-k bornée sur un
Float64Arrayplat — les égalités gardent la ligne vue en premier, exactement ce que faisait le tri stable — et unpredictWithProbaexplicite pour les familles dont les deux réponses sortent du même calcul. L'ancienne implémentation triée est conservée telle quelle dans les tests comme oracle : le chemin rapide est asserté prédire à l'identique, ligne par ligne, en classification comme en régression.Résultat mesuré : le score k-NN passe de 59 564 ms à 707 ms, la latence de 1,3 ms à moins de 0,1 ms par ligne.
3. Comparer plus de deux runs
V21 compare deux runs ; de trois à six, la question change : « lequel de mes essais a vraiment marché ? ». Les runs sont lus par rapport au plus ancien de la sélection — là où la session a commencé — pour que les écarts disent ce qu'ont donné les changements, et non ce que vaut le dernier run.
Délibérément pas un second moteur de diff : la matrice par modèle est le classement V35 partagé, et les colonnes de variables sont de l'algèbre d'ensembles sur le même
summary.featureColumnsque lit V21.Deux défauts latents, trouvés par l'instrumentation
<0,1 mspour exactement les familles parallèles.rebuildTrainedModelproduisait un prédicteur sanstoJSON, donc le bouton « Modèle (JSON) » ne téléchargeait plus rien pour une famille entraînée en parallèle. Le parallélisme aurait discrètement retiré une fonctionnalité. Les deux e2e d'export l'ont attrapé.Descope assumé : la reprise d'un run interrompu
C'était le troisième point de la vague et il ne part pas, pour une raison énoncée plutôt que par manque de temps. Une reprise exige le pipeline persisté autant que les modèles ; k-NN n'a pas de
toJSONdu tout, donc le leaderboard repris perdrait une famille que le run interrompu avait ; et ça ne marcherait que pour les runs dont l'utilisateur a choisi de garder le dataset sous le budget de 50 Mo de V19. Une fonction qui marche pour certains runs et produit silencieusement un autre leaderboard pour les autres est pire que pas de fonction. La fenêtre d'exposition qu'elle protégeait a par ailleurs été divisée par 7,8 dans cette vague même.Ce que la vague ne fait délibérément pas
Mettre en cache les modèles entre les runs (la clé de cache est toute la config plus les données — un hit périmé est un leaderboard silencieusement faux), ni déplacer le scoring k-NN vers un helper (il coûte maintenant 700 ms, et le modèle doit de toute façon exister dans le worker principal pour les explications et le what-if).
Validation
lint+format:check+tsc --noEmit+ 423 tests unitaires (50 fichiers) + 71 e2e +build— tout vert. Deux e2e ajoutés (annonce des cœurs d'appoint sur titanic, comparaison de trois runs sur iris) ; deux sélecteurs detext.spec.tsresserrés sur la ligne du tableau, puisque le pied de leaderboard mentionne désormais aussi les noms de familles.Generated by Claude Code