shared_pad_k.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. """Pad the shared expert's down_proj K from 576 to 640 so the shape goes back to b12x.
  2. DIAGNOSIS. At TP4 the shared expert's `down_proj` is N=5120, K=moe_intermediate_size/4
  3. = 576. FlashInfer's b12x MXFP8 kernel rejects any K that is not a multiple of 128
  4. (576 = 4.5 x 128), so this ONE shape in the whole model falls back to CUTLASS -- a
  5. 128x32x128 tile that pads M to 128 at decode (M=6), takes 96256 B of shared memory,
  6. and therefore fits one block per SM at 25% occupancy. The boot log names it:
  7. DSV41 MXFP8 backend 'b12x' rejected N=5120 K=576 (b12x mm_mxfp8 requires the
  8. contraction dim K to be a multiple of 128 (one full BK128 tile). Got K=576.)
  9. It is the only rejection in the model; `gate_up` has K=5120 and takes b12x normally.
  10. MEASUREMENT (43 weight copies so L2 is cold, CUDA graphs captured once, interleaved
  11. rounds, M=6):
  12. cutlass K=576 (production) 58.00 us/call 52.4 GB/s 2.494 ms/step
  13. b12x K=640 (this patch) 17.24 us/call 196.0 GB/s 0.742 ms/step -70.3%
  14. cutlass K=640 (control) 58.41 us/call 57.9 GB/s 2.512 ms/step +0.7%
  15. The third row is the important one: padding by itself buys nothing. The entire gain
  16. comes from the padded shape being eligible for b12x.
  17. LOSSLESS. Measured, not argued: b12x@640 against cutlass@576 is 0 of 30720 elements
  18. different bitwise, max |difference| 0.000e+00. The scale code used for the padding
  19. does not matter (checked 0, 127, 200) -- an e4m3 zero weight times any scale is zero.
  20. ON THE HEADLINE NUMBER. 92% of this kernel overlaps the routed experts' grouped GEMM,
  21. because the shared expert runs on alt_stream (deepseek_v2.py:1105). So 1.75 ms/step
  22. saved on the kernel is an UPPER BOUND, not a forecast. What settles it is the step.
  23. Measured on the live server at concurrency 1: DeviceGemmMxf disappears entirely
  24. (43 calls x 2.391 ms -> 0), dense_blockscaled grows 1.49 ms, net -0.90 ms of a 61.3 ms
  25. step = +1.47% predicted. Measured steady state (first run after each boot discarded,
  26. because FlashInfer autotunes during it and that run reads several percent off):
  27. prose c=1 49.81 / 51.69 -> 50.59 / 52.06 tok/s +1.14%
  28. code c=1 93.80 -> 95.22 tok/s +1.51%
  29. and greedy output is identical on the live server, 4 of 4 prompts, across two boots each
  30. way -- the same test reports 1 of 4 for a change that does alter the output, so it
  31. discriminates.
  32. WHAT THE PATCH DOES
  33. 1. Before `_prepare_block_fp8_as_mxfp8`, pads `weight` [5120,576]->[5120,640] with
  34. zeros and `weight_scale_inv` [160,18]->[160,20] with ones. SGLang then runs its
  35. own swizzle on the padded shape, so this uses EXACTLY the production path
  36. (block_fp8_scale_to_mxfp8_e8m0 + block_scale_interleave), not a copy of it.
  37. 2. At call time, pads the input [M,576]->[M,640] through a PERSISTENT, once-zeroed
  38. buffer (a CUDA graph needs a stable address; the tail is never written).
  39. Measurement showed that garbage or NaN in the tail of h does not corrupt the result,
  40. because it multiplies against zero weights -- but that is a property of the MXFP8
  41. quantizer rather than of the algebra, so the buffer is zeroed once at allocation
  42. anyway.
  43. DSV41_SHARED_PAD_K=1 enables it (OFF by default).
  44. DSV41_SHARED_PAD_BUF_ROWS sets the input buffer capacity (default 256 rows; anything
  45. larger takes a fallback that allocates on the fly, which is safe because prefill runs
  46. eager).
  47. Every shape or scale-layout mismatch RAISES instead of serving on: silent degradation
  48. under 32x32 scales is exactly what produced garbage in the wo_a fp8 path.
  49. """
  50. import logging
  51. import os
  52. import torch
  53. logger = logging.getLogger(__name__)
  54. HID = 5120 # hidden_size -- the N of the padded GEMM
  55. SRC_K = 576 # moe_intermediate_size / TP4
  56. TGT_K = 640 # 5 * 128, the smallest multiple of 128 above 576
  57. BLOCK = 32 # weight_block_size from config.json
  58. _bufs = {} # weight data_ptr -> persistent, zeroed input buffer
  59. _state = {"padded": 0, "logged_first": False, "logged_accept": False, "warned_big": False}
  60. def _enabled() -> bool:
  61. return os.environ.get("DSV41_SHARED_PAD_K", "0").strip() not in ("0", "off", "false", "")
  62. def _looks_like_shared_down(layer) -> bool:
  63. w = getattr(layer, "weight", None)
  64. return (w is not None and w.ndim == 2
  65. and tuple(w.shape) == (HID, SRC_K)
  66. and w.dtype is torch.float8_e4m3fn)
  67. def _pad_weight(layer, prefix: str) -> None:
  68. """weight [5120,576]->[5120,640] with zeros, weight_scale_inv [160,18]->[160,20] with ones."""
  69. w = layer.weight
  70. s = getattr(layer, "weight_scale_inv", None)
  71. if s is None:
  72. raise RuntimeError(
  73. f"{prefix}: shared-expert down_proj without weight_scale_inv -- "
  74. "this is not the block-fp8 path this patch expects"
  75. )
  76. n, k = w.shape
  77. want = (n // BLOCK, k // BLOCK)
  78. if tuple(s.shape) != want:
  79. raise RuntimeError(
  80. f"{prefix}: weight {(n, k)} requires scales {want}, found {tuple(s.shape)} -- "
  81. f"the block layout is not {BLOCK}x{BLOCK}, refusing to pad"
  82. )
  83. if s.dtype is not torch.float32:
  84. raise RuntimeError(
  85. f"{prefix}: weight_scale_inv has dtype {s.dtype}, expected float32 "
  86. "(block-fp8 keeps scales in fp32 and only then encodes them to e8m0)"
  87. )
  88. sf = s.data.detach().float().contiguous()
  89. bits = sf.view(torch.int32)
  90. if not bool((((bits & 0x7FFFFF) == 0) & (sf > 0)).all()):
  91. raise RuntimeError(
  92. f"{prefix}: weight_scale_inv is not made of positive powers of two, so it "
  93. "is not the ue8m0 layout this patch requires"
  94. )
  95. nw = torch.zeros((n, TGT_K), dtype=w.dtype, device=w.device)
  96. nw[:, :SRC_K] = w.data
  97. ns = torch.ones((want[0], TGT_K // BLOCK), dtype=s.dtype, device=s.device)
  98. ns[:, :want[1]] = s.data
  99. keep = getattr(s, "format_ue8m0", None)
  100. w.data = nw
  101. s.data = ns
  102. if keep is not None:
  103. s.format_ue8m0 = keep
  104. layer._dsv41_shared_pad_k = True
  105. _state["padded"] += 1
  106. if not _state["logged_first"]:
  107. _state["logged_first"] = True
  108. logger.warning(
  109. "DSV41 shared-expert K padding: %s padded %s -> %s, scales %s -> %s "
  110. "(SGLang runs the swizzle on the padded shape)",
  111. prefix, (n, SRC_K), (n, TGT_K), want, tuple(ns.shape),
  112. )
  113. def _pad_input(x: torch.Tensor, weight: torch.Tensor) -> torch.Tensor:
  114. """[M,576] -> [M,640] through a persistent zeroed buffer (stable address for CUDA graphs)."""
  115. x2 = x.view(-1, x.shape[-1])
  116. rows = x2.shape[0]
  117. cap = int(os.environ.get("DSV41_SHARED_PAD_BUF_ROWS", "256"))
  118. key = (weight.data_ptr(), x2.dtype, x2.device.index)
  119. buf = _bufs.get(key)
  120. if buf is None or buf.shape[0] < rows:
  121. if rows > cap:
  122. # Prefill is eager (prefill CUDA graph: disabled), so allocating here is
  123. # safe; the decode graph never reaches this branch.
  124. if not _state["warned_big"]:
  125. _state["warned_big"] = True
  126. logger.warning(
  127. "DSV41 shared-expert K padding: %d rows exceed the %d-row buffer, "
  128. "allocating on the fly. Expected for prefill, which runs eager; raise "
  129. "DSV41_SHARED_PAD_BUF_ROWS only if this appears for a decode batch.",
  130. rows, cap,
  131. )
  132. out = torch.zeros((rows, TGT_K), dtype=x2.dtype, device=x2.device)
  133. out[:, :SRC_K] = x2
  134. return out
  135. buf = torch.zeros((cap, TGT_K), dtype=x2.dtype, device=x2.device)
  136. _bufs[key] = buf
  137. buf[:rows, :SRC_K] = x2 # the tail is never written and stays zero
  138. return buf[:rows]
  139. def install_fp8(module):
  140. """Hook Fp8LinearMethod._prepare_block_fp8_as_mxfp8 -- pad BEFORE the scales are
  141. prepared, so the swizzle already runs on the 640 shape."""
  142. if not _enabled():
  143. return
  144. cls = getattr(module, "Fp8LinearMethod", None)
  145. if cls is None or not hasattr(cls, "_prepare_block_fp8_as_mxfp8"):
  146. raise RuntimeError(
  147. "DSV41 shared-expert K padding: Fp8LinearMethod._prepare_block_fp8_as_mxfp8 "
  148. "does not exist -- the MXFP8 path differs from what this assumes, not arming"
  149. )
  150. original = cls._prepare_block_fp8_as_mxfp8
  151. def prepare(self, layer):
  152. prefix = str(getattr(layer, "prefix", layer.__class__.__name__))
  153. if "shared_experts.down_proj" in prefix and not _looks_like_shared_down(layer):
  154. w = getattr(layer, "weight", None)
  155. raise RuntimeError(
  156. f"{prefix}: looks like the shared-expert down_proj, but the weight is "
  157. f"{None if w is None else (tuple(w.shape), w.dtype)} where "
  158. f"{((HID, SRC_K), torch.float8_e4m3fn)} was expected -- the TP sharding "
  159. "or the weight layout changed, refusing to skip silently"
  160. )
  161. if _looks_like_shared_down(layer) and not getattr(layer, "_dsv41_shared_pad_k", False):
  162. _pad_weight(layer, prefix)
  163. return original(self, layer)
  164. cls._prepare_block_fp8_as_mxfp8 = prepare
  165. logger.warning(
  166. "DSV41 shared-expert K padding ARMED: down_proj %s -> %s, to take this shape "
  167. "off the CUTLASS fallback and back onto b12x", (HID, SRC_K), (HID, TGT_K)
  168. )
  169. def install(module):
  170. """Hook fp8_utils.flashinfer_mxfp8_blockscaled_linear -- pad the input.
  171. Install AFTER mxfp8_b12x.install so this wraps its wrapper, not the original."""
  172. if not _enabled():
  173. return
  174. inner = module.flashinfer_mxfp8_blockscaled_linear
  175. def linear(input, weight, weight_scale, input_scale=None, bias=None,
  176. output_dtype=None, backend="cutlass", pin_tactic=False):
  177. if (weight.ndim == 2 and weight.shape[0] == HID and weight.shape[1] == TGT_K
  178. and input.shape[-1] == SRC_K):
  179. if input_scale is not None:
  180. raise RuntimeError(
  181. "DSV41 shared-expert K padding: the input is already quantized "
  182. "(input_scale != None) and the padding has to happen before "
  183. "quantization -- refusing, because the result would be wrong"
  184. )
  185. input = _pad_input(input, weight)
  186. if not _state["logged_accept"]:
  187. _state["logged_accept"] = True
  188. out = inner(input, weight, weight_scale, input_scale, bias,
  189. output_dtype, backend=backend, pin_tactic=pin_tactic)
  190. try:
  191. from mxfp8_b12x import _REJECTED
  192. accepted = (HID, TGT_K) not in _REJECTED
  193. except Exception:
  194. accepted = None
  195. logger.warning(
  196. "DSV41 shared-expert K padding: first call N=%d K=%d went through; "
  197. "b12x accepted the shape: %s (%d layers padded)",
  198. HID, TGT_K, accepted, _state["padded"],
  199. )
  200. return out
  201. return inner(input, weight, weight_scale, input_scale, bias,
  202. output_dtype, backend=backend, pin_tactic=pin_tactic)
  203. module.flashinfer_mxfp8_blockscaled_linear = linear