hc_fused.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. """Hyper-connection mix statistics for prefill-size batches in one K walk (DSV41_HC_FUSED=1).
  2. The engine computes the hc pre/post/comb coefficients with a split-K Triton kernel
  3. (`_hc_mix_stats_partial_kernel`: 80 K slices of 256, one CTA per slice and row block, every
  4. slice's [rows, 24] partial written to memory) and a reduce+sinkhorn kernel that sums the 80
  5. partials in slice order. At prefill size (4096 rows) that is 1.48 ms per call on GB10, 86 calls
  6. per forward: the partial round trip is 63 MB of extra traffic next to the 168 MB of activations.
  7. Here one CTA walks all of K for its rows and keeps a running total, so no partials are stored.
  8. It reproduces the stock result bit for bit:
  9. * each slice's partial starts from zero and accumulates the same 64-wide chunks in order;
  10. the slice totals are added in slice order starting from zero, which is the stock reduce;
  11. * tf32x3 on a bf16 activation: Triton splits both operands into tf32 big+small and issues
  12. dot(a_small, b_big, 0) -> dot(a_big, b_small, .) -> dot(a_big, b_big, .) + acc. A bf16 value
  13. is exactly representable in tf32, so a_small is 0 and the first dot is exactly +0; this kernel
  14. issues the other two dots in the same order (two MMAs per chunk instead of three);
  15. * the row sum of squares follows the stock kernel's reduction tree (lanes hold k % 32, xor
  16. butterfly 16..1, then the two warps holding k // 32), spelled out with length-2 sums so the
  17. layout Triton picks here does not matter;
  18. * sinkhorn is the stock `_hc_mix_reduce_sinkhorn_kernel`, fed one "slice" (the total).
  19. Measured on GB10, CUDA graph of 43 layers, cold weights (rows: stock -> this, us per call):
  20. 512: 167 -> 161, 1024: 370 -> 250, 2048: 744 -> 421, 4096: 1482 -> 824. Below ~512 rows a
  21. single CTA per row block is latency-bound (K walk of 320 chunks), so smaller batches (decode,
  22. verify, draft) keep the stock kernels; DSV41_HC_FUSED_MIN_ROWS moves the cut.
  23. Drift guard: the stock kernel sources, the slice/tile constants and the Triton version are
  24. checked at install; any change raises (the exactness argument depends on all of them). The
  25. first fused call in a process (outside CUDA graph capture) also runs the stock path on the same
  26. inputs and permanently falls back to stock, loudly, if a single bit differs.
  27. """
  28. import hashlib
  29. import inspect
  30. import logging
  31. import os
  32. import torch
  33. import triton
  34. import triton.language as tl
  35. logger = logging.getLogger(__name__)
  36. ENABLED = os.environ.get("DSV41_HC_FUSED", "0").strip() not in ("0", "", "off", "false")
  37. MIN_ROWS = int(os.environ.get("DSV41_HC_FUSED_MIN_ROWS", "512"))
  38. # sha256[:16] of the stock sources this kernel reproduces (sglang dsv4.1 f80c91a4b), Triton 3.7.1
  39. _EXPECTED = {
  40. "_hc_mix_stats_partial_kernel": "869788ff0152119b",
  41. "_hc_mix_reduce_sinkhorn_kernel": "be4af40b07b7396d",
  42. "hc_mix_stats_sinkhorn": "a65077c012452c36",
  43. "_num_slices_for": "23e78e75dc040b8e",
  44. }
  45. _TRITON = "3.7.1"
  46. _K, _MIX, _HC, _BLOCK_K, _NUM_SLICES = 20480, 24, 4, 64, 80
  47. _state = {"checked": False, "disabled": False}
  48. @triton.jit
  49. def _tf32_rna(x):
  50. # the rounding Triton's tf32x3 lowering uses for the "big" operand
  51. return tl.inline_asm_elementwise(
  52. "cvt.rna.tf32.f32 $0, $1;", "=r,r", [x], dtype=tl.float32, is_pure=True, pack=1
  53. )
  54. @triton.jit
  55. def _sq_tree(v, BLOCK_M: tl.constexpr):
  56. """Row sums of a [BLOCK_M, 64] tile in the stock reduction tree (see module docstring)."""
  57. t = tl.reshape(v, [BLOCK_M, 2, 2, 2, 2, 2, 2]) # k bits 5..0
  58. t = tl.sum(t, axis=2) # xor 16
  59. t = tl.sum(t, axis=2) # xor 8
  60. t = tl.sum(t, axis=2) # xor 4
  61. t = tl.sum(t, axis=2) # xor 2
  62. t = tl.sum(t, axis=2) # xor 1
  63. return tl.sum(t, axis=1) # the two warps
  64. @triton.jit
  65. def _hc_mix_fullk_kernel(
  66. x_ptr, w_ptr, mix_ptr, sq_ptr, M, x_stride_m, w_stride_n,
  67. K: tl.constexpr, MIX: tl.constexpr, MIX_PAD: tl.constexpr, NUM_SLICES: tl.constexpr,
  68. BLOCK_M: tl.constexpr, BLOCK_K: tl.constexpr,
  69. ):
  70. pid = tl.program_id(0)
  71. offs_m = pid * BLOCK_M + tl.arange(0, BLOCK_M)
  72. offs_n = tl.arange(0, MIX_PAD)
  73. mask_m = offs_m < M
  74. mask_n = offs_n < MIX
  75. KB: tl.constexpr = K // NUM_SLICES // BLOCK_K # chunks per slice
  76. NCH: tl.constexpr = K // BLOCK_K
  77. acc = tl.zeros([BLOCK_M, MIX_PAD], dtype=tl.float32)
  78. tot = tl.zeros([BLOCK_M, MIX_PAD], dtype=tl.float32)
  79. sq = tl.zeros([BLOCK_M], dtype=tl.float32)
  80. sqt = tl.zeros([BLOCK_M], dtype=tl.float32)
  81. for i in range(NCH):
  82. offs_k = i * BLOCK_K + tl.arange(0, BLOCK_K)
  83. x_tile = tl.load(
  84. x_ptr + offs_m[:, None] * x_stride_m + offs_k[None, :],
  85. mask=mask_m[:, None], other=0.0,
  86. ).to(tl.float32)
  87. w_tile = tl.load(
  88. w_ptr + offs_n[None, :] * w_stride_n + offs_k[:, None],
  89. mask=mask_n[None, :], other=0.0,
  90. )
  91. w_big = _tf32_rna(w_tile)
  92. w_small = w_tile - w_big
  93. d = tl.dot(x_tile, w_small, input_precision="tf32")
  94. d = tl.dot(x_tile, w_big, d, input_precision="tf32")
  95. acc = acc + d
  96. sq += _sq_tree(x_tile * x_tile, BLOCK_M)
  97. last = (i % KB) == KB - 1
  98. tot = tl.where(last, tot + acc, tot)
  99. sqt = tl.where(last, sqt + sq, sqt)
  100. acc = tl.where(last, 0.0, acc)
  101. sq = tl.where(last, 0.0, sq)
  102. tl.store(mix_ptr + offs_m[:, None] * MIX + offs_n[None, :], tot,
  103. mask=mask_m[:, None] & mask_n[None, :])
  104. tl.store(sq_ptr + offs_m, sqt, mask=mask_m)
  105. def _config_for(m):
  106. """(BLOCK_M, num_warps, num_stages), tuned on GB10; any choice gives the same bits."""
  107. if m <= 2048:
  108. return 16, 4, 4
  109. if m <= 3072:
  110. return 16, 4, 3
  111. return 32, 4, 3
  112. def fused_mix_stats_sinkhorn(mod, x_flat, hc_fn, hc_scale, hc_base, hc_mult, sinkhorn_iters,
  113. rms_eps, hc_eps):
  114. m, k = x_flat.shape
  115. mix = hc_fn.shape[0]
  116. dev = x_flat.device
  117. pre = torch.empty(m, hc_mult, dtype=torch.float32, device=dev)
  118. post = torch.empty(m, hc_mult, dtype=torch.float32, device=dev)
  119. comb = torch.empty(m, hc_mult, hc_mult, dtype=torch.float32, device=dev)
  120. mixes = torch.empty((1, m, mix), dtype=torch.float32, device=dev)
  121. sq = torch.empty((1, m), dtype=torch.float32, device=dev)
  122. block_m, num_warps, num_stages = _config_for(m)
  123. _hc_mix_fullk_kernel[(triton.cdiv(m, block_m),)](
  124. x_flat, hc_fn, mixes, sq, m, x_flat.stride(0), hc_fn.stride(0),
  125. K=k, MIX=mix, MIX_PAD=32, NUM_SLICES=_NUM_SLICES, BLOCK_M=block_m, BLOCK_K=_BLOCK_K,
  126. num_warps=num_warps, num_stages=num_stages,
  127. )
  128. mod._hc_mix_reduce_sinkhorn_kernel[(m,)](
  129. mixes, sq, hc_scale.float().contiguous(), hc_base.float().contiguous(),
  130. pre, post, comb, m, 1.0 / k, rms_eps,
  131. MIX=mix, HC=hc_mult, NUM_SLICES=1, ITERS=sinkhorn_iters, EPS=hc_eps, num_warps=1,
  132. )
  133. return pre, post, comb
  134. try: # prefill sequence parallel: row gates key on the full chunk row count (prefill_sp.py)
  135. from prefill_sp import sp_logical_rows as _logical_rows
  136. except ImportError: # pragma: no cover
  137. def _logical_rows(x):
  138. return x.shape[0]
  139. def eligible(x_flat, hc_fn, hc_mult, min_rows=None):
  140. return (
  141. x_flat.dim() == 2
  142. and _logical_rows(x_flat) >= (MIN_ROWS if min_rows is None else min_rows)
  143. and x_flat.shape[1] == _K and x_flat.dtype == torch.bfloat16 and x_flat.stride(1) == 1
  144. and x_flat.is_cuda and hc_mult == _HC
  145. and hc_fn.shape == (_MIX, _K) and hc_fn.dtype == torch.float32 and hc_fn.stride(1) == 1
  146. )
  147. def check_engine(mod):
  148. """Raise if anything the exactness argument relies on differs from the audited engine."""
  149. for name, want in _EXPECTED.items():
  150. obj = getattr(mod, name, None)
  151. if obj is None:
  152. raise RuntimeError(f"hc_fused: engine has no {name}")
  153. src = inspect.getsource(getattr(obj, "fn", obj))
  154. got = hashlib.sha256(src.encode()).hexdigest()[:16]
  155. if got != want:
  156. raise RuntimeError(f"hc_fused: engine source drifted ({name} {got} != {want})")
  157. consts = (mod._HC_MIX_BLOCK_K, mod._HC_MIX_DOT_PRECISION, mod._HC_MIX_NUM_WARPS,
  158. mod._num_slices_for(_K))
  159. if consts != (_BLOCK_K, "tf32x3", 4, _NUM_SLICES):
  160. raise RuntimeError(f"hc_fused: engine constants drifted {consts}")
  161. if triton.__version__ != _TRITON:
  162. raise RuntimeError(f"hc_fused: Triton {triton.__version__} != {_TRITON}; lowering unaudited")
  163. def _self_check(stock, args):
  164. """First fused call: compare against stock on the same inputs; fall back for good on any bit."""
  165. _state["checked"] = True
  166. ref = stock(*args)
  167. got = fused_mix_stats_sinkhorn(_state["mod"], *args)
  168. if not all(torch.equal(a, b) for a, b in zip(ref, got)):
  169. _state["disabled"] = True
  170. logger.error("hc_fused: fused hc stats differ from stock on live inputs; using stock")
  171. print("DSV41 hc_fused: MISMATCH vs stock on first call, disabled", flush=True)
  172. else:
  173. print(f"DSV41 hc_fused: first call ({args[0].shape[0]} rows) bit-identical to stock", flush=True)
  174. return ref
  175. def install(mod):
  176. """Patch sglang.kernels.ops.layernorm.mhc.hc_mix_stats_sinkhorn (callers import it per call)."""
  177. if not ENABLED:
  178. return
  179. check_engine(mod)
  180. stock = mod.hc_mix_stats_sinkhorn
  181. _state["mod"] = mod
  182. def hc_mix_stats_sinkhorn(x_flat, hc_fn, hc_scale, hc_base, hc_mult, sinkhorn_iters,
  183. rms_eps, hc_eps):
  184. args = (x_flat, hc_fn, hc_scale, hc_base, hc_mult, sinkhorn_iters, rms_eps, hc_eps)
  185. if _state["disabled"] or not eligible(x_flat, hc_fn, hc_mult):
  186. return stock(*args)
  187. if not _state["checked"]:
  188. if torch.cuda.is_current_stream_capturing():
  189. return stock(*args) # never capture an unchecked kernel
  190. return _self_check(stock, args)
  191. return fused_mix_stats_sinkhorn(mod, *args)
  192. hc_mix_stats_sinkhorn.__wrapped__ = stock
  193. mod.hc_mix_stats_sinkhorn = hc_mix_stats_sinkhorn
  194. print(f"DSV41 hc_fused: hc mix stats for >= {MIN_ROWS} rows in one K walk", flush=True)