l2_prefetch.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. """Prefetch the next dense weights into L2 while a RoCE collective runs (decode only).
  2. Every decode step spends ~3.6 ms (c1, rank 1-3 profile 2026-09-24) inside ~89 one-shot
  3. all-reduces and ~49 all-gathers during which no other kernel runs: the RoCE kernel is a 1-8 CTA
  4. grid polling flags, so the SMs and DRAM are idle. The kernel that follows each window is a dense
  5. MXFP8 GEMM (post-MoE AR -> next layer's wqkv_a; all-gather -> wq_b) or the router + shared expert
  6. (post-attention AR), all weight-bound at M<=16. This adapter issues
  7. ``cp.async.bulk.prefetch.L2`` for the first DSV41_L2_PREFETCH_MB of those weights right before
  8. the collective is launched, so the GEMM's first K tiles come from L2. The prefetch kernel runs on
  9. a side stream forked right before the collective (issuing 8 MB of bulk prefetches takes ~20 us
  10. because the TMA queue drains at DRAM speed, so it must not sit on the main stream) and is joined
  11. at the next collective or at the end of the forward, long after it finished. Weights are static and a
  12. prefetch is only a cache hint: outputs are bit-identical by construction.
  13. Which weights follow which collective is learned, not hard-coded: during the eager warm-up
  14. forwards that SGLang runs before capturing each CUDA graph, the collectives are numbered per
  15. forward and every MXFP8 dense linear records itself under the last collective. The capture pass
  16. (the same Python sequence) then forks the prefetch right before the collective, so it becomes a
  17. graph branch. A forward whose signature was never learned prefetches nothing; weights are static,
  18. so a stale plan can only waste bandwidth, never change a result.
  19. Gate ``DSV41_L2_PREFETCH=1`` (off by default). ``DSV41_L2_PREFETCH_MB`` budget per window
  20. (default 6), ``DSV41_L2_PREFETCH_AG=0`` skips all-gather windows.
  21. PROTOTYPE, not wired into sitecustomize.py yet. Wiring (all three gated by the env flag):
  22. sglang.srt.layers.quantization.fp8_utils -> l2_prefetch.install_fp8_utils(module) (after mxfp8_b12x)
  23. b12x.comm.roce.roce_oneshot -> l2_prefetch.install_roce(module)
  24. sglang.srt.models.deepseek_v4 -> l2_prefetch.install_model(module)
  25. sglang.srt.models.deepseek_v2 -> l2_prefetch.install_router(module)
  26. Microbench and go/no-go: sparks/diagnostics/dsv41-l2-pdl/RESULTS.md.
  27. """
  28. import ctypes as C
  29. import logging
  30. import os
  31. import subprocess
  32. from pathlib import Path
  33. import torch
  34. logger = logging.getLogger(__name__)
  35. _SRC = r"""
  36. #include <cuda_runtime.h>
  37. #include <stdint.h>
  38. // segs: [n][2] int64 {address, bytes}; one bulk prefetch per <=chunk piece.
  39. __global__ void l2pf(const long long* __restrict__ segs, int n, long long chunk) {
  40. for (int s = blockIdx.x; s < n; s += gridDim.x) {
  41. long long a = segs[2 * s], b = segs[2 * s + 1];
  42. for (long long off = threadIdx.x * chunk; off < b; off += (long long)blockDim.x * chunk) {
  43. long long sz = b - off < chunk ? b - off : chunk;
  44. sz &= ~15LL;
  45. if (sz > 0)
  46. asm volatile("cp.async.bulk.prefetch.L2.global [%0], %1;" :: "l"(a + off), "r"((unsigned)sz) : "memory");
  47. }
  48. }
  49. }
  50. extern "C" int dsv41_l2pf(const void* segs, int n, long long chunk, uintptr_t stream) {
  51. l2pf<<<n < 8 ? n : 8, 64, 0, (cudaStream_t)stream>>>((const long long*)segs, n, chunk);
  52. return (int)cudaGetLastError();
  53. }
  54. """
  55. _state = {"lib": None, "learning": {}, "plans": {}, "sig": None, "idx": 0, "side": {},
  56. "pending": None, "stats": [0, 0], "installed": set(), "logged": set(), "frozen": set(),
  57. "keepalive": []}
  58. _CHUNK = 16384
  59. def enabled():
  60. return os.environ.get("DSV41_L2_PREFETCH", "0").strip() in ("1", "on", "true")
  61. def _budget():
  62. return int(float(os.environ.get("DSV41_L2_PREFETCH_MB", "6")) * (1 << 20))
  63. def _lib():
  64. if _state["lib"] is None:
  65. cache = Path(os.environ.get("B12X_COMPILE_CACHE_DIR", "/tmp")) / "dsv41_l2pf"
  66. cache.mkdir(parents=True, exist_ok=True)
  67. so = cache / "libdsv41_l2pf.so"
  68. if not so.exists():
  69. cu = cache / "l2pf.cu"
  70. cu.write_text(_SRC)
  71. tmp = cache / f"libdsv41_l2pf.{os.getpid()}.so"
  72. subprocess.run(["nvcc", "-O3", "-arch=sm_121a", "-shared", "-Xcompiler", "-fPIC",
  73. "-o", str(tmp), str(cu)], check=True)
  74. os.replace(tmp, so)
  75. lib = C.CDLL(str(so))
  76. lib.dsv41_l2pf.argtypes = [C.c_void_p, C.c_int, C.c_longlong, C.c_void_p]
  77. _state["lib"] = lib
  78. return _state["lib"]
  79. # -- forward bracketing -------------------------------------------------------------------------
  80. def _begin_forward(sig):
  81. """Every eager decode/verify forward of a shape that has not been captured yet re-learns its
  82. plan. The first eager forward of a shape can be a dummy/profiling run outside the graph-capture
  83. communicator state, whose all-reduces go through NCCL and never reach RoCE (seen 2026-09-25:
  84. the bs16 verify shape learned 0 collectives and ran uncaptured-plan). The two warm-up forwards
  85. SGLang runs right before each capture are what the capture replays, so the latest eager
  86. observation wins, and a shape is frozen once captured."""
  87. _state["sig"], _state["idx"] = sig, 0
  88. _state["stats"][1] = 0
  89. if sig not in _state["frozen"] and not torch.cuda.is_current_stream_capturing():
  90. _state["learning"][sig] = {}
  91. def _join():
  92. pending = _state["pending"]
  93. if pending is not None:
  94. torch.cuda.current_stream().wait_stream(pending)
  95. _state["pending"] = None
  96. def _side_stream():
  97. dev = torch.cuda.current_device()
  98. if dev not in _state["side"]:
  99. _state["side"][dev] = torch.cuda.Stream(device=dev)
  100. return _state["side"][dev]
  101. def _end_forward():
  102. _join()
  103. sig = _state["sig"]
  104. capturing = torch.cuda.is_current_stream_capturing()
  105. rec = _state["learning"].pop(sig, None) if sig is not None else None
  106. if rec is not None and not capturing:
  107. n = _state["idx"]
  108. key = (n, tuple((i, tuple(v)) for i, v in sorted(rec.items())))
  109. old = _state["plans"].get(sig)
  110. if n == 0:
  111. # no RoCE collective seen (NCCL path): keep any earlier plan, never learn "nothing"
  112. if old is None and ("zero",) + sig[1:] not in _state["logged"]:
  113. _state["logged"].add(("zero",) + sig[1:])
  114. logger.warning("DSV41_L2_PREFETCH eager forward tokens=%d mode=%s saw no RoCE collective "
  115. "(NCCL path); not learned, waiting for the capture warm-up", sig[1], sig[2])
  116. elif old is None or old[2] != key:
  117. plan = _make_plan(rec)
  118. _state["plans"][sig] = (n, plan, key)
  119. _side_stream() # created eagerly, never first inside a graph capture
  120. nbytes = sum(int(t[1::2].sum()) for t, _ in plan.values())
  121. logger.warning("DSV41_L2_PREFETCH plan tokens=%d mode=%s collectives=%d windows=%d "
  122. "prefetch=%.1f MB/forward%s", sig[1], sig[2], n, len(plan), nbytes / 2**20,
  123. "" if old is None else f" (relearned; was {old[0]} collectives)")
  124. elif sig is not None and capturing:
  125. _state["frozen"].add(sig)
  126. key = ("capture",) + sig[1:]
  127. if key not in _state["logged"]:
  128. _state["logged"].add(key)
  129. entry = _state["plans"].get(sig)
  130. learned = entry[0] if entry else 0
  131. ok = entry is not None and learned == _state["idx"] and _state["stats"][1] > 0
  132. logger.warning("DSV41_L2_PREFETCH captured tokens=%d mode=%s branches=%d collectives=%d "
  133. "(learned %d)%s", sig[1], sig[2], _state["stats"][1], _state["idx"], learned,
  134. "" if ok else " MISMATCH: this graph runs without prefetch")
  135. _state["sig"] = None
  136. def _make_plan(rec):
  137. """rec: {collective idx: [(ptr, bytes), ...] in consumption order} -> device segment tables."""
  138. budget = _budget()
  139. plan = {}
  140. for idx, segs in rec.items():
  141. out, left = [], budget
  142. for ptr, nbytes in segs:
  143. if left <= 0:
  144. break
  145. take = min(nbytes, left) & ~15
  146. if take > 0:
  147. out.append((ptr, take))
  148. left -= take
  149. if out:
  150. t = torch.tensor([v for s in out for v in s], dtype=torch.int64, device="cuda")
  151. plan[idx] = (t, len(out))
  152. _state["keepalive"].append(t) # a captured graph keeps reading this address
  153. return plan
  154. # -- hooks --------------------------------------------------------------------------------------
  155. def _record_weight(*tensors):
  156. sig = _state["sig"]
  157. if sig is None or sig not in _state["learning"]:
  158. return
  159. rec = _state["learning"][sig].setdefault(_state["idx"], [])
  160. for t in tensors:
  161. if t is not None and t.is_cuda:
  162. rec.append((t.data_ptr(), t.numel() * t.element_size()))
  163. def _before_collective(kind, stream):
  164. sig = _state["sig"]
  165. if sig is None:
  166. return
  167. _join()
  168. _state["idx"] += 1
  169. idx = _state["idx"]
  170. if kind == "ag" and os.environ.get("DSV41_L2_PREFETCH_AG", "1") == "0":
  171. return
  172. entry = _state["plans"].get(sig)
  173. if entry is None:
  174. return
  175. if idx > entry[0]:
  176. return # more collectives than learned: sequence differs, launch nothing past the plan
  177. plan = entry[1]
  178. item = plan.get(idx)
  179. if item is None:
  180. return
  181. t, n = item
  182. main = stream if isinstance(stream, torch.cuda.Stream) else torch.cuda.current_stream()
  183. side = _side_stream()
  184. side.wait_stream(main)
  185. rc = _lib().dsv41_l2pf(t.data_ptr(), n, _CHUNK, side.cuda_stream)
  186. if rc:
  187. raise RuntimeError(f"DSV41_L2_PREFETCH launch failed: cuda error {rc}")
  188. _state["pending"] = side
  189. _state["stats"][0] += 1
  190. _state["stats"][1] += 1
  191. def install_fp8_utils(module):
  192. """Wrap the MXFP8 dense linear (after mxfp8_b12x) so each call records its weight + scales."""
  193. if not enabled() or ("fp8", id(module)) in _state["installed"]:
  194. return
  195. _state["installed"].add(("fp8", id(module)))
  196. original = module.flashinfer_mxfp8_blockscaled_linear
  197. def linear(input, weight, weight_scale, *args, **kwargs):
  198. _record_weight(weight, weight_scale)
  199. return original(input, weight, weight_scale, *args, **kwargs)
  200. module.flashinfer_mxfp8_blockscaled_linear = linear
  201. logger.warning("DSV41_L2_PREFETCH: MXFP8 linears recorded (budget %.1f MB/window)", _budget() / 2**20)
  202. def install_router(module):
  203. """Record the bf16 router weight (tiny_gemm_bf16 in deepseek_v2, 3.9 MB, read after the
  204. post-attention all-reduce at ~140 GB/s)."""
  205. if not enabled() or not hasattr(module, "tiny_gemm_bf16") or ("router", id(module)) in _state["installed"]:
  206. return
  207. _state["installed"].add(("router", id(module)))
  208. original = module.tiny_gemm_bf16
  209. def tiny_gemm_bf16(hidden_states, weight, *args, **kwargs):
  210. _record_weight(weight)
  211. return original(hidden_states, weight, *args, **kwargs)
  212. module.tiny_gemm_bf16 = tiny_gemm_bf16
  213. def install_roce(module):
  214. """Patch b12x.comm.roce(_ring).roce_oneshot: launch the prefetch right before each collective."""
  215. if not enabled():
  216. return
  217. cls = module.RoceOneshotAllReduce
  218. if getattr(cls, "_dsv41_l2_prefetch", False):
  219. return
  220. cls._dsv41_l2_prefetch = True
  221. ar, ag = cls.all_reduce, cls.all_gather
  222. def all_reduce(self, inp, *args, **kwargs):
  223. if self.should_allreduce(inp):
  224. _before_collective("ar", kwargs.get("stream"))
  225. return ar(self, inp, *args, **kwargs)
  226. def all_gather(self, inp, *args, **kwargs):
  227. _before_collective("ag", kwargs.get("stream"))
  228. return ag(self, inp, *args, **kwargs)
  229. cls.all_reduce, cls.all_gather = all_reduce, all_gather
  230. _lib()
  231. logger.warning("DSV41_L2_PREFETCH: RoCE collectives prefetch the next weights into L2")
  232. def install_model(module):
  233. """Bracket DeepseekV4Model.forward (decode / target-verify only; prefill is compute-bound)."""
  234. if not enabled():
  235. return
  236. cls = module.DeepseekV4Model
  237. if getattr(cls, "_dsv41_l2_prefetch", False):
  238. return
  239. cls._dsv41_l2_prefetch = True
  240. import sys
  241. for name in ("b12x.comm.roce.roce_oneshot", "b12x.comm.roce_ring.roce_oneshot"):
  242. if name in sys.modules: # imported before the finder saw it
  243. install_roce(sys.modules[name])
  244. original = cls.forward
  245. def forward(self, input_ids, positions, forward_batch, *args, **kwargs):
  246. mode = getattr(forward_batch, "forward_mode", None)
  247. decode_like = mode is not None and any(
  248. getattr(mode, f, lambda: False)() for f in ("is_decode", "is_target_verify", "is_draft_extend"))
  249. if not decode_like:
  250. return original(self, input_ids, positions, forward_batch, *args, **kwargs)
  251. sig = (id(self), int(input_ids.shape[0]), str(mode))
  252. _begin_forward(sig)
  253. try:
  254. return original(self, input_ids, positions, forward_batch, *args, **kwargs)
  255. finally:
  256. _end_forward()
  257. cls.forward = forward
  258. logger.warning("DSV41_L2_PREFETCH: model forward bracketed (decode/verify only)")