Épique #8236 (SAE + J-lens multi-échelle : forme et dynamique des catastrophes sous perturbations appariées). Cette série calibration précède le notebook d’expérience : elle rend la comparaison cross-échelle valide en contrôlant, avant toute expérience, ce qui doit l’être.
La Phase 0 établit la vérité-terrain des artefacts. Le body de l’issue annonçait des SAE pour « 0.7B, 2B, 4B, 8B, 27B, 35B-A3B » ; rien n’est supposé en amont — on mesure ici ce qui existe réellement sur le Hub, avec quelles largeurs, quelles couches couvertes, quel point d’accrochage. Les écarts mesurés contre l’annonce sont un résultat, pas un incident : ils redessinent le plan d’expérience (quelles tailles sont réellement comparables).
Conventions de la série : sorties committées (C.2), aucune erreur volontaire (C.1), documentation en français.
Statut épistémique — Sans verdict à ce jour : aucune ligne de la matrice de dissociations ne concerne ce notebook ; son statut épistémique sera porté par la matrice le cas échéant.
import jsonimport osimport reimport urllib.requestimport pandas as pd# Toutes les requetes passent par ce helper : un User-Agent explicite et,# si present dans l'environnement, le token HF (repos eventuellement gated).# Le token ne s'affiche jamais (regle secrets : env uniquement).def hf_get(url, as_json=True): headers = {"User-Agent": "ict43-phase0"} tok = os.environ.get("HF_TOKEN")if tok: headers["Authorization"] =f"Bearer {tok}" req = urllib.request.Request(url, headers=headers)with urllib.request.urlopen(req, timeout=30) as r: raw = r.read()return json.loads(raw.decode("utf-8")) if as_json else rawprint("helper HF pret")
Le balayage énumère les dépôts de l’organisation Qwen dont l’identifiant contient sae (insensible à la casse), puis récupère pour chacun son config.json et la liste de ses fichiers. Les dépôts sont nommés SAE-Res-<modèle>-W<largeur>-L0_<cible> : deux régimes de sparsité (L0_50, L0_100) par modèle — chaque dépôt porte un seul régime.
def scan_sae_repos():"""Enumere les repos SAE de l'org Qwen avec config + couches couvertes.""" repos = [m["modelId"] for m in hf_get("https://huggingface.co/api/models?author=Qwen&limit=1000")if"sae"in m["modelId"].lower()] rows = []for repo insorted(repos):try: cfg = hf_get(f"https://huggingface.co/{repo}/resolve/main/config.json") info = hf_get(f"https://huggingface.co/api/models/{repo}") layers =sorted(int(m.group(1)) for s in info.get("siblings", [])if (m := re.match(r"layer(\d+)\.sae\.pt$", s["rfilename"]))) m = re.match(r"Qwen/SAE-Res-(.+)-W(\d+K)-L0_(\d+)$", repo) rows.append({"modele": m.group(1) if m else repo, "largeur": m.group(2) if m else"?","l0_cible": int(m.group(3)) if m elseNone,"d_sae": cfg.get("d_sae"), "n_layers_cfg": cfg.get("num_layers"),"hook": cfg.get("hook_point"),"couches": f"{min(layers)}..{max(layers)} ({len(layers)})","toutes_couches": len(layers) == cfg.get("num_layers"), })exceptExceptionas e: rows.append({"modele": repo, "erreur": f"{type(e).__name__}"})return pd.DataFrame(rows)inventaire = scan_sae_repos()print(f"{len(inventaire)} repos SAE mesures sur le Hub")inventaire
14 repos SAE mesures sur le Hub
modele
largeur
l0_cible
d_sae
n_layers_cfg
hook
couches
toutes_couches
0
Qwen3-1.7B-Base
32K
100
32768
28
resid_post
0..27 (28)
True
1
Qwen3-1.7B-Base
32K
50
32768
28
resid_post
0..27 (28)
True
2
Qwen3-30B-A3B-Base
128K
100
131072
48
resid_post
0..47 (48)
True
3
Qwen3-30B-A3B-Base
32K
50
32768
48
resid_post
0..47 (48)
True
4
Qwen3-8B-Base
64K
100
65536
36
resid_post
0..35 (36)
True
5
Qwen3-8B-Base
64K
50
65536
36
resid_post
0..35 (36)
True
6
Qwen3.5-27B
80K
100
81920
64
resid_post
0..63 (64)
True
7
Qwen3.5-27B
80K
50
81920
64
resid_post
0..63 (64)
True
8
Qwen3.5-2B-Base
32K
100
32768
24
resid_post
0..23 (24)
True
9
Qwen3.5-2B-Base
32K
50
32768
24
resid_post
0..23 (24)
True
10
Qwen3.5-35B-A3B-Base
128K
100
131072
40
resid_post
0..39 (40)
True
11
Qwen3.5-35B-A3B-Base
32K
50
32768
40
resid_post
0..39 (40)
True
12
Qwen3.5-9B-Base
64K
100
65536
32
resid_post
0..31 (32)
True
13
Qwen3.5-9B-Base
64K
50
65536
32
resid_post
0..31 (32)
True
Lecture du balayage — quatre écarts mesurés contre l’annonce de l’issue :
Pas de SAE 0.7B ni 4B : la liste annoncée (0.7B, 2B, 4B, 8B, 27B, 35B-A3B) ne correspond pas au terrain. Les tailles réellement couvertes forment un ensemble différent mais comparable : 1.7B et 8B (Qwen3) ; 2B, 9B, 27B, 35B-A3B (Qwen3.5) ; 30B-A3B (Qwen3). Soit 7 tailles effectives au lieu de 6 annoncées.
Deux régimes de sparsité (L0_50 et L0_100) existent pour chaque dépôt — l’issue ne les mentionnait pas ; le choix du régime devient un paramètre d’expérience à documenter.
Le 27B n’existe qu’en version instruct (Qwen3.5-27B, sans suffixe -Base) : un mismatch base/instruct à inscrire dans les limites de la comparaison.
En régime L0_100, les deux A3B (30B et 35B-A3B) n’existent qu’en W128K — pattern croisé {L0_50→W32K, L0_100→W128K} strictement identique sur les deux tailles : une structure de publication du Hub, pas un retrait spécifique. Le fait mesuré derrière ce point : le dépôt W32K-L0_100 du 30B-A3B est absent de l’énumération des 14 dépôts du Hub (jamais publié ou retiré) — la ligne W32K de la table ci-dessus est son régime L0_50, bien vivant.
Point positif inattendu : chaque dépôt couvre toutes les couches du modèle (layer0.sae.pt … layer{N-1}.sae.pt), pas seulement le mi-réseau — l’appariement de profondeur n’est pas contraint par la disponibilité.
# d_model des modeles de base : les configs Qwen3.5 sont omni (champ text_config imbrique),# les configs Qwen3 planes. On mesure les deux formes sans rien supposer.BASES = {"Qwen3-1.7B-Base": "Qwen/Qwen3-1.7B-Base","Qwen3-8B-Base": "Qwen/Qwen3-8B-Base","Qwen3-30B-A3B-Base": "Qwen/Qwen3-30B-A3B-Base","Qwen3.5-2B-Base": "Qwen/Qwen3.5-2B-Base","Qwen3.5-9B-Base": "Qwen/Qwen3.5-9B-Base","Qwen3.5-27B": "Qwen/Qwen3.5-27B","Qwen3.5-35B-A3B-Base": "Qwen/Qwen3.5-35B-A3B-Base",}def d_model_de(repo): cfg = hf_get(f"https://huggingface.co/{repo}/resolve/main/config.json") t = cfg.get("text_config", cfg) # omni -> text_config ; classique -> platreturn t["hidden_size"], t["num_hidden_layers"]bases = pd.DataFrame( [{"modele": k, "d_model": d_model_de(v)[0], "n_layers": d_model_de(v)[1]}for k, v in BASES.items()])bases
modele
d_model
n_layers
0
Qwen3-1.7B-Base
2048
28
1
Qwen3-8B-Base
4096
36
2
Qwen3-30B-A3B-Base
2048
48
3
Qwen3.5-2B-Base
2048
24
4
Qwen3.5-9B-Base
4096
32
5
Qwen3.5-27B
5120
64
6
Qwen3.5-35B-A3B-Base
2048
40
Phase 0b — Appariement des échelles : facteur d’expansion et profondeur relative
Deux quantités conditionnent la validité de la comparaison cross-échelle :
le facteur d’expansiond_sae / d_model : s’il diffère d’une taille à l’autre, les dictionnaires n’ont pas la même résolution relative et les ordres-paramètres ne sont pas directement comparables ;
la profondeur relative d’ancrageround(0.5 · n_layers) : l’issue impose un ancrage à mi-réseau apparié (le pilote ICT-42 utilisait déjà layer12of24 au 2B et layer14of28 au 1.7B — la même règle, avant la lettre).
# Jointure inventaire (regime L0_100, une ligne par taille) x d_model.# Les lignes d'erreur du balayage n'ont pas de l0_cible : le filtre les ecarte.l0100 = inventaire[inventaire["l0_cible"] ==100]appariement = ( l0100.drop_duplicates(subset=["modele"]) .merge(bases, on="modele", how="inner") .assign(expansion=lambda d: d["d_sae"] // d["d_model"], couche_ancre=lambda d: (0.5* d["n_layers"]).round().astype(int)) [["modele", "d_model", "n_layers", "d_sae", "expansion","couche_ancre", "couches", "hook"]] .reset_index(drop=True))appariement
modele
d_model
n_layers
d_sae
expansion
couche_ancre
couches
hook
0
Qwen3-1.7B-Base
2048
28
32768
16
14
0..27 (28)
resid_post
1
Qwen3-30B-A3B-Base
2048
48
131072
64
24
0..47 (48)
resid_post
2
Qwen3-8B-Base
4096
36
65536
16
18
0..35 (36)
resid_post
3
Qwen3.5-27B
5120
64
81920
16
32
0..63 (64)
resid_post
4
Qwen3.5-2B-Base
2048
24
32768
16
12
0..23 (24)
resid_post
5
Qwen3.5-35B-A3B-Base
2048
40
131072
64
20
0..39 (40)
resid_post
6
Qwen3.5-9B-Base
4096
32
65536
16
16
0..31 (32)
resid_post
Lecture de l’appariement. Trois familles se dégagent :
expansion 16× partout sauf 64× pour les deux A3B en W128K (30B-A3B et 35B-A3B) — les deux seules tailles MoE à grande largeur portent un dictionnaire 4× plus résolu par dimension. Une comparaison qui mélange 16× et 64× doit le déclarer (le pilote 2-tailles était homogène 16×).
l’ancrage mi-réseau tombe toujours sur un entier (12, 14, 16, 18, 20, 24, 32) et chaque dépôt couvre toutes les couches : la règle round(0.5 · n_layers) est applicable partout sans exception.
l’architecture hybride des Qwen3.5 (attention linéaire : pleine = 3:1 sur toutes leurs couches, lu dans layer_types des configs publiées — a priori d’architecture, non mesuré dans ce notebook) diffère structurellement des Qwen3 denses — la profondeur relative contrôle la position dans le réseau, pas l’homogénéité du mécanisme d’attention traversé. À inscrire dans les limites.
Le pilote suivant (Phase 1) fittera la J-lens par taille à cette couche appariée ; la fidélité de reconstruction des SAE (MSE, variance expliquée — l’autre moitié de la Phase 0) exige des passes avant GPU et fait l’objet de la Phase 0c.
Exercices
Trois exercices préparent la Phase 1. Ils se complètent sans dépendre l’un de l’autre ; les squelettes s’exécutent (C.1).
# Exercice 1 : URL d'artefact verifiee.# Ecrire couche_url(modele, couche) qui construit l'URL du .sae.pt de la couche# demandee pour le depot L0_100, et verifie sa disponibilite par une requete HEAD# (code 200). Retourner (url, disponible). Attention : le 30B-A3B n'existe qu'en W128K.# Indice : appariement contient d_sae par modele, le nom du depot suit# SAE-Res-<modele>-W<largeur>-L0_100 avec largeur = d_sae en K (32768 -> 32K, ...).def couche_url(modele, couche):# TODO etudiantprint("Exercice a completer")returnNone# TEMOIN attendu : couche_url("Qwen3.5-2B-Base", 12) doit rendre une URL disponible.resultat_ex1 =None# TODO etudiantresultat_ex1
# Exercice 2 : grille de comparaison homogene en expansion.# Construire grille_16x() : le sous-ensemble d'appariement ou toutes les tailles# portent expansion == 16 (comparaison homogene), trie par n_layers croissant,# avec la colonne couche_ancre. Verifier que la grille compte 5 tailles.# Etape 1 : filtrer sur expansion. Etape 2 : trier. Etape 3 : verifier le compte.def grille_16x(tableau):# TODO etudiantprint("Exercice a completer")returnNoneresultat_ex2 =None# TODO etudiantresultat_ex2
# Exercice 3 : cout de telechargement d'une experience.# Pour la grille 16x, estimer le nombre d'artefacts SAE a telecharger pour la# Phase 1 (J-lens) si l'on veut, par taille, la couche d'ancrage ET ses deux# voisines (ancre-1, ancre, ancre+1). Retourner un DataFrame (modele, couches_voulues,# n_artefacts). bornes : la couche 0 et la couche n_layers-1 existent, jamais hors bornes.# Indice : appariement["n_layers"] donne la borne haute par modele.def cout_phase1(tableau):# TODO etudiantprint("Exercice a completer")returnNoneresultat_ex3 =None# TODO etudiantresultat_ex3
Limites de la Phase 0
Fidélité de reconstruction non mesurée ici : l’inventaire établit l’existence et la géométrie des dictionnaires (largeurs, couches, hook), pas leur qualité (MSE, variance expliquée). Cette mesure exige des passes avant sur GPU-2 (garde-fous #8236 : CUDA_VISIBLE_DEVICES=2, un job sérialisé, abort thermique > 85 °C) — c’est l’objet de la Phase 0c, à livrer avant toute expérience.
27B en instruct seulement : le dépôt Qwen3.5-27B n’a pas de jumeau -Base. Toute comparaison impliquant le 27B mélange un modèle post-entraîné avec des modèles de base — à déclarer dans le plan d’expérience ou à exclure cette taille des comparaisons sensibles au post-training.
Hétérogénéité d’expansion : 16× (5 tailles denses) contre 64× (2 tailles A3B en W128K). La comparaison stricte se fait en expansion homogène ; l’analyse d’échelle A3B est un régime séparé.
Qwen3.5 hybrides (attention linéaire 3:1) : la profondeur relative n’égalise pas le mécanisme traversé. Le facteur de structure (dense vs MoE vs hybride) est un covariant à tracer, pas à ignorer. Le ratio 3:1 est lui-même un a priori (config layer_types), pas une mesure de ce notebook.
Inventaire daté : mesuré au Hub tel qu’il existe au moment de l’exécution — le dépôt W32K-L0_100 du 30B-A3B est ainsi absent de l’énumération des 14 dépôts (jamais publié ou retiré), son W32K-L0_50 restant la ligne 3 de la table. Toute re-exécution re-mesure.
Suite (Phase 1) : fit J-lens par taille à couche_ancre, report qualité de sonde (accuracy/AUC control vs trained). See #8236.