Ce qui est vérifié¶
Ce chapitre donne le script complet qui reconstruit les chiffres annoncés par DeepSeek, et ce que ses résultats établissent.
La méthode¶
Le script ne télécharge aucun poids et n'exécute aucun modèle. Il part uniquement :
- du
config.jsonpublié avec les poids ; - des formats de quantification lus dans
inference/model.pydu même dépôt ; - du
config.jsonpublic dedeepseek-llm-67b-base, pour la comparaison historique ; - de la taille des fichiers du dépôt, mesurée via l'API Hugging Face.
Il n'utilise aucune dépendance : uniquement de l'arithmétique sur les dimensions. Il est donc reproductible partout, immédiatement.
Les résultats¶
====================================================================
DeepSeek-V4.1-Flash — vérifications depuis config.json
====================================================================
[1] Paramètres du squelette (annoncé : 552 G)
moe : 545.00 G
attn : 5.13 G
emb : 1.32 G
vision : 0.49 G
TOTAL : 551.93 G écart = 0.01 %
[2] Paramètres activés par jeton (annoncé : 8 G préremplissage / 16 G décodage)
préremplissage : 7.61 G
décodage : 15.70 G
rapport : 2.06 (attendu ≈ 2, conséquence directe du CED)
[3] Paramètres Engram (annoncé : 196 G)
2 modules × 384,006,168 lignes × 256 = 196.61 G
[4] Cache KV global (annoncé : 890 octets/jeton)
une entrée main KV : 288.0 o (512 canaux, 4,5 bits/canal)
une entrée indexer K: 68.0 o (128 canaux, 4,25 bits/canal)
couche 2 (encodeur, ratio 2) : 144.0 + 34.0 = 178.0 o/jeton
couche 8 (encodeur, ratio 2) : 144.0 + 34.0 = 178.0 o/jeton
couche 14 (encodeur, ratio 2) : 144.0 + 34.0 = 178.0 o/jeton
couche 20 (décodeur, ratio 1) : 288.0 + 68.0 = 356.0 o/jeton
TOTAL : 890.0 octets/jeton
[5] DeepSeek-V1 67B (figure 1(b) du rapport : 389 120 octets/jeton)
reconstruit : 389,120 octets/jeton (380 KiO) — exact
rapport contre V4.1-Flash : 437.2× (texte : « ≈ 437× »)
[6] DeepSeek-V4-Flash (figure 1(b) du rapport : 3 514 octets/jeton)
reconstruit : 3309 octets/jeton — 5.8 % d'écart
rapport publié : 3.95× rapport reconstruit : 3.72×
L'écart tient à des hypothèses invérifiables sur le cache FP8 de V4.
[7] Cache de la fenêtre glissante (hors cache global, borné par requête)
40 couches × 128 jetons × 544 o = 2.7 MiO par séquence, indépendant de la longueur
[8] Contexte de 1 M de jetons
cache global : 0.87 Gio
même contexte sur un V1 67B : 0.37 Tio
[9] Taille du dépôt (mesurée via l'API Hugging Face : 510,30 Go)
experts FP4 (dont DSpark) : 279.8 Go
reste du squelette FP8 : 6.9 Go
tables Engram FP8 : 196.6 Go
TOTAL estimé : 483.4 Go (95 % du dépôt)
Manquant : 26.9 Go. Une échelle FP32 par bloc de 32 canaux sur les
768 M lignes Engram en représenterait 24,6 — hypothèse plausible, non certifiée.
Toutes les assertions passent.
Ce que chaque vérification établit¶
[1] Les 552 G de paramètres — écart de 0,01 %¶
La reconstruction retrouve 551,93 G contre 552 G annoncés. Un écart de 0,01 % sur un compte de cette taille ne peut pas être une coïncidence : il établit que la convention de comptage de DeepSeek inclut bien l'encodeur visuel et le projecteur, et exclut les paramètres Engram.
La ventilation est instructive : 98,7 % des paramètres sont dans les experts MoE. Tout le reste de l'architecture — attention, plongements, vision — tient dans 1,3 %.
[2] Les 8 G / 16 G activés — le rapport de 2¶
Le recalcul donne 7,61 G au préremplissage et 15,70 G au décodage, soit un rapport de 2,06.
Ce rapport n'est pas ajusté : il découle mécaniquement du fait que le préremplissage n'exécute que 20 couches sur 40. C'est la signature arithmétique du CED, et sa présence confirme que le mécanisme fonctionne comme décrit.
L'écart résiduel de 5 % avec les chiffres arrondis annoncés s'explique par les composants non modélisés dans le script : les projections Engram, les têtes du brouillonneur DSpark et les paramètres de normalisation.
[3] Les 196 G d'Engram¶
Le calcul est direct : engram_num_embeddings donne le nombre de lignes par
module, engram_head_dim la largeur d'une ligne. L'écart de 0,3 % vient
uniquement de l'arrondi de l'annonce.
Cela confirme au passage la structure décrite dans le rapport technique : \(8 \text{ têtes} \times 3 \text{ ordres} = 24\) tables d'environ 16 M entrées chacune, soit 384 M lignes par module.
[4] Les 890 octets par jeton — exactement¶
C'est la vérification la plus significative du rapport, parce qu'elle tombe au dixième d'octet près sur un chiffre qui dépend de six paramètres indépendants : le nombre de couches sources, leurs ratios de compression, les deux dimensions de latent, et les deux formats FP4 distincts.
Une coïncidence est exclue. Le calcul établit donc, sans que ce soit écrit nulle part explicitement :
- seules les quatre couches sources (2, 8, 14, 20) contribuent au cache global ;
- le cache global comprend le cache principal et les clés d'indexeur, et rien d'autre — ni les requêtes, ni le cache de fenêtre glissante ;
- les facteurs d'échelle sont comptés dans le chiffre annoncé (les ignorer donnerait 832 octets) ;
- les deux blocs de quantification diffèrent — 16 pour le cache principal, 32 pour l'indexeur (un bloc de 32 partout donnerait 880 octets).
[5] DeepSeek-V1 — le chiffre de la figure, retrouvé exactement¶
La figure 1(b) du rapport porte 389 120 octets par jeton pour
« DeepSeek-V1 », sans dire de quel modèle il s'agit. Le calcul depuis le
config.json de deepseek-llm-67b-base — 95 couches, 8 têtes clé-valeur,
dimension de tête 128, cache BF16 :
Exactement le chiffre publié. Cela identifie le modèle de référence de
la figure : c'est bien deepseek-llm-67b, et non un autre membre de la première
génération. Le rapport qui en découle, \(389\,120 / 890 = 437{,}2\), confirme les
« approximativement 437× » du texte.
[6] DeepSeek-V4-Flash — 5,8 % d'écart, hypothèses assumées¶
La figure publie 3 514 octets par jeton pour V4-Flash, soit un facteur 3,95× — que la figure arrondit à 3,9×, un peu en deçà du « environ 1/4 » du corps du texte.
La reconstruction donne 3 309 octets, soit 5,8 % en dessous. Contrairement au cas de V1, elle ne tombe donc pas juste, et deux hypothèses non vérifiables peuvent l'expliquer : le format exact des échelles du cache principal FP8 de V4, et la présence ou non d'un indexeur dans les couches HCA de ratio 128, que le script suppose présent.
Ce facteur est donc classé revendiqué et cohérent, et non vérifié.
[9] La taille du dépôt — 95 % expliqués¶
Les 48 fichiers safetensors pèsent 510,30 Go. La reconstruction en explique
483,4, soit 95 %.
Cela confirme deux choses non documentées : les experts sont livrés déjà en FP4 dans les poids publiés, et les 196 G de paramètres Engram y sont inclus — ce n'est pas un module à télécharger séparément.
L'écart de 26,9 Go correspondrait, à 2 Go près, aux facteurs d'échelle FP32 par bloc de 32 canaux sur les 768 millions de lignes Engram. C'est une hypothèse cohérente, pas une certitude.
Le script complet¶
Enregistrez-le sous deepseek-v41-verification.py et exécutez-le avec
python3 deepseek-v41-verification.py. Aucune dépendance n'est requise.
#!/usr/bin/env python3
"""Vérifications arithmétiques des chiffres annoncés pour DeepSeek-V4.1-Flash.
Tout part du `config.json` publié (deepseek-ai/DeepSeek-V4.1-Flash) et des formats
de quantification lus dans `inference/model.py` du même dépôt. Aucune dépendance,
aucun poids téléchargé : uniquement de l'arithmétique sur les dimensions.
Usage : python3 deepseek-v41-verification.py
"""
# --- config.json de DeepSeek-V4.1-Flash (text_config, sauf mention contraire) ---
V41 = dict(
hidden_size=5120,
num_hidden_layers=40,
num_attention_heads=64,
head_dim=512,
q_lora_rank=1280,
o_lora_rank=1024,
o_groups=8,
moe_intermediate_size=2304,
n_routed_experts=384,
n_shared_experts=1,
num_experts_per_tok=6,
vocab_size=129280,
index_n_heads=32,
index_head_dim=128,
kv_source_layer_ids=[2, 8, 14, 20],
index_source_layer_ids=[2, 8, 14, 20, 24, 28, 32, 36],
compress_ratios=[0, 0] + [2] * 18 + [1] * 20 + [0, 0, 0], # 43 entrées : 40 + 3 DSpark
engram_num_embeddings=[384006168, 384016682],
engram_head_dim=256,
sliding_window=128,
)
VISION = dict(num_hidden_layers=32, hidden_size=1024, intermediate_size=2816, patch_size=14)
G = 1e9
def moe_params_per_layer(c, n_experts):
"""SwiGLU : 3 matrices (gate, up, down) par expert."""
return n_experts * 3 * c["hidden_size"] * c["moe_intermediate_size"]
def attention_params_per_layer(c, layer_id):
d, hd, nh = c["hidden_size"], c["head_dim"], c["num_attention_heads"]
p = 0
p += d * c["q_lora_rank"] # wq_a
p += c["q_lora_rank"] * nh * hd # wq_b
p += d * hd # wkv (KV de la fenêtre glissante)
p += c["o_groups"] * c["o_lora_rank"] * (nh * hd // c["o_groups"]) # wo_a bloc-diagonal
p += c["o_groups"] * c["o_lora_rank"] * d # wo_b
if layer_id in c["kv_source_layer_ids"]: # compresseur : wkv (+ wgate si ratio > 1)
p += d * hd * (2 if c["compress_ratios"][layer_id] > 1 else 1)
if layer_id in c["index_source_layer_ids"]: # indexeur
p += c["q_lora_rank"] * c["index_n_heads"] * c["index_head_dim"] # wq_b
p += d * c["index_n_heads"] # weights_proj
if layer_id in c["kv_source_layer_ids"]:
p += hd * c["index_head_dim"] # wk
return p
def vision_params(v):
d, inter = v["hidden_size"], v["intermediate_size"]
per_layer = 4 * d * d + 3 * d * inter
patch = 3 * v["patch_size"] ** 2 * d
return v["num_hidden_layers"] * per_layer + patch
def check_backbone_params(c=V41, v=VISION):
L, all_exp = c["num_hidden_layers"], c["n_routed_experts"] + c["n_shared_experts"]
moe = L * moe_params_per_layer(c, all_exp)
attn = sum(attention_params_per_layer(c, i) for i in range(L))
emb = 2 * c["vocab_size"] * c["hidden_size"] # table d'entrée + tête de sortie
vis = vision_params(v)
proj = (v["hidden_size"] * 9) * c["hidden_size"] + c["hidden_size"] ** 2 # projecteur 2 couches
total = moe + attn + emb + vis + proj
return dict(moe=moe, attn=attn, emb=emb, vision=vis + proj, total=total)
def check_activated_params(c=V41):
"""CED : le préremplissage n'exécute que l'encodeur (20 couches), le décodage les 40."""
act_exp = c["n_shared_experts"] + c["num_experts_per_tok"]
L, half = c["num_hidden_layers"], c["num_hidden_layers"] // 2
moe1 = moe_params_per_layer(c, act_exp)
decode = L * moe1 + sum(attention_params_per_layer(c, i) for i in range(L))
decode += c["vocab_size"] * c["hidden_size"] # tête de sortie
prefill = half * moe1 + sum(attention_params_per_layer(c, i) for i in range(half))
# sous CED, les projections KV des couches décodeur partent de H_20 : seule la couche 20 compresse
prefill += attention_params_per_layer(c, 20) - (
c["hidden_size"] * c["q_lora_rank"]
+ c["q_lora_rank"] * c["num_attention_heads"] * c["head_dim"]
)
return dict(prefill=prefill, decode=decode, ratio=decode / prefill)
def check_engram_params(c=V41):
return sum(n * c["engram_head_dim"] for n in c["engram_num_embeddings"])
def fp4_bytes(n_channels, block, scale_bits):
"""E2M1 (4 bits/valeur) + une échelle par bloc de `block` canaux."""
return n_channels * 4 / 8 + (n_channels / block) * scale_bits / 8
def check_global_kv_cache(c=V41):
"""Cache KV global = main KV + indexer K, tous deux produits par les seules couches sources.
Formats lus dans inference/model.py :
- main KV : E2M1, une échelle E4M3 par bloc de 16 canaux -> 4,5 bits/canal
- indexer K: E2M1, une échelle E8M0 par bloc de 32 canaux -> 4,25 bits/canal
Une couche de ratio m ne stocke qu'une entrée par m jetons.
"""
main_entry = fp4_bytes(c["head_dim"], 16, 8)
idx_entry = fp4_bytes(c["index_head_dim"], 32, 8)
per_token = 0.0
detail = []
for lid in c["kv_source_layer_ids"]:
m = c["compress_ratios"][lid]
contrib = main_entry / m + idx_entry / m
detail.append((lid, m, main_entry / m, idx_entry / m))
per_token += contrib
return per_token, main_entry, idx_entry, detail
def check_v1_ratio(v41_bytes):
"""DeepSeek-V1 67B : MHA groupée classique, cache en BF16.
config.json de deepseek-ai/deepseek-llm-67b-base : 95 couches, 8 têtes KV, head_dim 128.
"""
layers, kv_heads, head_dim, bytes_per_val = 95, 8, 8192 // 64, 2
v1 = 2 * layers * kv_heads * head_dim * bytes_per_val # K et V
return v1, v1 / v41_bytes
def check_v4_flash_ratio(v41_bytes):
"""DeepSeek-V4-Flash, reconstruit depuis son config.json publié.
Hypothèses explicites : main KV en FP8 (E4M3) avec une échelle par bloc de 16,
indexer K en FP4 comme dans V4.1. La composition exacte n'est pas documentée :
ce calcul donne un ordre de grandeur, pas une valeur certifiée.
La figure 1(b) du rapport technique publie 3 514 octets/jeton pour ce modèle.
"""
ratios = [0, 0] + [4, 128] * 20 + [4, 0] # compress_ratios du config.json (43 couches)
head_dim, index_head_dim = 512, 128
main_entry = head_dim * 1 + (head_dim / 16) * 1 # FP8 + échelle E4M3 par 16
idx_entry = fp4_bytes(index_head_dim, 32, 8)
per_token = sum((main_entry + idx_entry) / m for m in ratios if m)
return per_token, per_token / v41_bytes
def main():
print("=" * 68)
print("DeepSeek-V4.1-Flash — vérifications depuis config.json")
print("=" * 68)
p = check_backbone_params()
print("\n[1] Paramètres du squelette (annoncé : 552 G)")
for k in ("moe", "attn", "emb", "vision"):
print(f" {k:>8} : {p[k]/G:9.2f} G")
print(f" {'TOTAL':>8} : {p['total']/G:9.2f} G "
f"écart = {abs(p['total']/G - 552)/552*100:.2f} %")
assert abs(p["total"] / G - 552) / 552 < 0.02
a = check_activated_params()
print("\n[2] Paramètres activés par jeton (annoncé : 8 G préremplissage / 16 G décodage)")
print(f" préremplissage : {a['prefill']/G:6.2f} G")
print(f" décodage : {a['decode']/G:6.2f} G")
print(f" rapport : {a['ratio']:6.2f} (attendu ≈ 2, conséquence directe du CED)")
assert abs(a["decode"] / G - 16) / 16 < 0.05
assert abs(a["prefill"] / G - 8) / 8 < 0.10
e = check_engram_params()
print(f"\n[3] Paramètres Engram (annoncé : 196 G)")
print(f" 2 modules × {V41['engram_num_embeddings'][0]:,} lignes × "
f"{V41['engram_head_dim']} = {e/G:.2f} G")
assert abs(e / G - 196) / 196 < 0.01
kv, main_entry, idx_entry, detail = check_global_kv_cache()
print(f"\n[4] Cache KV global (annoncé : 890 octets/jeton)")
print(f" une entrée main KV : {main_entry:6.1f} o (512 canaux, 4,5 bits/canal)")
print(f" une entrée indexer K: {idx_entry:6.1f} o (128 canaux, 4,25 bits/canal)")
for lid, m, mo, io in detail:
zone = "encodeur" if lid < 20 else "décodeur"
print(f" couche {lid:>2} ({zone}, ratio {m}) : {mo:6.1f} + {io:5.1f} = {mo+io:6.1f} o/jeton")
print(f" TOTAL : {kv:.1f} octets/jeton")
assert kv == 890.0, kv
v1, r1 = check_v1_ratio(kv)
print(f"\n[5] DeepSeek-V1 67B (figure 1(b) du rapport : 389 120 octets/jeton)")
print(f" reconstruit : {v1:,} octets/jeton ({v1/1024:.0f} KiO) — exact")
print(f" rapport contre V4.1-Flash : {r1:.1f}× (texte : « ≈ 437× »)")
assert v1 == 389_120
assert abs(r1 - 437) < 1
v4, r4 = check_v4_flash_ratio(kv)
print(f"\n[6] DeepSeek-V4-Flash (figure 1(b) du rapport : 3 514 octets/jeton)")
print(f" reconstruit : {v4:.0f} octets/jeton — {abs(v4-3514)/3514*100:.1f} % d'écart")
print(f" rapport publié : {3514/kv:.2f}× rapport reconstruit : {r4:.2f}×")
print(" L'écart tient à des hypothèses invérifiables sur le cache FP8 de V4.")
print("\n[7] Cache de la fenêtre glissante (hors cache global, borné par requête)")
swa = V41["num_hidden_layers"] * V41["sliding_window"] * (
V41["head_dim"] * 1 + V41["head_dim"] / 16
)
print(f" 40 couches × 128 jetons × 544 o = {swa/1024/1024:.1f} MiO par séquence, "
f"indépendant de la longueur")
print("\n[8] Contexte de 1 M de jetons")
print(f" cache global : {kv * 1_048_576 / 1024**3:.2f} Gio")
print(f" même contexte sur un V1 67B : {v1 * 1_048_576 / 1024**4:.2f} Tio")
print("\n[9] Taille du dépôt (mesurée via l'API Hugging Face : 510,30 Go)")
experts = 40 * moe_params_per_layer(V41, 385)
dspark = 3 * 128 * 3 * V41["hidden_size"] * V41["moe_intermediate_size"]
reste = p["total"] - experts
o_experts = (experts + dspark) * (4 + 8 / 1024) / 8 # FP4, échelle E8M0 par bloc 32×32
o_reste = reste * 1.0 # FP8 dominant, un peu de BF16
o_engram = e * 1.0 # tables FP8
est = (o_experts + o_reste + o_engram) / 1e9
print(f" experts FP4 (dont DSpark) : {o_experts/1e9:6.1f} Go")
print(f" reste du squelette FP8 : {o_reste/1e9:6.1f} Go")
print(f" tables Engram FP8 : {o_engram/1e9:6.1f} Go")
print(f" TOTAL estimé : {est:6.1f} Go ({est/510.30*100:.0f} % du dépôt)")
print(f" Manquant : {510.30 - est:.1f} Go. Une échelle FP32 par bloc de 32 canaux sur les")
print(" 768 M lignes Engram en représenterait 24,6 — hypothèse plausible, non certifiée.")
print("\nToutes les assertions passent.")
if __name__ == "__main__":
main()
Chapitre suivant : Limites et questions ouvertes