Skip to content

Commit d19cd22

Browse files
committed
Add DeepSeek-V4 training FLOPs and MFU calculation
This change implements training FLOP calculation support for DeepSeek-V4: 1. Defines `calculate_deepseek4_tflops_training_per_device` modeling multi-track attention topologies (prefix-only sliding window, HCA, and CSA branches). 2. Accounts for dense weight projections, indexer top-k query scoring, and sparse attention ratios. 3. Integrates parameter scaling overheads for Manifold-Constrained Hyper-Connections (mHC) applied twice per block. 4. Adds unit test `test_deepseek4_284b_flops` to validate estimated values against dry-run analytical targets. TAG=agy CONV=33685296-b5bf-4bc8-b94f-1a9508b00d80
1 parent 409f43b commit d19cd22

3 files changed

Lines changed: 232 additions & 1 deletion

File tree

src/maxtext/configs/types.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3412,6 +3412,9 @@ def calculate_global_batch_sizes(per_device_batch_size, expansion_factor, num_de
34123412
if val != 0 and val < 4:
34133413
raise ValueError(f"compress_ratio must be 0 (disabled) or >= 4, got {val}")
34143414

3415+
if self.decoder_block == DecoderBlockType.DEEPSEEK4 and self.mtp_num_layers > 0:
3416+
raise ValueError("DeepSeek4 decoder block currently does not support MTP layers.")
3417+
34153418
if self.num_kv_shared_layers > 0:
34163419
if self.fused_qkv:
34173420
raise ValueError("`num_kv_shared_layers > 0` is not compatible with `fused_qkv`.")

src/maxtext/utils/maxtext_utils.py

Lines changed: 163 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -702,7 +702,7 @@ def get_dense_moe_layers(config):
702702
num_moe_layers = config.num_decoder_layers // config.interleave_moe_layer_step
703703
num_dense_layers = config.num_decoder_layers - num_moe_layers
704704
return num_dense_layers, num_moe_layers
705-
elif config.decoder_block in (DecoderBlockType.QWEN3_NEXT, DecoderBlockType.QWEN3_5):
705+
elif config.decoder_block in (DecoderBlockType.QWEN3_NEXT, DecoderBlockType.QWEN3_5, DecoderBlockType.DEEPSEEK4):
706706
return 0, config.num_decoder_layers
707707
elif config.decoder_block == DecoderBlockType.DEFAULT:
708708
raise ValueError("Unsupported decoder block for dense/MoE layer calculation")
@@ -938,6 +938,163 @@ def calculate_vision_encoder_tflops(config):
938938
return mm_total_tflops, mm_learnable_weight_tflops, mm_attention_tflops
939939

940940

941+
def calculate_mhc_flops_per_layer(config):
942+
"""Calculates the training FLOPs per layer for Manifold-Constrained Hyper-Connections (mHC)."""
943+
batch_size = config.per_device_batch_size
944+
seq_len = config.max_target_length
945+
k_mhc = getattr(config, "mhc_expansion_rate", 1)
946+
emb_dim = config.emb_dim
947+
948+
# Inside mhc.py, inputs are projected to an expanded manifold space bounded by k_mhc.
949+
# pre_alpha / post_alpha maps input [B, S, k * D] to [B, S, k]
950+
pre_alpha_flops = 2 * batch_size * seq_len * (k_mhc * emb_dim) * k_mhc
951+
pre_contract_flops = 2 * batch_size * seq_len * k_mhc * emb_dim
952+
post_alpha_flops = 2 * batch_size * seq_len * (k_mhc * emb_dim) * k_mhc
953+
post_outer_flops = batch_size * seq_len * k_mhc * emb_dim
954+
# res_alpha maps input [B, S, k * D] to [B, S, k^2]
955+
res_alpha_flops = 2 * batch_size * seq_len * (k_mhc * emb_dim) * (k_mhc**2)
956+
res_contract_flops = 2 * batch_size * seq_len * (k_mhc**2) * emb_dim
957+
958+
return pre_alpha_flops + pre_contract_flops + post_alpha_flops + post_outer_flops + res_alpha_flops + res_contract_flops
959+
960+
961+
def calculate_deepseek4_tflops_training_per_device(config, total_ffn_flops_all_layers, embedding_flops):
962+
"""Calculates the training TFLOPs per device for DeepSeek-V4.
963+
964+
DeepSeek-V4 adopts a multi-track attention topology and Manifold-Constrained
965+
Hyper-Connections (mHC). This function models:
966+
1. Base Attention block projections (Q/KV/Grouped Out mapping).
967+
2. mHC projections (pre, post, residual alpha/contraction projections) applied twice per layer.
968+
3. Prefix-only Sliding Window track (C=0).
969+
4. HCA (Hybrid Compressed Attention, C=128) incorporating global context.
970+
5. CSA (Causal Sparse Attention, C=4) containing poolers, indexer top-k query scoring,
971+
and sparse sequence attention.
972+
973+
Args:
974+
config: MaxTextConfig instance containing architectural hyperparameters.
975+
total_ffn_flops_all_layers: Pre-computed Feed-Forward (MoE/Shared MLP) FLOPs.
976+
embedding_flops: Pre-computed input token embedding layer FLOPs.
977+
978+
Returns:
979+
A tuple of (total_attn_flops_in_tflops, total_weight_flops_in_tflops).
980+
"""
981+
batch_size = config.per_device_batch_size
982+
seq_len = config.max_target_length
983+
sliding_window_size = config.sliding_window_size
984+
num_layers = config.num_decoder_layers
985+
986+
# DeepSeek-V4 Attention topology compression rates (HCA = 128, CSA = 4)
987+
HCA_RATIO = 128
988+
CSA_RATIO = 4
989+
990+
# 1. Dynamic YAML-compliant Layer Counting
991+
# DeepSeek-V4 alternates layers with different compression rates (C = 0, 128, 4).
992+
# If the YAML config doesn't declare it explicitly, we default to the standard
993+
# DeepSeek-V4 profile (2 prefix sliding window, 20 HCA, and 21 CSA layers).
994+
c_ratios = getattr(config, "compress_ratios", [])
995+
if not c_ratios:
996+
num_sliding_layers, num_hca_layers, num_csa_layers = 2, 20, 21
997+
else:
998+
num_sliding_layers = c_ratios.count(0)
999+
num_hca_layers = c_ratios.count(HCA_RATIO)
1000+
num_csa_layers = c_ratios.count(CSA_RATIO)
1001+
1002+
# 2. Base Attention Block Projections (Executed on all layers)
1003+
# - Q projection: projects inputs to q_lora_rank, then up-projects to query_heads * head_dim.
1004+
# FLOPs = 2 * B * S * (D * Q_rank + Q_rank * H * D_h)
1005+
wq_flops = (
1006+
2
1007+
* batch_size
1008+
* seq_len
1009+
* ((config.emb_dim * config.q_lora_rank) + (config.q_lora_rank * config.num_query_heads * config.head_dim))
1010+
)
1011+
# - KV projection: embeds to kv_heads * head_dim.
1012+
# FLOPs = 2 * B * S * D * (KV_heads * D_h)
1013+
wkv_flops = 2 * batch_size * seq_len * config.emb_dim * (config.num_kv_heads * config.head_dim)
1014+
# - Grouped Out projection: projects out groups of heads down to o_lora_rank, then up to emb_dim.
1015+
# FLOPs = 2 * B * S * (H * D_h) * o_lora_rank + 2 * B * S * (o_groups * o_lora_rank) * D
1016+
grp_feat = (config.num_query_heads * config.head_dim) // config.o_groups
1017+
o_flops = (2 * batch_size * seq_len * config.o_groups * grp_feat * config.o_lora_rank) + (
1018+
2 * batch_size * seq_len * (config.o_groups * config.o_lora_rank) * config.emb_dim
1019+
)
1020+
base_attention_weights = wq_flops + wkv_flops + o_flops
1021+
1022+
# 3. Manifold-Constrained Hyper-Connections (mHC) Overheads
1023+
# Inside mhc.py, inputs are projected to an expanded manifold space.
1024+
# These operations are executed twice per layer (wrapping the attention block and FFN block).
1025+
if getattr(config, "mhc_expansion_rate", 1) > 1:
1026+
mhc_flops_per_layer = calculate_mhc_flops_per_layer(config)
1027+
else:
1028+
mhc_flops_per_layer = 0
1029+
1030+
# 4. Multi-Track Sequence Length Operations
1031+
# - Track 1: Sliding Window Prefix (C=0)
1032+
# Uses exact causal sliding window surface area formula to count attention dot products:
1033+
# FLOPs = 4 * B * (S * W - 0.5 * W^2) * H * D_h
1034+
prefix_attn = (
1035+
4
1036+
* batch_size
1037+
* (seq_len * sliding_window_size - 0.5 * sliding_window_size**2)
1038+
* config.num_query_heads
1039+
* config.head_dim
1040+
)
1041+
1042+
# - Track 2: HCA Compressed Branch (C=128)
1043+
# Includes HCA compressor dense projection: D -> D_h
1044+
hca_pooler = 4 * batch_size * seq_len * config.emb_dim * config.head_dim
1045+
# Calculates prefix sliding window attention + global cross-attention to the S/128 elements
1046+
hca_attn = prefix_attn + 2 * batch_size * seq_len * (seq_len / HCA_RATIO) * config.num_query_heads * config.head_dim
1047+
1048+
# - Track 3: CSA Sparse Branch (C=4)
1049+
# - Indexer Projections: Q_rank -> H_idx * D_idx, and D -> H_idx
1050+
idx_proj = (
1051+
2
1052+
* batch_size
1053+
* seq_len
1054+
* (
1055+
(config.q_lora_rank * config.indexer_n_heads * config.indexer_head_dim)
1056+
+ (config.emb_dim * config.indexer_n_heads)
1057+
)
1058+
)
1059+
# - Indexer query-block scoring (divided by 2 for causal mask)
1060+
idx_score = batch_size * seq_len * (seq_len / CSA_RATIO) * config.indexer_n_heads * config.indexer_head_dim
1061+
# - Indexer head reduction (divided by 2 for causal mask)
1062+
idx_reduce = batch_size * seq_len * (seq_len / CSA_RATIO) * config.indexer_n_heads
1063+
# - Indexer key/gate pooler projections: Down-projects D -> 2 * D_idx
1064+
idx_kv_gate_pool = 4 * batch_size * seq_len * config.emb_dim * (2 * config.indexer_head_dim)
1065+
csa_indexer = idx_proj + idx_score + idx_reduce + idx_kv_gate_pool
1066+
1067+
# Includes CSA compressor pooler projection: D -> 2 * D_h
1068+
csa_pooler = 4 * batch_size * seq_len * config.emb_dim * (2 * config.head_dim)
1069+
# - Sparse causal attention: attends to top-k blocks of size 4 in preceding sequence of size S/4
1070+
csa_k = min(config.indexer_topk, seq_len // CSA_RATIO)
1071+
# We reuse the mask multiplier helper for the csa causal ratio:
1072+
# Ratio = (K/T) - 0.5 * (K/T)^2
1073+
csa_mask_ratio = calculate_indexer_mask_ratio(csa_k, seq_len // CSA_RATIO)
1074+
csa_sparse_attn = (
1075+
4 * batch_size * (seq_len * (seq_len / CSA_RATIO)) * config.num_query_heads * config.head_dim * csa_mask_ratio
1076+
)
1077+
csa_attn = prefix_attn + csa_sparse_attn + csa_indexer
1078+
1079+
# 5. Pipeline TFLOP Aggregation
1080+
total_attn_flops = (num_sliding_layers * prefix_attn) + (num_hca_layers * hca_attn) + (num_csa_layers * csa_attn)
1081+
1082+
total_weight_flops = (
1083+
(num_layers * base_attention_weights)
1084+
+ (2 * num_layers * mhc_flops_per_layer) # mHC runs twice per layer
1085+
+ (num_hca_layers * hca_pooler)
1086+
+ (num_csa_layers * csa_pooler)
1087+
+ total_ffn_flops_all_layers
1088+
+ embedding_flops
1089+
)
1090+
1091+
# Scale final values by 3x to account for Forward + Backward (2x Forward) training passes
1092+
return (
1093+
total_attn_flops * 3 / 10**12,
1094+
total_weight_flops * 3 / 10**12,
1095+
)
1096+
1097+
9411098
def calculate_tflops_training_per_device(config, log=True):
9421099
"""Calculate training TFLOP"""
9431100
# MLP flops
@@ -950,6 +1107,7 @@ def calculate_tflops_training_per_device(config, log=True):
9501107
DecoderBlockType.QWEN3_NEXT,
9511108
DecoderBlockType.QWEN3_5,
9521109
DecoderBlockType.GEMMA4,
1110+
DecoderBlockType.DEEPSEEK4,
9531111
):
9541112
total_ffn_flops = calculate_routed_and_shared_ffn_tflops_per_device(config)
9551113
is_ffn_flops_already_total = True
@@ -1040,6 +1198,10 @@ def calculate_tflops_training_per_device(config, log=True):
10401198
# KV sharing, double-wide MLP on shared layers, and the per-layer-embedding
10411199
# block.
10421200
attention_tflops, learnable_weight_tflops = calculate_gemma4_small_tflops_training_per_device(config, embedding_flops)
1201+
elif config.decoder_block == DecoderBlockType.DEEPSEEK4:
1202+
attention_tflops, learnable_weight_tflops = calculate_deepseek4_tflops_training_per_device(
1203+
config, total_ffn_flops_all_layers, embedding_flops
1204+
)
10431205
elif config.decoder_block == DecoderBlockType.DEEPSEEK:
10441206
learnable_weight_tflops = (
10451207
(total_ffn_flops_all_layers + (qkv_flops + projection_flops) * config.num_decoder_layers + embedding_flops)

tests/unit/flop_calculation_test.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,72 @@ def test_deepseek32_671b_flops(self):
377377
calculated_tflops, _, _ = calculate_tflops_training_per_device(cfg)
378378
self.assertFlopsAlmostEqual(calculated_tflops, golden_tflops)
379379

380+
def test_deepseek4_284b_flops(self):
381+
"""Test DeepSeek V4 284B MFU FLOP calculation"""
382+
cfg = self._initialize_model_config(
383+
"deepseek4-284b",
384+
max_target_length=4096,
385+
per_device_batch_size=4,
386+
attention="dot_product",
387+
)
388+
B = cfg.per_device_batch_size
389+
S = cfg.max_target_length
390+
391+
# Calculate analytical active parameters per token
392+
embedding_params = cfg.vocab_size * cfg.emb_dim
393+
wq_params = (cfg.emb_dim * cfg.q_lora_rank) + (cfg.q_lora_rank * cfg.num_query_heads * cfg.head_dim)
394+
wkv_params = cfg.emb_dim * (cfg.num_kv_heads * cfg.head_dim)
395+
o_params = (cfg.num_query_heads * cfg.head_dim * cfg.o_lora_rank) + (cfg.o_groups * cfg.o_lora_rank * cfg.emb_dim)
396+
base_attn_params = wq_params + wkv_params + o_params
397+
398+
k = cfg.mhc_expansion_rate
399+
mhc_norm_params = k * cfg.emb_dim
400+
pre_alpha_params = k * cfg.emb_dim * k + 1 + k
401+
post_alpha_params = k * cfg.emb_dim * k + 1 + k
402+
res_alpha_params = k * cfg.emb_dim * (k * k) + 1 + (k * k)
403+
mhc_params_per_layer = 2 * (mhc_norm_params + pre_alpha_params + post_alpha_params + res_alpha_params)
404+
405+
gate_params = cfg.emb_dim * cfg.num_experts
406+
active_experts = cfg.num_experts_per_tok + cfg.shared_experts
407+
moe_ffn_params = gate_params + (active_experts * 3 * cfg.emb_dim * cfg.moe_mlp_dim)
408+
409+
hca_pooler_params = cfg.emb_dim * cfg.head_dim
410+
csa_pooler_params = cfg.emb_dim * (2 * cfg.head_dim)
411+
idx_proj_params = (cfg.q_lora_rank * cfg.indexer_n_heads * cfg.indexer_head_dim) + (cfg.emb_dim * cfg.indexer_n_heads)
412+
idx_pool_params = cfg.emb_dim * (2 * cfg.indexer_head_dim)
413+
csa_indexer_params = idx_proj_params + idx_pool_params
414+
415+
c_ratios = getattr(cfg, "compress_ratios", [])
416+
num_hca_layers = c_ratios.count(128)
417+
num_csa_layers = c_ratios.count(4)
418+
419+
total_active_params = (
420+
embedding_params
421+
+ cfg.num_decoder_layers * (base_attn_params + mhc_params_per_layer + moe_ffn_params)
422+
+ num_hca_layers * hca_pooler_params
423+
+ num_csa_layers * (csa_pooler_params + csa_indexer_params)
424+
)
425+
426+
calculated_total, calculated_weight, calculated_attn = calculate_tflops_training_per_device(cfg, log=False)
427+
428+
golden_weight_flops = 6 * B * S * total_active_params / 1e12
429+
golden_total_flops = golden_weight_flops + calculated_attn
430+
431+
# Verify our calculated FLOPs align within 5% tolerance of the standard 6BP analytical baseline
432+
self.assertFlopsAlmostEqual(calculated_total, golden_total_flops)
433+
self.assertFlopsAlmostEqual(calculated_weight, golden_weight_flops)
434+
435+
def test_deepseek4_mtp_validation(self):
436+
"""Test that DeepSeek-V4 with MTP layers raises a ValueError"""
437+
with self.assertRaises(ValueError):
438+
self._initialize_model_config(
439+
"deepseek4-284b",
440+
max_target_length=4096,
441+
per_device_batch_size=4,
442+
attention="dot_product",
443+
mtp_num_layers=1,
444+
)
445+
380446
def test_custom_engram_flops(self):
381447
"""Test model with Engram Flops calculation"""
382448
cfg = self._initialize_model_config(

0 commit comments

Comments
 (0)