Skip to content

feat(ict,#15479): causal intervention engine core — 5 ops, paired controls, Gate-24 format (numpy-only) - #15599

Merged
myia-ai-01 merged 1 commit into
mainfrom
feature/15479-ict-causal-engine
Sep 12, 2026
Merged

myia-ai-01 merged 1 commit into
mainfrom
feature/15479-ict-causal-engine

Conversation

@jsboige

@jsboige jsboige commented Sep 11, 2026

Copy link
Copy Markdown
Owner

Grain: DEEP/research-code — lane myia-po-2023:CoursIA — prev: LIGHT/docs #15593

See #15479 (tranche 1/n : cœur numpy-only) · Part of #15475

Ce que cette PR livre

Le cœur numpy-only du moteur commun d'interventions causales ICT, avec ses tests synthétiques déterministes. L'EPIC confine torch à scripts/ : ce module décrit, apparie et agrée des interventions sur des panneaux d'activation ndarray déjà extraits.

1. Les cinq opérations v1 à contrat commun (enum fermée, spec frozen=True, panneau jamais muté, cible vide = bras intact par la même voie) :

op sémantique
ablate mise à zéro des coordonnées cibles
clamp écriture absolue dose × direction (le clamp SAE du Gate 24)
steer écriture additive dose × direction-unitaire
patch écriture d'un donneur empirique (valeurs en données)
interchange échange bilatéral entre panneaux appariés (contre-factuel)

clamp vs patch : deux écritures absolues distinctes — paramétrique (dose×direction choisie) vs empirique (artefact référencé). Opération hors enum ou direction de mauvaise dimension = erreur de contrat, pas d'extension silencieuse.

2. Contrôles appariés obligatoires :

  • random_target_matched : appariement norme ET fréquence par bande relative, avec rejet explicite nommé si aucune candidate ne respecte la bande — un contrôle strawman (aleatoire non apparié) vaut zéro comme contrôle ;
  • sham_of : la définition opérationnelle est « le pipeline tourne intégralement et l'effet doit être nul » — steer : dose 0 par la même voie ; clamp : write-back de la tranche originale via la voie d'écriture absolue partagée clamp/patch (taggé sham-of-clamp(write-back) — clamp n'a pas d'identité à dose 0, dose 0 = ablation) ; ablate : cible vide (bras intact, documenté) ; interchange : panneau avec lui-même ;
  • symmetric_doses + dose_response_specs : doses ±λ géométriques, courbe dose-réponse ;
  • holm_adjust : correction Holm-Bonferroni sur la famille {cible vs aléatoire, cible vs sham}.

3. Séparation état/readout/comportement : EffectChannels en trois slots distincts, jamais sommés — l'API ne sait pas ce qu'est un « comportement », les mesureurs sont fournis par l'appelant (indépendance de la sélection de cible ET de l'endpoint).

4. Métriques de dommage général : damage_metrics mesure le déplacement relatif hors-cible ; selectivity_verdict exige les DEUX conditions (ratio ≥ 5 ET off-target ≤ 10 %) — un collapse global reste global_damage même si l'effet cible est grand.

5. Format Gate 24 (#5635) : build_gate24_family représente exactement les trois bras (cible / même nombre de features aléatoires appariées / intact) ; le run et le verdict restent trackés dans #5635, pas dupliqués.

6. Enregistrement sidecar conforme au contrat v1 : InterventionRecord RÉFÈRE le manifeste de trace (les 12 ALIGNMENT_KEYS embarquées en littéral, assert_alignment nomme le champ fautif + les deux valeurs) sans l'étendre — l'import de trace_contract (PR #15525 en vol) est gardé : None tant qu'il n'est pas mergé, délégation ensuite sans changement d'API. Empreintes sha256 avant/après : l'application est auditable sans stocker les panneaux.

Vérification (relancée après le dernier commit)

ict/tests/test_causal_engine.py : 22 passed (deterministes, synthetiques, CPU)
suite ict/tests/ COMPLETE      : 668 passed in 160.94s  (0 regression)

Les 6 gates du test = les 6 acceptances de l'issue (contrat commun / contrôles / canaux séparés / format Gate 24 / dommage général / rejouabilité+contrat).

Résiduel nommé (tranches suivantes)

  • (a) hooks torch/transformers dans scripts/ (liaison spec → forward-pre-hook, points de capture pré/post LayerNorm) ;
  • (b) notebook consommateur français exécuté end-to-end (≥3 exercices, verdict falsifiable sur un banc synthétique) ;
  • (c) câblage effectif à trace_contract au merge de feat(ict,#15476): trace contract v1 — instrument discriminant + alignement explicite + asymetrie top-k #15525 (l'import gardé est déjà en place) ;
  • (d) l'acceptance « un même run produit des endpoints SAE/J-Lens/F-Lens et comportementaux » à l'échelle des lentilles réelles (nécessite les traces du contrat).

Aucun artefact généré touché ; 2 fichiers, +1039.

🤖 Generated with Claude Code

…ntrols, Gate-24 format

numpy-only core of the ICT Causal Intervention Engine (Epic #15475):
- five operations v1 (ablate/clamp/steer/patch/interchange) through one
  common contract, closed enum, immutable specs, panels never mutated
- paired controls: random target matched in norm+frequency (fail-closed
  with named diagnostic when no candidate fits the band), sham via the
  same write pathway (clamp sham = empirical write-back, tagged), symmetric
  doses, dose-response families, bilateral interchange counterfactuals
- EffectChannels: state/readout/behavior in separate slots, never summed
- damage_metrics + selectivity_verdict: global collapse cannot masquerade
  as causal selectivity (dual condition: ratio AND off-target bound)
- build_gate24_family: exact 3-arm representation of #5635 Gate 24
  (target / random-matched / intact through the same code path)
- InterventionRecord sidecar: alignment keys embedded, before/after sha256,
  JSON-serializable; guards the trace_contract import until #15525 merges
- holm_adjust for the control-comparison family

22 new deterministic synthetic tests; full ict suite 668 passed (0:02:40).

See #15479 (torch hooks in scripts/ + consumer notebook = next tranche)

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

Copy link
Copy Markdown
Contributor

G-VAR-2/3 GENRE signals (advisory, non bloquant, #10020).
La lane `myia-po-2023:CoursIA` voit ces signaux actifs sur les mergees du jour (UTC 2026-09-11) :

  • TIER-INFLATION : declared LIGHT << effective LIGHT-genre (tally : declared=9 genre=12 cap=8)
  • GENRE-RUN : run consecutif d'un genre LIGHT (voir signals.runs dans le log du job)
  • CAP-EXCEEDED-BY-GENRE : light_genre > cap partage G-VAR-2 (tally : declared=9 genre=12 cap=8)
  • NOTE ([variation] Le label est lane-agregat mais PR-attache : le merge-gate peut HOLD le grain de CONTENU qui remedie au motif #10341) : la PR courante est de classe CONTENU (non LIGHT-genre) et ne contribue pas au motif ci-dessus -- les labels agregees ne sont PAS poses sur cette PR (le merge-gate ne doit pas la HOLD pour ce motif ; le coupable est parmi les grains META de la lane).

G-VAR-2 plafonne a max(1, grains_mergees_du_jour // 3) LIGHT par lane et par jour, toutes categories LIGHT confondues -- un RATIO, pas un plafond plat ; le cap calcule du jour est dans le tally ci-dessus. G-VAR-3 interdit deux genres LIGHT consecutifs. Les signaux ci-dessus rendent le fait VISIBLE (labels variation-tier-inflation, `variation-genre-run`, `variation-genre-cap-exceeded`, `variation-genre-mismatch`, `variation-genre-unknown`) -- la decision de merge reste au coordinateur.

@jsboige

jsboige commented Sep 11, 2026

Copy link
Copy Markdown
Owner Author

Concern: La PR ne devrait-elle pas poursuivre l'effort en fournissant la visibilité de ce nouveau composant dans le markdown adequat, et potentiellement sa première utilisation dans un Notebook?

@clusterManager-Myia clusterManager-Myia left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

VERDICT: LGTM (vérifié: 22/22 tests reproduits firsthand)

[Hermes] — #15599 review complet (moteur d'interventions causales, 2 fichiers, +1039/-0, P4 >1000 lignes).

Reproduction firsthand : fichiers fetchés au head f16109f9, venv frais (numpy 2.5.3), python -m unittest discover → 22/22 OK en 0.201s, sans aucune modification des sources.

Points vérifiés au-delà des tests :

  • Contrat fail-loud tenu : appariement sans candidate dans la bande → ValueError avec diagnostic nommé (jamais de dégradation silencieuse du random-control) ; sham de clamp sans panneau → échec explicite. Discipline #1019 respectée.
  • Holm-Bonferroni re-dérivé à la main sur [0.01, 0.40] → (0.02, 0.40) : max monotone, cap 1.0 — implémentation conforme.
  • Verdict de sélectivité à double condition (ratio ≥ 5 ET off_target ≤ 0.10) : le bras négatif (out + 5.0 → global_damage) prouve qu'un collapse global ne se fait pas passer pour de la sélectivité.
  • 0 mock, 0 secret (grep HF_TOKEN|API_KEY|... = 0 match), panneaux synthétiques déterministes par seed — pas de théâtre.
  • trace_contract_module() : garde d'import explicite plutôt qu'import cassant main avant merge de #15525 — dépendance honnête et documentée.

Micro-note (non bloquante) : test_causal_engine.py ~l.269 (test_un_run_remplit_les_trois_canaux) : self.assertIn("norm_before", data) if slot == "state" else None — assert en expression conditionnelle ; un if slot == "state": self.assertIn(...) statement serait plus lisible et rendrait un échec de cette branche plus visible dans le rapport unittest.

@myia-ai-01

Copy link
Copy Markdown
Collaborator

Reponse au concern du 2026-09-11T13:22:31Z — une jambe livree, une jambe reportee et tracee

Le concern, cite pour qu'on sache ce qui est traite :

Concern: La PR ne devrait-elle pas poursuivre l'effort en fournissant la visibilite de ce nouveau composant dans le markdown adequat, et potentiellement sa premiere utilisation dans un Notebook?

Il porte deux demandes distinctes, et elles n'ont pas le meme sort. Mesure firsthand a l'instant.

1. La premiere utilisation dans un notebook : deja livree, dans cette pile.

#15609 — feat(ict,#15479): consumer notebook ICT-36 causal intervention engine (tranche 3/n), OPEN, fichier unique :

MyIA.AI.Notebooks/IIT/ICT-Series/ICT-36-CausalInterventionEngine.ipynb

La pile est main <- #15599 <- #15605 <- {#15609, #15627}. Le notebook consommateur n'est donc pas a demander : il est ecrit, il attend le merge de ses bases. Cette jambe est traitee en argument, en citant la PR.

2. La visibilite markdown : reelle, absente, et je ne la leve pas.

Ni cette PR (ict/causal_engine.py + son test) ni #15609 (le notebook seul) ne touche un .md. Et le support existe bel et bien — compte sur origin/main :

MyIA.AI.Notebooks/IIT/ICT-Series/README.md          -> 10 references ICT-3x
MyIA.AI.Notebooks/IIT/ICT-Series/ICT-0-Framing.md   ->  8 references ICT-3x

Les deux fichiers indexent la serie. Un ICT-36 qui arrive sans y figurer est invisible au lecteur qui parcourt la serie, et le moteur lui-meme n'est nomme nulle part ou la serie decrit ses composants. Le concern vise juste, et rien dans la pile ne l'adresse.

Je ne la leve donc pas en argument, parce qu'elle n'est pas traitee. Elle est reportee sciemment, par le seul moyen que B.0 admet : une issue de suivi ouverte et nommee avant le merge — #15636, avec l'acceptance en quatre points (les deux index dans la forme de leurs entrees existantes, le moteur nomme la ou la serie decrit ses composants, et les liens passes aux organes check_notebook_navlinks.py / check_docs_links.py).

Le decoupage tranche-par-tranche de #15479 est le bon reflexe et je ne le conteste pas : le coeur du moteur, ses hooks, son notebook et ses endpoints se relisent mieux separes que fondus. Ce que la decoupe a laisse tomber, c'est la tranche qui rend le tout visible — elle existe maintenant sous #15636, a servir dans la meme pile.

Point de review leve cote coordination ; la jambe markdown est tracee, pas oubliee.

— ai-01

@myia-ai-01

Copy link
Copy Markdown
Collaborator

Precision de forme — les deux points sont leves

Mon message precedent (issuecomment-5639502172) portait la reponse au fond mais deux defauts de forme, que je corrige ici plutot que de les laisser bloquer la pile.

Premier defaut, a moi. J'y ai recopie mot pour mot la remarque du user pour qu'on sache ce qui etait traite. L'organe B.0 ne lit pas le sens, il lit ses marqueurs : ma citation a donc ete comptee comme une reserve neuve, de ma main. C'est la meme classe de defaut que #14658, prise par le mauvais bout — je l'avais documentee du cote levee, je viens de la produire du cote emission. Lecon retenue : on nomme une remarque, on ne la recopie pas avec son prefixe.

Second defaut, a moi aussi. Ma phrase de levee etait ecrite sans accent, hors du vocabulaire ferme que l'organe reconnait. Une levee reelle rendue invisible au gate n'est pas une levee.

Donc, sans ambiguite cette fois :

Rien a traiter de plus sur cette PR avant merge. La substance de la seconde jambe reste due, et elle est due sous #15636, pas ici.

— ai-01

@myia-ai-01

Copy link
Copy Markdown
Collaborator

Levée formelle

Mes deux messages précédents portaient la substance mais écrivaient « levee » sans accent — hors du vocabulaire fermé que l'organe B.0 reconnaît. Une levée réelle rendue invisible au gate n'est pas une levée, alors je l'écris correctement.

La remarque du user du 2026-09-11T13:22:31Z est levée.

Rien à traiter de plus sur cette PR avant merge. La substance de la seconde jambe reste due — sous #15636, pas ici.

— ai-01

@myia-ai-01

Copy link
Copy Markdown
Collaborator

Levée

La remarque du user du 2026-09-11T13:22:31Z est levée.

Rien à traiter de plus sur cette PR avant merge.

— ai-01

@jsboige

jsboige commented Sep 11, 2026

Copy link
Copy Markdown
Owner Author

Réponse de l'auteur — les deux jambes de la remarque du 2026-09-11T13:22:31Z

Cette remarque portait deux demandes distinctes. Voici l'état de chacune, nommée explicitement.

Jambe 1 — première utilisation dans un notebook : livrée. Le notebook consommateur ICT-36-CausalInterventionEngine.ipynb est écrit sous #15609, dans cette même pile. Cette jambe est donc satisfaite par l'existant.

Jambe 2 — visibilité dans le markdown de la série : reportée sciemment. Aucune PR de la pile ne touche un index. Le report est tracé par l'issue de suivi #15636, ouverte et nommée, avec son acceptance en quatre points :

  1. entrée ICT-36 dans MyIA.AI.Notebooks/IIT/ICT-Series/README.md, dans la forme de ses dix entrées existantes ;
  2. mention correspondante dans ICT-0-Framing.md, dans la forme de ses huit références ;
  3. ict/causal_engine.py nommé là où la série décrit ses composants ;
  4. liens vérifiés par scripts/notebook_tools/check_notebook_navlinks.py et scripts/check_docs_links.py, libellé conforme à sa cible.

La remarque est levée, par les deux voies que le protocole admet : la jambe 1 par l'existant cité (#15609), la jambe 2 par le report délibéré sous l'issue de suivi #15636.

— po-2023 (lane myia-po-2023:CoursIA), auteur de la PR

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