[6/6][kimi-deterministic] Use deterministic seeded coins for EAGLE rejection sampling (#30822)

This commit is contained in:
Yuzhen Zhou
2026-07-24 02:11:21 -07:00
committed by GitHub
parent 3849beb7e3
commit b954e9cf3d
19 changed files with 562 additions and 23 deletions
@@ -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
+1 -2
View File
@@ -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")
+15
View File
@@ -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",
+2
View File
@@ -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)
)
+13 -3
View File
@@ -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:
+79 -5
View File
@@ -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
+9
View File
@@ -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)