Aller au contenu

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.json publié avec les poids ;
  • des formats de quantification lus dans inference/model.py du même dépôt ;
  • du config.json public de deepseek-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

\[ 2 \times 384\,006\,168 \times 256 = 196{,}61\ \text{G} \]

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 :

  1. seules les quatre couches sources (2, 8, 14, 20) contribuent au cache global ;
  2. 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 ;
  3. les facteurs d'échelle sont comptés dans le chiffre annoncé (les ignorer donnerait 832 octets) ;
  4. 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 :

\[ 2 \times 95 \times 8 \times 128 \times 2 = 389\,120 \]

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