test_moe_b12x_next.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578
  1. """GPU test for adapter/moe_b12x_next.py on the real engine method (dsv4.1 f80c91a4b image).
  2. Drives the engine's own Mxfp4FlashinferCutlassMoEMethod with the adapter installed:
  3. create_weights -> FusedMoE._weight_loader_impl (the real TP narrowing, real checkpoint slices)
  4. -> process_weights_after_loading -> create_moe_runner -> apply -> FusedOpPool fused func,
  5. on a FusedMoE shell that carries only the attributes those methods read (no distributed init).
  6. Cases (each loads CKPT_EXPERTS experts of a layer, not all 384, to stay inside ~2 GB):
  7. EP1 N=576 target layers (topk 6), moe_tp_rank 3 and 0 vs fp32 reference from the checkpoint
  8. EP1 N=576 draft layer mtp.0 (128 experts, topk 3) vs fp32 reference
  9. EP2 N=1152 target layer, ep_rank 1, moe_tp_rank 1 vs stock FlashInfer CUTLASS + fp32 ref
  10. for M in {6, 48, 96, 1024} (+ 5000 > top capacity for the chunk path), with CUDA-graph padding
  11. rows (topk id -1), CUDA-graph capture/replay of the decode path, and a dense M sweep of the
  12. capacity ladder. Also:
  13. ep_remap on strided views (padded row stride, transposed, ids and weights with different strides,
  14. int64 ids, -1 padding) against the torch reference, eager and under graph replay; the fused
  15. path fed strided router outputs (eager + graph) against the contiguous run
  16. a layer the adapter did not convert (stock FlashInfer weights) refuses in the fused function
  17. the M64 prefill tile against the M16 plan on the same prepared experts and inputs, M = 2048,
  18. 2049, 4095, 4096, 5000, concentrated and uniform routing (rel-L2 M64 vs M16 <= max(1e-3, 3x the
  19. M16 run-to-run spread), and the same rel-L2 vs the fp32 reference within 2e-4)
  20. DSV41_MOE_B12X_NEXT_DETERMINISTIC (internal route planner at <= 8 rows): builds, matches the
  21. fp32 reference, and two runs are bit-identical
  22. usage (inside `docker run --rm --gpus all` of the image, see RESULTS / report):
  23. PYTHONPATH=/ds41/adapter:/b12x_next python3 /ds41/tests/test_moe_b12x_next.py [--quick]
  24. env: MODEL_DIR (default /model), TEST_MAX_GB (2.0), CKPT_EXPERTS (32), DSV41_MOE_B12X_NEXT_TUNE.
  25. """
  26. import json
  27. import os
  28. import sys
  29. import time
  30. from types import SimpleNamespace as NS
  31. os.environ.setdefault("DSV41_MOE_B12X_NEXT", "1")
  32. os.environ.setdefault("SGLANG_FLASHINFER_MOE_FUSED_FINALIZE", "0")
  33. os.environ.setdefault("DSV41_MOE_B12X_NEXT_MEMSTATS", "1")
  34. import torch # noqa: E402
  35. QUICK = "--quick" in sys.argv
  36. MODEL = os.environ.get("MODEL_DIR", "/model")
  37. NE = int(os.environ.get("CKPT_EXPERTS", "32"))
  38. K_HID, N_FULL, LIMIT = 5120, 2304, 10.0
  39. torch.cuda.set_per_process_memory_fraction(
  40. float(os.environ.get("TEST_MAX_GB", "2.0")) * 2**30 / torch.cuda.get_device_properties(0).total_memory)
  41. DEV = torch.device("cuda", torch.cuda.current_device())
  42. RESULTS = {"cases": [], "sweep": {}, "graph": [], "memory": {}}
  43. class _Backend:
  44. """get_moe_runner_backend()/get_moe_a2a_backend() stand-in: every is_*() False except `true`."""
  45. def __init__(self, *true):
  46. self.true = set(true)
  47. def __getattr__(self, name):
  48. if name.startswith("is_"):
  49. return lambda: name in self.true
  50. raise AttributeError(name)
  51. # ---------------------------------------------------------------------------- engine + stubs
  52. import sglang.srt.layers.quantization.fp8 as fp8 # noqa: E402
  53. import sglang.srt.layers.quantization.mxfp4_flashinfer_cutlass_moe as mq # noqa: E402
  54. import sglang.srt.layers.moe.moe_runner.flashinfer_cutlass as fic # noqa: E402
  55. import sglang.srt.layers.moe.moe_runner.runner as runner_mod # noqa: E402
  56. from sglang.srt.layers.moe.fused_moe_triton.layer import FusedMoE # noqa: E402
  57. from sglang.srt.layers.moe.moe_runner.base import FusedOpPool, MoeRunnerConfig # noqa: E402
  58. from sglang.srt.layers.moe.token_dispatcher.standard import StandardDispatchOutput # noqa: E402
  59. from sglang.srt.layers.moe.topk import StandardTopKOutput # noqa: E402
  60. PLATFORM = NS(is_sm120=True, is_sm90=False, is_sm100=False)
  61. _parallel = NS(tp_size=4)
  62. fp8.get_parallel = lambda: _parallel
  63. fp8.get_moe_runner_backend = lambda: _Backend("is_flashinfer_mxfp4")
  64. fp8.get_moe_a2a_backend = lambda: _Backend()
  65. fp8.get_platform = lambda: PLATFORM
  66. mq.get_platform = lambda: PLATFORM
  67. mq.get_exec = lambda: NS(moe=NS(flashinfer_mxfp4_moe_precision="default"))
  68. mq.log_info_on_rank0 = lambda *a, **k: None
  69. fic.get_tp_group = lambda: NS(world_size=1)
  70. fic.is_allocation_symmetric = lambda: False
  71. class _Runner:
  72. """MoeRunner stand-in: the real FusedOpPool entry, as MoeRunner.__init__ resolves it."""
  73. def __init__(self, backend, config):
  74. self.config = config
  75. self.fused = FusedOpPool.get_fused_func("none", backend.value)
  76. def run(self, dispatch_output, quant_info, lora_info=None):
  77. return self.fused(dispatch_output, quant_info, self.config)
  78. runner_mod.MoeRunner = _Runner
  79. import moe_b12x_next as ad # noqa: E402
  80. ad.install_method(mq)
  81. ad.install_runner(fic)
  82. ad._ENG["is_allocation_symmetric"] = lambda: False
  83. assert FusedOpPool.get_fused_func("none", "flashinfer_mxfp4") is ad._fused_b12x_next
  84. QUANT_CFG = NS(use_mxfp8=False, weight_block_size=[32, 32], is_fp4_experts=True, dequant_fp4_to_fp8=False,
  85. is_checkpoint_fp8_serialized=True, activation_scheme="dynamic", get_name=lambda: "fp8")
  86. # ---------------------------------------------------------------------------- checkpoint
  87. from safetensors import safe_open # noqa: E402
  88. _index = json.load(open(os.path.join(MODEL, "model.safetensors.index.json")))["weight_map"]
  89. def ckpt(name):
  90. with safe_open(os.path.join(MODEL, _index[name]), "pt", device="cpu") as f:
  91. return f.get_tensor(name)
  92. def expert_tensors(prefix, e):
  93. out = {}
  94. for w in ("w1", "w2", "w3"):
  95. out[w] = ckpt(f"{prefix}.ffn.experts.{e}.{w}.weight")
  96. out[w + "s"] = ckpt(f"{prefix}.ffn.experts.{e}.{w}.scale")
  97. return out
  98. # ---------------------------------------------------------------------------- layer shell
  99. def make_layer(prefix, *, n_global, topk, ep_size, ep_rank, moe_tp_size, moe_tp_rank, process="adapter"):
  100. """FusedMoE shell + the engine's method; loads this rank's experts through the real loader."""
  101. e_local = n_global // ep_size
  102. n = N_FULL // moe_tp_size
  103. layer = FusedMoE.__new__(FusedMoE)
  104. torch.nn.Module.__init__(layer)
  105. cfg = MoeRunnerConfig(num_experts=n_global, num_local_experts=e_local, hidden_size=K_HID,
  106. intermediate_size_per_partition=n, layer_id=0, top_k=topk, num_fused_shared_experts=0,
  107. params_dtype=torch.bfloat16, activation="silu", is_gated=True,
  108. routed_scaling_factor=None, swiglu_limit=LIMIT)
  109. for k, v in dict(layer_id=0, top_k=topk, hidden_size=K_HID, num_experts=n_global, num_fused_shared_experts=0,
  110. moe_ep_size=ep_size, moe_ep_rank=ep_rank, moe_tp_size=moe_tp_size, moe_tp_rank=moe_tp_rank,
  111. _num_global_routed=n_global, _expert_storage_rank=ep_rank, _num_local_routed=e_local,
  112. num_local_experts=e_local, _has_fused_shared=False, intermediate_size_per_partition=n,
  113. use_triton_kernels=False, use_presharded_weights=False, use_flashinfer_trtllm_moe=False,
  114. moe_runner_config=cfg, quant_config=QUANT_CFG, scheme=None, with_bias=False,
  115. reduce_results=False).items():
  116. setattr(layer, k, v)
  117. method = mq.Mxfp4FlashinferCutlassMoEMethod(fp8.Fp8MoEMethod(QUANT_CFG), prefix=prefix)
  118. layer.quant_method = method
  119. with torch.device(DEV):
  120. method.create_weights(layer, e_local, K_HID, n, torch.bfloat16,
  121. weight_loader=FusedMoE.weight_loader.__get__(layer))
  122. method.create_moe_runner(layer, cfg)
  123. src = {}
  124. for le in range(e_local):
  125. g = ep_rank * e_local + le
  126. t = expert_tensors(prefix, g)
  127. src[le] = t
  128. for w, shard in (("w1", "w1"), ("w3", "w3"), ("w2", "w2")):
  129. FusedMoE._weight_loader_impl(layer, param=getattr(layer, "w13_weight" if w != "w2" else "w2_weight"),
  130. loaded_weight=t[w], weight_name=f"experts.{w}.weight", shard_id=shard,
  131. expert_id=le)
  132. FusedMoE._weight_loader_impl(layer, param=getattr(layer, "w13_weight_scale_inv" if w != "w2"
  133. else "w2_weight_scale_inv"),
  134. loaded_weight=t[w + "s"], weight_name=f"experts.{w}.weight_scale_inv",
  135. shard_id=shard, expert_id=le)
  136. torch.cuda.synchronize()
  137. if process == "adapter":
  138. method.process_weights_after_loading(layer)
  139. else:
  140. type(method)._dsv41_b12x_next_orig_process(method, layer)
  141. torch.cuda.synchronize()
  142. return layer, method, src, None
  143. # ---------------------------------------------------------------------------- reference
  144. LUT = torch.tensor([0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0, -0.0, -0.5, -1.0, -1.5, -2.0, -3.0, -4.0, -6.0])
  145. def dq(packed, scale):
  146. p = packed.view(torch.uint8).to(DEV)
  147. lut = LUT.to(DEV)
  148. v = torch.stack([lut[(p & 15).long()], lut[(p >> 4).long()]], -1).reshape(p.shape[0], -1)
  149. s = torch.exp2(scale.view(torch.uint8).to(DEV).float() - 127)
  150. return (v.view(v.shape[0], -1, 32) * s[..., None]).view(v.shape[0], -1)
  151. @torch.no_grad()
  152. def reference(src, x, ids, w, *, e_local, ep_rank, moe_tp_size, moe_tp_rank):
  153. """fp32 MoE from the checkpoint slices, narrowed here independently of the engine loader."""
  154. n = N_FULL // moe_tp_size
  155. r0, r1 = moe_tp_rank * n, (moe_tp_rank + 1) * n
  156. out = torch.zeros(x.shape[0], K_HID, device=DEV)
  157. xf = x.float()
  158. for g in torch.unique(ids).tolist():
  159. le = g - ep_rank * e_local
  160. if g < 0 or not (0 <= le < e_local):
  161. continue
  162. rows, slots = (ids == g).nonzero(as_tuple=True)
  163. t = src[le]
  164. gate = xf[rows] @ dq(t["w1"][r0:r1], t["w1s"][r0:r1]).t()
  165. up = xf[rows] @ dq(t["w3"][r0:r1], t["w3s"][r0:r1]).t()
  166. act = torch.nn.functional.silu(gate.clamp(max=LIMIT)) * up.clamp(-LIMIT, LIMIT)
  167. w2 = dq(t["w2"][:, r0 // 2:r1 // 2].contiguous(), t["w2s"][:, r0 // 32:r1 // 32].contiguous())
  168. out.index_add_(0, rows, (act @ w2.t()) * w[rows, slots][:, None])
  169. return out
  170. def routing(m, n_global, topk, seed, pad_rows=1):
  171. g = torch.Generator(device=DEV)
  172. g.manual_seed(seed)
  173. ids = torch.rand(m, n_global, device=DEV, generator=g).argsort(dim=1)[:, :topk].to(torch.int32)
  174. w = torch.softmax(torch.randn(m, topk, device=DEV, generator=g), dim=1).float()
  175. if pad_rows and m > pad_rows:
  176. ids[-pad_rows:] = -1 # CUDA-graph padding rows
  177. x = torch.randn(m, K_HID, device=DEV, generator=g).to(torch.bfloat16)
  178. return x, ids.contiguous(), w
  179. def apply(layer, method, x, ids, w):
  180. d = StandardDispatchOutput(hidden_states=x, hidden_states_scale=None,
  181. topk_output=StandardTopKOutput(topk_weights=w, topk_ids=ids, router_logits=None))
  182. return method.apply(layer, d).hidden_states
  183. def err(a, b):
  184. a, b = a.float(), b.float()
  185. d = a - b
  186. return {"rel_l2": round(float(d.norm() / b.norm().clamp_min(1e-20)), 5), "max_abs": round(float(d.abs().max()), 4),
  187. "cos": round(float(torch.nn.functional.cosine_similarity(a.flatten(), b.flatten(), dim=0)), 6),
  188. "ref_max": round(float(b.abs().max()), 3)}
  189. def check_case(name, layer, method, src, geo, ms, *, fi=None):
  190. for m in ms:
  191. x, ids, w = routing(m, geo["n_global"], geo["topk"], 100 + m)
  192. t0 = time.time()
  193. out = apply(layer, method, x, ids, w)
  194. torch.cuda.synchronize()
  195. rec = {"case": name, "M": m, "eager_ms": round((time.time() - t0) * 1e3, 2)}
  196. ref = None
  197. if m <= 1024 or fi is None:
  198. ref = reference(src, x, ids, w, e_local=geo["n_global"] // geo["ep_size"], ep_rank=geo["ep_rank"],
  199. moe_tp_size=geo["moe_tp_size"], moe_tp_rank=geo["moe_tp_rank"])
  200. rec["vs_fp32_ref"] = err(out, ref)
  201. assert rec["vs_fp32_ref"]["rel_l2"] < 0.08, rec
  202. if fi is not None and m <= 1024: # FlashInfer's own workspace at M>1024 exceeds the 2 GB cap
  203. fo = apply(fi[0], fi[1], x, ids, w)
  204. rec["vs_flashinfer"] = err(out, fo)
  205. rec["flashinfer_vs_fp32_ref"] = err(fo, ref)
  206. assert rec["vs_flashinfer"]["rel_l2"] < 0.05, rec
  207. pad = out[-1].float().abs().max().item() if m > 1 else 0.0
  208. rec["pad_row_max_abs"] = pad
  209. assert pad == 0.0, rec
  210. assert torch.isfinite(out).all()
  211. RESULTS["cases"].append(rec)
  212. print(json.dumps(rec), flush=True)
  213. def strided(ids, w, layout):
  214. """The same values as views with other strides (fresh backing buffers)."""
  215. m, k = ids.shape
  216. if layout == "contig":
  217. return ids, w
  218. if layout == "padded": # router buffer wider than top-k: row stride k + 2
  219. bi = torch.full((m, k + 2), 7777, dtype=ids.dtype, device=DEV)
  220. bw = torch.full((m, k + 4), 5.0, dtype=w.dtype, device=DEV)
  221. bi[:, :k] = ids
  222. bw[:, 2:2 + k] = w
  223. return bi[:, :k], bw[:, 2:2 + k]
  224. if layout == "transposed": # [k, m] storage, column stride m
  225. return ids.t().contiguous().t(), w.t().contiguous().t()
  226. if layout == "mixed": # ids transposed, weights padded, ids int64
  227. bw = torch.full((m + 3, k + 3), 5.0, dtype=w.dtype, device=DEV)
  228. bw[3:, 1:1 + k] = w
  229. return ids.to(torch.int64).t().contiguous().t(), bw[3:, 1:1 + k]
  230. raise ValueError(layout)
  231. def remap_case():
  232. """ep_remap reads every input by its strides and writes contiguous outputs."""
  233. recs = []
  234. for m in (1, 5, 37, 130):
  235. for offset, num_local in ((0, 32), (16, 16), (40, 8)):
  236. g = torch.Generator(device=DEV)
  237. g.manual_seed(m * 100 + offset)
  238. ids = torch.randint(-1, 48, (m, 6), device=DEV, generator=g, dtype=torch.int32)
  239. if m > 2:
  240. ids[-2:] = -1 # CUDA-graph padding rows
  241. w = torch.rand(m, 6, device=DEV, generator=g)
  242. for layout in ("contig", "padded", "transposed", "mixed"):
  243. vi, vw = strided(ids, w, layout)
  244. oi = torch.full((m, 6), -99, dtype=torch.int32, device=DEV)
  245. ow = torch.full((m, 6), -9.0, dtype=torch.float32, device=DEV)
  246. ad.ep_remap(vi, vw, oi, ow, offset, num_local)
  247. ri, rw = ad.ep_remap_reference(vi, vw, offset, num_local)
  248. ok = torch.equal(oi, ri) and torch.equal(ow, rw)
  249. recs.append((m, offset, layout, vi.stride(), vw.stride(), ok))
  250. assert ok, recs[-1]
  251. # graph replay on strided views: new values written into the same backing buffers
  252. m = 37
  253. ids = torch.randint(-1, 48, (m, 6), device=DEV, dtype=torch.int32)
  254. w = torch.rand(m, 6, device=DEV)
  255. vi, vw = strided(ids, w, "padded")
  256. oi = torch.empty(m, 6, dtype=torch.int32, device=DEV)
  257. ow = torch.empty(m, 6, dtype=torch.float32, device=DEV)
  258. ad.ep_remap(vi, vw, oi, ow, 16, 16)
  259. torch.cuda.synchronize()
  260. gr = torch.cuda.CUDAGraph()
  261. with torch.cuda.graph(gr):
  262. ad.ep_remap(vi, vw, oi, ow, 16, 16)
  263. vi.copy_(torch.randint(-1, 48, (m, 6), device=DEV, dtype=torch.int32))
  264. vi[-3:] = -1
  265. vw.copy_(torch.rand(m, 6, device=DEV))
  266. gr.replay()
  267. torch.cuda.synchronize()
  268. ri, rw = ad.ep_remap_reference(vi, vw, 16, 16)
  269. assert torch.equal(oi, ri) and torch.equal(ow, rw), "graph replay on strided views"
  270. rec = {"remap_strided": len(recs), "layouts": sorted({r[2] for r in recs}), "graph_replay": True}
  271. RESULTS["remap"] = rec
  272. print(json.dumps(rec), flush=True)
  273. def strided_apply_case(name, layer, method, geo, ms):
  274. """The fused path fed strided router outputs matches the contiguous run (remap path)."""
  275. for m in ms:
  276. x, ids, w = routing(m, geo["n_global"], geo["topk"], 300 + m)
  277. base = apply(layer, method, x, ids, w)
  278. spread = err(apply(layer, method, x, ids, w), base)["rel_l2"] # atomic reduction order
  279. for layout in ("padded", "transposed", "mixed"):
  280. vi, vw = strided(ids, w, layout)
  281. out = apply(layer, method, x, vi, vw)
  282. e = err(out, base)
  283. rec = {"case": name, "M": m, "layout": layout, "vs_contiguous": e, "eager_spread_rel_l2": spread,
  284. "pad_row_max_abs": out[-1].float().abs().max().item() if m > 1 else 0.0}
  285. assert e["rel_l2"] < max(2e-3, 3 * spread) and e["rel_l2"] < 0.02, rec
  286. assert rec["pad_row_max_abs"] == 0.0, rec
  287. RESULTS["cases"].append(rec)
  288. print(json.dumps(rec), flush=True)
  289. def graph_case(name, layer, method, geo, m, layout="contig"):
  290. x, ids, w = routing(m, geo["n_global"], geo["topk"], 7 + m)
  291. ids, w = strided(ids, w, layout)
  292. s = torch.cuda.Stream()
  293. s.wait_stream(torch.cuda.current_stream())
  294. with torch.cuda.stream(s):
  295. apply(layer, method, x, ids, w)
  296. torch.cuda.current_stream().wait_stream(s)
  297. g = torch.cuda.CUDAGraph()
  298. with torch.cuda.graph(g):
  299. out = apply(layer, method, x, ids, w)
  300. x2, ids2, w2 = routing(m, geo["n_global"], geo["topk"], 900 + m)
  301. x.copy_(x2), ids.copy_(ids2), w.copy_(w2)
  302. g.replay()
  303. torch.cuda.synchronize()
  304. eager = apply(layer, method, x2, ids2, w2)
  305. eager2 = apply(layer, method, x2, ids2, w2)
  306. e = err(out, eager)
  307. e_run = err(eager2, eager) # run-to-run spread of eager itself (atomic reduction order)
  308. reps = 200
  309. a, b = torch.cuda.Event(enable_timing=True), torch.cuda.Event(enable_timing=True)
  310. a.record()
  311. for _ in range(reps):
  312. g.replay()
  313. b.record()
  314. b.synchronize()
  315. rec = {"case": name, "M": m, "layout": layout, "graph_vs_eager": e, "eager_vs_eager": e_run,
  316. "bit_identical": bool(torch.equal(out, eager)), "eager_deterministic": bool(torch.equal(eager2, eager)),
  317. "replay_us": round(a.elapsed_time(b) * 1000 / reps, 1)}
  318. # graph replay must match eager to within eager's own run-to-run spread (bf16 ulps)
  319. assert e["rel_l2"] < max(1e-3, 3 * e_run["rel_l2"]) and e["rel_l2"] < 0.02, rec
  320. RESULTS["graph"].append(rec)
  321. print(json.dumps(rec), flush=True)
  322. del g
  323. def sweep(name, layer, method, geo, ms):
  324. bad = []
  325. t0 = time.time()
  326. for m in ms:
  327. x, ids, w = routing(m, geo["n_global"], geo["topk"], m, pad_rows=0)
  328. try:
  329. out = apply(layer, method, x, ids, w)
  330. if not bool(torch.isfinite(out).all()):
  331. bad.append((m, "nonfinite"))
  332. except Exception as exc: # noqa: BLE001
  333. bad.append((m, repr(exc)[:200]))
  334. torch.cuda.synchronize()
  335. RESULTS["sweep"][name] = {"count": len(ms), "failures": bad[:20], "n_fail": len(bad),
  336. "seconds": round(time.time() - t0, 1)}
  337. print(json.dumps({"sweep": name, **RESULTS["sweep"][name]}), flush=True)
  338. assert not bad, bad[:5]
  339. def m64_case(layer, src, geo):
  340. """M64 prefill tile (the adapter's geometry, caps >= M64_MIN_CAP) against an M16 geometry built
  341. on the same prepared experts, same inputs."""
  342. st = layer._dsv41_b12x_next
  343. g64 = st.geom
  344. assert ad._m64_admitted(), "runtime lacks the compact-N64 M64 patch"
  345. tiles = {c: g64.plans[c].config for c in g64.caps}
  346. big = [c for c in g64.caps if c >= ad.M64_MIN_CAP and c > ad.EXACT_MAX]
  347. assert big and all("dynamic_tile_m=64" in tiles[c] for c in big), {c: tiles[c][:160] for c in big}
  348. assert not any("dynamic_tile_m=64" in tiles[c] for c in g64.caps if c not in big)
  349. saved = ad.M64_MIN_CAP, ad.TUNE
  350. ad.M64_MIN_CAP, ad.TUNE = 0, False
  351. try:
  352. g16 = ad._Geometry(("m16-check",) + g64.key, g64.E, g64.K, g64.N, g64.topk, 0, g64.limit, DEV, ep_size=1)
  353. g16.build(st.experts)
  354. finally:
  355. ad.M64_MIN_CAP, ad.TUNE = saved
  356. assert not any("dynamic_tile_m=64" in g16.plans[c].config for c in g16.caps)
  357. kw = {k: geo[k] for k in ("ep_rank", "moe_tp_size", "moe_tp_rank")}
  358. worst = {"m64_vs_m16_rel_l2": 0.0, "fp32_rel_l2_gap": 0.0}
  359. for kind in ("conc", "uniform"):
  360. for m in (2048, 2049, 4095, 4096, 5000):
  361. g = torch.Generator(device=DEV)
  362. g.manual_seed(5000 + m)
  363. if kind == "conc": # every row on the same top-k experts
  364. cand = torch.randperm(geo["n_global"], device=DEV, generator=g)[:geo["topk"]]
  365. ids = cand[torch.rand(m, geo["topk"], device=DEV, generator=g).argsort(dim=1)]
  366. else:
  367. ids = torch.rand(m, geo["n_global"], device=DEV, generator=g).argsort(dim=1)[:, :geo["topk"]]
  368. ids = ids.to(torch.int32).contiguous()
  369. w = torch.softmax(torch.randn(m, geo["topk"], device=DEV, generator=g), dim=1).float()
  370. x = torch.randn(m, K_HID, device=DEV, generator=g).to(torch.bfloat16)
  371. o64 = torch.empty(m, K_HID, dtype=torch.bfloat16, device=DEV)
  372. o16 = torch.empty_like(o64)
  373. o16b = torch.empty_like(o64)
  374. g64.forward(st.impl, x, ids, w, o64)
  375. g16.forward(st.impl, x, ids, w, o16)
  376. g16.forward(st.impl, x, ids, w, o16b)
  377. ref = reference(src, x, ids, w, e_local=geo["n_global"], **kw)
  378. d, e64, e16 = err(o64, o16), err(o64, ref), err(o16, ref)
  379. spread = err(o16b, o16)["rel_l2"]
  380. gap = abs(e64["rel_l2"] - e16["rel_l2"])
  381. rec = {"case": "m64_vs_m16", "routing": kind, "M": m, "m64_vs_m16": d, "m16_vs_m16": spread,
  382. "m64_vs_fp32": e64, "m16_vs_fp32": e16, "cap": g64.cap_for(min(m, g64.chunk))}
  383. print(json.dumps(rec), flush=True)
  384. RESULTS["cases"].append(rec)
  385. assert d["rel_l2"] <= max(1e-3, 3 * spread) and d["rel_l2"] < 5e-3, rec
  386. assert gap <= 2e-4 and e64["rel_l2"] < 0.08, rec
  387. assert torch.isfinite(o64).all()
  388. worst["m64_vs_m16_rel_l2"] = max(worst["m64_vs_m16_rel_l2"], d["rel_l2"])
  389. worst["fp32_rel_l2_gap"] = max(worst["fp32_rel_l2_gap"], round(gap, 5))
  390. del x, o64, o16, o16b, ref
  391. RESULTS["m64"] = worst
  392. print(json.dumps({"m64": worst}), flush=True)
  393. del g16
  394. torch.cuda.empty_cache()
  395. def deterministic_case():
  396. """DSV41_MOE_B12X_NEXT_DETERMINISTIC=1, as the module sets it at import: deterministic routing
  397. spec, remap path, internal route planner at <= 8 rows. A fresh EP1 geometry."""
  398. saved = dict(ad._GEOMS), ad.DETERMINISTIC, ad.SMALL_PLANNER
  399. ad._GEOMS.clear()
  400. ad.DETERMINISTIC, ad.SMALL_PLANNER = True, "internal"
  401. try:
  402. geo = dict(n_global=NE, topk=6, ep_size=1, ep_rank=0, moe_tp_size=4, moe_tp_rank=1)
  403. ly, me, sr, _ = make_layer("layers.5", **{k: geo[k] for k in ("topk", "ep_size", "ep_rank", "moe_tp_size",
  404. "moe_tp_rank")}, n_global=NE)
  405. g = ly._dsv41_b12x_next.geom
  406. assert not g.direct and "internal" in g.plans[6].config, g.plans[6].config[:200]
  407. check_case("ep1_deterministic_layer5_tp1", ly, me, sr, geo, [1, 6, 8, 48, 1024])
  408. same = []
  409. for m in (6, 96, 2049):
  410. x, ids, w = routing(m, NE, 6, 11 + m)
  411. same.append(bool(torch.equal(apply(ly, me, x, ids, w), apply(ly, me, x, ids, w))))
  412. rec = {"case": "ep1_deterministic", "bit_identical_rerun": same, "small_plan": g.plans[6].config[:160]}
  413. print(json.dumps(rec), flush=True)
  414. RESULTS["deterministic"] = rec
  415. assert all(same), rec
  416. del ly, me, sr
  417. finally:
  418. ad._GEOMS.clear()
  419. ad._GEOMS.update(saved[0])
  420. ad.DETERMINISTIC, ad.SMALL_PLANNER = saved[1], saved[2]
  421. torch.cuda.empty_cache()
  422. def sizing():
  423. """Scratch bytes per capacity for the production geometries (heuristic plans; no weights needed)."""
  424. B = ad._b12x()
  425. fm = B["fm"]
  426. from b12x_next.moe.fused_moe import _impl
  427. out = {}
  428. for name, (e, n, k) in {"ep1_target": (384, 576, 6), "ep1_draft": (128, 576, 3),
  429. "ep2_target": (192, 1152, 6), "ep2_draft": (64, 1152, 3)}.items():
  430. wp = fm.plan_weights(
  431. source=fm.PackedSource(format=fm.PackedSourceFormat.MXFP4_E8M0_K32, w13_layout=fm.W13Layout.W13),
  432. activation=fm.ActivationSpec(mode=fm.ActivationMode.A8, nonlinearity="silu", io_dtype=torch.bfloat16,
  433. swiglu_limit=LIMIT),
  434. geometry=fm.MoEGeometry(num_experts=e, hidden_size=K_HID, intermediate_size=n))
  435. from b12x_next.moe.fused_moe import _preparation as fp, _tuning as ft
  436. from b12x_next.preparation import FrozenMapping
  437. from b12x_next.preparation.device import detect_device
  438. ident = detect_device(DEV).identity
  439. fake = NS(plan=wp, num_experts=e, hidden_size=K_HID, intermediate_size=n,
  440. _impl=NS(can_share_input=lambda **kw: False))
  441. row = {}
  442. for cap in (6, 96, 1024, 4096):
  443. q = fp._query(fake, fm.ExecutionCapacity(max_tokens=cap, top_k=k), cap, fm.RoutingSpec(),
  444. fp._control_snapshot(), FrozenMapping())
  445. caps = fp._lower_caps(q, ft._default_config(q, ident), wp._impl, DEV)
  446. row[cap] = round(_impl.tp_moe_required_nbytes(caps) / 2**20, 1)
  447. out[name] = row
  448. RESULTS["scratch_mb_by_capacity"] = out
  449. print(json.dumps({"scratch_mb_by_capacity": out}), flush=True)
  450. def main():
  451. try:
  452. sizing()
  453. except Exception as exc: # noqa: BLE001 - informational
  454. print(f"sizing failed: {exc!r}", flush=True)
  455. remap_case()
  456. ms = [6, 48, 96, 1024] if QUICK else [6, 48, 96, 1024, 5000]
  457. torch.cuda.reset_peak_memory_stats()
  458. # ---- EP1 target (N=576), two layers sharing one geometry, different TP ranks
  459. geo1 = dict(n_global=NE, topk=6, ep_size=1, ep_rank=0, moe_tp_size=4, moe_tp_rank=3)
  460. l3, m3, s3, _ = make_layer("layers.3", **{k: geo1[k] for k in ("topk", "ep_size", "ep_rank", "moe_tp_size", "moe_tp_rank")},
  461. n_global=NE)
  462. check_case("ep1_target_layer3_tp3", l3, m3, s3, geo1, ms)
  463. geo1b = dict(geo1, moe_tp_rank=0)
  464. l20, m20, s20, _ = make_layer("layers.20", **{k: geo1b[k] for k in ("topk", "ep_size", "ep_rank", "moe_tp_size", "moe_tp_rank")},
  465. n_global=NE)
  466. check_case("ep1_target_layer20_tp0", l20, m20, s20, geo1b, [6, 96])
  467. check_case("ep1_target_layer3_tp3_after_layer20", l3, m3, s3, geo1, [6]) # shared plan, own experts
  468. graph_case("ep1_target_layer3", l3, m3, geo1, 6)
  469. graph_case("ep1_target_layer3", l3, m3, geo1, 96)
  470. graph_case("ep1_target_layer3", l3, m3, geo1, 6, layout="padded")
  471. strided_apply_case("ep1_target_layer3_strided", l3, m3, geo1, [6, 96])
  472. m64_case(l3, s3, geo1)
  473. # ---- EP1 draft (mtp.0: 128 experts, top-3)
  474. geod = dict(n_global=NE, topk=3, ep_size=1, ep_rank=0, moe_tp_size=4, moe_tp_rank=2)
  475. ld, md, sd, _ = make_layer("mtp.0", **{k: geod[k] for k in ("topk", "ep_size", "ep_rank", "moe_tp_size", "moe_tp_rank")},
  476. n_global=NE)
  477. check_case("ep1_draft_mtp0_tp2", ld, md, sd, geod, [5, 40, 80, 1024])
  478. graph_case("ep1_draft_mtp0", ld, md, geod, 5)
  479. RESULTS["memory"]["after_ep1_mb"] = round(torch.cuda.max_memory_allocated() / 2**20, 1)
  480. del l20, m20, s20, ld, md, sd
  481. torch.cuda.empty_cache()
  482. # ---- EP2 target (N=1152), ep_rank 1, vs stock FlashInfer on a second copy of the same slice
  483. geo2 = dict(n_global=2 * NE, topk=6, ep_size=2, ep_rank=1, moe_tp_size=2, moe_tp_rank=1)
  484. kw2 = {k: geo2[k] for k in ("topk", "ep_size", "ep_rank", "moe_tp_size", "moe_tp_rank")}
  485. lb, mb, sb, _ = make_layer("layers.3", n_global=2 * NE, **kw2)
  486. lf, mf, _, _ = make_layer("layers.3", n_global=2 * NE, process="flashinfer", **kw2)
  487. # stock FlashInfer weights reaching the b12x_next fused function: refuse, never run the stock kernel
  488. xr, ir, wr = routing(6, 2 * NE, 6, 1)
  489. try:
  490. apply(lf, mf, xr, ir, wr)
  491. raise AssertionError("an unconverted layer ran through the b12x_next fused function")
  492. except RuntimeError as exc:
  493. assert "without a b12x_next conversion" in str(exc), exc
  494. print(json.dumps({"case": "unconverted_layer_refuses", "ok": True}), flush=True)
  495. mf.runner.fused = ad._state["orig_fused"] # the reference layer runs the stock kernel directly
  496. check_case("ep2_target_layer3_rank1", lb, mb, sb, geo2, ms, fi=(lf, mf))
  497. graph_case("ep2_target_layer3", lb, mb, geo2, 6)
  498. graph_case("ep2_target_layer3", lb, mb, geo2, 96)
  499. graph_case("ep2_target_layer3", lb, mb, geo2, 96, layout="mixed")
  500. strided_apply_case("ep2_target_layer3_strided", lb, mb, geo2, [6, 96, 1024])
  501. del lf, mf
  502. torch.cuda.empty_cache()
  503. # ---- capacity ladder sweep (every M up to 1100, then strided to past the top capacity)
  504. sweep_ms = list(range(1, 1101)) + list(range(1101, 4400, 13)) if not QUICK else list(range(1, 200))
  505. sweep("ep2_target", lb, mb, geo2, sweep_ms)
  506. sweep("ep1_target", l3, m3, geo1, sweep_ms)
  507. del lb, mb, sb
  508. torch.cuda.empty_cache()
  509. deterministic_case()
  510. RESULTS["memory"]["peak_total_mb"] = round(torch.cuda.max_memory_allocated() / 2**20, 1)
  511. RESULTS["geometries"] = ad.geometry_reports()
  512. print("RESULTS " + json.dumps(RESULTS, default=str), flush=True)
  513. print("ALL OK", flush=True)
  514. if __name__ == "__main__":
  515. main()