Skip to content

feat(ci,#15574): instrument de caractérisation de la variance runners (items 1-2) - #18673

Merged
myia-ai-01 merged 1 commit into
mainfrom
feature/15574-runner-variance
Oct 1, 2026
Merged

myia-ai-01 merged 1 commit into
mainfrom
feature/15574-runner-variance

Conversation

@jsboige

@jsboige jsboige commented Oct 1, 2026

Copy link
Copy Markdown
Owner

Grain: DEEP/research-code — lane myia-po-2024:CoursIA — prev: LIGHT/tooling #18668

Summary

Items 1-2 de #15574 : la variance 3-4x du pool self-hosted est caractérisée par mesure et la co-résidence par hôte est écrite. Deux livrables : un instrument (scripts/ci/characterize_runner_variance.py, 15 tests, sans réseau) et les nombres qu'il produit, croisés avec une mesure directe de la flotte de slots.

Mesure directe de la co-résidence (item 2) — po-2024, 2026-10-01 ~11:20Z

La flotte CI de po-2024 vit dans une VM WSL unique (16 vCPU, 24 Go) derrière le daemon docker-ce dédié (socket /var/run/docker-ce.sock, cf piège « docker ps nu rend 0 »). État mesuré :

Famille Slots actifs Cap CPU/slot Cap RAM/slot Σ CPU Σ RAM
linux-docker (pool coursia-linux) 8 3,0 1,5 Go 24,0 12 Go
lean-docker (pool coursia-lean) 2 6,0 6 Go 12,0 12 Go
linux-waiter (pool coursia-waiter) 12 1,0 0,5 Go 12,0 6 Go
Total live 22 — — 48,0 30 Go

Instrument (item 1) — characterize_runner_variance.py

Collecte les jobs complétés d'un workflow via l'API GitHub (runner, horodatages, conclusion), attribue chaque job à son hôte physique par préfixe de nom de runner (#15152 : le préfixe dérive de la machine), puis mesure la durée selon trois axes :

  1. par runner (médiane/p90) — isole un slot défaillant ;
  2. par heure locale — teste la corrélation nocturne suspectée par ci(infra): variance 3-4x du pool self-hosted coursia-ephemeral a code constant — gouvernance de co-residence #15574 ;
  3. par co-résidence — histogramme du nombre d'autres jobs lourds (docker/lean, les waiters pollent au lieu de calculer) chevauchés sur le même hôte : c'est la grandeur qu'un plafond de concurrence borne.

Sortie JSON (machine) + tableau (humain). Instrument de mesure, pas un gate : rc 0 = mesure produite, 1 = [PANNE] (API injoignable). Trois pièges d'API corrigés en cours de route, documentés dans le code : le >= du filtre created doit être percent-encodé (un > brut rend zéro run en silence), le endpoint LIST ne porte pas completed_at de façon fiable (préfiltre sur created_at), et gh api n'expose pas next_page_url dans le corps (pagination &page=N jusqu'à page courte — sans elle, seule la centaine de runs la plus récente est vue).

Limite d'attribution assumée : les slots ai-01-wsl-N sont traités chacun comme son propre hôte (le préfixe ne distingue pas leurs familles de slots) — leur co-résidence interne est donc sous-comptée. Ils servent ici de groupe de contraste externe, pas d'objet de mesure ; leurs hosts relèvent d'ai-01.

Complémentaire de #18626 (item 4, mergé) : check_runner_variance.py signale la variance à postériori (garde advisory), l'instrument présent explique la variance (par runner / par heure / par co-résidence) — la décision item 3 consomme les deux.

Résultats sur 7 jours (scripts-tests.yml)

Fenêtre 2026-09-24 → 2026-10-01, 1749 jobs complétés dont 637 lourds (docker/lean). Médiane globale 133 s.

Durée par co-résidence — le chiffre-titre (jobs lourds, hôte po-2024) :

Voisins lourds chevauchés n médiane (s) p90 (s)
0 217 42 716
1 211 54 884
2 123 500 1186
3+ 86 873 1271
  • La médiane est multipliée par ~21 de 0 à 3+ voisins (42 → 873 s), et le p90 par 1,8 (716 → 1271 s). L'histogramme montre jusqu'à 7 jobs lourds simultanés sur l'hôte — pour 10 slots lourds et 16 vCPU, chaque job au-delà de 2-3 voisins n'a plus de cœur entier.
  • Confondant assumé : un job long chevauche mécaniquement plus de voisins qu'un job court (temps d'exposition plus grand). La médiane à 0 voisin (42 s) est donc tirée vers le bas par les jobs triviaux. Le p90 — qui compare les jobs longs entre eux — reste multiplié par 1,8, et le mécanisme est corroboré indépendamment par la mesure statique (3,0× de sur-souscription) : la corrélation dit la même chose que le comptage de cœurs.
  • Pas de pattern horaire net (4h : 68,5 s médiane ; 22h : 281,5 s — élévation vespérale/nocturne modérée, pas le facteur principal). L'hypothèse « nocturne » de ci(infra): variance 3-4x du pool self-hosted coursia-ephemeral a code constant — gouvernance de co-residence #15574 n'est pas le mécanisme dominant : la charge, pas l'heure, prédit.
  • Par hôte (contraste) : po-2024 n=637, médiane 115 s, p90 1084 s (ratio p90/médiane ≈ 9,4×) ; les slots ai-01-wsl-* (chacun traité comme son hôte, co-résidence interne sous-comptée) plafonnent à ~540-600 s de p90. La signature « p90 ≫ médiane » est concentrée sur l'hôte sur-souscrit.
  • Par runner po-2024 : médianes 55-368 s (docker-8 et docker-6 les plus lents) — un effet slot existe, mais il est dominé par la co-résidence.

Proposition pour l'item 3 (décision — pas appliquée ici)

La décision de plafond est un arbitrage débit/variance qui dépasse cette lane ; proposition soumise sur l'issue :

  • Activer le garde existant : COURSIA_RUNNER_CPU_BUDGET=16 dans l'environnement de déploiement des superviseurs (variable déjà lue par assert_cpu_budget, refus au démarrage, jamais avertissement).
  • Right-size pour que la flotte puisse démarrer sous ce budget : docker 8→5 slots × 3 CPU (15) + lean 2×4 (8) + waiters 12×1 (12) = 35 — encore > 16 : le budget réaliste borne la part non-idle, ou les waiters doivent être exclus du décompte (ils consomment ~0 en attente). Alternative mesurable : budget 24 (= 1,5x) avec la famille docker à 6 slots.
  • Les hosts ai-01 (ai-01-wsl-*) relèvent d'ai-01 : recommandation uniquement, pas de geste.

Test plan

  • python -m pytest scripts/tests/test_characterize_runner_variance.py -q → 15 passed (collecte injectable, aucun réseau)
  • Passe live 7 jours sur scripts-tests.yml : rc 0, 1749 jobs / 637 lourds collectés (sortie ci-dessus)
  • host_of/is_heavy couverts par tests unitaires (suffixes de famille, machines étrangères)

See #15574 (items 1-2 ; item 3 proposé, item 4 déjà livré par #18626)

🤖 Generated with Claude Code

… (items 1-2)

characterize_runner_variance.py collecte les jobs complets d'un workflow
(runner, horodatages, conclusion), attribue chaque job a son hote physique
par prefixe de runner, et mesure la duree selon trois axes : par runner,
par heure locale, et par co-residence (nombre d'autres jobs lourds
chevauches sur le meme hote -- la grandeur que l'item 3 borne). Sortie
JSON + tableau humain ; instrument de mesure, pas un gate (rc 0 = mesure,
rc 1 = [PANNE] API).

Trois pieges d'API mesures et documentes dans le code : percent-encodage
du >= du filtre created (un > brut rend 0 run en silence), completed_at
non fiable sur le endpoint LIST (prefiltre sur created_at), pagination
&page=N jusqu'a page courte (gh api n'expose pas next_page_url dans le
corps).

15 tests sans reseau (collecte injectable, chevauchements frontieres,
buckets de co-residence, [PANNE]).

See #15574

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

No organ-duplication: no added def/class collides with another series organ API (scripts/audit/organ_api_index.yaml).

Detector: python scripts/audit/detect_organ_duplication.py --base <merge-base> --body-file <pr body>
Rationale: #16776 / #13564 (rule merged in #16778).

@github-actions github-actions Bot added the large-pr-no-review PR > seuil sans review (ni bot ni humaine) -- retire quand une review arrive (#11232) label Oct 1, 2026
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

Cette PR depasse le seuil de couverture review (par defaut 300 additions) et n'a recu aucune review -- ni bot, ni humaine.

Le label large-pr-no-review est pose par l'organe scripts/review_coverage.py porte par l'issue #11232. Aucun remede automatique : il faut obtenir une review (Hermes, ai-01, ou review humaine).

Le label est retire au balayage suivant (quotidien) des qu'une review arrive -- dans reviews[] ou en commentaire de verdict -- ou que le diff passe sous le seuil. Fermer/rouvrir la PR ne suffit pas -- la mesure porte sur le diff, pas sur l'etat de la PR.

Seuil, historique et exceptions : cf. docs/reference/review-coverage-threshold.md.

@jsboige

jsboige commented Oct 1, 2026

Copy link
Copy Markdown
Owner Author

[ADJOINT PREFLIGHT]
schema: 1
lane: myia-po-2026:CoursIA-3
pr: 18673
head: b865421
complete: true
body: read
comments-reviewed: 2
reviews-reviewed: 0
threads-reviewed: 0
threads-unresolved: 0
surfaces-sha256: e2e17eadeecb0f1883bfd89ea3329b620c7eadf2994f881bc574d1da536ddde1
diff-files: 2
diff-additions: 562
diff-deletions: 0
checks: latest-wins-green
b0: clear
scope: pass
domain: pass
verdict: READY
[/ADJOINT PREFLIGHT]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

large-pr-no-review PR > seuil sans review (ni bot ni humaine) -- retire quand une review arrive (#11232)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants