执行摘要
- 一句话:使 EAGLE 验证阶段使用确定性种子硬币,实现推测解码可重现
- 推荐动作:本 PR 是 kimi-deterministic 系列的核心部分,值得精读。关键设计决策(哈希替代随机、浮点边界 clamp、列空间契约)展示了对采样内核确切行为的细致验证。建议阅读时关注
_seeded_verify_coins 的实现和测试文件中的结构化哈希验证方法,这对于构建确定性采样逻辑具有通用借鉴价值。同时注意配套的 MLP-sync 和 FA4 修正是确保整体确定性的必要前提。
功能与动机
EAGLE 验证阶段在拒绝采样中使用的硬币来自 torch.rand,因此即使为每个请求设置了 sampling_seed,推测输出仍无法跨运行重现。该 PR 是 kimi-deterministic 系列(共 6 个 PR)的第 6 部分,目标是通过确定性硬币使验证阶段 RNG 可种子化,从而在 top-k/greedy 草稿选择下实现完全确定性推断。
实现拆解
- 添加
_seeded_verify_coins 函数(python/sglang/srt/speculative/eagle_utils.py):使用 Triton 内核 murmur_hash32 基于种子、序列长度和列索引生成哈希,并转换为 [0,1) 区间内的浮点数。列 [0, draft_token_num) 用于拒绝采样,列 draft_token_num 用于最终采样。
- 修改
_verify_coins 函数:判断是否设置 sampling_seed,若已设置则调用 _seeded_verify_coins,否则回退到 torch.rand。
- 处理 float32 边界精度问题:将哈希值从 uint32 转换为 float32 时,前 129 个哈希值会向上舍入到 1.0,而采样内核期望半开区间
[0, 1)。通过 clamp(max=1.0-2**-24) 将最大值约束为 float32 中小于 1 的最大值。
- 更新调用入口:在
eagle_sample 函数中,用新的 _verify_coins 替换直接调用 torch.rand_like 的逻辑。
- 添加单元测试(
test/registered/unit/spec/test_eagle_seeded_coins.py):覆盖种子可重现性、不同种子/序列长度的发散性、列分割映射(结构化哈希验证)、未设置种子时仍使用 torch.rand,以及种子请求路由到 _seeded_verify_coins。
- 配套调整:包括 MLP-sync pad/unpad 的修正(在 DP attention 下正确切片位置/序列长度张量)、TBO 子批处理清理新增的
_original_num_tokens 字段、FA4 注意后在 Blackwell 老架构上的 MLA 分派门控、确定性推理下禁用 fused_qkv_a_proj_with_mqa 内核、DeepGEMM BF16 路径支持确定性模式、以及解码 logprobs 改用 F.log_softmax 以实现与 prefill 的比特级别对齐。
关键文件:
python/sglang/srt/speculative/eagle_utils.py(模块 推测解码;类别 source;类型 core-logic;符号 _seeded_verify_coins, _verify_coins, eagle_sample): 核心变更所在:新增 _seeded_verify_coins 和修改 _verify_coins,实现确定性硬币生成。
test/registered/unit/spec/test_eagle_seeded_coins.py(模块 推测解码;类别 test;类型 test-coverage;符号 TestSeededVerifyCoins, test_seeded_coins_are_reproducible, test_distinct_seeds_or_positions_diverge, test_column_split_maps_rejection_then_final): 新增单元测试,锁定确定性硬币的各项契约:可重现性、种子/长度发散、列分割、未种子回退。
python/sglang/srt/models/deepseek_common/attention_backend_handler.py(模块 注意力机制;类别 source;类型 data-contract;符号 handle_attention_fa4): FA4 注意力后端确定性支持:在 Blackwell 架构上启用 MLA 分派,其他架构保留 MHA_chunked_KV 回退。
python/sglang/srt/server_args.py(模块 配置管理;类别 source;类型 core-logic;符号 _handle_deterministic_inference): 确定性推理参数验证:强制 DeepSeek 模型在确定性模式下使用支持的注意力后端,并针对 FA4 检查架构。
test/registered/unit/model_executor/test_mlp_sync_pad_unpad.py(模块 DP注意力;类别 test;类型 test-coverage;符号 TestMlpSyncPadUnpad, test_decode_post_forward_unpads_per_request_tensors, test_extend_post_forward_unpads_positions): 新增针对 DP attention 下 MLP-sync pad/unpad 的单元测试,确保 post-forward 切片正确性,避免影响种子采样。
关键符号:_seeded_verify_coins, _verify_coins, eagle_sample, handle_attention_fa4, _handle_deterministic_inference
关键源码片段
python/sglang/srt/speculative/eagle_utils.py
核心变更所在:新增 _seeded_verify_coins 和修改 _verify_coins,实现确定性硬币生成。
# python/sglang/srt/speculative/eagle_utils.py (head excerpt)
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.
"""
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, 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((bs,), dtype=torch.float32, device=device)
return coins, coins_for_final_sampling
test/registered/unit/spec/test_eagle_seeded_coins.py
新增单元测试,锁定确定性硬币的各项契约:可重现性、种子/长度发散、列分割、未种子回退。
# test/registered/unit/spec/test_eagle_seeded_coins.py
class TestSeededVerifyCoins(CustomTestCase):
def test_seeded_coins_are_reproducible(self):
"""相同种子和序列长度产生比特完全相同的硬币"""
coins_a, final_a = _coins([12345, 67890, 12345], [7, 9, 7])
coins_b, final_b = _coins([12345, 67890, 12345], [7, 9, 7])
self.assertEqual(coins_a.shape, (3, DRAFT_TOKEN_NUM))
self.assertEqual(final_a.shape, (3,))
self.assertTrue(torch.equal(coins_a, coins_b))
self.assertTrue(torch.equal(final_a, final_b))
# 相同 (seed, seq_len) 对哈希相同,与行号无关
self.assertTrue(torch.equal(coins_a[0], coins_a[2]))
self.assertEqual(final_a[0].item(), final_a[2].item())
# 硬币应在 [0, 1) 内
self.assertTrue(bool((coins_a >= 0).all() and (coins_a < 1).all()))
self.assertTrue(bool((final_a >= 0).all() and (final_a < 1).all()))
def test_distinct_seeds_or_positions_diverge(self):
"""不同种子或不同长度产生不同的硬币"""
coins, final = _coins([12345, 67890, 12345], [7, 9, 11])
self.assertFalse(torch.equal(coins[0], coins[1])) # 不同种子
self.assertFalse(torch.equal(coins[0], coins[2])) # 不同 seq_len
def test_column_split_maps_rejection_then_final(self):
"""列分割契约:列 [0, draft_token_num) 为拒绝硬币,最后一列为最终采样硬币"""
umax = torch.iinfo(torch.uint32).max
def _structured_hash(seed, positions, col_indices):
rows = torch.arange(seed.shape[0], device=seed.device).unsqueeze(1)
return (rows * 1000 + col_indices.unsqueeze(0)).to(torch.uint32)
with patch(
"sglang.kernels.ops.sampling.murmur_hash.murmur_hash32",
side_effect=_structured_hash,
):
coins, final = _coins([1, 2], [3, 4])
def _expected(row, col):
return (
torch.tensor(row * 1000 + col, dtype=torch.float64)
.div(umax)
.to(torch.float32)
.item()
)
for row in range(2):
for col in range(DRAFT_TOKEN_NUM):
self.assertEqual(coins[row, col].item(), _expected(row, col))
self.assertEqual(final[row].item(), _expected(row, DRAFT_TOKEN_NUM))
def test_unseeded_requests_keep_torch_rand(self):
"""未设置种子时,硬币仍由 torch.rand 生成,两次调用不同"""
with patch(
"sglang.kernels.ops.sampling.murmur_hash.murmur_hash32"
) as mock_hash:
coins_a, final_a = _verify_coins(...) # 未设置种子
coins_b, final_b = _verify_coins(...)
mock_hash.assert_not_called()
self.assertFalse(torch.equal(coins_a, coins_b))
评论区精华
审查者 ch-wan 提出了三个核心评论:
- 草稿路径非确定性提醒:指出即使验证硬币确定,若使用 rejection sampling 草稿模式(
fast_sample/torch.multinomial),不同运行仍会产生不同草稿,从而影响最终输出。作者回应当前更改仅覆盖验证侧 RNG,草稿侧随机性需后续 PR 处理。
- 建议增加单元测试:要求针对固定
sampling_seed 检查硬币可重现性、未设置种子仍用 torch.rand 以及列分割契约。作者添加了 test_eagle_seeded_coins.py,包含结构化哈希模拟来锁定列行为。
-
float32 精度边界 bug:指出 hashed / UINT32_MAX 转换为 float32 时,最大 129 个 uint32 值会舍入为 1.0,而采样内核期望 [0, 1),可能导致选择零概率 token。作者在后续提交中通过 clamp(max=1.0 - 2**-24) 修复此问题。
-
草稿路径非确定性提醒 (design): 作者确认当前更改仅覆盖验证侧 RNG,草稿侧需后续 PR 处理,PR body 已明确说明范围。
- 建议增加单元测试 (testing): 作者添加了 test/registered/unit/spec/test_eagle_seeded_coins.py,覆盖所有建议点。
- float32 精度边界 bug (correctness): 作者在后续提交中通过 clamp(max=1.0-2**-24) 修复,确保硬币严格在 [0, 1) 内。
风险与影响
关联脉络
- PR #30821 [5/6][kimi-deterministic] Compute decode logprobs via F.log_softmax: 本 PR 的前置依赖,同一系列的第 5 部分,实现解码 logprobs 的确定性对齐。
- PR #30820 [4/6][kimi-deterministic] MLP-sync unpad and TBO child batch fix: 同一系列的第 4 部分,修复 DP attention 下 MLP-sync 后处理问题。
参与讨论