feat: support TP>1 Domino rollout for DFlash V2 (#37069)

Co-authored-by: Qiaolin-Yu <liin1211@outlook.com>
This commit is contained in:
Francis
2026-09-11 18:51:11 -07:00
committed by GitHub
co-authored by Qiaolin-Yu
parent 0d1bea77da
commit e91c948057
4 changed files with 424 additions and 27 deletions
@@ -285,7 +285,7 @@ class _SelectorDraftSampler:
class _DominoDraftSampler: class _DominoDraftSampler:
"""Capture-safe TP=1 Domino rollout over a fixed-size draft block.""" """Capture-safe Domino rollout over a fixed-size draft block."""
def __init__( def __init__(
self, self,
@@ -299,6 +299,10 @@ class _DominoDraftSampler:
shift_label, shift_label,
max_bs, max_bs,
candidate_pool_size, candidate_pool_size,
tp_group=None,
lm_head_org_vocab_start=0,
lm_head_num_org=None,
lm_head_num_org_padded=None,
): ):
self.target_embedding = target_embedding self.target_embedding = target_embedding
self.lm_head_weight = lm_head_weight self.lm_head_weight = lm_head_weight
@@ -308,6 +312,10 @@ class _DominoDraftSampler:
self.block_size = int(block_size) self.block_size = int(block_size)
self.shift_label = bool(shift_label) self.shift_label = bool(shift_label)
self.candidate_pool_size = int(candidate_pool_size) self.candidate_pool_size = int(candidate_pool_size)
self.tp_group = tp_group
self.lm_head_org_vocab_start = int(lm_head_org_vocab_start)
self.lm_head_num_org = lm_head_num_org
self.lm_head_num_org_padded = lm_head_num_org_padded
max_tokens = int(max_bs) * (self.block_size - 1) max_tokens = int(max_bs) * (self.block_size - 1)
self.out = torch.empty( self.out = torch.empty(
(max_tokens,), dtype=torch.int64, device=lm_head_weight.device (max_tokens,), dtype=torch.int64, device=lm_head_weight.device
@@ -329,6 +337,11 @@ class _DominoDraftSampler:
vocab_size=self.vocab_size, vocab_size=self.vocab_size,
shift_label=self.shift_label, shift_label=self.shift_label,
candidate_pool_size=self.candidate_pool_size, candidate_pool_size=self.candidate_pool_size,
tp_group=self.tp_group,
lm_head_org_vocab_start=self.lm_head_org_vocab_start,
lm_head_num_org=self.lm_head_num_org,
lm_head_num_org_padded=self.lm_head_num_org_padded,
prefer_tp_candidate_pool=bs > 1,
) )
self.out[: bs * (self.block_size - 1)].copy_(proposals.reshape(-1)) self.out[: bs * (self.block_size - 1)].copy_(proposals.reshape(-1))
@@ -408,6 +421,7 @@ class DFlashWorkerV2(BaseSpecWorker):
validate_domino_runtime( validate_domino_runtime(
device=torch.device(self.device), device=torch.device(self.device),
tp_size=int(get_tp_group().world_size), tp_size=int(get_tp_group().world_size),
tp_rank=int(self.ps.tp_rank),
target_vocab_size=int(self.model_runner.model_config.vocab_size), target_vocab_size=int(self.model_runner.model_config.vocab_size),
draft_vocab_size=int(self.draft_model_runner.model_config.vocab_size), draft_vocab_size=int(self.draft_model_runner.model_config.vocab_size),
hidden_size=int(self.draft_model.config.hidden_size), hidden_size=int(self.draft_model.config.hidden_size),
@@ -463,7 +477,8 @@ class DFlashWorkerV2(BaseSpecWorker):
) )
if self._is_domino: if self._is_domino:
logger.info( logger.info(
"DFLASH Domino rollout enabled (BF16, TP=1, block-shared candidate pool size=%s).", "DFLASH Domino rollout enabled (BF16, TP=%s, block-shared candidate pool size=%s).",
int(get_tp_group().world_size),
self.domino_candidate_pool_size, self.domino_candidate_pool_size,
) )
logger.info( logger.info(
@@ -755,16 +770,16 @@ class DFlashWorkerV2(BaseSpecWorker):
return _eager("quantized lm_head") return _eager("quantized lm_head")
tp_group = get_tp_group() tp_group = get_tp_group()
if self._is_domino: if self._is_domino:
if tp_group.world_size != 1:
return _eager("Domino cuda graph currently requires tp=1")
prefix_gru = self.draft_model.prefix_gru prefix_gru = self.draft_model.prefix_gru
embed_proj = self.draft_model.embed_proj embed_proj = self.draft_model.embed_proj
if prefix_gru is None or embed_proj is None: if prefix_gru is None or embed_proj is None:
return _eager("Domino projector modules are unavailable") return _eager("Domino projector modules are unavailable")
if self.ps.tp_rank == 0: if self.ps.tp_rank == 0:
logger.info( logger.info(
"DFLASH Domino rollout folded into the draft cuda graph (tp=1)." "DFLASH Domino rollout folded into the draft cuda graph (tp=%s).",
int(tp_group.world_size),
) )
shard = getattr(lm_head, "shard_indices", None)
return _DominoDraftSampler( return _DominoDraftSampler(
target_embedding=target_model.get_input_embeddings(), target_embedding=target_model.get_input_embeddings(),
lm_head_weight=lm_head.weight, lm_head_weight=lm_head.weight,
@@ -775,6 +790,16 @@ class DFlashWorkerV2(BaseSpecWorker):
shift_label=self.draft_model.shift_label, shift_label=self.draft_model.shift_label,
max_bs=max(get_exec().graph.cuda_graph_config.decode.bs), max_bs=max(get_exec().graph.cuda_graph_config.decode.bs),
candidate_pool_size=self.domino_candidate_pool_size, candidate_pool_size=self.domino_candidate_pool_size,
tp_group=tp_group,
lm_head_org_vocab_start=(
int(shard.org_vocab_start_index) if shard is not None else 0
),
lm_head_num_org=(
int(shard.num_org_elements) if shard is not None else None
),
lm_head_num_org_padded=(
int(shard.num_org_elements_padded) if shard is not None else None
),
) )
if not hasattr(lm_head, "shard_indices"): if not hasattr(lm_head, "shard_indices"):
if tp_group.world_size != 1: if tp_group.world_size != 1:
@@ -2287,6 +2312,8 @@ class DFlashWorkerV2(BaseSpecWorker):
embed_proj = self.draft_model.embed_proj embed_proj = self.draft_model.embed_proj
if prefix_gru is None or embed_proj is None: if prefix_gru is None or embed_proj is None:
raise RuntimeError("DFLASH Domino projector modules are unavailable.") raise RuntimeError("DFLASH Domino projector modules are unavailable.")
tp_group = get_tp_group()
shard = getattr(lm_head, "shard_indices", None)
draft_next = domino_greedy_rollout( draft_next = domino_greedy_rollout(
draft_hidden=draft_hidden, draft_hidden=draft_hidden,
bonus_tokens=block_ids[:, 0], bonus_tokens=block_ids[:, 0],
@@ -2297,6 +2324,16 @@ class DFlashWorkerV2(BaseSpecWorker):
vocab_size=int(self.model_runner.model_config.vocab_size), vocab_size=int(self.model_runner.model_config.vocab_size),
shift_label=bool(self.draft_model.shift_label), shift_label=bool(self.draft_model.shift_label),
candidate_pool_size=self.domino_candidate_pool_size, candidate_pool_size=self.domino_candidate_pool_size,
tp_group=tp_group,
lm_head_org_vocab_start=(
int(shard.org_vocab_start_index) if shard is not None else 0
),
lm_head_num_org=(
int(shard.num_org_elements) if shard is not None else None
),
lm_head_num_org_padded=(
int(shard.num_org_elements_padded) if shard is not None else None
),
) )
elif self._draft_sampler is not None and draft_out.can_run_graph: elif self._draft_sampler is not None and draft_out.can_run_graph:
draft_next = self._draft_sampler.out[ draft_next = self._draft_sampler.out[
+288 -18
View File
@@ -19,10 +19,113 @@ def _domino_gru_cell(
) )
# Throughput policy for the logical full-vocabulary tensor. TP>1 uses the
# compact candidate pool when this gathered representation would be large.
_DOMINO_TP_FULL_BASE_LOGITS_MAX_BYTES = 96 * 1024 * 1024
def _domino_tp_first_ids(
local_logits: torch.Tensor,
*,
org_vocab_start: int,
num_org: int,
tp_group,
) -> torch.Tensor:
"""Select the full-vocab first argmax from contiguous vocab shards."""
local_max, local_arg = torch.max(local_logits[:, :num_org], dim=-1)
local_ids = local_arg.to(torch.int64) + int(org_vocab_start)
tp_size = int(tp_group.world_size)
batch_size = int(local_logits.shape[0])
gathered_max = torch.empty(
(tp_size * batch_size,), dtype=local_max.dtype, device=local_max.device
)
gathered_ids = torch.empty(
(tp_size * batch_size,), dtype=torch.int64, device=local_max.device
)
tp_group.all_gather_into_tensor(gathered_max, local_max.contiguous())
tp_group.all_gather_into_tensor(gathered_ids, local_ids.contiguous())
gathered_max = gathered_max.view(tp_size, batch_size)
gathered_ids = gathered_ids.view(tp_size, batch_size)
best_rank = torch.argmax(gathered_max, dim=0, keepdim=True)
return torch.gather(gathered_ids, 0, best_rank).squeeze(0)
def _domino_tp_candidate_state(
local_feedback_logits: torch.Tensor,
*,
candidate_pool_size: int,
org_vocab_start: int,
num_org: int,
tp_group,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Build one strict global candidate pool from vocab-sharded base logits."""
num_steps, batch_size, local_vocab_size = local_feedback_logits.shape
tp_size = int(tp_group.world_size)
local_k = min(int(candidate_pool_size), int(local_vocab_size))
if int(candidate_pool_size) > tp_size * local_k:
raise ValueError("Domino TP shards do not cover the requested candidate pool.")
pool_scores = local_feedback_logits.amax(dim=0)
if num_org < local_vocab_size:
pool_scores[:, num_org:].fill_(float("-inf"))
local_scores, local_positions = torch.topk(
pool_scores, k=local_k, dim=-1, sorted=False
)
local_ids = local_positions.to(torch.int64) + int(org_vocab_start)
gathered_scores = torch.empty(
(tp_size * batch_size, local_k),
dtype=local_scores.dtype,
device=local_scores.device,
)
gathered_ids = torch.empty(
(tp_size * batch_size, local_k),
dtype=torch.int64,
device=local_scores.device,
)
tp_group.all_gather_into_tensor(gathered_scores, local_scores.contiguous())
tp_group.all_gather_into_tensor(gathered_ids, local_ids.contiguous())
gathered_scores = (
gathered_scores.view(tp_size, batch_size, local_k)
.permute(1, 0, 2)
.reshape(batch_size, tp_size * local_k)
)
gathered_ids = (
gathered_ids.view(tp_size, batch_size, local_k)
.permute(1, 0, 2)
.reshape(batch_size, tp_size * local_k)
)
global_positions = torch.topk(
gathered_scores,
k=int(candidate_pool_size),
dim=-1,
sorted=False,
).indices
candidate_ids = torch.gather(gathered_ids, 1, global_positions).contiguous()
owned = (candidate_ids >= int(org_vocab_start)) & (
candidate_ids < int(org_vocab_start + num_org)
)
local_positions = (candidate_ids - int(org_vocab_start)).clamp(
0, max(num_org - 1, 0)
)
candidate_base = torch.gather(
local_feedback_logits.transpose(0, 1),
2,
local_positions[:, None, :].expand(-1, num_steps, -1),
)
# Each global candidate is owned by exactly one vocab shard, so SUM
# reconstructs its base logit without gathering the full vocabulary.
candidate_base.masked_fill_(~owned[:, None, :], 0)
candidate_base = tp_group.all_reduce(candidate_base.contiguous())
return candidate_ids, candidate_base.transpose(0, 1)
def validate_domino_runtime( def validate_domino_runtime(
*, *,
device: torch.device, device: torch.device,
tp_size: int, tp_size: int,
tp_rank: int,
target_vocab_size: int, target_vocab_size: int,
draft_vocab_size: int, draft_vocab_size: int,
hidden_size: int, hidden_size: int,
@@ -34,8 +137,14 @@ def validate_domino_runtime(
"""Validate the deliberately narrow correctness-first Domino runtime.""" """Validate the deliberately narrow correctness-first Domino runtime."""
if device.type != "cuda": if device.type != "cuda":
raise ValueError(f"DFLASH Domino currently requires CUDA, got {device}.") raise ValueError(f"DFLASH Domino currently requires CUDA, got {device}.")
if int(tp_size) != 1: tp_size = int(tp_size)
raise ValueError(f"DFLASH Domino currently requires TP=1, got TP={tp_size}.") if tp_size < 1:
raise ValueError(f"DFLASH Domino requires TP>=1, got TP={tp_size}.")
tp_rank = int(tp_rank)
if not 0 <= tp_rank < tp_size:
raise ValueError(
f"DFLASH Domino requires 0<=TP rank<TP size, got rank={tp_rank}, size={tp_size}."
)
if int(target_vocab_size) != int(draft_vocab_size): if int(target_vocab_size) != int(draft_vocab_size):
raise ValueError( raise ValueError(
"DFLASH Domino requires identical target and draft vocab sizes, " "DFLASH Domino requires identical target and draft vocab sizes, "
@@ -49,25 +158,108 @@ def validate_domino_runtime(
"DFLASH Domino requires target embedding and lm_head weight tensors." "DFLASH Domino requires target embedding and lm_head weight tensors."
) )
shard = getattr(lm_head, "shard_indices", None) lm_head_shard = getattr(lm_head, "shard_indices", None)
if shard is not None: if lm_head_shard is not None:
if int(shard.num_added_elements) != 0: if (
int(getattr(lm_head, "num_added_embeddings", 0)) != 0
or int(lm_head_shard.num_added_elements) != 0
):
raise ValueError( raise ValueError(
"DFLASH Domino does not support added-vocab lm_head shards." "DFLASH Domino does not support added-vocab lm_head shards."
) )
if int(shard.org_vocab_start_index) != 0 or int(shard.num_org_elements) != int( if int(getattr(lm_head, "org_vocab_size", target_vocab_size)) != int(
target_vocab_size target_vocab_size
): ):
raise ValueError( raise ValueError(
"DFLASH Domino requires the complete target vocabulary on TP=1." "DFLASH Domino lm_head original vocab size does not match the target."
) )
elif int(lm_head_weight.shape[0]) != int(target_vocab_size): if int(getattr(lm_head, "tp_size", tp_size)) != tp_size:
raise ValueError( raise ValueError(
"DFLASH Domino lm_head row count must equal the target vocab size, " "DFLASH Domino lm_head TP size does not match the runtime TP size."
f"got rows={int(lm_head_weight.shape[0])}, vocab={target_vocab_size}." )
required_shard_fields = (
"org_vocab_start_index",
"org_vocab_end_index",
"num_org_elements",
"num_org_elements_padded",
) )
missing_shard_fields = [
name for name in required_shard_fields if not hasattr(lm_head_shard, name)
]
if missing_shard_fields:
raise ValueError(
"DFLASH Domino lm_head shard metadata is missing: "
+ ", ".join(missing_shard_fields)
)
org_vocab_start = int(lm_head_shard.org_vocab_start_index)
org_vocab_end = int(lm_head_shard.org_vocab_end_index)
num_org = int(lm_head_shard.num_org_elements)
num_org_padded = int(lm_head_shard.num_org_elements_padded)
if (
num_org <= 0
or org_vocab_start < 0
or org_vocab_end != org_vocab_start + num_org
or org_vocab_end > int(target_vocab_size)
or num_org_padded < num_org
):
raise ValueError("DFLASH Domino lm_head original-vocab shard is invalid.")
if int(lm_head_weight.shape[0]) < num_org_padded:
raise ValueError(
"DFLASH Domino lm_head weight is smaller than its padded vocab shard."
)
expected_start = tp_rank * num_org_padded
expected_end = min(expected_start + num_org_padded, int(target_vocab_size))
if (
org_vocab_start != expected_start
or org_vocab_end != expected_end
or num_org != expected_end - expected_start
):
raise ValueError(
"DFLASH Domino lm_head vocab shard does not match its TP rank."
)
else:
if tp_size != 1:
raise ValueError("DFLASH Domino requires lm_head shard metadata for TP>1.")
if int(lm_head_weight.shape[0]) != int(target_vocab_size):
raise ValueError(
"DFLASH Domino lm_head row count must equal the target vocab size, "
f"got rows={int(lm_head_weight.shape[0])}, vocab={target_vocab_size}."
)
if int(embedding_weight.shape[0]) < int(target_vocab_size): embedding_shard = getattr(target_embedding, "shard_indices", None)
if embedding_shard is not None:
if (
int(getattr(target_embedding, "num_added_embeddings", 0)) != 0
or int(embedding_shard.num_added_elements) != 0
):
raise ValueError(
"DFLASH Domino does not support added-vocab embedding shards."
)
if int(getattr(target_embedding, "org_vocab_size", target_vocab_size)) != int(
target_vocab_size
):
raise ValueError(
"DFLASH Domino embedding original vocab size does not match the target."
)
embedding_tp_size = int(getattr(target_embedding, "tp_size", tp_size))
if embedding_tp_size not in (1, tp_size):
raise ValueError(
"DFLASH Domino embedding TP size does not match the runtime TP size."
)
required_embedding_rows = (
int(target_vocab_size)
if embedding_tp_size == 1
else int(embedding_shard.num_org_elements_padded)
)
if int(embedding_weight.shape[0]) < required_embedding_rows:
raise ValueError(
"DFLASH Domino embedding weight is smaller than its padded vocab shard."
)
elif int(embedding_weight.shape[0]) < int(target_vocab_size):
if tp_size != 1:
raise ValueError(
"DFLASH Domino requires embedding shard metadata for TP>1."
)
raise ValueError( raise ValueError(
"DFLASH Domino target embedding has fewer rows than the target vocab " "DFLASH Domino target embedding has fewer rows than the target vocab "
f"size: rows={int(embedding_weight.shape[0])}, vocab={target_vocab_size}." f"size: rows={int(embedding_weight.shape[0])}, vocab={target_vocab_size}."
@@ -119,6 +311,11 @@ def domino_greedy_rollout(
vocab_size: int, vocab_size: int,
shift_label: bool, shift_label: bool,
candidate_pool_size: int, candidate_pool_size: int,
tp_group=None,
lm_head_org_vocab_start: int = 0,
lm_head_num_org: int | None = None,
lm_head_num_org_padded: int | None = None,
prefer_tp_candidate_pool: bool | None = None,
) -> torch.Tensor: ) -> torch.Tensor:
"""Generate a Domino chain using one block-shared base-logit candidate pool.""" """Generate a Domino chain using one block-shared base-logit candidate pool."""
if draft_hidden.ndim != 3: if draft_hidden.ndim != 3:
@@ -148,24 +345,97 @@ def domino_greedy_rollout(
"Domino draft hidden states do not contain enough proposal positions." "Domino draft hidden states do not contain enough proposal positions."
) )
weight = lm_head_weight[: int(vocab_size)] tp_size = int(tp_group.world_size) if tp_group is not None else 1
if tp_size > 1 and (lm_head_num_org is None or lm_head_num_org_padded is None):
raise ValueError(
"Domino TP rollout requires local lm_head vocab shard metadata."
)
local_vocab_size = int(lm_head_num_org_padded) if tp_size > 1 else int(vocab_size)
if tp_size > 1:
num_org = int(lm_head_num_org)
org_vocab_start = int(lm_head_org_vocab_start)
if (
num_org <= 0
or num_org > local_vocab_size
or org_vocab_start < 0
or org_vocab_start + num_org > int(vocab_size)
):
raise ValueError(
"Domino TP rollout received an invalid lm_head vocab shard."
)
if int(lm_head_weight.shape[0]) < local_vocab_size:
raise ValueError(
"Domino lm_head weight is smaller than its padded vocab shard."
)
weight = lm_head_weight[:local_vocab_size]
z_for_logits = z.to(weight.dtype) if z.dtype != weight.dtype else z z_for_logits = z.to(weight.dtype) if z.dtype != weight.dtype else z
logits_input = ( logits_input = (
z_for_logits.transpose(0, 1) z_for_logits.transpose(0, 1)
.contiguous() .contiguous()
.view(num_proposals * batch_size, hidden_size) .view(num_proposals * batch_size, hidden_size)
) )
base_logits = F.linear(logits_input, weight).view(num_proposals, batch_size, -1) local_logits = F.linear(logits_input, weight).view(
num_proposals, batch_size, local_vocab_size
)
if prefer_tp_candidate_pool is None:
full_base_logits_bytes = (
num_proposals * batch_size * int(vocab_size) * local_logits.element_size()
)
prefer_tp_candidate_pool = (
full_base_logits_bytes > _DOMINO_TP_FULL_BASE_LOGITS_MAX_BYTES
)
use_tp_candidate_pool = (
tp_size > 1
and prefer_tp_candidate_pool
and 0 < candidate_pool_size < int(vocab_size)
)
first_ids = None
candidate_ids = None
candidate_base = None
if tp_size == 1:
base_logits = local_logits[:, :, : int(vocab_size)]
elif use_tp_candidate_pool:
first_ids = _domino_tp_first_ids(
local_logits[0],
org_vocab_start=int(lm_head_org_vocab_start),
num_org=int(lm_head_num_org),
tp_group=tp_group,
)
if num_proposals > 1:
candidate_ids, candidate_base = _domino_tp_candidate_state(
local_logits[1:],
candidate_pool_size=candidate_pool_size,
org_vocab_start=int(lm_head_org_vocab_start),
num_org=int(lm_head_num_org),
tp_group=tp_group,
)
base_logits = None
else:
local_logits_t = local_logits.view(
num_proposals * batch_size, local_vocab_size
).T.contiguous()
gathered_logits = torch.empty(
(tp_size * local_vocab_size, num_proposals * batch_size),
dtype=local_logits.dtype,
device=local_logits.device,
)
tp_group.all_gather_into_tensor(gathered_logits, local_logits_t)
base_logits = (
gathered_logits.T[:, : int(vocab_size)]
.contiguous()
.view(num_proposals, batch_size, int(vocab_size))
)
first_ids = torch.argmax(base_logits[0], dim=-1).to(torch.long) if first_ids is None:
first_ids = torch.argmax(base_logits[0], dim=-1).to(torch.long)
proposals = [first_ids] proposals = [first_ids]
if num_proposals == 1: if num_proposals == 1:
return first_ids[:, None] return first_ids[:, None]
candidate_ids = None
candidate_base = None
candidate_weight = None candidate_weight = None
if 0 < candidate_pool_size < int(vocab_size): if candidate_ids is not None:
candidate_weight = F.embedding(candidate_ids, embed_proj[2].weight)
elif 0 < candidate_pool_size < int(vocab_size):
feedback_logits = base_logits[1:] feedback_logits = base_logits[1:]
candidate_ids = torch.topk( candidate_ids = torch.topk(
feedback_logits.amax(dim=0), feedback_logits.amax(dim=0),
@@ -14,7 +14,7 @@ from sglang.test.test_utils import (
popen_launch_server, popen_launch_server,
) )
register_cuda_ci(est_time=600, stage="base-b", runner_config="1-gpu-small") register_cuda_ci(est_time=600, stage="base-b", runner_config="2-gpu-large")
class TestDFlashDominoFullVocab(CustomTestCase): class TestDFlashDominoFullVocab(CustomTestCase):
@@ -38,7 +38,7 @@ class TestDFlashDominoFullVocab(CustomTestCase):
"--dtype", "--dtype",
"bfloat16", "bfloat16",
"--tp-size", "--tp-size",
"1", "2",
"--attention-backend", "--attention-backend",
"triton", "triton",
"--speculative-algorithm", "--speculative-algorithm",
@@ -64,6 +64,7 @@ class TestDFlashDominoFullVocab(CustomTestCase):
response = requests.get(self.base_url + "/server_info", timeout=10) response = requests.get(self.base_url + "/server_info", timeout=10)
response.raise_for_status() response.raise_for_status()
state = response.json()["internal_states"][0] state = response.json()["internal_states"][0]
self.assertEqual(state["tp_size"], 2)
self.assertEqual(state["speculative_num_draft_tokens"], 16) self.assertEqual(state["speculative_num_draft_tokens"], 16)
self.assertFalse(state["disable_overlap_schedule"]) self.assertFalse(state["disable_overlap_schedule"])
self.assertEqual( self.assertEqual(
@@ -71,11 +72,11 @@ class TestDFlashDominoFullVocab(CustomTestCase):
) )
log = Path(self.server_log.name).read_text() log = Path(self.server_log.name).read_text()
self.assertIn( self.assertIn(
"DFLASH Domino rollout enabled (BF16, TP=1, " "DFLASH Domino rollout enabled (BF16, TP=2, "
f"block-shared candidate pool size={self.candidate_pool_size}).", f"block-shared candidate pool size={self.candidate_pool_size}).",
log, log,
) )
self.assertIn("Domino rollout folded into the draft cuda graph", log) self.assertIn("Domino rollout folded into the draft cuda graph (tp=2)", log)
self.assertIn( self.assertIn(
"Capture draft verify CUDA graph begin. backend=full, num_tokens_per_req=16,", "Capture draft verify CUDA graph begin. backend=full, num_tokens_per_req=16,",
log, log,
@@ -6,6 +6,7 @@ from torch import nn
from sglang.srt.models.dflash import DFlashDraftModel from sglang.srt.models.dflash import DFlashDraftModel
from sglang.srt.speculative.dflash_utils import parse_dflash_draft_config from sglang.srt.speculative.dflash_utils import parse_dflash_draft_config
from sglang.srt.speculative.domino_utils import validate_domino_runtime
from sglang.test.ci.ci_register import register_cpu_ci from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.test_utils import CustomTestCase from sglang.test.test_utils import CustomTestCase
@@ -126,5 +127,93 @@ class TestDFlashDominoWeights(CustomTestCase):
model.load_weights([("prefix_gru.weight_ih_l0", torch.empty(12, 8))]) model.load_weights([("prefix_gru.weight_ih_l0", torch.empty(12, 8))])
class TestDFlashDominoRuntimeValidation(CustomTestCase):
def _modules(self, dtype=torch.bfloat16):
embedding = nn.Embedding(31, 8, dtype=dtype)
lm_head = nn.Linear(8, 31, bias=False, dtype=dtype)
prefix_gru = nn.GRU(8, 4, batch_first=True, bias=False, dtype=dtype)
embed_proj = nn.Sequential(
nn.Linear(12, 5, bias=False, dtype=dtype),
nn.SiLU(),
nn.Linear(5, 31, bias=False, dtype=dtype),
)
return embedding, lm_head, prefix_gru, embed_proj
def _tp2_modules(self):
embedding, lm_head, prefix_gru, embed_proj = self._modules()
embedding = nn.Embedding(16, 8, dtype=torch.bfloat16)
lm_head = nn.Linear(8, 16, bias=False, dtype=torch.bfloat16)
shard = SimpleNamespace(
num_added_elements=0,
org_vocab_start_index=0,
org_vocab_end_index=16,
num_org_elements=16,
num_org_elements_padded=16,
)
for module in (embedding, lm_head):
module.shard_indices = shard
module.org_vocab_size = 31
module.tp_size = 2
module.num_added_embeddings = 0
return embedding, lm_head, prefix_gru, embed_proj
def _validate(self, **overrides):
embedding, lm_head, prefix_gru, embed_proj = overrides.pop(
"modules", self._modules()
)
args = {
"device": torch.device("cuda"),
"tp_size": 1,
"tp_rank": 0,
"target_vocab_size": 31,
"draft_vocab_size": 31,
"hidden_size": 8,
"target_embedding": embedding,
"lm_head": lm_head,
"prefix_gru": prefix_gru,
"embed_proj": embed_proj,
}
args.update(overrides)
validate_domino_runtime(**args)
def test_tp_requires_vocab_shard_metadata(self):
with self.assertRaisesRegex(ValueError, "lm_head shard metadata"):
self._validate(tp_size=2)
def test_tp2_vocab_shards_supported(self):
self._validate(tp_size=2, modules=self._tp2_modules())
def test_tp2_incomplete_lm_head_shard_fails(self):
modules = self._tp2_modules()
modules[1].shard_indices = SimpleNamespace(
num_added_elements=0,
num_org_elements_padded=16,
)
with self.assertRaisesRegex(ValueError, "shard metadata is missing"):
self._validate(tp_size=2, modules=modules)
def test_tp_vocab_shard_must_match_rank(self):
modules = self._tp2_modules()
modules[1].shard_indices.org_vocab_start_index = 1
modules[1].shard_indices.org_vocab_end_index = 17
with self.assertRaisesRegex(ValueError, "does not match its TP rank"):
self._validate(tp_size=2, modules=modules)
def test_tp1_requires_complete_vocab_shard(self):
modules = self._modules()
modules[1].shard_indices = SimpleNamespace(
num_added_elements=0,
org_vocab_start_index=0,
org_vocab_end_index=30,
num_org_elements=30,
num_org_elements_padded=31,
)
modules[1].org_vocab_size = 31
modules[1].tp_size = 1
modules[1].num_added_embeddings = 0
with self.assertRaisesRegex(ValueError, "does not match its TP rank"):
self._validate(modules=modules)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()