Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from sglang.srt.speculative.dflash_info_v2 import DFlashDraftInputV2
from sglang.srt.speculative.dflash_utils import (
_get_or_create_chain_verify_buffers,
build_dflash_verify_target_probs,
build_speculative_verify_target_probs,
compute_dflash_correct_drafts_and_bonus,
)
from sglang.srt.utils import is_npu
Expand Down Expand Up @@ -104,7 +104,7 @@ def _accept_sampling_core(
rows_per_request=verify_num_draft_tokens,
).view(bs, verify_num_draft_tokens, -1)
else:
target_probs = build_dflash_verify_target_probs(
target_probs = build_speculative_verify_target_probs(
next_token_logits=target_logits,
sampling_info=sampling_info,
draft_token_num=verify_num_draft_tokens,
Expand Down
11 changes: 9 additions & 2 deletions python/sglang/srt/layers/logits_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ class SamplingMaskOutput:
selected_logprobs: torch.Tensor
support_logprobs: Optional[torch.Tensor]
statuses: torch.Tensor
num_accept_tokens: Optional[torch.Tensor] = None

def map_device_tensors(self, fn) -> None:
self.token_ids = fn(self.token_ids)
Expand All @@ -114,6 +115,8 @@ def map_device_tensors(self, fn) -> None:
if self.support_logprobs is not None:
self.support_logprobs = fn(self.support_logprobs)
self.statuses = fn(self.statuses)
if self.num_accept_tokens is not None:
self.num_accept_tokens = fn(self.num_accept_tokens)


def _trace_e2e_logits(stage: str, **fields) -> None:
Expand Down Expand Up @@ -235,8 +238,12 @@ class LogitsProcessorOutput:
# Post-filter support IDs and requested behavior logprobs, bounded by server
# capacity. Logprobs are normalized over the full realized support.
sampling_mask_output: Optional[SamplingMaskOutput] = None
next_token_sampling_mask_idx: Optional[List[Optional[np.ndarray]]] = None
next_token_sampling_logprobs: Optional[List[Optional[np.ndarray]]] = None
next_token_sampling_mask_idx: Optional[
List[Optional[Union[np.ndarray, List[np.ndarray]]]]
] = None
next_token_sampling_logprobs: Optional[
List[Optional[Union[np.ndarray, List[np.ndarray]]]]
] = None
next_token_sampling_mask_status: Optional[List[Optional[int]]] = None

## Part 3: Prefill-only. This part will be assigned in python/sglang/srt/layers/logits_processor.py::LogitsProcessor
Expand Down
77 changes: 69 additions & 8 deletions python/sglang/srt/managers/scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,9 @@
get_draft_recurrent_hidden_state_spec_from_config,
)
from sglang.srt.speculative.spec_info import SpeculativeAlgorithm
from sglang.srt.speculative.spec_sampling_mask import (
validate_spec_sampling_mask_request,
)
from sglang.srt.speculative.uno_validation import validate_uno_request
from sglang.srt.state_capturer.indexer_topk import destroy_global_indexer_capturer
from sglang.srt.state_capturer.routed_experts import destroy_global_experts_capturer
Expand Down Expand Up @@ -2967,14 +2970,10 @@ def handle_generate_request(
return

if req.return_sampling_mask and not self.spec_algorithm.is_none():
# Spec workers do not emit one sampling support per accepted token, so
# the returned mask would not align 1:1 with generated tokens. Reject
# the combination instead of silently returning a misaligned mask.
error_msg = (
"return_sampling_mask is not supported with speculative decoding."
)
self._reject_sampling_mask_request(req, error_msg)
return
error_msg = validate_spec_sampling_mask_request(req, self.spec_algorithm)
if error_msg is not None:
self._reject_sampling_mask_request(req, error_msg)
return

if req.return_sampling_mask and get_exec().kernel.sampling_backend == "ascend":
# The ascend backend samples from logits directly and never builds the
Expand Down Expand Up @@ -5300,6 +5299,52 @@ def get_internal_state(self, recv_req: GetInternalStateReq):

return GetInternalStateReqOutput(internal_state=msgspec_to_builtins(ret))

def _has_active_sampling_mask_request(self) -> bool:
"""Whether any unfinished request holding a sampling mask is queued or
running, including PP micro-batches and PD decode queues."""
active_batches = [self.running_batch, self.last_batch]
active_batches.extend(getattr(self, "running_mbs", ()))
active_batches.extend(getattr(self, "mbs", ()))
active_batches.extend(getattr(self, "last_mbs", ()))
has_active_mask_request = any(
req.return_sampling_mask and not req.finished()
for batch in active_batches
if batch is not None
for req in batch.reqs
)
has_active_mask_request |= any(
req.return_sampling_mask and not req.finished()
for req in self.waiting_queue
)
has_active_mask_request |= any(
req.return_sampling_mask and not req.finished()
for req in self.grammar_manager.grammar_queue
)
has_active_mask_request |= (
self.chunked_req is not None
and self.chunked_req.return_sampling_mask
and not self.chunked_req.finished()
)
if self.disaggregation_mode == DisaggregationMode.DECODE:
prealloc_queue = self.disagg_decode_prealloc_queue
decode_reqs = [
*prealloc_queue.queue,
*prealloc_queue.pending_reqs,
*self.disagg_decode_transfer_queue.queue,
]
has_active_mask_request |= any(
decode_req.req.return_sampling_mask and not decode_req.req.finished()
for decode_req in decode_reqs
)
has_active_mask_request |= any(
req.return_sampling_mask and not req.finished()
for req in (
*prealloc_queue.retracted_queue,
*prealloc_queue.held_rebootstrap_reqs,
)
)
return has_active_mask_request

def set_internal_state(self, recv_req: SetInternalStateReq):
server_args_dict = recv_req.server_args
args_allow_update = set(
Expand All @@ -5326,6 +5371,22 @@ def set_internal_state(self, recv_req: SetInternalStateReq):
)
if_success = False
break
elif (
k
in (
"speculative_accept_threshold_single",
"speculative_accept_threshold_acc",
)
and self.spec_algorithm.is_dflash()
and float(v) != 1.0
):
if self._has_active_sampling_mask_request():
logging.warning(
f"Updating {k} is rejected while DFlash sampling-mask "
"requests are active."
)
if_success = False
break
elif k == "dspark_force_budget_frac":
if not self.spec_algorithm.is_dspark() or not hasattr(
self.draft_worker, "set_dspark_forced_budget_frac"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
Union,
)

import numpy as np
import torch

from sglang.srt.disaggregation.utils import DisaggregationMode
Expand Down Expand Up @@ -1034,9 +1035,9 @@ def process_batch_result_decode(
)

if req.return_sampling_mask:
# return_sampling_mask + speculative decoding is rejected at
# request entry, so this remains one support mask per token.
self.add_sampling_mask_return_values(i, req, logits_output)
self.add_sampling_mask_return_values(
i, req, logits_output, accept_len=new_accept_len
)

if req.return_hidden_states and logits_output.hidden_states is not None:
# hidden_states is [bs * stride, hidden_dim], one row per emitted
Expand Down Expand Up @@ -1161,12 +1162,68 @@ def add_sampling_mask_return_values(
i: int,
req: Req,
output: LogitsProcessorOutput,
*,
accept_len: int = 1,
) -> None:
"""Attach sparse sampling support metadata to the return values."""
req.sampling_mask_rows.append(
output.next_token_sampling_mask_idx[i],
output.next_token_sampling_logprobs[i],
masks = output.next_token_sampling_mask_idx[i]
logprobs = output.next_token_sampling_logprobs[i]
if not isinstance(masks, list):
req.sampling_mask_rows.append(masks, logprobs)
return
# Speculative rows carry one support per accepted token; drop the ones
# past finished_len, as output_ids is trimmed there.
step_start = len(req.output_ids) - accept_len
num_visible = max(
0, min(accept_len, self._visible_output_len(req) - step_start)
)
for token in range(num_visible):
req.sampling_mask_rows.append(masks[token], logprobs[token])

@staticmethod
def _fill_speculative_sampling_mask_rows(
*,
reqs: List[Req],
batch_indices: List[int],
num_accept_tokens: List[int],
lengths: List[List[int]],
statuses: List[List[int]],
token_ids: np.ndarray,
selected_logprobs: np.ndarray,
support_logprobs: Optional[np.ndarray],
masks: list,
logprobs: list,
status_by_batch: list,
) -> None:
"""Speculative rows carry one support per draft position; keep the
accepted prefix, as a list with one entry per accepted token."""
assert len(batch_indices) == len(num_accept_tokens)
support_row = 0
for row, batch_index in enumerate(batch_indices):
returns_support_logprobs = (
reqs[batch_index].sampling_logprobs_mode == "support"
)
num_tokens = num_accept_tokens[row]
status = max(statuses[row][:num_tokens])
status_by_batch[batch_index] = status
if status == SamplingMaskStatus.OK:
row_lengths = lengths[row][:num_tokens]
masks[batch_index] = [
token_ids[row, token, :length]
for token, length in enumerate(row_lengths)
]
if returns_support_logprobs:
logprobs[batch_index] = [
support_logprobs[support_row, token, :length]
for token, length in enumerate(row_lengths)
]
else:
logprobs[batch_index] = [
selected_logprobs[row, token : token + 1]
for token in range(num_tokens)
]
if returns_support_logprobs:
support_row += 1

@staticmethod
def materialize_sampling_mask_output(
Expand Down Expand Up @@ -1194,6 +1251,26 @@ def materialize_sampling_mask_output(
if sampling_output.support_logprobs is None
else sampling_output.support_logprobs.cpu().numpy()
)
if sampling_output.num_accept_tokens is not None:
SchedulerBatchResultProcessor._fill_speculative_sampling_mask_rows(
reqs=reqs,
batch_indices=batch_indices,
num_accept_tokens=sampling_output.num_accept_tokens.tolist(),
lengths=lengths,
statuses=statuses,
token_ids=token_ids,
selected_logprobs=selected_logprobs,
support_logprobs=support_logprobs,
masks=masks,
logprobs=logprobs,
status_by_batch=status_by_batch,
)
output.next_token_sampling_mask_idx = masks
output.next_token_sampling_logprobs = logprobs
output.next_token_sampling_mask_status = status_by_batch
output.sampling_mask_output = None
return

support_row = 0
for row, batch_index in enumerate(batch_indices):
returns_support_logprobs = (
Expand Down
6 changes: 6 additions & 0 deletions python/sglang/srt/managers/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,11 @@ def get_logprob_dict_from_result(result: GenerationBatchResult) -> dict:
"sampling_mask_statuses": (
None if sampling_mask_output is None else sampling_mask_output.statuses
),
"sampling_mask_num_accept_tokens": (
None
if sampling_mask_output is None
else sampling_mask_output.num_accept_tokens
),
"input_token_logprobs": result.logits_output.input_token_logprobs,
"input_top_logprobs_val": result.logits_output.input_top_logprobs_val,
"input_top_logprobs_idx": result.logits_output.input_top_logprobs_idx,
Expand All @@ -332,6 +337,7 @@ def get_logprob_from_pp_outputs(
selected_logprobs=next_pp_outputs["sampling_mask_selected_logprobs"],
support_logprobs=next_pp_outputs["sampling_mask_support_logprobs"],
statuses=next_pp_outputs["sampling_mask_statuses"],
num_accept_tokens=next_pp_outputs["sampling_mask_num_accept_tokens"],
)
logits_output = LogitsProcessorOutput(
# Do not send logits and hidden states because they are large
Expand Down
13 changes: 13 additions & 0 deletions python/sglang/srt/sampling/sampling_batch_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ class SamplingBatchInfo:
return_sampling_support_logprobs: Optional[List[bool]] = None
sampling_mask_batch_indices: Optional[torch.Tensor] = None
sampling_support_logprobs_capture_indices: Optional[torch.Tensor] = None
# Per-row top_k of mask requests (1 elsewhere); bounds the capture width.
sampling_mask_top_ks: Optional[List[int]] = None

# Device
device: str = "cuda"
Expand Down Expand Up @@ -201,6 +203,9 @@ def from_schedule_batch(cls, batch: ScheduleBatch, vocab_size: int):
device,
)
)
sampling_mask_top_ks = [
r.sampling_params.top_k if r.return_sampling_mask else 1 for r in reqs
]

if has_custom_logit_processor:
# Merge the same type of custom logit processors together
Expand Down Expand Up @@ -271,6 +276,7 @@ def from_schedule_batch(cls, batch: ScheduleBatch, vocab_size: int):
sampling_support_logprobs_capture_indices=(
sampling_support_logprobs_capture_indices
),
sampling_mask_top_ks=sampling_mask_top_ks,
)
ret.adjusted_from_schedule_batch(batch, vocab_size)
return ret
Expand Down Expand Up @@ -435,6 +441,10 @@ def filter_batch(self, keep_indices: List[int], keep_indices_device: torch.Tenso
self.return_sampling_masks = [
self.return_sampling_masks[i] for i in keep_indices
]
if self.sampling_mask_top_ks is not None:
self.sampling_mask_top_ks = [
self.sampling_mask_top_ks[i] for i in keep_indices
]
if self.return_sampling_support_logprobs is not None:
self.return_sampling_support_logprobs = [
self.return_sampling_support_logprobs[i] for i in keep_indices
Expand Down Expand Up @@ -537,6 +547,9 @@ def merge_batch(self, other: SamplingBatchInfo):
self.device,
)
)
self.sampling_mask_top_ks = (
self.sampling_mask_top_ks or [1] * self_len
) + (other.sampling_mask_top_ks or [1] * other_len)

# Note: because the __len()__ operator is defined on the temperatures tensor,
# please make sure any merge operation with len(self) or len(other) is done before
Expand Down
4 changes: 2 additions & 2 deletions python/sglang/srt/speculative/dflash_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1086,7 +1086,7 @@ def compute_dflash_sampling_correct_drafts_and_bonus(
dtype=torch.float32,
)

target_probs = build_dflash_verify_target_probs(
target_probs = build_speculative_verify_target_probs(
next_token_logits=next_token_logits,
sampling_info=sampling_info,
draft_token_num=draft_token_num,
Expand Down Expand Up @@ -1127,7 +1127,7 @@ def compute_dflash_sampling_correct_drafts_and_bonus(
return correct_len, bonus


def build_dflash_verify_target_probs(
def build_speculative_verify_target_probs(
*,
next_token_logits: torch.Tensor,
sampling_info: Any,
Expand Down
16 changes: 16 additions & 0 deletions python/sglang/srt/speculative/dflash_worker_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@
target_input_embeddings,
)
from sglang.srt.speculative.spec_info import SpeculativeAlgorithm
from sglang.srt.speculative.spec_sampling_mask import (
SpeculativeSamplingMaskCapture,
)
from sglang.srt.speculative.spec_tp_sync import SpecTpSync, SpecTpSyncSite
from sglang.srt.speculative.spec_utils import (
SIMULATE_ACC_LEN,
Expand Down Expand Up @@ -2836,6 +2839,19 @@ def forward_batch_generation(
# accept_len; recompute it from the forced commit_lens.
new_seq_lens = None

sampling_mask_capture = SpeculativeSamplingMaskCapture.from_logits(
sampling_info,
next_token_logits=logits_output.next_token_logits,
draft_input=draft_input,
draft_token_num=int(self.block_size),
bs=bs,
)
if sampling_mask_capture is not None:
logits_output.sampling_mask_output = sampling_mask_capture.build_output(
out_tokens=out_tokens,
commit_lens=commit_lens,
)

if batch.return_logprob:
compute_spec_logprobs(
batch,
Expand Down
Loading
Loading