test_prefill_sp.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598
  1. """CPU check for adapter/prefill_sp.py: the row partition, the tail-row gathers, and the whole
  2. sharded layer loop against the engine's stock loop, with four TP ranks emulated by threads over
  3. a fake process group (all-gather = concatenation, all-reduce = rank-order bf16 sum, reduce-scatter
  4. = a different summation order, like NCCL's).
  5. The model is a toy with the engine's structure: residual [M, 4, D], a row-mixing "attention"
  6. whose wo_b all-reduces unless told to skip, a hash-routed "MoE" that honours mlp_reduce_scatter,
  7. an Engram (owned-row lookup + all-reduce, wkv, gate), bounded replay (late layers on the tail
  8. rows), DSpark aux capture and the vision `where`. Its hc combine picks one of two numerically
  9. different reductions by row count (as the engine's >= 4096 fused norm does), keyed on
  10. sp_logical_rows(): a gate keyed on the shard size would change bits, and the test shows it.
  11. Checks, for several chunk sizes (4096, 2052 = odd shards, 2048 with a full-row tail, 2050 = not
  12. divisible, below MIN_ROWS) and tail layouts (one request, requests straddling shard boundaries,
  13. many short requests, no bounded replay):
  14. * stage 1 EXACT == stock, bit for bit, on every rank;
  15. * stage 1 fast == P0 (comm), bit for bit (same reduce-scatters on the same tensors);
  16. * P0 differs from stock only through the summation order (and matches it with an exact RS);
  17. * the wkv self-check falls back to the full-M GEMM when the shard GEMM is not bit-exact.
  18. PYTHONPATH=adapter python tests/test_prefill_sp.py
  19. """
  20. import os
  21. import threading
  22. from contextlib import contextmanager, nullcontext
  23. from types import SimpleNamespace
  24. os.environ.setdefault("DSV41_PREFILL_SP", "1")
  25. import torch # noqa: E402
  26. import prefill_sp as sp # noqa: E402
  27. W = 4
  28. D = 64
  29. BF = torch.bfloat16
  30. # ------------------------------------------------------------------------------------------
  31. # fake process group over threads
  32. # ------------------------------------------------------------------------------------------
  33. class ThreadGroup:
  34. def __init__(self, world, rs_order="ring"):
  35. self.world_size = world
  36. self.unique_name = "tp"
  37. self.device_group = None
  38. self._bar = threading.Barrier(world)
  39. self._slots = [None] * world
  40. self._tl = threading.local()
  41. self.rs_order = rs_order
  42. self.calls = {"ag": 0, "ar": 0, "rs": 0}
  43. @property
  44. def rank_in_group(self):
  45. return self._tl.rank
  46. def bind(self, rank):
  47. self._tl.rank = rank
  48. def _exchange(self, t):
  49. r = self.rank_in_group
  50. self._slots[r] = t.clone()
  51. self._bar.wait()
  52. vals = list(self._slots)
  53. self._bar.wait()
  54. return vals
  55. def all_gather_into_tensor(self, out, x):
  56. vals = self._exchange(x.contiguous())
  57. if self.rank_in_group == 0:
  58. self.calls["ag"] += 1
  59. out.view(-1).copy_(torch.cat([v.reshape(-1) for v in vals]))
  60. def all_reduce(self, x):
  61. vals = self._exchange(x)
  62. if self.rank_in_group == 0:
  63. self.calls["ar"] += 1
  64. acc = vals[0].clone()
  65. for v in vals[1:]:
  66. acc = acc + v # rounds to the dtype at every hop
  67. return acc
  68. def reduce_scatter_tensor(self, out, x):
  69. vals = self._exchange(x.contiguous())
  70. r, s = self.rank_in_group, out.shape[0]
  71. if r == 0:
  72. self.calls["rs"] += 1
  73. parts = [v[r * s:(r + 1) * s] for v in vals]
  74. order = list(range(self.world_size)) if self.rs_order == "same" else \
  75. [(r + 1 + k) % self.world_size for k in range(self.world_size)]
  76. acc = parts[order[0]].clone()
  77. for o in order[1:]:
  78. acc = acc + parts[o]
  79. out.copy_(acc)
  80. def run_ranks(group, fn):
  81. out, errs = [None] * group.world_size, []
  82. def body(r):
  83. group.bind(r)
  84. try:
  85. out[r] = fn(r)
  86. except BaseException as exc: # noqa: BLE001
  87. errs.append((r, exc))
  88. group._bar.abort()
  89. ts = [threading.Thread(target=body, args=(r,)) for r in range(group.world_size)]
  90. for t in ts:
  91. t.start()
  92. for t in ts:
  93. t.join()
  94. if errs:
  95. raise errs[0][1]
  96. return out
  97. # ------------------------------------------------------------------------------------------
  98. # fake engine
  99. # ------------------------------------------------------------------------------------------
  100. _fwd = threading.local()
  101. class _Forward:
  102. @property
  103. def mlp_reduce_scatter(self):
  104. return getattr(_fwd, "mrs", False)
  105. @contextmanager
  106. def scoped(self, mlp_reduce_scatter=False):
  107. prev = getattr(_fwd, "mrs", False)
  108. _fwd.mrs = mlp_reduce_scatter
  109. try:
  110. yield
  111. finally:
  112. _fwd.mrs = prev
  113. FORWARD = _Forward()
  114. GATE = {"logical": True}
  115. def hc_rows(x):
  116. return sp.sp_logical_rows(x) if GATE["logical"] else x.shape[0]
  117. class RowParallel(torch.nn.Module):
  118. def __init__(self, group, weights):
  119. super().__init__()
  120. self.group, self.w = group, weights
  121. self.reduce_results, self.tp_size = True, group.world_size
  122. def forward(self, input_, skip_all_reduce=False):
  123. out = (input_.float() @ self.w[self.group.rank_in_group]).to(BF)
  124. if not skip_all_reduce:
  125. out = self.group.all_reduce(out)
  126. return out, None
  127. class FakeAttn(torch.nn.Module):
  128. def __init__(self, group, g):
  129. super().__init__()
  130. self.group = group
  131. self.scale = torch.randn(group.world_size, D, generator=g)
  132. self.wo_b = RowParallel(group, torch.randn(group.world_size, D, D, generator=g) / D ** 0.5)
  133. self.attn_tp_size = group.world_size
  134. self.seen_rows = []
  135. def forward(self, x, positions, forward_batch, x_quant=None):
  136. assert positions.shape[0] == x.shape[0], "attention needs the positions of every row"
  137. self.seen_rows.append(x.shape[0])
  138. # causal row mixing: attention needs every earlier row
  139. h = x.float().cumsum(0) / torch.arange(1, x.shape[0] + 1).unsqueeze(1)
  140. h = (h * self.scale[self.group.rank_in_group]).to(BF)
  141. o, _ = self.wo_b(h)
  142. return o
  143. def maybe_use_decode_attn_tp(self, fb):
  144. return nullcontext()
  145. class FakeMoE(torch.nn.Module):
  146. def __init__(self, group, g):
  147. super().__init__()
  148. self.group = group
  149. self.table = torch.randn(16, group.world_size, D, generator=g)
  150. self.tp_size = group.world_size
  151. self._shared_expert_tp1 = False
  152. self._enable_a2a_moe = False
  153. def forward(self, hidden_states, forward_batch=None, gemm_output_zero_allocator=None,
  154. input_ids=None, input_ids_global=None, skip_shared_experts=False):
  155. assert input_ids.shape[0] == hidden_states.shape[0], "hash routing needs every row's id"
  156. w = self.table[input_ids % 16, self.group.rank_in_group]
  157. out = (hidden_states.float() * torch.tanh(w)).to(BF)
  158. if not FORWARD.mlp_reduce_scatter:
  159. out = self.group.all_reduce(out)
  160. return out
  161. class FakeEmbed:
  162. def __init__(self, group, g, vocab, cols, dim):
  163. self.group, self.tp_size, self._shared = group, group.world_size, False
  164. self.table = torch.randn(vocab, cols, dim, generator=g).to(BF)
  165. self.vocab = vocab
  166. def _owned_rows(self, ids):
  167. r = self.group.rank_in_group
  168. lo, hi = self.vocab * r // self.tp_size, self.vocab * (r + 1) // self.tp_size
  169. owned = (ids >= lo) & (ids < hi) # [T, cols]
  170. vals = self.table[ids, torch.arange(ids.shape[1])] # [T, cols, dim]
  171. return vals.masked_fill(~owned.unsqueeze(-1), 0)
  172. class FakeEngram:
  173. def __init__(self, group, g, hash_index, m_dependent=False):
  174. self.group, self.layer_hash_index = group, hash_index
  175. self.embed = FakeEmbed(group, g, 97, 3, 8)
  176. self.wkv_w = torch.randn(24, D, generator=g) / 5
  177. self.m_dependent = m_dependent
  178. self.wkv_calls = []
  179. def wkv(self, t):
  180. self.wkv_calls.append(t.shape[0])
  181. out = (t.float() @ self.wkv_w)
  182. if self.m_dependent and t.shape[0] >= 2048: # a GEMM whose tactic changes with M
  183. out = out * (1 + 2 ** -7)
  184. return out.to(BF), None
  185. def apply_gate(self, x, kv):
  186. return (x.float() + torch.sigmoid(kv.float()).unsqueeze(1)).to(x.dtype)
  187. def __call__(self, x, ids, forward_batch=None, cp_all_tokens=False):
  188. emb = self.group.all_reduce(self.embed._owned_rows(ids))
  189. kv, _ = self.wkv(emb.flatten(-2))
  190. return self.apply_gate(x, kv)
  191. class FakeLayer:
  192. def __init__(self, group, g, layer_id, engram=None):
  193. self.layer_id = layer_id
  194. self.self_attn = FakeAttn(group, g)
  195. self.mlp = FakeMoE(group, g)
  196. self.engram = engram
  197. self.fn = torch.randn(2, 4, D, generator=g)
  198. self.norm_w = torch.randn(D, generator=g)
  199. self.dsa_enable_prefill_cp = False
  200. self.use_fused_mhc_post_pre = False
  201. def _hc_combine(self, R, pre):
  202. if pre is None:
  203. return R[:, 0, :].contiguous()
  204. p = pre.float()
  205. if hc_rows(R) >= 4096: # "fused" reduction order
  206. y = ((R[:, 0].float() * p[:, 0:1] + R[:, 1].float() * p[:, 1:2])
  207. + (R[:, 2].float() * p[:, 2:3] + R[:, 3].float() * p[:, 3:4]))
  208. else: # "unfused": sequential, rounded before the norm
  209. y = R[:, 0].float() * p[:, 0:1]
  210. for c in (1, 2, 3):
  211. y = y + R[:, c].float() * p[:, c:c + 1]
  212. y = y.to(BF).float()
  213. y = y * torch.rsqrt(y.square().mean(-1, keepdim=True) + 1e-6) * self.norm_w
  214. return y.to(BF)
  215. def _stats(self, R, which):
  216. mix = (R.float() * self.fn[which]).sum(-1) # [N, 4]
  217. pre = torch.sigmoid(mix)
  218. post = 2 * torch.sigmoid(mix.flip(-1))
  219. comb = torch.softmax(mix.unsqueeze(-1) * mix.unsqueeze(-2) / 8, dim=-1)
  220. return pre, post, comb
  221. def hc_post(self, x, R, post, comb):
  222. out = post.unsqueeze(-1) * x.float().unsqueeze(1) + (comb.unsqueeze(-1) * R.float().unsqueeze(2)).sum(1)
  223. return out.to(BF)
  224. def forward_hc_pre_from_prev(self, positions, hidden_states, input_ids, forward_batch,
  225. input_ids_global, prev_pre, precomputed_attn=None,
  226. next_norm=None, next_input=None):
  227. R = hidden_states
  228. x = self._hc_combine(R, prev_pre)
  229. x = self.self_attn(x=x, positions=positions, forward_batch=forward_batch, x_quant=None)
  230. attn_pre, post, comb = self._stats(R, 0)
  231. R = self.hc_post(x, R, post, comb)
  232. x = self._hc_combine(R, attn_pre)
  233. with FORWARD.scoped(mlp_reduce_scatter=False):
  234. x = self.mlp(x, forward_batch, input_ids=input_ids, input_ids_global=input_ids_global,
  235. skip_shared_experts=False)
  236. ffn_pre, post, comb = self._stats(R, 1)
  237. R = self.hc_post(x, R, post, comb)
  238. return R, ffn_pre
  239. class Tail:
  240. def __init__(self, lens, window=128, pad_rows=0):
  241. starts, idx = [], []
  242. pos = 0
  243. for n in lens:
  244. k = min(window, n)
  245. idx.extend(range(pos + n - k, pos + n))
  246. pos += n
  247. self.token_indices = torch.tensor(idx, dtype=torch.int64)
  248. self.contiguous_start = (pos - min(window, lens[0])) if len(lens) == 1 else None
  249. self.positions = self.token_indices.clone()
  250. self.pad_rows = pad_rows
  251. self.cp_metadata = None
  252. def real_rows(self, t):
  253. return t[self.contiguous_start:] if self.contiguous_start is not None else t[self.token_indices]
  254. def rows(self, t):
  255. r = self.real_rows(t)
  256. if self.pad_rows:
  257. r = torch.cat([r, r.new_zeros((self.pad_rows, *r.shape[1:]))])
  258. return r
  259. class Backend:
  260. def __init__(self, tail):
  261. self.tail_forward_metadata = SimpleNamespace(late_layer_tail=tail)
  262. self.entered = 0
  263. def enter_late_layer_tail(self, fb):
  264. self.entered += 1
  265. return "saved"
  266. def exit_late_layer_tail(self, saved, fb):
  267. assert saved == "saved"
  268. class FakeModel:
  269. def __init__(self, group, seed, n_layers=6, late=4, vision=False, m_dependent_wkv=False):
  270. g = torch.Generator().manual_seed(seed)
  271. self.pp_group = SimpleNamespace(world_size=1)
  272. self.hidden_size, self.hc_mult, self.hc_pre_from_prev_sublayer = D, 4, True
  273. self.start_layer, self.end_layer, self.late_layer_start = 0, n_layers, late
  274. self.config = SimpleNamespace(model_type="deepseek_v41", vision_n_layers=1 if vision else 0,
  275. image_token_id=7)
  276. eng = {1: FakeEngram(group, g, 0, m_dependent_wkv), 3: FakeEngram(group, g, 1, m_dependent_wkv)}
  277. self.layers = [FakeLayer(group, g, i, eng.get(i)) for i in range(n_layers)]
  278. self.hash_a = torch.randint(1, 97, (2, 3), generator=g)
  279. self.dspark_layers_to_capture = [2, 5]
  280. def engram_hasher(self, input_ids, fb):
  281. return (input_ids[:, None, None] * self.hash_a + torch.arange(3)) % 97
  282. def _check_late_layer_tail_readers(self, fb):
  283. pass
  284. def fake_engine(group, backend):
  285. return SimpleNamespace(
  286. get_attn_backend=lambda: backend,
  287. check_cuda_graph_backend=lambda *a: False,
  288. Phase=SimpleNamespace(PREFILL="prefill"),
  289. Backend=SimpleNamespace(TC_PIECEWISE="pcg"),
  290. get_global_expert_distribution_recorder=lambda: SimpleNamespace(
  291. with_current_layer=lambda i: nullcontext()),
  292. get_forward=lambda: FORWARD,
  293. is_in_breakable_cuda_graph=lambda: False,
  294. is_cp_active=lambda fb: False,
  295. get_attn_tp_context=lambda: SimpleNamespace(input_scattered=False),
  296. get_tp_group=lambda: group,
  297. get_parallel=lambda: SimpleNamespace(attn_dp_size=1, tp_size=group.world_size,
  298. attn_tp_size=group.world_size),
  299. get_moe_a2a_backend=lambda: SimpleNamespace(is_none=lambda: True),
  300. get_platform=lambda: SimpleNamespace(is_sm100=False),
  301. envs=SimpleNamespace(SGLANG_ENABLE_DETERMINISTIC_INFERENCE=SimpleNamespace(get=lambda: False)),
  302. MQALayer=FakeAttn,
  303. )
  304. FakeAttn.forward = sp._make_attn_forward(FakeAttn.forward)
  305. FakeMoE.forward = sp._make_moe_forward(FakeMoE.forward)
  306. FakeLayer.forward_hc_pre_from_prev = sp._make_layer_forward(FakeLayer.forward_hc_pre_from_prev)
  307. sp._REQUIRE_CUDA = False
  308. def _agree_min(p, local):
  309. vals = p.group._exchange(torch.tensor([1 if local else 0]))
  310. return bool(min(int(v) for v in vals))
  311. sp._agree_min = _agree_min
  312. # ------------------------------------------------------------------------------------------
  313. # reference: the engine's stock loop, same fake model
  314. # ------------------------------------------------------------------------------------------
  315. def stock_forward_layers(self, positions, hidden_states, forward_batch, input_ids, input_ids_global,
  316. capture_dspark, aux_out):
  317. hash_ids = self.engram_hasher(input_ids, forward_batch)
  318. tail = None
  319. if self.late_layer_start is not None:
  320. backend = sp._M["v4"].get_attn_backend()
  321. tail = backend.tail_forward_metadata.late_layer_tail
  322. prev_pre = None
  323. saved = None
  324. for i in range(self.start_layer, self.end_layer):
  325. if tail is not None and i == self.late_layer_start:
  326. saved = backend.enter_late_layer_tail(forward_batch)
  327. hidden_states, prev_pre, input_ids, input_ids_global = (
  328. tail.rows(hidden_states), tail.rows(prev_pre), tail.rows(input_ids),
  329. tail.rows(input_ids_global))
  330. positions = tail.positions
  331. hash_ids = tail.rows(hash_ids)
  332. engram = self.layers[i].engram
  333. if engram is not None:
  334. before = hidden_states
  335. hidden_states = engram(hidden_states, hash_ids[:, engram.layer_hash_index], forward_batch)
  336. if self.config.vision_n_layers > 0:
  337. hidden_states = torch.where((input_ids == self.config.image_token_id)[:, None, None],
  338. before, hidden_states)
  339. if capture_dspark and i in self.dspark_layers_to_capture:
  340. aux = hidden_states
  341. if tail is not None and i < self.late_layer_start:
  342. aux = tail.rows(aux)
  343. aux_out.append(aux.mean(dim=1))
  344. hidden_states, prev_pre = self.layers[i].forward_hc_pre_from_prev(
  345. positions=positions, hidden_states=hidden_states, input_ids=input_ids,
  346. forward_batch=forward_batch, input_ids_global=input_ids_global, prev_pre=prev_pre)
  347. if saved is not None:
  348. backend.exit_late_layer_tail(saved, forward_batch)
  349. return hidden_states, prev_pre, tail
  350. return hidden_states, prev_pre, None
  351. FB = SimpleNamespace(forward_mode=SimpleNamespace(is_extend_without_speculative=lambda: True))
  352. def run(mode, rows, lens, *, exact=False, late=4, vision=False, rs_order="ring", m_dep=False, seed=1,
  353. debug=False):
  354. """Every rank's (hidden, pre, aux list) for one forward in `mode` (stock | comm | shard)."""
  355. group = ThreadGroup(W, rs_order)
  356. model = FakeModel(group, seed, late=late, vision=vision, m_dependent_wkv=m_dep)
  357. tail = Tail(lens) if late is not None else None
  358. backend = Backend(tail)
  359. sp._M.clear()
  360. sp._M.update(v4=fake_engine(group, backend), v2=SimpleNamespace(DeepseekV2MoE=FakeMoE))
  361. sp._STATIC["checked"] = False
  362. sp._STATIC["tail_checked"] = True # the engine's tail helpers are hash-checked on GPU
  363. sp._WKV_OK.clear()
  364. g = torch.Generator().manual_seed(seed + 100)
  365. R0 = torch.randn(rows, 4, D, generator=g).to(BF)
  366. ids = torch.randint(0, 40, (rows,), generator=g)
  367. pos = torch.arange(rows)
  368. old = sp.MODE, sp.EXACT
  369. def body(r):
  370. aux = []
  371. if mode == "stock":
  372. out = stock_forward_layers(model, pos, R0.clone(), FB, ids, ids, True, aux)
  373. return out[0], out[1], aux, None
  374. p = sp._plan(model, R0, FB)
  375. if p is None:
  376. out = stock_forward_layers(model, pos, R0.clone(), FB, ids, ids, True, aux)
  377. return out[0], out[1], aux, None
  378. d = sp._Dbg(1, rows, p, r) if debug else None
  379. sp._ctx.dbg = d
  380. try:
  381. out = sp._sp_forward_layers(model, p, pos, R0.clone(), FB, ids, ids, True, aux)
  382. finally:
  383. sp._ctx.dbg = None
  384. if d is not None:
  385. sp._debug_end(d, out)
  386. return out[0], out[1], aux, (p, d)
  387. return out[0], out[1], aux, p
  388. sp.MODE, sp.EXACT = ("off" if mode == "stock" else mode), exact
  389. try:
  390. res = run_ranks(group, body)
  391. finally:
  392. sp.MODE, sp.EXACT = old
  393. return res, model, group
  394. def same(a, b):
  395. ha, pa, xa, _ = a
  396. hb, pb, xb, _ = b
  397. return (torch.equal(ha, hb) and torch.equal(pa, pb) and len(xa) == len(xb)
  398. and all(torch.equal(u, v) for u, v in zip(xa, xb)))
  399. def main():
  400. torch.set_num_threads(1)
  401. # --- partition -------------------------------------------------------------------------
  402. for rows in (4096, 2048, 2052, 1500, 128, 65536):
  403. if not sp.eligible_rows(rows, W, 128):
  404. continue
  405. cover = []
  406. for r in range(W):
  407. lo, hi = sp.shard_range(rows, W, r)
  408. cover.extend(range(lo, hi))
  409. assert cover == list(range(rows)), rows
  410. assert not sp.eligible_rows(2050, W, 1024) and not sp.eligible_rows(2044, W, 2048)
  411. assert sp.eligible_rows(2048, W, 2048) and not sp.eligible_rows(4096, 1, 2048)
  412. idx = torch.tensor([0, 511, 512, 1023, 1024, 4095])
  413. o, l = sp.owner_local(idx, 1024)
  414. assert o.tolist() == [0, 0, 0, 0, 1, 3] and l.tolist() == [0, 511, 512, 1023, 0, 1023]
  415. assert sp.tail_plan(4096, 4, 128) == "owners" and sp.tail_plan(2048, 4, 512) == "full"
  416. # --- tail-row gathers against tail.rows(full) ---------------------------------------------
  417. for rows, lens, pad in ((4096, [4096], 0), (4096, [1000, 50, 2046, 1000], 0),
  418. (2052, [513, 1, 1025, 513], 0), (2048, [64] * 32, 0),
  419. (4096, [4000, 96], 3), (8192, [2047, 2049, 4096], 0)):
  420. group = ThreadGroup(W)
  421. tail = Tail(lens, pad_rows=pad)
  422. full = torch.randn(rows, 4, D).to(BF)
  423. pre = torch.rand(rows, 4)
  424. def body(r, rows=rows, tail=tail, full=full, pre=pre, group=group):
  425. p = sp._Plan(rows, W, r, group, True, False)
  426. return sp._tail_rows(p, tail, full[p.lo:p.hi]), sp._tail_rows(p, tail, pre[p.lo:p.hi])
  427. for got_h, got_p in run_ranks(group, body):
  428. assert torch.equal(got_h, tail.rows(full)) and torch.equal(got_p, tail.rows(pre)), (rows, lens)
  429. # --- whole loop ----------------------------------------------------------------------------
  430. MIN = sp.MIN_ROWS
  431. sp.MIN_ROWS = 1024
  432. try:
  433. cases = [
  434. ("one request", 4096, [4096], dict()),
  435. ("straddling requests", 4096, [1000, 50, 2046, 1000], dict()),
  436. ("odd shards + vision", 2052, [513, 1, 1025, 513], dict(vision=True)),
  437. ("tail == all rows", 2048, [64] * 32, dict()),
  438. ("no bounded replay", 4096, [4096], dict(late=None)),
  439. ("not divisible (stock)", 2050, [2050], dict()),
  440. ("below MIN_ROWS (stock)", 1020, [1020], dict()),
  441. ]
  442. for name, rows, lens, kw in cases:
  443. stock, _, _ = run("stock", rows, lens, **kw)
  444. exact, m_ex, g_ex = run("shard", rows, lens, exact=True, **kw)
  445. fast, m_fast, g_fast = run("shard", rows, lens, **kw)
  446. comm, _, g_comm = run("comm", rows, lens, **kw)
  447. comm_same, _, _ = run("comm", rows, lens, rs_order="same", **kw)
  448. planned = exact[0][3] is not None
  449. for r in range(W):
  450. assert same(exact[r], stock[r]), (name, "exact != stock", r)
  451. assert same(fast[r], comm[r]), (name, "fast != P0", r)
  452. assert same(comm_same[r], stock[r]), (name, "P0 with a same-order RS != stock", r)
  453. if planned:
  454. assert not same(fast[0], stock[0]), (name, "the RS order should show")
  455. attn = m_fast.layers[0].self_attn
  456. assert set(attn.seen_rows) == {rows}, (name, attn.seen_rows)
  457. late = kw.get("late", 4)
  458. tail_ars = 0 if late is None else 2 * (len(m_fast.layers) - late) # stock late layers
  459. assert g_fast.calls["ar"] == tail_ars == g_comm.calls["ar"], (name, g_fast.calls)
  460. eng_shard = m_fast.layers[1].engram.wkv_calls
  461. assert rows // W in eng_shard, (name, eng_shard)
  462. else:
  463. assert same(fast[0], stock[0]) and same(comm[0], stock[0]), name
  464. print(f" {name:24s} rows={rows}: exact==stock, fast==P0"
  465. f"{'' if planned else ' (stock path)'}; collectives fast {g_fast.calls}")
  466. # gates keyed on the shard size instead of the chunk: bits change (the test is sensitive)
  467. GATE["logical"] = False
  468. try:
  469. wrong, _, _ = run("shard", 4096, [4096], exact=True)
  470. finally:
  471. GATE["logical"] = True
  472. stock, _, _ = run("stock", 4096, [4096])
  473. assert not same(wrong[0], stock[0]), "a shard-keyed gate should change the result"
  474. # wkv whose result depends on M: the self-check must fall back and stay exact
  475. stock, _, _ = run("stock", 4096, [1000, 3096], m_dep=True)
  476. exact, m_ex, _ = run("shard", 4096, [1000, 3096], exact=True, m_dep=True)
  477. assert all(same(exact[r], stock[r]) for r in range(W)), "wkv fallback not exact"
  478. assert sp._WKV_OK and not any(sp._WKV_OK.values()), sp._WKV_OK
  479. stock, _, _ = run("stock", 4096, [1000, 3096])
  480. exact, _, _ = run("shard", 4096, [1000, 3096], exact=True)
  481. assert all(same(exact[r], stock[r]) for r in range(W))
  482. assert sp._WKV_OK and all(sp._WKV_OK.values()), sp._WKV_OK
  483. finally:
  484. sp.MIN_ROWS = MIN
  485. # debug compare + fingerprints: on (exact and fast), results unchanged, nothing flagged
  486. old_dbg = set(sp.DEBUG)
  487. sp.DEBUG.clear()
  488. sp.DEBUG.update({"compare", "fp"})
  489. try:
  490. stock, _, _ = run("stock", 4096, [1000, 50, 2046, 1000])
  491. for exact_ in (True, False):
  492. dbg, _, _ = run("shard", 4096, [1000, 50, 2046, 1000], exact=exact_, debug=True)
  493. ref, _, _ = run("shard", 4096, [1000, 50, 2046, 1000], exact=exact_)
  494. for r in range(W):
  495. d = dbg[r][3][1]
  496. assert same(dbg[r][:3] + (None,), ref[r][:3] + (None,)), "debug changed the result"
  497. assert d.checked >= 10 and not d.bad, (d.checked, d.bad)
  498. assert {k for _, k, _ in d.fps} == {"Rin", "ain", "aout", "min", "mout", "Rout", "pre"}
  499. assert {layer for layer, _, _ in d.fps} == set(range(6)), d.fps[:3]
  500. if exact_:
  501. assert all(same(dbg[r][:3] + (None,), stock[r]) for r in range(W))
  502. finally:
  503. sp.DEBUG.clear()
  504. sp.DEBUG.update(old_dbg)
  505. print("test_prefill_sp: ok")
  506. if __name__ == "__main__":
  507. main()