Skip to content

Commit ec08ba0

Browse files
committed
[perf] feat: add GDN (Gated DeltaNet) FLOPs calculator
Port the GDN FLOPs formula from Megatron-LM (PR #1989) into Bridge's flop_utils.py so that Qwen3.5 VL and Qwen3-Next models report accurate throughput numbers instead of treating all layers as standard attention. When experimental_attention_variant="gated_delta_net" is set on the model config, transformer_flops() now: - Parses linear_attention_freq to split layers into GDN vs standard attention - Computes per-layer GDN cost (in_proj + conv1d + gated delta rule + out_proj) - Produces a weighted self_attn_term over both layer types Signed-off-by: Chen Cui <chcui@nvidia.com>
1 parent 987269b commit ec08ba0

2 files changed

Lines changed: 252 additions & 0 deletions

File tree

‎src/megatron/bridge/training/utils/flop_utils.py‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,52 @@ def transformer_flops():
382382
)
383383
)
384384

385+
# Handle GDN (Gated DeltaNet) hybrid attention variant.
386+
# When experimental_attention_variant is "gated_delta_net", a fraction of the
387+
# layers use GDN instead of standard attention. Override self_attn_term with a
388+
# weighted sum of GDN and standard-attention per-layer costs.
389+
experimental_attention_variant = getattr(cfg.model, "experimental_attention_variant", None)
390+
if experimental_attention_variant == "gated_delta_net":
391+
linear_attention_freq = getattr(cfg.model, "linear_attention_freq", None)
392+
if linear_attention_freq is not None:
393+
if isinstance(linear_attention_freq, int):
394+
linear_attention_pattern = [
395+
0 if ((i + 1) % linear_attention_freq == 0) else 1 for i in range(num_layers)
396+
]
397+
elif isinstance(linear_attention_freq, list):
398+
linear_attention_pattern = list(linear_attention_freq)
399+
else:
400+
linear_attention_pattern = [1] * num_layers
401+
402+
num_gdn_layers = sum(linear_attention_pattern)
403+
num_standard_attn_layers = num_layers - num_gdn_layers
404+
405+
standard_self_attn_per_layer = self_attn_term / num_layers if num_layers > 0 else 0
406+
407+
qk_head_dim = getattr(cfg.model, "linear_key_head_dim", 128)
408+
v_head_dim = getattr(cfg.model, "linear_value_head_dim", 128)
409+
num_qk_heads = getattr(cfg.model, "linear_num_key_heads", 16)
410+
num_v_heads = getattr(cfg.model, "linear_num_value_heads", 48)
411+
conv_kernel_dim = getattr(cfg.model, "linear_conv_kernel_dim", 4)
412+
413+
qk_dim = qk_head_dim * num_qk_heads
414+
v_dim = v_head_dim * num_v_heads
415+
416+
gdn_self_attn_per_layer = (
417+
3
418+
* 2
419+
* (
420+
cfg.model.hidden_size * (2 * qk_dim + 2 * v_dim + 2 * num_v_heads)
421+
+ conv_kernel_dim * (2 * qk_dim + v_dim)
422+
+ num_v_heads * (v_head_dim**2) * 4
423+
+ cfg.model.hidden_size * v_dim
424+
)
425+
)
426+
427+
self_attn_term = (
428+
gdn_self_attn_per_layer * num_gdn_layers + standard_self_attn_per_layer * num_standard_attn_layers
429+
)
430+
385431
padded_vocab_size = calculate_padded_vocab_size(
386432
cfg.model.vocab_size,
387433
cfg.model.make_vocab_size_divisible_by,

‎tests/unit_tests/training/utils/test_flop_utils.py‎

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@ class MockModelConfig:
6161
group_query_attention: bool = True
6262
gated_linear_unit: bool = True
6363
activation_func: object = field(default=None)
64+
# GDN (Gated DeltaNet) settings
65+
experimental_attention_variant: str | None = None
66+
linear_attention_freq: int | list | None = None
67+
linear_conv_kernel_dim: int = 4
68+
linear_key_head_dim: int = 128
69+
linear_value_head_dim: int = 128
70+
linear_num_key_heads: int = 16
71+
linear_num_value_heads: int = 48
6472

6573
def __post_init__(self):
6674
import torch.nn.functional as F
@@ -380,6 +388,204 @@ def test_swiglu_scaling_factor(self):
380388
assert flops_swiglu == expected_swiglu, f"SwiGLU: expected {expected_swiglu:.2e} but got {flops_swiglu:.2e}"
381389

382390

391+
class TestGDNLayerFlops:
392+
"""Tests for Gated DeltaNet (GDN) FLOPs calculation in transformer_flops path."""
393+
394+
def _qwen35_27b_config(self, **overrides):
395+
"""Return a MockModelConfig resembling Qwen3.5-27B (dense, 64 layers, freq=4)."""
396+
defaults = dict(
397+
num_layers=64,
398+
hidden_size=5120,
399+
seq_length=4096,
400+
ffn_hidden_size=17408,
401+
num_attention_heads=24,
402+
num_query_groups=4,
403+
kv_channels=256,
404+
vocab_size=248320,
405+
make_vocab_size_divisible_by=128,
406+
tensor_model_parallel_size=1,
407+
gated_linear_unit=True,
408+
experimental_attention_variant="gated_delta_net",
409+
linear_attention_freq=4,
410+
linear_conv_kernel_dim=4,
411+
linear_key_head_dim=128,
412+
linear_value_head_dim=128,
413+
linear_num_key_heads=16,
414+
linear_num_value_heads=48,
415+
)
416+
defaults.update(overrides)
417+
return MockModelConfig(**defaults)
418+
419+
def test_gdn_flops_differ_from_pure_attention(self):
420+
"""GDN-enabled config should produce different FLOPs than pure-attention baseline."""
421+
batch_size = 1
422+
gdn_cfg = MockConfigContainer(model=self._qwen35_27b_config())
423+
baseline_cfg = MockConfigContainer(model=self._qwen35_27b_config(experimental_attention_variant=None))
424+
gdn_flops = num_floating_point_operations(gdn_cfg, batch_size=batch_size)
425+
baseline_flops = num_floating_point_operations(baseline_cfg, batch_size=batch_size)
426+
assert gdn_flops != baseline_flops, "GDN FLOPs should differ from pure-attention FLOPs"
427+
assert gdn_flops > 0
428+
429+
def test_gdn_only_layers(self):
430+
"""With linear_attention_freq=1 (no standard attn), self_attn_term should be pure GDN."""
431+
batch_size = 1
432+
num_layers = 4
433+
hidden_size = 1024
434+
seq_length = 512
435+
vocab_size = 32000
436+
qk_head_dim = 64
437+
v_head_dim = 64
438+
num_qk_heads = 8
439+
num_v_heads = 16
440+
conv_kernel_dim = 4
441+
442+
model_cfg = MockModelConfig(
443+
num_layers=num_layers,
444+
hidden_size=hidden_size,
445+
seq_length=seq_length,
446+
ffn_hidden_size=4096,
447+
num_attention_heads=8,
448+
num_query_groups=8,
449+
kv_channels=128,
450+
vocab_size=vocab_size,
451+
make_vocab_size_divisible_by=128,
452+
tensor_model_parallel_size=1,
453+
gated_linear_unit=False,
454+
experimental_attention_variant="gated_delta_net",
455+
linear_attention_freq=1,
456+
linear_conv_kernel_dim=conv_kernel_dim,
457+
linear_key_head_dim=qk_head_dim,
458+
linear_value_head_dim=v_head_dim,
459+
linear_num_key_heads=num_qk_heads,
460+
linear_num_value_heads=num_v_heads,
461+
)
462+
cfg = MockConfigContainer(model=model_cfg)
463+
actual_flops = num_floating_point_operations(cfg, batch_size=batch_size)
464+
465+
# freq=1: pattern = [0 if (i+1)%1==0 else 1 for i in range(4)] = [0,0,0,0]
466+
# All layers are standard attention, 0 GDN layers.
467+
# This is because freq=1 means every layer is standard attention.
468+
# So actual_flops should equal baseline (no GDN).
469+
baseline_cfg = MockConfigContainer(
470+
model=MockModelConfig(
471+
num_layers=num_layers,
472+
hidden_size=hidden_size,
473+
seq_length=seq_length,
474+
ffn_hidden_size=4096,
475+
num_attention_heads=8,
476+
num_query_groups=8,
477+
kv_channels=128,
478+
vocab_size=vocab_size,
479+
make_vocab_size_divisible_by=128,
480+
tensor_model_parallel_size=1,
481+
gated_linear_unit=False,
482+
)
483+
)
484+
baseline_flops = num_floating_point_operations(baseline_cfg, batch_size=batch_size)
485+
assert actual_flops == baseline_flops, (
486+
"freq=1 means every layer is standard attention, so FLOPs should match baseline"
487+
)
488+
489+
def test_gdn_layer_freq_list(self):
490+
"""Test GDN with linear_attention_freq as a list pattern."""
491+
batch_size = 1
492+
# [1, 1, 0, 1, 1, 0, 1, 1] means 6 GDN, 2 standard
493+
freq_list = [1, 1, 0, 1, 1, 0, 1, 1]
494+
model_cfg = self._qwen35_27b_config(
495+
num_layers=8,
496+
linear_attention_freq=freq_list,
497+
)
498+
cfg = MockConfigContainer(model=model_cfg)
499+
flops = num_floating_point_operations(cfg, batch_size=batch_size)
500+
assert flops > 0
501+
502+
def test_gdn_exact_self_attn_term(self):
503+
"""Verify the GDN self_attn_term matches the expected formula from Megatron-LM."""
504+
batch_size = 1
505+
num_layers = 4
506+
hidden_size = 1024
507+
seq_length = 256
508+
vocab_size = 32000
509+
qk_head_dim = 64
510+
v_head_dim = 64
511+
num_qk_heads = 8
512+
num_v_heads = 16
513+
conv_kernel_dim = 4
514+
ffn_hidden_size = 4096
515+
516+
qk_dim = qk_head_dim * num_qk_heads
517+
v_dim = v_head_dim * num_v_heads
518+
519+
# freq=2: layers 0,2 are GDN (pattern[i]=1), layers 1,3 are standard (pattern[i]=0)
520+
model_cfg = MockModelConfig(
521+
num_layers=num_layers,
522+
hidden_size=hidden_size,
523+
seq_length=seq_length,
524+
ffn_hidden_size=ffn_hidden_size,
525+
num_attention_heads=8,
526+
num_query_groups=8,
527+
kv_channels=128,
528+
vocab_size=vocab_size,
529+
make_vocab_size_divisible_by=128,
530+
tensor_model_parallel_size=1,
531+
gated_linear_unit=False,
532+
experimental_attention_variant="gated_delta_net",
533+
linear_attention_freq=2,
534+
linear_conv_kernel_dim=conv_kernel_dim,
535+
linear_key_head_dim=qk_head_dim,
536+
linear_value_head_dim=v_head_dim,
537+
linear_num_key_heads=num_qk_heads,
538+
linear_num_value_heads=num_v_heads,
539+
)
540+
cfg = MockConfigContainer(model=model_cfg)
541+
gdn_flops = num_floating_point_operations(cfg, batch_size=batch_size)
542+
543+
# Compute expected manually
544+
expansion_factor = 3 * 2 * 2 # 12
545+
# Standard attention per-layer (MHA, num_query_groups==num_attention_heads so ratio=1)
546+
kv_channels = 128
547+
query_projection_size = kv_channels * 8
548+
query_projection_to_hidden_size_ratio = query_projection_size / hidden_size
549+
standard_attn_per_layer = (
550+
expansion_factor
551+
* hidden_size
552+
* hidden_size
553+
* ((1 + 8 / 8 + seq_length / hidden_size / 2) * query_projection_to_hidden_size_ratio)
554+
)
555+
# GDN per-layer
556+
gdn_per_layer = (
557+
3
558+
* 2
559+
* (
560+
hidden_size * (2 * qk_dim + 2 * v_dim + 2 * num_v_heads)
561+
+ conv_kernel_dim * (2 * qk_dim + v_dim)
562+
+ num_v_heads * (v_head_dim**2) * 4
563+
+ hidden_size * v_dim
564+
)
565+
)
566+
# freq=2: pattern = [1, 0, 1, 0] -> 2 GDN, 2 standard
567+
expected_self_attn = gdn_per_layer * 2 + standard_attn_per_layer * 2
568+
# MLP: gated_linear_unit=False -> gated_linear_multiplier=1
569+
expected_mlp = expansion_factor * num_layers * hidden_size * ffn_hidden_size * 1
570+
# Logit
571+
padded_vocab = vocab_size # 32000 is already divisible by 128
572+
expected_logit = 3 * 2 * hidden_size * padded_vocab * 1
573+
expected_total = batch_size * seq_length * (expected_mlp + expected_self_attn + expected_logit)
574+
575+
assert gdn_flops == expected_total, f"Expected {expected_total:.6e} but got {gdn_flops:.6e}"
576+
577+
def test_gdn_more_gdn_layers_changes_flops(self):
578+
"""Increasing GDN layer ratio (higher freq) should change FLOPs."""
579+
batch_size = 1
580+
# freq=4: 3/4 GDN, 1/4 standard
581+
cfg_freq4 = MockConfigContainer(model=self._qwen35_27b_config(num_layers=8, linear_attention_freq=4))
582+
# freq=8: 7/8 GDN, 1/8 standard
583+
cfg_freq8 = MockConfigContainer(model=self._qwen35_27b_config(num_layers=8, linear_attention_freq=8))
584+
flops_freq4 = num_floating_point_operations(cfg_freq4, batch_size=batch_size)
585+
flops_freq8 = num_floating_point_operations(cfg_freq8, batch_size=batch_size)
586+
assert flops_freq4 != flops_freq8, "Different GDN ratios should produce different FLOPs"
587+
588+
383589
class TestHybridMtpPatternParsing:
384590
"""Tests for hybrid/MTP pattern parsing in FLOPs accounting."""
385591

0 commit comments

Comments
 (0)