Bloc B, item 6 de l’epic #16058 — pendant industriel du 3.4c-MoE-from-scratch (bloc A.3, PR #16152 en revue) et compagnon du TV-01 (attention SOTA).
Le 3.4c a codé un MoE jouet (8 experts, top-2, perte d’équilibrage) et mesuré ce que le routage coûte et rapporte sur une tâche synthétique. Ce notebook ouvre la boîte d’un vrai MoE entraîné de zéro : OLMoE-1B-7B (AllenAI, release 0125, Apache-2.0) — 64 experts, top-8, ~6,9 Md paramètres dont ~1,28 Md actifs par jeton, chargé bf16 plein via transformers sur GPU (le §1 explique pourquoi le 4-bit ne s’applique pas ici — mesuré, pas supposé).
Ce que l’on va mesurer sur le routage réel :
quelle charge par expert, par couche (64 experts ne servent pas à parts égales) ;
l’entropie de routage et l’entropie de charge — deux grandeurs distinctes, comme le 3.4c l’a établi sur le jouet ;
la spécialisation : les experts voient-ils des jetons différents (lettres, chiffres, ponctuation) ;
le contrat MoE : capacité ×5 environ en paramètres, calcul par jeton quasi constant ;
le tableau comparatif from scratch vs industriel (item 7 de l’epic, étendu).
Exécution : GPU CUDA requis (~12,5 Gio VRAM pour le bf16 plein — la carte 16 Gio le tient, cf. mesure §1) — règle H.2, l’équivalent CPU resterait exécutable mais des ordres de grandeur plus lent. Prérequis : TV-00b (variantes d’attention), 3.4c (MoE from scratch), TV-01 (chargement SOTA 4-bit).
1. Chargement — et une leçon de quantification mesurée
Le TV-01 chargeait son Mistral-7B en NF4 depuis un mirror pré-quantifié. Ici, la même idée se heurte à la réalité, mesurée à chaque étape :
Deux mirrors 4-bit indépendants (lennyhans/...-bnb-4bit, RichardErkhov/...-4bits) plantent à la première passe avec la même erreur de forme (mat1 5x2048 et mat2 1x65536). Deux auteurs indépendants ne corrompent pas un fichier identiquement : le défaut est structurel, au croisement de bitsandbytes et du routeur OLMoE de transformers — le gate du routeur n’est pas un nn.Linear standard, et la déquantification 4-bit rend son tampon compacté sans lui rendre sa forme [num_experts, hidden].
La réparation standard — exclure le routeur de la quantification (llm_int8_skip_modules=["mlp.gate"]) — rend le modèle exécutable (la génération redevient cohérente), mais la VRAM reste à 12,4 Gio : la quantification ne s’applique de fait à aucun poids. Cause mesurée : chez OLMoE, les 64 experts vivent dans un module spécial (OlmoeExperts, tenseurs 3D fusionnés [experts, dim, ffn]), pas dans des nn.Linear individuels — or bitsandbytes quantifie des nn.Linear.
Conclusion honnête : on charge le bf16 plein du dépôt officiel AllenAI (12,4 Gio — la carte 16 Gio le tient), et cette limite de la chaîne 4-bit est documentée comme une mesure, pas contournée. La leçon transférable : quantifier un MoE n’est pas un interrupteur — l’architecture décide de ce que la chaîne d’outils sait comprimer.
import mathimport osimport pathlibimport timeos.environ["HF_HUB_DISABLE_SYMLINKS_WARNING"] ="1"# cache-first : HF_HUB_OFFLINE doit etre pose AVANT l'import de transformers (lu a l'import par huggingface_hub) ;# au premier run (cache absent) on reste en ligne, le telechargement se fait, les runs suivants passent hors-ligneif (pathlib.Path.home() /".cache/huggingface/hub/models--allenai--OLMoE-1B-7B-0125-Instruct").exists(): os.environ["HF_HUB_OFFLINE"] ="1"import torchfrom transformers.utils import logging as hf_logginghf_logging.disable_progress_bar() # tue les barres de chargement ; les chiffres utiles sont dans nos printsDEVICE ="cuda:0"if torch.cuda.is_available() else"cpu"print("device:", DEVICE, "| torch", torch.__version__, "| transformers", __import__("transformers").__version__)GPU_REQUIS ="bloc B : GPU CUDA requis (H.2, documente en tete de notebook)"assert torch.cuda.is_available(), GPU_REQUIS
Le compte se fait depuis la config, puis se confronte au numel réel du checkpoint (en bf16 plein, le numel est un compte direct — plus de sous-comptage NF4 comme au TV-01) :
par couche : l’attention MHA (16 têtes, hidden 2048) + le routeur (2048 × 64) + les 64 experts (chacun une FFN 2048→1024→2048) ;
actif par jeton : l’attention + le routeur + 8 experts seulement sur 64 ;
hors couches : les embeddings (50 304 × 2048, non liés — OLMoE a des embeddings de sortie séparés).
C’est le contrat MoE vu au 3.4c, à l’échelle : la capacité (tous les paramètres) est disponible mais seul un sous-ensemble calcule.
def params_olmoe(c): d, inter = c.hidden_size, c.intermediate_size e, k, l = c.num_local_experts, c.num_experts_per_tok, c.num_hidden_layers attn =4* d * d # MHA : q, k, v, o (16/16 tetes) gate = d * e # routeur expert =3* d * inter # FFN d'un expert (gate/up/down) mlp_total = gate + e * expert mlp_actif = gate + k * expert emb = c.vocab_size * d *2# entree + sortie non liees total = l * (attn + mlp_total) + emb actif = l * (attn + mlp_actif) + embreturn total, actifN_TOTAL, N_ACTIF = params_olmoe(cfg)N_NUMEL =sum(p.numel() for p in model.parameters())GROUPES = {"embeddings": 0, "attention": 0, "routeur": 0, "experts": 0}for nom, p in model.named_parameters():if"embed_tokens"in nom or"lm_head"in nom: GROUPES["embeddings"] += p.numel()elifany(x in nom for x in ("q_proj", "k_proj", "v_proj", "o_proj")): GROUPES["attention"] += p.numel()elif"gate"in nom and"proj"notin nom: GROUPES["routeur"] += p.numel()else: GROUPES["experts"] += p.numel()print(f"total (formule config) : {N_TOTAL:,}")print(f"numel du checkpoint : {N_NUMEL:,} (ecart {100*abs(N_TOTAL-N_NUMEL)/N_TOTAL:.1f} %)")for g, n in GROUPES.items():print(f" {g:12s}{n:,}")print(f"actif par jeton : {N_ACTIF:,} ({100*N_ACTIF/N_TOTAL:.1f} % du total)")print(f"rapport capacite/calcul : {N_TOTAL/N_ACTIF:.1f}x")print(f"VRAM {VRAM_GIO:.2f} Gio | {VRAM_GIO*2**30/N_NUMEL:.2f} octets/parametre (bf16 plein)")
total (formule config) : 6,919,028,736
numel du checkpoint : 6,919,161,856 (ecart 0.0 %)
embeddings 206,045,184
attention 268,435,456
routeur 2,097,152
experts 6,442,584,064
actif par jeton : 1,281,884,160 (18.5 % du total)
rapport capacite/calcul : 5.4x
VRAM 12.89 Gio | 2.00 octets/parametre (bf16 plein)
Lecture : ~6,9 Md paramètres totaux, ~1,3 Md actifs — ≈ 19 % du modèle calcule pour chaque jeton, le reste est de la capacité en attente. Le 3.4c mesurait le même phénomène sur son jouet (MoE 566 784 params, 4,0× le dense iso-calcul) ; ici le rapport est ~5,4× avec 64 experts. À noter : OLMoE est MHA (16/16) — pas de GQA, contrairement au Mistral du TV-01 ; les deux axes d’économie du SOTA (attention groupée, experts épars) se lisent en croisant les deux notebooks.
3. Le corpus — le même que le TV-01
Mêmes 100 lignes de WikiText-2 validation que le TV-01 ([240:340]) : les nombres des deux notebooks du bloc B restent comparables.
from datasets import load_datasetds = load_dataset("Salesforce/wikitext", "wikitext-2-raw-v1", split="validation")LIGNES = [l for l in ds["text"] if l.strip()][240:340]CORPUS ="".join(LIGNES)ENC = tok(CORPUS, return_tensors="pt").input_ids.to(DEVICE)T_CORPUS = ENC.shape[1]print(f"corpus : {len(CORPUS):,} caracteres / {T_CORPUS:,} jetons")
corpus : 33,107 caracteres / 7,570 jetons
4. Le routage réel : quel jeton va chez quel expert
forward(..., output_router_logits=True) rend les logits du routeur de chaque couche — la matrice [jetons × 64 experts]avant le top-8, c’est-à-dire exactement ce que le MoECouche du 3.4c exposait comme probs completes du routeur. Le routage effectif : softmax puis top-8 (renormalisé par le modèle, mais l’affectation ne dépend que du top-8 brut).
t0 = time.time()with torch.no_grad(): RES = model(ENC, output_router_logits=True)T_FWD = time.time() - t0ROUTER = torch.stack(RES.router_logits, dim=0).float() # [couches, jetons, experts] (sans dim batch)if ROUTER.dim() ==3: ROUTER = ROUTER.unsqueeze(1) # -> [couches, 1, jetons, experts]L_, B_, T_, E_ = ROUTER.shapeassert E_ == N_EXPERTS and L_ == N_COUCHESprint(f"router_logits : {L_} couches x {T_} jetons x {E_} experts")PROBS = torch.softmax(ROUTER, dim=-1)TOPK_IDX = torch.topk(PROBS, k=TOPK, dim=-1).indices # affectation effective# perplexite du passage (gratuite : la passe de routage est aussi une passe avant)# par blocs de 512 : le .float() du vocab complet (50 304) d'un seul jeton couterait 1,7 Giotot_nll, tot_cible =0.0, 0with torch.no_grad():for i inrange(0, T_CORPUS -1, 512): j =min(i +512, T_CORPUS -1) tot_nll += torch.nn.functional.cross_entropy( RES.logits[0, i:j].float(), ENC[0, i +1:j +1], reduction="sum").item() tot_cible += j - iNLL = tot_nll / tot_ciblePPL = math.exp(NLL)print(f"ppl du passage : {PPL:.2f} ({NLL:.3f} nat/jeton) | forward {T_FWD:.1f} s")
router_logits : 16 couches x 7570 jetons x 64 experts
ppl du passage : 112.40 (4.722 nat/jeton) | forward 3.0 s
4.1 La charge : 64 experts ne servent pas à parts égales
AFFECT = TOPK_IDX.reshape(L_, -1) # [couches, jetons*k]COUNTS = torch.zeros(L_, E_, device=DEVICE)for l inrange(L_): COUNTS[l] = torch.bincount(AFFECT[l], minlength=E_)PARTS = COUNTS / COUNTS.sum(-1, keepdim=True) # distribution de charge par coucheENT_CHARGE =-(PARTS * PARTS.clamp_min(1e-12).log()).sum(-1) # entropie de chargeENT_MAX = math.log(E_)PART_MAX = PARTS.max(-1).valuesTOP3 = PARTS[0].topk(3)print("equilibre parfait : part", f"{1/E_:.4f}", "| entropie max", f"{ENT_MAX:.3f} (ln {E_})")print(f"entropie de charge par couche : min {ENT_CHARGE.min():.3f} | moy {ENT_CHARGE.mean():.3f} | max {ENT_CHARGE.max():.3f}")print(f"part max par couche : min {PART_MAX.min():.4f} | moy {PART_MAX.mean():.4f} | max {PART_MAX.max():.4f}")print(f"couche 0 : top-3 experts {TOP3.indices.tolist()} parts {[round(v,4) for v in TOP3.values.tolist()]}")print(f"couche 0 : 3 experts les moins charges parts {[round(v,4) for v in PARTS[0].topk(3, largest=False).values.tolist()]}")NOTE_CHARGE ="les 64 experts ne sont pas equirepartis - c'est une distribution apprise, pas un hasard"print(NOTE_CHARGE)
equilibre parfait : part 0.0156 | entropie max 4.159 (ln 64)
entropie de charge par couche : min 3.794 | moy 3.892 | max 4.032
part max par couche : min 0.0431 | moy 0.0601 | max 0.0862
couche 0 : top-3 experts [17, 41, 46] parts [0.0576, 0.0439, 0.0399]
couche 0 : 3 experts les moins charges parts [0.0005, 0.0007, 0.0019]
les 64 experts ne sont pas equirepartis - c'est une distribution apprise, pas un hasard
Lecture : l’entropie de charge est sensiblement en dessous de ln(64) ≈ 4,16 — le routage appris concentre le trafic sur certains experts. C’est voulu : l’équilibrage parfait ferait du MoE un dense au rabais ; l’entraînement (perte d’auxiliaire de charge, comme le alpha_load du 3.4c) tient la charge assez équilibrée pour que tous les experts apprennent, sans l’écraser vers l’uniforme.
4.1bis La perplexité de ce passage — un nombre honnête, avec son cadre
La passe de routage ci-dessus est aussi une passe avant : la ppl qui en sort (≈ 112) se lit sans chat template sur du texte brut — OLMoE-0125-Instruct est optimisé pour le dialogue formaté, pas pour la continuation libre ; le TV-01, lui, évaluait un modèle base (ppl 7,95). Au bloc B, la comparaison qui compte est structurelle (routage, charge, capacité) ; les ppl des deux notebooks ne sont pas échangeables, et on ne les présente pas comme telles.
4.2 Les deux entropies — le vrai routeur est plat, et c’est le finding
Le 3.4c a établi sur le jouet : entropie de routage et entropie de charge sont deux objets différents (son routeur était confiant — entropie nulle — sans effondrer la charge). Le vrai OLMoE est l’exact opposé : son routeur est plat — l’entropie de la softmax complète est quasi maximale — et pourtant la charge est structurée (§4.1). Autrement dit : aucun expert n’est « choisi avec confiance » ; le classement top-8 se joue sur des marges infimes, et c’est lui qui décide de tout.
ENT_ROUTE =-(PROBS * PROBS.clamp_min(1e-12).log()).sum(-1).mean().item()STD_LOGITS = ROUTER.std().item()P9 = torch.topk(PROBS, k=TOPK +1, dim=-1).valuesGAP_89 = (P9[..., TOPK -1] - P9[..., TOPK]).mean().item()print(f"entropie de Routage (softmax complete, toutes couches) : {ENT_ROUTE:.4f} / {ENT_MAX:.4f} (quasi max)")print(f"ecart-type des logits du routeur : {STD_LOGITS:.4f} (plat : les 64 scores sont presque egaux)")print(f"gap moyen top-8 -> top-9 en probabilite : {GAP_89:.2e} - la selection se joue sur des marges infimes")print(f"entropie de CHARGE (affectation top-8) : {ENT_CHARGE.mean():.3f} / {ENT_MAX:.3f}")VERDICT_DEUX_ENTROPIES ="routeur plat, charge structuree : le classement top-8, pas la confiance softmax, decide"print(VERDICT_DEUX_ENTROPIES)
entropie de Routage (softmax complete, toutes couches) : 4.1587 / 4.1589 (quasi max)
ecart-type des logits du routeur : 0.0176 (plat : les 64 scores sont presque egaux)
gap moyen top-8 -> top-9 en probabilite : 4.04e-05 - la selection se joue sur des marges infimes
entropie de CHARGE (affectation top-8) : 3.892 / 4.159
routeur plat, charge structuree : le classement top-8, pas la confiance softmax, decide
4.3 La capacité : OLMoE est dropless, le 3.4c jetait
Le 3.4c mesurait qu’avec un facteur de capacité c = 1,0, 6 affectations sur 8 étaient jetées — le filet de sécurité anti-effondrement a un prix. OLMoE en production est dropless : chaque jeton va chez ses 8 experts, point. Le prix du filet, rejoué sur la charge mesurée ci-dessus : si l’on imposait une capacité par expert, quelle fraction des affectations survivrait ?
def taux_servis(parts, c, k=TOPK, t=T_CORPUS): capacite = c * t * k / N_EXPERTS # jetons par expert sous charge uniforme servis = torch.clamp(parts * t * k, max=capacite) # chaque expert plafonne a sa capacitereturn servis.sum().item() / (t * k)for c in (1.0, 1.25, 1.5, 2.0):print(f"c = {c:.2f} : {100*taux_servis(PARTS.mean(0), c):.1f} % des affectations servies")NOTE_CAPACITE ="meme c genereux, la charge apprise fait jeter - voila pourquoi OLMoE est dropless"print(NOTE_CAPACITE)
c = 1.00 : 91.5 % des affectations servies
c = 1.25 : 98.4 % des affectations servies
c = 1.50 : 99.9 % des affectations servies
c = 2.00 : 100.0 % des affectations servies
meme c genereux, la charge apprise fait jeter - voila pourquoi OLMoE est dropless
5. Spécialisation : les experts voient-ils des jetons différents ?
Si tous les experts voyaient les mêmes jetons, le MoE serait un dense déguisé. Test simple : classer les jetons du corpus (lettres, chiffres, ponctuation, espace) et comparer la distribution de charge moyenne par classe — un expert spécialisé devrait s’écarter nettement entre classes.
def classe_jeton(i): s = tok.decode([ENC[0, i].item()]).strip()ifnot s:return"espaces"ifany(ch.isdigit() for ch in s):return"chiffres"ifall(not ch.isalnum() for ch in s):return"ponctuation"return"lettres"CLASSES = [classe_jeton(i) for i inrange(T_CORPUS)]UNIQ =sorted(set(CLASSES))print("classes :", {u: CLASSES.count(u) for u in UNIQ})
DISTRIBS = {}for u in UNIQ: mask = torch.tensor([c == u for c in CLASSES], device=DEVICE)# charge moyenne par expert sur les jetons de la classe, couche 0 (la plus interpretable) sel = TOPK_IDX[0, 0][mask] # [n_cls, k] d = torch.bincount(sel.reshape(-1), minlength=E_).float() DISTRIBS[u] = d / d.sum()ECART_MAX =max((DISTRIBS[a] - DISTRIBS[b]).abs().max().item()for a in UNIQ for b in UNIQ if a < b)champ =max(((DISTRIBS[a] - DISTRIBS[b]).abs().max().item(), a, b)for a in UNIQ for b in UNIQ if a < b)print(f"ecart max de part entre deux classes : {ECART_MAX:.4f} (entre '{champ[1]}' et '{champ[2]}')")print("un expert specialise se voit : sa part bouge selon la classe du jeton")NOTE_SPECIALISATION ="l'ecart existe mais reste modere - la specialisation OLMoE est douce, pas un expert par classe"print(NOTE_SPECIALISATION)
ecart max de part entre deux classes : 0.0911 (entre 'chiffres' et 'espaces')
un expert specialise se voit : sa part bouge selon la classe du jeton
l'ecart existe mais reste modere - la specialisation OLMoE est douce, pas un expert par classe
6. Le contrat MoE à l’échelle : capacité ×5,4, calcul ~constant
Le 3.4c mesurait sur son jouet : MoE 4,0× les paramètres du dense iso-calcul pour 0,75× son temps. La même lecture à l’échelle : le temps par jeton du OLMoE (~1,3 Md actifs) contre ce que coûterait un dense qui calcule ses 6,9 Md paramètres.
def bench_forward(t_tokens, n=3): x = ENC[:, :t_tokens] torch.cuda.synchronize() t0 = time.time()for _ inrange(n):with torch.no_grad(): model(x) torch.cuda.synchronize()return (time.time() - t0) / nfor t in (256, 512, 1024):if t <= T_CORPUS: ms = bench_forward(t) *1000print(f"forward T={t:4d} : {ms:7.1f} ms ({ms/t*1000:.0f} us/jeton, ~{N_ACTIF/1e6:.0f} M params actifs/jeton)")BUDGET_DENSE = N_TOTAL / N_ACTIF # un dense qui calcule tout coute ce rapport en FLOPsprint(f"un dense iso-parametres couterait ~{BUDGET_DENSE:.1f}x le calcul par jeton du MoE")
forward T= 256 : 704.5 ms (2752 us/jeton, ~1282 M params actifs/jeton)
forward T= 512 : 864.2 ms (1688 us/jeton, ~1282 M params actifs/jeton)
forward T=1024 : 850.8 ms (831 us/jeton, ~1282 M params actifs/jeton)
un dense iso-parametres couterait ~5.4x le calcul par jeton du MoE
7. Tableau comparatif — from scratch (3.4c) vs industriel (TV-02)
Item 7 de l’epic, étendu au MoE. Les constantes 3.4c sont les nombres committés de la PR #16152 ; les colonnes TV-02 sont mesurées dans ce notebook.
import inspectimport pathlibLOC_34C =431# LOC effectives du 3.4c-MoE-from-scratch (constantes commitees, PR #16152)src_path = pathlib.Path(inspect.getsourcefile(type(model)))LOC_INDUSTRIE =len(src_path.read_text(encoding="utf-8").splitlines())lignes = [ ("echelle", "0,57 M params (jouet)", f"{N_TOTAL/1e9:.2f} Md params (entraîne sur ~5 T jetons)"), ("experts / top-k", "8 / top-2", f"{N_EXPERTS} / top-{TOPK}"), ("actif par jeton", "4,0x le dense iso-calcul", f"{100*N_ACTIF/N_TOTAL:.0f}% du total ({N_TOTAL/N_ACTIF:.1f}x capacite)"), ("capacite", "c=1,0 : 6/8 affectations jetees", "dropless (aucun jeton jete)"), ("equilibrage", "perte L_aux (alpha=0,01)", "perte de charge integree a l'entrainement"), ("entropie de charge", "jouet : equilibre ~0,12 par expert", f"moy {ENT_CHARGE.mean():.3f} / {ENT_MAX:.3f} (ln {E_})"), ("ppl de la tache", "tache synthetique : 1,00", f"WikiText-2 brut (instruct) : {PPL:.2f}"), ("LOC", f"{LOC_34C} (3.4c)", "194"),]print(f"{'mesure':28s} | {'3.4c from scratch':34s} | TV-02 industriel (mesure ici)")print("-"*110)for a, b, c in lignes:print(f"{a:28s} | {b:34s} | {c}")print(f"\ncote industrie : {src_path.name} seul = {LOC_INDUSTRIE} lignes (hors quantification, generation, noyaux fused)")
mesure | 3.4c from scratch | TV-02 industriel (mesure ici)
--------------------------------------------------------------------------------------------------------------
echelle | 0,57 M params (jouet) | 6.92 Md params (entraîne sur ~5 T jetons)
experts / top-k | 8 / top-2 | 64 / top-8
actif par jeton | 4,0x le dense iso-calcul | 19% du total (5.4x capacite)
capacite | c=1,0 : 6/8 affectations jetees | dropless (aucun jeton jete)
equilibrage | perte L_aux (alpha=0,01) | perte de charge integree a l'entrainement
entropie de charge | jouet : equilibre ~0,12 par expert | moy 3.892 / 4.159 (ln 64)
ppl de la tache | tache synthetique : 1,00 | WikiText-2 brut (instruct) : 112.40
LOC | 431 (3.4c) | 194
cote industrie : modeling_olmoe.py seul = 723 lignes (hors quantification, generation, noyaux fused)
Lecture du tableau : le jouet du 3.4c reproduit fidèlement les pièces du mécanisme (routeur, top-k, capacité, perte d’équilibrage) — c’est exactement pourquoi il précède celui-ci. Ce que seul l’industriel montre : la specialisation douce d’un routage appris sur données réelles, un équilibrage tenu à l’échelle de 64 experts, et le choix dropless qui suit naturellement de la charge mesurée en §4.3.
8. Exercices
Exercice 1 — Charge top-1 contre top-8
L’entropie de charge ci-dessus est calculée sur les affectations top-8. Recalculez-la en ne comptant que le premier choix du routeur (TOPK_IDX[..., 0]), et comparez : la concentration du premier choix dit-elle la même chose que la concentration des 8 ?
def entropie_charge_top1():# Compter les premiers choix (TOPK_IDX[..., 0]) par expert et par couche,# puis retourner l'entropie moyenne des parts.returnNone# TODO etudiant
Exercice 2 — Spécialisation : début contre fin de corpus
La distribution de charge de la §5 compare des classes de jetons. Comparez maintenant la distribution par expert des 64 premiers jetons du corpus contre les 64 derniers (couche 0) : si les experts étaient interchangeables, les deux distributions seraient identiques à l’échantillonnage près.
def ecart_debut_fin():# Distribution de charge des ENC[:, :64] contre ENC[:, -64:] (couche 0),# puis ecart maximal absolu entre les deux parts.returnNone# TODO etudiant
Exercice 3 — Capacité : quel c pour ne rien jeter ?
La §4.3 rejoue le filet de capacité du 3.4c sur la charge mesurée, couche moyenne. Faites-le par couche : pour chaque couche, trouvez le plus petit facteur de capacité c (au dixième) tel que 99 % des affectations soient servies. Le résultat devrait confirmer que le dropless n’est pas un luxe.
def c_min_par_couche(seuil=0.99):# Pour chaque couche : plus petit c (pas 0,1) tel que taux_servis(PARTS[l], c) >= seuil.returnNone# TODO etudiant
9. Conclusion
Le routage d’un vrai MoE est mesurable directement (output_router_logits) : les logits du routeur du 3.4c ont leur exacte contrepartie industrielle.
Le routeur d’OLMoE est plat : entropie de softmax quasi maximale, écarts inter-experts de l’ordre de 1e-2 en logits — et pourtant la charge est structurée (3,89 contre 4,16). C’est le classement top-8 sur des marges infimes, pas une « confiance » softmax, qui répartit le travail — et c’est précisément ce que la perte auxiliaire d’équilibrage négocie pendant l’entraînement.
Dropless : la charge mesurée rend le filet de capacité du 3.4c (6/8 jetés à c = 1,0) impraticable à cette échelle — OLMoE assume la promesse : chaque jeton va chez ses 8 experts.
Le contrat tenu : ~5,4× la capacité pour ~19 % du calcul par jeton.
Résidus honnêtes : la spécialisation mesurée ici reste douce (écarts de parts entre classes modérés) — la vraie spécialisation d’OLMoE se lit à l’échelle de l’entraînement (5 000 Md de jetons), pas sur 100 lignes de WikiText-2 ; la ppl du passage (~112) est celle d’un modèle instruct évalué en texte brut, pas échangeable avec la ppl 7,95 du TV-01 (modèle base) ; et la comparaison de coût avec le 3.4c traverse trois ordres de grandeur d’échelle — les rapports sont indicatifs, pas des benchmarks.