Skip to content

V37 — ML Lab : la vitesse, et le confort des longues sessions - #55

Merged
dapiced merged 1 commit into
mainfrom
claude/labml-detailed-plan-nzc98m
Aug 23, 2026
Merged

V37 — ML Lab : la vitesse, et le confort des longues sessions#55
dapiced merged 1 commit into
mainfrom
claude/labml-detailed-plan-nzc98m

Conversation

@dapiced

@dapiced dapiced commented Aug 23, 2026

Copy link
Copy Markdown
Owner

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 :

k-NN tel que livré avant k-NN corrigé
séquentiel 73 950 ms 14 654 ms
parallèle 69 738 ms 9 509 ms

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 : predict est une closure et le structured clone abandonne les fonctions. Chaque helper renvoie donc toJSON() 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çait this.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 Float64Array plat — les égalités gardent la ligne vue en premier, exactement ce que faisait le tri stable — et un predictWithProba explicite 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.featureColumns que lit V21.

Deux défauts latents, trouvés par l'instrumentation

  • Latence à 0 ms. Les familles entraînées dans un helper héritaient de la latence renvoyée par le helper, soit zéro. La colonne aurait affiché <0,1 ms pour exactement les familles parallèles.
  • Export cassé. rebuildTrainedModel produisait un prédicteur sans toJSON, 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 toJSON du 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 de text.spec.ts resserrés sur la ligne du tableau, puisque le pied de leaderboard mentionne désormais aussi les noms de familles.


Generated by Claude Code

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
@dapiced
dapiced marked this pull request as ready for review August 23, 2026 05:20
Copilot AI lite review requested due to automatic review settings August 23, 2026 05:20
@dapiced
dapiced merged commit 752f616 into main Aug 23, 2026
4 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 predictWithProba to 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,
},
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants