[6/6][kimi-deterministic] Use deterministic seeded coins for EAGLE rejection sampling (#30822)
This commit is contained in:
@@ -297,6 +297,7 @@ def flash_attn_varlen_func(
|
||||
cu_seqlens_k,
|
||||
max_seqlen_q=max_seqlen_q,
|
||||
max_seqlen_k=max_seqlen_k,
|
||||
qv=qv,
|
||||
seqused_q=seqused_q,
|
||||
seqused_k=seqused_k,
|
||||
page_table=page_table,
|
||||
|
||||
@@ -34,6 +34,7 @@ def flash_attn_varlen_func(
|
||||
v: torch.Tensor,
|
||||
cu_seqlens_q: Optional[torch.Tensor] = None,
|
||||
cu_seqlens_k: Optional[torch.Tensor] = None,
|
||||
qv: Optional[torch.Tensor] = None,
|
||||
seqused_q: Optional[torch.Tensor] = None,
|
||||
seqused_k: Optional[torch.Tensor] = None,
|
||||
max_seqlen_q: Optional[int] = None,
|
||||
@@ -75,7 +76,7 @@ def flash_attn_varlen_func(
|
||||
"vendored FA4 package is importable."
|
||||
) from _flash_attn_import_error
|
||||
|
||||
q, k, v = [_maybe_contiguous(t) for t in (q, k, v)]
|
||||
q, k, v, qv = [_maybe_contiguous(t) for t in (q, k, v, qv)]
|
||||
cu_seqlens_q, cu_seqlens_k = [
|
||||
_maybe_contiguous(t) for t in (cu_seqlens_q, cu_seqlens_k)
|
||||
]
|
||||
@@ -118,6 +119,7 @@ def flash_attn_varlen_func(
|
||||
q=q,
|
||||
k=k,
|
||||
v=v,
|
||||
qv=qv,
|
||||
cu_seqlens_q=cu_seqlens_q,
|
||||
cu_seqlens_k=cu_seqlens_k,
|
||||
seqused_q=seqused_q,
|
||||
@@ -189,7 +191,7 @@ def flash_attn_with_kvcache(
|
||||
return_softmax_lse: bool = False,
|
||||
**_: object,
|
||||
):
|
||||
if k is not None or v is not None or qv is not None:
|
||||
if k is not None or v is not None:
|
||||
raise NotImplementedError("FA4 does not support updating KV cache in-place.")
|
||||
if rotary_cos is not None or rotary_sin is not None or rotary_seqlens is not None:
|
||||
raise NotImplementedError("FA4 path does not support rotary embedding.")
|
||||
@@ -206,6 +208,7 @@ def flash_attn_with_kvcache(
|
||||
q=q,
|
||||
k=k_cache,
|
||||
v=v_cache,
|
||||
qv=qv,
|
||||
cu_seqlens_q=cu_seqlens_q,
|
||||
seqused_k=cache_seqlens,
|
||||
max_seqlen_q=max_seqlen_q,
|
||||
|
||||
@@ -783,6 +783,7 @@ class TboForwardBatchPreparer:
|
||||
original_global_num_tokens_cpu=None,
|
||||
_original_batch_size=None,
|
||||
_original_forward_mode=None,
|
||||
_original_num_tokens=None,
|
||||
global_num_tokens_gpu=None,
|
||||
global_num_tokens_cpu=None,
|
||||
global_dp_buffer_len=global_dp_buffer_len,
|
||||
|
||||
@@ -1535,7 +1535,10 @@ class FlashAttentionBackend(AttentionBackend):
|
||||
return output, lse
|
||||
return output
|
||||
else:
|
||||
assert self.fa_impl_ver == 3, "Only FA3 support here"
|
||||
# FA4 absorbed MLA is shared by extend and decode: once qv is
|
||||
# threaded through the wrappers, decode's flash_attn_with_kvcache
|
||||
# call takes the same qv/ver arguments as this extend path.
|
||||
assert self.fa_impl_ver in (3, 4), "Only FA3/FA4 support here"
|
||||
# Do absorbed multi-latent attention
|
||||
kv_cache = self.token_to_kv_pool.get_key_buffer(layer.layer_id).to(
|
||||
q.dtype
|
||||
|
||||
@@ -115,13 +115,12 @@ class DeepEPMoE(FusedMoE):
|
||||
and self.w13_weight.dtype == torch.bfloat16
|
||||
and get_moe_runner_backend().is_deep_gemm()
|
||||
and get_moe_a2a_backend().is_deepep()
|
||||
and get_deepep_mode().enable_low_latency()
|
||||
and not _is_npu
|
||||
and not _is_hip
|
||||
):
|
||||
assert (
|
||||
deep_gemm_wrapper.ENABLE_JIT_DEEPGEMM
|
||||
), "Unquantized DeepEP low-latency MoE requires DeepGEMM BF16"
|
||||
), "Unquantized DeepEP MoE requires DeepGEMM BF16"
|
||||
self.deprecate_flag = True
|
||||
else:
|
||||
self.deprecate_flag = False
|
||||
|
||||
@@ -25,6 +25,7 @@ from sglang.srt.layers.moe.moe_runner.base import (
|
||||
register_pre_permute,
|
||||
)
|
||||
from sglang.srt.layers.moe.utils import MoeRunnerBackend
|
||||
from sglang.srt.runtime_context import get_exec
|
||||
from sglang.srt.utils import (
|
||||
ceil_div,
|
||||
dispose_tensor,
|
||||
@@ -828,7 +829,15 @@ def pre_permute_deepep_normal_to_deep_gemm(
|
||||
running_state["topk_ids"] = topk_ids
|
||||
running_state["topk_weights"] = topk_weights
|
||||
|
||||
input_tensor = torch.empty(
|
||||
# Deterministic inference zero-fills the scatter buffers: expert-alignment
|
||||
# padding leaves slots that ep_scatter never writes, and pad garbage in
|
||||
# input_tensor would leak batch-dependent values into the grouped GEMM.
|
||||
# The scale buffer only matters for FP8 activations sharing this
|
||||
# pre-permute (ep_scatter skips scales entirely for BF16 dispatch).
|
||||
deterministic = get_exec().deterministic.enable_deterministic_inference
|
||||
buffer_init = torch.zeros if deterministic else torch.empty
|
||||
|
||||
input_tensor = buffer_init(
|
||||
(all_tokens, K),
|
||||
device=hidden_states.device,
|
||||
dtype=hidden_states.dtype,
|
||||
@@ -841,12 +850,12 @@ def pre_permute_deepep_normal_to_deep_gemm(
|
||||
dtype=torch.int,
|
||||
).transpose(0, 1)
|
||||
else:
|
||||
input_tensor_scale = torch.empty(
|
||||
input_tensor_scale = buffer_init(
|
||||
(all_tokens, K // 128),
|
||||
device=hidden_states.device,
|
||||
dtype=torch.float32,
|
||||
)
|
||||
m_indices = torch.empty(all_tokens, device=hidden_states.device, dtype=torch.int32)
|
||||
m_indices = buffer_init(all_tokens, device=hidden_states.device, dtype=torch.int32)
|
||||
output_index = torch.empty_like(topk_ids)
|
||||
|
||||
if get_offloader().forbid_copy_engine_usage:
|
||||
|
||||
@@ -19,7 +19,6 @@ from sglang.srt.layers.moe import (
|
||||
MoeRunner,
|
||||
MoeRunnerBackend,
|
||||
MoeRunnerConfig,
|
||||
get_deepep_mode,
|
||||
get_moe_a2a_backend,
|
||||
get_moe_runner_backend,
|
||||
)
|
||||
@@ -367,7 +366,6 @@ class UnquantizedFusedMoEMethod(FusedMoEMethodBase, MultiPlatformOp):
|
||||
self.use_deep_gemm
|
||||
and layer.w13_weight.dtype == torch.bfloat16
|
||||
and get_moe_a2a_backend().is_deepep()
|
||||
and get_deepep_mode().enable_low_latency()
|
||||
and not _is_npu
|
||||
and not _is_hip
|
||||
and hasattr(layer, "dispatcher")
|
||||
|
||||
@@ -186,6 +186,21 @@ class Sampler(nn.Module):
|
||||
# Standard path: do softmax and sample from probs.
|
||||
logits.div_(sampling_info.temperatures)
|
||||
|
||||
# Deterministic inference must derive the returned logprobs
|
||||
# from F.log_softmax — the same kernel prefill rescoring uses —
|
||||
# not log(softmax(x)) below: the two disagree at ~1e-6 despite
|
||||
# being mathematically equivalent, which breaks bitwise
|
||||
# prefill/decode logprob alignment.
|
||||
if (
|
||||
return_logprob
|
||||
and self.enable_deterministic
|
||||
and logprobs_via_logsoftmax_kernel is None
|
||||
and not SGLANG_RETURN_ORIGINAL_LOGPROB
|
||||
):
|
||||
logprobs_via_logsoftmax_kernel = torch.nn.functional.log_softmax(
|
||||
logits, dim=-1
|
||||
)
|
||||
|
||||
# In-place op to save memory
|
||||
logits[:] = torch.softmax(logits, dim=-1)
|
||||
probs = logits
|
||||
|
||||
@@ -501,6 +501,7 @@ class ForwardBatch(ForwardBatchDeepSeekMHAMixin):
|
||||
original_global_num_tokens_cpu: Optional[List[int]] = None
|
||||
_original_batch_size: Optional[int] = None
|
||||
_original_forward_mode: Optional[ForwardMode] = None
|
||||
_original_num_tokens: Optional[int] = None
|
||||
global_num_tokens_cpu: Optional[List[int]] = None
|
||||
global_num_tokens_gpu: Optional[torch.Tensor] = None
|
||||
# Has to be None when cuda graph is captured.
|
||||
@@ -1382,6 +1383,7 @@ class ForwardBatch(ForwardBatchDeepSeekMHAMixin):
|
||||
|
||||
def _pad_inputs_to_size(self, model_runner: ModelRunner, num_tokens, bs):
|
||||
# padding
|
||||
self._original_num_tokens = self.positions.shape[0]
|
||||
self.input_ids = self._pad_tensor_to_size(self.input_ids, num_tokens)
|
||||
self.req_pool_indices = self._pad_tensor_to_size(self.req_pool_indices, bs)
|
||||
if self.lora_ids is not None:
|
||||
@@ -1492,6 +1494,17 @@ class ForwardBatch(ForwardBatchDeepSeekMHAMixin):
|
||||
self.batch_size = self._original_batch_size
|
||||
bs = self.batch_size
|
||||
|
||||
# MLP-sync padding appended dummy rows after the real ones; slice the
|
||||
# per-request tensors back so post-forward consumers (seeded sampling,
|
||||
# ngram token-table updates) never see the padding. The draft-decode
|
||||
# branch below does the same for speculative batches.
|
||||
if self.spec_info is None and self._original_num_tokens is not None:
|
||||
self.positions = self.positions[: self._original_num_tokens]
|
||||
self.seq_lens = self.seq_lens[:bs]
|
||||
self.req_pool_indices = self.req_pool_indices[:bs]
|
||||
if self.seq_lens_cpu is not None:
|
||||
self.seq_lens_cpu = self.seq_lens_cpu[:bs]
|
||||
|
||||
if self.spec_info is not None:
|
||||
if self.forward_mode.is_decode(): # draft
|
||||
num_tokens = self.hidden_states_backup.shape[0]
|
||||
|
||||
@@ -11,8 +11,8 @@ from sglang.srt.models.deepseek_common.attention_forward_methods.forward_methods
|
||||
AttnForwardMethod,
|
||||
)
|
||||
from sglang.srt.models.deepseek_common.utils import _is_hip
|
||||
from sglang.srt.runtime_context import get_server_args
|
||||
from sglang.srt.utils import use_intel_amx_backend
|
||||
from sglang.srt.runtime_context import get_exec, get_server_args
|
||||
from sglang.srt.utils import is_sm100_or_sm110_supported, use_intel_amx_backend
|
||||
|
||||
MHA_ONE_SHOT_SUPPORTED_BACKENDS = ["fa3", "flashinfer", "flashmla"]
|
||||
|
||||
@@ -133,8 +133,15 @@ def handle_attention_cutlass_mla(attn, forward_batch):
|
||||
|
||||
|
||||
def handle_attention_fa4(attn, forward_batch):
|
||||
# TODO(cicirori): use FA4 MHA for DeepSeekV3 for now
|
||||
return AttnForwardMethod.MHA_CHUNKED_KV
|
||||
# FA4 absorbed MLA feeds q_nope through the qv argument, which
|
||||
# flash_attn.cute only implements on SM100/SM110 (not SM120); keep the
|
||||
# pre-existing MHA chunked-KV path elsewhere. Deterministic inference
|
||||
# requires MLA and rejects fa4 on other archs at startup (server_args).
|
||||
if not is_sm100_or_sm110_supported():
|
||||
return AttnForwardMethod.MHA_CHUNKED_KV
|
||||
if get_exec().deterministic.enable_deterministic_inference:
|
||||
return _dispatch_mla_subtype(attn, forward_batch)
|
||||
return _handle_attention_backend(attn, forward_batch, "fa4")
|
||||
|
||||
|
||||
def handle_attention_trtllm_mla(attn, forward_batch):
|
||||
|
||||
@@ -59,6 +59,7 @@ NVFP4_CKPT_FP8_ATTN_QUANT_MODULES = ["q_b_proj"]
|
||||
|
||||
FORWARD_ABSORB_CORE_ATTENTION_BACKENDS = [
|
||||
"fa3",
|
||||
"fa4",
|
||||
"dsa",
|
||||
"nsa", # Deprecated alias for "dsa"
|
||||
"flashinfer",
|
||||
|
||||
@@ -184,6 +184,7 @@ from sglang.srt.models.deepseek_common.utils import (
|
||||
is_wint4afp8_or_wint4a16_config,
|
||||
)
|
||||
from sglang.srt.runtime_context import (
|
||||
get_exec,
|
||||
get_flags,
|
||||
get_forward,
|
||||
get_model,
|
||||
@@ -1783,6 +1784,7 @@ class DeepseekV2AttentionMLA(
|
||||
)
|
||||
self.use_min_latency_fused_a_gemm = (
|
||||
self.has_fused_proj
|
||||
and not get_exec().deterministic.enable_deterministic_inference
|
||||
and not self.is_packed_weight
|
||||
and fused_a_gemm_weight_eligible(self.fused_qkv_a_proj_with_mqa)
|
||||
)
|
||||
|
||||
@@ -82,6 +82,7 @@ from sglang.srt.utils.common import (
|
||||
is_npu,
|
||||
is_remote_url,
|
||||
is_sm90_supported,
|
||||
is_sm100_or_sm110_supported,
|
||||
is_sm100_supported,
|
||||
is_sm120_supported,
|
||||
is_xpu,
|
||||
@@ -7379,10 +7380,19 @@ class ServerArgs:
|
||||
|
||||
attention_backend = resolved_view(self).attention_backend
|
||||
if is_deepseek_model:
|
||||
deepseek_deterministic_attention_backends = ["fa3", "triton"]
|
||||
if attention_backend not in deepseek_deterministic_attention_backends:
|
||||
if (
|
||||
attention_backend
|
||||
not in RADIX_SUPPORTED_DETERMINISTIC_ATTENTION_BACKEND
|
||||
):
|
||||
raise ValueError(
|
||||
f"Currently only {deepseek_deterministic_attention_backends} attention backends are supported for deterministic inference with DeepSeek models. But you're using {attention_backend}."
|
||||
f"Currently only {RADIX_SUPPORTED_DETERMINISTIC_ATTENTION_BACKEND} attention backends are supported for deterministic inference with DeepSeek models. But you're using {attention_backend}."
|
||||
)
|
||||
if attention_backend == "fa4" and not is_sm100_or_sm110_supported():
|
||||
raise ValueError(
|
||||
"Deterministic inference with DeepSeek models on the fa4 "
|
||||
"attention backend requires SM100/SM110: it runs "
|
||||
"absorbed MLA, whose qv argument flash_attn.cute only "
|
||||
"implements on those archs."
|
||||
)
|
||||
|
||||
if attention_backend not in RADIX_SUPPORTED_DETERMINISTIC_ATTENTION_BACKEND:
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import logging
|
||||
import math
|
||||
from enum import IntEnum
|
||||
from typing import TYPE_CHECKING, List, Optional
|
||||
from typing import TYPE_CHECKING, List, Optional, Tuple
|
||||
|
||||
import torch
|
||||
|
||||
@@ -33,6 +33,7 @@ if TYPE_CHECKING:
|
||||
from sglang.srt.managers.tp_worker import TpModelWorker
|
||||
from sglang.srt.mem_cache.memory_pool import ReqToTokenPool
|
||||
from sglang.srt.model_executor.model_runner import ModelRunner
|
||||
from sglang.srt.sampling.sampling_batch_info import SamplingBatchInfo
|
||||
from sglang.srt.speculative.eagle_info import EagleVerifyInput
|
||||
|
||||
_is_cuda = is_cuda()
|
||||
@@ -566,6 +567,76 @@ def eagle_prepare_for_verify(
|
||||
return verify_forward_batch, can_run_cuda_graph
|
||||
|
||||
|
||||
def _seeded_verify_coins(
|
||||
*,
|
||||
sampling_seed: torch.Tensor,
|
||||
seq_lens: torch.Tensor,
|
||||
draft_token_num: int,
|
||||
device,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Derive deterministic verify-side coins from per-request sampling seeds.
|
||||
|
||||
Mirrors the main seeded-sampling path: murmur_hash32(seed, seq_lens,
|
||||
column) mapped to [0, 1). Columns [0, draft_token_num) drive the
|
||||
per-draft rejection coins; column draft_token_num drives the final
|
||||
fallback-sampling coin.
|
||||
|
||||
Scope: this seeds only the verify-side RNG. With rejection sampling the
|
||||
draft workers still pick candidates via unseeded multinomial
|
||||
(fast_sample in eagle_worker_v2), so that mode stays non-deterministic
|
||||
until the draft RNG is seeded in a follow-up; top-k/greedy draft
|
||||
selection is already deterministic.
|
||||
"""
|
||||
from sglang.kernels.ops.sampling.murmur_hash import murmur_hash32
|
||||
|
||||
cols = torch.arange(draft_token_num + 1, device=device, dtype=torch.int64)
|
||||
hashed = murmur_hash32(
|
||||
sampling_seed.to(torch.uint64), seq_lens.to(torch.uint64), cols
|
||||
)
|
||||
uniforms = hashed.to(torch.float64) / torch.iinfo(torch.uint32).max
|
||||
# The float32 cast rounds the top 129 uint32 hashes to exactly 1.0, but
|
||||
# the sampling kernels expect half-open [0, 1) coins: a 1.0 coin walks
|
||||
# past the last CDF bucket and can return a zero-probability token.
|
||||
# Clamp to the largest float32 below one; every other coin value is
|
||||
# untouched, so previously verified bitwise baselines stay intact.
|
||||
max_coin = 1.0 - 2**-24
|
||||
coins = (
|
||||
uniforms[:, :draft_token_num].to(torch.float32).clamp_(max=max_coin)
|
||||
).contiguous()
|
||||
coins_for_final_sampling = (
|
||||
uniforms[:, draft_token_num].to(torch.float32).clamp_(max=max_coin)
|
||||
).contiguous()
|
||||
return coins, coins_for_final_sampling
|
||||
|
||||
|
||||
def _verify_coins(
|
||||
*,
|
||||
sampling_info: SamplingBatchInfo,
|
||||
seq_lens: torch.Tensor,
|
||||
draft_token_num: int,
|
||||
candidates: torch.Tensor,
|
||||
device,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Rejection and final-sampling coins for verify: deterministic seeded
|
||||
coins when sampling_seed is set (see _seeded_verify_coins), torch.rand
|
||||
otherwise.
|
||||
"""
|
||||
if sampling_info.sampling_seed is not None:
|
||||
return _seeded_verify_coins(
|
||||
sampling_seed=sampling_info.sampling_seed,
|
||||
seq_lens=seq_lens,
|
||||
draft_token_num=draft_token_num,
|
||||
device=device,
|
||||
)
|
||||
# coins for rejection sampling
|
||||
coins = torch.rand_like(candidates, dtype=torch.float32, device=device)
|
||||
# coins for final sampling
|
||||
coins_for_final_sampling = torch.rand(
|
||||
(candidates.shape[0],), dtype=torch.float32, device=device
|
||||
)
|
||||
return coins, coins_for_final_sampling
|
||||
|
||||
|
||||
def eagle_sample(
|
||||
verify_input: EagleVerifyInput,
|
||||
batch: ScheduleBatch,
|
||||
@@ -717,10 +788,13 @@ def eagle_sample(
|
||||
"does not produce one (draft_probs missing or vocab-mismatched)."
|
||||
)
|
||||
|
||||
# coins for rejection sampling
|
||||
coins = torch.rand_like(candidates, dtype=torch.float32, device=device)
|
||||
# coins for final sampling
|
||||
coins_for_final_sampling = torch.rand((bs,), dtype=torch.float32, device=device)
|
||||
coins, coins_for_final_sampling = _verify_coins(
|
||||
sampling_info=sampling_info,
|
||||
seq_lens=batch.seq_lens,
|
||||
draft_token_num=verify_input.draft_token_num,
|
||||
candidates=candidates,
|
||||
device=device,
|
||||
)
|
||||
|
||||
sampling_fn = (
|
||||
chain_speculative_sampling_triton
|
||||
|
||||
@@ -288,6 +288,15 @@ is_sm100_supported = lru_cache(maxsize=1)(
|
||||
_check_cuda_device_version, device_capability_majors=[10], cuda_version=(12, 8)
|
||||
)
|
||||
)
|
||||
# Datacenter Blackwell (SM100) plus SM110; excludes consumer Blackwell (SM120).
|
||||
# This is the arch set flash_attn.cute accepts for the absorbed-MLA qv argument.
|
||||
is_sm100_or_sm110_supported = lru_cache(maxsize=1)(
|
||||
partial(
|
||||
_check_cuda_device_version,
|
||||
device_capability_majors=[10, 11],
|
||||
cuda_version=(12, 8),
|
||||
)
|
||||
)
|
||||
is_sm80_supported = lru_cache(maxsize=1)(
|
||||
partial(
|
||||
_check_cuda_device_version, device_capability_majors=[8], cuda_version=(11, 0)
|
||||
|
||||
Reference in New Issue
Block a user