Files
wehub-resource-sync 59a0a3844c
PR Test AMD / cancel-on-close (push) Has been skipped
PR Test NVIDIA ARM / scan (push) Has been skipped
PR Test NVIDIA / cancel-on-close (push) Has been skipped
PR Test AMD / scan (push) Has been skipped
PR Test NVIDIA ARM / cancel-on-close (push) Has been skipped
PR Test NVIDIA / scan (push) Has been skipped
Release Docker Images / build (cu129-torch-2.11.0) (push) Has been skipped
Release Docker Images / build (cu130-torch-2.11.0) (push) Has been skipped
Release PyPI / publish (push) Has been skipped
Scheduler Python Test / test (push) Successful in 27m19s
Docs / build (push) Successful in 28m8s
Scheduler C++ Test / test (push) Successful in 28m19s
Scheduler C++ Test / test-flat (push) Successful in 28m18s
Docs / deploy (push) Has been cancelled
PR Test AMD / finish (push) Has been cancelled
PR Test NVIDIA / finish (push) Has been cancelled
PR Test NVIDIA ARM / finish (push) Has been cancelled
PR Test NVIDIA ARM / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test AMD / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test NVIDIA / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:31 +08:00

243 lines
8.7 KiB
Python

# Copyright (c) 2026 LightSeek Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""Decode-context-parallel (DCP) support for the MLA decode kernel.
Under DCP each rank holds a strided ``1/cp_world`` slice of the KV context
(local key ``c`` is global position ``c*cp_world + rank``). The kernel gained
``return_lse`` + ``causal_seqs`` + ``cp_world`` so a caller can run the kernel on
each rank's slice and merge the partials into the full-context result.
This test asserts that property end to end: splitting the context into strided
slices, running the kernel per slice (``return_lse=True``), and merging the
partials via their LSE weights reproduces the ``cp_world=1`` full-context output.
A wrong strided mask makes a slice's partial wrong; a wrong LSE makes the merge
weights wrong -- either breaks the reconstruction. Keys are skewed by parity so
the merge weight is far from 0.5, which makes the (log2) LSE scaling load-bearing.
"""
from __future__ import annotations
import pytest
import torch
_HAS_SM100 = torch.cuda.is_available() and torch.cuda.get_device_capability()[0] == 10
pytestmark = pytest.mark.skipif(
not _HAS_SM100, reason="TokenSpeed MLA decode kernel requires Blackwell SM100"
)
KV_LORA, QK_ROPE = 512, 64
D = KV_LORA + QK_ROPE
H, Q = 128, 4
PAGE = 64
def _workspace():
from tokenspeed_mla import get_num_sm
n = get_num_sm(torch.device("cuda")) * H * 8 * (KV_LORA + 1) * 4
return torch.empty(n, dtype=torch.int8, device="cuda")
def _paged(key_rows, dtype):
"""Pack [n, D] key rows into a [pages, PAGE, D] cache + [1, pages] block table."""
n = key_rows.shape[0]
pages = (n + PAGE - 1) // PAGE
cache = torch.zeros(pages, PAGE, D, device="cuda", dtype=dtype)
cache.view(-1, D)[:n] = key_rows
bt = torch.arange(pages, device="cuda", dtype=torch.int32).view(1, pages)
return cache, bt
def _decode(
query,
cache,
bt,
n,
ws,
dtype,
*,
causal_seqs=None,
cp_world=1,
cp_rank=0,
lse=False,
):
from tokenspeed_mla import tokenspeed_mla_decode
return tokenspeed_mla_decode(
query=query,
kv_cache=cache,
workspace_buffer=ws,
kv_lora_rank=KV_LORA,
qk_rope_head_dim=QK_ROPE,
block_tables=bt,
seq_lens=torch.tensor([n], device="cuda", dtype=torch.int32),
max_seq_len=n,
softmax_scale=1.0 / (D**0.5),
output_scale=1.0,
return_lse=lse,
causal_seqs=causal_seqs,
cp_world=cp_world,
cp_rank=cp_rank,
)
def test_dcp_strided_slices_merge_to_full_context():
# DCP (strided global-coordinate masking) is implemented on the fp8 kernel.
dtype, tol = torch.float8_e4m3fn, 6e-2
torch.manual_seed(0)
L, W = 128, 2 # global context length, cp_world
ws = _workspace()
# Skew keys by parity (even keys aligned with the query direction, odd keys
# anti-aligned) so the two strided slices carry very different attention mass
# and the LSE-derived merge weight is far from 0.5.
direction = torch.randn(D, device="cuda")
query = (
direction.view(1, 1, 1, D) + 0.2 * torch.randn(1, Q, H, D, device="cuda")
).to(dtype)
keys = 0.2 * torch.randn(L, D, device="cuda")
keys[0::2] += 0.6 * direction
keys[1::2] -= 0.6 * direction
keys = keys.to(dtype)
cache, bt = _paged(keys, dtype)
o_full = _decode(query, cache, bt, L, ws, dtype).float()
parts = []
for r in range(W):
rk = keys[r::W]
c, b = _paged(rk, dtype)
# Clean API: pass the SAME global bound L on every rank; the wrapper folds
# in cp_rank=r. (Equivalent to pre-subtracting and passing L - r.)
o, lse = _decode(
query,
c,
b,
rk.shape[0],
ws,
dtype,
causal_seqs=torch.tensor([L], device="cuda", dtype=torch.int32),
cp_world=W,
cp_rank=r,
lse=True,
)
parts.append((o.float(), lse.float()))
# Kernel stores LSE in log2 units (global_lse = lse_max + log2(sum exp2(...))),
# so the natural-softmax normalizer of a slice is Z = 2**lse. Merge accordingly.
z = torch.stack([torch.exp2(p[1]) for p in parts], 0) # [W, B, Q, H]
w = (z / z.sum(0)).unsqueeze(-1) # [W, B, Q, H, 1]
o_merge = sum(w[i] * parts[i][0] for i in range(W))
rel = (o_merge - o_full).abs().max() / o_full.abs().max().clamp_min(1e-6)
w0 = w[0].flatten()
assert w0.min() < 0.4 or w0.max() > 0.6, (
f"merge weight not skewed (range [{w0.min():.3f},{w0.max():.3f}]); "
"LSE scaling would be untested"
)
assert (
rel < tol
), f"DCP strided merge != full context: rel max|Δ|={rel:.3e} (tol {tol})"
def test_cp_rank_folds_into_causal_seqs():
"""Passing the global bound + cp_rank must be identical to pre-subtracting the
rank (causal_seqs = global - rank, cp_rank=0). Guards the documented contract
that callers pass the SAME global causal_seqs on every rank."""
dtype = torch.float8_e4m3fn
torch.manual_seed(4)
L, W, r = 128, 2, 1
ws = _workspace()
query = torch.randn(1, Q, H, D, device="cuda").to(dtype)
rk = (torch.randn(L, D, device="cuda") * 0.3).to(dtype)[r::W]
c, b = _paged(rk, dtype)
def call(cs, rank):
return _decode(
query,
c,
b,
rk.shape[0],
ws,
dtype,
causal_seqs=torch.tensor([cs], device="cuda", dtype=torch.int32),
cp_world=W,
cp_rank=rank,
lse=True,
)
o_global, lse_global = call(L, r) # clean API: global bound + cp_rank
o_pre, lse_pre = call(L - r, 0) # pre-subtracted bound
torch.testing.assert_close(o_global, o_pre, rtol=0, atol=0)
torch.testing.assert_close(lse_global, lse_pre, rtol=0, atol=0)
def test_return_lse_toggles_output_shape():
"""return_lse=False keeps the bare-tensor API; True returns (output, lse)."""
torch.manual_seed(1)
dtype, L = torch.float8_e4m3fn, 64
ws = _workspace()
query = torch.randn(1, Q, H, D, device="cuda").to(dtype)
cache, bt = _paged(torch.randn(L, D, device="cuda").to(dtype), dtype)
out = _decode(query, cache, bt, L, ws, dtype) # defaults: cp_world=1, no LSE
assert isinstance(out, torch.Tensor)
assert out.shape == (1, Q, H, KV_LORA)
o, lse = _decode(query, cache, bt, L, ws, dtype, lse=True)
assert o.shape == (1, Q, H, KV_LORA)
assert lse.shape == (1, Q, H)
torch.testing.assert_close(o, out, rtol=0, atol=0) # LSE path must not alter output
def test_dcp_rejects_non_fp8_dtype():
"""DCP args on a non-fp8 query must raise a clear error in the wrapper rather
than silently ignore them or fail deep in the kernel. The guard runs before
the kernel is invoked, so this does not depend on the (fp8-only) decode path."""
torch.manual_seed(2)
dtype, L = torch.bfloat16, 64
ws = _workspace()
query = torch.randn(1, Q, H, D, device="cuda", dtype=dtype)
cache, bt = _paged(torch.randn(L, D, device="cuda", dtype=dtype), dtype)
with pytest.raises(ValueError, match="fp8"):
_decode(
query,
cache,
bt,
L,
ws,
dtype,
causal_seqs=torch.tensor([L], device="cuda", dtype=torch.int32),
cp_world=2,
)
def test_dcp_requires_causal_seqs():
"""cp_world>1 without causal_seqs must raise: the kernel divides the bound by
cp_world, so falling back to rank-local seq_lens would mask each rank to
~1/cp_world of its slice and silently produce a wrong partial."""
torch.manual_seed(3)
dtype, L = torch.float8_e4m3fn, 64
ws = _workspace()
query = torch.randn(1, Q, H, D, device="cuda").to(dtype)
cache, bt = _paged(torch.randn(L, D, device="cuda").to(dtype), dtype)
with pytest.raises(ValueError, match="causal_seqs"):
_decode(query, cache, bt, L, ws, dtype, cp_world=2)