Skip to content

vllm.v1.worker.gpu.spec_decode.utils

Classes:

Functions:

DraftTokensHandler

Methods:

  • get_draft_tokens –

    Return the latest drafts needed for the current step's grammar bitmask.

Source code in vllm/v1/worker/gpu/spec_decode/utils.py
class DraftTokensHandler:
    def __init__(self, device: torch.device | None = None):
        self.device = device
        self.copy_stream = new_d2h_stream(device)
        # Blocking (sleep) event to avoid busy-polling the CUDA driver lock.
        self.copy_event = torch.cuda.Event(blocking=True)
        self._copy_pending = False

        # Latest drafts per live request: a row view into an async-copied
        # pinned array (synced in get_draft_tokens), or just the draft width
        # when the scheduler doesn't need the token ids.
        self._draft_tokens_by_req: dict[str, np.ndarray | int] = {}
        # Requests whose drafts the scheduler will fetch for the latest step's
        # structured-output grammar bitmask; None means all requests.
        self._spec_decode_req_ids: Iterable[str] | None = None

    def begin_step(self, scheduler_output: SchedulerOutput) -> None:
        # get_draft_tokens is only called for the most recently executed
        # scheduler output.
        self._spec_decode_req_ids = (
            scheduler_output.scheduled_spec_decode_tokens.keys()
            if scheduler_output.has_structured_output_spec_decode
            else None
        )

    def set_draft_tokens(
        self, input_batch: InputBatch, draft_tokens: torch.Tensor
    ) -> None:
        if not input_batch.has_structured_output_reqs:
            # No draft token validation needs to be performed by
            # the scheduler for this batch. Record draft token widths only.
            num_draft_tokens = draft_tokens.shape[1]
            for req_id in input_batch.req_ids:
                self._draft_tokens_by_req[req_id] = num_draft_tokens
            return

        # For spec decoding + structured outputs, we must transfer the
        # draft tokens back to the scheduler for grammar validation.
        current_stream = torch.cuda.current_stream(self.device)
        self.copy_stream.wait_stream(current_stream)
        with torch.cuda.stream(self.copy_stream):
            draft_tokens_np = async_copy_to_np(draft_tokens)
            # draft_tokens is a temporary allocation on the main stream and read here on
            # copy_stream; without record_stream, the caching allocator may reuse its
            # memory before the async copy executes.
            draft_tokens.record_stream(self.copy_stream)
            self.copy_event.record()
            self._copy_pending = True

        for req_id, row in zip(input_batch.req_ids, draft_tokens_np):
            self._draft_tokens_by_req[req_id] = row

    def discard(self, req_id: str) -> None:
        self._draft_tokens_by_req.pop(req_id, None)

    def get_draft_tokens(self) -> DraftTokenIds | None:
        """Return the latest drafts needed for the current step's grammar bitmask.
        Requests without retained drafts are omitted.
        """
        if self._copy_pending:
            self.copy_event.synchronize()
            self._copy_pending = False

        drafts_by_req = self._draft_tokens_by_req
        req_ids = self._spec_decode_req_ids
        if req_ids is None:
            out_req_ids = list(drafts_by_req)
        else:
            out_req_ids = [req_id for req_id in req_ids if req_id in drafts_by_req]
        draft_token_ids: list[list[int]] = []
        for req_id in out_req_ids:
            draft_tokens = drafts_by_req[req_id]
            if isinstance(draft_tokens, int):
                # Construct placeholders from draft token width
                draft_tokens = [-1] * draft_tokens
            else:
                # Convert numpy view to list
                draft_tokens = draft_tokens.tolist()
            draft_token_ids.append(draft_tokens)

        return DraftTokenIds(out_req_ids, draft_token_ids)

get_draft_tokens()

Return the latest drafts needed for the current step's grammar bitmask. Requests without retained drafts are omitted.

Source code in vllm/v1/worker/gpu/spec_decode/utils.py
def get_draft_tokens(self) -> DraftTokenIds | None:
    """Return the latest drafts needed for the current step's grammar bitmask.
    Requests without retained drafts are omitted.
    """
    if self._copy_pending:
        self.copy_event.synchronize()
        self._copy_pending = False

    drafts_by_req = self._draft_tokens_by_req
    req_ids = self._spec_decode_req_ids
    if req_ids is None:
        out_req_ids = list(drafts_by_req)
    else:
        out_req_ids = [req_id for req_id in req_ids if req_id in drafts_by_req]
    draft_token_ids: list[list[int]] = []
    for req_id in out_req_ids:
        draft_tokens = drafts_by_req[req_id]
        if isinstance(draft_tokens, int):
            # Construct placeholders from draft token width
            draft_tokens = [-1] * draft_tokens
        else:
            # Convert numpy view to list
            draft_tokens = draft_tokens.tolist()
        draft_token_ids.append(draft_tokens)

    return DraftTokenIds(out_req_ids, draft_token_ids)

get_drafter_hidden_states(model, hidden_states)

Hidden states fed to the drafter.

Targets such as DeepSeek V4 expose the pre-hc_head residual through get_mtp_target_hidden_states(). The buffer is sized at max_num_batched_tokens and only allocated for drafters that consume target hidden states, so None means "use the regular hidden states".

Source code in vllm/v1/worker/gpu/spec_decode/utils.py
def get_drafter_hidden_states(
    model: torch.nn.Module, hidden_states: torch.Tensor
) -> torch.Tensor:
    """Hidden states fed to the drafter.

    Targets such as DeepSeek V4 expose the pre-hc_head residual through
    get_mtp_target_hidden_states(). The buffer is sized at
    max_num_batched_tokens and only allocated for drafters that consume
    target hidden states, so None means "use the regular hidden states".
    """
    get_target_hidden_states = getattr(model, "get_mtp_target_hidden_states", None)
    if get_target_hidden_states is None:
        return hidden_states
    target_hidden_states = get_target_hidden_states()
    if target_hidden_states is None:
        return hidden_states
    return target_hidden_states[: hidden_states.shape[0]]

get_parallel_drafting_token_id(hf_config)

Resolve the mask token id used for parallel drafting slots.

Checks (in order): dflash_config.mask_token_id, top-level mask_token_id, dspark_noise_token_id, pard_token, ptd_token_id. Raises ValueError if none are present.

Source code in vllm/v1/worker/gpu/spec_decode/utils.py
def get_parallel_drafting_token_id(hf_config) -> int:
    """Resolve the mask token id used for parallel drafting slots.

    Checks (in order): `dflash_config.mask_token_id`, top-level `mask_token_id`,
    `dspark_noise_token_id`, `pard_token`, `ptd_token_id`. Raises ValueError if
    none are present.
    """
    dflash_config = getattr(hf_config, "dflash_config", None) or {}
    if "mask_token_id" in dflash_config:
        return int(dflash_config["mask_token_id"])
    if getattr(hf_config, "mask_token_id", None) is not None:
        return int(hf_config.mask_token_id)
    if hasattr(hf_config, "dspark_noise_token_id"):
        return int(hf_config.dspark_noise_token_id)
    if hasattr(hf_config, "pard_token"):
        return int(hf_config.pard_token)
    if hasattr(hf_config, "ptd_token_id"):
        return int(hf_config.ptd_token_id)
    raise ValueError(
        "Model config must specify `dflash_config.mask_token_id`,"
        " `mask_token_id`, `dspark_noise_token_id`, `pard_token`, or"
        " `ptd_token_id` for parallel drafting."
    )

get_pp_safe_draft_load_config(load_config)

Avoid collectives that include PP ranks without a draft model.

Source code in vllm/v1/worker/gpu/spec_decode/utils.py
def get_pp_safe_draft_load_config(load_config: LoadConfig) -> LoadConfig:
    """Avoid collectives that include PP ranks without a draft model."""
    if get_pp_group().world_size > 1 and load_config.load_format == "fastsafetensors":
        logger.warning_once(
            "fastsafetensors cannot load a draft model instantiated on only "
            "one pipeline stage; falling back to the standard safetensors "
            "loader for the draft model. The target model still uses "
            "fastsafetensors."
        )
        return replace(load_config, load_format="auto")
    return load_config

max_num_tokens_after_draft_slots(max_num_tokens, num_reqs, max_num_new_slots_per_req)

Return the input-token budget after reserving each request's draft slots.

Source code in vllm/v1/worker/gpu/spec_decode/utils.py
def max_num_tokens_after_draft_slots(
    max_num_tokens: int, num_reqs: int, max_num_new_slots_per_req: int
) -> int:
    """Return the input-token budget after reserving each request's draft slots."""
    return max_num_tokens - num_reqs * max_num_new_slots_per_req