draft_head_fp8.py 4.5 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. """The DSpark draft's LM head from an fp8 copy of the (shared) target LM head. Default OFF.
  2. DSV41_DRAFT_HEAD_FP8=1. The draft borrows the target's vocab-parallel lm_head (bf16, 32320 x 5120
  3. per rank at TP4) and reads it every step for its 5 block rows. Only the DRAFT gets the fp8 copy
  4. (e4m3 + one power-of-two scale per 32x32 block): the target's logits, and therefore every accepted
  5. token, are untouched. Offline the draft's acceptance moved 3.0882 -> 3.0875 tokens/step.
  6. """
  7. import os
  8. import torch
  9. import triton
  10. import triton.language as tl
  11. ENABLED = os.environ.get("DSV41_DRAFT_HEAD_FP8", "0").strip() not in ("0", "", "off", "false")
  12. MAX_M = 64
  13. _TWIN = {} # lm_head weight data_ptr -> (e4m3, exponent)
  14. @triton.jit
  15. def _head_fp8_kernel(X, W8, S, Y, M, N, K: tl.constexpr, BN: tl.constexpr, BK: tl.constexpr):
  16. pid = tl.program_id(0)
  17. m = tl.program_id(1) * 16 + tl.arange(0, 16)
  18. n = pid * BN + tl.arange(0, BN)
  19. nmask = n < N
  20. acc = tl.zeros((16, BN), tl.float32)
  21. for k0 in range(0, K, BK):
  22. k = k0 + tl.arange(0, BK)
  23. x = tl.load(X + m[:, None] * K + k[None, :], m[:, None] < M, 0.0)
  24. w8 = tl.load(W8 + n[None, :] * K + k[:, None], nmask[None, :], 0.0)
  25. e = tl.load(S + (n[None, :] // 32) * (K // 32) + k[:, None] // 32, nmask[None, :], 127)
  26. w = (w8.to(tl.float32) * tl.exp2(e.to(tl.float32) - 127.0)).to(tl.bfloat16)
  27. acc += tl.dot(x, w)
  28. tl.store(Y + m[:, None] * N + n[None, :], acc, (m[:, None] < M) & nmask[None, :])
  29. def make_twin(w: torch.Tensor):
  30. """bf16 [N, K] -> (e4m3 [N, K], exponent uint8 [ceil(N/32), K/32]); lossy, draft-only."""
  31. n, k = w.shape
  32. pad = (-n) % 32
  33. wf = torch.nn.functional.pad(w.float(), (0, 0, 0, pad)).view((n + pad) // 32, 32, k // 32, 32)
  34. amax = wf.abs().amax(dim=(1, 3)).clamp_min(2.0 ** -126)
  35. e = torch.ceil(torch.log2(amax / 448.0)).clamp(-127, 127)
  36. q = (wf / torch.exp2(e)[:, None, :, None]).to(torch.float8_e4m3fn)
  37. return q.view(n + pad, k)[:n].contiguous(), (e + 127).to(torch.uint8).contiguous()
  38. def head_fp8(x: torch.Tensor, twin) -> torch.Tensor:
  39. """x [M, K] bf16 (M <= 16) -> fp32 logits [M, N]."""
  40. w8, s = twin
  41. m, k = x.shape
  42. n = w8.shape[0]
  43. y = torch.empty((m, n), dtype=torch.float32, device=x.device)
  44. # tuned on GB10 at M=5: BN 32 / BK 256 / 4 warps / 3 stages = 230 GB/s of fp8 (bf16 cuBLAS: 1405 us -> 721 us)
  45. _head_fp8_kernel[(triton.cdiv(n, 32), triton.cdiv(m, 16))](x.contiguous(), w8, s, y, m, n, k, 32, 256,
  46. num_warps=4, num_stages=3)
  47. return y
  48. def install(dspark_module):
  49. """sglang.srt.models.deepseek_v4_dspark: fp8 twin built when the target's lm_head is attached,
  50. used by _logits_from_x_post_hc for up to MAX_M rows; everything else is the stock path."""
  51. if not ENABLED:
  52. return
  53. cls = dspark_module.DeepseekV4ForCausalLMDSpark
  54. for name in ("attach_shared_modules", "_logits_from_x_post_hc"):
  55. if not hasattr(cls, name):
  56. raise RuntimeError(f"DSV41_DRAFT_HEAD_FP8: DeepseekV4ForCausalLMDSpark.{name} is gone; engine drifted")
  57. if getattr(cls, "_dsv41_draft_head_fp8", False):
  58. return
  59. cls._dsv41_draft_head_fp8 = True
  60. orig_attach = cls.attach_shared_modules
  61. orig_logits = cls._logits_from_x_post_hc
  62. gather = getattr(dspark_module, "gather_and_crop_vocab")
  63. def attach_shared_modules(self, *a, **kw):
  64. out = orig_attach(self, *a, **kw)
  65. w = getattr(self.lm_head, "weight", None)
  66. if w is not None and w.dtype == torch.bfloat16 and w.dim() == 2 and w.shape[1] % 32 == 0:
  67. _TWIN[w.data_ptr()] = make_twin(w.data)
  68. torch.cuda.empty_cache() # the fp32 temporaries must not shrink the KV pool
  69. print(f"[draft_head_fp8] fp8 twin of the draft LM head {tuple(w.shape)}", flush=True)
  70. return out
  71. def _logits_from_x_post_hc(self, x_post_hc):
  72. w = getattr(self.lm_head, "weight", None)
  73. twin = None if w is None else _TWIN.get(w.data_ptr())
  74. if twin is None or x_post_hc.shape[0] > MAX_M or x_post_hc.shape[0] == 0:
  75. return orig_logits(self, x_post_hc)
  76. x = self.stages[-1].norm(x_post_hc)
  77. local = head_fp8(x.to(torch.bfloat16), twin).to(w.dtype)
  78. if self._opt_markov_w2_tp_shard:
  79. return local
  80. return gather(local, self.lm_head)
  81. cls.attach_shared_modules = attach_shared_modules
  82. cls._logits_from_x_post_hc = _logits_from_x_post_hc
  83. print("[draft_head_fp8] armed", flush=True)