[Feature] Add DFLASH speculative decoding support (#22077)
Co-authored-by: Jian Chen <141193260+jianc99@users.noreply.github.com> Co-authored-by: Zhijian Liu <5782437+zhijian-liu@users.noreply.github.com> Co-authored-by: Richard Gong <8001209+gongy@users.noreply.github.com> Co-authored-by: David Wang <21328423+dcw02@users.noreply.github.com> Co-authored-by: yilian49 <43861414+yilian49@users.noreply.github.com> Co-authored-by: xm:D <38322020+xiaomin-d@users.noreply.github.com>
This commit is contained in:
co-authored by
Jian Chen
Zhijian Liu
Richard Gong
yilian49
xm:D
parent
e14876742a
commit
f08726fd56
@@ -596,8 +596,24 @@ class FlashInferAttnBackend(AttentionBackend):
|
||||
fast_decode_plan, decode_wrappers[i]
|
||||
)
|
||||
elif forward_mode.is_target_verify():
|
||||
# FlashInfer's prefill wrapper decides mask mode based on whether
|
||||
# `custom_mask_buf` is initialized (not whether a custom mask is provided).
|
||||
# For cases like DFLASH draft (ENCODER_ONLY / non-causal) we do NOT use a
|
||||
# custom mask, so we must avoid initializing `custom_mask_buf`, otherwise
|
||||
# FlashInfer will treat the (zero) buffer as a real mask and block attention.
|
||||
use_custom_mask = (
|
||||
spec_info is not None
|
||||
and getattr(spec_info, "custom_mask", None) is not None
|
||||
)
|
||||
prefill_wrappers = []
|
||||
for i in range(self.num_wrappers):
|
||||
wrapper_kwargs = {}
|
||||
if use_custom_mask:
|
||||
wrapper_kwargs = {
|
||||
"custom_mask_buf": self.cuda_graph_custom_mask,
|
||||
"mask_indptr_buf": self.cuda_graph_qk_indptr[i][: bs + 1],
|
||||
}
|
||||
|
||||
prefill_wrappers.append(
|
||||
BatchPrefillWithPagedKVCacheWrapper(
|
||||
self.workspace_buffer,
|
||||
@@ -608,8 +624,7 @@ class FlashInferAttnBackend(AttentionBackend):
|
||||
paged_kv_indptr_buf=self.kv_indptr[i][: bs + 1],
|
||||
paged_kv_indices_buf=self.cuda_graph_kv_indices[i],
|
||||
paged_kv_last_page_len_buf=self.kv_last_page_len[:bs],
|
||||
custom_mask_buf=self.cuda_graph_custom_mask,
|
||||
mask_indptr_buf=self.cuda_graph_qk_indptr[i][: bs + 1],
|
||||
**wrapper_kwargs,
|
||||
)
|
||||
)
|
||||
seq_lens_sum = seq_lens.sum().item()
|
||||
@@ -783,10 +798,14 @@ class FlashInferAttnBackend(AttentionBackend):
|
||||
layer, cache_loc, k, v, layer.k_scale, layer.v_scale
|
||||
)
|
||||
|
||||
causal = (
|
||||
not layer.is_cross_attention
|
||||
and layer.attn_type != AttentionType.ENCODER_ONLY
|
||||
)
|
||||
o = prefill_wrapper_paged.forward(
|
||||
q.view(-1, layer.tp_q_head_num, layer.head_dim),
|
||||
forward_batch.token_to_kv_pool.get_kv_buffer(layer.layer_id),
|
||||
causal=not layer.is_cross_attention,
|
||||
causal=causal,
|
||||
sm_scale=layer.scaling,
|
||||
# Disable sliding window attention for multi-item scoring:
|
||||
# - Sliding window could cut across item boundaries, breaking semantic coherence
|
||||
@@ -838,11 +857,6 @@ class FlashInferAttnBackend(AttentionBackend):
|
||||
)
|
||||
|
||||
else:
|
||||
if not self.is_dllm_model:
|
||||
# TODO: design a better interface
|
||||
# For other models, use causal attention for the ragged part as previously
|
||||
causal = True
|
||||
|
||||
o1, s1 = self.prefill_wrapper_ragged.forward_return_lse(
|
||||
q.view(-1, layer.tp_q_head_num, layer.head_dim),
|
||||
k.view(-1, layer.tp_k_head_num, layer.head_dim),
|
||||
|
||||
@@ -276,6 +276,24 @@ class EmbeddingBatchResult:
|
||||
self.copy_done.record()
|
||||
|
||||
|
||||
def validate_dflash_request(req: Req) -> Optional[str]:
|
||||
if req.return_logprob:
|
||||
return "DFLASH speculative decoding does not support return_logprob yet."
|
||||
|
||||
if (
|
||||
req.sampling_params.json_schema is not None
|
||||
or req.sampling_params.regex is not None
|
||||
or req.sampling_params.ebnf is not None
|
||||
or req.sampling_params.structural_tag is not None
|
||||
):
|
||||
return (
|
||||
"DFLASH speculative decoding does not support "
|
||||
"grammar-constrained decoding yet."
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class Scheduler(
|
||||
SchedulerOutputProcessorMixin,
|
||||
SchedulerUpdateWeightsMixin,
|
||||
@@ -1861,6 +1879,14 @@ class Scheduler(
|
||||
self._add_request_to_queue(req)
|
||||
return
|
||||
|
||||
if self.spec_algorithm.is_dflash():
|
||||
error_msg = validate_dflash_request(req)
|
||||
if error_msg is not None:
|
||||
req.set_finish_with_abort(error_msg)
|
||||
self.init_req_max_new_tokens(req)
|
||||
self._add_request_to_queue(req)
|
||||
return
|
||||
|
||||
# Handle multimodal inputs
|
||||
if recv_req.mm_inputs is not None:
|
||||
image_inputs = self._get_multimodal_inputs(recv_req.mm_inputs)
|
||||
|
||||
@@ -547,18 +547,15 @@ class CudaGraphRunner:
|
||||
self.capture_forward_mode = ForwardMode.DECODE
|
||||
self.capture_hidden_mode = CaptureHiddenMode.NULL
|
||||
self.num_tokens_per_bs = 1
|
||||
if (
|
||||
model_runner.spec_algorithm.is_eagle()
|
||||
or model_runner.spec_algorithm.is_standalone()
|
||||
or model_runner.spec_algorithm.is_ngram()
|
||||
):
|
||||
if model_runner.spec_algorithm.is_speculative():
|
||||
if self.model_runner.is_draft_worker:
|
||||
raise RuntimeError("This should not happen")
|
||||
else:
|
||||
self.capture_forward_mode = ForwardMode.TARGET_VERIFY
|
||||
self.num_tokens_per_bs = (
|
||||
self.model_runner.server_args.speculative_num_draft_tokens
|
||||
)
|
||||
# DFLASH draft workers reuse this runner for TARGET_VERIFY mode.
|
||||
if not self.model_runner.spec_algorithm.is_dflash():
|
||||
raise RuntimeError("This should not happen")
|
||||
self.capture_forward_mode = ForwardMode.TARGET_VERIFY
|
||||
self.num_tokens_per_bs = (
|
||||
self.model_runner.server_args.speculative_num_draft_tokens
|
||||
)
|
||||
elif self.is_dllm:
|
||||
self.capture_forward_mode = ForwardMode.DLLM_EXTEND
|
||||
self.num_tokens_per_bs = self.dllm_config.block_size
|
||||
@@ -646,6 +643,18 @@ class CudaGraphRunner:
|
||||
and model_runner.eagle_use_aux_hidden_state
|
||||
):
|
||||
self.model_runner.model.set_eagle3_layers_to_capture()
|
||||
if (
|
||||
model_runner.spec_algorithm.is_dflash()
|
||||
and model_runner.dflash_use_aux_hidden_state
|
||||
):
|
||||
if not hasattr(self.model_runner.model, "set_dflash_layers_to_capture"):
|
||||
raise ValueError(
|
||||
f"Model {self.model_runner.model.__class__.__name__} does not implement set_dflash_layers_to_capture, "
|
||||
"which is required for DFLASH aux hidden capture."
|
||||
)
|
||||
self.model_runner.model.set_dflash_layers_to_capture(
|
||||
self.model_runner.dflash_target_layer_ids
|
||||
)
|
||||
|
||||
# Capture
|
||||
try:
|
||||
@@ -671,6 +680,7 @@ class CudaGraphRunner:
|
||||
max(forward_batch.global_num_tokens_cpu) // self.num_tokens_per_bs
|
||||
if self.model_runner.spec_algorithm.is_eagle()
|
||||
or self.model_runner.spec_algorithm.is_standalone()
|
||||
or self.model_runner.spec_algorithm.is_dflash()
|
||||
else max(forward_batch.global_num_tokens_cpu)
|
||||
)
|
||||
else:
|
||||
@@ -1007,6 +1017,12 @@ class CudaGraphRunner:
|
||||
kwargs["pp_proxy_tensors"] = PPProxyTensors(
|
||||
{k: v.clone() for k, v in pp_proxy_tensors.tensors.items()}
|
||||
)
|
||||
if (
|
||||
self.model_runner.spec_algorithm.is_dflash()
|
||||
and self.model_runner.is_draft_worker
|
||||
and "input_embeds" in inspect.signature(forward).parameters
|
||||
):
|
||||
kwargs["input_embeds"] = buffers.input_embeds[:num_tokens]
|
||||
|
||||
logits_output_or_pp_proxy_tensors = forward(
|
||||
input_ids,
|
||||
@@ -1083,6 +1099,7 @@ class CudaGraphRunner:
|
||||
max_num_tokens / self.num_tokens_per_bs
|
||||
if self.model_runner.spec_algorithm.is_eagle()
|
||||
or self.model_runner.spec_algorithm.is_standalone()
|
||||
or self.model_runner.spec_algorithm.is_dflash()
|
||||
else max_num_tokens
|
||||
)
|
||||
index = bisect.bisect_left(self.capture_bs, max_batch_size)
|
||||
@@ -1104,6 +1121,13 @@ class CudaGraphRunner:
|
||||
),
|
||||
pp_proxy_tensors=pp_proxy_tensors,
|
||||
)
|
||||
if (
|
||||
self.model_runner.spec_algorithm.is_dflash()
|
||||
and self.model_runner.is_draft_worker
|
||||
and forward_batch.input_embeds is not None
|
||||
):
|
||||
buffers.input_embeds[:raw_num_token].copy_(forward_batch.input_embeds)
|
||||
# Padded tokens aren't read, so skip zeroing them.
|
||||
if self.enable_two_batch_overlap:
|
||||
self.tbo_plugin.replay_prepare(
|
||||
forward_mode=self.capture_forward_mode,
|
||||
@@ -1152,6 +1176,14 @@ class CudaGraphRunner:
|
||||
# In speculative decoding, these two fields are still needed.
|
||||
self.buffers.input_ids[: self.raw_num_token].copy_(forward_batch.input_ids)
|
||||
self.buffers.positions[: self.raw_num_token].copy_(forward_batch.positions)
|
||||
if (
|
||||
self.model_runner.spec_algorithm.is_dflash()
|
||||
and self.model_runner.is_draft_worker
|
||||
and forward_batch.input_embeds is not None
|
||||
):
|
||||
self.buffers.input_embeds[: self.raw_num_token].copy_(
|
||||
forward_batch.input_embeds
|
||||
)
|
||||
|
||||
# Replay
|
||||
if self.enable_pdmux:
|
||||
@@ -1164,10 +1196,18 @@ class CudaGraphRunner:
|
||||
if isinstance(output, LogitsProcessorOutput):
|
||||
if self.is_dllm:
|
||||
next_token_logits = None
|
||||
full_logits = output.full_logits[: self.raw_num_token]
|
||||
full_logits = (
|
||||
output.full_logits[: self.raw_num_token]
|
||||
if output.full_logits is not None
|
||||
else None
|
||||
)
|
||||
else:
|
||||
full_logits = None
|
||||
next_token_logits = output.next_token_logits[: self.raw_num_token]
|
||||
next_token_logits = (
|
||||
output.next_token_logits[: self.raw_num_token]
|
||||
if output.next_token_logits is not None
|
||||
else None
|
||||
)
|
||||
|
||||
return LogitsProcessorOutput(
|
||||
next_token_logits=next_token_logits,
|
||||
@@ -1209,6 +1249,32 @@ class CudaGraphRunner:
|
||||
seq_lens_sum=None,
|
||||
seq_lens_cpu=None,
|
||||
)
|
||||
elif self.model_runner.spec_algorithm.is_dflash():
|
||||
from sglang.srt.speculative.dflash_info import DFlashVerifyInput
|
||||
from sglang.srt.speculative.dflash_utils import (
|
||||
resolve_dflash_verify_mask_policy,
|
||||
)
|
||||
|
||||
# Avoid enabling custom-mask modes during graph capture for backends that
|
||||
# can express DFLASH verify via their built-in causal path.
|
||||
_, build_custom_mask = resolve_dflash_verify_mask_policy(
|
||||
self.model_runner.attn_backend
|
||||
)
|
||||
spec_info = DFlashVerifyInput(
|
||||
draft_token=None,
|
||||
positions=None,
|
||||
draft_token_num=self.model_runner.server_args.speculative_num_draft_tokens,
|
||||
custom_mask=(
|
||||
None
|
||||
if (self.model_runner.is_draft_worker or not build_custom_mask)
|
||||
else self.buffers.custom_mask
|
||||
),
|
||||
capture_hidden_mode=(
|
||||
CaptureHiddenMode.NULL
|
||||
if self.model_runner.is_draft_worker
|
||||
else CaptureHiddenMode.FULL
|
||||
),
|
||||
)
|
||||
|
||||
elif self.model_runner.spec_algorithm.is_ngram():
|
||||
from sglang.srt.speculative.ngram_info import NgramVerifyInput
|
||||
|
||||
@@ -354,6 +354,9 @@ class ModelRunner(ModelRunnerKVCacheMixin):
|
||||
self.remote_instance_transfer_engine_weight_info = None
|
||||
# auxiliary hidden capture mode. TODO: expose this to server args?
|
||||
self.eagle_use_aux_hidden_state = False
|
||||
self.dflash_use_aux_hidden_state = False
|
||||
self.dflash_target_layer_ids = None
|
||||
self.dflash_draft_num_layers = None
|
||||
if self.spec_algorithm.is_eagle3() and not self.is_draft_worker:
|
||||
# load draft config
|
||||
draft_model_config = ModelConfig.from_server_args(
|
||||
@@ -379,6 +382,52 @@ class ModelRunner(ModelRunnerKVCacheMixin):
|
||||
# if there is no aux layer, set to None
|
||||
self.eagle_aux_hidden_state_layer_ids = None
|
||||
|
||||
if self.spec_algorithm.is_dflash() and not self.is_draft_worker:
|
||||
from sglang.srt.speculative.dflash_utils import (
|
||||
parse_dflash_draft_config,
|
||||
)
|
||||
|
||||
# Select target layers to capture for building DFlash context features.
|
||||
draft_model_config = ModelConfig.from_server_args(
|
||||
server_args,
|
||||
model_path=(server_args.speculative_draft_model_path),
|
||||
model_revision=server_args.speculative_draft_model_revision,
|
||||
is_draft_model=True,
|
||||
)
|
||||
dflash_draft_config = parse_dflash_draft_config(
|
||||
draft_hf_config=draft_model_config.hf_config
|
||||
)
|
||||
draft_num_layers = dflash_draft_config.require_num_layers()
|
||||
trained_target_layers = dflash_draft_config.num_target_layers
|
||||
|
||||
target_num_layers = getattr(
|
||||
self.model_config.hf_text_config, "num_hidden_layers", None
|
||||
)
|
||||
if target_num_layers is None:
|
||||
raise ValueError(
|
||||
"DFLASH requires target num_hidden_layers in config. "
|
||||
f"Got target={target_num_layers}."
|
||||
)
|
||||
target_num_layers = int(target_num_layers)
|
||||
|
||||
if (
|
||||
trained_target_layers is not None
|
||||
and trained_target_layers != target_num_layers
|
||||
):
|
||||
logger.warning(
|
||||
"DFLASH draft config num_target_layers=%s differs from runtime target num_hidden_layers=%s; "
|
||||
"selecting capture layers based on the runtime target model.",
|
||||
trained_target_layers,
|
||||
target_num_layers,
|
||||
)
|
||||
|
||||
self.dflash_use_aux_hidden_state = True
|
||||
self.dflash_draft_num_layers = int(draft_num_layers)
|
||||
self.dflash_target_layer_ids = dflash_draft_config.resolve_target_layer_ids(
|
||||
target_num_layers=int(target_num_layers),
|
||||
draft_num_layers=int(draft_num_layers),
|
||||
)
|
||||
|
||||
# Apply the rank zero filter to logger
|
||||
if server_args.show_time_cost:
|
||||
enable_show_time_cost()
|
||||
@@ -670,6 +719,14 @@ class ModelRunner(ModelRunnerKVCacheMixin):
|
||||
self.eagle_aux_hidden_state_layer_ids
|
||||
)
|
||||
|
||||
if self.dflash_use_aux_hidden_state:
|
||||
if not hasattr(self.model, "set_dflash_layers_to_capture"):
|
||||
raise ValueError(
|
||||
f"Model {self.model.__class__.__name__} does not implement set_dflash_layers_to_capture, "
|
||||
"which is required for DFLASH."
|
||||
)
|
||||
self.model.set_dflash_layers_to_capture(self.dflash_target_layer_ids)
|
||||
|
||||
# Initialize piecewise CUDA graph
|
||||
self.init_piecewise_cuda_graphs()
|
||||
|
||||
@@ -2100,11 +2157,7 @@ class ModelRunner(ModelRunnerKVCacheMixin):
|
||||
if major < 9:
|
||||
return False
|
||||
|
||||
if (
|
||||
self.spec_algorithm.is_eagle()
|
||||
or self.spec_algorithm.is_standalone()
|
||||
or self.spec_algorithm.is_ngram()
|
||||
):
|
||||
if self.spec_algorithm.is_speculative():
|
||||
return not self.is_draft_worker
|
||||
|
||||
return True
|
||||
@@ -2134,16 +2187,12 @@ class ModelRunner(ModelRunnerKVCacheMixin):
|
||||
capture_forward_mode = ForwardMode.EXTEND
|
||||
capture_hidden_mode = CaptureHiddenMode.NULL
|
||||
num_tokens_per_bs = 1
|
||||
if (
|
||||
self.spec_algorithm.is_eagle()
|
||||
or self.spec_algorithm.is_standalone()
|
||||
or self.spec_algorithm.is_ngram()
|
||||
):
|
||||
if self.spec_algorithm.is_speculative():
|
||||
if self.is_draft_worker:
|
||||
raise RuntimeError("This should not happen")
|
||||
else:
|
||||
capture_forward_mode = ForwardMode.TARGET_VERIFY
|
||||
num_tokens_per_bs = self.server_args.speculative_num_draft_tokens
|
||||
if not self.spec_algorithm.is_dflash():
|
||||
raise RuntimeError("This should not happen")
|
||||
capture_forward_mode = ForwardMode.TARGET_VERIFY
|
||||
num_tokens_per_bs = self.server_args.speculative_num_draft_tokens
|
||||
|
||||
if self.server_args.enable_return_hidden_states:
|
||||
capture_hidden_mode = CaptureHiddenMode.FULL
|
||||
@@ -2173,6 +2222,8 @@ class ModelRunner(ModelRunnerKVCacheMixin):
|
||||
|
||||
if self.eagle_use_aux_hidden_state:
|
||||
self.model.set_eagle3_layers_to_capture()
|
||||
if self.dflash_use_aux_hidden_state:
|
||||
self.model.set_dflash_layers_to_capture(self.dflash_target_layer_ids)
|
||||
|
||||
require_mlp_tp_gather_ = require_mlp_tp_gather(self.server_args)
|
||||
if require_gathered_buffer(self.server_args):
|
||||
@@ -2286,6 +2337,21 @@ class ModelRunner(ModelRunnerKVCacheMixin):
|
||||
seq_lens_sum=None,
|
||||
seq_lens_cpu=None,
|
||||
)
|
||||
elif self.spec_algorithm.is_dflash():
|
||||
from sglang.srt.speculative.dflash_info import DFlashVerifyInput
|
||||
|
||||
# Dummy warmup only needs shape metadata; avoid forcing custom-mask mode.
|
||||
spec_info = DFlashVerifyInput(
|
||||
draft_token=None,
|
||||
positions=None,
|
||||
draft_token_num=self.server_args.speculative_num_draft_tokens,
|
||||
custom_mask=None,
|
||||
capture_hidden_mode=(
|
||||
CaptureHiddenMode.NULL
|
||||
if self.is_draft_worker
|
||||
else CaptureHiddenMode.FULL
|
||||
),
|
||||
)
|
||||
|
||||
elif self.spec_algorithm.is_ngram():
|
||||
from sglang.srt.speculative.ngram_info import NgramVerifyInput
|
||||
|
||||
@@ -167,6 +167,22 @@ class ModelRunnerKVCacheMixin:
|
||||
num_layers = self.num_effective_layers
|
||||
|
||||
cell_size = self.get_cell_size_per_token(num_layers)
|
||||
if self.spec_algorithm.is_dflash() and not self.is_draft_worker:
|
||||
from sglang.srt.speculative.dflash_utils import (
|
||||
scale_kv_cell_size_per_token_for_dflash,
|
||||
)
|
||||
|
||||
draft_num_layers = getattr(self, "dflash_draft_num_layers", None)
|
||||
if (
|
||||
draft_num_layers is not None
|
||||
and int(draft_num_layers) > 0
|
||||
and int(num_layers) > 0
|
||||
):
|
||||
cell_size = scale_kv_cell_size_per_token_for_dflash(
|
||||
target_cell_size_per_token=cell_size,
|
||||
target_num_layers=int(num_layers),
|
||||
draft_num_layers=int(draft_num_layers),
|
||||
)
|
||||
|
||||
rest_memory = post_model_load_memory - pre_model_load_memory * (
|
||||
1 - self.mem_fraction_static
|
||||
|
||||
@@ -0,0 +1,399 @@
|
||||
# Adapted from the DFlash reference implementation (HF) but implemented with
|
||||
# SGLang primitives (RadixAttention + SGLang KV cache). This model intentionally
|
||||
# does not include token embeddings or an LM head; DFlash uses the target model's
|
||||
# embedding/lm_head.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Iterable, Optional, Tuple
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from torch import nn
|
||||
|
||||
from sglang.srt.distributed import get_tensor_model_parallel_world_size
|
||||
from sglang.srt.layers.activation import SiluAndMul
|
||||
from sglang.srt.layers.layernorm import RMSNorm
|
||||
from sglang.srt.layers.linear import (
|
||||
MergedColumnParallelLinear,
|
||||
QKVParallelLinear,
|
||||
RowParallelLinear,
|
||||
)
|
||||
from sglang.srt.layers.logits_processor import LogitsProcessorOutput
|
||||
from sglang.srt.layers.radix_attention import AttentionType, RadixAttention
|
||||
from sglang.srt.layers.rotary_embedding import get_rope
|
||||
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
|
||||
from sglang.srt.model_loader.weight_utils import default_weight_loader
|
||||
from sglang.srt.models.utils import apply_qk_norm
|
||||
from sglang.srt.speculative.dflash_utils import (
|
||||
can_dflash_slice_qkv_weight,
|
||||
parse_dflash_draft_config,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DFlashAttention(nn.Module):
|
||||
def __init__(self, config, layer_id: int) -> None:
|
||||
super().__init__()
|
||||
hidden_size = int(config.hidden_size)
|
||||
tp_size = int(get_tensor_model_parallel_world_size())
|
||||
total_num_heads = int(config.num_attention_heads)
|
||||
total_num_kv_heads = int(
|
||||
getattr(config, "num_key_value_heads", total_num_heads)
|
||||
)
|
||||
head_dim = int(getattr(config, "head_dim", hidden_size // total_num_heads))
|
||||
|
||||
self.hidden_size = hidden_size
|
||||
self.total_num_heads = total_num_heads
|
||||
self.total_num_kv_heads = total_num_kv_heads
|
||||
assert self.total_num_heads % tp_size == 0, (
|
||||
f"DFlashAttention requires total_num_heads divisible by tp_size. "
|
||||
f"total_num_heads={self.total_num_heads}, tp_size={tp_size}."
|
||||
)
|
||||
self.num_heads = self.total_num_heads // tp_size
|
||||
if self.total_num_kv_heads >= tp_size:
|
||||
assert self.total_num_kv_heads % tp_size == 0, (
|
||||
f"DFlashAttention requires total_num_kv_heads divisible by tp_size when >= tp_size. "
|
||||
f"total_num_kv_heads={self.total_num_kv_heads}, tp_size={tp_size}."
|
||||
)
|
||||
else:
|
||||
assert tp_size % self.total_num_kv_heads == 0, (
|
||||
f"DFlashAttention requires tp_size divisible by total_num_kv_heads when total_num_kv_heads < tp_size. "
|
||||
f"total_num_kv_heads={self.total_num_kv_heads}, tp_size={tp_size}."
|
||||
)
|
||||
self.num_kv_heads = max(1, self.total_num_kv_heads // tp_size)
|
||||
self.head_dim = head_dim
|
||||
self.q_size = self.num_heads * head_dim
|
||||
self.kv_size = self.num_kv_heads * head_dim
|
||||
|
||||
attention_bias = bool(getattr(config, "attention_bias", False))
|
||||
rms_norm_eps = float(getattr(config, "rms_norm_eps", 1e-6))
|
||||
|
||||
self.qkv_proj = QKVParallelLinear(
|
||||
hidden_size=hidden_size,
|
||||
head_size=head_dim,
|
||||
total_num_heads=self.total_num_heads,
|
||||
total_num_kv_heads=self.total_num_kv_heads,
|
||||
bias=attention_bias,
|
||||
prefix="qkv_proj",
|
||||
)
|
||||
self.o_proj = RowParallelLinear(
|
||||
self.total_num_heads * head_dim,
|
||||
hidden_size,
|
||||
bias=attention_bias,
|
||||
prefix="o_proj",
|
||||
)
|
||||
|
||||
# Per-head Q/K RMSNorm, matching HF Qwen3.
|
||||
self.q_norm = RMSNorm(head_dim, eps=rms_norm_eps)
|
||||
self.k_norm = RMSNorm(head_dim, eps=rms_norm_eps)
|
||||
|
||||
rope_theta = float(getattr(config, "rope_theta", 1000000))
|
||||
rope_scaling = getattr(config, "rope_scaling", None)
|
||||
rope_is_neox_style = bool(
|
||||
getattr(
|
||||
config, "rope_is_neox_style", getattr(config, "is_neox_style", True)
|
||||
)
|
||||
)
|
||||
max_position_embeddings = int(getattr(config, "max_position_embeddings", 32768))
|
||||
self.rotary_emb = get_rope(
|
||||
head_dim,
|
||||
rotary_dim=head_dim,
|
||||
max_position=max_position_embeddings,
|
||||
base=rope_theta,
|
||||
rope_scaling=rope_scaling,
|
||||
is_neox_style=rope_is_neox_style,
|
||||
)
|
||||
|
||||
self.scaling = head_dim**-0.5
|
||||
# DFlash uses non-causal attention over the draft block.
|
||||
self.attn = RadixAttention(
|
||||
num_heads=self.num_heads,
|
||||
head_dim=head_dim,
|
||||
scaling=self.scaling,
|
||||
num_kv_heads=self.num_kv_heads,
|
||||
layer_id=layer_id,
|
||||
attn_type=AttentionType.ENCODER_ONLY,
|
||||
)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
positions: torch.Tensor,
|
||||
hidden_states: torch.Tensor,
|
||||
forward_batch: ForwardBatch,
|
||||
) -> torch.Tensor:
|
||||
qkv, _ = self.qkv_proj(hidden_states)
|
||||
q, k, v = qkv.split([self.q_size, self.kv_size, self.kv_size], dim=-1)
|
||||
q, k = apply_qk_norm(q, k, self.q_norm, self.k_norm, self.head_dim)
|
||||
q, k = self.rotary_emb(positions, q, k)
|
||||
attn_output = self.attn(q, k, v, forward_batch)
|
||||
output, _ = self.o_proj(attn_output)
|
||||
return output
|
||||
|
||||
def kv_proj_only(
|
||||
self, hidden_states: torch.Tensor
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Project hidden_states to K/V only (skip Q).
|
||||
|
||||
This is used by DFlash to materialize ctx tokens into the draft KV cache:
|
||||
we only need K/V for the cached tokens; Q is never consumed.
|
||||
"""
|
||||
# Fast path for unquantized weights: slice the fused QKV weight and run one GEMM.
|
||||
can_slice_qkv_weight, _ = can_dflash_slice_qkv_weight(self.qkv_proj)
|
||||
if can_slice_qkv_weight:
|
||||
kv_slice = slice(self.q_size, self.q_size + 2 * self.kv_size)
|
||||
weight = self.qkv_proj.weight[kv_slice]
|
||||
bias = (
|
||||
self.qkv_proj.bias[kv_slice] if self.qkv_proj.bias is not None else None
|
||||
)
|
||||
kv = F.linear(hidden_states, weight, bias)
|
||||
k, v = kv.split([self.kv_size, self.kv_size], dim=-1)
|
||||
return k, v
|
||||
|
||||
# Fallback: compute full QKV and discard Q (keeps compatibility with quantized weights).
|
||||
qkv, _ = self.qkv_proj(hidden_states)
|
||||
_, k, v = qkv.split([self.q_size, self.kv_size, self.kv_size], dim=-1)
|
||||
return k, v
|
||||
|
||||
def apply_k_norm(self, k: torch.Tensor) -> torch.Tensor:
|
||||
k_by_head = k.reshape(-1, self.head_dim)
|
||||
k_by_head = self.k_norm(k_by_head)
|
||||
return k_by_head.view_as(k)
|
||||
|
||||
def apply_k_rope(self, positions: torch.Tensor, k: torch.Tensor) -> torch.Tensor:
|
||||
# Use a minimal dummy query (1 head) to avoid doing full-Q work.
|
||||
dummy_q = k.new_empty((k.shape[0], self.head_dim))
|
||||
_, k = self.rotary_emb(positions, dummy_q, k)
|
||||
return k
|
||||
|
||||
|
||||
class DFlashMLP(nn.Module):
|
||||
def __init__(self, config, quant_config=None, prefix: str = "") -> None:
|
||||
super().__init__()
|
||||
hidden_size = int(config.hidden_size)
|
||||
intermediate_size = int(getattr(config, "intermediate_size", 0))
|
||||
if intermediate_size <= 0:
|
||||
raise ValueError(
|
||||
f"Invalid intermediate_size={intermediate_size} for DFlash MLP."
|
||||
)
|
||||
|
||||
self.gate_up_proj = MergedColumnParallelLinear(
|
||||
hidden_size,
|
||||
[intermediate_size] * 2,
|
||||
bias=False,
|
||||
quant_config=quant_config,
|
||||
prefix="gate_up_proj" if not prefix else f"{prefix}.gate_up_proj",
|
||||
)
|
||||
self.down_proj = RowParallelLinear(
|
||||
intermediate_size,
|
||||
hidden_size,
|
||||
bias=False,
|
||||
quant_config=quant_config,
|
||||
prefix="down_proj" if not prefix else f"{prefix}.down_proj",
|
||||
)
|
||||
hidden_act = getattr(config, "hidden_act", "silu")
|
||||
if hidden_act != "silu":
|
||||
raise ValueError(
|
||||
f"Unsupported DFlash activation: {hidden_act}. Only silu is supported for now."
|
||||
)
|
||||
self.act_fn = SiluAndMul()
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
gate_up, _ = self.gate_up_proj(x)
|
||||
x = self.act_fn(gate_up)
|
||||
x, _ = self.down_proj(x)
|
||||
return x
|
||||
|
||||
|
||||
class DFlashDecoderLayer(nn.Module):
|
||||
def __init__(self, config, layer_id: int) -> None:
|
||||
super().__init__()
|
||||
hidden_size = int(config.hidden_size)
|
||||
rms_norm_eps = float(getattr(config, "rms_norm_eps", 1e-6))
|
||||
|
||||
self.input_layernorm = RMSNorm(hidden_size, eps=rms_norm_eps)
|
||||
self.self_attn = DFlashAttention(config=config, layer_id=layer_id)
|
||||
self.post_attention_layernorm = RMSNorm(hidden_size, eps=rms_norm_eps)
|
||||
self.mlp = DFlashMLP(config=config)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
positions: torch.Tensor,
|
||||
hidden_states: torch.Tensor,
|
||||
forward_batch: ForwardBatch,
|
||||
residual: Optional[torch.Tensor],
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
if hidden_states.numel() == 0:
|
||||
# Keep return types consistent for upstream callers.
|
||||
if residual is None:
|
||||
residual = hidden_states
|
||||
return hidden_states, residual
|
||||
|
||||
# Pre-norm attention with fused residual+norm when possible (Qwen3-style).
|
||||
if residual is None:
|
||||
residual = hidden_states
|
||||
hidden_states = self.input_layernorm(hidden_states)
|
||||
else:
|
||||
hidden_states, residual = self.input_layernorm(hidden_states, residual)
|
||||
|
||||
attn_out = self.self_attn(
|
||||
positions=positions,
|
||||
hidden_states=hidden_states,
|
||||
forward_batch=forward_batch,
|
||||
)
|
||||
hidden_states, residual = self.post_attention_layernorm(attn_out, residual)
|
||||
hidden_states = self.mlp(hidden_states)
|
||||
return hidden_states, residual
|
||||
|
||||
|
||||
class DFlashDraftModel(nn.Module):
|
||||
"""SGLang DFlash draft model (no embedding / lm_head weights).
|
||||
|
||||
The checkpoint provides:
|
||||
- transformer weights for `layers.*`
|
||||
- `fc.weight`, `hidden_norm.weight` for projecting target context features
|
||||
- `norm.weight` for final normalization
|
||||
"""
|
||||
|
||||
def __init__(self, config, quant_config=None, prefix: str = "") -> None:
|
||||
super().__init__()
|
||||
self.config = config
|
||||
|
||||
hidden_size = int(config.hidden_size)
|
||||
num_layers = int(config.num_hidden_layers)
|
||||
rms_norm_eps = float(getattr(config, "rms_norm_eps", 1e-6))
|
||||
|
||||
self.layers = nn.ModuleList(
|
||||
[DFlashDecoderLayer(config=config, layer_id=i) for i in range(num_layers)]
|
||||
)
|
||||
self.norm = RMSNorm(hidden_size, eps=rms_norm_eps)
|
||||
|
||||
# Project per-token target context features:
|
||||
# concat(K * hidden_size) -> hidden_size, where K is the number of target-layer
|
||||
# feature tensors concatenated per token (not necessarily equal to num_layers).
|
||||
draft_config = parse_dflash_draft_config(draft_hf_config=config)
|
||||
target_num_layers = (
|
||||
int(draft_config.num_target_layers)
|
||||
if draft_config.num_target_layers is not None
|
||||
else num_layers
|
||||
)
|
||||
target_layer_ids = draft_config.resolve_target_layer_ids(
|
||||
target_num_layers=target_num_layers, draft_num_layers=num_layers
|
||||
)
|
||||
num_context_features = len(target_layer_ids)
|
||||
|
||||
self.num_context_features = int(num_context_features)
|
||||
self.fc = nn.Linear(
|
||||
self.num_context_features * hidden_size, hidden_size, bias=False
|
||||
)
|
||||
self.hidden_norm = RMSNorm(hidden_size, eps=rms_norm_eps)
|
||||
|
||||
self.block_size = draft_config.resolve_block_size(default=16)
|
||||
|
||||
def project_target_hidden(self, target_hidden: torch.Tensor) -> torch.Tensor:
|
||||
"""Project concatenated target-layer hidden states into draft hidden_size."""
|
||||
expected = int(self.fc.in_features)
|
||||
if target_hidden.ndim != 2 or int(target_hidden.shape[-1]) != expected:
|
||||
raise ValueError(
|
||||
"DFLASH target_hidden feature dim mismatch. "
|
||||
f"Expected shape [N, {expected}] "
|
||||
f"(num_context_features={self.num_context_features}, hidden_size={int(self.config.hidden_size)}), "
|
||||
f"but got shape={tuple(target_hidden.shape)}. "
|
||||
"This usually means the target model is capturing a different number of layer features than "
|
||||
"the draft checkpoint/config expects."
|
||||
)
|
||||
return self.hidden_norm(self.fc(target_hidden))
|
||||
|
||||
@torch.no_grad()
|
||||
def forward(
|
||||
self,
|
||||
input_ids: torch.Tensor,
|
||||
positions: torch.Tensor,
|
||||
forward_batch: ForwardBatch,
|
||||
input_embeds: Optional[torch.Tensor] = None,
|
||||
get_embedding: bool = False,
|
||||
pp_proxy_tensors=None,
|
||||
) -> LogitsProcessorOutput:
|
||||
if input_embeds is None:
|
||||
raise ValueError(
|
||||
"DFlashDraftModel requires `input_embeds` (use the target embedding)."
|
||||
)
|
||||
hidden_states = input_embeds
|
||||
residual: Optional[torch.Tensor] = None
|
||||
|
||||
for layer in self.layers:
|
||||
hidden_states, residual = layer(
|
||||
positions, hidden_states, forward_batch, residual
|
||||
)
|
||||
|
||||
if hidden_states.numel() != 0:
|
||||
if residual is None:
|
||||
hidden_states = self.norm(hidden_states)
|
||||
else:
|
||||
hidden_states, _ = self.norm(hidden_states, residual)
|
||||
|
||||
return LogitsProcessorOutput(
|
||||
next_token_logits=None,
|
||||
hidden_states=hidden_states,
|
||||
)
|
||||
|
||||
def load_weights(self, weights: Iterable[Tuple[str, torch.Tensor]]):
|
||||
stacked_params_mapping = [
|
||||
# (param_name, weight_name, shard_id)
|
||||
("qkv_proj", "q_proj", "q"),
|
||||
("qkv_proj", "k_proj", "k"),
|
||||
("qkv_proj", "v_proj", "v"),
|
||||
("gate_up_proj", "gate_proj", 0),
|
||||
("gate_up_proj", "up_proj", 1),
|
||||
]
|
||||
|
||||
params_dict = dict(self.named_parameters())
|
||||
|
||||
def resolve_param_name(name: str) -> Optional[str]:
|
||||
if name in params_dict:
|
||||
return name
|
||||
if name.startswith("model."):
|
||||
stripped_name = name[len("model.") :]
|
||||
if stripped_name in params_dict:
|
||||
return stripped_name
|
||||
else:
|
||||
prefixed_name = f"model.{name}"
|
||||
if prefixed_name in params_dict:
|
||||
return prefixed_name
|
||||
return None
|
||||
|
||||
for name, loaded_weight in weights:
|
||||
for param_name, weight_name, shard_id in stacked_params_mapping:
|
||||
if f".{weight_name}." not in name:
|
||||
continue
|
||||
mapped_name = name.replace(weight_name, param_name)
|
||||
resolved_name = resolve_param_name(mapped_name)
|
||||
if resolved_name is None:
|
||||
continue
|
||||
param = params_dict[resolved_name]
|
||||
weight_loader = getattr(param, "weight_loader", default_weight_loader)
|
||||
weight_loader(param, loaded_weight, shard_id)
|
||||
break
|
||||
else:
|
||||
resolved_name = resolve_param_name(name)
|
||||
if resolved_name is None:
|
||||
# Ignore unexpected weights (e.g., HF rotary caches).
|
||||
continue
|
||||
param = params_dict[resolved_name]
|
||||
if resolved_name.endswith("fc.weight") and tuple(
|
||||
loaded_weight.shape
|
||||
) != tuple(param.shape):
|
||||
raise ValueError(
|
||||
"DFLASH fc.weight shape mismatch. This usually means the draft checkpoint's "
|
||||
"number of context features (K) does not match this config. "
|
||||
f"Expected fc.weight.shape={tuple(param.shape)} "
|
||||
f"(num_context_features={self.num_context_features}, hidden_size={int(self.config.hidden_size)}), "
|
||||
f"but got {tuple(loaded_weight.shape)} for weight '{name}'."
|
||||
)
|
||||
weight_loader = getattr(param, "weight_loader", default_weight_loader)
|
||||
weight_loader(param, loaded_weight)
|
||||
|
||||
|
||||
EntryClass = DFlashDraftModel
|
||||
@@ -794,6 +794,18 @@ class LlamaForCausalLM(nn.Module):
|
||||
# of the (i-1)th layer as aux hidden state
|
||||
self.model.layers_to_capture = [val + 1 for val in layer_ids]
|
||||
|
||||
def set_dflash_layers_to_capture(self, layer_ids: List[int]):
|
||||
if not self.pp_group.is_last_rank:
|
||||
return
|
||||
|
||||
if layer_ids is None:
|
||||
raise ValueError(
|
||||
"DFLASH requires explicit layer_ids for aux hidden capture."
|
||||
)
|
||||
|
||||
self.capture_aux_hidden_states = True
|
||||
self.model.layers_to_capture = [val + 1 for val in layer_ids]
|
||||
|
||||
|
||||
class Phi3ForCausalLM(LlamaForCausalLM):
|
||||
pass
|
||||
|
||||
@@ -499,6 +499,8 @@ class ServerArgs:
|
||||
speculative_num_steps: Optional[int] = None
|
||||
speculative_eagle_topk: Optional[int] = None
|
||||
speculative_num_draft_tokens: Optional[int] = None
|
||||
speculative_dflash_block_size: Optional[int] = None
|
||||
speculative_dflash_draft_window_size: Optional[int] = None
|
||||
speculative_accept_threshold_single: float = 1.0
|
||||
speculative_accept_threshold_acc: float = 1.0
|
||||
speculative_token_map: Optional[str] = None
|
||||
@@ -3027,6 +3029,134 @@ class ServerArgs:
|
||||
if self.speculative_algorithm == "NEXTN":
|
||||
self.speculative_algorithm = "EAGLE"
|
||||
|
||||
if self.speculative_algorithm == "DFLASH":
|
||||
if self.enable_dp_attention:
|
||||
raise ValueError(
|
||||
"Currently DFLASH speculative decoding does not support dp attention."
|
||||
)
|
||||
|
||||
if self.pp_size != 1:
|
||||
raise ValueError(
|
||||
"Currently DFLASH speculative decoding only supports pp_size == 1."
|
||||
)
|
||||
|
||||
if self.speculative_draft_model_path is None:
|
||||
raise ValueError(
|
||||
"DFLASH speculative decoding requires setting --speculative-draft-model-path."
|
||||
)
|
||||
|
||||
# DFLASH does not use EAGLE-style `num_steps`/`topk`, but those fields still
|
||||
# affect generic scheduler/KV-cache accounting (buffer sizing, KV freeing,
|
||||
# RoPE reservation). Force them to 1 to avoid surprising memory behavior.
|
||||
#
|
||||
# For DFlash, the natural unit is `block_size` (verify window length).
|
||||
if self.speculative_num_steps is None:
|
||||
self.speculative_num_steps = 1
|
||||
elif int(self.speculative_num_steps) != 1:
|
||||
logger.warning(
|
||||
"DFLASH only supports speculative_num_steps == 1; overriding speculative_num_steps=%s to 1.",
|
||||
self.speculative_num_steps,
|
||||
)
|
||||
self.speculative_num_steps = 1
|
||||
|
||||
if self.speculative_eagle_topk is None:
|
||||
self.speculative_eagle_topk = 1
|
||||
elif int(self.speculative_eagle_topk) != 1:
|
||||
logger.warning(
|
||||
"DFLASH only supports speculative_eagle_topk == 1; overriding speculative_eagle_topk=%s to 1.",
|
||||
self.speculative_eagle_topk,
|
||||
)
|
||||
self.speculative_eagle_topk = 1
|
||||
|
||||
if self.speculative_dflash_block_size is not None:
|
||||
if int(self.speculative_dflash_block_size) <= 0:
|
||||
raise ValueError(
|
||||
"DFLASH requires --speculative-dflash-block-size to be positive, "
|
||||
f"got {self.speculative_dflash_block_size}."
|
||||
)
|
||||
if self.speculative_num_draft_tokens is not None and int(
|
||||
self.speculative_num_draft_tokens
|
||||
) != int(self.speculative_dflash_block_size):
|
||||
raise ValueError(
|
||||
"Both --speculative-num-draft-tokens and --speculative-dflash-block-size are set "
|
||||
"but they differ. For DFLASH they must match. "
|
||||
f"speculative_num_draft_tokens={self.speculative_num_draft_tokens}, "
|
||||
f"speculative_dflash_block_size={self.speculative_dflash_block_size}."
|
||||
)
|
||||
self.speculative_num_draft_tokens = int(
|
||||
self.speculative_dflash_block_size
|
||||
)
|
||||
|
||||
window_size = None
|
||||
if self.speculative_dflash_draft_window_size is not None:
|
||||
window_size = int(self.speculative_dflash_draft_window_size)
|
||||
if window_size <= 0:
|
||||
raise ValueError(
|
||||
"DFLASH requires --speculative-dflash-draft-window-size "
|
||||
f"to be positive, got {window_size}."
|
||||
)
|
||||
self.speculative_dflash_draft_window_size = window_size
|
||||
|
||||
if self.speculative_num_draft_tokens is None:
|
||||
from sglang.srt.speculative.dflash_utils import (
|
||||
parse_dflash_draft_config,
|
||||
)
|
||||
|
||||
model_override_args = json.loads(self.json_model_override_args)
|
||||
inferred_block_size = None
|
||||
try:
|
||||
from sglang.srt.utils.hf_transformers_utils import get_config
|
||||
|
||||
draft_hf_config = get_config(
|
||||
self.speculative_draft_model_path,
|
||||
trust_remote_code=self.trust_remote_code,
|
||||
revision=self.speculative_draft_model_revision,
|
||||
model_override_args=model_override_args,
|
||||
)
|
||||
inferred_block_size = parse_dflash_draft_config(
|
||||
draft_hf_config=draft_hf_config
|
||||
).resolve_block_size(default=None)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"Failed to infer DFLASH block_size from draft model config; "
|
||||
"defaulting speculative_num_draft_tokens to 16. Error: %s",
|
||||
e,
|
||||
)
|
||||
|
||||
if inferred_block_size is None:
|
||||
inferred_block_size = 16
|
||||
logger.warning(
|
||||
"speculative_num_draft_tokens is not set; defaulting to %d for DFLASH.",
|
||||
inferred_block_size,
|
||||
)
|
||||
self.speculative_num_draft_tokens = inferred_block_size
|
||||
|
||||
if window_size is not None:
|
||||
draft_tokens = int(self.speculative_num_draft_tokens)
|
||||
if window_size < draft_tokens:
|
||||
raise ValueError(
|
||||
"DFLASH --speculative-dflash-draft-window-size must be >= "
|
||||
"--speculative-num-draft-tokens (block_size). "
|
||||
f"window_size={window_size}, block_size={draft_tokens}."
|
||||
)
|
||||
|
||||
if self.max_running_requests is None:
|
||||
self.max_running_requests = 48
|
||||
logger.warning(
|
||||
"Max running requests is reset to 48 for speculative decoding. You can override this by explicitly setting --max-running-requests."
|
||||
)
|
||||
|
||||
self.disable_overlap_schedule = True
|
||||
logger.warning(
|
||||
"Overlap scheduler is disabled when using DFLASH speculative decoding (spec v2 is not supported yet)."
|
||||
)
|
||||
|
||||
if self.enable_mixed_chunk:
|
||||
self.enable_mixed_chunk = False
|
||||
logger.warning(
|
||||
"Mixed chunked prefill is disabled because of using dflash speculative decoding."
|
||||
)
|
||||
|
||||
if self.speculative_algorithm in ("EAGLE", "EAGLE3", "STANDALONE"):
|
||||
if self.speculative_algorithm == "STANDALONE" and self.enable_dp_attention:
|
||||
# TODO: support dp attention for standalone speculative decoding
|
||||
@@ -4832,7 +4962,7 @@ class ServerArgs:
|
||||
parser.add_argument(
|
||||
"--speculative-algorithm",
|
||||
type=str,
|
||||
choices=["EAGLE", "EAGLE3", "NEXTN", "STANDALONE", "NGRAM"],
|
||||
choices=["DFLASH", "EAGLE", "EAGLE3", "NEXTN", "STANDALONE", "NGRAM"],
|
||||
help="Speculative algorithm.",
|
||||
)
|
||||
parser.add_argument(
|
||||
@@ -4876,6 +5006,21 @@ class ServerArgs:
|
||||
help="The number of tokens sampled from the draft model in Speculative Decoding.",
|
||||
default=ServerArgs.speculative_num_draft_tokens,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--speculative-dflash-block-size",
|
||||
type=int,
|
||||
help="DFLASH only. Block size (verify window length). Alias of --speculative-num-draft-tokens for DFLASH.",
|
||||
default=ServerArgs.speculative_dflash_block_size,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--speculative-dflash-draft-window-size",
|
||||
type=int,
|
||||
help="DFLASH only. Sliding window size for the draft-model KV cache. "
|
||||
"When set, the draft worker keeps a recent target-token window in its "
|
||||
"local cache (paged backends may retain up to one extra page on the left "
|
||||
"for alignment). Default is full context.",
|
||||
default=ServerArgs.speculative_dflash_draft_window_size,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--speculative-accept-threshold-single",
|
||||
type=float,
|
||||
|
||||
@@ -0,0 +1,501 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Tuple
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.srt.layers.attention.utils import create_flashinfer_kv_indices_triton
|
||||
from sglang.srt.layers.logits_processor import LogitsProcessorOutput
|
||||
from sglang.srt.layers.sampler import apply_custom_logit_processor
|
||||
from sglang.srt.managers.schedule_batch import ScheduleBatch
|
||||
from sglang.srt.mem_cache.common import (
|
||||
alloc_paged_token_slots_extend,
|
||||
alloc_token_slots,
|
||||
get_last_loc,
|
||||
)
|
||||
from sglang.srt.model_executor.forward_batch_info import CaptureHiddenMode
|
||||
from sglang.srt.speculative.dflash_utils import (
|
||||
compute_dflash_accept_len_and_bonus,
|
||||
compute_dflash_sampling_accept_len_and_bonus,
|
||||
is_dflash_sampling_verify_available,
|
||||
)
|
||||
from sglang.srt.speculative.spec_info import SpecInput, SpecInputType
|
||||
from sglang.srt.speculative.spec_utils import assign_req_to_token_pool_func
|
||||
|
||||
|
||||
def _compute_paged_keep_slots(
|
||||
*,
|
||||
prefix_lens: torch.Tensor,
|
||||
commit_lens: torch.Tensor,
|
||||
draft_token_num: int,
|
||||
page_size: int,
|
||||
) -> torch.Tensor:
|
||||
"""Compute how many draft slots per request must remain allocated.
|
||||
|
||||
The allocator frees at page granularity for paged mode, so we can only release
|
||||
full pages from the tail after verify.
|
||||
"""
|
||||
|
||||
if page_size <= 1:
|
||||
raise ValueError(f"Expected page_size > 1, got {page_size}.")
|
||||
|
||||
seq_dtype = prefix_lens.dtype
|
||||
extended_lens = prefix_lens + int(draft_token_num)
|
||||
new_lens = prefix_lens + commit_lens.to(seq_dtype)
|
||||
aligned_new_lens = ((new_lens + page_size - 1) // page_size) * page_size
|
||||
keep_lens = torch.minimum(aligned_new_lens, extended_lens)
|
||||
keep_slots = (keep_lens - prefix_lens).to(torch.int64)
|
||||
keep_slots.clamp_(min=0, max=int(draft_token_num))
|
||||
return keep_slots
|
||||
|
||||
|
||||
@dataclass
|
||||
class DFlashDraftInput(SpecInput):
|
||||
"""Per-batch DFlash draft state for spec-v1 (non-overlap) scheduling.
|
||||
|
||||
This object is stored on `ScheduleBatch.spec_info` between decode iterations.
|
||||
It is NOT sent to model attention backends; the DFlash worker uses it to run
|
||||
the draft model and to track draft-side cache progress.
|
||||
|
||||
When draft windowing is disabled, `draft_seq_lens` matches the committed target
|
||||
prefix length already materialized in the draft KV cache. When windowing is
|
||||
enabled, `draft_seq_lens` is the logical resident length in the draft worker's
|
||||
compact req-to-token mapping. In paged mode this may exceed the requested
|
||||
window by up to `page_size - 1` so the local page table remains valid. `ctx_lens`
|
||||
tracks newly committed target tokens that still need draft KV materialization.
|
||||
"""
|
||||
|
||||
# Current token to start the next DFlash block (one per request).
|
||||
verified_id: torch.Tensor
|
||||
|
||||
# Flattened context features for tokens that need to be appended into the draft cache.
|
||||
# Shape: [sum(ctx_lens), K * hidden_size], where K is the number of target-layer
|
||||
# hidden-state features concatenated per token (len(dflash_config.target_layer_ids),
|
||||
# or default K == draft_num_layers for existing checkpoints).
|
||||
target_hidden: torch.Tensor
|
||||
|
||||
# Context lengths per request, used to slice `target_hidden`. Device tensor (int32).
|
||||
ctx_lens: torch.Tensor
|
||||
|
||||
# How many committed tokens are visible to the draft worker per request.
|
||||
draft_seq_lens: torch.Tensor
|
||||
|
||||
def __post_init__(self):
|
||||
super().__init__(spec_input_type=SpecInputType.DFLASH_DRAFT)
|
||||
|
||||
def get_spec_adjust_token_coefficient(self) -> Tuple[int, int]:
|
||||
# Draft state does not change token accounting.
|
||||
return (1, 1)
|
||||
|
||||
def filter_batch(self, new_indices: torch.Tensor, has_been_filtered: bool = True):
|
||||
old_ctx_lens = self.ctx_lens
|
||||
old_target_hidden = self.target_hidden
|
||||
|
||||
self.verified_id = self.verified_id[new_indices]
|
||||
self.ctx_lens = old_ctx_lens[new_indices]
|
||||
self.draft_seq_lens = self.draft_seq_lens[new_indices]
|
||||
|
||||
if old_target_hidden is None or old_target_hidden.numel() == 0:
|
||||
self.target_hidden = old_target_hidden
|
||||
return
|
||||
|
||||
# Rebuild target_hidden for the filtered batch using vectorized indexing.
|
||||
old_bs = int(old_ctx_lens.shape[0])
|
||||
offsets = torch.zeros(
|
||||
(old_bs + 1,), dtype=torch.int64, device=old_ctx_lens.device
|
||||
)
|
||||
offsets[1:].copy_(old_ctx_lens.to(torch.int64).cumsum(0))
|
||||
|
||||
start = offsets[:-1]
|
||||
seg_start = start[new_indices]
|
||||
seg_lens = old_ctx_lens[new_indices].to(torch.int64)
|
||||
|
||||
max_len = int(seg_lens.max().item()) if seg_lens.numel() > 0 else 0
|
||||
if max_len <= 0:
|
||||
self.target_hidden = old_target_hidden[:0]
|
||||
return
|
||||
|
||||
r = torch.arange(max_len, device=old_ctx_lens.device, dtype=torch.int64)[
|
||||
None, :
|
||||
]
|
||||
pos2d = seg_start[:, None] + r
|
||||
mask = r < seg_lens[:, None]
|
||||
flat_pos = pos2d[mask]
|
||||
self.target_hidden = (
|
||||
old_target_hidden.index_select(0, flat_pos)
|
||||
if flat_pos.numel() > 0
|
||||
else old_target_hidden[:0]
|
||||
)
|
||||
|
||||
def merge_batch(self, spec_info: "DFlashDraftInput"):
|
||||
self.verified_id = torch.cat([self.verified_id, spec_info.verified_id], dim=0)
|
||||
self.ctx_lens = torch.cat([self.ctx_lens, spec_info.ctx_lens], dim=0)
|
||||
self.draft_seq_lens = torch.cat(
|
||||
[self.draft_seq_lens, spec_info.draft_seq_lens], dim=0
|
||||
)
|
||||
if self.target_hidden is None or self.target_hidden.numel() == 0:
|
||||
self.target_hidden = spec_info.target_hidden
|
||||
elif (
|
||||
spec_info.target_hidden is not None and spec_info.target_hidden.numel() > 0
|
||||
):
|
||||
self.target_hidden = torch.cat(
|
||||
[self.target_hidden, spec_info.target_hidden], dim=0
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DFlashVerifyInput(SpecInput):
|
||||
"""Inputs for a target-model verify forward in DFlash (spec-v1).
|
||||
|
||||
The verify forward is run with `ForwardMode.TARGET_VERIFY` so that the target
|
||||
model returns logits for all tokens in the block, enabling accept-length
|
||||
computation.
|
||||
"""
|
||||
|
||||
draft_token: torch.Tensor
|
||||
positions: torch.Tensor
|
||||
draft_token_num: int
|
||||
# Kept for compatibility with attention backends that gate tree metadata by `topk > 1`.
|
||||
# DFLASH verify is linear (non-tree), so this is always 1.
|
||||
topk: int = 1
|
||||
# Custom attention "allow mask" for TARGET_VERIFY in backends that require it (e.g. triton).
|
||||
# Semantics follow SGLang speculative conventions: True means the (q, k) pair is allowed.
|
||||
custom_mask: torch.Tensor | None = None
|
||||
capture_hidden_mode: CaptureHiddenMode = CaptureHiddenMode.FULL
|
||||
|
||||
# Shape info for padding (e.g., DP attention / CUDA graph).
|
||||
num_tokens_per_batch: int = -1
|
||||
|
||||
def __post_init__(self):
|
||||
super().__init__(spec_input_type=SpecInputType.DFLASH_VERIFY)
|
||||
if self.num_tokens_per_batch == -1:
|
||||
self.num_tokens_per_batch = int(self.draft_token_num)
|
||||
|
||||
def get_spec_adjust_token_coefficient(self) -> Tuple[int, int]:
|
||||
return self.draft_token_num, self.draft_token_num
|
||||
|
||||
def prepare_for_verify(
|
||||
self,
|
||||
batch: ScheduleBatch,
|
||||
page_size: int,
|
||||
*,
|
||||
build_custom_mask: bool = True,
|
||||
):
|
||||
if batch.forward_mode.is_idle():
|
||||
return
|
||||
|
||||
batch.input_ids = self.draft_token
|
||||
|
||||
if page_size == 1:
|
||||
batch.out_cache_loc = alloc_token_slots(
|
||||
batch.tree_cache, len(batch.input_ids)
|
||||
)
|
||||
end_offset = batch.seq_lens + self.draft_token_num
|
||||
else:
|
||||
prefix_lens = batch.seq_lens
|
||||
prefix_lens_cpu = batch.seq_lens_cpu
|
||||
end_offset = prefix_lens + self.draft_token_num
|
||||
end_offset_cpu = prefix_lens_cpu + self.draft_token_num
|
||||
last_loc = get_last_loc(
|
||||
batch.req_to_token_pool.req_to_token,
|
||||
batch.req_pool_indices,
|
||||
prefix_lens,
|
||||
)
|
||||
batch.out_cache_loc = alloc_paged_token_slots_extend(
|
||||
batch.tree_cache,
|
||||
prefix_lens,
|
||||
prefix_lens_cpu,
|
||||
end_offset,
|
||||
end_offset_cpu,
|
||||
last_loc,
|
||||
len(batch.input_ids),
|
||||
)
|
||||
self.last_loc = last_loc
|
||||
|
||||
bs = batch.batch_size()
|
||||
assign_req_to_token_pool_func(
|
||||
batch.req_pool_indices,
|
||||
batch.req_to_token_pool.req_to_token,
|
||||
batch.seq_lens,
|
||||
end_offset,
|
||||
batch.out_cache_loc,
|
||||
bs,
|
||||
)
|
||||
|
||||
if not build_custom_mask:
|
||||
self.custom_mask = None
|
||||
return
|
||||
|
||||
if self.draft_token_num <= 0:
|
||||
raise ValueError(
|
||||
f"DFLASH draft_token_num must be positive, got {self.draft_token_num}."
|
||||
)
|
||||
mask_chunks: List[torch.Tensor] = []
|
||||
q_len = int(self.draft_token_num)
|
||||
q_idx = torch.arange(q_len, device=batch.device, dtype=torch.int32).unsqueeze(1)
|
||||
for prefix_len in batch.seq_lens_cpu.tolist():
|
||||
prefix_len_i = int(prefix_len)
|
||||
kv_len = prefix_len_i + q_len
|
||||
k_idx = torch.arange(
|
||||
kv_len, device=batch.device, dtype=torch.int32
|
||||
).unsqueeze(0)
|
||||
# Allow attending to the full prefix and to tokens up to (and including) the
|
||||
# current query position within the verify block (standard causal masking).
|
||||
allow = k_idx <= (prefix_len_i + q_idx)
|
||||
mask_chunks.append(allow.flatten())
|
||||
self.custom_mask = (
|
||||
torch.cat(mask_chunks, dim=0)
|
||||
if mask_chunks
|
||||
else torch.empty((0,), dtype=torch.bool, device=batch.device)
|
||||
)
|
||||
|
||||
def generate_attn_arg_prefill(
|
||||
self,
|
||||
req_pool_indices: torch.Tensor,
|
||||
paged_kernel_lens: torch.Tensor,
|
||||
paged_kernel_lens_sum: int,
|
||||
req_to_token: torch.Tensor,
|
||||
):
|
||||
device = req_pool_indices.device
|
||||
bs = len(req_pool_indices)
|
||||
|
||||
qo_indptr = torch.arange(
|
||||
0,
|
||||
(bs + 1) * self.draft_token_num,
|
||||
step=self.draft_token_num,
|
||||
dtype=torch.int32,
|
||||
device=device,
|
||||
)
|
||||
|
||||
cum_kv_seq_len = torch.zeros((bs + 1,), dtype=torch.int32, device=device)
|
||||
paged_kernel_lens = paged_kernel_lens + self.draft_token_num
|
||||
cum_kv_seq_len[1:] = torch.cumsum(paged_kernel_lens, dim=0)
|
||||
|
||||
kv_indices = torch.empty(
|
||||
paged_kernel_lens_sum + self.draft_token_num * bs,
|
||||
dtype=torch.int32,
|
||||
device=device,
|
||||
)
|
||||
create_flashinfer_kv_indices_triton[(bs,)](
|
||||
req_to_token,
|
||||
req_pool_indices,
|
||||
paged_kernel_lens,
|
||||
cum_kv_seq_len,
|
||||
None,
|
||||
kv_indices,
|
||||
req_to_token.size(1),
|
||||
)
|
||||
mask = self.custom_mask
|
||||
if mask is not None:
|
||||
mask_numel = (
|
||||
paged_kernel_lens_sum * self.draft_token_num
|
||||
+ (self.draft_token_num**2) * bs
|
||||
)
|
||||
if mask.numel() < mask_numel:
|
||||
# FIXME(attn): temporary fix for custom mask padding with cuda graph
|
||||
mask = torch.cat(
|
||||
[
|
||||
mask,
|
||||
torch.full(
|
||||
(mask_numel - mask.numel(),),
|
||||
True,
|
||||
dtype=torch.bool,
|
||||
device=device,
|
||||
),
|
||||
],
|
||||
dim=0,
|
||||
)
|
||||
self.custom_mask = mask
|
||||
return kv_indices, cum_kv_seq_len, qo_indptr, mask
|
||||
|
||||
def verify(
|
||||
self,
|
||||
*,
|
||||
batch: ScheduleBatch,
|
||||
logits_output: LogitsProcessorOutput,
|
||||
page_size: int,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, List[int]]:
|
||||
"""DFlash verification for greedy and non-greedy sampling.
|
||||
|
||||
Returns:
|
||||
new_verified_id: int64 tensor [bs] (the new current token per request)
|
||||
commit_lens: int32 tensor [bs] (how many verify-input tokens are committed)
|
||||
next_target_hidden: tensor [sum(commit_lens), feature_dim]
|
||||
accept_length_per_req_cpu: list[int] (accepted draft tokens per request)
|
||||
"""
|
||||
if batch.forward_mode.is_idle():
|
||||
empty = torch.empty((0,), dtype=torch.int64, device=batch.device)
|
||||
return empty, empty.to(torch.int32), empty, []
|
||||
|
||||
bs = batch.batch_size()
|
||||
device = logits_output.next_token_logits.device
|
||||
|
||||
sampling_info = batch.sampling_info
|
||||
if sampling_info is not None:
|
||||
if len(sampling_info) != bs:
|
||||
raise RuntimeError(
|
||||
"DFLASH verify sampling_info size mismatch: "
|
||||
f"len(sampling_info)={len(sampling_info)}, bs={bs}."
|
||||
)
|
||||
|
||||
# Keep speculative verify semantics consistent with normal sampling path.
|
||||
if sampling_info.has_custom_logit_processor:
|
||||
apply_custom_logit_processor(
|
||||
logits_output.next_token_logits,
|
||||
sampling_info,
|
||||
num_tokens_in_batch=self.draft_token_num,
|
||||
)
|
||||
|
||||
if (
|
||||
sampling_info.penalizer_orchestrator.is_required
|
||||
or sampling_info.logit_bias is not None
|
||||
):
|
||||
linear_penalty = torch.zeros(
|
||||
(bs, logits_output.next_token_logits.shape[1]),
|
||||
dtype=torch.float32,
|
||||
device=device,
|
||||
)
|
||||
sampling_info.apply_logits_bias(linear_penalty)
|
||||
logits_output.next_token_logits.add_(
|
||||
torch.repeat_interleave(linear_penalty, self.draft_token_num, dim=0)
|
||||
)
|
||||
|
||||
candidates = self.draft_token.view(bs, self.draft_token_num)
|
||||
if (
|
||||
sampling_info is not None
|
||||
and not sampling_info.is_all_greedy
|
||||
and is_dflash_sampling_verify_available()
|
||||
):
|
||||
accept_len, bonus = compute_dflash_sampling_accept_len_and_bonus(
|
||||
candidates=candidates,
|
||||
next_token_logits=logits_output.next_token_logits,
|
||||
sampling_info=sampling_info,
|
||||
)
|
||||
else:
|
||||
target_predict = torch.argmax(logits_output.next_token_logits, dim=-1).view(
|
||||
bs, self.draft_token_num
|
||||
)
|
||||
accept_len, bonus = compute_dflash_accept_len_and_bonus(
|
||||
candidates=candidates,
|
||||
target_predict=target_predict,
|
||||
)
|
||||
|
||||
# Single D2H transfer: candidates[1:] + accept_len + bonus
|
||||
packed = torch.cat(
|
||||
[candidates[:, 1:], accept_len.unsqueeze(1), bonus.unsqueeze(1)], dim=1
|
||||
).cpu()
|
||||
|
||||
max_acc = self.draft_token_num - 1
|
||||
accept_length_per_req_cpu: List[int] = []
|
||||
commit_lens_cpu: List[int] = []
|
||||
new_verified_list: List[int] = []
|
||||
|
||||
for i, req in enumerate(batch.reqs):
|
||||
acc_len = int(packed[i, max_acc].item())
|
||||
proposed = packed[i, :acc_len].tolist() + [
|
||||
int(packed[i, max_acc + 1].item())
|
||||
]
|
||||
|
||||
appended = 0
|
||||
for token_id in proposed:
|
||||
token_id = int(token_id)
|
||||
req.output_ids.append(token_id)
|
||||
appended += 1
|
||||
req.check_finished()
|
||||
if req.finished():
|
||||
break
|
||||
if req.grammar is not None:
|
||||
req.grammar.accept_token(token_id)
|
||||
|
||||
if req.output_ids:
|
||||
new_verified_token = int(req.output_ids[-1])
|
||||
elif req.origin_input_ids:
|
||||
# If no token was appended in this verify step, keep the current token unchanged.
|
||||
new_verified_token = int(req.origin_input_ids[-1])
|
||||
else:
|
||||
raise RuntimeError(
|
||||
"DFLASH verify cannot determine current token: both output_ids and origin_input_ids are empty."
|
||||
)
|
||||
|
||||
commit_lens_cpu.append(appended)
|
||||
new_verified_list.append(new_verified_token)
|
||||
accept_length_per_req_cpu.append(max(0, appended - 1))
|
||||
req.spec_verify_ct += 1
|
||||
req.spec_accepted_tokens += accept_length_per_req_cpu[-1]
|
||||
|
||||
commit_lens = torch.tensor(commit_lens_cpu, dtype=torch.int32, device=device)
|
||||
new_verified_id = torch.tensor(
|
||||
new_verified_list, dtype=torch.int64, device=device
|
||||
)
|
||||
|
||||
# Free uncommitted KV cache slots and compact out_cache_loc.
|
||||
if page_size == 1:
|
||||
out_cache_loc = batch.out_cache_loc.view(bs, self.draft_token_num)
|
||||
keep_mask = (
|
||||
torch.arange(self.draft_token_num, device=device)[None, :]
|
||||
< commit_lens[:, None]
|
||||
)
|
||||
batch.token_to_kv_pool_allocator.free(out_cache_loc[~keep_mask])
|
||||
batch.out_cache_loc = out_cache_loc[keep_mask]
|
||||
else:
|
||||
out_cache_loc = batch.out_cache_loc.view(bs, self.draft_token_num)
|
||||
row_offsets = torch.arange(self.draft_token_num, device=device)[None, :]
|
||||
keep_slots = _compute_paged_keep_slots(
|
||||
prefix_lens=batch.seq_lens,
|
||||
commit_lens=commit_lens,
|
||||
draft_token_num=self.draft_token_num,
|
||||
page_size=page_size,
|
||||
)
|
||||
free_mask = row_offsets >= keep_slots[:, None]
|
||||
batch.token_to_kv_pool_allocator.free(out_cache_loc[free_mask])
|
||||
|
||||
keep_mask = row_offsets < commit_lens[:, None]
|
||||
batch.out_cache_loc = out_cache_loc[keep_mask]
|
||||
|
||||
# Update req-level KV cache accounting.
|
||||
for req, commit_len in zip(batch.reqs, commit_lens_cpu, strict=True):
|
||||
req.kv_committed_len += commit_len
|
||||
req.kv_allocated_len = req.kv_committed_len
|
||||
|
||||
# Update req_to_token pool mapping for newly committed tokens.
|
||||
end_offset = batch.seq_lens + commit_lens.to(batch.seq_lens.dtype)
|
||||
assign_req_to_token_pool_func(
|
||||
batch.req_pool_indices,
|
||||
batch.req_to_token_pool.req_to_token,
|
||||
batch.seq_lens,
|
||||
end_offset,
|
||||
batch.out_cache_loc,
|
||||
bs,
|
||||
)
|
||||
|
||||
# Update batch seq lens.
|
||||
batch.seq_lens.add_(commit_lens.to(batch.seq_lens.dtype))
|
||||
batch.seq_lens_cpu.add_(
|
||||
torch.tensor(commit_lens_cpu, dtype=batch.seq_lens_cpu.dtype)
|
||||
)
|
||||
# Keep seq_lens_sum in sync; flashinfer indices updaters rely on this for buffer sizing.
|
||||
batch.seq_lens_sum += sum(commit_lens_cpu)
|
||||
|
||||
# Build next-step context features from the committed verify-input tokens.
|
||||
hidden = logits_output.hidden_states
|
||||
if hidden is None:
|
||||
raise RuntimeError(
|
||||
"DFLASH verify requires target hidden states, but got None."
|
||||
)
|
||||
hidden = hidden.view(bs, self.draft_token_num, -1)
|
||||
segments: List[torch.Tensor] = []
|
||||
for i, ln in enumerate(commit_lens_cpu):
|
||||
if ln > 0:
|
||||
segments.append(hidden[i, :ln, :])
|
||||
next_target_hidden = torch.cat(segments, dim=0) if segments else hidden[:0]
|
||||
|
||||
# Avoid confusing downstream consumers (spec-v1 decode doesn't use this).
|
||||
logits_output.hidden_states = None
|
||||
|
||||
return (
|
||||
new_verified_id,
|
||||
commit_lens,
|
||||
next_target_hidden,
|
||||
accept_length_per_req_cpu,
|
||||
)
|
||||
@@ -0,0 +1,637 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from numbers import Integral
|
||||
from typing import Any, List, Optional, Tuple
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
from sglang.srt.layers.quantization.unquant import UnquantizedLinearMethod
|
||||
from sglang.srt.utils import is_cuda
|
||||
|
||||
DEFAULT_DFLASH_MASK_TOKEN = "<|MASK|>"
|
||||
|
||||
_DFLASH_SAMPLING_VERIFY_AVAILABLE = False
|
||||
_DFLASH_CHAIN_VERIFY_BUFFERS: dict[tuple[Optional[int], int], dict[str, Any]] = {}
|
||||
_DFLASH_VERIFY_SKIP_CUSTOM_MASK_BACKENDS = frozenset(
|
||||
{
|
||||
"FlashInferAttnBackend",
|
||||
"FlashInferMLAAttnBackend",
|
||||
"FlashAttentionBackend",
|
||||
"TRTLLMHAAttnBackend",
|
||||
"TRTLLMMLABackend",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
if is_cuda():
|
||||
try:
|
||||
from sgl_kernel import (
|
||||
top_k_renorm_prob,
|
||||
top_p_renorm_prob,
|
||||
tree_speculative_sampling_target_only,
|
||||
)
|
||||
|
||||
_DFLASH_SAMPLING_VERIFY_AVAILABLE = True
|
||||
except Exception:
|
||||
top_k_renorm_prob = None
|
||||
top_p_renorm_prob = None
|
||||
tree_speculative_sampling_target_only = None
|
||||
else:
|
||||
top_k_renorm_prob = None
|
||||
top_p_renorm_prob = None
|
||||
tree_speculative_sampling_target_only = None
|
||||
|
||||
|
||||
def is_dflash_sampling_verify_available() -> bool:
|
||||
return _DFLASH_SAMPLING_VERIFY_AVAILABLE
|
||||
|
||||
|
||||
def scale_kv_cell_size_per_token_for_dflash(
|
||||
*,
|
||||
target_cell_size_per_token: int,
|
||||
target_num_layers: int,
|
||||
draft_num_layers: int,
|
||||
draft_cell_size_per_token: Optional[int] = None,
|
||||
) -> int:
|
||||
"""Compute bytes/token budget for combined target+draft KV pools (DFLASH).
|
||||
|
||||
DFLASH runs a separate draft runner with its own KV pool. The target runner's
|
||||
token capacity must fit both pools in aggregate.
|
||||
|
||||
Returns:
|
||||
Approximate per-token bytes for (target KV + draft KV), expressed as a
|
||||
scaled version of `target_cell_size_per_token`, unless an explicit
|
||||
`draft_cell_size_per_token` is provided (in which case we sum them).
|
||||
"""
|
||||
if target_cell_size_per_token <= 0:
|
||||
raise ValueError(
|
||||
"target_cell_size_per_token must be positive, "
|
||||
f"got {target_cell_size_per_token}."
|
||||
)
|
||||
|
||||
if draft_cell_size_per_token is not None:
|
||||
draft_cell_size_per_token = int(draft_cell_size_per_token)
|
||||
if draft_cell_size_per_token <= 0:
|
||||
raise ValueError(
|
||||
"draft_cell_size_per_token must be positive when provided, "
|
||||
f"got {draft_cell_size_per_token}."
|
||||
)
|
||||
return int(target_cell_size_per_token) + int(draft_cell_size_per_token)
|
||||
|
||||
if target_num_layers <= 0 or draft_num_layers <= 0:
|
||||
return int(target_cell_size_per_token)
|
||||
|
||||
total_layers = int(target_num_layers) + int(draft_num_layers)
|
||||
return (
|
||||
int(target_cell_size_per_token) * int(total_layers) + int(target_num_layers) - 1
|
||||
) // int(target_num_layers)
|
||||
|
||||
|
||||
def resolve_dflash_verify_mask_policy(attn_backend: Any) -> tuple[str, bool]:
|
||||
backend = attn_backend
|
||||
for _ in range(4):
|
||||
full_backend = getattr(backend, "full_attn_backend", None)
|
||||
if full_backend is None:
|
||||
break
|
||||
backend = full_backend
|
||||
backend_name = type(backend).__name__
|
||||
return backend_name, (backend_name not in _DFLASH_VERIFY_SKIP_CUSTOM_MASK_BACKENDS)
|
||||
|
||||
|
||||
def _get_or_create_chain_verify_buffers(
|
||||
*,
|
||||
bs: int,
|
||||
draft_token_num: int,
|
||||
device: torch.device,
|
||||
) -> tuple[
|
||||
torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor
|
||||
]:
|
||||
key = (device.index, int(draft_token_num))
|
||||
cached = _DFLASH_CHAIN_VERIFY_BUFFERS.get(key)
|
||||
cap_bs = 0 if cached is None else int(cached["cap_bs"])
|
||||
if cap_bs < bs:
|
||||
new_cap = max(int(bs), cap_bs * 2 if cap_bs > 0 else int(bs))
|
||||
retrieve_index = torch.arange(
|
||||
new_cap * draft_token_num, dtype=torch.int64, device=device
|
||||
).view(new_cap, draft_token_num)
|
||||
row_next = torch.arange(
|
||||
1, draft_token_num + 1, dtype=torch.int64, device=device
|
||||
)
|
||||
row_next[-1] = -1
|
||||
retrieve_next_token = row_next.unsqueeze(0).expand(new_cap, -1).clone()
|
||||
retrieve_next_sibling = torch.full(
|
||||
(new_cap, draft_token_num), -1, dtype=torch.int64, device=device
|
||||
)
|
||||
predicts = torch.empty(
|
||||
(new_cap * draft_token_num,), dtype=torch.int32, device=device
|
||||
)
|
||||
accept_index = torch.empty(
|
||||
(new_cap, draft_token_num), dtype=torch.int32, device=device
|
||||
)
|
||||
accept_token_num = torch.empty((new_cap,), dtype=torch.int32, device=device)
|
||||
cached = {
|
||||
"cap_bs": int(new_cap),
|
||||
"retrieve_index": retrieve_index,
|
||||
"retrieve_next_token": retrieve_next_token,
|
||||
"retrieve_next_sibling": retrieve_next_sibling,
|
||||
"predicts": predicts,
|
||||
"accept_index": accept_index,
|
||||
"accept_token_num": accept_token_num,
|
||||
}
|
||||
_DFLASH_CHAIN_VERIFY_BUFFERS[key] = cached
|
||||
|
||||
assert cached is not None
|
||||
retrieve_index = cached["retrieve_index"][:bs]
|
||||
retrieve_next_token = cached["retrieve_next_token"][:bs]
|
||||
retrieve_next_sibling = cached["retrieve_next_sibling"][:bs]
|
||||
predicts = cached["predicts"][: bs * draft_token_num]
|
||||
accept_index = cached["accept_index"][:bs]
|
||||
accept_token_num = cached["accept_token_num"][:bs]
|
||||
return (
|
||||
retrieve_index,
|
||||
retrieve_next_token,
|
||||
retrieve_next_sibling,
|
||||
predicts,
|
||||
accept_index,
|
||||
accept_token_num,
|
||||
)
|
||||
|
||||
|
||||
def build_target_layer_ids(num_target_layers: int, num_draft_layers: int) -> List[int]:
|
||||
"""Select target layer indices used to build DFlash context features.
|
||||
|
||||
Args:
|
||||
num_target_layers: Number of transformer layers in the runtime target model.
|
||||
num_draft_layers: Number of layers in the DFlash draft model.
|
||||
|
||||
Returns:
|
||||
A list of 0-based target layer indices of length `num_draft_layers`.
|
||||
|
||||
Notes:
|
||||
- DFlash uses hidden states after each selected target layer (HF-style).
|
||||
- SGLang captures "before layer i", so the model hook will typically add +1
|
||||
when mapping to capture points.
|
||||
"""
|
||||
if num_target_layers <= 0:
|
||||
raise ValueError(
|
||||
f"num_target_layers must be positive, got {num_target_layers}."
|
||||
)
|
||||
if num_draft_layers <= 0:
|
||||
raise ValueError(f"num_draft_layers must be positive, got {num_draft_layers}.")
|
||||
|
||||
if num_draft_layers == 1:
|
||||
return [num_target_layers // 2]
|
||||
|
||||
start = 1
|
||||
end = num_target_layers - 3
|
||||
if end < start:
|
||||
raise ValueError(
|
||||
"DFlash layer selection requires num_target_layers >= 4. "
|
||||
f"Got num_target_layers={num_target_layers}."
|
||||
)
|
||||
|
||||
span = end - start
|
||||
return [
|
||||
int(round(start + (i * span) / (num_draft_layers - 1)))
|
||||
for i in range(num_draft_layers)
|
||||
]
|
||||
|
||||
|
||||
def _cfg_get(config: Any, key: str, default: Any = None) -> Any:
|
||||
if isinstance(config, dict):
|
||||
return config.get(key, default)
|
||||
return getattr(config, key, default)
|
||||
|
||||
|
||||
def _get_text_config(config: Any) -> Any:
|
||||
if config is None:
|
||||
return None
|
||||
if isinstance(config, dict):
|
||||
return config.get("text_config", config)
|
||||
text_config = getattr(config, "text_config", None)
|
||||
if text_config is not None:
|
||||
return text_config
|
||||
get_text_config = getattr(config, "get_text_config", None)
|
||||
if callable(get_text_config):
|
||||
try:
|
||||
resolved = get_text_config()
|
||||
if resolved is not None:
|
||||
return resolved
|
||||
except TypeError:
|
||||
pass
|
||||
return config
|
||||
|
||||
|
||||
def _get_dflash_config(config: Any) -> dict:
|
||||
if isinstance(config, dict):
|
||||
cfg = config.get("dflash_config", None)
|
||||
else:
|
||||
cfg = getattr(config, "dflash_config", None)
|
||||
if cfg is None:
|
||||
return {}
|
||||
if isinstance(cfg, dict):
|
||||
return cfg
|
||||
|
||||
try:
|
||||
return dict(cfg)
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def _parse_optional_int(
|
||||
value: Any,
|
||||
*,
|
||||
field_name: str,
|
||||
min_value: Optional[int] = None,
|
||||
) -> Optional[int]:
|
||||
if value is None:
|
||||
return None
|
||||
try:
|
||||
parsed = int(value)
|
||||
except Exception as e:
|
||||
raise ValueError(f"Invalid {field_name}={value!r}.") from e
|
||||
if min_value is not None and parsed < int(min_value):
|
||||
comparator = "positive" if int(min_value) == 1 else f">= {int(min_value)}"
|
||||
raise ValueError(f"{field_name} must be {comparator}, got {parsed}.")
|
||||
return parsed
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DFlashDraftConfig:
|
||||
num_hidden_layers: Optional[int]
|
||||
num_target_layers: Optional[int]
|
||||
block_size: Optional[int]
|
||||
target_layer_ids: Optional[List[int]]
|
||||
mask_token: str
|
||||
mask_token_id: Optional[int]
|
||||
|
||||
def require_num_layers(self) -> int:
|
||||
if self.num_hidden_layers is None:
|
||||
raise ValueError(
|
||||
"DFLASH requires draft num_hidden_layers in config. "
|
||||
"Got config without num_hidden_layers."
|
||||
)
|
||||
return int(self.num_hidden_layers)
|
||||
|
||||
def resolve_block_size(self, *, default: Optional[int] = None) -> Optional[int]:
|
||||
return self.block_size if self.block_size is not None else default
|
||||
|
||||
def resolve_target_layer_ids(
|
||||
self,
|
||||
*,
|
||||
target_num_layers: int,
|
||||
draft_num_layers: Optional[int] = None,
|
||||
) -> List[int]:
|
||||
target_num_layers = int(target_num_layers)
|
||||
if target_num_layers <= 0:
|
||||
raise ValueError(
|
||||
f"target_num_layers must be positive, got {target_num_layers}."
|
||||
)
|
||||
|
||||
if self.target_layer_ids is None:
|
||||
if draft_num_layers is None:
|
||||
draft_num_layers = self.require_num_layers()
|
||||
return build_target_layer_ids(target_num_layers, int(draft_num_layers))
|
||||
|
||||
resolved = list(self.target_layer_ids)
|
||||
if len(resolved) <= 0:
|
||||
raise ValueError(
|
||||
"DFLASH dflash_config.target_layer_ids must be non-empty. "
|
||||
f"Got len(target_layer_ids)={len(resolved)}."
|
||||
)
|
||||
for idx, val in enumerate(resolved):
|
||||
if val < 0 or val >= target_num_layers:
|
||||
raise ValueError(
|
||||
"DFLASH target_layer_ids contains an out-of-range layer id. "
|
||||
f"target_layer_ids[{idx}]={val}, target_num_layers={target_num_layers}."
|
||||
)
|
||||
return resolved
|
||||
|
||||
|
||||
def parse_dflash_draft_config(*, draft_hf_config: Any) -> DFlashDraftConfig:
|
||||
"""Parse and validate DFLASH draft config fields from HF config/dict."""
|
||||
dflash_cfg = _get_dflash_config(draft_hf_config)
|
||||
draft_text_config = _get_text_config(draft_hf_config)
|
||||
|
||||
num_hidden_layers = _parse_optional_int(
|
||||
_cfg_get(draft_text_config, "num_hidden_layers", None),
|
||||
field_name="DFLASH draft num_hidden_layers",
|
||||
min_value=1,
|
||||
)
|
||||
raw_num_target_layers = dflash_cfg.get(
|
||||
"num_target_layers",
|
||||
_cfg_get(draft_hf_config, "num_target_layers", None),
|
||||
)
|
||||
num_target_layers = _parse_optional_int(
|
||||
raw_num_target_layers,
|
||||
field_name="DFLASH draft num_target_layers",
|
||||
min_value=1,
|
||||
)
|
||||
|
||||
# Keep support for current checkpoints where block_size is top-level.
|
||||
raw_block_size = dflash_cfg.get(
|
||||
"block_size",
|
||||
_cfg_get(draft_hf_config, "block_size", None),
|
||||
)
|
||||
block_size = _parse_optional_int(
|
||||
raw_block_size,
|
||||
field_name="DFLASH block_size",
|
||||
min_value=1,
|
||||
)
|
||||
|
||||
layer_ids = dflash_cfg.get(
|
||||
"target_layer_ids",
|
||||
_cfg_get(draft_hf_config, "target_layer_ids", None),
|
||||
)
|
||||
parsed_target_layer_ids: Optional[List[int]]
|
||||
if layer_ids is None:
|
||||
parsed_target_layer_ids = None
|
||||
else:
|
||||
if not isinstance(layer_ids, (list, tuple)):
|
||||
raise ValueError(
|
||||
"DFLASH dflash_config.target_layer_ids must be a list of ints, "
|
||||
f"got type={type(layer_ids).__name__}."
|
||||
)
|
||||
parsed_target_layer_ids = [int(x) for x in layer_ids]
|
||||
if len(parsed_target_layer_ids) <= 0:
|
||||
raise ValueError(
|
||||
"DFLASH dflash_config.target_layer_ids must be non-empty. "
|
||||
f"Got len(target_layer_ids)={len(parsed_target_layer_ids)}."
|
||||
)
|
||||
|
||||
mask_token = dflash_cfg.get("mask_token", None)
|
||||
if mask_token is None:
|
||||
mask_token = DEFAULT_DFLASH_MASK_TOKEN
|
||||
if not isinstance(mask_token, str) or not mask_token:
|
||||
raise ValueError(
|
||||
"DFLASH dflash_config.mask_token must be a non-empty string, "
|
||||
f"got {mask_token!r}."
|
||||
)
|
||||
|
||||
mask_token_id = dflash_cfg.get("mask_token_id", None)
|
||||
if mask_token_id is not None:
|
||||
if not isinstance(mask_token_id, Integral) or isinstance(mask_token_id, bool):
|
||||
raise ValueError(
|
||||
"DFLASH dflash_config.mask_token_id must be an integer, "
|
||||
f"got {mask_token_id!r} (type={type(mask_token_id).__name__})."
|
||||
)
|
||||
mask_token_id = int(mask_token_id)
|
||||
if mask_token_id < 0:
|
||||
raise ValueError(
|
||||
"DFLASH dflash_config.mask_token_id must be non-negative, "
|
||||
f"got {mask_token_id}."
|
||||
)
|
||||
|
||||
return DFlashDraftConfig(
|
||||
num_hidden_layers=num_hidden_layers,
|
||||
num_target_layers=num_target_layers,
|
||||
block_size=block_size,
|
||||
target_layer_ids=parsed_target_layer_ids,
|
||||
mask_token=mask_token,
|
||||
mask_token_id=mask_token_id,
|
||||
)
|
||||
|
||||
|
||||
def can_dflash_slice_qkv_weight(qkv_proj: Any) -> Tuple[bool, str]:
|
||||
"""Validate whether DFlash can slice KV weights from a fused QKV linear layer."""
|
||||
quant_method = getattr(qkv_proj, "quant_method", None)
|
||||
if not isinstance(quant_method, UnquantizedLinearMethod):
|
||||
return (
|
||||
False,
|
||||
"quantized qkv_proj is not supported for this path "
|
||||
f"(quant_method={type(quant_method).__name__})",
|
||||
)
|
||||
if not hasattr(qkv_proj, "weight"):
|
||||
return False, "qkv weight tensor is missing"
|
||||
return True, ""
|
||||
|
||||
|
||||
def can_dflash_use_fused_qkv_proj(qkv_proj: Any) -> Tuple[bool, str]:
|
||||
"""Validate whether a QKV layer is eligible for DFlash fused KV materialization."""
|
||||
eligible, reason = can_dflash_slice_qkv_weight(qkv_proj)
|
||||
if not eligible:
|
||||
return False, reason
|
||||
if getattr(qkv_proj, "bias", None) is not None:
|
||||
return False, "qkv bias is not supported for fused KV path"
|
||||
return True, ""
|
||||
|
||||
|
||||
def compute_dflash_accept_len_and_bonus(
|
||||
*,
|
||||
candidates: torch.Tensor,
|
||||
target_predict: torch.Tensor,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Compute DFlash accept lengths and bonus tokens (greedy verify rule).
|
||||
|
||||
Args:
|
||||
candidates: Token ids proposed by the DFlash draft, including the current token.
|
||||
Shape: [bs, block_size]. candidates[:, 0] is the current token.
|
||||
target_predict: Token ids predicted by the target model for each position in the block.
|
||||
Shape: [bs, block_size]. target_predict[:, t] corresponds to argmax at position t.
|
||||
|
||||
Returns:
|
||||
accept_len: int32 tensor [bs], number of accepted *draft* tokens (excluding current token and bonus token).
|
||||
bonus: int64 tensor [bs], the target-predicted token at index accept_len (the "bonus" token to append).
|
||||
|
||||
Notes:
|
||||
Matches the reference implementation rule:
|
||||
accept while candidates[:, 1:] == target_predict[:, :-1] consecutively.
|
||||
"""
|
||||
if candidates.ndim != 2:
|
||||
raise ValueError(f"candidates must be 2D, got shape={tuple(candidates.shape)}")
|
||||
if target_predict.shape != candidates.shape:
|
||||
raise ValueError(
|
||||
"target_predict must have the same shape as candidates. "
|
||||
f"candidates.shape={tuple(candidates.shape)}, target_predict.shape={tuple(target_predict.shape)}"
|
||||
)
|
||||
|
||||
bs, block_size = candidates.shape
|
||||
if bs <= 0:
|
||||
raise ValueError(f"batch size must be positive, got {bs}.")
|
||||
if block_size <= 0:
|
||||
raise ValueError(f"block_size must be positive, got {block_size}.")
|
||||
|
||||
matches = candidates[:, 1:] == target_predict[:, :-1]
|
||||
accept_len = matches.to(torch.int32).cumprod(dim=1).sum(dim=1)
|
||||
bonus = target_predict[torch.arange(bs, device=target_predict.device), accept_len]
|
||||
return accept_len, bonus.to(torch.int64)
|
||||
|
||||
|
||||
def compute_dflash_sampling_accept_len_and_bonus(
|
||||
*,
|
||||
candidates: torch.Tensor,
|
||||
next_token_logits: torch.Tensor,
|
||||
sampling_info: Any,
|
||||
threshold_single: Optional[float] = None,
|
||||
threshold_acc: Optional[float] = None,
|
||||
uniform_samples: Optional[torch.Tensor] = None,
|
||||
uniform_samples_for_final_sampling: Optional[torch.Tensor] = None,
|
||||
use_sparse_topk: bool = True,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Compute DFlash accept lengths and bonus tokens for non-greedy sampling.
|
||||
|
||||
This is a chain-specialized variant of speculative target-only verification:
|
||||
- DFlash proposals are linear (topk == 1), so each verify level has at most one candidate.
|
||||
- When a candidate is rejected at a level, the final token is sampled from
|
||||
`relu(q - p)` where `p` has only the rejected candidate mass.
|
||||
"""
|
||||
if not _DFLASH_SAMPLING_VERIFY_AVAILABLE:
|
||||
raise RuntimeError(
|
||||
"DFLASH non-greedy verification is unavailable on this build/device."
|
||||
)
|
||||
if candidates.ndim != 2:
|
||||
raise ValueError(f"candidates must be 2D, got shape={tuple(candidates.shape)}")
|
||||
if next_token_logits.ndim != 2:
|
||||
raise ValueError(
|
||||
"next_token_logits must be 2D, "
|
||||
f"got shape={tuple(next_token_logits.shape)}."
|
||||
)
|
||||
|
||||
bs, draft_token_num = candidates.shape
|
||||
if bs <= 0:
|
||||
raise ValueError(f"batch size must be positive, got {bs}.")
|
||||
if draft_token_num <= 0:
|
||||
raise ValueError(f"draft_token_num must be positive, got {draft_token_num}.")
|
||||
if next_token_logits.shape[0] != bs * draft_token_num:
|
||||
raise ValueError(
|
||||
"next_token_logits row count mismatch. "
|
||||
f"Expected {bs * draft_token_num}, got {next_token_logits.shape[0]}."
|
||||
)
|
||||
if candidates.device != next_token_logits.device:
|
||||
raise ValueError(
|
||||
"candidates and next_token_logits must be on the same device, "
|
||||
f"got {candidates.device} and {next_token_logits.device}."
|
||||
)
|
||||
|
||||
if threshold_single is None:
|
||||
from sglang.srt.server_args import get_global_server_args
|
||||
|
||||
threshold_single = get_global_server_args().speculative_accept_threshold_single
|
||||
if threshold_acc is None:
|
||||
from sglang.srt.server_args import get_global_server_args
|
||||
|
||||
threshold_acc = get_global_server_args().speculative_accept_threshold_acc
|
||||
threshold_single = float(threshold_single)
|
||||
threshold_acc = max(float(threshold_acc), 1e-9)
|
||||
|
||||
device = next_token_logits.device
|
||||
|
||||
if uniform_samples is None:
|
||||
uniform_samples = torch.rand(
|
||||
(bs, draft_token_num), dtype=torch.float32, device=device
|
||||
)
|
||||
else:
|
||||
if uniform_samples.shape != (bs, draft_token_num):
|
||||
raise ValueError(
|
||||
"uniform_samples shape mismatch. "
|
||||
f"Expected {(bs, draft_token_num)}, got {tuple(uniform_samples.shape)}."
|
||||
)
|
||||
uniform_samples = uniform_samples.to(device=device, dtype=torch.float32)
|
||||
|
||||
if uniform_samples_for_final_sampling is None:
|
||||
uniform_samples_for_final_sampling = torch.rand(
|
||||
(bs,), dtype=torch.float32, device=device
|
||||
)
|
||||
else:
|
||||
if uniform_samples_for_final_sampling.shape != (bs,):
|
||||
raise ValueError(
|
||||
"uniform_samples_for_final_sampling shape mismatch. "
|
||||
f"Expected {(bs,)}, got {tuple(uniform_samples_for_final_sampling.shape)}."
|
||||
)
|
||||
uniform_samples_for_final_sampling = uniform_samples_for_final_sampling.to(
|
||||
device=device,
|
||||
dtype=torch.float32,
|
||||
)
|
||||
|
||||
need_top_k = bool(getattr(sampling_info, "need_top_k_sampling", True))
|
||||
need_top_p = bool(getattr(sampling_info, "need_top_p_sampling", False))
|
||||
# Build target distribution once over all verify rows.
|
||||
expanded_temperature = torch.repeat_interleave(
|
||||
sampling_info.temperatures, draft_token_num, dim=0
|
||||
)
|
||||
scaled_logits = next_token_logits / expanded_temperature
|
||||
sparse_topk_applied = False
|
||||
|
||||
if use_sparse_topk and need_top_k:
|
||||
repeated_top_ks = torch.repeat_interleave(
|
||||
sampling_info.top_ks, draft_token_num, dim=0
|
||||
).to(dtype=torch.int64)
|
||||
vocab_size = int(scaled_logits.shape[-1])
|
||||
repeated_top_ks.clamp_(min=1, max=vocab_size)
|
||||
max_top_k = int(repeated_top_ks.max().item())
|
||||
|
||||
# Sparse exact path for top-k/top-p (top-k-first semantics), then scatter to dense.
|
||||
if 0 < max_top_k < vocab_size:
|
||||
topk_logits, topk_indices = torch.topk(scaled_logits, k=max_top_k, dim=-1)
|
||||
if not torch.all(repeated_top_ks == max_top_k):
|
||||
ranks = torch.arange(max_top_k, device=device, dtype=torch.int64)[
|
||||
None, :
|
||||
]
|
||||
valid = ranks < repeated_top_ks.unsqueeze(1)
|
||||
topk_logits = topk_logits.masked_fill(~valid, float("-inf"))
|
||||
|
||||
topk_probs = F.softmax(topk_logits, dim=-1)
|
||||
if need_top_p:
|
||||
repeated_top_ps = torch.repeat_interleave(
|
||||
sampling_info.top_ps, draft_token_num, dim=0
|
||||
)
|
||||
topk_probs = top_p_renorm_prob(topk_probs, repeated_top_ps)
|
||||
|
||||
target_probs = torch.zeros_like(scaled_logits, dtype=topk_probs.dtype)
|
||||
target_probs.scatter_(1, topk_indices, topk_probs)
|
||||
sparse_topk_applied = True
|
||||
|
||||
if not sparse_topk_applied:
|
||||
target_probs = F.softmax(scaled_logits, dim=-1)
|
||||
if need_top_k:
|
||||
target_probs = top_k_renorm_prob(
|
||||
target_probs,
|
||||
torch.repeat_interleave(sampling_info.top_ks, draft_token_num, dim=0),
|
||||
)
|
||||
if need_top_p:
|
||||
target_probs = top_p_renorm_prob(
|
||||
target_probs,
|
||||
torch.repeat_interleave(sampling_info.top_ps, draft_token_num, dim=0),
|
||||
)
|
||||
target_probs = target_probs.view(bs, draft_token_num, -1).contiguous()
|
||||
draft_probs = torch.zeros_like(target_probs)
|
||||
|
||||
(
|
||||
retrieve_index,
|
||||
retrieve_next_token,
|
||||
retrieve_next_sibling,
|
||||
predicts,
|
||||
accept_index,
|
||||
accept_token_num,
|
||||
) = _get_or_create_chain_verify_buffers(
|
||||
bs=bs,
|
||||
draft_token_num=draft_token_num,
|
||||
device=device,
|
||||
)
|
||||
candidates_i64 = (
|
||||
candidates if candidates.dtype == torch.int64 else candidates.to(torch.int64)
|
||||
)
|
||||
tree_speculative_sampling_target_only(
|
||||
predicts=predicts,
|
||||
accept_index=accept_index,
|
||||
accept_token_num=accept_token_num,
|
||||
candidates=candidates_i64,
|
||||
retrive_index=retrieve_index,
|
||||
retrive_next_token=retrieve_next_token,
|
||||
retrive_next_sibling=retrieve_next_sibling,
|
||||
uniform_samples=uniform_samples,
|
||||
uniform_samples_for_final_sampling=uniform_samples_for_final_sampling,
|
||||
target_probs=target_probs,
|
||||
draft_probs=draft_probs,
|
||||
threshold_single=threshold_single,
|
||||
threshold_acc=threshold_acc,
|
||||
deterministic=True,
|
||||
)
|
||||
|
||||
accept_len = accept_token_num
|
||||
row_ids = torch.arange(bs, dtype=torch.long, device=device)
|
||||
accept_pos = accept_index[row_ids, accept_len.to(torch.long)].to(torch.long)
|
||||
bonus = predicts[accept_pos].to(torch.int64)
|
||||
return accept_len, bonus
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,7 @@ if TYPE_CHECKING:
|
||||
class SpeculativeAlgorithm(Enum):
|
||||
"""Enumeration of speculative decoding algorithms."""
|
||||
|
||||
DFLASH = auto()
|
||||
EAGLE = auto()
|
||||
EAGLE3 = auto()
|
||||
STANDALONE = auto()
|
||||
@@ -33,6 +34,9 @@ class SpeculativeAlgorithm(Enum):
|
||||
def is_none(self) -> bool:
|
||||
return self == SpeculativeAlgorithm.NONE
|
||||
|
||||
def is_speculative(self) -> bool:
|
||||
return self != SpeculativeAlgorithm.NONE
|
||||
|
||||
def is_eagle(self) -> bool:
|
||||
# NOTE: EAGLE3 is a variant of EAGLE
|
||||
return self == SpeculativeAlgorithm.EAGLE or self == SpeculativeAlgorithm.EAGLE3
|
||||
@@ -40,6 +44,9 @@ class SpeculativeAlgorithm(Enum):
|
||||
def is_eagle3(self) -> bool:
|
||||
return self == SpeculativeAlgorithm.EAGLE3
|
||||
|
||||
def is_dflash(self) -> bool:
|
||||
return self == SpeculativeAlgorithm.DFLASH
|
||||
|
||||
def is_standalone(self) -> bool:
|
||||
return self == SpeculativeAlgorithm.STANDALONE
|
||||
|
||||
@@ -57,6 +64,16 @@ class SpeculativeAlgorithm(Enum):
|
||||
), "Cannot create worker for NONE speculative algorithm."
|
||||
|
||||
enable_overlap = not server_args.disable_overlap_schedule
|
||||
|
||||
if self.is_dflash():
|
||||
if enable_overlap:
|
||||
raise ValueError(
|
||||
"DFLASH does not support overlap scheduling (spec v2)."
|
||||
)
|
||||
from sglang.srt.speculative.dflash_worker import DFlashWorker
|
||||
|
||||
return DFlashWorker
|
||||
|
||||
if self.is_eagle() and server_args.enable_multi_layer_eagle:
|
||||
# FIXME: migrate to EagleWorker
|
||||
if enable_overlap:
|
||||
@@ -110,6 +127,8 @@ class SpecInputType(IntEnum):
|
||||
# If all algorithms can share the same datastrucutre of draft_input and verify_input, consider simplify it
|
||||
EAGLE_DRAFT = auto()
|
||||
EAGLE_VERIFY = auto()
|
||||
DFLASH_DRAFT = auto()
|
||||
DFLASH_VERIFY = auto()
|
||||
NGRAM_VERIFY = auto()
|
||||
|
||||
|
||||
@@ -120,11 +139,15 @@ class SpecInput(ABC):
|
||||
def is_draft_input(self) -> bool:
|
||||
# FIXME: remove this function which is only used for assertion
|
||||
# or use another variable name like `draft_input` to substitute `spec_info`
|
||||
return self.spec_input_type == SpecInputType.EAGLE_DRAFT
|
||||
return self.spec_input_type in {
|
||||
SpecInputType.EAGLE_DRAFT,
|
||||
SpecInputType.DFLASH_DRAFT,
|
||||
}
|
||||
|
||||
def is_verify_input(self) -> bool:
|
||||
return self.spec_input_type in {
|
||||
SpecInputType.EAGLE_VERIFY,
|
||||
SpecInputType.DFLASH_VERIFY,
|
||||
SpecInputType.NGRAM_VERIFY,
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# Copyright 2023-2024 SGLang Team
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ==============================================================================
|
||||
"""Triton kernels for speculative decoding."""
|
||||
|
||||
from sglang.srt.speculative.triton_ops.fused_kv_materialize import (
|
||||
FusedKVMaterializeHelper,
|
||||
)
|
||||
|
||||
__all__ = ["FusedKVMaterializeHelper"]
|
||||
@@ -0,0 +1,303 @@
|
||||
# Copyright 2023-2024 SGLang Team
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ==============================================================================
|
||||
"""Fused Triton kernel for DFlash KV materialization.
|
||||
|
||||
Combines: KV projection (cuBLAS) + RMSNorm + RoPE (Triton), then pool-managed KV writes.
|
||||
"""
|
||||
|
||||
from typing import Callable, List
|
||||
|
||||
import torch
|
||||
import triton
|
||||
import triton.language as tl
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _fused_norm_rope_kernel(
|
||||
kv_ptr, # [total_ctx, kv_size * 2]
|
||||
k_norm_weight_ptr, # [head_dim]
|
||||
cos_sin_cache_ptr, # [max_pos, rotary_dim]
|
||||
positions_ptr, # [total_ctx]
|
||||
k_out_ptr, # [total_ctx, num_kv_heads, head_dim]
|
||||
v_out_ptr, # [total_ctx, num_kv_heads, head_dim]
|
||||
kv_stride_ctx,
|
||||
cos_sin_stride_pos,
|
||||
k_out_stride_ctx,
|
||||
k_out_stride_head,
|
||||
v_out_stride_ctx,
|
||||
v_out_stride_head,
|
||||
total_ctx,
|
||||
num_kv_heads: tl.constexpr,
|
||||
head_dim: tl.constexpr,
|
||||
kv_size: tl.constexpr,
|
||||
rotary_dim: tl.constexpr,
|
||||
half_rotary_dim: tl.constexpr,
|
||||
eps: tl.constexpr,
|
||||
BLOCK_HD: tl.constexpr,
|
||||
):
|
||||
"""Fused RMSNorm(K) + RoPE(K) materialization. Grid: (total_ctx, num_kv_heads)."""
|
||||
ctx_id = tl.program_id(0)
|
||||
head_id = tl.program_id(1)
|
||||
if ctx_id >= total_ctx:
|
||||
return
|
||||
|
||||
# Load metadata
|
||||
position = tl.load(positions_ptr + ctx_id)
|
||||
|
||||
# Compute base pointers
|
||||
kv_base = kv_ptr + ctx_id * kv_stride_ctx
|
||||
k_base = kv_base + head_id * head_dim
|
||||
v_base = kv_base + kv_size + head_id * head_dim
|
||||
k_write = k_out_ptr + ctx_id * k_out_stride_ctx + head_id * k_out_stride_head
|
||||
v_write = v_out_ptr + ctx_id * v_out_stride_ctx + head_id * v_out_stride_head
|
||||
|
||||
# Load K and V
|
||||
offs = tl.arange(0, BLOCK_HD)
|
||||
mask_hd = offs < head_dim
|
||||
mask_half = offs < half_rotary_dim
|
||||
|
||||
k_raw = tl.load(k_base + offs, mask=mask_hd, other=0.0).to(tl.float32)
|
||||
v_raw = tl.load(v_base + offs, mask=mask_hd, other=0.0)
|
||||
|
||||
# RMSNorm on K
|
||||
inv_rms = tl.rsqrt(tl.sum(k_raw * k_raw) / head_dim + eps)
|
||||
norm_w = tl.load(k_norm_weight_ptr + offs, mask=mask_hd, other=1.0).to(tl.float32)
|
||||
k_normed = k_raw * inv_rms * norm_w
|
||||
|
||||
# RoPE (neox style): k_first, k_second -> rotated
|
||||
cos_sin_base = cos_sin_cache_ptr + position * cos_sin_stride_pos
|
||||
cos_v = tl.load(cos_sin_base + offs, mask=mask_half, other=1.0).to(tl.float32)
|
||||
sin_v = tl.load(
|
||||
cos_sin_base + half_rotary_dim + offs, mask=mask_half, other=0.0
|
||||
).to(tl.float32)
|
||||
|
||||
# Extract first/second halves of K for rotation
|
||||
k_first = tl.where(mask_half, k_normed, 0.0)
|
||||
k_second_raw = tl.load(
|
||||
k_base + half_rotary_dim + offs, mask=mask_half, other=0.0
|
||||
).to(tl.float32)
|
||||
norm_w_second = tl.load(
|
||||
k_norm_weight_ptr + half_rotary_dim + offs, mask=mask_half, other=1.0
|
||||
).to(tl.float32)
|
||||
k_second = k_second_raw * inv_rms * norm_w_second
|
||||
|
||||
# Apply rotation
|
||||
k_rot_first = k_first * cos_v - k_second * sin_v
|
||||
k_rot_second = k_second * cos_v + k_first * sin_v
|
||||
|
||||
# Store V (no transform)
|
||||
tl.store(v_write + offs, v_raw, mask=mask_hd)
|
||||
|
||||
# Store K: rotated halves + pass-through
|
||||
tl.store(k_write + offs, k_rot_first.to(v_raw.dtype), mask=mask_half)
|
||||
tl.store(
|
||||
k_write + half_rotary_dim + offs, k_rot_second.to(v_raw.dtype), mask=mask_half
|
||||
)
|
||||
mask_pass = (offs >= rotary_dim) & (offs < head_dim)
|
||||
tl.store(k_write + offs, k_normed.to(v_raw.dtype), mask=mask_pass)
|
||||
|
||||
|
||||
def _fused_norm_rope(
|
||||
kv: torch.Tensor, # [total_ctx, kv_size*2]
|
||||
k_norm_weight: torch.Tensor, # [head_dim]
|
||||
cos_sin_cache: torch.Tensor, # [max_pos, rotary_dim]
|
||||
positions: torch.Tensor, # [total_ctx]
|
||||
num_kv_heads: int,
|
||||
head_dim: int,
|
||||
rotary_dim: int,
|
||||
eps: float = 1e-6,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Fused RMSNorm + RoPE materialization for a single layer."""
|
||||
total_ctx = kv.shape[0]
|
||||
if total_ctx == 0:
|
||||
empty = torch.empty(
|
||||
(0, num_kv_heads, head_dim), dtype=kv.dtype, device=kv.device
|
||||
)
|
||||
return empty, empty
|
||||
|
||||
kv_size = num_kv_heads * head_dim
|
||||
if kv.shape[1] != kv_size * 2:
|
||||
raise ValueError(
|
||||
"Invalid fused KV projection shape: "
|
||||
f"got {tuple(kv.shape)}, expected second dim {kv_size * 2}."
|
||||
)
|
||||
if rotary_dim <= 0 or rotary_dim > head_dim or rotary_dim % 2 != 0:
|
||||
raise ValueError(
|
||||
"Invalid fused KV rotary/head dim pair: "
|
||||
f"rotary_dim={rotary_dim}, head_dim={head_dim}."
|
||||
)
|
||||
|
||||
half_rotary_dim = rotary_dim // 2
|
||||
BLOCK_HD = triton.next_power_of_2(head_dim)
|
||||
|
||||
# Ensure int64 for indexing
|
||||
if positions.device != kv.device:
|
||||
positions = positions.to(device=kv.device, dtype=torch.int64)
|
||||
elif positions.dtype != torch.int64:
|
||||
positions = positions.to(torch.int64)
|
||||
|
||||
k_out = torch.empty(
|
||||
(total_ctx, num_kv_heads, head_dim), dtype=kv.dtype, device=kv.device
|
||||
)
|
||||
v_out = torch.empty_like(k_out)
|
||||
|
||||
_fused_norm_rope_kernel[(total_ctx, num_kv_heads)](
|
||||
kv,
|
||||
k_norm_weight,
|
||||
cos_sin_cache,
|
||||
positions,
|
||||
k_out,
|
||||
v_out,
|
||||
kv.stride(0),
|
||||
cos_sin_cache.stride(0),
|
||||
k_out.stride(0),
|
||||
k_out.stride(1),
|
||||
v_out.stride(0),
|
||||
v_out.stride(1),
|
||||
total_ctx,
|
||||
num_kv_heads,
|
||||
head_dim,
|
||||
kv_size,
|
||||
rotary_dim,
|
||||
half_rotary_dim,
|
||||
eps,
|
||||
BLOCK_HD,
|
||||
)
|
||||
return k_out, v_out
|
||||
|
||||
|
||||
class FusedKVMaterializeHelper:
|
||||
"""Fused KV materialization helper using batched projection.
|
||||
|
||||
Uses torch.einsum for batched KV projection across all layers,
|
||||
then a Triton kernel for fused RMSNorm + RoPE materialization per layer.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
layers: List,
|
||||
rotary_emb,
|
||||
num_kv_heads: int,
|
||||
head_dim: int,
|
||||
device: torch.device,
|
||||
):
|
||||
self.num_kv_heads = num_kv_heads
|
||||
self.head_dim = head_dim
|
||||
self.rotary_emb = rotary_emb
|
||||
self.n_layers = len(layers)
|
||||
self.device = device
|
||||
|
||||
self.rotary_dim = int(getattr(rotary_emb, "rotary_dim", head_dim))
|
||||
self.is_neox_style = bool(getattr(rotary_emb, "is_neox_style", True))
|
||||
|
||||
if not self.is_neox_style:
|
||||
raise NotImplementedError("Only neox-style RoPE is supported.")
|
||||
if self.rotary_dim <= 0 or self.rotary_dim > self.head_dim:
|
||||
raise ValueError(
|
||||
"Invalid fused KV rotary/head dim pair: "
|
||||
f"rotary_dim={self.rotary_dim}, head_dim={self.head_dim}."
|
||||
)
|
||||
|
||||
# Pre-extract and stack weights for batched projection.
|
||||
kv_weights = []
|
||||
self.k_norm_weights = []
|
||||
self.eps_values = []
|
||||
|
||||
for layer_id, layer in enumerate(layers):
|
||||
attn = layer.self_attn
|
||||
if int(attn.num_kv_heads) != self.num_kv_heads:
|
||||
raise ValueError(
|
||||
"num_kv_heads mismatch across layers for fused KV path: "
|
||||
f"expected {self.num_kv_heads}, got {int(attn.num_kv_heads)} at layer {layer_id}."
|
||||
)
|
||||
if int(attn.head_dim) != self.head_dim:
|
||||
raise ValueError(
|
||||
"head_dim mismatch across layers for fused KV path: "
|
||||
f"expected {self.head_dim}, got {int(attn.head_dim)} at layer {layer_id}."
|
||||
)
|
||||
layer_rotary_dim = int(
|
||||
getattr(attn.rotary_emb, "rotary_dim", self.head_dim)
|
||||
)
|
||||
layer_is_neox = bool(getattr(attn.rotary_emb, "is_neox_style", True))
|
||||
if (
|
||||
layer_rotary_dim != self.rotary_dim
|
||||
or layer_is_neox != self.is_neox_style
|
||||
):
|
||||
raise ValueError(
|
||||
"RoPE config mismatch across layers for fused KV path: "
|
||||
f"expected (rotary_dim={self.rotary_dim}, neox={self.is_neox_style}), "
|
||||
f"got (rotary_dim={layer_rotary_dim}, neox={layer_is_neox}) at layer {layer_id}."
|
||||
)
|
||||
|
||||
# Extract KV portion of QKV weight
|
||||
qkv_w = attn.qkv_proj.weight
|
||||
kv_weight = qkv_w[attn.q_size : attn.q_size + 2 * attn.kv_size]
|
||||
kv_weights.append(kv_weight)
|
||||
self.k_norm_weights.append(attn.k_norm.weight)
|
||||
self.eps_values.append(attn.k_norm.variance_epsilon)
|
||||
|
||||
# Stack for batched einsum: [n_layers, kv_size*2, hidden_size]
|
||||
self.batched_kv_weight = torch.stack(kv_weights)
|
||||
|
||||
def materialize(
|
||||
self,
|
||||
ctx_hidden: torch.Tensor,
|
||||
positions: torch.Tensor,
|
||||
write_layer_kv: Callable[[int, torch.Tensor, torch.Tensor], None],
|
||||
) -> None:
|
||||
"""Materialize KV cache for all layers using batched projection."""
|
||||
total_ctx = ctx_hidden.shape[0]
|
||||
if total_ctx == 0:
|
||||
return
|
||||
|
||||
if positions.ndim != 1:
|
||||
positions = positions.reshape(-1)
|
||||
if positions.numel() != total_ctx:
|
||||
raise ValueError(
|
||||
"positions must match ctx_hidden token count for fused KV materialization: "
|
||||
f"positions={positions.numel()}, total_ctx={total_ctx}."
|
||||
)
|
||||
|
||||
max_position = int(positions.max().item())
|
||||
ensure_cos_sin_cache_length = getattr(
|
||||
self.rotary_emb, "_ensure_cos_sin_cache_length", None
|
||||
)
|
||||
if callable(ensure_cos_sin_cache_length):
|
||||
ensure_cos_sin_cache_length(max_position)
|
||||
|
||||
cos_sin_cache = self.rotary_emb.cos_sin_cache
|
||||
if max_position >= int(cos_sin_cache.shape[0]):
|
||||
raise RuntimeError(
|
||||
"RoPE cos/sin cache is too short for fused KV materialization: "
|
||||
f"max_position={max_position}, cache_len={int(cos_sin_cache.shape[0])}."
|
||||
)
|
||||
if cos_sin_cache.device != ctx_hidden.device:
|
||||
cos_sin_cache = cos_sin_cache.to(ctx_hidden.device)
|
||||
|
||||
# Batched KV projection: [n_layers, total_ctx, kv_size*2]
|
||||
kv_all = torch.einsum("th,loh->lto", ctx_hidden, self.batched_kv_weight)
|
||||
|
||||
# Per-layer fused norm/RoPE/materialize, then delegate writes to the KV pool.
|
||||
for layer_id in range(self.n_layers):
|
||||
cache_k, cache_v = _fused_norm_rope(
|
||||
kv_all[layer_id],
|
||||
self.k_norm_weights[layer_id],
|
||||
cos_sin_cache,
|
||||
positions,
|
||||
self.num_kv_heads,
|
||||
self.head_dim,
|
||||
self.rotary_dim,
|
||||
self.eps_values[layer_id],
|
||||
)
|
||||
write_layer_kv(layer_id, cache_k, cache_v)
|
||||
@@ -107,6 +107,10 @@ DEFAULT_DRAFT_MODEL_EAGLE = "lmsys/sglang-EAGLE-llama2-chat-7B"
|
||||
DEFAULT_TARGET_MODEL_EAGLE3 = "meta-llama/Llama-3.1-8B-Instruct"
|
||||
DEFAULT_DRAFT_MODEL_EAGLE3 = "lmsys/sglang-EAGLE3-LLaMA3.1-Instruct-8B"
|
||||
|
||||
# DFLASH model
|
||||
DEFAULT_TARGET_MODEL_DFLASH = "meta-llama/Llama-3.1-8B-Instruct"
|
||||
DEFAULT_DRAFT_MODEL_DFLASH = "z-lab/LLaMA3.1-8B-Instruct-DFlash-UltraChat"
|
||||
|
||||
# EAGLE2 with DP-Attention models
|
||||
DEFAULT_TARGET_MODEL_EAGLE_DP_ATTN = "Qwen/Qwen3-30B-A3B"
|
||||
DEFAULT_DRAFT_MODEL_EAGLE_DP_ATTN = "Tengyunw/qwen3_30b_moe_eagle3"
|
||||
|
||||
Reference in New Issue
Block a user