| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202 |
- """CPU equivalence test: chunked dense indexer (sglang#39187 backport) vs the stock
- function lifted verbatim from this image's deepseek_v4_backend.py, both driven by the
- same deterministic fake kernels. No GPU, no sglang import.
- Run: python3 test_indexer_chunked.py /path/to/backend-ours.py
- """
- import re
- import sys
- import types
- from types import SimpleNamespace
- import torch
- BACKEND_SRC = sys.argv[1] if len(sys.argv) > 1 else "backend-ours.py"
- TOPK = 4
- HEADS = 2
- # ----- deterministic fake kernels (row-invariant, so chunking cannot change values) -----
- def ceil_align(x, a):
- return (x + a - 1) // a * a
- def _as_int_list(x):
- return [int(v) for v in x] if x is not None else None
- def _dense_fp4_mqa_logits(q_fp4, kv_fp4, weights, ks, ke, max_seqlen_k):
- q, _ = q_fp4
- rows = q.shape[0]
- # score depends only on the row's own token id (q[:,0,0]) and the column: row-invariant
- tok = q[:, 0, 0].to(torch.float32)
- j = torch.arange(max_seqlen_k, dtype=torch.float32)
- logits = ((tok[:, None] * 31 + j[None, :] * 7) % 97) + 0.01 * j[None, :]
- # garbage beyond ke - ks, as the real kernel leaves it
- valid = j[None, :] < (ke - ks).to(torch.float32)[:, None]
- return torch.where(valid, logits, torch.full_like(logits, 12345.0))
- def topk_transform_ragged_v2(logits, lens, out_offsets, out_indices):
- rows, width = logits.shape
- k = out_indices.shape[1]
- for r in range(rows):
- n = int(lens[r])
- sc = logits[r, :n]
- if n == 0:
- out_indices[r].fill_(-1)
- continue
- order = torch.argsort(sc, descending=True, stable=True)[:k]
- out = torch.full((k,), -1, dtype=torch.int32)
- out[: order.shape[0]] = (order + int(out_offsets[r])).to(torch.int32)
- out_indices[r] = out
- def _mask_topk_scores(scores, indices, offsets=None):
- columns = indices.to(torch.int64)
- if offsets is not None:
- columns = columns - offsets[:, None]
- selected_scores = scores.gather(1, columns.clamp(0, scores.shape[1] - 1))
- valid = (columns >= 0) & (columns < scores.shape[1]) & (selected_scores > -torch.inf)
- return indices.masked_fill(~valid, -1)
- def select_candidate_blocks(scores, lens, topk_blocks, block_size):
- rows, lc = scores.shape
- nb = (lc + block_size - 1) // block_size
- mask = torch.zeros((rows, lc), dtype=torch.bool)
- for r in range(rows):
- bs = torch.full((nb,), -torch.inf)
- for b in range(nb):
- seg = scores[r, b * block_size : min(lc, (b + 1) * block_size)]
- seg = seg[seg > -torch.inf]
- if seg.numel():
- bs[b] = seg.max()
- keep = torch.argsort(bs, descending=True, stable=True)[:topk_blocks]
- for b in keep.tolist():
- if bs[b] > -torch.inf:
- mask[r, b * block_size : min(lc, (b + 1) * block_size)] = True
- return mask
- def quantize_fp4_indexer_tensor(q, rne=True):
- # q: [T*H, 128] -> ([T*H, 64] int8, [T*H] int32); keep the token id in column 0
- return q[:, :64].to(torch.int8), torch.zeros(q.shape[0], dtype=torch.int32)
- fake_fp4 = types.ModuleType("sglang.kernels.ops.attention.dsv4.fp4_indexer")
- fake_fp4.quantize_fp4_indexer_tensor = quantize_fp4_indexer_tensor
- for name in ("sglang", "sglang.kernels", "sglang.kernels.ops", "sglang.kernels.ops.attention",
- "sglang.kernels.ops.attention.dsv4"):
- sys.modules.setdefault(name, types.ModuleType(name))
- sys.modules["sglang.kernels.ops.attention.dsv4.fp4_indexer"] = fake_fp4
- fake_module = types.ModuleType("deepseek_v4_backend_fake")
- for name, obj in dict(
- _dense_fp4_mqa_logits=_dense_fp4_mqa_logits, topk_transform_ragged_v2=topk_transform_ragged_v2,
- _mask_topk_scores=_mask_topk_scores, select_candidate_blocks=select_candidate_blocks,
- ceil_align=ceil_align, _as_int_list=_as_int_list, torch=torch,
- _TORCH_INDEXER_SCORE_BUDGET_BYTES=1 << 30,
- ).items():
- setattr(fake_module, name, obj)
- # ----- lift the stock methods verbatim from the image's source -----
- src = open(BACKEND_SRC).read()
- def lift(name):
- m = re.search(r"\n def " + name + r"\(.*?(?=\n def |\Z)", src, re.S)
- assert m, name
- body = "\n".join(line[4:] if line.startswith(" ") else line for line in m.group(0).splitlines())
- ns = dict(vars(fake_module))
- exec(body, ns)
- return ns[name]
- stock_dense = lift("_low_ratio_index_topk_dense")
- stock_publish = lift("_publish_or_consume_candidates")
- sys.path.insert(0, ".")
- import indexer_chunked # noqa: E402
- class StockBackend:
- _low_ratio_index_topk_dense = stock_dense
- _publish_or_consume_candidates = stock_publish
- # ----- scenario builder -----
- def build(seq_lens, q_lens, ratio, source, consume_masks=None, tail_lens=None, topk=TOPK):
- T = sum(q_lens)
- max_slots = max(seq_lens) + 8
- req_to_token = torch.stack([torch.arange(max_slots) + 1000 * r for r in range(len(seq_lens))])
- class Pool:
- def get_low_ratio_index_k_fp4(self, layer_id, k_slots):
- return k_slots.to(torch.int8), torch.zeros(k_slots.shape[0], dtype=torch.int32)
- core = SimpleNamespace(
- _page=torch.full((T + 3, topk), -7, dtype=torch.int32),
- _raw=torch.full((T + 3, topk), -7, dtype=torch.int32),
- )
- core.sparse_page_indices = lambda r: core._page
- core.sparse_raw_indices = lambda r: core._raw
- indexer = SimpleNamespace(
- is_candidate_source=source,
- uses_candidates=source or consume_masks is not None,
- index_topk=topk, candidate_topk_blocks=2, candidate_block_size=3,
- queries=lambda q_lora, freqs: q_lora,
- head_weights=lambda x: torch.ones(x.shape[0], HEADS),
- )
- layer = SimpleNamespace(compress_ratio=ratio, indexer=indexer, freqs_cis=torch.zeros(4096, 4), layer_id=1)
- pos = torch.cat([torch.arange(s - q, s) for s, q in zip(seq_lens, q_lens)])
- q_lora = torch.zeros(T, HEADS, 128)
- q_lora[:, :, 0] = torch.arange(T)[:, None].to(q_lora.dtype) # token id in column 0
- x = torch.zeros(T, 8)
- fb = SimpleNamespace(seq_lens_cpu=torch.tensor(seq_lens), req_pool_indices=torch.arange(len(seq_lens)))
- tail = None
- if tail_lens is not None:
- tail = SimpleNamespace(late_layer_tail=SimpleNamespace(cp_metadata=None, extend_seq_lens_cpu=list(tail_lens)))
- self = SimpleNamespace(
- token_to_kv_pool=Pool(), forward_metadata=SimpleNamespace(core_metadata=core),
- req_to_token=req_to_token, candidate_masks=consume_masks, tail_forward_metadata=tail,
- )
- return self, layer, x, q_lora, pos, fb, torch.tensor(q_lens), list(q_lens)
- def run(fn, budget, **kw):
- self, layer, x, q_lora, pos, fb, q_lens, q_lens_cpu = build(**kw)
- if fn is None:
- self._publish_or_consume_candidates = types.MethodType(stock_publish, self)
- StockBackend._low_ratio_index_topk_dense(self, layer, x, q_lora, pos, fb, q_lens, q_lens_cpu)
- else:
- f = indexer_chunked.make_index_topk_dense(fake_module, quantize_fp4_indexer_tensor, budget)
- f(self, layer, x, q_lora, pos, fb, q_lens, q_lens_cpu)
- return self.forward_metadata.core_metadata._page.clone(), self.forward_metadata.core_metadata._raw.clone(), self.candidate_masks
- def check(name, budgets, **kw):
- ref_page, ref_raw, ref_masks = run(None, None, **kw)
- tail_lens = kw.get("tail_lens")
- for budget in budgets:
- page, raw, masks = run(True, budget, **kw)
- assert torch.equal(page, ref_page), f"{name} budget={budget}: page_indices differ"
- assert torch.equal(raw, ref_raw), f"{name} budget={budget}: raw_indices differ"
- if kw["source"]:
- assert len(masks) == len(ref_masks)
- for b, (m, rm) in enumerate(zip(masks, ref_masks)):
- if tail_lens is not None and rm.numel():
- rm = rm[rm.shape[0] - tail_lens[b]:] # what enter_late_layer_tail keeps
- assert m.shape == rm.shape and torch.equal(m, rm), f"{name} budget={budget}: mask {b} differs {tuple(m.shape)} vs {tuple(rm.shape)}"
- print(f"[OK] {name} (budgets {budgets})")
- small = [16, 64, 160, 1 << 31] # 16 B => 1 row per chunk at lc<=4 ... up to one chunk
- check("plain, single request", small, seq_lens=[40], q_lens=[12], ratio=2, source=False)
- check("plain, three requests ragged", small, seq_lens=[40, 9, 61], q_lens=[12, 9, 5], ratio=1, source=False)
- check("plain, request with lc=0 and t_len=0", small, seq_lens=[1, 30, 20], q_lens=[1, 0, 6], ratio=2, source=False)
- check("publish full masks (no tail metadata)", small, seq_lens=[40, 25], q_lens=[10, 7], ratio=1, source=True)
- check("publish tail-only masks", small, seq_lens=[40, 25, 33], q_lens=[10, 7, 9], ratio=1, source=True, tail_lens=[3, 7, 1])
- masks = [torch.rand(10, 40) > 0.4, torch.rand(7, 25) > 0.4]
- check("consume candidate masks", small, seq_lens=[40, 25], q_lens=[10, 7], ratio=1, source=False, consume_masks=masks)
- print("all passed")
|