Skip to content

perf(lean,#16031): life_compose caches de trajectoire + hoists byte-equivalents (test_life_compose 19.15s -> 10.1s) - #17347

Merged
myia-ai-01 merged 1 commit into
mainfrom
feature/16031-life-compose-perf
Sep 23, 2026
Merged

myia-ai-01 merged 1 commit into
mainfrom
feature/16031-life-compose-perf

Conversation

@jsboige

@jsboige jsboige commented Sep 22, 2026

Copy link
Copy Markdown
Owner

Grain: MED/refactor — lane myia-po-2023:CoursIA — prev: DEEP/notebook-python #17343

perf(lean,#16031) : caches de trajectoire + hoists byte-équivalents dans life_compose.py — test_life_compose.py 19,15 s → 10,1 s (−47 %). Tranche 6 de #15635, véhicule neuf disjoint de #15863 (tranche 5, life_components.py).

Preflight indépendant (acceptance 1) — AVANT toute modification

python -m pytest scripts/lean/tests/test_life_compose.py -q --durations=10 sur worktree propre à e8d76767e3 (main du 2026-09-22), Python 3.13.3 :

  • 27 passed en 19,15 s (wall 22 s), rc=0
  • Deux tests = 98,7 % du coût : test_reutilisation_stricte_impossible_borne 13,24 s (69 %) + test_variante_libre_found 5,66 s (30 %) — confirme le diagnostic préflight po-2025 (perf(lean,#15635): trajectoire cumulative dans measure_motif — tranche 5 perf #15863 c.1116) : le coût est dans life_compose.py, pas dans test_life_components.py (~0,2 s mesuré ici).
  • La moitié « classe po-2024-linux-docker » de l'acceptance 1 se lit sur le run Scripts Tests (CPU) de CETTE PR (même classe runner) ; le poste local est mesuré ci-dessus.

Diagnostic cProfile (acceptance 2)

python -m cProfile -o sur chacun des deux tests dominants, analyse pstats :

Hotspot stricte (26,9 s profilés) libre (15,7 s)
_enumerate_events self 6,51 s 0,27 s
certify cum (dont step 4,48 s) 6,73 s —
_extent_grid cum (44 800 appels, re-materialisé par combo de phases) 3,43 s —
cells_at cum (653 k / 1,07 M appels, non mémoïsé) 2,10 s 3,07 s
_min_chebyshev cum (paires re-vérifiées à chaque candidat) 1,24 s 5,34 s (376 k appels)
genexprs bindings/gliders (2,3 M + 3,8 M appels d'éléments) ~1,5 s —

Causes : (a) _extent_grid et le compte de gliders ne dépendent que de state mais étaient recalculés par combo de phases/candidat ; (b) cells_at est une fonction pure du contenu mais non mémoïsée — la même paire géométrique est recontrôlée pour chaque candidat suivant ; (c) le rectangle « ever » de fenêtre était reconstruit par candidat dans _surface_ok et _spatial_ok ; (d) le scan+tri de clearance tournait à chaque pas même sans violation.

Modification (1 fichier : scripts/lean/life_compose.py, +195/−52)

  1. Caches de trajectoire (_CELLS_AT_CACHE, _SWEPT_RECT_CACHE, _WINDOW_EVER_CACHE, _PAIR_CHEB_CACHE) : clés de contenu (_handle_content_key : motif, ancre, phase, naissance, cellules — jamais event_idx), garde de taille 400 k entrées (l'éviction recompute la même valeur), même convention de stabilité par motif_id/reaction_id que les caches existants _PHASE_OFFSET_CACHE/_WINDOW_CACHE. cells_at rend une copie fraîche : type de retour et mutabilité observés inchangés.
  2. _pair_chebyshev / _window_pair_chebyshev : distance de paire mémoïsée par contenu — la distance d'une paire au même instant ne peut pas changer entre deux contrôles.
  3. Hoists _enumerate_events : _extent_grid(state) et compte de gliders de state calculés une fois par énumération (la liste d'offsets est partagée en lecture, jamais mutée) ; spawn_pos remplace spawn_slots.index(i).
  4. _search : réutilise le consumed rendu par l'énumérateur (le set était reconstruit dans la condition pour CHAQUE handle vivant).
  5. certify : clearance scannée avec any() d'abord — le scan complet + tri ne sert qu'en cas de violation (rare), le contenu de l'entrée produite est inchangé.
  6. run_compositional : tous les caches vidés par rep — chaque rep reste la mesure d'une recherche froide, comme _WINDOW_CACHE.

test_life_compose.py non touché. life_components_fixture.json non touché. life_synthesize.py (moteur baseline #15571) non touché.

Acceptance stricte — 6 points (analogue #15863)

  1. ✅ Évolution/cache sûr par contenu, sans changer le premier résultat trouvé ni les verdicts : caches cléés sur le contenu intégral dont la valeur dépend ; les 8 rapports de référence (ci-dessous) sont byte-identiques, y compris compteurs de pruning (r1_anchor_collapsed, r2_*, r3_envelope, r4_phase, model_anchor, budget, replay_rejected…) — le hoist _extent_grid/gliders préserve les décomptes car len(offsets) et les valeurs sommées sont identiques.
  2. ✅ Même nombre de tests collectés, aucun skip/split/-x : 65 collectés (27 test_life_compose + 38 test_life_components), tous passent, fichiers de test inchangés.
  3. ✅ Rapports byte-équivalents et contrôles négatifs inchangés : batterie de 8 rapports complets (run_compositional reps=1 : two_blocks, two_blocks_catalyse, two_blocks_catalyse_free, 4 ablations r1–r4, bord TIMEOUT node_budget=1) dumpés en JSON avant (e8d76767e3) et après (7219b3a497) — diff vide : verdicts, stats compteur par compteur, témoins (12 113 bytes identiques des deux côtés). Les contrôles négatifs (replay falsifié, clearance voyou, budgets serrés, TIMEOUT≠IMPOSSIBLE) passent inchangés.
  4. ✅ Refactor sans changement observable pour le caller : signatures publiques inchangées (cells_at, swept_rect, certify, Searcher._spatial_ok appelé par les tests avec la même signature), comportement d'erreur identique, mêmes verdicts/témoins.
  5. ✅ Aucun changement workflow/timeout/labels : confirmé — aucun fichier .github/ touché.
  6. ✅/⏳ Scripts Tests (CPU) SUCCESS sous cap 20 min post-merge : attendu sur le run CI de cette PR (même classe runner) puis sur main post-merge — la dissipation du timeout po-2024-linux-docker dépend de la capacité de base (61/61 timeouts, mesure ai-01 c.1116 fin) : cette PR réduit la part du fichier de −47 %, elle ne garantit pas à elle seule la dissipation d'un timeout de capacité.

Benchmark avant/après (acceptance 4 — même runner, même commande)

Métrique Avant (e8d76767e3) Après (7219b3a497) Δ
test_reutilisation_stricte_impossible_borne 13,24 s 5,41 s −59 %
test_variante_libre_found 5,66 s 3,89 s −31 %
pytest scripts/lean/tests/test_life_compose.py (27 tests) 19,15 s ~10,1 s −47 %
test_life_components.py (38 tests, contrôle) ~0,2 s ~0,2 s ≈×1

Résiduel identifié non consommé (hors scope déclaré) : step() du moteur life_synthesize (#15571) ≈ 4,5 s profilés dans certify — toucher le moteur baseline partagé est un véhicule séparé, pas un élargissement silencieux de celle-ci.

Anti-régression (CLAUDE.md §D)

Aucune implémentation remplacée par un stub : le diff est additif (caches + hoists), chaque branche de calcul d'origine est conservée sur miss de cache. Insertions (195) > délétions (52), aucune suppression de preuve ni de contrôle négatif.

Scope disjoint (issue #16031)

See #16031 (acceptance 6 = vérification post-merge par nature : le numéro du run main sera cité en commentaire à l'observation ; fermeture sur cette preuve).

Références

🤖 Generated with Claude Code

…quivalents (19.15s -> 10.1s)

- cells_at/swept_rect memoises par cle de contenu (jamais event_idx),
  copie fraiche rendue : type et mutabilite observes inchanges
- _pair_chebyshev/_window_pair_chebyshev : distance de paire memoisee,
  la meme paire geometrique etait recontrolee pour chaque candidat
- _window_ever_rect : rectangle de fenetre reconstruit par candidat ->
  cache par (reaction.id, offset)
- _extent_grid + compte de gliders hoistes hors des boucles (ne
  dependent que de state)
- certify : clearance scannee avec any() avant tri (hit construit
  seulement en cas de violation)
- run_compositional : tous les caches vids par rep (mesures froides)

Preuve byte-equivalence : 8 rapports complets (3 objectifs + 4
ablations + bord TIMEOUT) identiques avant/apres -- verdicts, stats
compteur par compteur, temoins. 65 tests passent inchanges (27
life_compose + 38 life_components).

Benchmark (meme runner, pytest --durations) :
- test_reutilisation_stricte_impossible_borne : 13.24s -> 5.41s (-59%)
- test_variante_libre_found : 5.66s -> 3.89s (-31%)
- fichier test_life_compose.py : 19.15s -> 10.1s (-47%)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@jsboige

jsboige commented Sep 22, 2026

Copy link
Copy Markdown
Owner Author

Rouge Scripts Tests (CPU) herite de la base -- justification ecrite (protocole rouge non reparable par la lane)

Le run 35674498995 (rejeu, complete 2026-09-22T01:55:28Z) rend 14 failed, 14453 passed, 98 skipped, 8 xfailed. Les 14 echecs sont TOUS la classe environnement runner, aucun dans le perimetre de cette PR :

  • 4x scripts/audit/tests/test_check_orphan_merged_pr.py -- FileNotFoundError: [Errno 2] No such file or directory: 'gh'
  • 10x scripts/tests/test_guard_gauntlet.py -- BASELINE_FAILED / check_exit: 127 (commande introuvable, meme classe)

Le runner CoursIA-runners-p0/slot-1 n'a pas le binaire gh dans le PATH. Les tests scripts/lean/tests/test_life_compose.py + test_life_components.py (perimetre de la PR) sont dans les 14453 passes.

Corroboration que c'est la base, pas cette PR : les guards du meme commit echouent aussi sur des fetch API CI (closingIssuesReferences fetch failed), le golden-set a rendu ModuleNotFoundError: numpy sur des notebooks preexistants d'autres PRs (01:20-01:28Z), et le picker signale les memes rouges sur #17325/#17327/#17328/#17329/#17332. Fenetre complete documentee sur le dashboard workspace CoursIA ([INFO] c.32 suite).

La lane ne revoit pas ces runs (chaque rejeu re-tombe sur le pool degrade) : reparation = cote runner (installer gh sur slot-1 / retablir l'env golden), unique reparateur = coordinateur. Un re-run sera lance des que le pool est sain.

@jsboige

jsboige commented Sep 22, 2026

Copy link
Copy Markdown
Owner Author

Rerun des jambes rouges effectue par la lane (04:0xZ) — verdicts : Always-on guards -> success (le rouge perimeter/fastlane etait bien le runner sans gh, corrige par re-routage), Scripts & Notebook-Tools Tests -> failure sur XDIST-WATCHDOG: BLOQUE -- silence de sortie depuis 480 s apres Fatal Python error: Aborted en plein run (classe hang #16288, la famille documentee po-2025 c.102 : jobs tues en vol sur slots WSL, 0 tentative arrivee sur docker en 4 essais). Aucun test de contenu n'a echoue — le job s'est tu au milieu. La lane ne peut pas reparer un hang de runner : justification ecrite pour --ignore-red, le sweep pr-gate-stale re-routera quand un slot docker se libere. Aucun push effectue (DWELL non rearme).

@jsboige

jsboige commented Sep 22, 2026

Copy link
Copy Markdown
Owner Author

[Échappatoire non-réparable par la lane — Scripts Tests (CPU)]

Rouge redocumenté ce cycle avec 2 rejeus supplémentaires (attempts 2 et 3 du run 35674498995), même signature à chaque fois :

La cause est le runner (hang WSL), pas le diff : rien à corriger côté branche, et un push ne changerait rien (re-route non garanti). La réparation qui ferme la classe — router Scripts Tests (CPU) sur la famille docker ou installer l'env WSL manquant — est une tâche coordinateur/infra, déjà signalée (dashboard, runners po-2026-wsl-1/2 et ai-01-wsl dégradés).

La lane poursuit sa file ; cette candidate attend un Scripts Tests vert par rejeu sur runner sain ou fix infra.

@jsboige

jsboige commented Sep 22, 2026

Copy link
Copy Markdown
Owner Author

Rouge d'infra runner, pas un defaut du diff — Scripts Tests (CPU) a echoue sur :

failure: The self-hosted runner lost communication with the server.

Annotation du job 106637215483 (run 35674498995), arbre intact, aucun step de test en echec. Le PR gate ne fait que relayer ce seul enfant : [pr-gate] FAIL -- failing checks: Scripts Tests (CPU) (failure). Job relance.

@myia-po-2023

Copy link
Copy Markdown
Collaborator

Diagnostic du rouge Scripts Tests (CPU) (run 35674498995, job 106700007882, 2026-09-22T10:03-10:08Z) : le rouge n'est pas porte par le diff, et le mecanisme est lisible dans le log.

[83%]  ....[gw2] node down: Not properly terminated
       F
       replacing crashed worker gw2
[89%]  ..........Fatal Python error: Aborted

Un worker xdist meurt, pytest le remplace, la suite repart (83 -> 89 %) puis le master avorte. Les gardes de plancher de couverture qui suivent (--collect-only returned no tests — the collection crashed) sont une consequence, pas la cause : elles constatent que la session est morte.

La classe est deja mesuree dans le workflow lui-meme. L'en-tete de .github/workflows/scripts-tests.yml (l.141-146, mesure du 2026-09-15T07:00Z) porte le tableau : sur 45 runs porteurs d'un job, 30 success / 11 failure / 4 cancelled. Ce job echoue donc environ un quart du temps sur ce parc. Le chien de garde xdist (#16288) ne couvre pas ce variant : il tire quand la sortie se tait, alors qu'ici elle progressait — le master a avorte avant tout silence.

Verification firsthand que le module de la PR est sain (po-2023) :

python -m pytest scripts/lean/tests/test_life_compose.py -q
   -> 27 passed in 18.19s

scripts/lean/life_compose.py n'appelle aucun sous-processus : grep -nE "subprocess|lake|lean |Popen" ne rend que def run(self) (l.1032) et deux appels searcher.run() (l.1286, l.1399). Le test importe des modules Python purs. Aucun mecanisme ne relie donc ce diff a la mort d'un worker partage.

Contexte de parc, pour situer la classe (issue ouverte ce matin, non claimee) : #17407 documente que deux parcs partagent les labels coursia-linux avec des contrats differents, et que ce meme job a rendu 14 rouges d'environnement sur des slots natifs sans gh. Ce job tourne en [self-hosted, coursia-ephemeral, coursia-linux] (l.127).

Geste pris : relance des deux runs en echec (35674498995, 35674498957). Ce qui deplacerait la lecture : le meme avortement au meme point apres relance, ce qui sortirait du flottant et demanderait une repro ciblee.

@jsboige

jsboige commented Sep 23, 2026

Copy link
Copy Markdown
Owner Author

[ADJOINT PREFLIGHT]
schema: 1
lane: myia-po-2026:CoursIA-3
pr: 17347
head: 7219b3a
complete: true
body: read
comments-reviewed: 5
reviews-reviewed: 0
threads-reviewed: 0
threads-unresolved: 0
surfaces-sha256: 1ee87379550892592eddeada296ece44c3fc0337492b790a8d4ff3d6f32602bd
diff-files: 1
diff-additions: 195
diff-deletions: 52
checks: latest-wins-green
b0: clear
scope: pass
domain: not-applicable
verdict: READY
[/ADJOINT PREFLIGHT]

@myia-ai-01
myia-ai-01 merged commit 3130814 into main Sep 23, 2026
23 of 36 checks passed
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