| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193 |
- #!/usr/bin/env python3
- """Make the kpool indexer's top-k the same on every run.
- The prefill and decode top-k kernels return the right pools, but in a
- run-dependent order, and when pools tie at the k-th score they keep a
- run-dependent subset of the tied ones. The pools expand to token ids in list
- order and the sparse MLA kernel sums keys in that order, so either difference
- changes the rounding. The next layer's indexer then picks different keys, and
- identical requests past 2048 tokens drifted apart by up to 9.8 nats.
- The kernels are asked for TIE_MARGIN extra candidates. A Triton pass reads the
- candidates' scores, keeps the top k with ties going to the lowest pool id, and
- writes the ids in ascending order. A row whose tie runs past the extra
- candidates is rescanned in full.
- The persistent and cooperative decode kernels only take k in (512, 1024, 2048),
- so with those backends decode rows are only sorted.
- Same contract as the other patchers: each anchor exactly once, or the build
- fails.
- """
- from pathlib import Path
- PATH = Path("/usr/local/lib/python3.12/dist-packages/vllm/models/glm5next/nvidia/sparse_indexer.py")
- HELPER_ANCHOR = "@eager_break_during_capture\ndef sparse_attn_indexer_kpool(\n"
- HELPER = '''from vllm.triton_utils import tl, triton
- # Extra candidates asked of the top-k kernel. Pools tied at the k-th score are
- # almost always among them.
- _TIE_MARGIN = 32
- @triton.jit
- def _order_key(x):
- # float32 -> int64 in [0, 2^32) with the same order as the floats.
- b = x.to(tl.int32, bitcast=True)
- return (b ^ ((b >> 31) & 0x7FFFFFFF)).to(tl.int64) + 2147483648
- @triton.jit
- def _select_topk_rows_kernel(
- cand_ptr, cand_stride, logits_ptr, logits_stride, ks_ptr, ke_ptr,
- out_ptr, out_stride, K, KM,
- HAS_KS: tl.constexpr, BLOCK: tl.constexpr, SCAN: tl.constexpr,
- ):
- row = tl.program_id(0).to(tl.int64)
- if HAS_KS:
- ks = tl.load(ks_ptr + row).to(tl.int64)
- else:
- ks = tl.zeros((), dtype=tl.int64)
- ke = tl.load(ke_ptr + row).to(tl.int64)
- lrow = logits_ptr + row * logits_stride + ks
- orow = out_ptr + row * out_stride
- offs = tl.arange(0, BLOCK)
- pad = 2147483647
- ids = tl.load(cand_ptr + row * cand_stride + offs, mask=offs < KM, other=-1)
- ids = tl.sort(tl.where(ids < 0, pad, ids))
- valid = ids != pad
- n_valid = tl.sum(valid.to(tl.int32))
- u = tl.where(valid, _order_key(tl.load(lrow + ids, mask=valid, other=0.0)), -1)
- # The k-th largest key: the largest t with at least K keys >= t.
- thr = tl.zeros((), dtype=tl.int64)
- for bit in tl.static_range(31, -1, -1):
- t = thr | (1 << bit)
- thr = tl.where(tl.sum((u >= t).to(tl.int32)) >= K, t, thr)
- tied = u == thr
- need = K - tl.sum((u > thr).to(tl.int32))
- if n_valid <= K:
- keep = valid
- else:
- keep = (u > thr) | (tied & (tl.cumsum(tied.to(tl.int32), axis=0) <= need))
- lowest = tl.min(tl.where(valid, u, 1 << 40))
- if (n_valid == KM) & (lowest == thr):
- # The tie may continue past the candidates. Every score above it is
- # a candidate, so walk the row in id order and take everything above
- # it plus the lowest-id tied pools.
- base = 0
- for start in range(0, ke - ks, SCAN):
- c = start + tl.arange(0, SCAN)
- m = c < ke - ks
- uc = _order_key(tl.load(lrow + c, mask=m, other=0.0))
- eq = m & (uc == thr)
- take = m & ((uc > thr) | (eq & (tl.cumsum(eq.to(tl.int32), axis=0) <= need)))
- tl.store(orow + base + tl.cumsum(take.to(tl.int32), axis=0) - 1, c.to(tl.int32), mask=take)
- base += tl.sum(take.to(tl.int32))
- need -= tl.sum(eq.to(tl.int32))
- else:
- n = tl.sum(keep.to(tl.int32))
- tl.store(orow + tl.cumsum(keep.to(tl.int32), axis=0) - 1, ids, mask=keep)
- tl.store(orow + offs, tl.full((BLOCK,), -1, tl.int32), mask=(offs >= n) & (offs < K))
- def _select_topk_rows(cand, logits, ks, ke, out):
- """Write each row's top out.shape[1] ids to out in ascending order, lowest id first among equal scores.
- cand holds an exact top-(k + _TIE_MARGIN) per row as ids relative to ks,
- -1 padded. logits[r, ks[r]:ke[r]] are row r's scores; ks=None means 0.
- """
- if cand.shape[0] == 0:
- return
- _select_topk_rows_kernel[(cand.shape[0],)](
- cand, cand.stride(0), logits, logits.stride(0), ke if ks is None else ks, ke,
- out, out.stride(0), out.shape[1], cand.shape[1],
- HAS_KS=ks is not None, BLOCK=triton.next_power_of_2(cand.shape[1]), SCAN=1024,
- )
- @triton.jit
- def _sort_topk_rows_kernel(ptr, stride, n, BLOCK: tl.constexpr):
- row = tl.program_id(0)
- offs = tl.arange(0, BLOCK)
- pad = 2147483647
- v = tl.load(ptr + row * stride + offs, mask=offs < n, other=pad)
- v = tl.sort(tl.where(v < 0, pad, v))
- tl.store(ptr + row * stride + offs, tl.where(v == pad, -1, v), mask=offs < n)
- def _sort_topk_rows(t: torch.Tensor) -> None:
- """Order each row's ids ascending in place, keeping the -1 padding last."""
- if t.shape[0] == 0:
- return
- _sort_topk_rows_kernel[(t.shape[0],)](
- t, t.stride(0), t.shape[1], BLOCK=triton.next_power_of_2(t.shape[1])
- )
- '''
- PREFILL = """ torch.ops._C.top_k_per_row_prefill(
- logits,
- chunk.cu_seqlen_ks,
- chunk.cu_seqlen_ke,
- topk_dst,
- num_rows,
- logits.stride(0),
- logits.stride(1),
- select_k,
- )
- """
- PREFILL_NEW = """ cand = torch.full(
- (num_rows, select_k + _TIE_MARGIN), -1, dtype=torch.int32, device=logits.device
- )
- torch.ops._C.top_k_per_row_prefill(
- logits,
- chunk.cu_seqlen_ks,
- chunk.cu_seqlen_ke,
- cand,
- num_rows,
- logits.stride(0),
- logits.stride(1),
- select_k + _TIE_MARGIN,
- )
- _select_topk_rows(cand, logits, chunk.cu_seqlen_ks, chunk.cu_seqlen_ke, topk_dst)
- """
- DECODE = """ get_indexer_topk(topk_backend)(
- logits,
- seq_lens,
- next_n,
- topk_dst,
- select_k,
- attn_metadata_narrowed.max_seq_len,
- )
- """
- DECODE_NEW = """ if topk_backend == "per_row":
- # 2-D seq_lens hold each row's own length, as the kernel reads them.
- cand = torch.full(
- (num_rows, select_k + _TIE_MARGIN), -1, dtype=torch.int32, device=logits.device
- )
- get_indexer_topk(topk_backend)(
- logits,
- seq_lens,
- next_n,
- cand,
- select_k + _TIE_MARGIN,
- attn_metadata_narrowed.max_seq_len,
- )
- _select_topk_rows(cand, logits, None, seq_lens.reshape(-1)[:num_rows], topk_dst)
- else:
- """ + "".join(" " + line + "\n" for line in DECODE.splitlines()) + """ _sort_topk_rows(topk_dst)
- """
- text = PATH.read_text()
- if "def _select_topk_rows(" in text:
- print("[deterministic-topk] already applied")
- else:
- for name, anchor in (("helper", HELPER_ANCHOR), ("prefill", PREFILL), ("decode", DECODE)):
- n = text.count(anchor)
- assert n == 1, f"[deterministic-topk] {name} anchor matched {n} times; stock tree changed"
- text = text.replace(HELPER_ANCHOR, HELPER + HELPER_ANCHOR)
- text = text.replace(PREFILL, PREFILL_NEW)
- text = text.replace(DECODE, DECODE_NEW)
- PATH.write_text(text)
- print("[deterministic-topk] kpool top-k: lowest pool id wins ties, rows sorted (prefill and decode)")
|