Files
wehub-resource-sync 94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:38:16 +08:00

1700 lines
71 KiB
Python

import logging
import math
from typing import List, Optional
import torch
from sglang.kernels.ops.speculative.cache_locs import assign_extend_cache_locs_func
from sglang.kernels.ops.speculative.dflash import (
_compute_dflash_accept_bonus_triton_unchecked,
_prepare_dflash_draft_block_unchecked,
)
from sglang.srt.distributed import get_tp_group
from sglang.srt.managers.schedule_batch import ScheduleBatch
from sglang.srt.managers.scheduler import GenerationBatchResult
from sglang.srt.managers.tp_worker import TpModelWorker
from sglang.srt.model_executor.cuda_graph_config import Backend
from sglang.srt.model_executor.forward_batch_info import (
CaptureHiddenMode,
ForwardBatch,
ForwardMode,
compute_position,
)
from sglang.srt.server_args import ServerArgs
from sglang.srt.speculative.base_spec_worker import BaseSpecWorker
from sglang.srt.speculative.dflash_info import DFlashVerifyInput
from sglang.srt.speculative.dflash_info_v2 import DFlashDraftInputV2
from sglang.srt.speculative.dflash_utils import (
apply_dflash_verify_logits_adjustments,
can_dflash_use_fused_qkv_proj,
compute_dflash_correct_drafts_and_bonus,
compute_dflash_sampling_correct_drafts_and_bonus,
is_dflash_sampling_verify_available,
parse_dflash_draft_config,
)
from sglang.srt.speculative.draft_worker_common import (
build_block_pos_offsets,
build_draft_tp_worker,
make_draft_block_spec_info,
make_draft_input_v2,
make_draft_sampler_capture_hook,
)
from sglang.srt.speculative.spec_info import SpeculativeAlgorithm
from sglang.srt.speculative.spec_utils import assign_req_to_token_pool_func
from sglang.srt.utils import get_available_gpu_memory, is_cuda, is_hip, is_npu
_is_npu = is_npu()
logger = logging.getLogger(__name__)
_FusedKVMaterializeHelper = None
def _get_fused_kv_materialize_helper():
global _FusedKVMaterializeHelper
if _FusedKVMaterializeHelper is None:
from sglang.kernels.ops.speculative.fused_kv_materialize import (
FusedKVMaterializeHelper,
)
_FusedKVMaterializeHelper = FusedKVMaterializeHelper
return _FusedKVMaterializeHelper
class _DflashDraftSampler:
"""Capture-safe greedy argmax over the target LM head, run inside the draft
cuda graph so the draft sampling is captured and counted in fwd_occupancy.
DFLASH's draft has no head of its own; it borrows the target `lm_head`.
tp=1 / no-added-vocab only; TP>1 stays eager in the worker.
"""
def __init__(self, *, weight, block_size, num_org, org_vocab_start, max_bs):
self.weight = weight
self.block_size = int(block_size)
self.num_org = int(num_org)
self.org_vocab_start = int(org_vocab_start)
# Proposed draft tokens: written in-graph, read by the worker after replay.
self.out = torch.empty(
(int(max_bs) * (self.block_size - 1),),
dtype=torch.int64,
device=weight.device,
)
def __call__(self, hidden_states, input_ids=None):
# draft tokens are block positions 1: (pos 0 is the seeded bonus token)
bs = hidden_states.shape[0] // self.block_size
hs = hidden_states.view(bs, self.block_size, -1)[:, 1:, :].reshape(
-1, hidden_states.shape[-1]
)
if hs.dtype != self.weight.dtype:
hs = hs.to(self.weight.dtype)
logits = torch.matmul(hs, self.weight[: self.num_org].T)
tokens = torch.argmax(logits, dim=-1).to(torch.long)
if self.org_vocab_start:
tokens += self.org_vocab_start
self.out[: tokens.shape[0]].copy_(tokens)
class DFlashWorkerV2(BaseSpecWorker):
"""DFLASH speculative decoding worker (spec-v2).
Drives both overlap and non-overlap scheduling, same as EAGLE: the
scheduler runs it synchronously when overlap is disabled.
"""
def __init__(
self,
server_args: ServerArgs,
gpu_id: int,
tp_rank: int,
dp_rank: Optional[int],
moe_ep_rank: int,
attn_cp_rank: int,
moe_dp_rank: int,
nccl_port: int,
target_worker: TpModelWorker,
):
self.server_args = server_args
self.gpu_id = gpu_id
self.tp_rank = tp_rank
self.dp_rank = dp_rank
self.moe_ep_rank = moe_ep_rank
self.attn_cp_rank = attn_cp_rank
self.moe_dp_rank = moe_dp_rank
self.nccl_port = nccl_port
self._target_worker = target_worker
self.model_runner = target_worker.model_runner
self._need_mamba_verify_commit = False
self.page_size = server_args.page_size
# Normalized in arg_groups.speculative_hook.handle_speculative_decoding.
self.draft_window_size: Optional[int] = (
server_args.speculative_draft_window_size
)
self.use_compact_draft_cache = self.draft_window_size is not None
self.device = target_worker.device
self._warned_sampling_fallback = False
self._logged_first_verify = False
bundle = build_draft_tp_worker(
server_args=server_args,
gpu_id=gpu_id,
tp_rank=tp_rank,
dp_rank=dp_rank,
moe_ep_rank=moe_ep_rank,
attn_cp_rank=attn_cp_rank,
moe_dp_rank=moe_dp_rank,
nccl_port=nccl_port,
target_model_config=target_worker.model_runner.model_config,
algo_label="DFLASH",
)
self._draft_worker = bundle.draft_worker
self.draft_model_runner = bundle.draft_model_runner
self._draft_sampler = None
self.draft_model = bundle.draft_model
draft_config = parse_dflash_draft_config(
draft_hf_config=self.draft_model_runner.model_config.hf_config
)
if server_args.speculative_num_draft_tokens is None:
# Should not happen (ServerArgs should have inferred it), but keep a fallback.
self.block_size = int(draft_config.resolve_block_size(default=16))
else:
self.block_size = int(server_args.speculative_num_draft_tokens)
model_block_size = draft_config.block_size
if model_block_size is None:
model_block_size = getattr(self.draft_model, "block_size", None)
if model_block_size is not None and int(model_block_size) != int(
self.block_size
):
logger.warning(
"DFLASH block size mismatch: using speculative_num_draft_tokens=%s but draft config block_size=%s.",
self.block_size,
model_block_size,
)
self.speculative_num_draft_tokens = int(self.block_size)
self._mask_token = draft_config.mask_token
self._mask_token_id_override = draft_config.mask_token_id
self._mask_token_id = self._resolve_mask_token_id(
mask_token=self._mask_token,
mask_token_id=self._mask_token_id_override,
)
if self.tp_rank == 0:
logger.info(
"Initialized DFLASH draft runner. attention_backend=%s, model=%s, block_size=%s, draft_window_size=%s, compact_cache=%s",
bundle.resolved_attention_backend,
self.draft_model.__class__.__name__,
self.block_size,
self.draft_window_size,
self.use_compact_draft_cache,
)
logger.info(
"DFLASH draft runner ready. mask_token=%s, mask_token_id=%s, mask_token_id_override=%s",
self._mask_token,
self._mask_token_id,
self._mask_token_id_override,
)
self._block_pos_offsets = build_block_pos_offsets(
length=self.block_size, device=self.device
)
self._draft_block_ids_buf: Optional[torch.Tensor] = None # [cap_bs, block_size]
self._draft_block_positions_buf: Optional[torch.Tensor] = (
None # [cap_bs, block_size]
)
self._draft_block_tokens_buf: Optional[torch.Tensor] = (
None # [cap_bs, block_size]
)
self._draft_verify_out_cache_loc_buf: Optional[torch.Tensor] = (
None # [cap_bs, block_size]
)
self._draft_block_end_buf: Optional[torch.Tensor] = None # [cap_bs]
self._draft_seq_lens_cpu_buf: Optional[torch.Tensor] = None # [cap_bs] on CPU
self._draft_block_spec_info = make_draft_block_spec_info(
draft_token_num=int(self.block_size), device=self.device
)
self._draft_greedy_gathered_max_buf: Optional[torch.Tensor] = None
self._draft_greedy_gathered_ids_buf: Optional[torch.Tensor] = None
self._draft_greedy_gather_cap: int = 0
self._draft_greedy_local_max_buf: Optional[torch.Tensor] = None
self._draft_greedy_local_arg_buf: Optional[torch.Tensor] = None
self._draft_greedy_local_cap: int = 0
self._draft_greedy_best_rank_buf: Optional[torch.Tensor] = None
self._draft_greedy_rank_index_buf: Optional[torch.Tensor] = None
self._draft_greedy_selected_ids_buf: Optional[torch.Tensor] = None
self._draft_greedy_index_cap: int = 0
self._use_fused_kv_materialize = is_cuda() or is_hip()
self._fused_kv_helper: Optional[object] = None
if self._use_fused_kv_materialize:
self._init_fused_kv_helper()
supports_gpu_triton = is_cuda() or is_hip()
self._use_triton_prepare_block = supports_gpu_triton
self._use_triton_accept_bonus = supports_gpu_triton
self._accept_bonus_buffer_cap: int = 0
self._accept_bonus_buffer_slot: int = 0
self._accept_len_buf: Optional[torch.Tensor] = None
self._commit_lens_bufs: List[torch.Tensor] = []
self._bonus_id_bufs: List[torch.Tensor] = []
self._out_tokens_bufs: List[torch.Tensor] = []
self._new_seq_lens_bufs: List[torch.Tensor] = []
@property
def target_worker(self) -> TpModelWorker:
return self._target_worker
@property
def draft_worker(self):
# DFLASH drives the draft model through a plain TpModelWorker: the
# draft KV is materialized from target hidden states, so there is no
# EagleDraftWorkerBase draft/draft_extend split to wrap it in.
return self._draft_worker
@property
def spec_v2_attn_backends(self) -> tuple:
# Every attn backend a spec_v2 forward touches; consumed by
# decide_needs_cpu_seq_lens to gate the seq_lens_cpu D2H.
return (
self._target_worker.model_runner.attn_backend,
self.draft_model_runner.attn_backend,
)
def alloc_memory_pool(
self,
memory_pool_config=None,
req_to_token_pool=None,
token_to_kv_pool_allocator=None,
):
# Without draft windowing, the draft worker aliases the target
# request->token mapping and allocation state. With draft windowing
# enabled, the draft worker keeps a private compact req->token table
# over the same global KV index space, so radix-cache/prefix-hit KV
# remains reusable while draft attention sees only the recent window.
self._draft_worker.alloc_memory_pool(
memory_pool_config=memory_pool_config,
req_to_token_pool=(
None if self.use_compact_draft_cache else req_to_token_pool
),
token_to_kv_pool_allocator=token_to_kv_pool_allocator,
)
def init_attention_backends(self):
self._draft_worker.init_attention_backends()
self._need_mamba_verify_commit = (
self.model_runner.mambaish_config is not None
and hasattr(
self.model_runner.attn_backend,
"update_mamba_state_after_mtp_verify",
)
)
def init_cuda_graphs(self):
capture_decode_cuda_graph = (
self.server_args.cuda_graph_config.decode.backend != Backend.DISABLED
)
if is_cuda() and capture_decode_cuda_graph:
available_mem = get_available_gpu_memory(self.device, self.gpu_id)
if available_mem < 1.0:
capture_decode_cuda_graph = False
logger.warning(
"Disable DFLASH draft cuda graph because only %.2f GB GPU "
"memory is available after target backend initialization.",
available_mem,
)
if capture_decode_cuda_graph:
# Must run before capture so the draft graph folds the head in.
self._draft_sampler = self._maybe_build_draft_sampler()
if self._draft_sampler is not None:
self.draft_model_runner.capture_tail_hooks.append(
make_draft_sampler_capture_hook(self._draft_sampler)
)
self._draft_worker.init_cuda_graphs(
capture_decode_cuda_graph=capture_decode_cuda_graph
)
def _maybe_build_draft_sampler(self):
def _eager(reason):
if self.tp_rank == 0:
logger.info("DFLASH draft greedy head kept eager (reason=%s).", reason)
return None
if get_tp_group().world_size != 1:
return _eager("tp>1")
if self.block_size <= 1:
return _eager("block_size<=1")
target_model = self._target_worker.model_runner.model
lm_head = getattr(target_model, "lm_head", None)
if lm_head is None or not hasattr(lm_head, "weight"):
return _eager("no target lm_head")
if not torch.is_floating_point(lm_head.weight):
# Quantized lm_head (FP8/INT) would break the static matmul.
return _eager("quantized lm_head")
if not hasattr(lm_head, "shard_indices"):
num_org = int(lm_head.weight.shape[0])
org_vocab_start = 0
else:
shard = lm_head.shard_indices
if int(shard.num_added_elements) != 0:
return _eager("added vocab")
num_org = int(shard.num_org_elements)
org_vocab_start = int(shard.org_vocab_start_index)
if self.tp_rank == 0:
logger.info("DFLASH draft greedy head folded into the draft cuda graph.")
return _DflashDraftSampler(
weight=lm_head.weight,
block_size=self.block_size,
num_org=num_org,
org_vocab_start=org_vocab_start,
max_bs=max(self.server_args.cuda_graph_config.decode.bs),
)
def _init_fused_kv_helper(self) -> None:
"""Initialize the fused KV materialization helper with pre-stacked weights."""
try:
layers = self.draft_model.layers
fused_disable_reason: Optional[str] = None
if len(layers) == 0:
fused_disable_reason = "no layers found"
elif not getattr(self.draft_model, "supports_fused_context_kv", True):
fused_disable_reason = "draft model does not support fused context KV"
if fused_disable_reason is not None:
if self.tp_rank == 0:
logger.info(
"DFLASH fused KV materialization disabled: %s",
fused_disable_reason,
)
self._use_fused_kv_materialize = False
self._fused_kv_helper = None
return
for layer_idx, layer in enumerate(layers):
attn = layer.self_attn
eligible, reason = can_dflash_use_fused_qkv_proj(attn.qkv_proj)
if not eligible:
fused_disable_reason = f"{reason}: layer={layer_idx}"
break
# Keep semantics aligned with set_kv_buffer scaling behavior.
k_scale = getattr(attn.attn, "k_scale", None)
v_scale = getattr(attn.attn, "v_scale", None)
if k_scale is not None and not math.isclose(float(k_scale), 1.0):
fused_disable_reason = (
"non-unit k_scale is not supported for fused KV path: "
f"layer={layer_idx}, k_scale={k_scale}"
)
break
if v_scale is not None and not math.isclose(float(v_scale), 1.0):
fused_disable_reason = (
"non-unit v_scale is not supported for fused KV path: "
f"layer={layer_idx}, v_scale={v_scale}"
)
break
rope_is_neox_style = bool(
getattr(attn.rotary_emb, "is_neox_style", True)
)
if not rope_is_neox_style:
fused_disable_reason = (
"non-neox RoPE is not supported for fused KV path: "
f"layer={layer_idx}, rope_is_neox_style={rope_is_neox_style}"
)
break
if fused_disable_reason is not None:
if self.tp_rank == 0:
logger.info(
"DFLASH fused KV materialization disabled: %s",
fused_disable_reason,
)
self._use_fused_kv_materialize = False
self._fused_kv_helper = None
return
FusedKVMaterializeHelper = _get_fused_kv_materialize_helper()
first_attn = layers[0].self_attn
rotary_emb = first_attn.rotary_emb
self._fused_kv_helper = FusedKVMaterializeHelper(
layers=layers,
rotary_emb=rotary_emb,
num_kv_heads=first_attn.num_kv_heads,
head_dim=first_attn.head_dim,
device=self.device,
max_position_hint=self.target_worker.model_runner.model_config.context_len
+ int(self.block_size),
)
if self.tp_rank == 0:
logger.info(
"DFLASH fused KV materialization enabled. "
"n_layers=%d, num_kv_heads=%d, head_dim=%d",
len(layers),
first_attn.num_kv_heads,
first_attn.head_dim,
)
except Exception as e:
logger.warning(
"DFLASH fused KV initialization failed, falling back to sequential path: %s",
e,
)
self._use_fused_kv_materialize = False
self._fused_kv_helper = None
def _ensure_draft_block_buffers(self, bs: int) -> None:
cap = (
0
if self._draft_block_ids_buf is None
else int(self._draft_block_ids_buf.shape[0])
)
if cap >= int(bs):
return
new_cap = max(int(bs), cap * 2 if cap > 0 else int(bs))
device = self.device
block_size = int(self.block_size)
self._draft_block_ids_buf = torch.empty(
(new_cap, block_size), dtype=torch.long, device=device
)
self._draft_block_positions_buf = torch.empty(
(new_cap, block_size), dtype=torch.int64, device=device
)
self._draft_block_tokens_buf = torch.empty(
(new_cap, block_size), dtype=torch.long, device=device
)
self._draft_verify_out_cache_loc_buf = torch.empty(
(new_cap, block_size), dtype=torch.int64, device=device
)
self._draft_block_end_buf = torch.empty(
(new_cap,), dtype=torch.int32, device=device
)
self._draft_seq_lens_cpu_buf = torch.empty(
(new_cap,), dtype=torch.int32, device="cpu"
)
def __getattr__(self, name):
# Delegate anything not implemented yet to the target worker. Guard
# the backing field so a lookup before __init__ sets it raises
# AttributeError instead of recursing through the property.
if name == "_target_worker":
raise AttributeError(name)
return getattr(self.target_worker, name)
def clear_cache_pool(self):
# The target worker owns the shared KV allocator/cache. For the compact
# sliding-window path, the draft req->token view is rebuilt from committed
# target state before each draft forward, so there is nothing persistent
# to flush here.
pass
def _gather_req_to_token_masked(
self,
*,
req_to_token: torch.Tensor,
req_pool_indices: torch.Tensor,
pos2d: torch.Tensor,
mask: torch.Tensor,
context: str,
) -> torch.Tensor:
if pos2d.ndim != 2:
raise RuntimeError(
f"{context} expected 2D positions, got shape={tuple(pos2d.shape)}."
)
if mask.shape != pos2d.shape:
raise RuntimeError(
f"{context} mask/position shape mismatch: {tuple(mask.shape)} vs {tuple(pos2d.shape)}."
)
if req_pool_indices.dtype != torch.int64:
req_pool_indices = req_pool_indices.to(torch.int64)
if mask.dtype != torch.bool:
mask = mask.to(torch.bool)
table_width = int(req_to_token.shape[1])
if table_width <= 0:
if bool(mask.any().item()):
raise RuntimeError(
f"{context} req_to_token table is empty but gather mask is non-empty."
)
return torch.empty((0,), dtype=torch.int64, device=self.device)
# Only the masked-off rectangular padding can be out of range in the normal
# ragged-batch case. Replace those don't-care columns with a valid in-range
# position before the gather so the kernel only sees real positions.
safe_pos2d = pos2d.masked_fill(~mask, 0)
return req_to_token[req_pool_indices[:, None], safe_pos2d][mask].to(torch.int64)
def _gather_req_to_token_segments(
self,
*,
req_to_token: torch.Tensor,
req_pool_indices: torch.Tensor,
start: torch.Tensor | None,
lengths: torch.Tensor,
) -> torch.Tensor:
lengths = lengths.to(torch.int64)
if lengths.numel() == 0:
return torch.empty((0,), dtype=torch.int64, device=self.device)
max_len = int(lengths.max().item())
if max_len <= 0:
return torch.empty((0,), dtype=torch.int64, device=self.device)
if req_pool_indices.dtype != torch.int64:
req_pool_indices = req_pool_indices.to(torch.int64)
offsets = torch.arange(
max_len, device=self.device, dtype=torch.int64
).unsqueeze(0)
if start is None:
pos2d = offsets.expand(req_pool_indices.shape[0], -1)
else:
pos2d = start.to(torch.int64).unsqueeze(1) + offsets
mask = offsets < lengths.unsqueeze(1)
return self._gather_req_to_token_masked(
req_to_token=req_to_token,
req_pool_indices=req_pool_indices,
pos2d=pos2d,
mask=mask,
context="DFLASH req_to_token segment gather",
)
def _compute_compact_draft_seq_lens(self, seq_lens: torch.Tensor) -> torch.Tensor:
assert self.draft_window_size is not None
visible_lens = torch.clamp(
seq_lens.to(dtype=torch.int32, device=self.device),
max=int(self.draft_window_size),
)
if self.page_size <= 1:
return visible_lens
# Paged FA backends derive the page table from local token positions, so the
# compact suffix must start on a page boundary. Keep up to page_size - 1 extra
# tokens on the left to preserve valid local page structure.
seq_lens_i64 = seq_lens.to(torch.int64)
visible_lens_i64 = visible_lens.to(torch.int64)
visible_start = seq_lens_i64 - visible_lens_i64
aligned_start = visible_start - torch.remainder(visible_start, self.page_size)
return (seq_lens_i64 - aligned_start).to(torch.int32)
def _resolve_mask_token_id(
self, *, mask_token: str, mask_token_id: Optional[int] = None
) -> int:
if not isinstance(mask_token, str) or not mask_token:
raise ValueError(
f"DFLASH mask_token must be a non-empty string, got {mask_token!r}."
)
vocab_size = int(self.target_worker.model_runner.model_config.vocab_size)
if mask_token_id is not None:
resolved_id = int(mask_token_id)
if resolved_id >= vocab_size:
raise ValueError(
"DFLASH mask_token_id is outside the target vocab size. "
f"mask_token_id={resolved_id}, vocab_size={vocab_size}. "
f"This likely means mask_token={mask_token!r} requires vocab expansion beyond the model's embedding size. "
"SGLang does not support resizing target embeddings for DFLASH yet."
)
tokenizer = getattr(self.target_worker, "tokenizer", None)
if tokenizer is not None:
token_id_from_vocab = tokenizer.get_vocab().get(mask_token, None)
if (
token_id_from_vocab is not None
and int(token_id_from_vocab) != resolved_id
):
raise ValueError(
"DFLASH config mismatch: dflash_config.mask_token_id conflicts with tokenizer vocab id "
f"for dflash_config.mask_token. mask_token={mask_token!r}, "
f"mask_token_id={resolved_id}, tokenizer_vocab_id={int(token_id_from_vocab)}."
)
return resolved_id
tokenizer = getattr(self.target_worker, "tokenizer", None)
if tokenizer is None:
raise RuntimeError(
"DFLASH requires tokenizer initialization when dflash_config.mask_token_id is not set "
"(skip_tokenizer_init is not supported in this mode)."
)
resolved_id = None
if getattr(tokenizer, "mask_token", None) == mask_token:
resolved_id = getattr(tokenizer, "mask_token_id", None)
if resolved_id is None:
# Prefer checking the explicit vocab mapping first.
vocab = tokenizer.get_vocab()
resolved_id = vocab.get(mask_token, None)
if resolved_id is None:
# Mirror the reference DFlash HF demo by adding the mask token to the tokenizer.
# This is safe only when the resulting id stays within the target model vocab size.
added = tokenizer.add_special_tokens({"mask_token": mask_token})
resolved_id = getattr(tokenizer, "mask_token_id", None)
if resolved_id is None:
resolved_id = tokenizer.convert_tokens_to_ids(mask_token)
if added and self.tp_rank == 0:
logger.info(
"Added DFLASH mask token to tokenizer. token=%s, mask_token_id=%s, tokenizer_len=%s, model_vocab_size=%s",
mask_token,
resolved_id,
len(tokenizer),
vocab_size,
)
if resolved_id is None or int(resolved_id) < 0:
raise ValueError(
"DFLASH requires resolving a mask token id, but it could not be resolved. "
f"mask_token={mask_token!r}."
)
if resolved_id >= vocab_size:
raise ValueError(
"DFLASH mask_token_id is outside the target vocab size. "
f"mask_token_id={resolved_id}, vocab_size={vocab_size}. "
f"This likely means mask_token={mask_token!r} requires vocab expansion beyond the model's embedding size. "
"SGLang does not support resizing target embeddings for DFLASH yet."
)
return int(resolved_id)
def _greedy_sample_from_vocab_parallel_head(
self,
*,
hidden_states: torch.Tensor,
lm_head,
chunk_size: int = 256,
) -> torch.Tensor:
"""Greedy argmax over the target LM head in a TP-safe way.
We cannot materialize full logits for large vocabularies efficiently, and with
TP>1 each rank only owns a shard of the LM head weight. This computes the
per-rank max, gathers candidates across TP ranks, and selects the global max.
"""
if hidden_states.numel() == 0:
return torch.empty((0,), dtype=torch.long, device=hidden_states.device)
weight = lm_head.weight # [local_vocab_padded, hidden]
weight_dtype = weight.dtype
num_tokens = int(hidden_states.shape[0])
out_tokens = torch.empty(
(num_tokens,), dtype=torch.long, device=hidden_states.device
)
def _cast_hs(x: torch.Tensor) -> torch.Tensor:
return x if x.dtype == weight_dtype else x.to(weight_dtype)
if not hasattr(lm_head, "shard_indices"):
for start in range(0, num_tokens, int(chunk_size)):
end = min(num_tokens, start + int(chunk_size))
hs = _cast_hs(hidden_states[start:end])
logits = torch.matmul(hs, weight.T)
out_tokens[start:end] = torch.argmax(logits, dim=-1).to(torch.long)
return out_tokens
shard = lm_head.shard_indices
tp_group = get_tp_group()
tp_size = int(tp_group.world_size)
# Valid ranges in the local shard (excluding padding):
# base vocab: [0, num_org)
# added vocab: [num_org_padded, num_org_padded + num_added)
num_org = int(shard.num_org_elements)
num_org_padded = int(shard.num_org_elements_padded)
num_added = int(shard.num_added_elements)
org_vocab_start = int(shard.org_vocab_start_index)
added_vocab_start = int(shard.added_vocab_start_index)
def _ensure_local_reduce_buffers(
chunk_len: int,
value_dtype: torch.dtype,
device: torch.device,
) -> tuple[torch.Tensor, torch.Tensor]:
if (
self._draft_greedy_local_cap < chunk_len
or self._draft_greedy_local_max_buf is None
or self._draft_greedy_local_arg_buf is None
or self._draft_greedy_local_max_buf.dtype != value_dtype
or self._draft_greedy_local_max_buf.device != device
or self._draft_greedy_local_arg_buf.device != device
):
cap = max(int(chunk_size), chunk_len)
self._draft_greedy_local_max_buf = torch.empty(
(cap,), dtype=value_dtype, device=device
)
self._draft_greedy_local_arg_buf = torch.empty(
(cap,), dtype=torch.int64, device=device
)
self._draft_greedy_local_cap = cap
return (
self._draft_greedy_local_max_buf[:chunk_len],
self._draft_greedy_local_arg_buf[:chunk_len],
)
# Fast path (common): single-rank greedy sampling over the base vocab shard.
# Avoids extra max/id bookkeeping that is only needed for TP sync or added vocab.
#
# DFLASH draft sampling only materializes a small fixed block of hidden states
# each step. On tp=1, splitting those states into many 256-token chunks adds
# extra matmul/argmax launches without reducing peak memory meaningfully.
if tp_size == 1 and num_added == 0:
fast_chunk_size = max(int(chunk_size), 1024)
for start in range(0, num_tokens, fast_chunk_size):
end = min(num_tokens, start + fast_chunk_size)
hs = _cast_hs(hidden_states[start:end])
if num_org > 0:
base_logits = torch.matmul(hs, weight[:num_org].T)
local_max, local_arg = _ensure_local_reduce_buffers(
end - start, base_logits.dtype, hs.device
)
torch.max(base_logits, dim=-1, out=(local_max, local_arg))
out_tokens[start:end].copy_(local_arg)
out_tokens[start:end].add_(org_vocab_start)
else:
out_tokens[start:end] = 0
return out_tokens
for start in range(0, num_tokens, int(chunk_size)):
end = min(num_tokens, start + int(chunk_size))
hs = _cast_hs(hidden_states[start:end])
chunk_len = int(hs.shape[0])
# Base vocab logits.
if num_org > 0:
base_logits = torch.matmul(hs, weight[:num_org].T)
local_max, local_arg = _ensure_local_reduce_buffers(
chunk_len, base_logits.dtype, hs.device
)
torch.max(base_logits, dim=-1, out=(local_max, local_arg))
else:
local_max = torch.full(
(chunk_len,),
torch.finfo(weight_dtype).min,
dtype=weight_dtype,
device=hs.device,
)
local_arg = torch.zeros(
(chunk_len,), dtype=torch.int64, device=hs.device
)
# Added vocab logits (e.g., LoRA-added embeddings), if present.
if num_added > 0:
added_slice_start = num_org_padded
added_slice_end = num_org_padded + num_added
added_logits = torch.matmul(
hs, weight[added_slice_start:added_slice_end].T
)
added_max, added_arg = torch.max(added_logits, dim=-1)
use_added = added_max > local_max
local_max = torch.where(use_added, added_max, local_max)
# For base/added conversion below, keep local_arg expressed in the full local
# weight index space (base + padding + added), matching `lm_head.weight`.
local_arg = torch.where(
use_added, added_arg.to(local_arg.dtype) + num_org_padded, local_arg
)
# Convert local argmax indices to global token ids.
if num_added == 0:
local_arg.add_(org_vocab_start)
global_ids = local_arg
else:
global_ids = torch.empty(
(chunk_len,), dtype=torch.int64, device=hs.device
)
is_base = local_arg < num_org
global_ids[is_base] = org_vocab_start + local_arg[is_base]
global_ids[~is_base] = added_vocab_start + (
local_arg[~is_base] - num_org_padded
)
if tp_size == 1:
out_tokens[start:end] = global_ids.to(torch.long)
continue
# Gather per-rank maxima and associated global ids, then select the global max.
needed = tp_size * chunk_len
chunk_cap = int(chunk_size)
if (
self._draft_greedy_gather_cap < needed
or self._draft_greedy_gathered_max_buf is None
or self._draft_greedy_gathered_ids_buf is None
or self._draft_greedy_gathered_max_buf.dtype != local_max.dtype
or self._draft_greedy_gathered_max_buf.device != hs.device
):
# Allocate enough space for the max chunk size to avoid reallocations.
cap = tp_size * chunk_cap
self._draft_greedy_gathered_max_buf = torch.empty(
(cap,), dtype=local_max.dtype, device=hs.device
)
self._draft_greedy_gathered_ids_buf = torch.empty(
(cap,), dtype=global_ids.dtype, device=hs.device
)
self._draft_greedy_gather_cap = cap
if (
self._draft_greedy_index_cap < chunk_len
or self._draft_greedy_best_rank_buf is None
or self._draft_greedy_rank_index_buf is None
or self._draft_greedy_selected_ids_buf is None
or self._draft_greedy_best_rank_buf.device != hs.device
or self._draft_greedy_selected_ids_buf.device != hs.device
):
self._draft_greedy_best_rank_buf = torch.empty(
(chunk_cap,), dtype=torch.int64, device=hs.device
)
self._draft_greedy_rank_index_buf = torch.empty(
(1, chunk_cap), dtype=torch.int64, device=hs.device
)
self._draft_greedy_selected_ids_buf = torch.empty(
(1, chunk_cap), dtype=torch.int64, device=hs.device
)
self._draft_greedy_index_cap = chunk_cap
gathered_max = self._draft_greedy_gathered_max_buf[:needed]
gathered_ids = self._draft_greedy_gathered_ids_buf[:needed]
tp_group.all_gather_into_tensor(gathered_max, local_max.contiguous())
tp_group.all_gather_into_tensor(gathered_ids, global_ids.contiguous())
gathered_max = gathered_max.view(tp_size, chunk_len)
gathered_ids = gathered_ids.view(tp_size, chunk_len)
best_rank = self._draft_greedy_best_rank_buf[:chunk_len]
torch.argmax(gathered_max, dim=0, out=best_rank)
rank_index = self._draft_greedy_rank_index_buf[:, :chunk_len]
rank_index[0].copy_(best_rank)
selected_ids = self._draft_greedy_selected_ids_buf[:, :chunk_len]
torch.gather(gathered_ids, 0, rank_index, out=selected_ids)
out_tokens[start:end].copy_(selected_ids.view(-1))
return out_tokens
def _append_target_hidden_to_draft_kv_by_loc(
self,
*,
target_hidden: torch.Tensor,
cache_loc: torch.Tensor,
positions: torch.Tensor,
cache_loc_2d: Optional[torch.Tensor] = None,
commit_lens: Optional[torch.Tensor] = None,
) -> None:
"""Materialize target context features into the draft KV cache at explicit slots.
For the spec-v2 overlap path, callers can pass dense `[bs, block_size]`
`cache_loc_2d` plus `commit_lens`; the prefix-valid writer then commits
only the live prefix rows without constructing masked/packed index tensors.
"""
if target_hidden is None:
raise RuntimeError("DFLASH missing target hidden context features.")
if target_hidden.numel() == 0:
return
if target_hidden.ndim != 2:
raise ValueError(
"DFLASH target_hidden must be 2D, "
f"got shape={tuple(target_hidden.shape)}."
)
if cache_loc.ndim != 1:
raise ValueError(
f"DFLASH cache_loc must be 1D, got shape={tuple(cache_loc.shape)}."
)
if positions.ndim != 1:
raise ValueError(
f"DFLASH positions must be 1D, got shape={tuple(positions.shape)}."
)
num_tokens = int(target_hidden.shape[0])
if int(cache_loc.numel()) != num_tokens:
raise ValueError(
"DFLASH cache_loc length mismatch: "
f"cache_loc={int(cache_loc.numel())}, target_hidden={num_tokens}."
)
if int(positions.numel()) != num_tokens:
raise ValueError(
"DFLASH positions length mismatch: "
f"positions={int(positions.numel())}, target_hidden={num_tokens}."
)
if cache_loc_2d is not None:
if cache_loc_2d.ndim != 2:
raise ValueError(
"DFLASH cache_loc_2d must be 2D, "
f"got shape={tuple(cache_loc_2d.shape)}."
)
if int(cache_loc_2d.numel()) != num_tokens:
raise ValueError(
"DFLASH cache_loc_2d size mismatch: "
f"cache_loc_2d={int(cache_loc_2d.numel())}, target_hidden={num_tokens}."
)
if commit_lens is None:
raise ValueError(
"DFLASH cache_loc_2d requires commit_lens for prefix-valid writes."
)
device = self.model_runner.device
if cache_loc.device != device:
cache_loc = cache_loc.to(device, non_blocking=True)
if positions.device != device:
positions = positions.to(device, non_blocking=True)
if target_hidden.device != device:
target_hidden = target_hidden.to(device, non_blocking=True)
if cache_loc.dtype != torch.int64:
cache_loc = cache_loc.to(torch.int64)
if positions.dtype != torch.int64:
positions = positions.to(torch.int64)
if cache_loc_2d is not None:
if cache_loc_2d.device != device:
cache_loc_2d = cache_loc_2d.to(device, non_blocking=True)
if cache_loc_2d.dtype != torch.int64:
cache_loc_2d = cache_loc_2d.to(torch.int64)
if commit_lens is not None:
if commit_lens.device != device:
commit_lens = commit_lens.to(device, non_blocking=True)
if commit_lens.dtype != torch.int32:
commit_lens = commit_lens.to(torch.int32)
with torch.inference_mode():
ctx_hidden = self.draft_model.project_target_hidden(target_hidden)
if cache_loc_2d is not None:
bs = int(commit_lens.shape[0])
if int(cache_loc_2d.shape[0]) != bs:
raise ValueError(
"DFLASH cache_loc_2d batch size mismatch: "
f"cache_loc_2d={tuple(cache_loc_2d.shape)}, commit_lens={tuple(commit_lens.shape)}."
)
if bs == 0:
return
if self._use_fused_kv_materialize and self._fused_kv_helper is not None:
try:
self._append_target_hidden_fused(
ctx_hidden=ctx_hidden,
ctx_positions=positions,
ctx_cache_loc=cache_loc,
ctx_cache_loc_2d=cache_loc_2d,
commit_lens=commit_lens,
)
return
except Exception as e:
logger.warning(
"DFLASH fused prefix-direct KV append failed; falling back to the per-layer prefix-direct path: %s",
e,
)
self._use_fused_kv_materialize = False
self._fused_kv_helper = None
for layer in self.draft_model.layers:
attn = layer.self_attn
layer_ctx_hidden = self.draft_model.prepare_context_hidden_for_kv(
layer, ctx_hidden
)
k, v = attn.kv_proj_only(layer_ctx_hidden)
k = attn.apply_k_norm(k)
k = attn.apply_k_rope(positions, k)
k = k.view(-1, attn.num_kv_heads, attn.head_dim)
v = v.view(-1, attn.num_kv_heads, attn.head_dim)
self.draft_model_runner.token_to_kv_pool.set_kv_buffer_prefix_valid(
attn.attn,
cache_loc_2d,
commit_lens,
k,
v,
attn.attn.k_scale,
attn.attn.v_scale,
)
return
if self._use_fused_kv_materialize and self._fused_kv_helper is not None:
try:
self._append_target_hidden_fused(
ctx_hidden=ctx_hidden,
ctx_positions=positions,
ctx_cache_loc=cache_loc,
)
return
except Exception as e:
logger.warning(
"DFLASH fused KV append-by-loc failed; falling back to sequential path: %s",
e,
)
self._use_fused_kv_materialize = False
self._fused_kv_helper = None
self._append_target_hidden_sequential(
ctx_hidden=ctx_hidden,
ctx_positions=positions,
ctx_cache_loc=cache_loc,
)
def _append_target_hidden_sequential(
self,
ctx_hidden: torch.Tensor,
ctx_positions: torch.Tensor,
ctx_cache_loc: torch.Tensor,
) -> None:
for layer in self.draft_model.layers:
attn = layer.self_attn
layer_ctx_hidden = self.draft_model.prepare_context_hidden_for_kv(
layer, ctx_hidden
)
if _is_npu:
_, k, v = attn.forward_prepare_npu(ctx_positions, layer_ctx_hidden)
else:
k, v = attn.kv_proj_only(layer_ctx_hidden)
k = attn.apply_k_norm(k)
k = attn.apply_k_rope(ctx_positions, k)
k = k.view(-1, attn.num_kv_heads, attn.head_dim)
v = v.view(-1, attn.num_kv_heads, attn.head_dim)
self.draft_model_runner.token_to_kv_pool.set_kv_buffer(
attn.attn,
ctx_cache_loc,
k,
v,
attn.attn.k_scale,
attn.attn.v_scale,
)
def _append_target_hidden_fused(
self,
ctx_hidden: torch.Tensor,
ctx_positions: torch.Tensor,
ctx_cache_loc: torch.Tensor,
ctx_cache_loc_2d: Optional[torch.Tensor] = None,
commit_lens: Optional[torch.Tensor] = None,
) -> None:
"""Fused KV materialization using batched projection + Triton kernel."""
token_to_kv_pool = self.draft_model_runner.token_to_kv_pool
if self._fused_kv_helper is None:
raise RuntimeError("DFLASH fused KV helper is not initialized.")
def _write_layer_kv(
layer_idx: int,
cache_k: torch.Tensor,
cache_v: torch.Tensor,
) -> None:
attn = self.draft_model.layers[layer_idx].self_attn.attn
if ctx_cache_loc_2d is not None and commit_lens is not None:
token_to_kv_pool.set_kv_buffer_prefix_valid(
attn,
ctx_cache_loc_2d,
commit_lens,
cache_k,
cache_v,
attn.k_scale,
attn.v_scale,
)
else:
token_to_kv_pool.set_kv_buffer(
attn,
ctx_cache_loc,
cache_k,
cache_v,
attn.k_scale,
attn.v_scale,
)
self._fused_kv_helper.materialize(
ctx_hidden=ctx_hidden,
positions=ctx_positions,
write_layer_kv=_write_layer_kv,
)
def _update_target_mamba_state_after_verify(
self,
*,
batch: ScheduleBatch,
seq_lens_pre_verify: torch.Tensor,
commit_lens: torch.Tensor,
) -> None:
"""Commit Mamba intermediate states for accepted verify steps.
During TARGET_VERIFY, Mamba kernels run with `disable_state_update=True` and
cache per-step intermediate states. After acceptance, we need to commit the
state corresponding to each request's last accepted step.
"""
if not self._need_mamba_verify_commit:
return
attn_backend = self.target_worker.model_runner.attn_backend
last_correct_step_indices = commit_lens.to(torch.int64) - 1
mamba_steps_to_track = None
if batch.mamba_track_indices is not None:
mamba_track_interval = self.server_args.mamba_track_interval
to_track_mask = (
seq_lens_pre_verify // mamba_track_interval
!= batch.seq_lens // mamba_track_interval
)
tracking_point = (
batch.seq_lens // mamba_track_interval * mamba_track_interval
)
to_track_ith = torch.clamp(tracking_point - seq_lens_pre_verify - 1, min=0)
can_track_mask = to_track_mask & (
to_track_ith < commit_lens.to(to_track_ith.dtype)
)
mamba_steps_to_track = torch.where(
can_track_mask,
to_track_ith.to(torch.int64),
torch.full_like(to_track_ith, -1, dtype=torch.int64),
)
attn_backend.update_mamba_state_after_mtp_verify(
last_correct_step_indices=last_correct_step_indices,
mamba_track_indices=batch.mamba_track_indices,
mamba_steps_to_track=mamba_steps_to_track,
model=self.target_worker.model_runner.model,
)
def _ensure_accept_bonus_buffers(self, bs: int) -> None:
if self._accept_bonus_buffer_cap >= int(bs):
return
new_cap = max(
int(bs),
(
self._accept_bonus_buffer_cap * 2
if self._accept_bonus_buffer_cap > 0
else int(bs)
),
)
device = self.device
block_size = int(self.block_size)
self._accept_len_buf = torch.empty((new_cap,), dtype=torch.int32, device=device)
self._commit_lens_bufs = [
torch.empty((new_cap,), dtype=torch.int32, device=device) for _ in range(2)
]
self._bonus_id_bufs = [
torch.empty((new_cap,), dtype=torch.int32, device=device) for _ in range(2)
]
self._out_tokens_bufs = [
torch.empty((new_cap, block_size), dtype=torch.int64, device=device)
for _ in range(2)
]
self._new_seq_lens_bufs = [
torch.empty((new_cap,), dtype=torch.int64, device=device) for _ in range(2)
]
self._accept_bonus_buffer_cap = new_cap
def _next_accept_bonus_buffers(self, bs: int) -> tuple[
torch.Tensor,
torch.Tensor,
torch.Tensor,
torch.Tensor,
torch.Tensor,
]:
self._ensure_accept_bonus_buffers(bs)
assert self._accept_len_buf is not None
slot = self._accept_bonus_buffer_slot
self._accept_bonus_buffer_slot = (slot + 1) % 2
return (
self._accept_len_buf[:bs],
self._commit_lens_bufs[slot][:bs],
self._bonus_id_bufs[slot][:bs],
self._out_tokens_bufs[slot][:bs],
self._new_seq_lens_bufs[slot][:bs],
)
def _validate_phase1_sampling_support(self, batch: ScheduleBatch) -> None:
sampling_info = batch.sampling_info
if sampling_info is None or sampling_info.is_all_greedy:
return
if (
not is_dflash_sampling_verify_available()
and not self._warned_sampling_fallback
and self.tp_rank == 0
):
logger.warning(
"DFLASH non-greedy verification is unavailable on this build/device; "
"falling back to greedy argmax verification."
)
self._warned_sampling_fallback = True
def _make_next_draft_input_prefill(
self,
*,
bonus_tokens: torch.Tensor,
seq_lens: torch.Tensor,
) -> DFlashDraftInputV2:
return make_draft_input_v2(bonus_tokens=bonus_tokens, new_seq_lens=seq_lens)
def _make_next_draft_input_decode(
self,
*,
bonus_tokens: torch.Tensor,
new_seq_lens: torch.Tensor,
) -> DFlashDraftInputV2:
return make_draft_input_v2(bonus_tokens=bonus_tokens, new_seq_lens=new_seq_lens)
def forward_batch_generation(
self,
batch: ScheduleBatch,
on_publish=None,
) -> GenerationBatchResult:
if getattr(batch, "return_logprob", False):
raise ValueError(
"DFLASH speculative decoding does not support return_logprob yet."
)
self._validate_phase1_sampling_support(batch)
if batch.forward_mode.is_extend() or batch.is_extend_in_batch:
# Target prefill: capture DFlash aux hidden states for prompt tokens.
batch.capture_hidden_mode = CaptureHiddenMode.FULL
batch_output = self.target_worker.forward_batch_generation(batch)
logits_output, next_token_ids = (
batch_output.logits_output,
batch_output.next_token_ids,
)
batch_output.new_seq_lens = batch.seq_lens
if on_publish is not None:
on_publish(batch_output.new_seq_lens)
if logits_output.hidden_states is None:
raise RuntimeError(
"DFLASH requires target aux hidden capture for prefill, but got None. "
"Make sure the target model has DFlash layers-to-capture configured."
)
if batch.extend_lens is None or batch.prefix_lens is None:
raise RuntimeError(
"DFLASH expected extend_lens / prefix_lens to be populated in extend mode, "
"but got None."
)
# Materialize prompt tokens into the draft KV cache immediately. This is required
# for radix cache safety (the scheduler may update radix after prefill returns).
device = next_token_ids.device
ctx_lens = torch.tensor(batch.extend_lens, dtype=torch.int32, device=device)
draft_seq_lens = torch.tensor(
batch.prefix_lens, dtype=torch.int32, device=device
)
if batch.out_cache_loc is None:
raise RuntimeError(
"DFLASH prefill expected out_cache_loc, but got None."
)
positions, _ = compute_position(
self.model_runner.server_args.attention_backend,
draft_seq_lens,
ctx_lens,
int(sum(batch.extend_lens)),
)
self._append_target_hidden_to_draft_kv_by_loc(
target_hidden=logits_output.hidden_states,
cache_loc=batch.out_cache_loc,
positions=positions,
)
# Avoid copying large hidden-state buffers to CPU in overlap scheduling.
logits_output.hidden_states = None
batch_output.next_draft_input = self._make_next_draft_input_prefill(
bonus_tokens=next_token_ids,
seq_lens=batch.seq_lens,
)
return batch_output
# Decode / target-verify stage.
if batch.spec_info is None:
batch.spec_info = DFlashDraftInputV2.create_idle_input(device=self.device)
draft_input = batch.spec_info
if not isinstance(draft_input, DFlashDraftInputV2):
raise RuntimeError(
"DFLASH spec-v2 expected DFlashDraftInputV2 state on the running batch."
)
if batch.forward_mode.is_idle():
empty_ids = torch.empty((0,), dtype=torch.int64, device=self.device)
empty_lens = torch.empty((0,), dtype=torch.int32, device=self.device)
next_draft_input = self._make_next_draft_input_decode(
bonus_tokens=torch.empty((0,), device=self.device, dtype=torch.int64),
new_seq_lens=torch.empty((0,), device=self.device, dtype=torch.int64),
)
if on_publish is not None:
on_publish(next_draft_input.new_seq_lens)
return GenerationBatchResult(
logits_output=None,
next_token_ids=empty_ids,
accept_lens=empty_lens,
next_draft_input=next_draft_input,
can_run_cuda_graph=False,
speculative_num_draft_tokens=int(self.block_size),
new_seq_lens=next_draft_input.new_seq_lens,
)
# `seq_lens` is carried over from the previous overlap iteration and may have been
# produced on another stream.
batch.seq_lens.record_stream(
torch.get_device_module(self.device).current_stream()
)
bs = len(batch.seq_lens)
device = self.device
# --- 1) Draft a fixed block with the draft model.
target_model = self.target_worker.model_runner.model
embed_module = target_model.get_input_embeddings()
lm_head = getattr(target_model, "lm_head", None)
if lm_head is None or not hasattr(lm_head, "weight"):
raise RuntimeError(
"DFLASH requires the target model to expose `lm_head` with `weight`."
)
block_size = int(self.block_size)
self._ensure_draft_block_buffers(bs)
assert self._draft_block_ids_buf is not None
assert self._draft_block_positions_buf is not None
assert self._draft_block_tokens_buf is not None
assert self._draft_verify_out_cache_loc_buf is not None
assert self._draft_block_end_buf is not None
assert self._draft_seq_lens_cpu_buf is not None
block_ids = self._draft_block_ids_buf[:bs]
prefix_lens = batch.seq_lens
positions_2d = self._draft_block_positions_buf[:bs]
verify_out_cache_loc_2d = self._draft_verify_out_cache_loc_buf[:bs]
if self._use_triton_prepare_block:
try:
_prepare_dflash_draft_block_unchecked(
bonus_tokens=draft_input.bonus_tokens.view(-1),
prefix_lens=prefix_lens.view(-1),
req_pool_indices=batch.req_pool_indices.view(-1),
req_to_token=self.model_runner.req_to_token_pool.req_to_token,
block_ids_out=block_ids,
positions_out=positions_2d,
cache_loc_out=verify_out_cache_loc_2d,
mask_token_id=int(self._mask_token_id),
)
except Exception as e:
self._use_triton_prepare_block = False
logger.warning(
"DFLASH Triton prepare_block failed; falling back to eager path: %s",
e,
)
block_ids.fill_(int(self._mask_token_id))
block_ids[:, 0].copy_(draft_input.bonus_tokens)
torch.add(
prefix_lens.unsqueeze(1),
self._block_pos_offsets,
out=positions_2d,
)
end_offset = prefix_lens + block_size
verify_out_cache_loc = assign_extend_cache_locs_func(
req_pool_indices=batch.req_pool_indices,
req_to_token=self.model_runner.req_to_token_pool.req_to_token,
start_offset=prefix_lens,
end_offset=end_offset,
batch_size=bs,
draft_token_num=block_size,
device=device,
)
verify_out_cache_loc_2d.copy_(verify_out_cache_loc.view(bs, block_size))
else:
block_ids.fill_(int(self._mask_token_id))
block_ids[:, 0].copy_(draft_input.bonus_tokens)
torch.add(
prefix_lens.unsqueeze(1),
self._block_pos_offsets,
out=positions_2d,
)
end_offset = prefix_lens + block_size
verify_out_cache_loc = assign_extend_cache_locs_func(
req_pool_indices=batch.req_pool_indices,
req_to_token=self.model_runner.req_to_token_pool.req_to_token,
start_offset=prefix_lens,
end_offset=end_offset,
batch_size=bs,
draft_token_num=block_size,
device=device,
)
verify_out_cache_loc_2d.copy_(verify_out_cache_loc.view(bs, block_size))
noise_embedding = embed_module(block_ids)
input_embeds = noise_embedding.view(-1, noise_embedding.shape[-1])
positions = positions_2d.reshape(-1)
verify_out_cache_loc = verify_out_cache_loc_2d.reshape(-1)
seq_lens_cpu = self._draft_seq_lens_cpu_buf[:bs]
if self.use_compact_draft_cache:
# Rebuild the draft-local sliding-window view from committed target state.
draft_prefix_lens = self._compute_compact_draft_seq_lens(prefix_lens)
seq_lens_cpu.copy_(draft_prefix_lens.to(device="cpu", dtype=torch.int32))
suffix_start = prefix_lens.to(torch.int64) - draft_prefix_lens.to(
torch.int64
)
suffix_cache_loc = self._gather_req_to_token_segments(
req_to_token=self.model_runner.req_to_token_pool.req_to_token,
req_pool_indices=batch.req_pool_indices,
start=suffix_start,
lengths=draft_prefix_lens,
)
assign_req_to_token_pool_func(
batch.req_pool_indices,
self.draft_model_runner.req_to_token_pool.req_to_token,
torch.zeros_like(draft_prefix_lens),
draft_prefix_lens,
suffix_cache_loc,
bs,
)
block_end = self._draft_block_end_buf[:bs]
torch.add(draft_prefix_lens, block_size, out=block_end)
assign_req_to_token_pool_func(
batch.req_pool_indices,
self.draft_model_runner.req_to_token_pool.req_to_token,
draft_prefix_lens,
block_end,
verify_out_cache_loc,
bs,
)
draft_seq_lens = draft_prefix_lens
draft_seq_lens_sum = int(seq_lens_cpu.sum().item())
else:
# Non-windowed path uses the shared overallocated mapping directly.
# Backend planning only needs a safe upper bound for the committed
# prefix lengths, not the full allocator reservation length.
draft_seq_lens = prefix_lens
if batch.seq_lens_cpu is not None:
# Host bound = committed prefix + one verify block.
seq_lens_cpu.copy_(batch.seq_lens_cpu)
seq_lens_cpu.add_(block_size)
draft_seq_lens_sum = int(seq_lens_cpu.sum())
elif draft_input.reserved_seq_lens_cpu is not None:
# GPU-only backend: reserved is a safe over-estimate.
seq_lens_cpu.copy_(draft_input.reserved_seq_lens_cpu)
draft_seq_lens_sum = int(draft_input.reserved_seq_lens_sum)
else:
seq_lens_cpu.copy_(prefix_lens.to("cpu", dtype=torch.int32))
draft_seq_lens_sum = int(prefix_lens.sum().item())
forward_batch = ForwardBatch(
forward_mode=ForwardMode.TARGET_VERIFY,
batch_size=bs,
input_ids=block_ids.flatten(),
req_pool_indices=batch.req_pool_indices,
seq_lens=draft_seq_lens,
out_cache_loc=verify_out_cache_loc,
seq_lens_sum=draft_seq_lens_sum,
seq_lens_cpu=seq_lens_cpu,
positions=positions,
input_embeds=input_embeds,
spec_algorithm=SpeculativeAlgorithm.DFLASH,
spec_info=self._draft_block_spec_info,
capture_hidden_mode=CaptureHiddenMode.NULL,
)
with torch.inference_mode():
draft_out = self.draft_model_runner.forward(forward_batch)
draft_logits_output = draft_out.logits_output
if self._draft_sampler is not None and draft_out.can_run_graph:
draft_next = self._draft_sampler.out[
: bs * (int(self.block_size) - 1)
].view(bs, int(self.block_size) - 1)
else:
draft_hidden = draft_logits_output.hidden_states
if draft_hidden is None:
raise RuntimeError("DFLASH draft model returned no hidden states.")
draft_hidden = draft_hidden.view(bs, int(self.block_size), -1)
draft_next = self._greedy_sample_from_vocab_parallel_head(
hidden_states=draft_hidden[:, 1:, :].reshape(
-1, draft_hidden.shape[-1]
),
lm_head=lm_head,
).view(bs, int(self.block_size) - 1)
draft_tokens = self._draft_block_tokens_buf[:bs]
draft_tokens[:, 0].copy_(block_ids[:, 0])
draft_tokens[:, 1:].copy_(draft_next)
# --- 2) Target verify.
# TARGET_VERIFY uses standard causal masking; custom masks are unnecessary here.
custom_mask = None
verify_input_ids = draft_tokens.reshape(-1)
verify_input = DFlashVerifyInput(
draft_token=verify_input_ids,
positions=positions,
draft_token_num=int(self.block_size),
custom_mask=custom_mask,
capture_hidden_mode=CaptureHiddenMode.FULL,
)
batch.out_cache_loc = verify_out_cache_loc
sampling_info = batch.sampling_info
seq_lens_pre_verify = (
batch.seq_lens.clone() if self._need_mamba_verify_commit else None
)
seq_lens_cpu_backup = batch.seq_lens_cpu
seq_lens_sum_backup = batch.seq_lens_sum
if seq_lens_cpu_backup is not None:
# Verify host bound = committed prefix + one verify block (matches draft).
verify_host_seq_lens = seq_lens_cpu_backup + block_size
batch.seq_lens_cpu = verify_host_seq_lens
batch.seq_lens_sum = int(verify_host_seq_lens.sum())
elif draft_input.reserved_seq_lens_cpu is not None:
batch.seq_lens_cpu = draft_input.reserved_seq_lens_cpu
batch.seq_lens_sum = int(draft_input.reserved_seq_lens_sum)
verify_forward_batch, _ = verify_input.prepare_for_verify(
batch, self.target_worker
)
batch.seq_lens_cpu = seq_lens_cpu_backup
batch.seq_lens_sum = seq_lens_sum_backup
target_out = self.target_worker.forward_batch_generation(
batch=None,
forward_batch=verify_forward_batch,
is_verify=True,
skip_attn_backend_init=True,
)
logits_output = target_out.logits_output
can_run_cuda_graph = target_out.can_run_cuda_graph
if sampling_info is not None:
apply_dflash_verify_logits_adjustments(
next_token_logits=logits_output.next_token_logits,
sampling_info=sampling_info,
draft_token_num=int(self.block_size),
)
candidates = draft_tokens
new_seq_lens = None
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_correct_drafts_and_bonus(
candidates=candidates,
next_token_logits=logits_output.next_token_logits,
sampling_info=sampling_info,
max_top_k=draft_input.max_top_k,
uniform_top_k_value=draft_input.uniform_top_k_value,
)
commit_lens = accept_len.to(torch.int32) + 1 # [bs]
out_tokens = torch.empty(
(bs, int(self.block_size)), dtype=torch.int64, device=device
)
if int(self.block_size) > 1:
out_tokens[:, : int(self.block_size) - 1].copy_(candidates[:, 1:])
out_tokens[:, int(self.block_size) - 1].fill_(0)
out_tokens.scatter_(1, accept_len.to(torch.int64)[:, None], bonus[:, None])
else:
target_predict = torch.argmax(logits_output.next_token_logits, dim=-1).view(
bs, int(self.block_size)
)
if self._use_triton_accept_bonus:
try:
(
accept_len,
commit_lens,
bonus,
out_tokens,
new_seq_lens,
) = self._next_accept_bonus_buffers(bs)
_compute_dflash_accept_bonus_triton_unchecked(
candidates=candidates,
target_top1=target_predict,
accept_lens_out=accept_len,
commit_lens_out=commit_lens,
bonus_ids_out=bonus,
out_tokens_out=out_tokens,
prefix_lens=prefix_lens,
new_seq_lens_out=new_seq_lens,
)
except Exception as e:
self._use_triton_accept_bonus = False
logger.warning(
"DFLASH Triton accept/bonus failed; falling back to eager path: %s",
e,
)
accept_len, bonus = compute_dflash_correct_drafts_and_bonus(
candidates=candidates,
target_predict=target_predict,
)
commit_lens = accept_len.to(torch.int32) + 1 # [bs]
out_tokens = torch.empty(
(bs, int(self.block_size)),
dtype=torch.int64,
device=device,
)
if int(self.block_size) > 1:
out_tokens[:, : int(self.block_size) - 1].copy_(
candidates[:, 1:]
)
out_tokens[:, int(self.block_size) - 1].fill_(0)
out_tokens.scatter_(
1, accept_len.to(torch.int64)[:, None], bonus[:, None]
)
else:
accept_len, bonus = compute_dflash_correct_drafts_and_bonus(
candidates=candidates,
target_predict=target_predict,
)
commit_lens = accept_len.to(torch.int32) + 1 # [bs]
out_tokens = torch.empty(
(bs, int(self.block_size)), dtype=torch.int64, device=device
)
if int(self.block_size) > 1:
out_tokens[:, : int(self.block_size) - 1].copy_(candidates[:, 1:])
out_tokens[:, int(self.block_size) - 1].fill_(0)
out_tokens.scatter_(
1, accept_len.to(torch.int64)[:, None], bonus[:, None]
)
if self._need_mamba_verify_commit:
assert seq_lens_pre_verify is not None
self._update_target_mamba_state_after_verify(
batch=batch,
seq_lens_pre_verify=seq_lens_pre_verify,
commit_lens=commit_lens,
)
if new_seq_lens is None:
new_seq_lens = prefix_lens + commit_lens.to(prefix_lens.dtype)
if on_publish is not None:
on_publish(new_seq_lens)
# --- 3) Materialize committed verify-input tokens into draft KV cache.
hidden = logits_output.hidden_states
if hidden is None:
raise RuntimeError(
"DFLASH verify requires target hidden states, but got None."
)
hidden = hidden.view(bs, int(self.block_size), -1)
self._append_target_hidden_to_draft_kv_by_loc(
target_hidden=hidden.reshape(-1, hidden.shape[-1]),
cache_loc=verify_out_cache_loc,
cache_loc_2d=verify_out_cache_loc_2d,
positions=positions,
commit_lens=commit_lens,
)
# Avoid copying large hidden-state buffers to CPU in overlap scheduling.
logits_output.hidden_states = None
next_draft_input = self._make_next_draft_input_decode(
bonus_tokens=bonus,
new_seq_lens=new_seq_lens,
)
return GenerationBatchResult(
logits_output=logits_output,
next_token_ids=out_tokens.reshape(-1),
accept_lens=commit_lens,
can_run_cuda_graph=can_run_cuda_graph,
next_draft_input=next_draft_input,
speculative_num_draft_tokens=int(self.block_size),
# The non-overlap (sync) scheduler path advances batch.seq_lens
# from the result; overlap carries it via next_draft_input instead.
new_seq_lens=new_seq_lens,
)