block_verify.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. """Block verification for DSpark sampled decoding (Sun et al., "Block Verification Accelerates
  2. Speculative Decoding", ICLR 2025). Default OFF. Lossless: the output distribution is the target's.
  3. DSV41_BLOCK_VERIFY=1 replaces the chain (token-by-token) rejection sampler used for sampled rows.
  4. Token verification accepts draft i with min(1, p_i/q_i) independently; block verification decides
  5. the accepted length jointly over the block and is provably optimal for a single draft chain:
  6. w_0 = 1, w_i = min(w_{i-1} * p_i(X_i) / q_i(X_i), 1)
  7. h_i = S_i / (S_i + 1 - w_i), S_i = sum_x max(w_i * p_{i+1}(x) - q_{i+1}(x), 0) (i < gamma)
  8. h_gamma = w_gamma
  9. tau = max{ i : eta_i <= h_i } (0 if none), eta_i ~ U(0,1)
  10. correction token ~ normalise(max(w_tau * p_{tau+1} - q_{tau+1}, 0)) (tau < gamma)
  11. ~ p_{gamma+1} (tau = gamma)
  12. p_i = target distribution after temperature / top-k / top-p (the same one the chain sampler uses),
  13. q_i = the distribution the draft token was actually sampled from.
  14. Measured offline on engine-exact p and q (capture schema v4, coding-with-thinking traffic):
  15. 2.927 -> 2.999 tokens per step (+2.5 %).
  16. Random draws come from torch's device generator in the same order on every TP rank, exactly like
  17. the stock sampler's torch.rand calls, so all ranks take the same decision.
  18. """
  19. import os
  20. import torch
  21. ENABLED = os.environ.get("DSV41_BLOCK_VERIFY", "0").strip() not in ("0", "", "off", "false")
  22. def block_accept_probs(X, target_probs, draft_probs, eta=None, gumbel=None, live=None):
  23. """X [bs, g] drafted tokens, target_probs [bs, g+1, V], draft_probs [bs, g, V].
  24. live [bs] (optional): drafts actually verified per request (<= g); positions beyond it are
  25. treated as never drafted (q = 0, p(X) = 0), which is the block rule for that shorter block.
  26. Returns (tau [bs] int64, correction token [bs] int64)."""
  27. bs, g = X.shape
  28. dev = X.device
  29. p_tok = target_probs[:, :g].gather(-1, X.unsqueeze(-1)).squeeze(-1).float()
  30. q_tok = draft_probs.gather(-1, X.unsqueeze(-1)).squeeze(-1).float()
  31. ratio = p_tok / q_tok.clamp_min(1e-30)
  32. if live is not None:
  33. alive = torch.arange(g, device=dev).view(1, -1) < live.view(-1, 1)
  34. ratio = torch.where(alive, ratio, torch.zeros_like(ratio))
  35. draft_probs = draft_probs * alive.unsqueeze(-1).to(draft_probs.dtype)
  36. w = torch.cumprod(ratio, dim=1)
  37. # w_i = min(w_{i-1} * r_i, 1) is not a plain cumprod once it saturates; run the recursion
  38. ws = []
  39. cur = torch.ones(bs, device=dev)
  40. for i in range(g):
  41. cur = torch.minimum(cur * ratio[:, i], torch.ones_like(cur))
  42. ws.append(cur)
  43. w = torch.stack(ws, 1) # [bs, g] = w_1..w_g
  44. h = torch.empty(bs, g, device=dev)
  45. if g > 1:
  46. s = (w[:, :g - 1, None] * target_probs[:, 1:g].float() - draft_probs[:, 1:g].float()).clamp_min(0).sum(-1)
  47. h[:, :g - 1] = s / (s + 1 - w[:, :g - 1]).clamp_min(1e-30)
  48. h[:, g - 1] = w[:, g - 1]
  49. if eta is None:
  50. eta = torch.rand(bs, g, device=dev)
  51. ok = eta <= h # [bs, g]
  52. if live is not None:
  53. ok = ok & alive
  54. idx = torch.arange(1, g + 1, device=dev).view(1, -1)
  55. tau = torch.where(ok, idx, torch.zeros_like(idx)).max(dim=1).values # [bs]
  56. # correction distribution at position tau (0-based row tau of target / draft)
  57. w_all = torch.cat([torch.ones(bs, 1, device=dev), w], dim=1) # w_0..w_g
  58. wt = w_all.gather(1, tau.view(-1, 1)) # [bs, 1]
  59. rows = tau.view(-1, 1, 1).expand(-1, 1, target_probs.shape[-1])
  60. p_row = target_probs.gather(1, rows).squeeze(1).float() # [bs, V]
  61. q_pad = torch.cat([draft_probs.float(), torch.zeros_like(draft_probs[:, :1]).float()], dim=1)
  62. q_row = q_pad.gather(1, rows).squeeze(1) # 0 for tau == g
  63. res = (wt * p_row - q_row).clamp_min(0)
  64. mass = res.sum(-1, keepdim=True)
  65. res = torch.where(mass > 0, res / mass.clamp_min(1e-30), p_row) # degenerate: p == q
  66. if gumbel is None:
  67. gumbel = torch.empty_like(res).exponential_()
  68. corr = (res / gumbel).argmax(dim=-1)
  69. return tau, corr
  70. def install(accept_module):
  71. """sglang.kernels.ops.speculative.dspark.dspark_accept: swap AcceptSampling for sampled rows."""
  72. if not ENABLED:
  73. return
  74. cls = getattr(accept_module, "AcceptSampling", None)
  75. core = getattr(accept_module, "_accept_sampling_core", None)
  76. if cls is None or core is None:
  77. raise RuntimeError("DSV41_BLOCK_VERIFY: AcceptSampling/_accept_sampling_core gone; engine drifted")
  78. if getattr(cls, "_dsv41_block_verify", False):
  79. return
  80. cls._dsv41_block_verify = True
  81. softmax_temp = accept_module.SoftmaxTemp
  82. import sglang.srt.speculative.dflash_utils as du
  83. def execute(*, candidates, target_logits, draft_probs, sampling_info, draft_input, gamma,
  84. verify_num_draft_tokens, cutoff_verify_lens=None):
  85. if cutoff_verify_lens is not None and os.environ.get("DSV41_VERIFY_CAP", "").strip() in ("", "0", "off"):
  86. return cls._dsv41_orig_execute(candidates=candidates, target_logits=target_logits,
  87. draft_probs=draft_probs, sampling_info=sampling_info,
  88. draft_input=draft_input, gamma=gamma,
  89. verify_num_draft_tokens=verify_num_draft_tokens,
  90. cutoff_verify_lens=cutoff_verify_lens)
  91. bs = candidates.shape[0]
  92. if not sampling_info.need_top_k_sampling and not sampling_info.need_top_p_sampling:
  93. target_probs = softmax_temp.execute(logits=target_logits, temperatures=sampling_info.temperatures,
  94. rows_per_request=verify_num_draft_tokens
  95. ).view(bs, verify_num_draft_tokens, -1)
  96. else:
  97. target_probs = du.build_dflash_verify_target_probs(
  98. next_token_logits=target_logits, sampling_info=sampling_info,
  99. draft_token_num=verify_num_draft_tokens, bs=bs, max_top_k=draft_input.max_top_k,
  100. uniform_top_k_value=draft_input.uniform_top_k_value)
  101. X = candidates.view(bs, verify_num_draft_tokens)[:, 1:gamma + 1].long()
  102. live = None if cutoff_verify_lens is None else (cutoff_verify_lens[:bs].to(torch.int64) - 1)
  103. tau, corr = block_accept_probs(X, target_probs.view(bs, verify_num_draft_tokens, -1),
  104. draft_probs.view(bs, gamma, -1), live=live)
  105. return tau.to(torch.int32), corr.to(torch.int64), torch.zeros(bs, dtype=torch.int32, device=candidates.device)
  106. cls._dsv41_orig_execute = cls.execute
  107. cls.execute = staticmethod(execute)
  108. print("[block_verify] armed: block verification for sampled DSpark rows", flush=True)