[kernel] Share the warp vectorized copy and enforce its alignment (#36176)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: BBuf <1182563586@qq.com>
This commit is contained in:
co-authored by
Claude Opus 5
BBuf
parent
9784d5f979
commit
81363bf8cb
@@ -15,19 +15,15 @@ Note: Uses do_bench instead of do_bench_cudagraph since CUDA graph
|
||||
capture doesn't support CPU-GPU memory transfers.
|
||||
"""
|
||||
|
||||
import itertools
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from typing import Tuple
|
||||
|
||||
import torch
|
||||
import triton
|
||||
import triton.testing
|
||||
from sgl_kernel import transfer_kv_all_layer, transfer_kv_per_layer
|
||||
|
||||
from sglang.kernels.jit.benchmark.utils import DEFAULT_QUANTILES, get_benchmark_range
|
||||
from sglang.kernels.jit.benchmark import marker
|
||||
from sglang.kernels.jit.benchmark.utils import get_benchmark_range
|
||||
from sglang.kernels.ops.kvcache.hicache import (
|
||||
can_use_hicache_jit_kernel,
|
||||
transfer_hicache_all_layer,
|
||||
transfer_hicache_one_layer,
|
||||
)
|
||||
@@ -39,7 +35,7 @@ register_cuda_ci(
|
||||
register_amd_ci(est_time=29, stage="jit-kernel-benchmark", runner_config="amd")
|
||||
|
||||
DISABLE_TORCH = os.environ.get("DISABLE_TORCH", "0") == "1"
|
||||
PAGE_SIZE = 1
|
||||
PAGE_SIZE = int(os.environ.get("PAGE_SIZE", "1"))
|
||||
ENABLE_SORT = True
|
||||
GPU_CACHE_SIZE = 256 * 1024 # 256K tokens on GPU
|
||||
HOST_CACHE_SIZE = 512 * 1024 # 512K tokens on CPU
|
||||
@@ -187,20 +183,14 @@ def pytorch_transfer(
|
||||
|
||||
# Benchmark configuration
|
||||
|
||||
BS_RANGE = get_benchmark_range(
|
||||
full_range=[2**n for n in range(0, 16)],
|
||||
ci_range=[16],
|
||||
)
|
||||
ELEMENT_SIZE_RANGE = get_benchmark_range(
|
||||
full_range=[64, 128, 256, 512, 1024],
|
||||
ci_range=[1024],
|
||||
)
|
||||
|
||||
LINE_VALS = ["aot", "jit", "torch"]
|
||||
LINE_NAMES = ["SGL AOT Kernel", "SGL JIT Kernel", "PyTorch"]
|
||||
STYLES = [("orange", "-"), ("blue", "--"), ("red", ":")]
|
||||
|
||||
CONFIGS = list(itertools.product(ELEMENT_SIZE_RANGE, BS_RANGE))
|
||||
if DISABLE_TORCH:
|
||||
LINE_VALS.remove("torch")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -208,22 +198,10 @@ CONFIGS = list(itertools.product(ELEMENT_SIZE_RANGE, BS_RANGE))
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@triton.testing.perf_report(
|
||||
triton.testing.Benchmark(
|
||||
x_names=["element_size", "batch_size"],
|
||||
x_vals=CONFIGS,
|
||||
line_arg="provider",
|
||||
line_vals=LINE_VALS,
|
||||
line_names=LINE_NAMES,
|
||||
styles=STYLES,
|
||||
ylabel="us",
|
||||
plot_name="hicache-one-layer-h2d",
|
||||
args={},
|
||||
)
|
||||
)
|
||||
def benchmark_one_layer_h2d(
|
||||
element_size: int, batch_size: int, provider: str
|
||||
) -> Tuple[float, float, float]:
|
||||
@marker.parametrize("element_size", ELEMENT_SIZE_RANGE)
|
||||
@marker.parametrize("batch_size", marker.range(14, pattern="pow2"), [16])
|
||||
@marker.benchmark("provider", LINE_VALS, unit="ms")
|
||||
def benchmark_one_layer_h2d(element_size: int, batch_size: int, provider: str):
|
||||
"""One Layer: Host (CPU) -> Device (GPU)."""
|
||||
global cache
|
||||
cache_local = cache.get_slice(num_layers=NUM_LAYERS, element_size=element_size)
|
||||
@@ -281,19 +259,10 @@ def benchmark_one_layer_h2d(
|
||||
],
|
||||
}
|
||||
|
||||
if provider == "jit" and not can_use_hicache_jit_kernel(element_size=element_bytes):
|
||||
return (float("nan"), float("nan"), float("nan"))
|
||||
|
||||
if DISABLE_TORCH and provider in ["torch"]:
|
||||
return (float("nan"), float("nan"), float("nan"))
|
||||
|
||||
ms, min_ms, max_ms = triton.testing.do_bench( # type: ignore
|
||||
FN_MAP[provider], quantiles=DEFAULT_QUANTILES, warmup=5, rep=25
|
||||
)
|
||||
return (
|
||||
1000 * ms / NUM_LAYERS,
|
||||
1000 * max_ms / NUM_LAYERS,
|
||||
1000 * min_ms / NUM_LAYERS,
|
||||
return marker.do_bench(
|
||||
FN_MAP[provider],
|
||||
use_cuda_graph=False,
|
||||
extra_memory_footprint=NUM_LAYERS * batch_size * (2 * element_bytes),
|
||||
)
|
||||
|
||||
|
||||
@@ -311,22 +280,10 @@ def _create_ptr_tensor(tensors, device="cuda"):
|
||||
)
|
||||
|
||||
|
||||
@triton.testing.perf_report(
|
||||
triton.testing.Benchmark(
|
||||
x_names=["element_size", "batch_size"],
|
||||
x_vals=CONFIGS,
|
||||
line_arg="provider",
|
||||
line_vals=LINE_VALS,
|
||||
line_names=LINE_NAMES,
|
||||
styles=STYLES,
|
||||
ylabel="us",
|
||||
plot_name="hicache-all-layer-d2h",
|
||||
args={},
|
||||
)
|
||||
)
|
||||
def benchmark_all_layer_d2h(
|
||||
element_size: int, batch_size: int, provider: str
|
||||
) -> Tuple[float, float, float]:
|
||||
@marker.parametrize("element_size", ELEMENT_SIZE_RANGE)
|
||||
@marker.parametrize("batch_size", marker.range(14, pattern="pow2"), [16])
|
||||
@marker.benchmark("provider", LINE_VALS, unit="ms")
|
||||
def benchmark_all_layer_d2h(element_size: int, batch_size: int, provider: str):
|
||||
"""All Layer: Device (GPU) -> Host (CPU)."""
|
||||
global cache
|
||||
cache_local = cache.get_slice(num_layers=NUM_LAYERS, element_size=element_size)
|
||||
@@ -385,19 +342,10 @@ def benchmark_all_layer_d2h(
|
||||
],
|
||||
}
|
||||
|
||||
if provider == "jit" and not can_use_hicache_jit_kernel(element_size=element_bytes):
|
||||
return (float("nan"), float("nan"), float("nan"))
|
||||
|
||||
if DISABLE_TORCH and provider in ["torch"]:
|
||||
return (float("nan"), float("nan"), float("nan"))
|
||||
|
||||
ms, min_ms, max_ms = triton.testing.do_bench( # type: ignore
|
||||
FN_MAP[provider], quantiles=DEFAULT_QUANTILES, warmup=5, rep=25
|
||||
)
|
||||
return (
|
||||
1000 * ms / NUM_LAYERS,
|
||||
1000 * max_ms / NUM_LAYERS,
|
||||
1000 * min_ms / NUM_LAYERS,
|
||||
return marker.do_bench(
|
||||
FN_MAP[provider],
|
||||
use_cuda_graph=False,
|
||||
extra_memory_footprint=NUM_LAYERS * batch_size * (2 * element_bytes),
|
||||
)
|
||||
|
||||
|
||||
@@ -413,12 +361,5 @@ if __name__ == "__main__":
|
||||
v_cache_host=torch.empty(HOST_SHAPE, dtype=torch.bfloat16, pin_memory=True),
|
||||
)
|
||||
|
||||
print("=" * 60)
|
||||
print("One Layer: Host -> Device (CPU -> GPU)")
|
||||
print("=" * 60)
|
||||
benchmark_one_layer_h2d.run(print_data=True)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("All Layer: Device -> Host (GPU -> CPU) [per-layer avg]")
|
||||
print("=" * 60)
|
||||
benchmark_all_layer_d2h.run(print_data=True)
|
||||
benchmark_one_layer_h2d.run(print_prefix="Per Layer: Host -> Device (CPU -> GPU)")
|
||||
benchmark_all_layer_d2h.run(print_prefix="All Layer: Device -> Host (GPU -> CPU)")
|
||||
|
||||
@@ -7,18 +7,13 @@ Compares three providers across a batch-size sweep:
|
||||
- ``triton``: the BLOCK-tiled Triton kernel (SM<90 fallback path).
|
||||
"""
|
||||
|
||||
import itertools
|
||||
from typing import Tuple
|
||||
|
||||
import torch
|
||||
import triton
|
||||
import triton.testing
|
||||
|
||||
from sglang.kernels.jit.benchmark import marker
|
||||
from sglang.kernels.jit.benchmark.utils import (
|
||||
DEFAULT_DEVICE,
|
||||
DEFAULT_DTYPE,
|
||||
DEFAULT_QUANTILES,
|
||||
get_benchmark_range,
|
||||
)
|
||||
from sglang.kernels.jit.utils import is_arch_support_pdl
|
||||
from sglang.kernels.ops.kvcache.set_mla_kv_buffer import set_mla_kv_buffer as jit_set
|
||||
@@ -50,62 +45,38 @@ def _triton_baseline(kv_buffer, loc, cache_k_nope, cache_k_rope):
|
||||
cache_k_rope.stride(0),
|
||||
nope_dim,
|
||||
rope_dim,
|
||||
BLOCK=BLOCK,
|
||||
DCP_RANK=0,
|
||||
DCP_WORLD_SIZE=1,
|
||||
**pdl_kwargs,
|
||||
BLOCK=BLOCK, # type: ignore
|
||||
DCP_RANK=0, # type: ignore
|
||||
DCP_WORLD_SIZE=1, # type: ignore
|
||||
**pdl_kwargs, # type: ignore
|
||||
)
|
||||
|
||||
|
||||
NUM_LAYERS = 8
|
||||
CACHE_SIZE = (2 * 1024 * 1024) // NUM_LAYERS
|
||||
|
||||
# 2M elements
|
||||
CACHE_SIZE = 2 * 1024 * 1024
|
||||
NOPE_DIM = 512
|
||||
ROPE_DIM = 64
|
||||
|
||||
BS_RANGE = get_benchmark_range(
|
||||
full_range=[1, 8, 32, 128, 512, 1024, 2048, 4096, 8192, 16384],
|
||||
ci_range=[1, 128, 2048, 4096, 8192],
|
||||
)
|
||||
|
||||
LINE_VALS = ["wrapper", "jit_tma", "triton"]
|
||||
LINE_NAMES = ["Wrapper (auto)", "JIT TMA bulk-store", "Triton (BLOCK=128 baseline)"]
|
||||
STYLES = [("blue", "-"), ("green", "--"), ("red", "-.")]
|
||||
X_NAMES = ["batch_size"]
|
||||
CONFIGS = list(itertools.product(BS_RANGE))
|
||||
|
||||
|
||||
@triton.testing.perf_report(
|
||||
triton.testing.Benchmark(
|
||||
x_names=X_NAMES,
|
||||
x_vals=CONFIGS,
|
||||
line_arg="provider",
|
||||
line_vals=LINE_VALS,
|
||||
line_names=LINE_NAMES,
|
||||
styles=STYLES,
|
||||
ylabel="us",
|
||||
plot_name="set-mla-kv-buffer-performance",
|
||||
args={},
|
||||
)
|
||||
)
|
||||
def benchmark(batch_size: int, provider: str) -> Tuple[float, float, float]:
|
||||
@marker.parametrize("batch_size", marker.range(15, pattern="pow2"), [1, 128, 8192])
|
||||
@marker.benchmark("provider", ["wrapper", "jit_tma", "triton"])
|
||||
def benchmark(batch_size: int, provider: str):
|
||||
cache_k_nope = torch.randn(
|
||||
(NUM_LAYERS, batch_size, 1, NOPE_DIM),
|
||||
(batch_size, 1, NOPE_DIM),
|
||||
dtype=DEFAULT_DTYPE,
|
||||
device=DEFAULT_DEVICE,
|
||||
)
|
||||
cache_k_rope = torch.randn(
|
||||
(NUM_LAYERS, batch_size, 1, ROPE_DIM),
|
||||
(batch_size, 1, ROPE_DIM),
|
||||
dtype=DEFAULT_DTYPE,
|
||||
device=DEFAULT_DEVICE,
|
||||
)
|
||||
kv_buffer = torch.randn(
|
||||
(NUM_LAYERS, CACHE_SIZE, 1, NOPE_DIM + ROPE_DIM),
|
||||
(CACHE_SIZE, 1, NOPE_DIM + ROPE_DIM),
|
||||
dtype=DEFAULT_DTYPE,
|
||||
device=DEFAULT_DEVICE,
|
||||
)
|
||||
loc = torch.randperm(CACHE_SIZE, device=DEFAULT_DEVICE)[:batch_size]
|
||||
torch.cuda.synchronize()
|
||||
|
||||
FN_MAP = {
|
||||
"wrapper": sglang_wrapper,
|
||||
@@ -113,20 +84,14 @@ def benchmark(batch_size: int, provider: str) -> Tuple[float, float, float]:
|
||||
"triton": _triton_baseline,
|
||||
}
|
||||
|
||||
def fn():
|
||||
impl = FN_MAP[provider]
|
||||
for i in range(NUM_LAYERS):
|
||||
impl(kv_buffer[i], loc, cache_k_nope[i], cache_k_rope[i])
|
||||
|
||||
ms, min_ms, max_ms = triton.testing.do_bench_cudagraph(
|
||||
fn, quantiles=DEFAULT_QUANTILES
|
||||
)
|
||||
return (
|
||||
1000 * ms / NUM_LAYERS,
|
||||
1000 * max_ms / NUM_LAYERS,
|
||||
1000 * min_ms / NUM_LAYERS,
|
||||
return marker.do_bench(
|
||||
FN_MAP[provider],
|
||||
input_args=(kv_buffer, loc, cache_k_nope, cache_k_rope),
|
||||
graph_clone_args=(1, 2, 3),
|
||||
memory_args=(loc, cache_k_nope, cache_k_rope),
|
||||
memory_output=(cache_k_nope, cache_k_rope),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
benchmark.run(print_data=True)
|
||||
benchmark.run()
|
||||
|
||||
@@ -17,7 +17,7 @@ BS_LIST = [2**n for n in range(0, 15)]
|
||||
BS_LIST += [x + 1 + i for i, x in enumerate(BS_LIST)]
|
||||
BS_LIST = get_ci_test_range(BS_LIST, [1, 9, 256, 16399])
|
||||
HIDDEN_DIMS = get_ci_test_range(
|
||||
[64, 128, 256, 512, 1024, 96, 98, 100], [64, 512, 1024, 98]
|
||||
[64, 128, 256, 512, 1024, 96, 97, 100], [64, 512, 1024, 97]
|
||||
)
|
||||
CACHE_SIZE = 1024 * 1024
|
||||
DTYPE = torch.bfloat16
|
||||
@@ -35,7 +35,6 @@ def test_store_cache(batch_size: int, element_dim: int) -> None:
|
||||
v_cache = torch.randn((CACHE_SIZE, element_dim), dtype=DTYPE, device=DEVICE)
|
||||
indices = torch.randperm(CACHE_SIZE - 1, device=DEVICE)[:batch_size] + 1
|
||||
|
||||
# AOT store cache
|
||||
store_cache(k, v, k_cache, v_cache, indices)
|
||||
|
||||
assert torch.all(k_cache[indices] == k)
|
||||
@@ -89,10 +88,7 @@ def test_store_cache_int32_indices(batch_size: int, element_dim: int) -> None:
|
||||
|
||||
|
||||
@pytest.mark.parametrize("index_dtype", [torch.int32, torch.int64])
|
||||
@pytest.mark.parametrize("num_split", [1, 2, 4])
|
||||
def test_store_cache_reserved_skip_index(
|
||||
index_dtype: torch.dtype, num_split: int
|
||||
) -> None:
|
||||
def test_store_cache_reserved_skip_index(index_dtype: torch.dtype) -> None:
|
||||
element_dim = 1024
|
||||
k = torch.randn((4, element_dim), dtype=DTYPE, device=DEVICE)
|
||||
v = torch.randn((4, element_dim), dtype=DTYPE, device=DEVICE)
|
||||
@@ -112,7 +108,6 @@ def test_store_cache_reserved_skip_index(
|
||||
k_cache,
|
||||
v_cache,
|
||||
indices,
|
||||
num_split=num_split,
|
||||
)
|
||||
|
||||
torch.testing.assert_close(k_cache[0], reserved_k_before, rtol=0.0, atol=0.0)
|
||||
@@ -137,43 +132,6 @@ def test_store_cache_zero_index_can_be_written_when_skip_disabled() -> None:
|
||||
torch.testing.assert_close(v_cache[0], v[0], rtol=0.0, atol=0.0)
|
||||
|
||||
|
||||
def _valid_num_splits(element_dim: int, dtype: torch.dtype) -> list:
|
||||
"""Return the list of valid num_split values for a given element_dim/dtype."""
|
||||
row_bytes = element_dim * dtype.itemsize
|
||||
splits = [1]
|
||||
if row_bytes % (2 * 128) == 0:
|
||||
splits.append(2)
|
||||
if row_bytes % (4 * 128) == 0:
|
||||
splits.append(4)
|
||||
return splits
|
||||
|
||||
|
||||
_NUM_SPLIT_CASES = [
|
||||
(_dim, _ns, _dtype)
|
||||
for _dtype in [torch.float16, torch.bfloat16, torch.float32]
|
||||
for _dim in REPR_DIMS
|
||||
for _ns in _valid_num_splits(_dim, _dtype)
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("element_dim,num_split,dtype", _NUM_SPLIT_CASES)
|
||||
def test_store_cache_num_split(
|
||||
element_dim: int, num_split: int, dtype: torch.dtype
|
||||
) -> None:
|
||||
batch_size = 128
|
||||
k = torch.randn((batch_size, element_dim), dtype=dtype, device=DEVICE)
|
||||
v = torch.randn((batch_size, element_dim), dtype=dtype, device=DEVICE)
|
||||
k_cache = torch.randn((SMALL_CACHE, element_dim), dtype=dtype, device=DEVICE)
|
||||
v_cache = torch.randn((SMALL_CACHE, element_dim), dtype=dtype, device=DEVICE)
|
||||
indices = torch.randperm(SMALL_CACHE - 1, device=DEVICE)[:batch_size] + 1
|
||||
|
||||
# Verify each num_split kernel path (1, 2, 4) produces correct results
|
||||
store_cache(k, v, k_cache, v_cache, indices, num_split=num_split)
|
||||
|
||||
assert torch.all(k_cache[indices] == k)
|
||||
assert torch.all(v_cache[indices] == v)
|
||||
|
||||
|
||||
# Asymmetric K/V (head_dim != v_head_dim): different row widths AND cache strides.
|
||||
# MiMoV2 is 192/128. Both orderings, since nothing may assume K is the wider one.
|
||||
ASYM_DIM_PAIRS = get_ci_test_range(
|
||||
@@ -208,55 +166,6 @@ def test_store_cache_asymmetric(k_dim: int, v_dim: int, dtype: torch.dtype) -> N
|
||||
assert torch.all(v_cache[untouched] == v_before[untouched])
|
||||
|
||||
|
||||
def _valid_asym_num_splits(k_dim: int, v_dim: int, dtype: torch.dtype) -> list:
|
||||
"""num_split values valid for BOTH rows; a split must divide each of them."""
|
||||
k_bytes, v_bytes = k_dim * dtype.itemsize, v_dim * dtype.itemsize
|
||||
splits = [1]
|
||||
if k_bytes % (2 * 128) == 0 and v_bytes % (2 * 128) == 0:
|
||||
splits.append(2)
|
||||
if k_bytes % (4 * 128) == 0 and v_bytes % (4 * 128) == 0:
|
||||
splits.append(4)
|
||||
return splits
|
||||
|
||||
|
||||
def _default_num_split(k_dim: int, v_dim: int, dtype: torch.dtype) -> int:
|
||||
"""Mirrors the heuristic in store_cache(); the default is already exercised
|
||||
by test_store_cache_asymmetric, which does not pass num_split."""
|
||||
k_bytes, v_bytes = k_dim * dtype.itemsize, v_dim * dtype.itemsize
|
||||
if k_bytes % 2048 == 0 and v_bytes % 2048 == 0:
|
||||
return 4
|
||||
if k_bytes % 1024 == 0 and v_bytes % 1024 == 0:
|
||||
return 2
|
||||
return 1
|
||||
|
||||
|
||||
# Only splits the default heuristic would NOT pick: the split gate is two-sided
|
||||
# (K and V must both align), so the off-default branches are what needs pinning.
|
||||
_ASYM_NUM_SPLIT_CASES = [
|
||||
(_k, _v, _ns)
|
||||
for _k, _v in ASYM_DIM_PAIRS
|
||||
for _ns in _valid_asym_num_splits(_k, _v, DTYPE)
|
||||
if _ns != _default_num_split(_k, _v, DTYPE)
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("k_dim,v_dim,num_split", _ASYM_NUM_SPLIT_CASES)
|
||||
def test_store_cache_asymmetric_num_split(
|
||||
k_dim: int, v_dim: int, num_split: int
|
||||
) -> None:
|
||||
batch_size = 128
|
||||
k = torch.randn((batch_size, k_dim), dtype=DTYPE, device=DEVICE)
|
||||
v = torch.randn((batch_size, v_dim), dtype=DTYPE, device=DEVICE)
|
||||
k_cache = torch.randn((SMALL_CACHE, k_dim), dtype=DTYPE, device=DEVICE)
|
||||
v_cache = torch.randn((SMALL_CACHE, v_dim), dtype=DTYPE, device=DEVICE)
|
||||
indices = torch.randperm(SMALL_CACHE - 1, device=DEVICE)[:batch_size] + 1
|
||||
|
||||
store_cache(k, v, k_cache, v_cache, indices, num_split=num_split)
|
||||
|
||||
assert torch.all(k_cache[indices] == k)
|
||||
assert torch.all(v_cache[indices] == v)
|
||||
|
||||
|
||||
def test_can_use_store_cache() -> None:
|
||||
assert can_use_store_cache(128)
|
||||
assert can_use_store_cache(256)
|
||||
|
||||
Reference in New Issue
Block a user