Skip to content

Commit 987d480

Browse files
committed
Reject recompute in Eagle TTT, drop wasteful GQA expansion, fix markers
Signed-off-by: seonjinn <sna@nvidia.com>
1 parent 292e29d commit 987d480

3 files changed

Lines changed: 40 additions & 2 deletions

File tree

nemo_rl/models/megatron/draft/eagle_ttt.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -486,7 +486,12 @@ def from_trunk(
486486
max_passes: int,
487487
activation_budget_bytes: int,
488488
) -> EagleTTTState:
489-
"""Validate the complete bound before retaining any supplied tensor."""
489+
"""Validate the per-layer multi-pass K/V bound before retaining tensors.
490+
491+
The complete bound across layers, hidden taps, rope, and loss rows is
492+
enforced by the provider's storage plan and, at runtime, by the
493+
resource ledger; this check covers only this layer's K/V retention.
494+
"""
490495
_validate_kv_pair(trunk_key, trunk_value, name="trunk")
491496
EagleTTTStoragePlan(
492497
batch_size=trunk_key.shape[0],
@@ -563,6 +568,16 @@ def __init__(
563568
if attention_type != "self":
564569
raise ValueError("EAGLE TTT only supports self attention")
565570
self.layer_number = layer_number
571+
recompute_granularity = getattr(config, "recompute_granularity", None)
572+
if recompute_granularity is not None:
573+
# Activation recompute re-enters this forward during backward, which
574+
# violates the one-shot begin_pass/forward/finish_pass lifecycle and
575+
# would corrupt the retained multi-pass state on re-entry.
576+
raise ValueError(
577+
"EAGLE TTT is incompatible with activation recompute "
578+
f"(recompute_granularity={recompute_granularity!r}); disable "
579+
"recompute for the draft decoder stack"
580+
)
566581
self.context_parallel_size = int(getattr(config, "context_parallel_size", 1))
567582
self.sequence_parallel = bool(getattr(config, "sequence_parallel", False))
568583
self.softmax_scale = softmax_scale
@@ -1509,7 +1524,8 @@ def eagle_ttt_attention(
15091524
raise ValueError("sequence layout must match query batch and sequence axes")
15101525
if layout.valid_tokens.device != query.device:
15111526
raise ValueError("sequence layout and query must share a device")
1512-
_expand_gqa(state.trunk_key, query_heads=query.shape[1])
1527+
if query.shape[1] % state.trunk_key.shape[1] != 0:
1528+
raise ValueError("query heads must be divisible by key/value heads")
15131529

15141530
attention_scale = scale if scale is not None else 1.0 / math.sqrt(query.shape[-1])
15151531
if query.is_cuda and query.dtype != torch.float64:

tests/unit/distributed/test_eagle_ttt_sp.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,7 @@ def _sp_packed_params_mismatch_worker(
587587
dist.destroy_process_group()
588588

589589

590+
@pytest.mark.mcore
590591
@pytest.mark.skipif(
591592
not torch.distributed.is_nccl_available() or torch.cuda.device_count() < _TP_SIZE,
592593
reason="two CUDA devices and NCCL are required",
@@ -606,6 +607,7 @@ def test_real_mcore_tp2_sequence_parallel_matches_global_oracle(
606607
)
607608

608609

610+
@pytest.mark.mcore
609611
@pytest.mark.skipif(
610612
not torch.distributed.is_nccl_available() or torch.cuda.device_count() < _TP_SIZE,
611613
reason="two CUDA devices and NCCL are required",
@@ -623,6 +625,7 @@ def test_real_mcore_tp2_sequence_parallel_packed_mismatch_agrees_and_resets(
623625
)
624626

625627

628+
@pytest.mark.mcore
626629
@pytest.mark.skipif(
627630
not torch.distributed.is_nccl_available() or torch.cuda.device_count() < _TP_SIZE,
628631
reason="two CUDA devices and NCCL are required",

tests/unit/models/megatron/test_eagle_ttt_mcore_session.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,25 @@ def test_core_preserves_mcore_tp_group_from_process_group_collection() -> None:
115115
assert core.tp_group is tp_group
116116

117117

118+
def test_core_rejects_activation_recompute_config() -> None:
119+
# Recompute re-enters the armed forward during backward, which the
120+
# one-shot pass lifecycle cannot tolerate; construction must fail fast.
121+
module = _load_module()
122+
for granularity in ("full", "selective"):
123+
with pytest.raises(ValueError, match="recompute"):
124+
module.EagleTTTCoreAttention(
125+
config=SimpleNamespace(
126+
context_parallel_size=1, recompute_granularity=granularity
127+
),
128+
layer_number=1,
129+
attn_mask_type=None,
130+
attention_type="self",
131+
cp_comm_type=None,
132+
softmax_scale=None,
133+
pg_collection=None,
134+
)
135+
136+
118137
def test_layer_spec_adapter_is_construction_time_and_does_not_mutate_default() -> None:
119138
module = _load_module()
120139
original_core = object()

0 commit comments

Comments
 (0)