Skip to content

Commit 4b9adf5

Browse files
committed
Add DFlash spec decode support for NemotronH (Nemotron Ultra 550B)
5 patches to enable DFlash speculative decoding with NemotronH models: 1. nemotron_h.py: Add DFLASH_AUX_HIDDEN_STATE_LAYERS env var hook in NemotronHModel.__init__ to auto-set aux hidden state layers during extraction/inference. Modify NemotronHForCausalLM.forward() to return (hidden_states, aux_hidden_states) tuple for DFlash spec decode mode, and save to disk for offline extraction mode (DFLASH_EXTRACT_PATH). 2. qwen2.py: Add relu2 activation support in Qwen2MLP. NemotronH uses relu2 (relu squared) instead of silu for MLP activation. Without this, DFlash draft model loading fails with 'Unsupported activation: relu2'. 3. flex_attention.py: Fix .view() -> .reshape() for non-contiguous KV cache tensors. Required for FLEX_ATTENTION backend with paged KV cache. 4. input_processor.py: Fix tensor boolean ambiguity. NemotronH prompts can produce multi-element tensors that fail 'if prompt_ids:' check. 5. gpu_model_runner.py: Add debug logging for aux hidden state tuple unpacking in _dummy_run. Helps diagnose DFlash spec decode issues. Tested with Nemotron Ultra 550B (NemotronH hybrid Mamba2+MoE+attention): - Offline hidden state extraction: 5000 samples, 656GB - DFlash draft training: ~4B params, val/pos_1_acc=56.2% - Inference: 1.66x speedup (233.85 vs 141.18 tok/s) with FLASH_ATTN FA2 - Beats built-in MTP (1.36x / 194.25 tok/s) by 22% - Lossless: 2/5 samples bit-identical (expected bf16 noise) Note: B200 (SM100) defaults to FA4 which requires cutlass.cute (Python 3.10 MLIR). Force flash_attn_version=2 via attention_config for DFlash spec decode. Based on vLLM v0.20.1.
1 parent 132765e commit 4b9adf5

5 files changed

Lines changed: 96 additions & 11 deletions

File tree

vllm/model_executor/models/nemotron_h.py

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1+
import os
2+
13
# SPDX-License-Identifier: Apache-2.0
24
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
35

6+
from vllm.logger import init_logger
7+
8+
logger = init_logger(__name__)
9+
410
# Adapted from https://github.com/vllm-project/vllm/blob/94d8ec8d2bcb4ec55e33022b313c7e978edf05e1/vllm/model_executor/models/bamba.py
511
# Copyright 2024 HuggingFace Inc. team. All rights reserved.
612
# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
@@ -64,9 +70,11 @@
6470
maybe_remap_kv_scale_name,
6571
)
6672
from vllm.model_executor.models.interfaces import (
73+
EagleModelMixin,
6774
HasInnerState,
6875
IsHybrid,
6976
MixtureOfExperts,
77+
SupportsEagle3,
7078
SupportsLoRA,
7179
SupportsMambaPrefixCaching,
7280
SupportsPP,
@@ -539,7 +547,7 @@ def forward(
539547

540548

541549
@support_torch_compile
542-
class NemotronHModel(nn.Module):
550+
class NemotronHModel(nn.Module, EagleModelMixin):
543551
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
544552
super().__init__()
545553

@@ -578,6 +586,14 @@ def get_layer(prefix: str):
578586
self.start_layer, self.end_layer, self.layers = make_layers(
579587
len(config.hybrid_override_pattern), get_layer, prefix=f"{prefix}.layers"
580588
)
589+
590+
# DFlash offline extraction: auto-set aux_hidden_state_layers from env var
591+
# This bypasses the need for apply_model() which has serialization issues
592+
_dflash_layers = os.environ.get("DFLASH_AUX_HIDDEN_STATE_LAYERS", "")
593+
if _dflash_layers:
594+
_layers = tuple(int(x) for x in _dflash_layers.split(","))
595+
self._set_aux_hidden_state_layers(_layers)
596+
logger.info("DFlash: auto-set aux_hidden_state_layers=%s from env", _layers)
581597
self.make_empty_intermediate_tensors = make_empty_intermediate_tensors_factory(
582598
["hidden_states", "residual"], config.hidden_size
583599
)
@@ -605,18 +621,27 @@ def forward(
605621
hidden_states = intermediate_tensors["hidden_states"]
606622
residual = intermediate_tensors["residual"]
607623

608-
for layer in islice(self.layers, self.start_layer, self.end_layer):
624+
# EAGLE3 auxiliary hidden state collection (for DFlash spec decoding)
625+
aux_hidden_states = self._maybe_add_hidden_state([], 0, hidden_states, residual)
626+
for idx, layer in enumerate(
627+
islice(self.layers, self.start_layer, self.end_layer)
628+
):
609629
hidden_states, residual = layer(
610630
positions=positions,
611631
hidden_states=hidden_states,
612632
residual=residual,
613633
)
634+
self._maybe_add_hidden_state(
635+
aux_hidden_states, idx + 1, hidden_states, residual
636+
)
614637

615638
if not get_pp_group().is_last_rank:
616639
return IntermediateTensors(
617640
{"hidden_states": hidden_states, "residual": residual}
618641
)
619642
hidden_states, _ = self.norm_f(hidden_states, residual)
643+
if len(aux_hidden_states) > 0:
644+
return hidden_states, aux_hidden_states
620645
return hidden_states
621646

622647
def is_spec_layer(self, config: NemotronHConfig, weight_name: str) -> bool:
@@ -771,6 +796,7 @@ class NemotronHForCausalLM(
771796
SupportsQuant,
772797
MixtureOfExperts,
773798
SupportsMambaPrefixCaching,
799+
SupportsEagle3,
774800
):
775801
# Relevant only if self.has_moe is True
776802
is_non_gated_moe: bool = True
@@ -919,11 +945,42 @@ def forward(
919945
intermediate_tensors: IntermediateTensors | None = None,
920946
inputs_embeds: torch.Tensor | None = None,
921947
**kwargs,
922-
):
923-
hidden_states = self.model(
948+
) -> torch.Tensor | IntermediateTensors:
949+
model_output = self.model(
924950
input_ids, positions, intermediate_tensors, inputs_embeds
925951
)
926952

953+
# Handle EAGLE3 aux_hidden_states (for offline extraction and DFlash spec decode)
954+
if isinstance(model_output, tuple):
955+
hidden_states, aux_hidden_states = model_output
956+
957+
# Extraction mode: save to disk (rank 0 only)
958+
_extract_path = os.environ.get("DFLASH_EXTRACT_PATH", "")
959+
if _extract_path:
960+
import torch.distributed as dist
961+
if not dist.is_initialized() or dist.get_rank() == 0:
962+
import safetensors.torch
963+
_num_tokens = hidden_states.shape[0]
964+
stacked = torch.stack(
965+
[t[:_num_tokens].detach().cpu() for t in aux_hidden_states], dim=1
966+
)
967+
_batch_id = id(input_ids) if input_ids is not None else id(hidden_states)
968+
_filename = os.path.join(_extract_path, f"batch_{_batch_id}.safetensors")
969+
if input_ids is not None:
970+
_token_ids = input_ids[:_num_tokens].detach().cpu().reshape(-1)
971+
else:
972+
_token_ids = torch.tensor([], dtype=torch.long)
973+
safetensors.torch.save_file({
974+
"hidden_states": stacked, "token_ids": _token_ids,
975+
}, _filename)
976+
# Extraction mode: return just hidden_states
977+
return hidden_states
978+
979+
# DFlash spec decode mode: return the tuple so model runner can extract aux_hidden_states
980+
return model_output
981+
else:
982+
hidden_states = model_output
983+
927984
return hidden_states
928985

929986
def compute_logits(

vllm/model_executor/models/qwen2.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,21 @@ def __init__(
104104
quant_config=quant_config,
105105
prefix=f"{prefix}.down_proj",
106106
)
107-
if hidden_act != "silu":
107+
if hidden_act == "silu":
108+
self.act_fn = SiluAndMul()
109+
elif hidden_act == "relu2":
110+
# Relu2AndMul: relu²(gate) * up, same split as SiluAndMul
111+
import torch.nn.functional as F
112+
class Relu2AndMul:
113+
def __call__(self, x):
114+
d = x.shape[-1] // 2
115+
gate, up = x[..., :d], x[..., d:]
116+
return F.relu(gate).pow(2) * up
117+
self.act_fn = Relu2AndMul()
118+
else:
108119
raise ValueError(
109-
f"Unsupported activation: {hidden_act}. Only silu is supported for now."
120+
f"Unsupported activation: {hidden_act}. Only silu and relu2 are supported."
110121
)
111-
self.act_fn = SiluAndMul()
112122

113123
def forward(self, x):
114124
gate_up, _ = self.gate_up_proj(x)

vllm/v1/attention/backends/flex_attention.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1098,8 +1098,8 @@ def forward(
10981098
key_cache, value_cache = kv_cache.unbind(0)
10991099

11001100
# View out the block_size dim
1101-
key_cache = key_cache.view(-1, self.num_kv_heads, self.head_size)
1102-
value_cache = value_cache.view(-1, self.num_kv_heads, self.head_size)
1101+
key_cache = key_cache.reshape(-1, self.num_kv_heads, self.head_size)
1102+
value_cache = value_cache.reshape(-1, self.num_kv_heads, self.head_size)
11031103
query, key_tensor, value_tensor = map(
11041104
lambda x: self.view_as_4d(x).permute(0, 2, 1, 3),
11051105
(query, key_cache, value_cache),

vllm/v1/engine/input_processor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -455,7 +455,7 @@ def _validate_model_input(
455455
f"by setting --limit-mm-per-prompt at startup."
456456
)
457457

458-
if prompt_ids and tokenizer is not None:
458+
if prompt_ids is not None and len(prompt_ids) > 0 and tokenizer is not None:
459459
max_input_id = max(prompt_ids, default=0)
460460

461461
# NOTE: tokenizer.max_token_id is the tokenizer’s vocab size while

vllm/v1/worker/gpu_model_runner.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5543,7 +5543,25 @@ def _dummy_run(
55435543
)
55445544

55455545
if self.use_aux_hidden_state_outputs:
5546-
hidden_states, _ = outputs
5546+
# Debug: check what outputs actually is
5547+
if not isinstance(outputs, tuple):
5548+
print(f"DEBUG _dummy_run: use_aux=True but outputs is {type(outputs)}, not tuple", flush=True)
5549+
hidden_states = outputs
5550+
elif len(outputs) != 2:
5551+
print(f"DEBUG _dummy_run: outputs has {len(outputs)} elements, expected 2. Types: {[type(o) for o in outputs]}", flush=True)
5552+
hidden_states = outputs[0] if len(outputs) > 0 else outputs
5553+
else:
5554+
hs, aux = outputs
5555+
print(f"DEBUG _dummy_run: outputs is 2-tuple, hs type={type(hs)}, aux type={type(aux)}", flush=True)
5556+
if not isinstance(hs, torch.Tensor):
5557+
print(f"DEBUG _dummy_run: hs is NOT a tensor! Trying outputs[0]... type={type(outputs[0])}", flush=True)
5558+
# Maybe the model returns (aux_list, hidden_states) instead
5559+
if isinstance(aux, torch.Tensor):
5560+
hidden_states = aux
5561+
else:
5562+
hidden_states = hs[0] if isinstance(hs, list) and len(hs) > 0 else hs
5563+
else:
5564+
hidden_states = hs
55475565
else:
55485566
hidden_states = outputs
55495567

0 commit comments

Comments
 (0)