test_moe_b12x_next_cpu.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. """CPU checks of adapter/moe_b12x_next.py's host-side logic (no GPU, no engine, no b12x_next build).
  2. - hook order: install_method -> install_runner and install_runner -> install_method both import
  3. b12x_next through ``_b12x()`` (pinned-commit and drift checks), and a wrong pin or a drifted
  4. b12x source refuses in both orders (fake ``b12x_next`` / ``sglang`` packages on disk).
  5. - a layer that reaches the fused function without a conversion refuses instead of running the
  6. stock FlashInfer kernel on checkpoint-layout weights.
  7. - chunk selection: the chunk is the largest prepared capacity one launch can run; with
  8. CHUNKED_PREFILL_SIZE=65536 (ladder top 65536 above the E=384 launch limit of 61,717 tokens) the
  9. 65536 plan is dropped, the chunk is 4096, and ``forward`` never hands a plan more rows than its
  10. capacity for any M up to 140k; ``_run`` refuses an oversized batch.
  11. - CUDA_GRAPH_MAX_BS_DECODE above the adapter's graph list names the uncovered row counts.
  12. - DSV41_MOE_B12X_NEXT_DETERMINISTIC=1 moves the <= 8 row plan to the internal route planner and
  13. races nothing.
  14. usage: python3 tests/test_moe_b12x_next_cpu.py (needs torch; runs each scenario in a subprocess)
  15. """
  16. import json
  17. import os
  18. import subprocess
  19. import sys
  20. import tempfile
  21. import textwrap
  22. HERE = os.path.dirname(os.path.abspath(__file__))
  23. ADAPTER = os.path.join(HERE, "..", "adapter")
  24. FAKE_FILES = {
  25. "b12x_next/__init__.py": "",
  26. "b12x_next/preparation/__init__.py": "class PreparationSession:\n pass\n",
  27. "b12x_next/moe/__init__.py": "",
  28. "b12x_next/moe/fused_moe/__init__.py": "def run(binding):\n pass\n",
  29. "b12x_next/moe/fused_moe/_preparation.py": textwrap.dedent("""\
  30. class _FusedMoeState:
  31. def bind(self, **kwargs):
  32. kwargs["experts"] = self.experts._impl
  33. b = self.scratch.bind(**kwargs)
  34. return b, dict(compact_launches=self.compact_launches), "unit_scale_contract", "_w4a16_launches"
  35. """),
  36. "b12x_next/moe/fused_moe/api.py": textwrap.dedent("""\
  37. from dataclasses import replace
  38. def bind(state, plan, **kwargs):
  39. return replace(state.bind(**kwargs), plan=plan)
  40. """),
  41. "sglang/__init__.py": "",
  42. "sglang/srt/__init__.py": "",
  43. "sglang/srt/distributed/__init__.py": "def get_tp_group():\n return None\n",
  44. "sglang/srt/distributed/device_communicators/__init__.py": "",
  45. "sglang/srt/distributed/device_communicators/pynccl_allocator.py": "def use_symmetric_memory(g):\n pass\n",
  46. "sglang/srt/layers/__init__.py": "",
  47. "sglang/srt/layers/dp_attention.py": "def is_allocation_symmetric():\n return False\n",
  48. "sglang/srt/layers/quantization/__init__.py": "",
  49. "sglang/srt/layers/quantization/mxfp4_flashinfer_cutlass_moe.py": textwrap.dedent("""\
  50. import torch
  51. # the stock method dispatches to fused_experts_none_to_flashinfer_mxfp4
  52. class Mxfp4FlashinferCutlassMoEMethod:
  53. load_up_proj_weight_first = True
  54. def create_weights(self, layer, e, h, n, dt, **kw):
  55. assert n % 128 == 0
  56. self._fp8.create_weights(layer, e, h, n, dt, fp4_scale_dtype=torch.float8_e8m0fnu, **kw)
  57. def process_weights_after_loading(self, layer):
  58. self._fp8.process_weights_after_loading(layer)
  59. block_scale_interleave(layer)
  60. def create_moe_runner(self, layer, cfg):
  61. pass
  62. def apply(self, layer, d):
  63. pass
  64. """),
  65. "sglang/srt/layers/moe/__init__.py": "",
  66. "sglang/srt/layers/moe/topk.py": "class TopKOutputChecker:\n pass\n",
  67. "sglang/srt/layers/moe/token_dispatcher/__init__.py": "",
  68. "sglang/srt/layers/moe/token_dispatcher/standard.py": "class StandardCombineInput:\n pass\n",
  69. "sglang/srt/layers/moe/moe_runner/__init__.py": "",
  70. "sglang/srt/layers/moe/moe_runner/base.py": textwrap.dedent("""\
  71. def fused_experts_none_to_flashinfer_mxfp4(dispatch_output, quant_info, runner_config):
  72. return "stock"
  73. class FusedOpPool:
  74. _fused_funcs = {("none", "flashinfer_mxfp4"): fused_experts_none_to_flashinfer_mxfp4}
  75. """),
  76. "sglang/srt/layers/moe/moe_runner/flashinfer_cutlass.py": "",
  77. "sglang/srt/layers/moe/fused_moe_triton/__init__.py": "",
  78. "sglang/srt/layers/moe/fused_moe_triton/layer.py": textwrap.dedent("""\
  79. class FusedMoE:
  80. def _load_w13(self, loaded_weight, shard_dim):
  81. if getattr(self.quant_method, "load_up_proj_weight_first", False):
  82. pass
  83. shard_size = loaded_weight.shape[shard_dim] // self.moe_tp_size
  84. start = shard_size
  85. def _load_w2(self, loaded_weight, shard_dim):
  86. shard_size = loaded_weight.shape[shard_dim] // self.moe_tp_size
  87. """),
  88. }
  89. def make_tree(root, commit, drift=False):
  90. for rel, text in FAKE_FILES.items():
  91. path = os.path.join(root, rel)
  92. os.makedirs(os.path.dirname(path), exist_ok=True)
  93. if drift and rel.endswith("_preparation.py"):
  94. text = text.replace("unit_scale_contract", "unit_scale")
  95. with open(path, "w") as f:
  96. f.write(text)
  97. with open(os.path.join(root, "b12x_next", "SOURCE_COMMIT"), "w") as f:
  98. f.write(commit + "\n")
  99. def run(code, env=None, path=None):
  100. """Runs ``code`` in a fresh interpreter with the adapter (and a fake tree) importable; returns stdout."""
  101. e = dict(os.environ)
  102. for k in list(e):
  103. # PYTHONPATH: the image's adapter dir carries sitecustomize.py, whose import hooks would
  104. # install the adapter on the fake modules' import and take the hook order out of our hands
  105. if k == "PYTHONPATH" or k.startswith(("DSV41_", "CUDA_GRAPH_MAX_BS_DECODE", "CHUNKED_PREFILL_SIZE",
  106. "DSPARK_", "B12X_")):
  107. del e[k]
  108. e.update({"DSV41_MOE_B12X_NEXT": "1", "STATE_PATH": tempfile.gettempdir()}, **(env or {}))
  109. pre = f"import sys; sys.path[:0] = {[p for p in (path, ADAPTER) if p]!r}\n"
  110. r = subprocess.run([sys.executable, "-c", pre + textwrap.dedent(code)], env=e, capture_output=True, text=True,
  111. timeout=300)
  112. if r.returncode != 0:
  113. raise AssertionError(f"scenario failed:\n{code}\n--- stdout\n{r.stdout}\n--- stderr\n{r.stderr[-3000:]}")
  114. return r.stdout
  115. HOOKS = """
  116. import moe_b12x_next as ad
  117. import sglang.srt.layers.quantization.mxfp4_flashinfer_cutlass_moe as mq
  118. import sglang.srt.layers.moe.moe_runner.flashinfer_cutlass as fic
  119. steps = {"method": lambda: ad.install_method(mq), "runner": lambda: ad.install_runner(fic)}
  120. out = []
  121. for name in ORDER:
  122. try:
  123. steps[name]()
  124. out.append([name, "ok", sorted(ad._B), sorted(ad._ENG)])
  125. except RuntimeError as exc:
  126. out.append([name, "refused", str(exc)[:200]])
  127. break
  128. print("RESULT " + __import__("json").dumps(out))
  129. """
  130. def hooks(tree, order):
  131. out = run(f"ORDER = {order!r}\n" + HOOKS, path=tree)
  132. return json.loads(out.split("RESULT ", 1)[1])
  133. def check_hook_order():
  134. pinned = run("import moe_b12x_next as ad; print(ad.PINNED_COMMIT)").strip()
  135. with tempfile.TemporaryDirectory() as good, tempfile.TemporaryDirectory() as bad, \
  136. tempfile.TemporaryDirectory() as drift:
  137. make_tree(good, pinned)
  138. make_tree(bad, "0" * 40)
  139. make_tree(drift, pinned, drift=True)
  140. for order in (["method", "runner"], ["runner", "method"]):
  141. res = hooks(good, order)
  142. assert [r[1] for r in res] == ["ok", "ok"], res
  143. for r in res: # b12x modules present from the first hook on
  144. assert {"fm", "prep", "pkg"} <= set(r[2]), (order, r)
  145. assert {"get_tp_group", "TopKOutputChecker"} <= set(res[-1][3]), res
  146. assert not {"fm", "prep"} & set(res[-1][3]) and "get_tp_group" not in res[-1][2], res
  147. for tree, why in ((bad, "is built from 0000"), (drift, "drifted")):
  148. res = hooks(tree, order)
  149. assert res[0][1] == "refused" and why in res[0][2], (order, why, res)
  150. print("hook order: method->runner and runner->method both run the pin + drift checks; "
  151. "a wrong pin / drifted source refuses in both orders")
  152. def check_unconverted_layer():
  153. out = run("""
  154. import torch, types, moe_b12x_next as ad
  155. qi = types.SimpleNamespace(w13_weight=torch.zeros(2, 4, dtype=torch.uint8))
  156. ad._state["orig_fused"] = lambda *a: "stock kernel ran"
  157. try:
  158. ad._fused_b12x_next(None, qi, None)
  159. print("RAN")
  160. except RuntimeError as exc:
  161. print("REFUSED", exc)
  162. """)
  163. assert out.startswith("REFUSED") and "without a b12x_next conversion" in out, out
  164. print("unconverted layer: refuses instead of running the stock kernel")
  165. CHUNK = """
  166. import torch, types, moe_b12x_next as ad
  167. LIMIT = 61717 # _safe_dynamic_token_chunk at E=384, K=5120, N=576, top-6
  168. g = ad._Geometry.__new__(ad._Geometry)
  169. caps = g.capacities()
  170. # dynamic plans launch up to LIMIT tokens; b12x's micro plan (tiny caps) launches its capacity
  171. limits = {c: (c if c <= 8 else LIMIT) for c in caps}
  172. chunk, kept, dropped = ad.select_chunk(limits)
  173. g.caps, g.chunk, g.direct = kept, chunk, True
  174. g.table = [None] + [g._lookup(m) for m in range(1, ad.EXACT_MAX + 1)]
  175. seen, worst = set(), 0
  176. def fake_run(cap, x, ids, w, out, impl):
  177. global worst
  178. assert x.shape[0] <= cap and cap in kept, (x.shape[0], cap)
  179. worst = max(worst, x.shape[0] - cap)
  180. seen.add(cap)
  181. g._run = fake_run
  182. for m in list(range(1, 5000)) + [8191, 8192, 61717, 61718, 65535, 65536, 65537, 140000]:
  183. x = torch.empty(m, 0)
  184. ids = torch.empty(m, 6, dtype=torch.int32)
  185. w = torch.empty(m, 6, dtype=torch.float32)
  186. g.forward(None, x, ids, w, x)
  187. # the real _run refuses a batch larger than the plan's capacity before touching the plan
  188. try:
  189. ad._Geometry._run(g, 4096, torch.empty(4097, 0), None, None, None, None)
  190. raised = False
  191. except RuntimeError:
  192. raised = True
  193. # a launch limit between two ladder steps (Fable M2): chunk = the largest capacity under it
  194. c2 = ad.select_chunk({c: (c if c <= 8 else 3000) for c in caps})
  195. try:
  196. ad.select_chunk({4096: 100})
  197. none_ok = True
  198. except RuntimeError:
  199. none_ok = False
  200. print("RESULT " + __import__("json").dumps(dict(ladder=ad.LADDER, caps=caps, chunk=chunk, kept_top=kept[-3:],
  201. dropped=dropped, raised=raised, c2=[c2[0], c2[2]], none_ok=none_ok, seen_top=max(seen))))
  202. """
  203. def check_chunk_selection():
  204. res = json.loads(run(CHUNK, env={"CHUNKED_PREFILL_SIZE": "65536"}).split("RESULT ", 1)[1])
  205. assert res["ladder"][-1] == 65536 and 65536 in res["caps"], res
  206. assert res["chunk"] == 4096 and res["dropped"] == [65536] and res["seen_top"] == 4096, res
  207. assert res["raised"] and res["c2"] == [2048, [4096, 65536]] and not res["none_ok"], res
  208. base = json.loads(run(CHUNK, env={"CHUNKED_PREFILL_SIZE": "4096"}).split("RESULT ", 1)[1])
  209. assert base["chunk"] == 4096 and base["dropped"] == [] and base["caps"] == res["caps"][:-1], base
  210. print(f"chunk selection: CHUNKED_PREFILL_SIZE=65536 -> capacity 65536 dropped (limit 61717), chunk "
  211. f"{res['chunk']}; every chunk of M in 1..140000 fits its plan; oversized _run refuses; limit 3000 -> "
  212. f"chunk {res['c2'][0]}; production ladder (4096) unchanged")
  213. GRAPH = """
  214. import moe_b12x_next as ad
  215. tuned = sorted(ad._Geometry.tuned_caps(type("G", (), {"topk": 6})()))
  216. print("RESULT " + __import__("json").dumps(dict(rows=ad.uncovered_rows(), planner=ad.SMALL_PLANNER, bs=ad.GRAPH_BS,
  217. tuned=tuned)))
  218. """
  219. def graph(env):
  220. return json.loads(run(GRAPH, env=env).split("RESULT ", 1)[1])
  221. def check_graph_bs_and_planner():
  222. assert graph({})["rows"] == [] and graph({"CUDA_GRAPH_MAX_BS_DECODE": "16"})["rows"] == []
  223. r = graph({"CUDA_GRAPH_MAX_BS_DECODE": "32"})
  224. want = sorted({k * b for k in (5, 6) for b in range(17, 33)})
  225. assert r["rows"] == want and r["bs"][-1] == 16, r
  226. r = graph({"CUDA_GRAPH_MAX_BS_DECODE": "32", "DSV41_MOE_B12X_NEXT_GRAPH_BS": ",".join(map(str, range(1, 33)))})
  227. assert r["rows"] == [], r
  228. r = graph({"CUDA_GRAPH_MAX_BS_DECODE": "8"})
  229. assert r["rows"] == [] and r["bs"] == [1, 2, 3, 4, 5, 6, 7, 8], r
  230. d = graph({})
  231. assert d["planner"] == "triton" and d["tuned"] == [6 * b for b in d["bs"]], d
  232. det = graph({"DSV41_MOE_B12X_NEXT_DETERMINISTIC": "1"})
  233. assert det["planner"] == "internal" and det["tuned"] == [], det
  234. assert graph({"DSV41_MOE_B12X_NEXT_SMALL_PLAN": "internal:none:16"})["planner"] == "internal"
  235. print(f"graph bs: CUDA_GRAPH_MAX_BS_DECODE=32 names {len(want)} uncovered row counts ({want[0]}..{want[-1]}), "
  236. f"none at 16 or with a covering list; DETERMINISTIC=1 -> internal route planner, no race")
  237. TABLE = """
  238. import moe_b12x_next as ad
  239. print("RESULT " + __import__("json").dumps(dict(
  240. table=ad.PLAN_TABLE, p6=[ad.table_plan(c, 6) for c in (1, 6, 8, 9, 96, 4096)],
  241. p3=[ad.table_plan(c, 3) for c in (5, 40, 80)])))
  242. """
  243. def table(env):
  244. return json.loads(run(TABLE, env=env).split("RESULT ", 1)[1])
  245. def check_plan_table():
  246. d = table({})
  247. assert d["p6"] == [["triton", 48, 16]] * 3 + [None] * 3 and d["p3"] == [["triton", 48, 16], None, None], d
  248. det = table({"DSV41_MOE_B12X_NEXT_DETERMINISTIC": "1"})
  249. assert det["p6"][:3] == [["internal", 48, 16]] * 3 and det["p6"][3:] == [None] * 3, det
  250. assert table({"DSV41_MOE_B12X_NEXT_SMALL_PLAN": "internal:none:16"})["p6"][0] == ["internal", None, 16]
  251. t = table({"DSV41_MOE_B12X_NEXT_DETERMINISTIC": "1",
  252. "DSV41_MOE_B12X_NEXT_PLAN_TABLE": "1-8=triton:48:16, 40-80@k3=internal:32:16, 96=heur, 9-96=internal:none:16"})
  253. assert t["p6"] == [["internal", 48, 16]] * 3 + [["internal", None, 16], None, None], t
  254. assert t["p3"] == [["internal", 48, 16], ["internal", 32, 16], ["internal", 32, 16]], t
  255. for bad in ("1-8=cutlass:48:16", "8-1=internal:48:16", "x=heur", "1-8=internal:48"):
  256. try:
  257. run(TABLE, env={"DSV41_MOE_B12X_NEXT_PLAN_TABLE": bad})
  258. raise RuntimeError(f"{bad!r} was accepted")
  259. except AssertionError as exc:
  260. assert "DSV41_MOE_B12X_NEXT_PLAN_TABLE: bad" in str(exc), (bad, str(exc)[-300:])
  261. print("plan table: default = <= 8 row pin only (unchanged plans), SMALL_PLAN still honoured, top-k scoping, "
  262. "first match wins, heur entries, deterministic triton -> internal, malformed entries refuse to load")
  263. def main():
  264. check_hook_order()
  265. check_unconverted_layer()
  266. check_chunk_selection()
  267. check_graph_bs_and_planner()
  268. check_plan_table()
  269. print("moe_b12x_next CPU OK")
  270. if __name__ == "__main__":
  271. main()