test_prefill_sp_gpu.py 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915
  1. """GPU check for adapter/prefill_sp.py on one GPU (engine image, <= 2 GB):
  2. docker run --rm --gpus all --network none -v $PWD:/ds41 -w /ds41 -e PYTHONPATH=/ds41/adapter \
  3. --entrypoint python3 <image> tests/test_prefill_sp_gpu.py [drift] [kernels] [emulate]
  4. drift the pinned engine sources hash as expected in this image (incl. the tail helpers), a
  5. drifted one is refused, and SP refuses an hc_prefill_fused that is not SP-aware.
  6. kernels every per-row kernel stage 1 runs on a shard gives, on rows [r*M/4, (r+1)*M/4), the bits
  7. of the full-M call when the gates key on the full row count (sp_logical_rows), at
  8. M = 4096 / 2048 / 1500 / 128: hc combine(+norm) incl. the first-layer norm, hc mix stats
  9. (hc_fused on and off), hc_post, the fused Engram gate, the Engram wkv MXFP8 GEMM
  10. (b12x, as production routes it, and FlashInfer cutlass). A shard-keyed control shows
  11. which of them would change bits without the helper.
  12. emulate four TP ranks as threads (baton-passing, so one issues GPU work at a time) on a fake
  13. process group; one full-row layer (the engine's real forward_hc_pre_from_prev and hc
  14. kernels, a real Engram gate and MXFP8 wkv, stand-in attention/MoE whose wo_b / expert
  15. outputs are per-rank partials) followed by one bounded-replay tail layer, through
  16. prefill_sp's loop copy. stage-1 EXACT must equal the unsharded reference bit for bit on
  17. every rank, stage-1 fast must equal P0 (comm) bit for bit.
  18. """
  19. import os
  20. import sys
  21. import threading
  22. import types
  23. from contextlib import contextmanager, nullcontext
  24. os.environ.setdefault("DSV41_PREFILL_SP", "1")
  25. os.environ.setdefault("DSV41_HC_FUSED", "1")
  26. os.environ.pop("DSV41_HC_PREFILL_FUSED", None)
  27. import torch # noqa: E402
  28. import prefill_sp as sp # noqa: E402
  29. torch.cuda.set_per_process_memory_fraction(min(1.0, 2e9 / torch.cuda.get_device_properties(0).total_memory))
  30. DEV = "cuda"
  31. BF = torch.bfloat16
  32. H, HC, MIX, ITERS = 5120, 4, 24, 20
  33. W = 4
  34. def _mods():
  35. from sglang.kernels.ops.layernorm import mhc
  36. from sglang.srt.models import deepseek_v4 as dm
  37. return mhc, dm
  38. _INSTALLED = {}
  39. def setup():
  40. """hc_fused as production has it, b12x MXFP8 routing, prefill_sp's modules + hc wrappers."""
  41. if _INSTALLED:
  42. return _INSTALLED
  43. mhc, dm = _mods()
  44. # no runtime context in a bare container: the one query mhc_post makes (prefill CP interleave?)
  45. # answers as production does (no CP)
  46. mhc.is_dsa_prefill_cp_interleave = lambda: False
  47. import hc_fused
  48. hc_fused.install(mhc)
  49. import mxfp8_b12x
  50. from sglang.srt.layers.quantization import fp8_utils
  51. _INSTALLED["mxfp8_cutlass"] = fp8_utils.flashinfer_mxfp8_blockscaled_linear
  52. mxfp8_b12x.install(fp8_utils)
  53. sp._load_modules(dm)
  54. L = dm.DeepseekV4DecoderLayer
  55. _INSTALLED.update(
  56. mhc=mhc, dm=dm, hc_fused=hc_fused, fp8_utils=fp8_utils,
  57. combine=sp._make_hc_combine(L._hc_combine), post=sp._make_hc_post(L.hc_post),
  58. stats=L._hc_mix_stats, fwd=L.forward_hc_pre_from_prev, stream=L._get_hc_stats_stream)
  59. return _INSTALLED
  60. def _norm(seed):
  61. from sglang.srt.layers.layernorm import RMSNorm
  62. n = RMSNorm(H, eps=1e-6).to(DEV)
  63. g = torch.Generator(device=DEV).manual_seed(seed)
  64. with torch.no_grad():
  65. n.weight.copy_((1 + 0.1 * torch.randn(H, device=DEV, generator=g)).to(n.weight.dtype))
  66. n.weight.data = n.weight.data.to(BF)
  67. return n
  68. def _hc_params(seed):
  69. g = torch.Generator(device=DEV).manual_seed(seed)
  70. fn = torch.randn(MIX, HC * H, device=DEV, generator=g) * 0.0225
  71. base = torch.randn(MIX, device=DEV, generator=g) * 3
  72. scale = torch.rand(3, device=DEV, generator=g) * 0.05 + 0.01
  73. return fn, scale, base
  74. def _residual(m, seed):
  75. g = torch.Generator(device=DEV).manual_seed(seed)
  76. r = torch.randn(m, HC, H, device=DEV, generator=g, dtype=BF) * 0.5
  77. r[:, :, :64] *= 40
  78. return r
  79. class _Fp8Linear:
  80. """The MXFP8 dense linear the Engram wkv runs (fp8 weight, ue8m0 block scales, swizzled)."""
  81. def __init__(self, n, k, seed, routed=True):
  82. from flashinfer import block_scale_interleave
  83. g = torch.Generator(device=DEV).manual_seed(seed)
  84. self.w = (torch.randn(n, k, device=DEV, generator=g) * 2).to(torch.float8_e4m3fn)
  85. su8 = (127 + torch.randint(-6, 1, (n, k // 32), device=DEV, generator=g)).to(torch.uint8)
  86. self.sf = block_scale_interleave(su8.contiguous()).contiguous()
  87. self.routed = routed # True: production routing (mxfp8_b12x -> b12x)
  88. self.calls = []
  89. def __call__(self, x):
  90. self.calls.append(x.shape[0])
  91. s = setup()
  92. f = s["fp8_utils"].flashinfer_mxfp8_blockscaled_linear if self.routed else s["mxfp8_cutlass"]
  93. return f(x, self.w, self.sf, backend="cutlass"), None
  94. class Layer:
  95. """DeepseekV4DecoderLayer's hc methods (real engine code, SP-wrapped where stage 1 wraps them)
  96. on a stand-in carrying the attributes they read."""
  97. hc_mult, hc_sinkhorn_iters, rms_norm_eps, hc_eps = HC, ITERS, 1e-6, 1e-6
  98. hc_pre_from_prev_sublayer = True
  99. hc_stats_stream = None
  100. dsa_enable_prefill_cp = False
  101. use_fused_mhc_post_pre = False
  102. config = types.SimpleNamespace(model_type="deepseek_v41", vision_n_layers=0, image_token_id=7)
  103. def __init__(self, seed, layer_id=0, attn=None, mlp=None, engram=None):
  104. self.layer_id = layer_id
  105. self.hc_attn_fn, self.hc_attn_scale, self.hc_attn_base = _hc_params(seed)
  106. self.hc_ffn_fn, self.hc_ffn_scale, self.hc_ffn_base = _hc_params(seed + 50)
  107. self.input_layernorm, self.post_attention_layernorm = _norm(seed + 1), _norm(seed + 2)
  108. self.self_attn, self.mlp, self.engram = attn, mlp, engram
  109. def _run_moe_ffn_dp_sync(self, hidden_states, forward_batch, *, input_ids, input_ids_global):
  110. # the TP-only branch of the engine's method (no DP gather / CP / a2a)
  111. with FORWARD.scoped(mlp_reduce_scatter=False):
  112. return self.mlp(hidden_states, forward_batch, input_ids=input_ids,
  113. input_ids_global=input_ids_global, skip_shared_experts=False)
  114. def _bind_layer_methods():
  115. s = setup()
  116. Layer._hc_combine = s["combine"]
  117. Layer.hc_post = s["post"]
  118. Layer._hc_mix_stats = s["stats"]
  119. Layer.forward_hc_pre_from_prev = s["fwd"]
  120. Layer._get_hc_stats_stream = s["stream"]
  121. @contextmanager
  122. def _as_shard(m, r):
  123. prev = sp._ctx.plan
  124. sp._ctx.plan = sp._Plan(m, W, r, None, True, False)
  125. try:
  126. yield
  127. finally:
  128. sp._ctx.plan = prev
  129. # ============================================================================================
  130. def test_drift():
  131. s = setup()
  132. got = sp.engine_hashes()
  133. bad = {k: v for k, v in got.items() if sp._EXPECTED[k] != v}
  134. assert not bad, bad
  135. sp._check_tail_engine()
  136. fp8_old, sp.FP8 = sp.FP8, True
  137. try:
  138. sp.check_engine() # incl. the stage-2 attention sources
  139. finally:
  140. sp.FP8 = fp8_old
  141. saved = dict(sp._EXPECTED)
  142. sp._EXPECTED["v4.MQALayer.forward"] = "0000000000000000"
  143. try:
  144. sp.check_engine()
  145. raise AssertionError("drift accepted")
  146. except RuntimeError as exc:
  147. assert "drifted" in str(exc)
  148. finally:
  149. sp._EXPECTED.clear()
  150. sp._EXPECTED.update(saved)
  151. os.environ["DSV41_HC_PREFILL_FUSED"] = "1"
  152. try:
  153. import hc_prefill_fused
  154. aware = getattr(hc_prefill_fused, "SP_LOGICAL_ROWS", False)
  155. try:
  156. sp._check_hc_prefill_fused()
  157. refused = False
  158. except RuntimeError:
  159. refused = True
  160. assert refused != bool(aware), (aware, refused)
  161. finally:
  162. os.environ.pop("DSV41_HC_PREFILL_FUSED", None)
  163. print(f" drift: {len(got)} engine sources + 3 tail helpers match; a drifted hash is refused; "
  164. f"hc_prefill_fused SP-aware={bool(aware)} -> {'accepted' if aware else 'refused'}", flush=True)
  165. # ============================================================================================
  166. def _kernels_at(m, layer, lin_b12x, lin_cut, seed):
  167. """{kernel: (full, [shard r outputs with the helper], [shard r outputs without])}."""
  168. from sglang.srt.layers.engram import engram_gate
  169. s = setup()
  170. R = _residual(m, seed)
  171. x_out = torch.randn(m, H, device=DEV, dtype=BF) * 0.3
  172. kv = torch.randn(m, (HC + 1) * H, device=DEV, dtype=BF) * 0.5
  173. qw = (1 + 0.1 * torch.randn(HC, H, device=DEV)).to(BF)
  174. kw = (1 + 0.1 * torch.randn(HC, H, device=DEV)).to(BF)
  175. emb = torch.randn(m, 6144, device=DEV, dtype=BF) * 0.2
  176. lay = layer
  177. def stats(t):
  178. return lay._hc_mix_stats(t, lay.hc_attn_fn, lay.hc_attn_scale, lay.hc_attn_base, None)
  179. pre, post, comb = stats(R)
  180. fns = {
  181. "combine0 (norm of stream 0)": lambda sl: lay._hc_combine(R[sl], None, lay.input_layernorm),
  182. "combine+norm": lambda sl: lay._hc_combine(R[sl], pre[sl], lay.input_layernorm),
  183. "mix stats (hc_fused)": lambda sl: stats(R[sl]),
  184. "mix stats (stock split-K)": lambda sl: _stock_stats(lambda: stats(R[sl])),
  185. "hc_post": lambda sl: lay.hc_post(x_out[sl], R[sl], post[sl], comb[sl]),
  186. "engram gate": lambda sl: engram_gate(R[sl].contiguous(), kv[sl], qw, kw, 1e-6, 1e-6),
  187. "wkv MXFP8 b12x": lambda sl: lin_b12x(emb[sl])[0],
  188. "wkv MXFP8 cutlass": lambda sl: lin_cut(emb[sl])[0],
  189. }
  190. out = {}
  191. for name, fn in fns.items():
  192. full = fn(slice(None))
  193. ok = raw_ok = True
  194. for r in range(W):
  195. lo, hi = sp.shard_range(m, W, r)
  196. want = _slice(full, lo, hi)
  197. with _as_shard(m, r):
  198. ok = ok and _eq(want, fn(slice(lo, hi)))
  199. raw_ok = raw_ok and _eq(want, fn(slice(lo, hi)))
  200. out[name] = (ok, raw_ok)
  201. del full
  202. return out
  203. def _stock_stats(f):
  204. hf = setup()["hc_fused"]
  205. prev = hf._state["disabled"]
  206. hf._state["disabled"] = True
  207. try:
  208. return f()
  209. finally:
  210. hf._state["disabled"] = prev
  211. def _eq(a, b):
  212. if isinstance(a, (tuple, list)):
  213. return all(_eq(u, v) for u, v in zip(a, b))
  214. return torch.equal(a, b)
  215. def _slice(a, lo, hi):
  216. if isinstance(a, (tuple, list)):
  217. return tuple(_slice(u, lo, hi) for u in a)
  218. return a[lo:hi]
  219. def test_kernels():
  220. _bind_layer_methods()
  221. lay = Layer(11)
  222. lin_b12x = _Fp8Linear(25600, 6144, 3) # production routing: b12x
  223. lin_cut = _Fp8Linear.__new__(_Fp8Linear) # SGLang's own FlashInfer cutlass,
  224. lin_cut.w, lin_cut.sf, lin_cut.routed, lin_cut.calls = lin_b12x.w, lin_b12x.sf, False, []
  225. fails = []
  226. for m in (4096, 2048, 1500, 128):
  227. res = _kernels_at(m, lay, lin_b12x, lin_cut, seed=m)
  228. line = []
  229. for name, (ok, raw_ok) in res.items():
  230. if not ok:
  231. fails.append((m, name))
  232. line.append(f"{name}: {'OK' if ok else 'DIFF'}{'' if raw_ok else ' (shard-keyed: DIFF)'}")
  233. print(f" kernels M={m} (shard {m // W}): " + "; ".join(line), flush=True)
  234. del res
  235. torch.cuda.empty_cache()
  236. assert not [f for f in fails if not f[1].startswith("wkv")], fails
  237. if fails:
  238. print(f" NOTE: wkv shard GEMM not row-invariant at {fails}: the runtime self-check falls back "
  239. "to the full-M GEMM there (exact either way)", flush=True)
  240. # ============================================================================================
  241. # four ranks on one GPU
  242. # ============================================================================================
  243. class ThreadGroup:
  244. """Collectives across rank threads. Baton passing: a thread holds the lock except while it
  245. waits at a barrier, so exactly one rank issues GPU work at a time (stream order = issue
  246. order, and no concurrent JIT compiles)."""
  247. def __init__(self, world, rs_order="ring"):
  248. self.world_size, self.unique_name, self.device_group = world, "tp", None
  249. self._bar = threading.Barrier(world)
  250. self._lock = threading.Lock()
  251. self._slots = [None] * world
  252. self._tl = threading.local()
  253. self.rs_order = rs_order
  254. @property
  255. def rank_in_group(self):
  256. return self._tl.rank
  257. def _wait(self):
  258. self._lock.release()
  259. try:
  260. self._bar.wait()
  261. finally:
  262. self._lock.acquire()
  263. def _exchange(self, t):
  264. # no copy: every rank's tensor stays untouched until the second barrier
  265. r = self.rank_in_group
  266. self._slots[r] = t
  267. self._wait()
  268. vals = list(self._slots)
  269. self._wait()
  270. return vals
  271. def all_gather_into_tensor(self, out, x):
  272. vals = self._exchange(x.contiguous())
  273. dst = out.view(self.world_size, -1)
  274. for i, v in enumerate(vals):
  275. dst[i].copy_(v.reshape(-1))
  276. def all_reduce(self, x):
  277. return rank_order_sum(self._exchange(x))
  278. def reduce_scatter_tensor(self, out, x):
  279. vals = self._exchange(x.contiguous())
  280. r, s = self.rank_in_group, out.shape[0]
  281. parts = [v[r * s:(r + 1) * s] for v in vals]
  282. order = list(range(self.world_size)) if self.rs_order == "same" else \
  283. [(r + 1 + k) % self.world_size for k in range(self.world_size)]
  284. out.copy_(rank_order_sum([parts[o] for o in order]))
  285. def run(self, fn):
  286. out, errs = [None] * self.world_size, []
  287. def body(r):
  288. self._tl.rank = r
  289. self._lock.acquire()
  290. try:
  291. out[r] = fn(r)
  292. except BaseException as exc: # noqa: BLE001
  293. errs.append(exc)
  294. self._bar.abort()
  295. finally:
  296. self._lock.release()
  297. ts = [threading.Thread(target=body, args=(r,)) for r in range(self.world_size)]
  298. for t in ts:
  299. t.start()
  300. for t in ts:
  301. t.join()
  302. if errs:
  303. raise errs[0]
  304. return out
  305. def rank_order_sum(parts):
  306. acc = parts[0].clone()
  307. for p in parts[1:]:
  308. acc = acc + p
  309. return acc
  310. class Serial:
  311. """The reference 'group': one thread computes every rank's partial and reduces in rank order."""
  312. world_size = W
  313. _fwd = threading.local()
  314. class _Forward:
  315. @property
  316. def mlp_reduce_scatter(self):
  317. return getattr(_fwd, "mrs", False)
  318. @contextmanager
  319. def scoped(self, mlp_reduce_scatter=False):
  320. prev = getattr(_fwd, "mrs", False)
  321. _fwd.mrs = mlp_reduce_scatter
  322. try:
  323. yield
  324. finally:
  325. _fwd.mrs = prev
  326. FORWARD = _Forward()
  327. class RowParallel(torch.nn.Module):
  328. def __init__(self, group, w):
  329. super().__init__()
  330. self.group, self.w = group, w
  331. self.reduce_results, self.tp_size = True, W
  332. def partial(self, r, h):
  333. return (h @ self.w[r]).to(BF)
  334. def forward(self, input_, skip_all_reduce=False):
  335. out = self.partial(self.group.rank_in_group, input_)
  336. return (out if skip_all_reduce else self.group.all_reduce(out)), None
  337. class Attn(torch.nn.Module):
  338. """Row-mixing stand-in (a causal running mean), per-rank head scale, wo_b partial."""
  339. def __init__(self, group, seed):
  340. super().__init__()
  341. g = torch.Generator(device=DEV).manual_seed(seed)
  342. self.group, self.attn_tp_size = group, W
  343. self.scale = (1 + 0.1 * torch.randn(W, H, device=DEV, generator=g)).to(BF)
  344. self.wo_b = RowParallel(group, (torch.randn(W, H, 256, device=DEV, generator=g) / 16).to(BF)
  345. .transpose(1, 2).contiguous()) # [W, 256, H]: h[:, :256] @ w
  346. self.rows_seen = []
  347. def accepts_mxfp8_swizzled_input(self):
  348. return False
  349. def maybe_use_decode_attn_tp(self, fb):
  350. return nullcontext()
  351. def head(self, r, x):
  352. h = x[:, :256].float().cumsum(0) / torch.arange(1, x.shape[0] + 1, device=DEV).unsqueeze(1)
  353. return (h * self.scale[r, :256].float()).to(BF)
  354. def forward(self, x, positions, forward_batch, x_quant=None):
  355. assert positions.shape[0] == x.shape[0]
  356. self.rows_seen.append(x.shape[0])
  357. if self.group is Serial:
  358. return rank_order_sum([self.wo_b.partial(r, self.head(r, x)) for r in range(W)])
  359. o, _ = self.wo_b(self.head(self.group.rank_in_group, x))
  360. return o
  361. class MoE(torch.nn.Module):
  362. def __init__(self, group, seed):
  363. super().__init__()
  364. g = torch.Generator(device=DEV).manual_seed(seed)
  365. self.group, self.tp_size = group, W
  366. self._shared_expert_tp1 = self._enable_a2a_moe = False
  367. self.table = (0.5 * torch.randn(16, W, H, device=DEV, generator=g)).to(BF)
  368. def partial(self, r, x, ids):
  369. return (x * self.table[ids % 16, r]).to(BF)
  370. def forward(self, hidden_states, forward_batch=None, gemm_output_zero_allocator=None,
  371. input_ids=None, input_ids_global=None, skip_shared_experts=False):
  372. assert input_ids.shape[0] == hidden_states.shape[0]
  373. if self.group is Serial:
  374. return rank_order_sum([self.partial(r, hidden_states, input_ids) for r in range(W)])
  375. out = self.partial(self.group.rank_in_group, hidden_states, input_ids)
  376. return out if FORWARD.mlp_reduce_scatter else self.group.all_reduce(out)
  377. class Embed:
  378. def __init__(self, group, seed, vocab=1009, cols=2):
  379. g = torch.Generator(device=DEV).manual_seed(seed)
  380. self.group, self.tp_size, self._shared, self.vocab = group, W, False, vocab
  381. self.table = (torch.randn(vocab, cols, 256, device=DEV, generator=g) * 0.3).to(BF)
  382. def owned(self, r, ids):
  383. lo, hi = self.vocab * r // W, self.vocab * (r + 1) // W
  384. vals = self.table[ids, torch.arange(ids.shape[1], device=DEV)]
  385. return vals.masked_fill(~((ids >= lo) & (ids < hi)).unsqueeze(-1), 0)
  386. def _owned_rows(self, ids):
  387. return self.owned(self.group.rank_in_group, ids)
  388. class Engram:
  389. def __init__(self, group, seed):
  390. from sglang.srt.layers.engram import engram_gate
  391. self.group, self.layer_hash_index = group, 0
  392. self.embed = Embed(group, seed)
  393. self.wkv = _Fp8Linear((HC + 1) * H, 512, seed + 1)
  394. g = torch.Generator(device=DEV).manual_seed(seed + 2)
  395. self.q = (1 + 0.1 * torch.randn(HC, H, device=DEV, generator=g)).to(BF)
  396. self.k = (1 + 0.1 * torch.randn(HC, H, device=DEV, generator=g)).to(BF)
  397. self._gate = engram_gate
  398. def apply_gate(self, x, kv):
  399. return self._gate(x, kv, self.q, self.k, 1e-6, 1e-6)
  400. def __call__(self, x, ids, forward_batch=None, cp_all_tokens=False):
  401. if self.group is Serial:
  402. emb = rank_order_sum([self.embed.owned(r, ids) for r in range(W)])
  403. else:
  404. emb = self.group.all_reduce(self.embed._owned_rows(ids))
  405. kv, _ = self.wkv(emb.flatten(-2))
  406. return self.apply_gate(x, kv)
  407. class Model:
  408. """Layer 0: Engram + one full-row layer. Layer 1: bounded-replay tail layer."""
  409. def __init__(self, group, seed):
  410. self.pp_group = types.SimpleNamespace(world_size=1)
  411. self.hidden_size, self.hc_mult, self.hc_pre_from_prev_sublayer = H, HC, True
  412. self.start_layer, self.end_layer, self.late_layer_start = 0, 2, 1
  413. self.config = Layer.config
  414. self.layers = [Layer(seed + 10 * i, i, Attn(group, seed + 10 * i + 3),
  415. MoE(group, seed + 10 * i + 4),
  416. Engram(group, seed + 99) if i == 0 else None) for i in range(2)]
  417. self.dspark_layers_to_capture = [0, 1]
  418. def engram_hasher(self, input_ids, fb):
  419. return ((input_ids[:, None, None] * torch.tensor([[31, 17]], device=DEV) + 5) % 1009)
  420. def _check_late_layer_tail_readers(self, fb):
  421. pass
  422. def _tail(lens):
  423. from sglang.srt.layers.attention.deepseek_v4_backend import LateLayerTail
  424. idx, pos = [], 0
  425. for n in lens:
  426. k = min(128, n)
  427. idx.extend(range(pos + n - k, pos + n))
  428. pos += n
  429. t = torch.tensor(idx, dtype=torch.int64, device=DEV)
  430. ext = [min(128, n) for n in lens]
  431. return LateLayerTail(token_indices=t, positions=t.clone(),
  432. extend_seq_lens=torch.tensor(ext, device=DEV), extend_seq_lens_cpu=ext,
  433. swa_out_cache_loc=t.clone(),
  434. contiguous_start=(pos - ext[0]) if len(lens) == 1 else None)
  435. class _Backend:
  436. def __init__(self, tail):
  437. self.tail_forward_metadata = types.SimpleNamespace(late_layer_tail=tail)
  438. def enter_late_layer_tail(self, fb):
  439. return "saved"
  440. def exit_late_layer_tail(self, saved, fb):
  441. assert saved == "saved"
  442. Attn.forward = sp._make_attn_forward(Attn.forward) # as install() wraps MQALayer.forward
  443. MoE.forward = sp._make_moe_forward(MoE.forward) # ... and DeepseekV2MoE.forward
  444. sp._mega_moe = lambda moe, x: False # stand-in MoE is not a mega-MoE
  445. FB = types.SimpleNamespace(forward_mode=types.SimpleNamespace(
  446. is_extend_without_speculative=lambda: True, is_decode=lambda: False,
  447. is_target_verify=lambda: False))
  448. def _engine_ns(group, backend):
  449. dm = setup()["dm"]
  450. ns = types.SimpleNamespace(**{k: getattr(dm, k) for k in dir(dm) if not k.startswith("__")})
  451. ns.get_attn_backend = lambda: backend
  452. ns.check_cuda_graph_backend = lambda *a: False
  453. ns.get_global_expert_distribution_recorder = lambda: types.SimpleNamespace(
  454. with_current_layer=lambda i: nullcontext())
  455. ns.get_forward = lambda: FORWARD
  456. ns.is_in_breakable_cuda_graph = lambda: False
  457. ns.is_cp_active = lambda fb: False
  458. ns.get_attn_tp_context = lambda: types.SimpleNamespace(input_scattered=False)
  459. ns.get_tp_group = lambda: group
  460. ns.get_parallel = lambda: types.SimpleNamespace(attn_dp_size=1, tp_size=W, attn_tp_size=W)
  461. ns.get_moe_a2a_backend = lambda: types.SimpleNamespace(is_none=lambda: True)
  462. ns.MQALayer = Attn
  463. return ns
  464. def _reference(model, R0, ids, pos, tail):
  465. """The engine's stock loop semantics on one 'rank' (all rows, partials reduced in rank order)."""
  466. hash_ids = model.engram_hasher(ids, FB)
  467. hs, prev_pre, aux = R0.clone(), None, []
  468. for i in range(model.end_layer):
  469. if i == model.late_layer_start:
  470. hs, prev_pre, ids, pos = tail.rows(hs), tail.rows(prev_pre), tail.rows(ids), tail.positions
  471. hash_ids = tail.rows(hash_ids)
  472. lay = model.layers[i]
  473. if lay.engram is not None:
  474. hs = lay.engram(hs, hash_ids[:, 0], FB)
  475. if i in model.dspark_layers_to_capture:
  476. aux.append((tail.rows(hs) if i < model.late_layer_start else hs).mean(dim=1))
  477. hs, prev_pre = lay.forward_hc_pre_from_prev(
  478. positions=pos, hidden_states=hs, input_ids=ids, forward_batch=FB, input_ids_global=ids,
  479. prev_pre=prev_pre, precomputed_attn=None, next_norm=None, next_input=[])
  480. return hs, prev_pre, aux
  481. def _emulate(m, lens, seed=5, with_comm=True):
  482. _bind_layer_methods()
  483. tail = _tail(lens)
  484. g = torch.Generator(device=DEV).manual_seed(seed)
  485. R0 = torch.randn(m, HC, H, device=DEV, generator=g, dtype=BF) * 0.5
  486. ids = torch.randint(0, 5000, (m,), device=DEV, generator=g)
  487. pos = torch.arange(m, device=DEV)
  488. ref_model = Model(Serial, seed)
  489. sp._M["v4"] = _engine_ns(None, _Backend(tail))
  490. ref = _reference(ref_model, R0, ids, pos, tail)
  491. del ref_model
  492. torch.cuda.empty_cache()
  493. def arm(mode, exact, rs_order="ring"):
  494. group = ThreadGroup(W, rs_order)
  495. model = Model(group, seed)
  496. sp._M["v4"] = _engine_ns(group, _Backend(tail))
  497. sp._M["v2"] = types.SimpleNamespace(DeepseekV2MoE=MoE)
  498. sp._M["linear"] = types.SimpleNamespace(RowParallelLinear=RowParallel)
  499. sp._STATIC["checked"] = False
  500. sp._WKV_OK.clear()
  501. old = sp.MODE, sp.EXACT
  502. def body(r):
  503. aux = []
  504. p = sp._plan(model, R0, FB)
  505. assert p is not None and p.rank == r
  506. # R0 itself (not a per-rank clone: 4 x 168 MB); the loop never writes its input
  507. hs, pre, _ = sp._sp_forward_layers(model, p, pos, R0, FB, ids, ids, True, aux)
  508. return hs, pre, aux
  509. sp.MODE, sp.EXACT = mode, exact
  510. try:
  511. out = group.run(body)
  512. finally:
  513. sp.MODE, sp.EXACT = old
  514. wkv = dict(sp._WKV_OK)
  515. rows_seen = model.layers[0].self_attn.rows_seen
  516. del model
  517. torch.cuda.empty_cache()
  518. return out, wkv, rows_seen
  519. r0_sum = R0.view(torch.int16).sum().item()
  520. exact, wkv_e, seen = arm("shard", True)
  521. fast, wkv_f, _ = arm("shard", False)
  522. # P0 keeps every row on every rank: 4 ranks of it fit the 2 GB budget only at ~2k rows
  523. comm = arm("comm", False)[0] if with_comm else None
  524. assert R0.view(torch.int16).sum().item() == r0_sum, "the loop wrote into its input"
  525. same = lambda a, b: _eq(a[0], b[0]) and _eq(a[1], b[1]) and _eq(tuple(a[2]), tuple(b[2])) # noqa: E731
  526. ok_exact = [same(exact[r], ref) for r in range(W)]
  527. ok_fast = [same(fast[r], comm[r]) for r in range(W)] if comm is not None else "n/a (memory)"
  528. diff = (fast[0][0].float() - ref[0].float()).abs()
  529. rel = (diff / ref[0].float().abs().clamp_min(1e-3)).max().item()
  530. print(f" emulate M={m} tails={lens if len(lens) <= 4 else f'{len(lens)} requests'}: "
  531. f"exact==reference per rank {ok_exact}; fast==P0 per rank {ok_fast}; "
  532. f"fast vs reference: {int((diff > 0).sum())}/{diff.numel()} elements differ, max rel {rel:.2e}; "
  533. f"wkv shard ON {wkv_e}; attention saw rows {sorted(set(seen))}", flush=True)
  534. assert all(ok_exact) and (comm is None or all(ok_fast)), (ok_exact, ok_fast)
  535. def test_emulate():
  536. sp._agree_min = lambda p, local: bool(min(int(v) for v in p.group._exchange(
  537. torch.tensor([1 if local else 0], device=DEV))))
  538. for m, lens, comm in ((4096, [1000, 50, 2046, 1000], False), (4096, [4096], False),
  539. (2052, [513, 1, 1025, 513], True), (2048, [2048], True)):
  540. _emulate(m, lens, with_comm=comm)
  541. def _debug_arm(m, lens, seed=5):
  542. """Stage-1 exact with DSV41_PREFILL_SP_DEBUG=compare,fp on the emulated 4 ranks."""
  543. _bind_layer_methods()
  544. s = setup()
  545. Layer._hc_mix_stats = sp._make_hc_mix_stats(s["stats"])
  546. Layer.forward_hc_pre_from_prev = sp._make_layer_forward(s["fwd"])
  547. tail = _tail(lens)
  548. g = torch.Generator(device=DEV).manual_seed(seed)
  549. R0 = torch.randn(m, HC, H, device=DEV, generator=g, dtype=BF) * 0.5
  550. ids = torch.randint(0, 5000, (m,), device=DEV, generator=g)
  551. pos = torch.arange(m, device=DEV)
  552. group = ThreadGroup(W)
  553. model = Model(group, seed)
  554. sp._M["v4"] = _engine_ns(group, _Backend(tail))
  555. sp._M["v2"] = types.SimpleNamespace(DeepseekV2MoE=MoE)
  556. sp._M["linear"] = types.SimpleNamespace(RowParallelLinear=RowParallel)
  557. sp._STATIC["checked"] = False
  558. sp._WKV_OK.clear()
  559. old = sp.MODE, sp.EXACT, set(sp.DEBUG)
  560. sp.DEBUG.clear()
  561. sp.DEBUG.update({"compare", "fp"})
  562. def body(r):
  563. p = sp._plan(model, R0, FB)
  564. d = sp._Dbg(1, m, p, r)
  565. sp._ctx.dbg = d
  566. try:
  567. out = sp._sp_forward_layers(model, p, pos, R0, FB, ids, ids, True, [])
  568. finally:
  569. sp._ctx.dbg = None
  570. sp._debug_end(d, out)
  571. return d.checked, list(d.bad)
  572. sp.MODE, sp.EXACT = "shard", True
  573. try:
  574. return group.run(body)
  575. finally:
  576. sp.MODE, sp.EXACT = old[0], old[1]
  577. sp.DEBUG.clear()
  578. sp.DEBUG.update(old[2])
  579. _bind_layer_methods()
  580. del model
  581. torch.cuda.empty_cache()
  582. def test_debug():
  583. """4 ranks x a gathered [M, 4, 5120] reference per compared op: M=2052 fits the 2 GB cap."""
  584. sp._agree_min = lambda p, local: bool(min(int(v) for v in p.group._exchange(
  585. torch.tensor([1 if local else 0], device=DEV))))
  586. m, lens = 2052, [513, 1, 1025, 513]
  587. res = _debug_arm(m, lens)
  588. assert all(c == 12 and not bad for c, bad in res), res # 1 full-row layer: 6 hc + wo_b + moe + 2 engram + 2 tail
  589. print(f" debug compare, clean: {[c for c, _ in res]} ops checked per rank, 0 mismatches", flush=True)
  590. # fault: the shard takes the combine branch with a combine kernel that is off by one ulp
  591. good_gate, good_mhc = sp._combine_norm_fused, sp._M["mhc"]
  592. real = good_mhc.hc_combine
  593. sp._combine_norm_fused = lambda layer, x, pre, norm, rows: rows == x.shape[0]
  594. sp._M["mhc"] = types.SimpleNamespace(hc_combine=lambda x, pre, hc, dt: (
  595. lambda y: y.view(torch.int16).add_(1).view(dt))(real(x, pre, hc, dt)))
  596. try:
  597. res = _debug_arm(m, lens)
  598. finally:
  599. sp._combine_norm_fused, sp._M["mhc"] = good_gate, good_mhc
  600. ops = sorted({b[1] for _, bad in res for b in bad})
  601. assert ops and all(o.startswith("hc_combine") for o in ops) and all(bad for _, bad in res), ops
  602. print(f" debug compare, injected 1-ulp combine on the shard: flagged {ops} on every rank "
  603. f"(first at layer {res[0][1][0][0]})", flush=True)
  604. # ============================================================================================
  605. # stage 2: MXFP8 attention-input gathers
  606. # ============================================================================================
  607. def test_fp8_quant():
  608. """Shard-wise MXFP8 (linear scales) + gather + 128x4 swizzle == the stock full-M quantization,
  609. and the MXFP8 linear on the pre-quantized tuple == on the bf16 rows (b12x and cutlass)."""
  610. s = setup()
  611. lin = _Fp8Linear(1792, H, 21)
  612. for m in (4096, 3152, 2052, 2048, 1500, 128):
  613. x = torch.randn(m, H, device=DEV, dtype=BF) * 0.5
  614. x[:, :64] *= 40
  615. rq, rs = sp._mxfp8_quantize(x, True)
  616. lq, ls = sp._mxfp8_quantize(x, False)
  617. parts_q, parts_s = [], []
  618. for r in range(W):
  619. lo, hi = sp.shard_range(m, W, r)
  620. q, sf = sp._mxfp8_quantize(x[lo:hi].contiguous(), False)
  621. parts_q.append(q.view(torch.uint8))
  622. parts_s.append(sf.view(torch.uint8).reshape(-1)[: (hi - lo) * (H // 32)].view(hi - lo, H // 32))
  623. qg, sg = torch.cat(parts_q), torch.cat(parts_s)
  624. sw = sp.swizzle_128x4(sg)
  625. data_ok = torch.equal(qg, rq.view(torch.uint8))
  626. lin_ok = torch.equal(sg, ls.view(torch.uint8).reshape(-1)[: m * (H // 32)].view(m, H // 32))
  627. sw_ok = torch.equal(sw, rs.view(torch.uint8).reshape(-1))
  628. xq = sp._mxfp8_input_cls()(qg.view(rq.dtype), sw.view(rs.dtype).view(rs.shape))
  629. f_b12x = s["fp8_utils"].flashinfer_mxfp8_blockscaled_linear
  630. f_cut = s["mxfp8_cutlass"]
  631. cons = {}
  632. for name, f in (("b12x", f_b12x), ("cutlass", f_cut)):
  633. ref = f(x, lin.w, lin.sf, backend="cutlass")
  634. got = f(xq[0], lin.w, lin.sf, input_scale=xq[1], backend="cutlass")
  635. cons[name] = torch.equal(ref, got)
  636. print(f" fp8 quant M={m} (shard {m // W}): data {data_ok}, linear scales {lin_ok}, swizzled "
  637. f"scales {sw_ok} (stock shapes q {tuple(rq.shape)} {rq.dtype}, sf {tuple(rs.shape)} {rs.dtype}, "
  638. f"linear sf {tuple(ls.shape)}); linear on pre-quantized == bf16: {cons}", flush=True)
  639. assert data_ok and sw_ok and all(cons.values()), (m, data_ok, lin_ok, sw_ok, cons)
  640. class Attn8(Attn):
  641. """Attention stand-in that consumes its input the way MQALayer's prefill does on a layer without
  642. compressor / indexer: wqkv_a (MXFP8 linear) on x_quant if given, else on the bf16 x."""
  643. def __init__(self, group, seed, source=False):
  644. super().__init__(group, seed)
  645. self.compressor = object() if source else None # a source layer reads bf16 x
  646. self.indexer = None
  647. self.fuse_wqa_wkv = True
  648. lin = _Fp8Linear(1792, H, seed + 7)
  649. def wqkv_a(xin):
  650. f = setup()["fp8_utils"].flashinfer_mxfp8_blockscaled_linear
  651. if isinstance(xin, tuple):
  652. return f(xin[0], lin.w, lin.sf, input_scale=xin[1], backend="cutlass"), None
  653. return f(xin, lin.w, lin.sf, backend="cutlass"), None
  654. self.wqkv_a = wqkv_a
  655. self.fp8_inputs = 0
  656. def accepts_mxfp8_swizzled_input(self):
  657. return True
  658. def head(self, r, x):
  659. return super().head(r, x)
  660. def forward(self, x, positions, forward_batch, x_quant=None):
  661. assert positions.shape[0] == x.shape[0]
  662. self.rows_seen.append(x.shape[0])
  663. if x_quant is not None:
  664. self.fp8_inputs += 1
  665. if self.compressor is not None: # reads the bf16 rows themselves
  666. assert x_quant is None and not torch.isnan(x[:1]).any()
  667. qkv = self.wqkv_a(x_quant if x_quant is not None else x)[0]
  668. h = qkv[:, :256].contiguous()
  669. if self.group is Serial:
  670. return rank_order_sum([self.wo_b.partial(r, self.head(r, h)) for r in range(W)])
  671. o, _ = self.wo_b(self.head(self.group.rank_in_group, h))
  672. return o
  673. Attn8.forward = sp._make_attn_forward(Attn8.forward)
  674. class Model8(Model):
  675. """Layer 0: Engram + fp8-eligible attention. Layer 1: a 'source' layer (bf16). Layer 2: tail."""
  676. def __init__(self, group, seed):
  677. self.pp_group = types.SimpleNamespace(world_size=1)
  678. self.hidden_size, self.hc_mult, self.hc_pre_from_prev_sublayer = H, HC, True
  679. self.start_layer, self.end_layer, self.late_layer_start = 0, 3, 2
  680. self.config = Layer.config
  681. self.layers = [Layer(seed + 10 * i, i, Attn8(group, seed + 10 * i + 3, source=(i == 1)),
  682. MoE(group, seed + 10 * i + 4),
  683. Engram(group, seed + 99) if i == 0 else None) for i in range(3)]
  684. self.dspark_layers_to_capture = [0]
  685. def _arm8(m, lens, *, fp8, exact, debug=False, seed=9):
  686. _bind_layer_methods()
  687. s = setup()
  688. if debug:
  689. Layer._hc_mix_stats = sp._make_hc_mix_stats(s["stats"])
  690. Layer.forward_hc_pre_from_prev = sp._make_layer_forward(s["fwd"])
  691. tail = _tail(lens)
  692. g = torch.Generator(device=DEV).manual_seed(seed)
  693. R0 = torch.randn(m, HC, H, device=DEV, generator=g, dtype=BF) * 0.5
  694. ids = torch.randint(0, 5000, (m,), device=DEV, generator=g)
  695. pos = torch.arange(m, device=DEV)
  696. group = ThreadGroup(W)
  697. model = Model8(group, seed)
  698. sp._M["v4"] = _engine_ns(group, _Backend(tail))
  699. sp._M["v4"].MQALayer = Attn8
  700. sp._M["v2"] = types.SimpleNamespace(DeepseekV2MoE=MoE)
  701. sp._M["linear"] = types.SimpleNamespace(RowParallelLinear=RowParallel)
  702. sp._STATIC["checked"] = False
  703. sp._WKV_OK.clear()
  704. sp._FP8_M.clear()
  705. old = sp.MODE, sp.EXACT, sp.FP8, set(sp.DEBUG)
  706. sp.DEBUG.clear()
  707. if debug:
  708. sp.DEBUG.update({"compare", "fp"})
  709. def body(r):
  710. p = sp._plan(model, R0, FB)
  711. d = sp._Dbg(1, m, p, r) if debug else None
  712. sp._ctx.dbg = d
  713. aux = []
  714. try:
  715. hs, pre, _ = sp._sp_forward_layers(model, p, pos, R0, FB, ids, ids, True, aux)
  716. finally:
  717. sp._ctx.dbg = None
  718. if d is not None:
  719. sp._debug_end(d, (hs, pre))
  720. return hs, pre, aux, (d.checked, list(d.bad)) if d is not None else None
  721. sp.MODE, sp.EXACT, sp.FP8 = "shard", exact, fp8
  722. try:
  723. out = group.run(body)
  724. finally:
  725. sp.MODE, sp.EXACT, sp.FP8 = old[0], old[1], old[2]
  726. sp.DEBUG.clear()
  727. sp.DEBUG.update(old[3])
  728. _bind_layer_methods()
  729. info = ([l.self_attn.fp8_inputs for l in model.layers], dict(sp._FP8_M))
  730. del model
  731. torch.cuda.empty_cache()
  732. return out, info
  733. def test_fp8_emulate(only_debug=False):
  734. sp._agree_min = lambda p, local: bool(min(int(v) for v in p.group._exchange(
  735. torch.tensor([1 if local else 0], device=DEV))))
  736. same = lambda a, b: _eq(a[0], b[0]) and _eq(a[1], b[1]) and _eq(tuple(a[2]), tuple(b[2])) # noqa: E731
  737. for m, lens in () if only_debug else ((4096, [1000, 50, 2046, 1000]), (3152, [3152]),
  738. (2052, [513, 1, 1025, 513])):
  739. for exact in (True, False):
  740. s1, _ = _arm8(m, lens, fp8=False, exact=exact)
  741. s2, info = _arm8(m, lens, fp8=True, exact=exact)
  742. ok = [same(s1[r], s2[r]) for r in range(W)]
  743. print(f" fp8 emulate M={m} {'exact' if exact else 'fast '}: stage2 == stage1 per rank {ok}; "
  744. f"x_quant calls per layer (x4 ranks) {info[0]}; check {info[1]}", flush=True)
  745. assert all(ok), ok
  746. assert info[0] == [W, 0, 0] and all(v[0] for v in info[1].values()), info
  747. # debug gathers a full [M, 4, 5120] reference per compared op on 4 ranks: 2052 rows fit 2 GB
  748. res, info = _arm8(2052, [513, 1, 1025, 513], fp8=True, exact=True, debug=True)
  749. dbg = [r[3] for r in res]
  750. assert all(c > 0 and not bad for c, bad in dbg), dbg
  751. print(f" fp8 debug compare: {[c for c, _ in dbg]} ops checked per rank (incl. attn x mxfp8 "
  752. f"data/scales and wqkv_a), 0 mismatches", flush=True)
  753. if __name__ == "__main__":
  754. which = sys.argv[1:] or ["drift", "kernels", "emulate", "debug", "fp8"]
  755. print(f"torch {torch.__version__}, {torch.cuda.get_device_name(0)}", flush=True)
  756. if "drift" in which:
  757. test_drift()
  758. if "kernels" in which:
  759. test_kernels()
  760. if "emulate" in which:
  761. test_emulate()
  762. if "debug" in which:
  763. test_debug()
  764. if "fp8debug" in which:
  765. test_fp8_emulate(only_debug=True)
  766. if "fp8" in which:
  767. test_fp8_quant()
  768. test_fp8_emulate()
  769. print(f"test_prefill_sp_gpu: ok (peak {torch.cuda.max_memory_allocated() / 2**30:.2f} GiB)")