| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915 |
- """GPU check for adapter/prefill_sp.py on one GPU (engine image, <= 2 GB):
- docker run --rm --gpus all --network none -v $PWD:/ds41 -w /ds41 -e PYTHONPATH=/ds41/adapter \
- --entrypoint python3 <image> tests/test_prefill_sp_gpu.py [drift] [kernels] [emulate]
- drift the pinned engine sources hash as expected in this image (incl. the tail helpers), a
- drifted one is refused, and SP refuses an hc_prefill_fused that is not SP-aware.
- kernels every per-row kernel stage 1 runs on a shard gives, on rows [r*M/4, (r+1)*M/4), the bits
- of the full-M call when the gates key on the full row count (sp_logical_rows), at
- M = 4096 / 2048 / 1500 / 128: hc combine(+norm) incl. the first-layer norm, hc mix stats
- (hc_fused on and off), hc_post, the fused Engram gate, the Engram wkv MXFP8 GEMM
- (b12x, as production routes it, and FlashInfer cutlass). A shard-keyed control shows
- which of them would change bits without the helper.
- emulate four TP ranks as threads (baton-passing, so one issues GPU work at a time) on a fake
- process group; one full-row layer (the engine's real forward_hc_pre_from_prev and hc
- kernels, a real Engram gate and MXFP8 wkv, stand-in attention/MoE whose wo_b / expert
- outputs are per-rank partials) followed by one bounded-replay tail layer, through
- prefill_sp's loop copy. stage-1 EXACT must equal the unsharded reference bit for bit on
- every rank, stage-1 fast must equal P0 (comm) bit for bit.
- """
- import os
- import sys
- import threading
- import types
- from contextlib import contextmanager, nullcontext
- os.environ.setdefault("DSV41_PREFILL_SP", "1")
- os.environ.setdefault("DSV41_HC_FUSED", "1")
- os.environ.pop("DSV41_HC_PREFILL_FUSED", None)
- import torch # noqa: E402
- import prefill_sp as sp # noqa: E402
- torch.cuda.set_per_process_memory_fraction(min(1.0, 2e9 / torch.cuda.get_device_properties(0).total_memory))
- DEV = "cuda"
- BF = torch.bfloat16
- H, HC, MIX, ITERS = 5120, 4, 24, 20
- W = 4
- def _mods():
- from sglang.kernels.ops.layernorm import mhc
- from sglang.srt.models import deepseek_v4 as dm
- return mhc, dm
- _INSTALLED = {}
- def setup():
- """hc_fused as production has it, b12x MXFP8 routing, prefill_sp's modules + hc wrappers."""
- if _INSTALLED:
- return _INSTALLED
- mhc, dm = _mods()
- # no runtime context in a bare container: the one query mhc_post makes (prefill CP interleave?)
- # answers as production does (no CP)
- mhc.is_dsa_prefill_cp_interleave = lambda: False
- import hc_fused
- hc_fused.install(mhc)
- import mxfp8_b12x
- from sglang.srt.layers.quantization import fp8_utils
- _INSTALLED["mxfp8_cutlass"] = fp8_utils.flashinfer_mxfp8_blockscaled_linear
- mxfp8_b12x.install(fp8_utils)
- sp._load_modules(dm)
- L = dm.DeepseekV4DecoderLayer
- _INSTALLED.update(
- mhc=mhc, dm=dm, hc_fused=hc_fused, fp8_utils=fp8_utils,
- combine=sp._make_hc_combine(L._hc_combine), post=sp._make_hc_post(L.hc_post),
- stats=L._hc_mix_stats, fwd=L.forward_hc_pre_from_prev, stream=L._get_hc_stats_stream)
- return _INSTALLED
- def _norm(seed):
- from sglang.srt.layers.layernorm import RMSNorm
- n = RMSNorm(H, eps=1e-6).to(DEV)
- g = torch.Generator(device=DEV).manual_seed(seed)
- with torch.no_grad():
- n.weight.copy_((1 + 0.1 * torch.randn(H, device=DEV, generator=g)).to(n.weight.dtype))
- n.weight.data = n.weight.data.to(BF)
- return n
- def _hc_params(seed):
- g = torch.Generator(device=DEV).manual_seed(seed)
- fn = torch.randn(MIX, HC * H, device=DEV, generator=g) * 0.0225
- base = torch.randn(MIX, device=DEV, generator=g) * 3
- scale = torch.rand(3, device=DEV, generator=g) * 0.05 + 0.01
- return fn, scale, base
- def _residual(m, seed):
- g = torch.Generator(device=DEV).manual_seed(seed)
- r = torch.randn(m, HC, H, device=DEV, generator=g, dtype=BF) * 0.5
- r[:, :, :64] *= 40
- return r
- class _Fp8Linear:
- """The MXFP8 dense linear the Engram wkv runs (fp8 weight, ue8m0 block scales, swizzled)."""
- def __init__(self, n, k, seed, routed=True):
- from flashinfer import block_scale_interleave
- g = torch.Generator(device=DEV).manual_seed(seed)
- self.w = (torch.randn(n, k, device=DEV, generator=g) * 2).to(torch.float8_e4m3fn)
- su8 = (127 + torch.randint(-6, 1, (n, k // 32), device=DEV, generator=g)).to(torch.uint8)
- self.sf = block_scale_interleave(su8.contiguous()).contiguous()
- self.routed = routed # True: production routing (mxfp8_b12x -> b12x)
- self.calls = []
- def __call__(self, x):
- self.calls.append(x.shape[0])
- s = setup()
- f = s["fp8_utils"].flashinfer_mxfp8_blockscaled_linear if self.routed else s["mxfp8_cutlass"]
- return f(x, self.w, self.sf, backend="cutlass"), None
- class Layer:
- """DeepseekV4DecoderLayer's hc methods (real engine code, SP-wrapped where stage 1 wraps them)
- on a stand-in carrying the attributes they read."""
- hc_mult, hc_sinkhorn_iters, rms_norm_eps, hc_eps = HC, ITERS, 1e-6, 1e-6
- hc_pre_from_prev_sublayer = True
- hc_stats_stream = None
- dsa_enable_prefill_cp = False
- use_fused_mhc_post_pre = False
- config = types.SimpleNamespace(model_type="deepseek_v41", vision_n_layers=0, image_token_id=7)
- def __init__(self, seed, layer_id=0, attn=None, mlp=None, engram=None):
- self.layer_id = layer_id
- self.hc_attn_fn, self.hc_attn_scale, self.hc_attn_base = _hc_params(seed)
- self.hc_ffn_fn, self.hc_ffn_scale, self.hc_ffn_base = _hc_params(seed + 50)
- self.input_layernorm, self.post_attention_layernorm = _norm(seed + 1), _norm(seed + 2)
- self.self_attn, self.mlp, self.engram = attn, mlp, engram
- def _run_moe_ffn_dp_sync(self, hidden_states, forward_batch, *, input_ids, input_ids_global):
- # the TP-only branch of the engine's method (no DP gather / CP / a2a)
- with FORWARD.scoped(mlp_reduce_scatter=False):
- return self.mlp(hidden_states, forward_batch, input_ids=input_ids,
- input_ids_global=input_ids_global, skip_shared_experts=False)
- def _bind_layer_methods():
- s = setup()
- Layer._hc_combine = s["combine"]
- Layer.hc_post = s["post"]
- Layer._hc_mix_stats = s["stats"]
- Layer.forward_hc_pre_from_prev = s["fwd"]
- Layer._get_hc_stats_stream = s["stream"]
- @contextmanager
- def _as_shard(m, r):
- prev = sp._ctx.plan
- sp._ctx.plan = sp._Plan(m, W, r, None, True, False)
- try:
- yield
- finally:
- sp._ctx.plan = prev
- # ============================================================================================
- def test_drift():
- s = setup()
- got = sp.engine_hashes()
- bad = {k: v for k, v in got.items() if sp._EXPECTED[k] != v}
- assert not bad, bad
- sp._check_tail_engine()
- fp8_old, sp.FP8 = sp.FP8, True
- try:
- sp.check_engine() # incl. the stage-2 attention sources
- finally:
- sp.FP8 = fp8_old
- saved = dict(sp._EXPECTED)
- sp._EXPECTED["v4.MQALayer.forward"] = "0000000000000000"
- try:
- sp.check_engine()
- raise AssertionError("drift accepted")
- except RuntimeError as exc:
- assert "drifted" in str(exc)
- finally:
- sp._EXPECTED.clear()
- sp._EXPECTED.update(saved)
- os.environ["DSV41_HC_PREFILL_FUSED"] = "1"
- try:
- import hc_prefill_fused
- aware = getattr(hc_prefill_fused, "SP_LOGICAL_ROWS", False)
- try:
- sp._check_hc_prefill_fused()
- refused = False
- except RuntimeError:
- refused = True
- assert refused != bool(aware), (aware, refused)
- finally:
- os.environ.pop("DSV41_HC_PREFILL_FUSED", None)
- print(f" drift: {len(got)} engine sources + 3 tail helpers match; a drifted hash is refused; "
- f"hc_prefill_fused SP-aware={bool(aware)} -> {'accepted' if aware else 'refused'}", flush=True)
- # ============================================================================================
- def _kernels_at(m, layer, lin_b12x, lin_cut, seed):
- """{kernel: (full, [shard r outputs with the helper], [shard r outputs without])}."""
- from sglang.srt.layers.engram import engram_gate
- s = setup()
- R = _residual(m, seed)
- x_out = torch.randn(m, H, device=DEV, dtype=BF) * 0.3
- kv = torch.randn(m, (HC + 1) * H, device=DEV, dtype=BF) * 0.5
- qw = (1 + 0.1 * torch.randn(HC, H, device=DEV)).to(BF)
- kw = (1 + 0.1 * torch.randn(HC, H, device=DEV)).to(BF)
- emb = torch.randn(m, 6144, device=DEV, dtype=BF) * 0.2
- lay = layer
- def stats(t):
- return lay._hc_mix_stats(t, lay.hc_attn_fn, lay.hc_attn_scale, lay.hc_attn_base, None)
- pre, post, comb = stats(R)
- fns = {
- "combine0 (norm of stream 0)": lambda sl: lay._hc_combine(R[sl], None, lay.input_layernorm),
- "combine+norm": lambda sl: lay._hc_combine(R[sl], pre[sl], lay.input_layernorm),
- "mix stats (hc_fused)": lambda sl: stats(R[sl]),
- "mix stats (stock split-K)": lambda sl: _stock_stats(lambda: stats(R[sl])),
- "hc_post": lambda sl: lay.hc_post(x_out[sl], R[sl], post[sl], comb[sl]),
- "engram gate": lambda sl: engram_gate(R[sl].contiguous(), kv[sl], qw, kw, 1e-6, 1e-6),
- "wkv MXFP8 b12x": lambda sl: lin_b12x(emb[sl])[0],
- "wkv MXFP8 cutlass": lambda sl: lin_cut(emb[sl])[0],
- }
- out = {}
- for name, fn in fns.items():
- full = fn(slice(None))
- ok = raw_ok = True
- for r in range(W):
- lo, hi = sp.shard_range(m, W, r)
- want = _slice(full, lo, hi)
- with _as_shard(m, r):
- ok = ok and _eq(want, fn(slice(lo, hi)))
- raw_ok = raw_ok and _eq(want, fn(slice(lo, hi)))
- out[name] = (ok, raw_ok)
- del full
- return out
- def _stock_stats(f):
- hf = setup()["hc_fused"]
- prev = hf._state["disabled"]
- hf._state["disabled"] = True
- try:
- return f()
- finally:
- hf._state["disabled"] = prev
- def _eq(a, b):
- if isinstance(a, (tuple, list)):
- return all(_eq(u, v) for u, v in zip(a, b))
- return torch.equal(a, b)
- def _slice(a, lo, hi):
- if isinstance(a, (tuple, list)):
- return tuple(_slice(u, lo, hi) for u in a)
- return a[lo:hi]
- def test_kernels():
- _bind_layer_methods()
- lay = Layer(11)
- lin_b12x = _Fp8Linear(25600, 6144, 3) # production routing: b12x
- lin_cut = _Fp8Linear.__new__(_Fp8Linear) # SGLang's own FlashInfer cutlass,
- lin_cut.w, lin_cut.sf, lin_cut.routed, lin_cut.calls = lin_b12x.w, lin_b12x.sf, False, []
- fails = []
- for m in (4096, 2048, 1500, 128):
- res = _kernels_at(m, lay, lin_b12x, lin_cut, seed=m)
- line = []
- for name, (ok, raw_ok) in res.items():
- if not ok:
- fails.append((m, name))
- line.append(f"{name}: {'OK' if ok else 'DIFF'}{'' if raw_ok else ' (shard-keyed: DIFF)'}")
- print(f" kernels M={m} (shard {m // W}): " + "; ".join(line), flush=True)
- del res
- torch.cuda.empty_cache()
- assert not [f for f in fails if not f[1].startswith("wkv")], fails
- if fails:
- print(f" NOTE: wkv shard GEMM not row-invariant at {fails}: the runtime self-check falls back "
- "to the full-M GEMM there (exact either way)", flush=True)
- # ============================================================================================
- # four ranks on one GPU
- # ============================================================================================
- class ThreadGroup:
- """Collectives across rank threads. Baton passing: a thread holds the lock except while it
- waits at a barrier, so exactly one rank issues GPU work at a time (stream order = issue
- order, and no concurrent JIT compiles)."""
- def __init__(self, world, rs_order="ring"):
- self.world_size, self.unique_name, self.device_group = world, "tp", None
- self._bar = threading.Barrier(world)
- self._lock = threading.Lock()
- self._slots = [None] * world
- self._tl = threading.local()
- self.rs_order = rs_order
- @property
- def rank_in_group(self):
- return self._tl.rank
- def _wait(self):
- self._lock.release()
- try:
- self._bar.wait()
- finally:
- self._lock.acquire()
- def _exchange(self, t):
- # no copy: every rank's tensor stays untouched until the second barrier
- r = self.rank_in_group
- self._slots[r] = t
- self._wait()
- vals = list(self._slots)
- self._wait()
- return vals
- def all_gather_into_tensor(self, out, x):
- vals = self._exchange(x.contiguous())
- dst = out.view(self.world_size, -1)
- for i, v in enumerate(vals):
- dst[i].copy_(v.reshape(-1))
- def all_reduce(self, x):
- return rank_order_sum(self._exchange(x))
- def reduce_scatter_tensor(self, out, x):
- vals = self._exchange(x.contiguous())
- r, s = self.rank_in_group, out.shape[0]
- parts = [v[r * s:(r + 1) * s] for v in vals]
- order = list(range(self.world_size)) if self.rs_order == "same" else \
- [(r + 1 + k) % self.world_size for k in range(self.world_size)]
- out.copy_(rank_order_sum([parts[o] for o in order]))
- def run(self, fn):
- out, errs = [None] * self.world_size, []
- def body(r):
- self._tl.rank = r
- self._lock.acquire()
- try:
- out[r] = fn(r)
- except BaseException as exc: # noqa: BLE001
- errs.append(exc)
- self._bar.abort()
- finally:
- self._lock.release()
- ts = [threading.Thread(target=body, args=(r,)) for r in range(self.world_size)]
- for t in ts:
- t.start()
- for t in ts:
- t.join()
- if errs:
- raise errs[0]
- return out
- def rank_order_sum(parts):
- acc = parts[0].clone()
- for p in parts[1:]:
- acc = acc + p
- return acc
- class Serial:
- """The reference 'group': one thread computes every rank's partial and reduces in rank order."""
- world_size = W
- _fwd = threading.local()
- class _Forward:
- @property
- def mlp_reduce_scatter(self):
- return getattr(_fwd, "mrs", False)
- @contextmanager
- def scoped(self, mlp_reduce_scatter=False):
- prev = getattr(_fwd, "mrs", False)
- _fwd.mrs = mlp_reduce_scatter
- try:
- yield
- finally:
- _fwd.mrs = prev
- FORWARD = _Forward()
- class RowParallel(torch.nn.Module):
- def __init__(self, group, w):
- super().__init__()
- self.group, self.w = group, w
- self.reduce_results, self.tp_size = True, W
- def partial(self, r, h):
- return (h @ self.w[r]).to(BF)
- def forward(self, input_, skip_all_reduce=False):
- out = self.partial(self.group.rank_in_group, input_)
- return (out if skip_all_reduce else self.group.all_reduce(out)), None
- class Attn(torch.nn.Module):
- """Row-mixing stand-in (a causal running mean), per-rank head scale, wo_b partial."""
- def __init__(self, group, seed):
- super().__init__()
- g = torch.Generator(device=DEV).manual_seed(seed)
- self.group, self.attn_tp_size = group, W
- self.scale = (1 + 0.1 * torch.randn(W, H, device=DEV, generator=g)).to(BF)
- self.wo_b = RowParallel(group, (torch.randn(W, H, 256, device=DEV, generator=g) / 16).to(BF)
- .transpose(1, 2).contiguous()) # [W, 256, H]: h[:, :256] @ w
- self.rows_seen = []
- def accepts_mxfp8_swizzled_input(self):
- return False
- def maybe_use_decode_attn_tp(self, fb):
- return nullcontext()
- def head(self, r, x):
- h = x[:, :256].float().cumsum(0) / torch.arange(1, x.shape[0] + 1, device=DEV).unsqueeze(1)
- return (h * self.scale[r, :256].float()).to(BF)
- def forward(self, x, positions, forward_batch, x_quant=None):
- assert positions.shape[0] == x.shape[0]
- self.rows_seen.append(x.shape[0])
- if self.group is Serial:
- return rank_order_sum([self.wo_b.partial(r, self.head(r, x)) for r in range(W)])
- o, _ = self.wo_b(self.head(self.group.rank_in_group, x))
- return o
- class MoE(torch.nn.Module):
- def __init__(self, group, seed):
- super().__init__()
- g = torch.Generator(device=DEV).manual_seed(seed)
- self.group, self.tp_size = group, W
- self._shared_expert_tp1 = self._enable_a2a_moe = False
- self.table = (0.5 * torch.randn(16, W, H, device=DEV, generator=g)).to(BF)
- def partial(self, r, x, ids):
- return (x * self.table[ids % 16, r]).to(BF)
- def forward(self, hidden_states, forward_batch=None, gemm_output_zero_allocator=None,
- input_ids=None, input_ids_global=None, skip_shared_experts=False):
- assert input_ids.shape[0] == hidden_states.shape[0]
- if self.group is Serial:
- return rank_order_sum([self.partial(r, hidden_states, input_ids) for r in range(W)])
- out = self.partial(self.group.rank_in_group, hidden_states, input_ids)
- return out if FORWARD.mlp_reduce_scatter else self.group.all_reduce(out)
- class Embed:
- def __init__(self, group, seed, vocab=1009, cols=2):
- g = torch.Generator(device=DEV).manual_seed(seed)
- self.group, self.tp_size, self._shared, self.vocab = group, W, False, vocab
- self.table = (torch.randn(vocab, cols, 256, device=DEV, generator=g) * 0.3).to(BF)
- def owned(self, r, ids):
- lo, hi = self.vocab * r // W, self.vocab * (r + 1) // W
- vals = self.table[ids, torch.arange(ids.shape[1], device=DEV)]
- return vals.masked_fill(~((ids >= lo) & (ids < hi)).unsqueeze(-1), 0)
- def _owned_rows(self, ids):
- return self.owned(self.group.rank_in_group, ids)
- class Engram:
- def __init__(self, group, seed):
- from sglang.srt.layers.engram import engram_gate
- self.group, self.layer_hash_index = group, 0
- self.embed = Embed(group, seed)
- self.wkv = _Fp8Linear((HC + 1) * H, 512, seed + 1)
- g = torch.Generator(device=DEV).manual_seed(seed + 2)
- self.q = (1 + 0.1 * torch.randn(HC, H, device=DEV, generator=g)).to(BF)
- self.k = (1 + 0.1 * torch.randn(HC, H, device=DEV, generator=g)).to(BF)
- self._gate = engram_gate
- def apply_gate(self, x, kv):
- return self._gate(x, kv, self.q, self.k, 1e-6, 1e-6)
- def __call__(self, x, ids, forward_batch=None, cp_all_tokens=False):
- if self.group is Serial:
- emb = rank_order_sum([self.embed.owned(r, ids) for r in range(W)])
- else:
- emb = self.group.all_reduce(self.embed._owned_rows(ids))
- kv, _ = self.wkv(emb.flatten(-2))
- return self.apply_gate(x, kv)
- class Model:
- """Layer 0: Engram + one full-row layer. Layer 1: bounded-replay tail layer."""
- def __init__(self, group, seed):
- self.pp_group = types.SimpleNamespace(world_size=1)
- self.hidden_size, self.hc_mult, self.hc_pre_from_prev_sublayer = H, HC, True
- self.start_layer, self.end_layer, self.late_layer_start = 0, 2, 1
- self.config = Layer.config
- self.layers = [Layer(seed + 10 * i, i, Attn(group, seed + 10 * i + 3),
- MoE(group, seed + 10 * i + 4),
- Engram(group, seed + 99) if i == 0 else None) for i in range(2)]
- self.dspark_layers_to_capture = [0, 1]
- def engram_hasher(self, input_ids, fb):
- return ((input_ids[:, None, None] * torch.tensor([[31, 17]], device=DEV) + 5) % 1009)
- def _check_late_layer_tail_readers(self, fb):
- pass
- def _tail(lens):
- from sglang.srt.layers.attention.deepseek_v4_backend import LateLayerTail
- idx, pos = [], 0
- for n in lens:
- k = min(128, n)
- idx.extend(range(pos + n - k, pos + n))
- pos += n
- t = torch.tensor(idx, dtype=torch.int64, device=DEV)
- ext = [min(128, n) for n in lens]
- return LateLayerTail(token_indices=t, positions=t.clone(),
- extend_seq_lens=torch.tensor(ext, device=DEV), extend_seq_lens_cpu=ext,
- swa_out_cache_loc=t.clone(),
- contiguous_start=(pos - ext[0]) if len(lens) == 1 else None)
- class _Backend:
- def __init__(self, tail):
- self.tail_forward_metadata = types.SimpleNamespace(late_layer_tail=tail)
- def enter_late_layer_tail(self, fb):
- return "saved"
- def exit_late_layer_tail(self, saved, fb):
- assert saved == "saved"
- Attn.forward = sp._make_attn_forward(Attn.forward) # as install() wraps MQALayer.forward
- MoE.forward = sp._make_moe_forward(MoE.forward) # ... and DeepseekV2MoE.forward
- sp._mega_moe = lambda moe, x: False # stand-in MoE is not a mega-MoE
- FB = types.SimpleNamespace(forward_mode=types.SimpleNamespace(
- is_extend_without_speculative=lambda: True, is_decode=lambda: False,
- is_target_verify=lambda: False))
- def _engine_ns(group, backend):
- dm = setup()["dm"]
- ns = types.SimpleNamespace(**{k: getattr(dm, k) for k in dir(dm) if not k.startswith("__")})
- ns.get_attn_backend = lambda: backend
- ns.check_cuda_graph_backend = lambda *a: False
- ns.get_global_expert_distribution_recorder = lambda: types.SimpleNamespace(
- with_current_layer=lambda i: nullcontext())
- ns.get_forward = lambda: FORWARD
- ns.is_in_breakable_cuda_graph = lambda: False
- ns.is_cp_active = lambda fb: False
- ns.get_attn_tp_context = lambda: types.SimpleNamespace(input_scattered=False)
- ns.get_tp_group = lambda: group
- ns.get_parallel = lambda: types.SimpleNamespace(attn_dp_size=1, tp_size=W, attn_tp_size=W)
- ns.get_moe_a2a_backend = lambda: types.SimpleNamespace(is_none=lambda: True)
- ns.MQALayer = Attn
- return ns
- def _reference(model, R0, ids, pos, tail):
- """The engine's stock loop semantics on one 'rank' (all rows, partials reduced in rank order)."""
- hash_ids = model.engram_hasher(ids, FB)
- hs, prev_pre, aux = R0.clone(), None, []
- for i in range(model.end_layer):
- if i == model.late_layer_start:
- hs, prev_pre, ids, pos = tail.rows(hs), tail.rows(prev_pre), tail.rows(ids), tail.positions
- hash_ids = tail.rows(hash_ids)
- lay = model.layers[i]
- if lay.engram is not None:
- hs = lay.engram(hs, hash_ids[:, 0], FB)
- if i in model.dspark_layers_to_capture:
- aux.append((tail.rows(hs) if i < model.late_layer_start else hs).mean(dim=1))
- hs, prev_pre = lay.forward_hc_pre_from_prev(
- positions=pos, hidden_states=hs, input_ids=ids, forward_batch=FB, input_ids_global=ids,
- prev_pre=prev_pre, precomputed_attn=None, next_norm=None, next_input=[])
- return hs, prev_pre, aux
- def _emulate(m, lens, seed=5, with_comm=True):
- _bind_layer_methods()
- tail = _tail(lens)
- g = torch.Generator(device=DEV).manual_seed(seed)
- R0 = torch.randn(m, HC, H, device=DEV, generator=g, dtype=BF) * 0.5
- ids = torch.randint(0, 5000, (m,), device=DEV, generator=g)
- pos = torch.arange(m, device=DEV)
- ref_model = Model(Serial, seed)
- sp._M["v4"] = _engine_ns(None, _Backend(tail))
- ref = _reference(ref_model, R0, ids, pos, tail)
- del ref_model
- torch.cuda.empty_cache()
- def arm(mode, exact, rs_order="ring"):
- group = ThreadGroup(W, rs_order)
- model = Model(group, seed)
- sp._M["v4"] = _engine_ns(group, _Backend(tail))
- sp._M["v2"] = types.SimpleNamespace(DeepseekV2MoE=MoE)
- sp._M["linear"] = types.SimpleNamespace(RowParallelLinear=RowParallel)
- sp._STATIC["checked"] = False
- sp._WKV_OK.clear()
- old = sp.MODE, sp.EXACT
- def body(r):
- aux = []
- p = sp._plan(model, R0, FB)
- assert p is not None and p.rank == r
- # R0 itself (not a per-rank clone: 4 x 168 MB); the loop never writes its input
- hs, pre, _ = sp._sp_forward_layers(model, p, pos, R0, FB, ids, ids, True, aux)
- return hs, pre, aux
- sp.MODE, sp.EXACT = mode, exact
- try:
- out = group.run(body)
- finally:
- sp.MODE, sp.EXACT = old
- wkv = dict(sp._WKV_OK)
- rows_seen = model.layers[0].self_attn.rows_seen
- del model
- torch.cuda.empty_cache()
- return out, wkv, rows_seen
- r0_sum = R0.view(torch.int16).sum().item()
- exact, wkv_e, seen = arm("shard", True)
- fast, wkv_f, _ = arm("shard", False)
- # P0 keeps every row on every rank: 4 ranks of it fit the 2 GB budget only at ~2k rows
- comm = arm("comm", False)[0] if with_comm else None
- assert R0.view(torch.int16).sum().item() == r0_sum, "the loop wrote into its input"
- 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
- ok_exact = [same(exact[r], ref) for r in range(W)]
- ok_fast = [same(fast[r], comm[r]) for r in range(W)] if comm is not None else "n/a (memory)"
- diff = (fast[0][0].float() - ref[0].float()).abs()
- rel = (diff / ref[0].float().abs().clamp_min(1e-3)).max().item()
- print(f" emulate M={m} tails={lens if len(lens) <= 4 else f'{len(lens)} requests'}: "
- f"exact==reference per rank {ok_exact}; fast==P0 per rank {ok_fast}; "
- f"fast vs reference: {int((diff > 0).sum())}/{diff.numel()} elements differ, max rel {rel:.2e}; "
- f"wkv shard ON {wkv_e}; attention saw rows {sorted(set(seen))}", flush=True)
- assert all(ok_exact) and (comm is None or all(ok_fast)), (ok_exact, ok_fast)
- def test_emulate():
- sp._agree_min = lambda p, local: bool(min(int(v) for v in p.group._exchange(
- torch.tensor([1 if local else 0], device=DEV))))
- for m, lens, comm in ((4096, [1000, 50, 2046, 1000], False), (4096, [4096], False),
- (2052, [513, 1, 1025, 513], True), (2048, [2048], True)):
- _emulate(m, lens, with_comm=comm)
- def _debug_arm(m, lens, seed=5):
- """Stage-1 exact with DSV41_PREFILL_SP_DEBUG=compare,fp on the emulated 4 ranks."""
- _bind_layer_methods()
- s = setup()
- Layer._hc_mix_stats = sp._make_hc_mix_stats(s["stats"])
- Layer.forward_hc_pre_from_prev = sp._make_layer_forward(s["fwd"])
- tail = _tail(lens)
- g = torch.Generator(device=DEV).manual_seed(seed)
- R0 = torch.randn(m, HC, H, device=DEV, generator=g, dtype=BF) * 0.5
- ids = torch.randint(0, 5000, (m,), device=DEV, generator=g)
- pos = torch.arange(m, device=DEV)
- group = ThreadGroup(W)
- model = Model(group, seed)
- sp._M["v4"] = _engine_ns(group, _Backend(tail))
- sp._M["v2"] = types.SimpleNamespace(DeepseekV2MoE=MoE)
- sp._M["linear"] = types.SimpleNamespace(RowParallelLinear=RowParallel)
- sp._STATIC["checked"] = False
- sp._WKV_OK.clear()
- old = sp.MODE, sp.EXACT, set(sp.DEBUG)
- sp.DEBUG.clear()
- sp.DEBUG.update({"compare", "fp"})
- def body(r):
- p = sp._plan(model, R0, FB)
- d = sp._Dbg(1, m, p, r)
- sp._ctx.dbg = d
- try:
- out = sp._sp_forward_layers(model, p, pos, R0, FB, ids, ids, True, [])
- finally:
- sp._ctx.dbg = None
- sp._debug_end(d, out)
- return d.checked, list(d.bad)
- sp.MODE, sp.EXACT = "shard", True
- try:
- return group.run(body)
- finally:
- sp.MODE, sp.EXACT = old[0], old[1]
- sp.DEBUG.clear()
- sp.DEBUG.update(old[2])
- _bind_layer_methods()
- del model
- torch.cuda.empty_cache()
- def test_debug():
- """4 ranks x a gathered [M, 4, 5120] reference per compared op: M=2052 fits the 2 GB cap."""
- sp._agree_min = lambda p, local: bool(min(int(v) for v in p.group._exchange(
- torch.tensor([1 if local else 0], device=DEV))))
- m, lens = 2052, [513, 1, 1025, 513]
- res = _debug_arm(m, lens)
- 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
- print(f" debug compare, clean: {[c for c, _ in res]} ops checked per rank, 0 mismatches", flush=True)
- # fault: the shard takes the combine branch with a combine kernel that is off by one ulp
- good_gate, good_mhc = sp._combine_norm_fused, sp._M["mhc"]
- real = good_mhc.hc_combine
- sp._combine_norm_fused = lambda layer, x, pre, norm, rows: rows == x.shape[0]
- sp._M["mhc"] = types.SimpleNamespace(hc_combine=lambda x, pre, hc, dt: (
- lambda y: y.view(torch.int16).add_(1).view(dt))(real(x, pre, hc, dt)))
- try:
- res = _debug_arm(m, lens)
- finally:
- sp._combine_norm_fused, sp._M["mhc"] = good_gate, good_mhc
- ops = sorted({b[1] for _, bad in res for b in bad})
- assert ops and all(o.startswith("hc_combine") for o in ops) and all(bad for _, bad in res), ops
- print(f" debug compare, injected 1-ulp combine on the shard: flagged {ops} on every rank "
- f"(first at layer {res[0][1][0][0]})", flush=True)
- # ============================================================================================
- # stage 2: MXFP8 attention-input gathers
- # ============================================================================================
- def test_fp8_quant():
- """Shard-wise MXFP8 (linear scales) + gather + 128x4 swizzle == the stock full-M quantization,
- and the MXFP8 linear on the pre-quantized tuple == on the bf16 rows (b12x and cutlass)."""
- s = setup()
- lin = _Fp8Linear(1792, H, 21)
- for m in (4096, 3152, 2052, 2048, 1500, 128):
- x = torch.randn(m, H, device=DEV, dtype=BF) * 0.5
- x[:, :64] *= 40
- rq, rs = sp._mxfp8_quantize(x, True)
- lq, ls = sp._mxfp8_quantize(x, False)
- parts_q, parts_s = [], []
- for r in range(W):
- lo, hi = sp.shard_range(m, W, r)
- q, sf = sp._mxfp8_quantize(x[lo:hi].contiguous(), False)
- parts_q.append(q.view(torch.uint8))
- parts_s.append(sf.view(torch.uint8).reshape(-1)[: (hi - lo) * (H // 32)].view(hi - lo, H // 32))
- qg, sg = torch.cat(parts_q), torch.cat(parts_s)
- sw = sp.swizzle_128x4(sg)
- data_ok = torch.equal(qg, rq.view(torch.uint8))
- lin_ok = torch.equal(sg, ls.view(torch.uint8).reshape(-1)[: m * (H // 32)].view(m, H // 32))
- sw_ok = torch.equal(sw, rs.view(torch.uint8).reshape(-1))
- xq = sp._mxfp8_input_cls()(qg.view(rq.dtype), sw.view(rs.dtype).view(rs.shape))
- f_b12x = s["fp8_utils"].flashinfer_mxfp8_blockscaled_linear
- f_cut = s["mxfp8_cutlass"]
- cons = {}
- for name, f in (("b12x", f_b12x), ("cutlass", f_cut)):
- ref = f(x, lin.w, lin.sf, backend="cutlass")
- got = f(xq[0], lin.w, lin.sf, input_scale=xq[1], backend="cutlass")
- cons[name] = torch.equal(ref, got)
- print(f" fp8 quant M={m} (shard {m // W}): data {data_ok}, linear scales {lin_ok}, swizzled "
- f"scales {sw_ok} (stock shapes q {tuple(rq.shape)} {rq.dtype}, sf {tuple(rs.shape)} {rs.dtype}, "
- f"linear sf {tuple(ls.shape)}); linear on pre-quantized == bf16: {cons}", flush=True)
- assert data_ok and sw_ok and all(cons.values()), (m, data_ok, lin_ok, sw_ok, cons)
- class Attn8(Attn):
- """Attention stand-in that consumes its input the way MQALayer's prefill does on a layer without
- compressor / indexer: wqkv_a (MXFP8 linear) on x_quant if given, else on the bf16 x."""
- def __init__(self, group, seed, source=False):
- super().__init__(group, seed)
- self.compressor = object() if source else None # a source layer reads bf16 x
- self.indexer = None
- self.fuse_wqa_wkv = True
- lin = _Fp8Linear(1792, H, seed + 7)
- def wqkv_a(xin):
- f = setup()["fp8_utils"].flashinfer_mxfp8_blockscaled_linear
- if isinstance(xin, tuple):
- return f(xin[0], lin.w, lin.sf, input_scale=xin[1], backend="cutlass"), None
- return f(xin, lin.w, lin.sf, backend="cutlass"), None
- self.wqkv_a = wqkv_a
- self.fp8_inputs = 0
- def accepts_mxfp8_swizzled_input(self):
- return True
- def head(self, r, x):
- return super().head(r, x)
- def forward(self, x, positions, forward_batch, x_quant=None):
- assert positions.shape[0] == x.shape[0]
- self.rows_seen.append(x.shape[0])
- if x_quant is not None:
- self.fp8_inputs += 1
- if self.compressor is not None: # reads the bf16 rows themselves
- assert x_quant is None and not torch.isnan(x[:1]).any()
- qkv = self.wqkv_a(x_quant if x_quant is not None else x)[0]
- h = qkv[:, :256].contiguous()
- if self.group is Serial:
- return rank_order_sum([self.wo_b.partial(r, self.head(r, h)) for r in range(W)])
- o, _ = self.wo_b(self.head(self.group.rank_in_group, h))
- return o
- Attn8.forward = sp._make_attn_forward(Attn8.forward)
- class Model8(Model):
- """Layer 0: Engram + fp8-eligible attention. Layer 1: a 'source' layer (bf16). Layer 2: tail."""
- def __init__(self, group, seed):
- self.pp_group = types.SimpleNamespace(world_size=1)
- self.hidden_size, self.hc_mult, self.hc_pre_from_prev_sublayer = H, HC, True
- self.start_layer, self.end_layer, self.late_layer_start = 0, 3, 2
- self.config = Layer.config
- self.layers = [Layer(seed + 10 * i, i, Attn8(group, seed + 10 * i + 3, source=(i == 1)),
- MoE(group, seed + 10 * i + 4),
- Engram(group, seed + 99) if i == 0 else None) for i in range(3)]
- self.dspark_layers_to_capture = [0]
- def _arm8(m, lens, *, fp8, exact, debug=False, seed=9):
- _bind_layer_methods()
- s = setup()
- if debug:
- Layer._hc_mix_stats = sp._make_hc_mix_stats(s["stats"])
- Layer.forward_hc_pre_from_prev = sp._make_layer_forward(s["fwd"])
- tail = _tail(lens)
- g = torch.Generator(device=DEV).manual_seed(seed)
- R0 = torch.randn(m, HC, H, device=DEV, generator=g, dtype=BF) * 0.5
- ids = torch.randint(0, 5000, (m,), device=DEV, generator=g)
- pos = torch.arange(m, device=DEV)
- group = ThreadGroup(W)
- model = Model8(group, seed)
- sp._M["v4"] = _engine_ns(group, _Backend(tail))
- sp._M["v4"].MQALayer = Attn8
- sp._M["v2"] = types.SimpleNamespace(DeepseekV2MoE=MoE)
- sp._M["linear"] = types.SimpleNamespace(RowParallelLinear=RowParallel)
- sp._STATIC["checked"] = False
- sp._WKV_OK.clear()
- sp._FP8_M.clear()
- old = sp.MODE, sp.EXACT, sp.FP8, set(sp.DEBUG)
- sp.DEBUG.clear()
- if debug:
- sp.DEBUG.update({"compare", "fp"})
- def body(r):
- p = sp._plan(model, R0, FB)
- d = sp._Dbg(1, m, p, r) if debug else None
- sp._ctx.dbg = d
- aux = []
- try:
- hs, pre, _ = sp._sp_forward_layers(model, p, pos, R0, FB, ids, ids, True, aux)
- finally:
- sp._ctx.dbg = None
- if d is not None:
- sp._debug_end(d, (hs, pre))
- return hs, pre, aux, (d.checked, list(d.bad)) if d is not None else None
- sp.MODE, sp.EXACT, sp.FP8 = "shard", exact, fp8
- try:
- out = group.run(body)
- finally:
- sp.MODE, sp.EXACT, sp.FP8 = old[0], old[1], old[2]
- sp.DEBUG.clear()
- sp.DEBUG.update(old[3])
- _bind_layer_methods()
- info = ([l.self_attn.fp8_inputs for l in model.layers], dict(sp._FP8_M))
- del model
- torch.cuda.empty_cache()
- return out, info
- def test_fp8_emulate(only_debug=False):
- sp._agree_min = lambda p, local: bool(min(int(v) for v in p.group._exchange(
- torch.tensor([1 if local else 0], device=DEV))))
- 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
- for m, lens in () if only_debug else ((4096, [1000, 50, 2046, 1000]), (3152, [3152]),
- (2052, [513, 1, 1025, 513])):
- for exact in (True, False):
- s1, _ = _arm8(m, lens, fp8=False, exact=exact)
- s2, info = _arm8(m, lens, fp8=True, exact=exact)
- ok = [same(s1[r], s2[r]) for r in range(W)]
- print(f" fp8 emulate M={m} {'exact' if exact else 'fast '}: stage2 == stage1 per rank {ok}; "
- f"x_quant calls per layer (x4 ranks) {info[0]}; check {info[1]}", flush=True)
- assert all(ok), ok
- assert info[0] == [W, 0, 0] and all(v[0] for v in info[1].values()), info
- # debug gathers a full [M, 4, 5120] reference per compared op on 4 ranks: 2052 rows fit 2 GB
- res, info = _arm8(2052, [513, 1, 1025, 513], fp8=True, exact=True, debug=True)
- dbg = [r[3] for r in res]
- assert all(c > 0 and not bad for c, bad in dbg), dbg
- print(f" fp8 debug compare: {[c for c, _ in dbg]} ops checked per rank (incl. attn x mxfp8 "
- f"data/scales and wqkv_a), 0 mismatches", flush=True)
- if __name__ == "__main__":
- which = sys.argv[1:] or ["drift", "kernels", "emulate", "debug", "fp8"]
- print(f"torch {torch.__version__}, {torch.cuda.get_device_name(0)}", flush=True)
- if "drift" in which:
- test_drift()
- if "kernels" in which:
- test_kernels()
- if "emulate" in which:
- test_emulate()
- if "debug" in which:
- test_debug()
- if "fp8debug" in which:
- test_fp8_emulate(only_debug=True)
- if "fp8" in which:
- test_fp8_quant()
- test_fp8_emulate()
- print(f"test_prefill_sp_gpu: ok (peak {torch.cuda.max_memory_allocated() / 2**30:.2f} GiB)")
|