40 layers › 2 groups of 6 layers › 5 reusing layers › Mixture of experts › Router
Top 6, normalized
×learned(scores + bias).topk(self.topk, dim=-1)[1] … · 818–826
Choose the 6 experts with the best biased scores, and weight them by their unbiased scores.
- in
- [B batch, T positions, 384 experts]
- out
- [B batch, T positions, 384 experts]
- parameters
- 768 · × 2 groups × 5 layers = 7,680
Two selection biases of 384 values: one for text positions and a second, bias_vl, for image positions (model.py 806–807, 819–820). Both are in the checkpoint; text never uses the second.
Parameters
768 parameters owned by this component
| Component | Calculation | Parameters |
|---|---|---|
| learned vector | 384 | |
| learned vector | 384 | |
| Per-instance total | 768 | |
Across 5 independent layers: parameters.
Derived from the pinned reference implementation and configuration; equal to the element counts of the checkpoint’s own tensors on the text path. The quantisation scales, the vision encoder and the DSpark blocks are not counted.
Codemodel.py 45–150, 792–827 · config.json 1–66
class Gate(nn.Module):
"""MoE gating. The correction bias steers expert selection only; the routing weights come from the
unbiased scores. Image-span tokens use a separate bias (training `noaux_tc_for_vl`)."""
def __init__(self, layer_id: int, args: ModelArgs):
super().__init__()
n_routed_experts, n_activated_experts = args.get_moe_config(layer_id)
self.dim = args.dim
self.topk = n_activated_experts
self.score_func = args.score_func
self.gate_temp = args.gate_temp
self.norm_topk_prob = args.norm_topk_prob
self.route_scale = args.route_scale
self.weight = nn.Parameter(torch.empty(n_routed_experts, args.dim))
self.bias = nn.Parameter(torch.empty(n_routed_experts, dtype=torch.float32))
self.bias_vl = nn.Parameter(torch.empty(n_routed_experts, dtype=torch.float32)) if args.vision_enabled else None
def forward(self, x: torch.Tensor, image_mask: torch.Tensor | None = None) -> tuple[torch.Tensor, torch.Tensor]:
"""x: [n, dim]; image_mask: [n] bool, True for tokens inside an image span."""
scores = linear(x.float(), self.weight.float()) / self.gate_temp
if self.score_func == "softmax":
scores = scores.softmax(dim=-1)
elif self.score_func == "sigmoid":
scores = scores.sigmoid()
else:
scores = F.softplus(scores).sqrt()
bias = self.bias
if image_mask is not None and self.bias_vl is not None:
bias = torch.where(image_mask.unsqueeze(-1), self.bias_vl, bias)
# the bias picks experts but does not scale them: weights come from the raw scores
indices = (scores + bias).topk(self.topk, dim=-1)[1]
weights = scores.gather(1, indices)
if self.norm_topk_prob and self.topk > 1:
weights /= weights.sum(dim=-1, keepdim=True) + 1e-20 # not norm_eps, matches training
weights *= self.route_scale
return weights, indicesclass ModelArgs:
"""Field names are exactly the config JSON keys. The defaults are a small model that
`python model.py` can run, not the released shapes -- though the scale-independent
values (norm_eps, score_func, hc_*, engram_*) do match it."""
# runtime limits rather than model shape: they size the KV caches
max_batch_size: int = 4
max_seq_len: int = 4096
temperature: float = 1
dtype: Literal["bf16", "fp8"] = "fp8"
expert_dtype: Literal["fp4"] | None = "fp4"
vocab_size: int = 129280
dim: int = 1024
moe_inter_dim: int = 1024
n_layers: int = 5
n_mtp_layers: int = 1 # extra draft layers appended after the backbone, indices n_layers..
n_heads: int = 16
# moe
n_routed_experts: int = 8
n_shared_experts: int = 1
n_activated_experts: int = 2
score_func: Literal["softmax", "sigmoid", "sqrtsoftplus"] = "sqrtsoftplus"
gate_temp: float = 1.0
norm_topk_prob: bool = True
route_scale: float = 1.0
swiglu_limit: float = 0.0
# attention: latent q/kv projections, plus a LoRA-factorised output projection over o_groups
q_lora_rank: int = 256
head_dim: int = 128
rope_head_dim: int = 32
norm_eps: float = 1e-20
o_groups: int = 8
o_lora_rank: int = 256
# sparse attention: every layer attends over a sliding window, and may add compressed KV on top
window_size: int = 128
# one entry per layer, MTP layers included: 0 = sliding window only, r = KV compressed r-to-1
compress_ratios: tuple[int, ...] = (0, 2, 2, 1, 1, 0)
# layers sharing a ratio also share one compressed KV and one indexer, produced by the first
kv_source_layers: tuple[int, ...] = (1, 3)
index_source_layers: tuple[int, ...] = (1, 3)
# rope, with YaRN extrapolation when original_seq_len > 0. Compressed KV rotates at its own
# theta because one latent stands for compress_ratio tokens, so its positions are further apart.
compress_rope_theta: float = 40000.0
original_seq_len: int = 0
rope_theta: float = 10000.0
rope_factor: float = 40
beta_fast: int = 32
beta_slow: int = 1
# the indexer: a small extra attention that scores compressed positions, so each query can keep
# just `index_topk` of them. Names match DeepSeek-V3.2-Exp, where this mechanism first appeared.
index_n_heads: int = 16
index_head_dim: int = 64
index_topk: int = 64
# candidate pre-filtering: candidate_source_layer < 0 turns it off and the other two are unused
candidate_source_layer: int = -1
candidate_topk_blocks: int = 0
candidate_block_size: int = 0
# hyper-connections: the residual stream is carried as hc_mult parallel copies
hc_mult: int = 4
hc_sinkhorn_iters: int = 20
hc_eps: float = 1e-6
# engram: n-gram hash lookups added into the residual stream at a few layers
engram_layer_ids: tuple[int, ...] = ()
engram_num_embeddings: tuple[int, ...] = () # unpadded table rows; each rank allocates ceil(rows / world_size)
engram_max_ngram_size: int = 1
engram_vocab_size: int = 0 # bucket size each (n-gram size, head) starts searching primes from
engram_n_heads: int = 0
engram_head_dim: int = 0
engram_pad_id: int = 2 # token that fills n-gram slots with no history; matches training
# size of the compressed tokenizer vocab; every hash multiplier is derived from it
engram_compressed_vocab_size: int = 0
# vision (VL); vision_n_layers == 0 disables the vision path
vision_n_layers: int = 0
vision_dim: int = 1024
vision_n_heads: int = 16
vision_inter_dim: int = 2816
vision_patch_size: int = 14
vision_rope_theta: float = 10000.0
vision_downsample_ratio: int = 3
vision_max_n_token: int = 1024
vision_min_pixels: int = 544 * 544
vision_max_wh_ratio: int | None = None
# raw id of <|deepseek_image|>; every position of an image span carries this id in input_ids
image_token_id: int = 129264
# dspark draft head. Only the forward pass is implemented here -- nothing calls forward_spec,
# so these are read but the speculative-decoding loop itself is out of scope for this repo.
dspark_block_size: int = 0
dspark_noise_token_id: int = 0
dspark_target_layer_ids: tuple[int, ...] = ()
dspark_markov_rank: int = 256
dspark_n_routed_experts: int = 0
dspark_n_activated_experts: int = 0
@property
def vision_enabled(self) -> bool:
return self.vision_n_layers > 0
def get_moe_config(self, layer_id: int) -> tuple[int, int]:
"""Return the routed/activated expert counts for a given layer."""
if layer_id < self.n_layers:
return self.n_routed_experts, self.n_activated_experts
return (
self.dspark_n_routed_experts or self.n_routed_experts,
self.dspark_n_activated_experts or self.n_activated_experts,
)
{
"vocab_size": 129280,
"dim": 5120,
"moe_inter_dim": 2304,
"n_layers": 40,
"n_mtp_layers": 3,
"dspark_block_size": 5,
"dspark_noise_token_id": 128799,
"dspark_target_layer_ids": [37, 38, 39],
"dspark_markov_rank": 256,
"dspark_n_routed_experts": 128,
"dspark_n_activated_experts": 3,
"n_heads": 64,
"n_routed_experts": 384,
"n_shared_experts": 1,
"n_activated_experts": 6,
"score_func": "sqrtsoftplus",
"route_scale": 1.5,
"swiglu_limit": 10.0,
"q_lora_rank": 1280,
"head_dim": 512,
"rope_head_dim": 64,
"norm_eps": 1e-20,
"o_groups": 8,
"o_lora_rank": 1024,
"window_size": 128,
"kv_source_layers": [2, 8, 14, 20],
"index_source_layers": [2, 8, 14, 20, 24, 28, 32, 36],
"original_seq_len": 65536,
"rope_theta": 10000,
"rope_factor": 16,
"beta_fast": 32,
"beta_slow": 1,
"index_n_heads": 32,
"index_head_dim": 128,
"index_topk": 512,
"candidate_source_layer": 20,
"candidate_topk_blocks": 2048,
"candidate_block_size": 8,
"hc_mult": 4,
"hc_sinkhorn_iters": 20,
"hc_eps": 1e-06,
"engram_layer_ids": [1, 14],
"engram_vocab_size": 16000000,
"engram_num_embeddings": [384006168, 384016682],
"engram_max_ngram_size": 4,
"engram_pad_id": 2,
"engram_compressed_vocab_size": 99092,
"dtype": "fp8",
"expert_dtype": "fp4",
"compress_rope_theta": 160000,
"compress_ratios": [0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0],
"vision_n_layers": 32,
"vision_dim": 1024,
"vision_n_heads": 16,
"vision_inter_dim": 2816,
"vision_patch_size": 14,
"vision_downsample_ratio": 3,
"vision_max_n_token": 1024,
"vision_min_pixels": 295936,
"vision_max_wh_ratio": null,
"image_token_id": 129264,
"engram_n_heads": 8,
"engram_head_dim": 256,
"vision_rope_theta": 10000
}Sources3
Direct evidence
The router: the square root of softplus of a projection, a selection bias (a second one at image positions), the best six, their unbiased scores normalised and scaled.
gate · deepseek-ai/DeepSeek-V4.1-Flash / inference/model.py
Pinned revision 2cba9e42aa02 ↗ · lines 792–827
Read code excerpt
class Gate(nn.Module):
"""MoE gating. The correction bias steers expert selection only; the routing weights come from the
unbiased scores. Image-span tokens use a separate bias (training `noaux_tc_for_vl`)."""
def __init__(self, layer_id: int, args: ModelArgs):
super().__init__()
n_routed_experts, n_activated_experts = args.get_moe_config(layer_id)
self.dim = args.dim
self.topk = n_activated_experts
self.score_func = args.score_func
self.gate_temp = args.gate_temp
self.norm_topk_prob = args.norm_topk_prob
self.route_scale = args.route_scale
self.weight = nn.Parameter(torch.empty(n_routed_experts, args.dim))
self.bias = nn.Parameter(torch.empty(n_routed_experts, dtype=torch.float32))
self.bias_vl = nn.Parameter(torch.empty(n_routed_experts, dtype=torch.float32)) if args.vision_enabled else None
def forward(self, x: torch.Tensor, image_mask: torch.Tensor | None = None) -> tuple[torch.Tensor, torch.Tensor]:
"""x: [n, dim]; image_mask: [n] bool, True for tokens inside an image span."""
scores = linear(x.float(), self.weight.float()) / self.gate_temp
if self.score_func == "softmax":
scores = scores.softmax(dim=-1)
elif self.score_func == "sigmoid":
scores = scores.sigmoid()
else:
scores = F.softplus(scores).sqrt()
bias = self.bias
if image_mask is not None and self.bias_vl is not None:
bias = torch.where(image_mask.unsqueeze(-1), self.bias_vl, bias)
# the bias picks experts but does not scale them: weights come from the raw scores
indices = (scores + bias).topk(self.topk, dim=-1)[1]
weights = scores.gather(1, indices)
if self.norm_topk_prob and self.topk > 1:
weights /= weights.sum(dim=-1, keepdim=True) + 1e-20 # not norm_eps, matches training
weights *= self.route_scale
return weights, indicesDirect evidence
The reference configuration: 40 layers of width 5,120, 64 heads of 512 features with a 64-feature rotary part, a 1,280-feature query latent, a grouped output of 8 groups at rank 1,024, a window of 128, the per-layer compression ratios and the two source lists, the indexer and its candidate stage, hyper-connections of 4 streams, 384 routed experts of which 6 are used with 1 shared, Engram before layers 1 and 14, and three DSpark blocks.
config · deepseek-ai/DeepSeek-V4.1-Flash / inference/config.json
Pinned revision 2cba9e42aa02 ↗ · lines 1–66
Read code excerpt
{
"vocab_size": 129280,
"dim": 5120,
"moe_inter_dim": 2304,
"n_layers": 40,
"n_mtp_layers": 3,
"dspark_block_size": 5,
"dspark_noise_token_id": 128799,
"dspark_target_layer_ids": [37, 38, 39],
"dspark_markov_rank": 256,
"dspark_n_routed_experts": 128,
"dspark_n_activated_experts": 3,
"n_heads": 64,
"n_routed_experts": 384,
"n_shared_experts": 1,
"n_activated_experts": 6,
"score_func": "sqrtsoftplus",
"route_scale": 1.5,
"swiglu_limit": 10.0,
"q_lora_rank": 1280,
"head_dim": 512,
"rope_head_dim": 64,
"norm_eps": 1e-20,
"o_groups": 8,
"o_lora_rank": 1024,
"window_size": 128,
"kv_source_layers": [2, 8, 14, 20],
"index_source_layers": [2, 8, 14, 20, 24, 28, 32, 36],
"original_seq_len": 65536,
"rope_theta": 10000,
"rope_factor": 16,
"beta_fast": 32,
"beta_slow": 1,
"index_n_heads": 32,
"index_head_dim": 128,
"index_topk": 512,
"candidate_source_layer": 20,
"candidate_topk_blocks": 2048,
"candidate_block_size": 8,
"hc_mult": 4,
"hc_sinkhorn_iters": 20,
"hc_eps": 1e-06,
"engram_layer_ids": [1, 14],
"engram_vocab_size": 16000000,
"engram_num_embeddings": [384006168, 384016682],
"engram_max_ngram_size": 4,
"engram_pad_id": 2,
"engram_compressed_vocab_size": 99092,
"dtype": "fp8",
"expert_dtype": "fp4",
"compress_rope_theta": 160000,
"compress_ratios": [0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0],
"vision_n_layers": 32,
"vision_dim": 1024,
"vision_n_heads": 16,
"vision_inter_dim": 2816,
"vision_patch_size": 14,
"vision_downsample_ratio": 3,
"vision_max_n_token": 1024,
"vision_min_pixels": 295936,
"vision_max_wh_ratio": null,
"image_token_id": 129264,
"engram_n_heads": 8,
"engram_head_dim": 256,
"vision_rope_theta": 10000
}Direct evidence
ModelArgs holds the configuration with its defaults, among them the router temperature and the normalisation of the routing weights, and gives the DSpark blocks their own mixture size.
args · deepseek-ai/DeepSeek-V4.1-Flash / inference/model.py
Pinned revision 2cba9e42aa02 ↗ · lines 45–150
Read code excerpt
class ModelArgs:
"""Field names are exactly the config JSON keys. The defaults are a small model that
`python model.py` can run, not the released shapes -- though the scale-independent
values (norm_eps, score_func, hc_*, engram_*) do match it."""
# runtime limits rather than model shape: they size the KV caches
max_batch_size: int = 4
max_seq_len: int = 4096
temperature: float = 1
dtype: Literal["bf16", "fp8"] = "fp8"
expert_dtype: Literal["fp4"] | None = "fp4"
vocab_size: int = 129280
dim: int = 1024
moe_inter_dim: int = 1024
n_layers: int = 5
n_mtp_layers: int = 1 # extra draft layers appended after the backbone, indices n_layers..
n_heads: int = 16
# moe
n_routed_experts: int = 8
n_shared_experts: int = 1
n_activated_experts: int = 2
score_func: Literal["softmax", "sigmoid", "sqrtsoftplus"] = "sqrtsoftplus"
gate_temp: float = 1.0
norm_topk_prob: bool = True
route_scale: float = 1.0
swiglu_limit: float = 0.0
# attention: latent q/kv projections, plus a LoRA-factorised output projection over o_groups
q_lora_rank: int = 256
head_dim: int = 128
rope_head_dim: int = 32
norm_eps: float = 1e-20
o_groups: int = 8
o_lora_rank: int = 256
# sparse attention: every layer attends over a sliding window, and may add compressed KV on top
window_size: int = 128
# one entry per layer, MTP layers included: 0 = sliding window only, r = KV compressed r-to-1
compress_ratios: tuple[int, ...] = (0, 2, 2, 1, 1, 0)
# layers sharing a ratio also share one compressed KV and one indexer, produced by the first
kv_source_layers: tuple[int, ...] = (1, 3)
index_source_layers: tuple[int, ...] = (1, 3)
# rope, with YaRN extrapolation when original_seq_len > 0. Compressed KV rotates at its own
# theta because one latent stands for compress_ratio tokens, so its positions are further apart.
compress_rope_theta: float = 40000.0
original_seq_len: int = 0
rope_theta: float = 10000.0
rope_factor: float = 40
beta_fast: int = 32
beta_slow: int = 1
# the indexer: a small extra attention that scores compressed positions, so each query can keep
# just `index_topk` of them. Names match DeepSeek-V3.2-Exp, where this mechanism first appeared.
index_n_heads: int = 16
index_head_dim: int = 64
index_topk: int = 64
# candidate pre-filtering: candidate_source_layer < 0 turns it off and the other two are unused
candidate_source_layer: int = -1
candidate_topk_blocks: int = 0
candidate_block_size: int = 0
# hyper-connections: the residual stream is carried as hc_mult parallel copies
hc_mult: int = 4
hc_sinkhorn_iters: int = 20
hc_eps: float = 1e-6
# engram: n-gram hash lookups added into the residual stream at a few layers
engram_layer_ids: tuple[int, ...] = ()
engram_num_embeddings: tuple[int, ...] = () # unpadded table rows; each rank allocates ceil(rows / world_size)
engram_max_ngram_size: int = 1
engram_vocab_size: int = 0 # bucket size each (n-gram size, head) starts searching primes from
engram_n_heads: int = 0
engram_head_dim: int = 0
engram_pad_id: int = 2 # token that fills n-gram slots with no history; matches training
# size of the compressed tokenizer vocab; every hash multiplier is derived from it
engram_compressed_vocab_size: int = 0
# vision (VL); vision_n_layers == 0 disables the vision path
vision_n_layers: int = 0
vision_dim: int = 1024
vision_n_heads: int = 16
vision_inter_dim: int = 2816
vision_patch_size: int = 14
vision_rope_theta: float = 10000.0
vision_downsample_ratio: int = 3
vision_max_n_token: int = 1024
vision_min_pixels: int = 544 * 544
vision_max_wh_ratio: int | None = None
# raw id of <|deepseek_image|>; every position of an image span carries this id in input_ids
image_token_id: int = 129264
# dspark draft head. Only the forward pass is implemented here -- nothing calls forward_spec,
# so these are read but the speculative-decoding loop itself is out of scope for this repo.
dspark_block_size: int = 0
dspark_noise_token_id: int = 0
dspark_target_layer_ids: tuple[int, ...] = ()
dspark_markov_rank: int = 256
dspark_n_routed_experts: int = 0
dspark_n_activated_experts: int = 0
@property
def vision_enabled(self) -> bool:
return self.vision_n_layers > 0
def get_moe_config(self, layer_id: int) -> tuple[int, int]:
"""Return the routed/activated expert counts for a given layer."""
if layer_id < self.n_layers:
return self.n_routed_experts, self.n_activated_experts
return (
self.dspark_n_routed_experts or self.n_routed_experts,
self.dspark_n_activated_experts or self.n_activated_experts,
)