[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
@@ -28,12 +28,15 @@ Add a new operation that scales each element of a tensor by a scalar factor:
|
||||
These hold for every step below.
|
||||
|
||||
- **`namespace sglang` is where JIT code lives.** Open it after the include block and close it at the end of the file, with the device kernels, traits and host wrapper inside. The shared `host::` / `device::` helpers are nested in it too, so they resolve unqualified. `load_jit` emits the `TVM_FFI_DLL_EXPORT_TYPED_FUNC` wrapper inside `namespace sglang` as well, so the `kernel_name` you pass from Python needs no `sglang::` prefix.
|
||||
- **Tuning policy belongs in Python; C++ gets the answer, not the decision.** For a hyperparameter — split factor, threads per item, block size, vector width, an algorithm variant — prefer (not required) making it a **template parameter** and letting the `@cache_once` module factory choose the value, over a runtime `if`/`switch` in the launcher that picks among pre-instantiated kernels. The kernel then compiles exactly one specialisation and asserts its own preconditions with `static_assert`, while the heuristic that produced the value sits in Python where it is readable, adjustable, and inspectable without recompiling CUDA. `store_cache`'s `num_threads` is the worked example: a `get_kernel(num_split)` ladder guarded by byte-alignment `if constexpr`s became one template argument plus a heuristic in `_jit_kvcache_module`. The exception is a knob that genuinely varies per call with the runtime shape — that has to stay a kernel argument.
|
||||
- **Check where the check is cheapest: `static_assert` > C++ host check > cached Python > per-call Python.** Anything fixed at compile time is a `static_assert`. Anything about the tensors is a `TensorMatcher` / `CHECK_HOST` in the C++ launcher, free next to a kernel launch. A check Python cannot delegate goes inside the `@cache_once` module factory, where it runs once per specialisation. What remains in the per-call entry point costs interpreter time on *every* forward, so it should be nothing but picking the module and allocating `out`.
|
||||
- **Fixed-width integer types.** Prefer `int32_t` / `int64_t` / `uint32_t` / `size_t` over `int`, `long`, or `long long`, so an index has the same width on both sides of the FFI boundary. Bare `int` is fine only where the width plainly cannot matter — an unrolled loop counter over a `constexpr` bound, a template `int` parameter. Shapes arrive as `int64_t` (`SymbolicSize::unwrap()`); narrowing to `uint32_t` for in-kernel indexing is a deliberate act, so write the `static_cast` explicitly and only where the range is known.
|
||||
- **Doxygen comments in C++.** Document exported entities with `///` or `/** ... */` blocks using `\brief`, `\param`, `\tparam`, `\return`, the way `include/sgl_kernel/` does. `python -m sglang.kernels.jit` writes `CommentFormat: Doxygen` into `.clangd` when clangd is 21 or newer, so these render on hover in the editor. Plain `//` remains fine for implementation notes inside a function body.
|
||||
- **ASCII only in C++ and CUDA sources.** Write `--`, `->`, `<=` instead of `—`, `→`, `≤`, including in comments. `grep -nP '[^\x00-\x7F]' <file>` before committing.
|
||||
- **`namespace details` is private.** Anything inside one is an implementation detail of its own header, free to change without notice. Do not name `details::` from another module, and never `using namespace details`. If you find yourself wanting something in there, that is the signal to promote it to a documented name instead.
|
||||
- **`const T* __restrict__` for read-only pointers.** This is what `csrc/` does throughout, and it lets the compiler emit non-coherent (`LDG`) loads.
|
||||
- **Watch the register budget.** For memory-bound kernels, keep to roughly 64 registers per thread so occupancy does not become the limit. Build once with `extra_cuda_cflags=["-Xptxas", "-v"]` to see the actual count, and prefer recomputing a value over letting it spill.
|
||||
- **Watch the register budget.** For memory-bound kernels, keep to roughly 64 registers per thread so occupancy does not become the limit; prefer recomputing a value over letting it spill. Run `SGLANG_JIT_LOG_RESOURCE_USAGE=1 SGLANG_JIT_FORCE_RECOMPILE=1` to log per-kernel registers, spills and shared memory at INFO. Both halves are load-bearing: a cache hit has no compiler output to report, and passing `-Xptxas -v` by hand does nothing on its own because the build captures compiler output and replays it only on failure.
|
||||
- **Alignment is a contract you enforce, not a property you hope for.** A vectorized copy derives its access width from the row *size* (`load_bytes` picks 16B for a 1024B row), but the address it lands on is `base + index * row_stride` — and a stride is not constrained by the size. `TensorMatcher` admits any stride unless you say otherwise, so a padded cache row silently produces `misaligned address`. Every kernel that vectorizes is highly recommended to end its validation with `.ensure_alignment(w)`, where `w` is the width the kernel actually uses.
|
||||
|
||||
---
|
||||
|
||||
@@ -62,7 +65,9 @@ These hold for every step below.
|
||||
|
||||
- **Type aliases**: `fp16_t`, `bf16_t`, `fp32_t`, `fp8_e4m3_t`, `fp8_e5m2_t` and their packed variants `fp16x2_t`, `bf16x2_t`, `fp32x2_t`, etc.
|
||||
- **`SGL_DEVICE`** — Expands to `__forceinline__ __device__`. Use on all device functions.
|
||||
- **`device::kWarpThreads`** — Constant `32`.
|
||||
- **`SGL_DEVICE_HOST`** — `__forceinline__ __device__ __host__`. Use it when a `constexpr` helper must be callable from both the kernel and its launcher, so the two cannot drift.
|
||||
- **`device::kWarpThreads`** — Constant `32` (the codebase's *logical* warp width). `kWarpSize` is an alias. Note this is a group size, not the hardware wave: on gfx950 `warpSize` is 64, and `warp::kFullWidth` reflects that.
|
||||
- **`device::get_lane_id<kNumThreads = kWarpThreads>()`** — this thread's index within its logical group. **Prefer it over `threadIdx.x % kNumThreads` whenever the value feeds an address.** On CUDA the lane register is one read that folds into the address computation, whereas the modulo is a derived value the compiler re-materialises at *every* address scale — 8 instructions and 2 registers on a two-tile warp copy, measured on sm_100a. It picks the cheaper form per platform, so callers never need to care. Only equals the true in-warp lane when `blockDim.x` is a multiple of `kNumThreads`.
|
||||
- **`device::load_as<T>(ptr, offset)`** / **`device::store_as<T>(ptr, val, offset)`** — Type-safe loads/stores from `void*`.
|
||||
- **`device::pointer::offset(ptr, offsets...)`** — Pointer arithmetic on device.
|
||||
- **`host::LaunchKernel(grid, block, device_or_stream [, smem])`** — RAII kernel launcher that:
|
||||
@@ -93,6 +98,7 @@ This is the **primary validation API** for all kernel launchers. Use it to valid
|
||||
- `.with_dtype<T1, T2, ...>()` — allow a set of types
|
||||
- `.with_device<kDLCUDA>(device_sym)` — require CUDA and bind the checked device to a `SymbolicDevice`
|
||||
- `.with_strides({strides...})` — validate strides (omit to require contiguous)
|
||||
- `.ensure_alignment(bytes)` — require the data pointer **and every stride except the innermost** to be a multiple of `bytes` (a power of two). Size-1 dimensions are skipped, since their strides are arbitrary. This is how a vectorized kernel states its precondition; see the alignment convention above.
|
||||
- `.verify(tensor_view)` — execute the check; throws `PanicError` with full context on failure; **chainable** (`verify(a).verify(b)` to check multiple tensors with the same shape)
|
||||
- **`host::is_type<T>(dtype)`** — whether a `DLDataType` denotes the C++ type `T` (e.g. `fp16_t`).
|
||||
|
||||
@@ -104,6 +110,7 @@ device.set_options<kDLCUDA>();
|
||||
TensorMatcher({N}) //
|
||||
.with_dtype<fp16_t>()
|
||||
.with_device<kDLCUDA>(device)
|
||||
.ensure_alignment(16) // e.g. for 128-bit vectorized load/store
|
||||
.verify(dst)
|
||||
.verify(src); // same shape, dtype, device as dst
|
||||
const int64_t n = N.unwrap();
|
||||
@@ -155,6 +162,7 @@ The counterpart to `tensor.h`: that one validates what came in, this one produce
|
||||
- `.store(ptr, offset)` — vectorized store to `ptr[offset]`
|
||||
- `.fill(value)` — fill all N elements with `value`
|
||||
- `operator[](i)` — element access
|
||||
- **`device::LoadStoreBytes`** — named widths to pass around instead of bare integers: `MAX_GMEM` (arch-dependent, 32 on Blackwell), `MAX_SMEM` (16), `MAX_PORTABLE` (16, safe on CUDA and HIP), `MIN_COALALESCED` (4), and `RAW_1B` .. `RAW_32B`.
|
||||
|
||||
### `tile.cuh` — `tile::Memory` (strided memory access pattern)
|
||||
|
||||
@@ -164,12 +172,22 @@ The counterpart to `tensor.h`: that one validates what came in, this one produce
|
||||
|
||||
- `tile::Memory<T>` is fundamentally a **1D cooperative accessor** over a contiguous region.
|
||||
- **`device::tile::Memory<T>::cta(blockDim.x)`** — Creates a tile accessor where each thread handles `tid = threadIdx.x` with stride `tsize` (for `cta(blockDim.x)`, this is `blockDim.x`). Common for loops over a 1D array.
|
||||
- **`device::tile::Memory<T>::warp()`** — the 32-lane flavour, and what `warp::load_bytes` is built on. It takes its lane index from `get_lane_id()` rather than `threadIdx.x % 32`, for the addressing reason described under `utils.cuh`. Use `warp(int n)` for a narrower sub-group; that overload keeps the modulo, since a hardware lane index is the wrong grouping below 32.
|
||||
- **`device::tile::Memory<T>::thread()`** — single-thread accessor, no cooperation.
|
||||
- **`.load(ptr, offset)`** — loads `ptr[tid + offset * tsize]`
|
||||
- **`.store(ptr, val, offset)`** — stores to `ptr[tid + offset * tsize]`
|
||||
- **`.in_bound(n, offset)`** — boundary check
|
||||
|
||||
For a **2D tile**, either flatten `(row, col)` into a linear tile index first, or compute the address manually with `ptr[row * stride + col]` using your thread/block coordinates.
|
||||
|
||||
### `bits.h` — Compile-time bit helpers (`host::`)
|
||||
|
||||
```cpp
|
||||
#include <sgl_kernel/bits.h>
|
||||
```
|
||||
|
||||
`constexpr` wrappers over `<bit>`, usable in `static_assert` and in template arguments: `host::is_pow2(x)`, `log2_floor(x)`, `log2_ceil(x)` (both `-1` for `x == 0`), `round_up_pow2(x)`, `round_down_pow2(x)`. They live in `host::` but are constant-expression-only, so device-side `static_assert` can call them.
|
||||
|
||||
### `math.cuh` — Device math (`device::math::`)
|
||||
|
||||
```cpp
|
||||
@@ -185,8 +203,19 @@ For a **2D tile**, either flatten `(row, col)` into a linear tile index first, o
|
||||
#include <sgl_kernel/warp.cuh>
|
||||
```
|
||||
|
||||
- `device::warp::reduce<Op, kNumThreads, kInner>(value, active_mask)` — generic warp reduction via `__shfl_xor_sync`. `Op` is a `device::ReductionOp` (`SUM`/`MAX`/`MIN`); `kNumThreads` is a power-of-two group size (default 32 = full warp); `kInner=true` (default) reduces within each `kNumThreads`-sized group, `kInner=false` reduces across groups (lanes at the same offset in different groups).
|
||||
- `device::warp::reduce_sum/reduce_max/reduce_min<kNumThreads, kInner>(value)` — convenience wrappers over `reduce`. Work for any type with a `ReductionTrait`: floats, integers, and packed x2 types.
|
||||
**Vectorized row copy — reach for this before hand-rolling.** A warp cooperatively moving one contiguous row is the single most common shape in this tree, and it used to be re-implemented per kernel.
|
||||
|
||||
- **`warp::load_bytes<kBytes, kPattern>(src)`** / **`warp::store_bytes<kBytes, kPattern>(dst, val)`** — the whole warp moves `kBytes` from/to one contiguous row. `kBytes` is arbitrary, down to 1; the helper picks the widest vector that divides it and handles the ragged tail. The returned value is opaque and encodes the width, so a load and its matching store **must use the same `<kBytes, kPattern>`**.
|
||||
- **`warp::LoadStorePattern`** — the `kPattern` values, and the sign carries the meaning. A **negative** one (`WARP_UNIFORM_16B` and friends: `_GMEM`, `_SMEM`, `_4B`/`_8B`/`_32B`) says *the whole warp splits this row*, so the width is derived from the per-lane share (`gcd(kBytes / 32, |kPattern|)`), falling back to `gcd(kBytes, 4)` when the row is too narrow to give every lane 4 bytes. A **positive** one (a bare `LoadStoreBytes` value) makes no warp-splitting assumption and just caps the per-thread vector at `gcd(kBytes, kPattern)`. `WARP_UNIFORM_16B` is the usual choice for a row copy.
|
||||
- **`LoadStorePattern::get_vec_bytes<kBytes, kPattern>()`** — the width actually chosen. `SGL_DEVICE_HOST`, so **the launcher must call this to feed `.ensure_alignment(...)`**. Pass it the same value the kernel passes `load_bytes` — for a split row that is the *per-warp share*, not the whole row; the full row can resolve to a different width and would under-constrain the strides.
|
||||
|
||||
**Reductions.**
|
||||
|
||||
- `warp::reduce<Op, kStart, kFinish>(value, active_mask)` — `Op` is a `device::ReductionOp` (`SUM`/`MAX`/`MIN`). `kStart` and `kFinish` bound a range of lane-index bits: `<N, 1>` reduces within contiguous groups of `N` lanes, `<kFullWidth, N>` reduces *across* groups at the same offset. The operation is **symmetric** — `<A, B>` and `<B, A>` reduce the same lane set.
|
||||
- `warp::reduce_sum/reduce_max/reduce_min<kStart, kFinish>(value)` — wrappers. Work for any type with a `ReductionTrait`: floats, integers, packed x2. This is more widely used in practice than generic `reduce`.
|
||||
- `warp::inclusive_reduce<Op, kWidth, kStart, kFinish>(val, lane_id, mask)` and `inclusive_sum` / `inclusive_max` / `inclusive_min` — segmented inclusive scan: every lane keeps its own running total, unlike `reduce`. Forward when `kStart < kFinish`, backward when `kStart > kFinish`. `lane_id` defaults to `get_lane_id<kWidth>()`, which is correct by construction — only pass it if you already have the **segment-relative** index (`threadIdx.x % kWidth`); a warp-relative one silently corrupts every segment past the first when `kWidth < 32`.
|
||||
- `warp::broadcast<kWidth>(value, src_lane)` — `src_lane` is **segment-relative**: each `kWidth` segment reads its own lane, not one warp-wide source.
|
||||
- `warp::get_lane_id`, `warp::elect_one_lane()` (one elected lane, for gating a single-thread TMA issue; CUDA-only), `warp::kFullMask` and `warp::kFullWidth` (32 on CUDA, 64 on HIP — note this differs from `kWarpThreads`).
|
||||
|
||||
### `cta.cuh` — CTA-level primitives
|
||||
|
||||
@@ -203,6 +232,11 @@ For a **2D tile**, either flatten `(row, col)` into a linear tile index first, o
|
||||
```
|
||||
|
||||
- `device::atomic::max(float* addr, float value)` — float atomic max (handles negative values correctly via bit tricks).
|
||||
- **`device::atomic::Event`** — a cross-CTA arrive/wait counter in one 32-bit word. Producers call `arrive()`; consumers call `wait(num_producers)` (exactly one consumer) or `wait_multi<kConsumerBits>(num_producers, num_consumers, n)` (several). The last released consumer resets the word, so one `Event` is reusable across launches with no host re-zero. Preconditions, all of them load-bearing:
|
||||
- the word must be **zeroed by the host** before first use, and must live in **global** memory — a shared or local `Event` is an illegal address;
|
||||
- `arrive()` is a release and the waits are acquires, so a producer's writes before `arrive()` are visible after the wait;
|
||||
- **generations must be explicitly ordered** — the word has no phase bit, so overlapping generation N+1 with a consumer still in generation N is undefined behaviour (a lagging consumer never exits);
|
||||
- `wait()` and `wait_multi()` lay the word out incompatibly; mixing them on one `Event` is undefined behaviour.
|
||||
|
||||
### `runtime.cuh` — Occupancy and device info
|
||||
|
||||
@@ -211,8 +245,10 @@ For a **2D tile**, either flatten `(row, col)` into a linear tile index first, o
|
||||
```
|
||||
|
||||
- `host::runtime::get_blocks_per_sm(kernel, block_dim)` — max active blocks per SM (occupancy)
|
||||
- `host::runtime::get_sm_count(device_id)` — number of SMs on the device
|
||||
- `host::runtime::get_cc_major(device_id)` — compute capability major version
|
||||
- `host::runtime::get_sm_count(device_id [, use_cache])` — number of SMs on the device. Memoized per device ordinal, so it is cheap enough to call on a launch path; pass `use_cache=false` to force a driver query.
|
||||
- `host::runtime::get_cc_major` / `get_cc_minor` / `get_sm_version` — compute capability, same caching.
|
||||
|
||||
**Do not query the architecture at runtime.** The JIT compiles for the exact local GPU, so the arch is a *compile-time* fact: `SGL_CUDA_ARCH` is injected by `load_jit`, and `SGL_ARCH_HOPPER_OR_GREATER` / `SGL_ARCH_BLACKWELL_OR_GREATER` / `device::kMaxVecBytes` are derived from it. Reach for `get_cc_*` only for something the arch genuinely does not determine. SM *count* is the opposite case — it varies within an arch (B200 vs B300 vs a MIG slice), so it has to stay a runtime query.
|
||||
|
||||
**Persistent kernel pattern** (cap blocks to SM count × occupancy):
|
||||
```cpp
|
||||
@@ -586,7 +622,7 @@ Benchmarks use the project's own `marker` framework (in `python/sglang/kernels/j
|
||||
- `graph_clone_args` / `graph_clone_kwargs`: which inputs to clone per CUDA-graph iteration to defeat L2 cache reuse. Defaults to `"all"` — pass an iterable of indices/keys to limit to the *read* args (writes don't need cloning).
|
||||
- `use_cuda_graph=False` for kernels that can't be captured.
|
||||
- `metrics=(0.5, "avg")` controls reported quantiles (the first metric becomes the table latency column).
|
||||
- `disable_log_bandwidth` (defaults from `SGLANG_KERNEL_DISABLE_LOG_BANDWIDTH=1`) skips the bandwidth column entirely.
|
||||
- `disable_log_bandwidth` (defaults from `SGLANG_JIT_BENCHMARK_DISABLE_LOG_BANDWIDTH=1`) skips the bandwidth column entirely.
|
||||
- **`utils.create_random(*shape)` / `utils.create_empty(*shape)`** — shorthand for `torch.randn` / `torch.empty` with `DEFAULT_DTYPE` (`bfloat16`) and `DEFAULT_DEVICE` (`"cuda"`). Override via the `dtype=` / `device=` kwargs.
|
||||
- **`utils.get_benchmark_range(full_range, ci_range)`** — returns the smaller `ci_range` under CI (`is_in_ci()`), the `full_range` locally. Still available for the `benchmark(...)` column axis (which has no `ci_vals`); for `parametrize` row axes prefer the built-in `ci_vals` argument.
|
||||
|
||||
@@ -640,7 +676,7 @@ if __name__ == "__main__":
|
||||
- The `line_arg` name passed to `benchmark` (`"impl"` here) must match a parameter on `benchmark(...)`; same for every `parametrize` name (`"size"`).
|
||||
- Stack `@parametrize` once per swept axis. The required `@marker.benchmark` is the **innermost** decorator (bottom of the stack, directly above the function) — `@parametrize` rows go above it.
|
||||
- Prefer `create_random` / `create_empty` from `utils.py` over open-coding `torch.randn(..., dtype=..., device=...)`.
|
||||
- The GB/s column appears by default (`memory_args="all"` + `memory_output="out"`). For memory-bound kernels it's the most informative number; scope `memory_args` / `memory_output` to the tensors actually touched if the defaults over- or under-count. For compute-bound kernels where bandwidth is misleading, set `SGLANG_KERNEL_DISABLE_LOG_BANDWIDTH=1` (or `disable_log_bandwidth=True`).
|
||||
- The GB/s column appears by default (`memory_args="all"` + `memory_output="out"`). For memory-bound kernels it's the most informative number; scope `memory_args` / `memory_output` to the tensors actually touched if the defaults over- or under-count. For compute-bound kernels where bandwidth is misleading, set `SGLANG_JIT_BENCHMARK_DISABLE_LOG_BANDWIDTH=1` (or `disable_log_bandwidth=True`).
|
||||
- For in-place kernels (which return `None`), pass the written tensors via `memory_output=(...)` since the `"out"` default would capture nothing.
|
||||
- Tune `graph_clone_args` / `graph_clone_kwargs` to all the arguments that might be read by the kernel. We can only skip cloning for write-only args. For in-place modified args, we still need to clone them to get accurate timing (reusing the same buffer keeps it L2-hot and skews results).
|
||||
- Call `benchmark.run()` (no `print_data=` kwarg — the marker framework prints directly).
|
||||
@@ -665,7 +701,7 @@ cd test && python3 run_suite.py --hw cuda --suite base-b-kernel-benchmark-test-1
|
||||
- **JIT compilation fails**: ensure the `.cuh` file is under `python/sglang/kernels/jit/csrc/`; reduce template argument combinations
|
||||
- **CUDA crash / illegal memory access**: `CUDA_LAUNCH_BLOCKING=1`; `compute-sanitizer --tool memcheck python ...`
|
||||
- **Unstable benchmark results**: `marker.do_bench` uses CUDA-graph-based timing by default; set `use_cuda_graph=False` only if the kernel can't be captured. `graph_clone_args` defaults to `"all"`; if you narrow it, it must still cover every *read* tensor — reusing a single buffer keeps it L2-hot and skews results. Keep *write* tensors in it too: they are what sets the rotation count, and a shared output buffer stays L2-hot the same way.
|
||||
- **Missing GB/s column**: the column is on by default; check that `SGLANG_KERNEL_DISABLE_LOG_BANDWIDTH` is not `1` and `disable_log_bandwidth` is not `True`. For in-place kernels (return `None`) the `memory_output="out"` default counts nothing — pass the written tensors via `memory_output=(...)`
|
||||
- **Missing GB/s column**: the column is on by default; check that `SGLANG_JIT_BENCHMARK_DISABLE_LOG_BANDWIDTH` is not `1` and `disable_log_bandwidth` is not `True`. For in-place kernels (return `None`) the `memory_output="out"` default counts nothing — pass the written tensors via `memory_output=(...)`
|
||||
|
||||
---
|
||||
|
||||
@@ -681,6 +717,7 @@ cd test && python3 run_suite.py --hw cuda --suite base-b-kernel-benchmark-test-1
|
||||
- `python/sglang/kernels/jit/include/sgl_kernel/utils.cuh` — type aliases, `LaunchKernel`, `SGL_DEVICE`
|
||||
- `python/sglang/kernels/jit/include/sgl_kernel/vec.cuh` — `AlignedVector`
|
||||
- `python/sglang/kernels/jit/include/sgl_kernel/tile.cuh` — `tile::Memory`
|
||||
- `python/sglang/kernels/jit/include/sgl_kernel/bits.h` — compile-time bit helpers
|
||||
- `python/sglang/kernels/jit/include/sgl_kernel/type.cuh` — `DTypeTrait`, `packed_t`, `device::cast`, `device::unpack`, `ReductionTrait`
|
||||
- `python/sglang/kernels/jit/include/sgl_kernel/math.cuh` — `device::math::`
|
||||
- `python/sglang/kernels/jit/include/sgl_kernel/warp.cuh` — `warp::reduce<Op>` and `reduce_sum/max/min` wrappers
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import builtins
|
||||
import contextlib
|
||||
import inspect
|
||||
import itertools
|
||||
import math
|
||||
import os
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
@@ -22,6 +22,7 @@ from typing import (
|
||||
import torch
|
||||
|
||||
from sglang.kernels.jit.utils import cache_once
|
||||
from sglang.srt.environ import envs
|
||||
from sglang.utils import is_in_ci
|
||||
|
||||
F = TypeVar("F", bound=Callable[..., "BenchResult"])
|
||||
@@ -29,8 +30,9 @@ Metric: TypeAlias = "float | Literal['avg']"
|
||||
BENCH_CONFIG: TypeAlias = "List[Tuple[Tuple[str, ...], List[Tuple[Any, ...]]]]"
|
||||
UNIT_SCALE = {"us": 1e-6, "ms": 1e-3, "s": 1.0}
|
||||
TYPE_LIST = (bool, int, float, str, torch.dtype, torch.device, None.__class__)
|
||||
DISABLE_LOG_BANDWIDTH = os.environ.get("SGLANG_KERNEL_DISABLE_LOG_BANDWIDTH") == "1"
|
||||
|
||||
DISABLE_LOG_BANDWIDTH = envs.SGLANG_JIT_BENCHMARK_DISABLE_LOG_BANDWIDTH.get()
|
||||
DISABLE_LOG_FLOPS = envs.SGLANG_JIT_BENCHMARK_DISABLE_LOG_FLOPS.get()
|
||||
PATTERN: TypeAlias = "Literal['pow2']"
|
||||
|
||||
__all__ = [
|
||||
"BenchResult",
|
||||
@@ -40,6 +42,7 @@ __all__ = [
|
||||
"parametrize",
|
||||
"do_bench",
|
||||
"skip",
|
||||
"range",
|
||||
]
|
||||
|
||||
|
||||
@@ -153,6 +156,7 @@ class BenchResult(NamedTuple):
|
||||
metrics: Tuple[Metric, ...]
|
||||
times: List[float] # in seconds
|
||||
memory_footprint: Optional[int]
|
||||
flops: Optional[float] = None
|
||||
|
||||
|
||||
class Table:
|
||||
@@ -172,12 +176,7 @@ class Table:
|
||||
def format_latency(r: float) -> str:
|
||||
if math.isnan(r):
|
||||
return "N/A"
|
||||
length = len(str(int(r)))
|
||||
if length < 5:
|
||||
return f"{r:.4f}"
|
||||
# decrease number of the digits
|
||||
digits = max(0, 4 - (length - 5))
|
||||
return f"{r:.{digits}f}"
|
||||
return f"{r:.4f}"
|
||||
|
||||
@staticmethod
|
||||
def format_bandwidth(b: float) -> str:
|
||||
@@ -185,6 +184,12 @@ class Table:
|
||||
return "N/A"
|
||||
return f"{b:.2f}"
|
||||
|
||||
@staticmethod
|
||||
def format_flops(f: float) -> str:
|
||||
if math.isnan(f):
|
||||
return "N/A"
|
||||
return f"{f:.3f}"
|
||||
|
||||
def col(
|
||||
self,
|
||||
header: str = "",
|
||||
@@ -205,7 +210,7 @@ class Table:
|
||||
assert len(cells) == len(self._headers)
|
||||
self._rows.append([str(c) for c in cells])
|
||||
|
||||
def print(self) -> None:
|
||||
def print(self, prefix: Optional[str], suffix: Optional[str]) -> None:
|
||||
widths = [
|
||||
max(max(len(c) + p for c in [h, *(r[i] for r in self._rows)]), mw)
|
||||
for i, (h, mw, p) in enumerate(zip(self._headers, self._mins, self._pads))
|
||||
@@ -220,12 +225,18 @@ class Table:
|
||||
parts.append(f"{cell:{a}{w}}")
|
||||
return "".join(parts)
|
||||
|
||||
if prefix is not None:
|
||||
print("=" * total)
|
||||
print(prefix)
|
||||
print("=" * total)
|
||||
print(fmt(self._headers))
|
||||
print("-" * total)
|
||||
for r in self._rows:
|
||||
print(fmt(r))
|
||||
print("=" * total)
|
||||
if suffix is not None:
|
||||
print(suffix)
|
||||
print("=" * total)
|
||||
|
||||
|
||||
class Benchmark(Generic[F]):
|
||||
@@ -259,15 +270,18 @@ class Benchmark(Generic[F]):
|
||||
self._seen_args.add(name)
|
||||
self._configs.insert(0, (names, vals))
|
||||
|
||||
def _collect_results(self) -> Tuple[List[List[float]], List[List[float]], bool]:
|
||||
def _collect_results(self):
|
||||
axis_names = [n for n, _ in self._configs]
|
||||
axis_vals = [v for _, v in self._configs]
|
||||
results: List[List[float]] = []
|
||||
bandwidth_results: List[List[float]] = []
|
||||
flops_results: List[List[float]] = []
|
||||
should_log_bandwidth = False
|
||||
should_log_flops = False
|
||||
for system in self._line_vals:
|
||||
latencies: List[float] = []
|
||||
bandwidths: List[float] = []
|
||||
flops: List[float] = []
|
||||
for combo in itertools.product(*axis_vals):
|
||||
kwargs: Dict[str, Any] = {self._line_arg: system}
|
||||
for names, values in zip(axis_names, combo):
|
||||
@@ -278,6 +292,8 @@ class Benchmark(Generic[F]):
|
||||
latencies.append(float("nan"))
|
||||
if not DISABLE_LOG_BANDWIDTH:
|
||||
bandwidths.append(float("nan"))
|
||||
if not DISABLE_LOG_FLOPS:
|
||||
flops.append(float("nan"))
|
||||
continue
|
||||
except BaseException:
|
||||
print(f"Benchmark failed at {system}, kwargs =", kwargs)
|
||||
@@ -288,11 +304,26 @@ class Benchmark(Generic[F]):
|
||||
bandwidths.append(
|
||||
result.memory_footprint / (1024**3) / result.times[0]
|
||||
)
|
||||
if not DISABLE_LOG_FLOPS and result.flops is not None:
|
||||
should_log_flops = True
|
||||
flops.append(result.flops / (1e12) / result.times[0])
|
||||
results.append(latencies)
|
||||
bandwidth_results.append(bandwidths)
|
||||
return results, bandwidth_results, should_log_bandwidth
|
||||
flops_results.append(flops)
|
||||
return (
|
||||
results,
|
||||
bandwidth_results,
|
||||
flops_results,
|
||||
should_log_bandwidth,
|
||||
should_log_flops,
|
||||
)
|
||||
|
||||
def run(self) -> None:
|
||||
def run(
|
||||
self,
|
||||
*,
|
||||
print_prefix: Optional[str] = None,
|
||||
print_suffix: Optional[str] = None,
|
||||
) -> None:
|
||||
# Pre-check: every required fn param must be covered.
|
||||
flat_names = [n for names, _ in self._configs for n in names]
|
||||
kinds = (
|
||||
@@ -308,7 +339,9 @@ class Benchmark(Generic[F]):
|
||||
f"parameters not parametrized for {self._fn.__name__}: {sorted(missing)}"
|
||||
)
|
||||
|
||||
results, bandwidths, should_log_bw = self._collect_results()
|
||||
results, bandwidths, flops, should_log_bw, should_log_flops = (
|
||||
self._collect_results()
|
||||
)
|
||||
|
||||
table = Table()
|
||||
table.col(min_width=0, pad=0, align="<") # id column (tight, left-aligned)
|
||||
@@ -317,21 +350,34 @@ class Benchmark(Generic[F]):
|
||||
table.sep()
|
||||
for system in self._line_vals:
|
||||
table.col(f"{system}({self._unit})", min_width=15)
|
||||
# one entry per row, per system -- guards the skip/append paths above
|
||||
row_count = math.prod(len(vals) for _, vals in self._configs)
|
||||
if should_log_bw:
|
||||
table.sep()
|
||||
for system in self._line_vals:
|
||||
table.col(f"{system}(GB/s)", min_width=15)
|
||||
assert all(len(b) == row_count for b in bandwidths)
|
||||
if should_log_flops:
|
||||
table.sep()
|
||||
for system in self._line_vals:
|
||||
table.col(f"{system}(TFLOPS)", min_width=15)
|
||||
assert all(len(f) == row_count for f in flops)
|
||||
|
||||
axis_vals = [v for _, v in self._configs]
|
||||
for row_id, combo in enumerate(itertools.product(*axis_vals)):
|
||||
# skip entries that are skipped by all systems
|
||||
if all(math.isnan(r[row_id]) for r in results):
|
||||
continue
|
||||
cells: List[Any] = [row_id]
|
||||
cells.extend(v for vt in combo for v in vt)
|
||||
cells.extend(table.format_latency(r[row_id]) for r in results)
|
||||
if should_log_bw:
|
||||
cells.extend(table.format_bandwidth(r[row_id]) for r in bandwidths)
|
||||
if should_log_flops:
|
||||
cells.extend(table.format_flops(r[row_id]) for r in flops)
|
||||
table.row(*cells)
|
||||
|
||||
table.print()
|
||||
table.print(print_prefix, print_suffix)
|
||||
|
||||
|
||||
def benchmark(line_arg: str, line_vals: List[Any], *, unit: str = "us"):
|
||||
@@ -402,14 +448,14 @@ def _do_bench_internal_graph(
|
||||
|
||||
graph = torch.cuda.CUDAGraph()
|
||||
# NOTE: we rotate the buffer here to avoid L2 cache effect
|
||||
for i in range(1, rotate_count):
|
||||
for i in builtins.range(1, rotate_count):
|
||||
input_args_list[i] = tuple(
|
||||
(
|
||||
_clone_recursive(input_args[j])
|
||||
if j in graph_clone_args
|
||||
else input_args[j]
|
||||
)
|
||||
for j in range(len(input_args))
|
||||
for j in builtins.range(len(input_args))
|
||||
)
|
||||
input_kwargs_list[i] = dict(
|
||||
(k, (_clone_recursive(v) if k in graph_clone_kwargs else v))
|
||||
@@ -417,7 +463,7 @@ def _do_bench_internal_graph(
|
||||
)
|
||||
with graph_context:
|
||||
with torch.cuda.graph(graph, stream=stream):
|
||||
for i in range(loop_count):
|
||||
for i in builtins.range(loop_count):
|
||||
args = input_args_list[i % rotate_count]
|
||||
kwargs = input_kwargs_list[i % rotate_count]
|
||||
fn(*args, **kwargs)
|
||||
@@ -427,7 +473,7 @@ def _do_bench_internal_graph(
|
||||
# then replay the graph and measure the time
|
||||
tic = torch.cuda.Event(enable_timing=True)
|
||||
toc = torch.cuda.Event(enable_timing=True)
|
||||
for _ in range(max(replay_iters // loop_count, 10)):
|
||||
for _ in builtins.range(max(replay_iters // loop_count, 10)):
|
||||
empty_tensor.zero_() # cold the L2 cache
|
||||
sync_multigpu_fn() # sync GPU before each iteration for precise timing
|
||||
tic.record(stream)
|
||||
@@ -444,7 +490,7 @@ def do_bench(
|
||||
input_args: Tuple[Any, ...] = (),
|
||||
input_kwargs: Dict[str, Any] = {},
|
||||
use_cuda_graph: bool = True,
|
||||
warmup_iters: int = 50,
|
||||
warmup_iters: int = 20,
|
||||
replay_iters: int = 1000,
|
||||
metrics: Tuple[Metric, ...] = (0.5, "avg"),
|
||||
stream: torch.cuda.Stream | None = None,
|
||||
@@ -457,8 +503,10 @@ def do_bench(
|
||||
memory_output: Iterable[Any] | Literal["out"] | None = "out",
|
||||
extra_memory_args: Iterable[Any] | None = None,
|
||||
extra_memory_footprint: int = 0,
|
||||
flops: Optional[float] = None,
|
||||
graph_context_fn: Optional[Callable[[], ContextManager]] = None,
|
||||
sync_multigpu_fn: Optional[Callable[[], Any]] = None,
|
||||
estimated_time_ms: Optional[float] = 1.0,
|
||||
) -> BenchResult:
|
||||
"""
|
||||
Benchmark a function using CUDA graph or naive loop.
|
||||
@@ -484,10 +532,15 @@ def do_bench(
|
||||
:param extra_memory_args: Additional arguments to consider for memory footprint calculation.
|
||||
:param extra_memory_footprint: Additional memory footprint to consider.
|
||||
This is typically used when the load/store bytes is dynamic.
|
||||
:param flops: The number of floating-point operations performed by the benchmark.
|
||||
Used for calculating the achieved computation utilization in the profile report.
|
||||
:param graph_context_fn: A callable returning a context manager that wraps the cuda graph capture.
|
||||
:param sync_multigpu_fn: A callable to synchronize multiple GPUs before each iteration. For precise
|
||||
benchmark number in multi-GPU benchmark, it should be some synchronization
|
||||
primitive on GPU side (not on CPU side).
|
||||
:param estimated_time_ms: Estimated time in milliseconds for the benchmark without CUDA graph.
|
||||
This is typically used to control the benchmark time.
|
||||
Ignored if `use_cuda_graph` is True.
|
||||
"""
|
||||
# first warmup the function
|
||||
device_id = torch.cuda.current_device()
|
||||
@@ -499,12 +552,12 @@ def do_bench(
|
||||
with torch.cuda.device(device_id), torch.cuda.stream(stream):
|
||||
stream.wait_stream(old_current_stream)
|
||||
sync_multigpu_fn()
|
||||
for _ in range(warmup_iters):
|
||||
for _ in builtins.range(warmup_iters):
|
||||
fn(*input_args, **input_kwargs)
|
||||
if use_cuda_graph:
|
||||
# NOTE: by default, reduce all the CPU-side overhead
|
||||
if graph_clone_args == "all":
|
||||
graph_clone_args = range(len(input_args))
|
||||
graph_clone_args = builtins.range(len(input_args))
|
||||
elif graph_clone_args is None:
|
||||
graph_clone_args = []
|
||||
if graph_clone_kwargs == "all":
|
||||
@@ -531,7 +584,18 @@ def do_bench(
|
||||
tic = torch.cuda.Event(enable_timing=True)
|
||||
toc = torch.cuda.Event(enable_timing=True)
|
||||
empty_tensor = _get_flush_l2_buffer()
|
||||
for _ in range(max(replay_iters, 10)):
|
||||
if estimated_time_ms is not None:
|
||||
empty_tensor.zero_() # cold the L2 cache
|
||||
sync_multigpu_fn()
|
||||
tic.record(stream)
|
||||
fn(*input_args, **input_kwargs)
|
||||
toc.record(stream)
|
||||
stream.synchronize()
|
||||
duration_ms = tic.elapsed_time(toc)
|
||||
estimted_iters = int(estimated_time_ms / duration_ms)
|
||||
replay_iters = min(replay_iters, estimted_iters)
|
||||
|
||||
for _ in builtins.range(max(replay_iters, 10)):
|
||||
empty_tensor.zero_() # cold the L2 cache
|
||||
sync_multigpu_fn()
|
||||
tic.record(stream)
|
||||
@@ -553,4 +617,10 @@ def do_bench(
|
||||
memory_footprint += _get_nbytes_recursive(memory_args)
|
||||
memory_footprint += _get_nbytes_recursive(memory_output)
|
||||
|
||||
return BenchResult(metrics, result, memory_footprint)
|
||||
return BenchResult(metrics, result, memory_footprint, flops)
|
||||
|
||||
|
||||
def range(*args: int, pattern: PATTERN) -> List[int]:
|
||||
# TODO: support other patterns
|
||||
assert pattern == "pow2", f"unsupported pattern: {pattern}"
|
||||
return [2**i for i in builtins.range(*args)]
|
||||
|
||||
@@ -107,20 +107,6 @@ struct DecodeParamsLegacy {
|
||||
|
||||
inline constexpr uint32_t kMaxPrefillBatchSize = 1024;
|
||||
|
||||
SGL_DEVICE uint32_t warp_inclusive_sum(uint32_t lane_id, uint32_t val) {
|
||||
static_assert(device::kWarpThreads == 32);
|
||||
#pragma unroll
|
||||
for (uint32_t offset = 1; offset < 32; offset *= 2) {
|
||||
#ifndef USE_ROCM
|
||||
uint32_t n = __shfl_up_sync(device::kFullMask, val, offset);
|
||||
#else
|
||||
uint32_t n = __shfl_up(val, offset, 32);
|
||||
#endif
|
||||
if (lane_id >= offset) val += n;
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
__global__ __launch_bounds__(1024, 1) //
|
||||
void plan_compress_prefill_kernel0(const Prefill0Params params) {
|
||||
using namespace device;
|
||||
|
||||
@@ -124,7 +124,7 @@ __global__ __launch_bounds__(CandidateBlockTableConfig::kBlockSize, CandidateBlo
|
||||
count[j] = __popc(words[j]);
|
||||
local += count[j];
|
||||
}
|
||||
const auto warp_inc = warp::inclusive_sum(lane_id, local);
|
||||
const auto warp_inc = warp::inclusive_sum(local, lane_id);
|
||||
if (lane_id == kWarpThreads - 1) smem.warp_sum[warp_id] = warp_inc;
|
||||
__syncthreads(); // also: every thread holds its words, the bitmap may become the queue
|
||||
const auto peer_sum = smem.warp_sum[lane_id];
|
||||
|
||||
@@ -181,7 +181,7 @@ INDEXER_KERNEL void fused_norm_rope_indexer(const __grid_constant__ FusedNormRop
|
||||
#pragma unroll
|
||||
for (int i = 0; i < kVecSize; ++i) {
|
||||
#ifndef USE_ROCM
|
||||
const float other = __shfl_xor_sync(kFullMask, data[i], mask, kWarpThreads);
|
||||
const float other = __shfl_xor_sync(warp::kFullMask, data[i], mask, kWarpThreads);
|
||||
#else
|
||||
const float other = __shfl_xor(data[i], mask, kWarpThreads);
|
||||
#endif
|
||||
@@ -337,7 +337,7 @@ INDEXER_KERNEL void fused_norm_rope_indexer_fp4(const __grid_constant__ FusedNor
|
||||
#pragma unroll
|
||||
for (int i = 0; i < kVecSize; ++i) {
|
||||
#ifndef USE_ROCM
|
||||
const float other = __shfl_xor_sync(kFullMask, data[i], mask, kWarpThreads);
|
||||
const float other = __shfl_xor_sync(warp::kFullMask, data[i], mask, kWarpThreads);
|
||||
#else
|
||||
const float other = __shfl_xor(data[i], mask, kWarpThreads);
|
||||
#endif
|
||||
|
||||
@@ -39,16 +39,6 @@ struct alignas(16) CTAWork {
|
||||
bool valid;
|
||||
};
|
||||
|
||||
SGL_DEVICE uint32_t warp_inclusive_sum(uint32_t lane_id, uint32_t val) {
|
||||
static_assert(device::kWarpThreads == 32);
|
||||
#pragma unroll
|
||||
for (uint32_t offset = 1; offset < 32; offset *= 2) {
|
||||
uint32_t n = __shfl_up_sync(0xFFFFFFFF, val, offset);
|
||||
if (lane_id >= offset) val += n;
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
template <bool kApplySwigluLimit, bool kPrecise = true, typename DType2>
|
||||
SGL_DEVICE fp32x2_t silu_and_mul(DType2 gate, DType2 up, float limit) {
|
||||
using namespace device;
|
||||
@@ -93,7 +83,7 @@ SGL_DEVICE CTAWork get_work(const SiluMulQuantVarlenParams& params) {
|
||||
const uint32_t val = tx < params.num_experts ? params.masked_m[tx] : 0u;
|
||||
|
||||
// Per-warp inclusive scan of masked_m.
|
||||
const uint32_t warp_inclusive = warp_inclusive_sum(lane_id, val);
|
||||
const uint32_t warp_inclusive = warp::inclusive_sum(val, lane_id);
|
||||
const uint32_t warp_exclusive = warp_inclusive - val;
|
||||
|
||||
// Write each warp total.
|
||||
|
||||
@@ -96,7 +96,7 @@ SGL_DEVICE uint32_t warp_exclusive_suffix_sum(uint32_t x, uint32_t lane_id) {
|
||||
uint32_t inc = x;
|
||||
#pragma unroll
|
||||
for (uint32_t offset = 1; offset < device::kWarpThreads; offset <<= 1) {
|
||||
const auto t = __shfl_down_sync(device::kFullMask, inc, offset);
|
||||
const auto t = __shfl_down_sync(device::warp::kFullMask, inc, offset);
|
||||
if (lane_id + offset < device::kWarpThreads) inc += t;
|
||||
}
|
||||
return inc - x;
|
||||
@@ -324,10 +324,10 @@ __global__ __launch_bounds__(TopKBF16Config::kBlockSize, TopKBF16Config::kOccupa
|
||||
// Block-wide exclusive prefix of (gt, eq), packed: one warp scan plus one shared atomic per
|
||||
// warp. Warps land in arrival order, which is fine since the output is unordered.
|
||||
const uint32_t local = cnt_gt << 16 | cnt_eq;
|
||||
const uint32_t warp_inc = warp::inclusive_sum(lane_id, local);
|
||||
const uint32_t warp_inc = warp::inclusive_sum(local, lane_id);
|
||||
uint32_t warp_base = 0;
|
||||
if (lane_id == kWarpThreads - 1) warp_base = atomicAdd(&smem.count_gt_eq, warp_inc);
|
||||
warp_base = __shfl_sync(kFullMask, warp_base, kWarpThreads - 1);
|
||||
warp_base = __shfl_sync(warp::kFullMask, warp_base, kWarpThreads - 1);
|
||||
const uint32_t before = warp_base + warp_inc - local;
|
||||
|
||||
// Everything above the pivot is taken, plus `remain` of the elements equal to it.
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
#include <sgl_kernel/utils.h>
|
||||
|
||||
#include <sgl_kernel/utils.cuh>
|
||||
#include <sgl_kernel/warp.cuh>
|
||||
|
||||
#include <tvm/ffi/container/tensor.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cuda_bf16.h>
|
||||
#include <cuda_runtime.h>
|
||||
|
||||
@@ -138,14 +140,25 @@ struct ConcatMlaKKernel {
|
||||
D_nope.set_value(QK_NOPE_HEAD_DIM);
|
||||
D_rope.set_value(QK_ROPE_HEAD_DIM);
|
||||
|
||||
// The widest access is `v2.s32` (8B), and the `>> 2` stride stepping needs
|
||||
// each row stride to be a whole number of int2's -- which is the same 8B.
|
||||
constexpr int64_t kAlignNope = 8;
|
||||
constexpr int64_t kAlignRope = 4; // rope is read with `v1.s32`
|
||||
|
||||
// Verify k: [num_tokens, num_heads, k_head_dim]
|
||||
TensorMatcher({N, H, D}).with_strides({S0_k, S1_k, 1}).with_dtype<bf16_t>().with_device<kDLCUDA>(device).verify(k);
|
||||
TensorMatcher({N, H, D})
|
||||
.with_strides({S0_k, S1_k, 1})
|
||||
.with_dtype<bf16_t>()
|
||||
.with_device<kDLCUDA>(device)
|
||||
.ensure_alignment(kAlignNope)
|
||||
.verify(k);
|
||||
|
||||
// Verify k_nope: [num_tokens, num_heads, nope_head_dim]
|
||||
TensorMatcher({N, H, D_nope})
|
||||
.with_strides({S0_k_nope, S1_k_nope, 1})
|
||||
.with_dtype<bf16_t>()
|
||||
.with_device<kDLCUDA>(device)
|
||||
.ensure_alignment(kAlignNope)
|
||||
.verify(k_nope);
|
||||
|
||||
// Verify k_rope: [num_tokens, 1, rope_head_dim]
|
||||
@@ -153,13 +166,9 @@ struct ConcatMlaKKernel {
|
||||
.with_strides({S0_k_rope, -1, 1})
|
||||
.with_dtype<bf16_t>()
|
||||
.with_device<kDLCUDA>(device)
|
||||
.ensure_alignment(kAlignRope)
|
||||
.verify(k_rope);
|
||||
|
||||
// Check alignment
|
||||
RuntimeCheck(reinterpret_cast<uintptr_t>(k.data_ptr()) % 16 == 0, "Tensor k must be 16-byte aligned");
|
||||
RuntimeCheck(reinterpret_cast<uintptr_t>(k_nope.data_ptr()) % 16 == 0, "Tensor k_nope must be 16-byte aligned");
|
||||
RuntimeCheck(reinterpret_cast<uintptr_t>(k_rope.data_ptr()) % 16 == 0, "Tensor k_rope must be 16-byte aligned");
|
||||
|
||||
const int num_tokens = static_cast<int>(N.unwrap());
|
||||
|
||||
constexpr int num_warps_per_block = 32;
|
||||
@@ -188,8 +197,8 @@ constexpr int OUT_LAST_DIM = A_LAST_DIM + B_LAST_DIM;
|
||||
|
||||
template <bool kUsePDL>
|
||||
__global__ void concat_mla_absorb_q_kernel(
|
||||
bf16_t* a,
|
||||
bf16_t* b,
|
||||
const bf16_t* a,
|
||||
const bf16_t* b,
|
||||
bf16_t* out,
|
||||
const int num_items,
|
||||
const int dim_1,
|
||||
@@ -199,51 +208,27 @@ __global__ void concat_mla_absorb_q_kernel(
|
||||
const int b_stride_1,
|
||||
const int64_t out_stride_0,
|
||||
const int out_stride_1) {
|
||||
device::PDLWaitPrimary<kUsePDL>();
|
||||
using namespace device;
|
||||
using enum warp::LoadStorePattern::type;
|
||||
constexpr int64_t kABytes = A_LAST_DIM * sizeof(bf16_t);
|
||||
constexpr int64_t kBBytes = B_LAST_DIM * sizeof(bf16_t);
|
||||
|
||||
const int flat_warp_id = (blockIdx.x * blockDim.x + threadIdx.x) / 32;
|
||||
const int lane_id = get_lane_id();
|
||||
PDLWaitPrimary<kUsePDL>();
|
||||
|
||||
const int flat_warp_id = (blockIdx.x * blockDim.x + threadIdx.x) / kWarpThreads;
|
||||
if (flat_warp_id >= num_items) return;
|
||||
|
||||
const int idx_0 = flat_warp_id / dim_1;
|
||||
const int idx_1 = flat_warp_id % dim_1;
|
||||
const auto out_row = out + idx_0 * out_stride_0 + idx_1 * out_stride_1;
|
||||
|
||||
if (flat_warp_id >= num_items) {
|
||||
return;
|
||||
}
|
||||
const auto b_val = warp::load_bytes<kBBytes, WARP_UNIFORM_16B>(b + idx_0 * b_stride_0 + idx_1 * b_stride_1);
|
||||
const auto a_val = warp::load_bytes<kABytes, WARP_UNIFORM_16B>(a + idx_0 * a_stride_0 + idx_1 * a_stride_1);
|
||||
|
||||
using ABufType = int4;
|
||||
constexpr int A_NUM_UNROLL = 2;
|
||||
static_assert(sizeof(ABufType) * A_NUM_UNROLL == A_LAST_DIM * sizeof(a[0]) / 32);
|
||||
ABufType a_buf[A_NUM_UNROLL];
|
||||
PDLTriggerSecondary<kUsePDL>();
|
||||
|
||||
using BBufType = int;
|
||||
constexpr int B_NUM_UNROLL = 1;
|
||||
static_assert(sizeof(BBufType) * B_NUM_UNROLL == B_LAST_DIM * sizeof(b[0]) / 32);
|
||||
BBufType b_buf;
|
||||
|
||||
{
|
||||
const BBufType* base_addr = reinterpret_cast<BBufType*>(b + idx_0 * b_stride_0 + idx_1 * b_stride_1);
|
||||
b_buf = *(base_addr + lane_id);
|
||||
}
|
||||
|
||||
#pragma unroll
|
||||
for (int i = 0; i < A_NUM_UNROLL; ++i) {
|
||||
const ABufType* base_addr = reinterpret_cast<ABufType*>(a + idx_0 * a_stride_0 + idx_1 * a_stride_1);
|
||||
a_buf[i] = *(base_addr + i * 32 + lane_id);
|
||||
}
|
||||
|
||||
device::PDLTriggerSecondary<kUsePDL>();
|
||||
|
||||
{
|
||||
BBufType* base_addr = reinterpret_cast<BBufType*>(out + idx_0 * out_stride_0 + idx_1 * out_stride_1 + A_LAST_DIM);
|
||||
*(base_addr + lane_id) = b_buf;
|
||||
}
|
||||
|
||||
#pragma unroll
|
||||
for (int i = 0; i < A_NUM_UNROLL; ++i) {
|
||||
ABufType* base_addr = reinterpret_cast<ABufType*>(out + idx_0 * out_stride_0 + idx_1 * out_stride_1);
|
||||
*(base_addr + i * 32 + lane_id) = a_buf[i];
|
||||
}
|
||||
warp::store_bytes<kBBytes, WARP_UNIFORM_16B>(out_row + A_LAST_DIM, b_val);
|
||||
warp::store_bytes<kABytes, WARP_UNIFORM_16B>(out_row, a_val);
|
||||
}
|
||||
|
||||
template <bool kUsePDL>
|
||||
@@ -273,11 +258,21 @@ struct ConcatMlaAbsorbQKernel {
|
||||
D_b.set_value(B_LAST_DIM);
|
||||
D_out.set_value(OUT_LAST_DIM);
|
||||
|
||||
using device::warp::LoadStorePattern;
|
||||
using enum LoadStorePattern::type;
|
||||
constexpr int64_t kAlignA = LoadStorePattern::get_vec_bytes<A_LAST_DIM * sizeof(bf16_t), WARP_UNIFORM_16B>();
|
||||
constexpr int64_t kAlignB = LoadStorePattern::get_vec_bytes<B_LAST_DIM * sizeof(bf16_t), WARP_UNIFORM_16B>();
|
||||
// `out` carries both halves, so it must satisfy the wider one; the B half
|
||||
// starts A_LAST_DIM in, which must not break B's own alignment.
|
||||
constexpr int64_t kAlignOut = std::max(kAlignA, kAlignB);
|
||||
static_assert(A_LAST_DIM * sizeof(bf16_t) % kAlignB == 0, "A width must not misalign the B half");
|
||||
|
||||
// Verify a: [dim_0, dim_1, A_LAST_DIM]
|
||||
TensorMatcher({N0_a, N1_a, D_a})
|
||||
.with_strides({S0_a, S1_a, 1})
|
||||
.with_dtype<bf16_t>()
|
||||
.with_device<kDLCUDA>(device)
|
||||
.ensure_alignment(kAlignA)
|
||||
.verify(a);
|
||||
|
||||
// Verify b: [dim_0, dim_1, B_LAST_DIM]
|
||||
@@ -285,6 +280,7 @@ struct ConcatMlaAbsorbQKernel {
|
||||
.with_strides({S0_b, S1_b, 1})
|
||||
.with_dtype<bf16_t>()
|
||||
.with_device<kDLCUDA>(device)
|
||||
.ensure_alignment(kAlignB)
|
||||
.verify(b);
|
||||
|
||||
// Verify out: [dim_0, dim_1, OUT_LAST_DIM]
|
||||
@@ -292,13 +288,9 @@ struct ConcatMlaAbsorbQKernel {
|
||||
.with_strides({S0_out, S1_out, 1})
|
||||
.with_dtype<bf16_t>()
|
||||
.with_device<kDLCUDA>(device)
|
||||
.ensure_alignment(kAlignOut)
|
||||
.verify(out);
|
||||
|
||||
// Check alignment
|
||||
RuntimeCheck(reinterpret_cast<uintptr_t>(a.data_ptr()) % 16 == 0, "Tensor a must be 16-byte aligned");
|
||||
RuntimeCheck(reinterpret_cast<uintptr_t>(b.data_ptr()) % 16 == 0, "Tensor b must be 16-byte aligned");
|
||||
RuntimeCheck(reinterpret_cast<uintptr_t>(out.data_ptr()) % 16 == 0, "Tensor out must be 16-byte aligned");
|
||||
|
||||
// Verify dimensions match: a.size(0) * a.size(1) == b.size(0) * b.size(1)
|
||||
RuntimeCheck(
|
||||
N0_a.unwrap() * N1_a.unwrap() == N0_b.unwrap() * N1_b.unwrap(),
|
||||
@@ -315,8 +307,8 @@ struct ConcatMlaAbsorbQKernel {
|
||||
LaunchKernel(grid_size, block_size, device.unwrap())
|
||||
.enable_pdl(kUsePDL)(
|
||||
concat_mla_absorb_q_kernel<kUsePDL>,
|
||||
static_cast<bf16_t*>(a.data_ptr()),
|
||||
static_cast<bf16_t*>(b.data_ptr()),
|
||||
static_cast<const bf16_t*>(a.data_ptr()),
|
||||
static_cast<const bf16_t*>(b.data_ptr()),
|
||||
static_cast<bf16_t*>(out.data_ptr()),
|
||||
num_items,
|
||||
dim_1,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
#include <sgl_kernel/tensor.h>
|
||||
#include <sgl_kernel/utils.h>
|
||||
|
||||
#include <sgl_kernel/tile.cuh>
|
||||
#include <sgl_kernel/runtime.cuh>
|
||||
#include <sgl_kernel/utils.cuh>
|
||||
#include <sgl_kernel/vec.cuh>
|
||||
#include <sgl_kernel/warp.cuh>
|
||||
|
||||
#include <dlpack/dlpack.h>
|
||||
#include <tvm/ffi/container/tensor.h>
|
||||
@@ -30,169 +31,30 @@ struct StoreKVCacheParams {
|
||||
int64_t reserved_skip_index;
|
||||
};
|
||||
|
||||
constexpr uint32_t kNumWarps = 4;
|
||||
constexpr uint32_t kThreadsPerBlock = kNumWarps * device::kWarpThreads;
|
||||
|
||||
/**
|
||||
* \brief How a warp vectorizes one row of kElementBytes: the widest aligned
|
||||
* vector type it can use, and how many full loop iterations that takes.
|
||||
* Shared by the interleaved and single-row copies so the two cannot drift.
|
||||
* kElementBytes == 0 is a valid (empty) plan, so a zero-width tail can be
|
||||
* queried before being branched away.
|
||||
*/
|
||||
template <int64_t kElementBytes>
|
||||
struct RowVecPlan {
|
||||
static constexpr int64_t kAlignment = (kElementBytes % (16 * device::kWarpThreads) == 0) ? 16
|
||||
: kElementBytes % (8 * device::kWarpThreads) == 0 ? 8
|
||||
: kElementBytes % (4 * device::kWarpThreads) == 0 ? 4
|
||||
: kElementBytes % 4 == 0 ? 4
|
||||
: 0;
|
||||
|
||||
static_assert(kAlignment > 0, "Element size must be multiple of 4 bytes");
|
||||
|
||||
using vec_t = device::AlignedStorage<uint32_t, kAlignment / 4>;
|
||||
static constexpr int64_t kLoopBytes = sizeof(vec_t) * device::kWarpThreads;
|
||||
static constexpr int64_t kLoopCount = kElementBytes / kLoopBytes;
|
||||
static constexpr int64_t kElementCount = kElementBytes / sizeof(vec_t);
|
||||
static constexpr bool kHasEpilogue = kLoopCount * kLoopBytes < kElementBytes;
|
||||
};
|
||||
|
||||
/**
|
||||
* \brief Use a single warp to copy key and value data from source to destination.
|
||||
* Each thread in the warp copies a portion of the data in a coalesced manner.
|
||||
* Both loads are issued before either store: the two rows live in different
|
||||
* tensors, and the params' __restrict__ does not survive into the kernel body,
|
||||
* so the compiler cannot prove k_dst and v_src disjoint and will not sink the
|
||||
* V load past the K store on its own.
|
||||
* \tparam kElementBytes The size of each key/value element in bytes.
|
||||
* \param k_src Pointer to the source key data.
|
||||
* \param v_src Pointer to the source value data.
|
||||
* \param k_dst Pointer to the destination key data.
|
||||
* \param v_dst Pointer to the destination value data.
|
||||
*/
|
||||
template <int64_t kElementBytes>
|
||||
SGL_DEVICE void copy_kv_warp(
|
||||
const void* __restrict__ k_src,
|
||||
const void* __restrict__ v_src,
|
||||
void* __restrict__ k_dst,
|
||||
void* __restrict__ v_dst) {
|
||||
using namespace device;
|
||||
using plan_t = RowVecPlan<kElementBytes>;
|
||||
using vec_t = typename plan_t::vec_t;
|
||||
constexpr auto kLoopCount = plan_t::kLoopCount;
|
||||
|
||||
const auto gmem = tile::Memory<vec_t>::warp();
|
||||
|
||||
#pragma unroll kLoopCount
|
||||
for (int64_t i = 0; i < kLoopCount; ++i) {
|
||||
const auto k = gmem.load(k_src, i);
|
||||
const auto v = gmem.load(v_src, i);
|
||||
gmem.store(k_dst, k, i);
|
||||
gmem.store(v_dst, v, i);
|
||||
}
|
||||
|
||||
// handle the epilogue if any
|
||||
if constexpr (plan_t::kHasEpilogue) {
|
||||
if (gmem.in_bound(plan_t::kElementCount, kLoopCount)) {
|
||||
const auto k = gmem.load(k_src, kLoopCount);
|
||||
const auto v = gmem.load(v_src, kLoopCount);
|
||||
gmem.store(k_dst, k, kLoopCount);
|
||||
gmem.store(v_dst, v, kLoopCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Use a single warp to copy one row from source to destination.
|
||||
* Serves the width by which asymmetric K/V rows differ, which has no counterpart
|
||||
* row to interleave with.
|
||||
* \tparam kElementBytes The size of the row in bytes.
|
||||
* \param src Pointer to the source data.
|
||||
* \param dst Pointer to the destination data.
|
||||
*/
|
||||
template <int64_t kElementBytes>
|
||||
SGL_DEVICE void copy_row_warp(const void* __restrict__ src, void* __restrict__ dst) {
|
||||
using namespace device;
|
||||
using plan_t = RowVecPlan<kElementBytes>;
|
||||
using vec_t = typename plan_t::vec_t;
|
||||
constexpr auto kLoopCount = plan_t::kLoopCount;
|
||||
|
||||
const auto gmem = tile::Memory<vec_t>::warp();
|
||||
|
||||
#pragma unroll kLoopCount
|
||||
for (int64_t i = 0; i < kLoopCount; ++i) {
|
||||
gmem.store(dst, gmem.load(src, i), i);
|
||||
}
|
||||
|
||||
// handle the epilogue if any
|
||||
if constexpr (plan_t::kHasEpilogue) {
|
||||
if (gmem.in_bound(plan_t::kElementCount, kLoopCount)) {
|
||||
gmem.store(dst, gmem.load(src, kLoopCount), kLoopCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Copy a K row of kKBytes and a V row of kVBytes with one warp.
|
||||
* The overlapping prefix goes through the interleaved copy; only the width by
|
||||
* which the rows differ is left as a serial tail. Equal widths degenerate to a
|
||||
* single interleaved copy with no tail.
|
||||
*/
|
||||
template <int64_t kKBytes, int64_t kVBytes>
|
||||
SGL_DEVICE void copy_kv_rows_warp(
|
||||
const void* __restrict__ k_src,
|
||||
const void* __restrict__ v_src,
|
||||
void* __restrict__ k_dst,
|
||||
void* __restrict__ v_dst) {
|
||||
using namespace device;
|
||||
constexpr auto kCommon = kKBytes < kVBytes ? kKBytes : kVBytes;
|
||||
constexpr auto kTail = (kKBytes < kVBytes ? kVBytes : kKBytes) - kCommon;
|
||||
|
||||
// The interleaved copy indexes BOTH rows with kCommon's vector width, so that
|
||||
// width must divide each row's split offset -- the narrower row's alignment
|
||||
// does not imply the wider one's (e.g. 512 picks 16B, but 516 is not 16B
|
||||
// aligned). The tail's own width must likewise divide its kCommon start.
|
||||
// Whatever these gates admit is alignment-safe for the strides too, since a
|
||||
// stride is a whole multiple of its split size.
|
||||
constexpr auto kTailOrCommon = kTail == 0 ? kCommon : kTail;
|
||||
constexpr auto kCommonAlign = RowVecPlan<kCommon>::kAlignment;
|
||||
constexpr auto kTailAlign = RowVecPlan<kTailOrCommon>::kAlignment;
|
||||
constexpr bool kCanInterleave =
|
||||
kKBytes % kCommonAlign == 0 && kVBytes % kCommonAlign == 0 && kCommon % kTailAlign == 0;
|
||||
|
||||
if constexpr (kCanInterleave) {
|
||||
copy_kv_warp<kCommon>(k_src, v_src, k_dst, v_dst);
|
||||
if constexpr (kTail > 0) {
|
||||
if constexpr (kKBytes > kVBytes) {
|
||||
copy_row_warp<kTail>(pointer::offset(k_src, kCommon), pointer::offset(k_dst, kCommon));
|
||||
} else {
|
||||
copy_row_warp<kTail>(pointer::offset(v_src, kCommon), pointer::offset(v_dst, kCommon));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
copy_row_warp<kKBytes>(k_src, k_dst);
|
||||
copy_row_warp<kVBytes>(v_src, v_dst);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Kernel to store key-value pairs into the KV cache.
|
||||
* Each element is split into multiple parts to allow parallel memory copy.
|
||||
* \tparam kKElementBytes The size of each key element in bytes.
|
||||
* \tparam kVElementBytes The size of each value element in bytes. Differs from
|
||||
* kKElementBytes for asymmetric KV (head_dim != v_head_dim).
|
||||
* \tparam kSplit The number of warps that handle each element.
|
||||
* \tparam kKBytes The size of each key element in bytes.
|
||||
* \tparam kVBytes The size of each value element in bytes.
|
||||
* \tparam kNumThreads Threads cooperating on one KV item; a multiple of the
|
||||
* warp size. The block shape is chosen at launch, independently.
|
||||
* \tparam kUsePDL Whether to use PDL feature.
|
||||
* \tparam T The data type of the indices (`int32_t` or `int64_t`).
|
||||
* \tparam TLoc The data type of the indices (`int32_t` or `int64_t`).
|
||||
*/
|
||||
template <int64_t kKElementBytes, int64_t kVElementBytes, int kSplit, bool kUsePDL, typename T>
|
||||
__global__ void store_kvcache(const __grid_constant__ StoreKVCacheParams params) {
|
||||
template <int64_t kKBytes, int64_t kVBytes, uint32_t kNumThreads, bool kUsePDL, typename TLoc>
|
||||
__global__ void store_kvcache_kernel(const __grid_constant__ StoreKVCacheParams params) {
|
||||
using namespace device;
|
||||
constexpr auto kKSplitSize = kKElementBytes / kSplit;
|
||||
constexpr auto kVSplitSize = kVElementBytes / kSplit;
|
||||
const uint32_t warp_id = blockIdx.x * kNumWarps + threadIdx.x / kWarpThreads;
|
||||
const uint32_t item_id = warp_id / kSplit;
|
||||
const uint32_t split_id = warp_id % kSplit;
|
||||
static_assert(kNumThreads % kWarpThreads == 0, "TODO: support sub-warp copy for small items");
|
||||
constexpr uint32_t kNumSplit = kNumThreads / kWarpThreads;
|
||||
// Integer division below would silently drop the remainder of every row.
|
||||
static_assert(kKBytes % kNumSplit == 0 && kVBytes % kNumSplit == 0, "the split must divide both rows exactly");
|
||||
constexpr uint32_t kKSplitBytes = static_cast<uint32_t>(kKBytes) / kNumSplit;
|
||||
constexpr uint32_t kVSplitBytes = static_cast<uint32_t>(kVBytes) / kNumSplit;
|
||||
|
||||
const auto warp_id = blockIdx.x * blockDim.y + threadIdx.y;
|
||||
const auto item_id = warp_id / kNumSplit;
|
||||
const auto split_id = warp_id % kNumSplit;
|
||||
|
||||
const auto& [
|
||||
k_input, v_input, k_cache, v_cache, indices, // ptr
|
||||
stride_k, stride_v, stride_k_cache, stride_v_cache, stride_indices, batch_size, // size
|
||||
@@ -200,45 +62,29 @@ __global__ void store_kvcache(const __grid_constant__ StoreKVCacheParams params)
|
||||
] = params;
|
||||
if (item_id >= batch_size) return;
|
||||
|
||||
const auto index_ptr = static_cast<const T*>(indices) + item_id * stride_indices;
|
||||
PDLWaitPrimary<kUsePDL>();
|
||||
const auto index = static_cast<const TLoc*>(indices)[item_id * stride_indices];
|
||||
const auto k_src = pointer::offset(k_input, item_id * stride_k, split_id * kKSplitBytes);
|
||||
const auto v_src = pointer::offset(v_input, item_id * stride_v, split_id * kVSplitBytes);
|
||||
|
||||
const auto index = *index_ptr;
|
||||
// A stale/OOB slot id would cause an illegal memory access in the store below;
|
||||
// fail fast at the culprit instead. always-on (kvcache JIT compiles without NDEBUG).
|
||||
assert(index >= 0 && index < size_limit);
|
||||
const auto k_src = pointer::offset(k_input, item_id * stride_k, split_id * kKSplitSize);
|
||||
const auto v_src = pointer::offset(v_input, item_id * stride_v, split_id * kVSplitSize);
|
||||
const auto k_dst = pointer::offset(k_cache, index * stride_k_cache, split_id * kKSplitSize);
|
||||
const auto v_dst = pointer::offset(v_cache, index * stride_v_cache, split_id * kVSplitSize);
|
||||
using enum warp::LoadStorePattern::type;
|
||||
const auto k = warp::load_bytes<kKSplitBytes, WARP_UNIFORM_16B>(k_src);
|
||||
const auto v = warp::load_bytes<kVSplitBytes, WARP_UNIFORM_16B>(v_src);
|
||||
|
||||
if (index != reserved_skip_index) {
|
||||
copy_kv_rows_warp<kKSplitSize, kVSplitSize>(k_src, v_src, k_dst, v_dst);
|
||||
}
|
||||
PDLTriggerSecondary<kUsePDL>();
|
||||
assert(index >= 0 && index < size_limit);
|
||||
if (index != reserved_skip_index) {
|
||||
const auto k_dst = pointer::offset(k_cache, index * stride_k_cache, split_id * kKSplitBytes);
|
||||
const auto v_dst = pointer::offset(v_cache, index * stride_v_cache, split_id * kVSplitBytes);
|
||||
warp::store_bytes<kKSplitBytes, WARP_UNIFORM_16B>(k_dst, k);
|
||||
warp::store_bytes<kVSplitBytes, WARP_UNIFORM_16B>(v_dst, v);
|
||||
}
|
||||
}
|
||||
|
||||
template <int64_t kKElementBytes, int64_t kVElementBytes, bool kUsePDL>
|
||||
template <int64_t kKBytes, int64_t kVBytes, uint32_t kNumThreads, bool kUsePDL>
|
||||
struct StoreKVCacheKernel {
|
||||
static_assert(kKElementBytes > 0 && kKElementBytes % 4 == 0);
|
||||
static_assert(kVElementBytes > 0 && kVElementBytes % 4 == 0);
|
||||
|
||||
template <int kSplit, typename T>
|
||||
static constexpr auto store_kernel = store_kvcache<kKElementBytes, kVElementBytes, kSplit, kUsePDL, T>;
|
||||
|
||||
template <typename T>
|
||||
static auto get_kernel(const int num_split) {
|
||||
using namespace host;
|
||||
// only apply split optimization when both element sizes are aligned
|
||||
if constexpr (kKElementBytes % (4 * 128) == 0 && kVElementBytes % (4 * 128) == 0) {
|
||||
if (num_split == 4) return store_kernel<4, T>;
|
||||
}
|
||||
if constexpr (kKElementBytes % (2 * 128) == 0 && kVElementBytes % (2 * 128) == 0) {
|
||||
if (num_split == 2) return store_kernel<2, T>;
|
||||
}
|
||||
if (num_split == 1) return store_kernel<1, T>;
|
||||
Panic("Unsupported num_split {} for element sizes k={} v={}", num_split, kKElementBytes, kVElementBytes);
|
||||
}
|
||||
static constexpr auto store_kernel = store_kvcache_kernel<kKBytes, kVBytes, kNumThreads, kUsePDL, T>;
|
||||
|
||||
static void
|
||||
run(const tvm::ffi::TensorView k,
|
||||
@@ -246,53 +92,63 @@ struct StoreKVCacheKernel {
|
||||
const tvm::ffi::TensorView k_cache,
|
||||
const tvm::ffi::TensorView v_cache,
|
||||
const tvm::ffi::TensorView indices,
|
||||
const int num_split,
|
||||
const int64_t size_limit,
|
||||
const int64_t reserved_skip_index) {
|
||||
using namespace host;
|
||||
auto B = SymbolicSize{"batch_size"};
|
||||
auto DK = SymbolicSize{"k_element_size"};
|
||||
auto DV = SymbolicSize{"v_element_size"};
|
||||
auto KS = SymbolicSize{"k_stride"};
|
||||
auto VS = SymbolicSize{"v_stride"};
|
||||
auto SK = SymbolicSize{"k_cache_stride"};
|
||||
auto SV = SymbolicSize{"v_cache_stride"};
|
||||
auto I = SymbolicSize{"indices_stride"};
|
||||
auto dtype = SymbolicDType{};
|
||||
auto device = SymbolicDevice{};
|
||||
auto indice_dtype = SymbolicDType{};
|
||||
device.set_options<kDLCUDA, kDLROCM>();
|
||||
auto device_ = SymbolicDevice{};
|
||||
auto idx_dtype = SymbolicDType{};
|
||||
device_.set_options<kDLGPU>();
|
||||
|
||||
using device::warp::LoadStorePattern;
|
||||
using enum LoadStorePattern::type;
|
||||
// Feed get_vec_bytes the SPLIT width, i.e. the exact value the kernel hands
|
||||
// to load_bytes -- the full row can resolve to a narrower vector and would
|
||||
// then under-constrain the strides.
|
||||
constexpr uint32_t kNumSplit = kNumThreads / device::kWarpThreads;
|
||||
constexpr int64_t kAlignK = LoadStorePattern::get_vec_bytes<kKBytes / kNumSplit, WARP_UNIFORM_16B>();
|
||||
constexpr int64_t kAlignV = LoadStorePattern::get_vec_bytes<kVBytes / kNumSplit, WARP_UNIFORM_16B>();
|
||||
|
||||
TensorMatcher({B, DK}) //
|
||||
.with_strides({KS, 1})
|
||||
.with_strides({-1, 1})
|
||||
.with_dtype(dtype)
|
||||
.with_device(device)
|
||||
.with_device(device_)
|
||||
.ensure_alignment(kAlignK)
|
||||
.verify(k);
|
||||
TensorMatcher({B, DV}) //
|
||||
.with_strides({VS, 1})
|
||||
.with_strides({-1, 1})
|
||||
.with_dtype(dtype)
|
||||
.with_device(device)
|
||||
.with_device(device_)
|
||||
.ensure_alignment(kAlignV)
|
||||
.verify(v);
|
||||
TensorMatcher({-1, DK}) //
|
||||
.with_strides({SK, 1})
|
||||
.with_strides({-1, 1})
|
||||
.with_dtype(dtype)
|
||||
.with_device(device)
|
||||
.with_device(device_)
|
||||
.ensure_alignment(kAlignK)
|
||||
.verify(k_cache);
|
||||
TensorMatcher({-1, DV}) //
|
||||
.with_strides({SV, 1})
|
||||
.with_strides({-1, 1})
|
||||
.with_dtype(dtype)
|
||||
.with_device(device)
|
||||
.with_device(device_)
|
||||
.ensure_alignment(kAlignV)
|
||||
.verify(v_cache);
|
||||
TensorMatcher({B}) //
|
||||
.with_strides({I})
|
||||
.with_dtype<int32_t, int64_t>(indice_dtype)
|
||||
.with_device(device)
|
||||
.with_strides({-1})
|
||||
.with_dtype<int32_t, int64_t>(idx_dtype)
|
||||
.with_device(device_)
|
||||
.verify(indices);
|
||||
|
||||
const int64_t dtype_size = dtype_bytes(dtype.unwrap());
|
||||
const uint32_t num_elements = static_cast<uint32_t>(B.unwrap());
|
||||
RuntimeCheck(kKElementBytes == dtype_size * DK.unwrap());
|
||||
RuntimeCheck(kVElementBytes == dtype_size * DV.unwrap());
|
||||
const auto dtype_size = static_cast<int64_t>(dtype_bytes(dtype.unwrap()));
|
||||
const auto batch_size = static_cast<uint32_t>(B.unwrap());
|
||||
const auto device = device_.unwrap();
|
||||
CHECK_HOST(kKBytes == dtype_size * DK.unwrap());
|
||||
CHECK_HOST(kVBytes == dtype_size * DV.unwrap());
|
||||
|
||||
if (batch_size == 0) return;
|
||||
|
||||
const auto params = StoreKVCacheParams{
|
||||
.k = k.data_ptr(),
|
||||
@@ -300,20 +156,28 @@ struct StoreKVCacheKernel {
|
||||
.k_cache = k_cache.data_ptr(),
|
||||
.v_cache = v_cache.data_ptr(),
|
||||
.indices = indices.data_ptr(),
|
||||
.stride_k_bytes = KS.unwrap() * dtype_size,
|
||||
.stride_v_bytes = VS.unwrap() * dtype_size,
|
||||
.stride_k_cache_bytes = SK.unwrap() * dtype_size,
|
||||
.stride_v_cache_bytes = SV.unwrap() * dtype_size,
|
||||
.stride_indices = I.unwrap(),
|
||||
.batch_size = static_cast<uint32_t>(B.unwrap()),
|
||||
.stride_k_bytes = k.stride(0) * dtype_size,
|
||||
.stride_v_bytes = v.stride(0) * dtype_size,
|
||||
.stride_k_cache_bytes = k_cache.stride(0) * dtype_size,
|
||||
.stride_v_cache_bytes = v_cache.stride(0) * dtype_size,
|
||||
.stride_indices = indices.stride(0),
|
||||
.batch_size = batch_size,
|
||||
.size_limit = size_limit,
|
||||
.reserved_skip_index = reserved_skip_index,
|
||||
};
|
||||
// select kernel and update num_split if needed
|
||||
const auto use_int32 = indice_dtype.is_type<int32_t>();
|
||||
const auto kernel = use_int32 ? get_kernel<int32_t>(num_split) : get_kernel<int64_t>(num_split);
|
||||
const auto num_blocks = div_ceil(num_elements * num_split, kNumWarps);
|
||||
LaunchKernel(num_blocks, kThreadsPerBlock, device.unwrap()) //
|
||||
|
||||
const auto kernel = idx_dtype.is_type<int32_t>() ? store_kernel<int32_t> : store_kernel<int64_t>;
|
||||
const auto total_warps = batch_size * kNumSplit;
|
||||
const auto num_warps = [&] {
|
||||
const auto sm_count = runtime::get_sm_count(device.device_id);
|
||||
#pragma unroll
|
||||
for (uint32_t n : {1, 2, 4}) {
|
||||
if (total_warps <= sm_count * n) return n;
|
||||
}
|
||||
return 8u;
|
||||
}();
|
||||
const auto num_blocks = div_ceil(total_warps, num_warps);
|
||||
LaunchKernel(num_blocks, {device::kWarpThreads, num_warps}, device) //
|
||||
.enable_pdl(kUsePDL)(kernel, params);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,38 +1,17 @@
|
||||
// JIT TMA bulk-store kernel for MLA paged-KV scatter writes.
|
||||
//
|
||||
// Each warp:
|
||||
// 1. Cooperatively loads one item's (nope, rope) row into a per-warp slot in
|
||||
// shared memory via vectorised ld/st.
|
||||
// 2. Lane 0 issues a single ``cp.async.bulk.global.shared::cta`` (TMA bulk
|
||||
// store, non-tensor variant) to scatter the row to
|
||||
// ``kv_buffer + loc[item] * stride_buffer``.
|
||||
//
|
||||
// End-of-CTA: ``cp.async.bulk.commit_group`` + ``wait_group<0>`` ensures all
|
||||
// in-flight stores commit before the kernel exits so the writes are visible
|
||||
// to subsequent kernels and the host.
|
||||
//
|
||||
// Two correctness gotchas worth a comment (easy to lose):
|
||||
// - ``fence.proxy.async.shared::cta`` between the smem fill and the TMA
|
||||
// store. The TMA engine reads via the async proxy; without the fence it
|
||||
// observes stale smem under heavy concurrency (manifests as zero rows at
|
||||
// large bs).
|
||||
// - ``wait_group`` not ``wait_group_read`` — the latter only allows early
|
||||
// smem reuse; it does not wait for the gmem store to commit globally.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <sgl_kernel/tensor.h>
|
||||
#include <sgl_kernel/utils.h>
|
||||
|
||||
#include <sgl_kernel/runtime.cuh>
|
||||
#include <sgl_kernel/tile.cuh>
|
||||
#include <sgl_kernel/utils.cuh>
|
||||
#include <sgl_kernel/vec.cuh>
|
||||
#include <sgl_kernel/warp.cuh>
|
||||
|
||||
#include <cuda/ptx>
|
||||
#include <dlpack/dlpack.h>
|
||||
#include <tvm/ffi/container/tensor.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
|
||||
namespace sglang {
|
||||
@@ -49,170 +28,115 @@ struct SetMlaKVBufferParams {
|
||||
int64_t reserved_skip_index;
|
||||
};
|
||||
|
||||
template <int64_t kNopeBytes, int64_t kRopeBytes, int kNumWarps, bool kUsePDL, typename TLoc>
|
||||
template <int64_t kNopeBytes, int64_t kRopeBytes, bool kUsePDL, typename TLoc>
|
||||
__global__ void set_mla_kv_buffer_kernel(const __grid_constant__ SetMlaKVBufferParams params) {
|
||||
using namespace device;
|
||||
static_assert((kNopeBytes + kRopeBytes) % 16 == 0, "TMA bulk store requires total row to be 16-byte aligned");
|
||||
|
||||
constexpr int64_t kRowBytes = kNopeBytes + kRopeBytes;
|
||||
|
||||
// One contiguous smem slot per warp; align to 16 for TMA.
|
||||
__shared__ alignas(16) uint8_t smem[kNumWarps][kRowBytes];
|
||||
|
||||
const uint32_t warp_in_cta = threadIdx.x / kWarpThreads;
|
||||
const uint32_t item_id = blockIdx.x * kNumWarps + warp_in_cta;
|
||||
if (item_id >= params.batch_size) return;
|
||||
using enum warp::LoadStorePattern::type;
|
||||
const auto global_warp_id = threadIdx.y + blockIdx.x * blockDim.y;
|
||||
const auto input_nope = pointer::offset(params.k_nope, params.stride_nope_bytes * global_warp_id);
|
||||
const auto input_rope = pointer::offset(params.k_rope, params.stride_rope_bytes * global_warp_id);
|
||||
if (global_warp_id >= params.batch_size) return;
|
||||
|
||||
PDLWaitPrimary<kUsePDL>();
|
||||
|
||||
const int64_t loc = static_cast<int64_t>(static_cast<const TLoc*>(params.loc)[item_id]);
|
||||
|
||||
const auto nope_src = pointer::offset(params.k_nope, item_id * params.stride_nope_bytes);
|
||||
const auto rope_src = pointer::offset(params.k_rope, item_id * params.stride_rope_bytes);
|
||||
void* const gmem_dst = pointer::offset(params.kv_buffer, loc * params.stride_buffer_bytes);
|
||||
|
||||
// Warp-cooperative load (nope, rope) into the per-warp smem slot.
|
||||
warp::copy_bytes<kNopeBytes>(nope_src, &smem[warp_in_cta][0]);
|
||||
warp::copy_bytes<kRopeBytes>(rope_src, &smem[warp_in_cta][kNopeBytes]);
|
||||
|
||||
// Fence required: TMA reads smem via the async proxy, normal sts writes
|
||||
// through the generic proxy. Without this the TMA engine can observe stale
|
||||
// values at large bs.
|
||||
__syncwarp();
|
||||
asm volatile("fence.proxy.async.shared::cta;" ::: "memory");
|
||||
|
||||
// Lane 0 issues one bulk store from the smem slot to the scattered gmem row.
|
||||
if (threadIdx.x % kWarpThreads == 0 && loc != params.reserved_skip_index) {
|
||||
cuda::ptx::cp_async_bulk(
|
||||
cuda::ptx::space_global,
|
||||
cuda::ptx::space_shared,
|
||||
gmem_dst,
|
||||
&smem[warp_in_cta][0],
|
||||
static_cast<uint32_t>(kRowBytes));
|
||||
}
|
||||
|
||||
// Commit and wait for the CTA's bulk-stores to be globally visible before
|
||||
// returning. ``wait_group`` (not ``_read``) is the one that waits for gmem
|
||||
// commit; ``_read`` only releases smem for reuse.
|
||||
cuda::ptx::cp_async_bulk_commit_group();
|
||||
cuda::ptx::cp_async_bulk_wait_group(cuda::ptx::n32_t<0>{});
|
||||
const int64_t loc = static_cast<int64_t>(static_cast<const TLoc*>(params.loc)[global_warp_id]);
|
||||
const auto nope = warp::load_bytes<kNopeBytes, WARP_UNIFORM_16B>(input_nope);
|
||||
const auto rope = warp::load_bytes<kRopeBytes, WARP_UNIFORM_16B>(input_rope);
|
||||
|
||||
PDLTriggerSecondary<kUsePDL>();
|
||||
if (loc != params.reserved_skip_index) {
|
||||
const auto output_nope = pointer::offset(params.kv_buffer, params.stride_buffer_bytes * loc);
|
||||
const auto output_rope = pointer::offset(output_nope, kNopeBytes);
|
||||
warp::store_bytes<kNopeBytes, WARP_UNIFORM_16B>(output_nope, nope);
|
||||
warp::store_bytes<kRopeBytes, WARP_UNIFORM_16B>(output_rope, rope);
|
||||
}
|
||||
}
|
||||
|
||||
template <int64_t kNopeBytes, int64_t kRopeBytes, bool kUsePDL>
|
||||
struct SetMlaKVBufferKernel {
|
||||
static_assert(kNopeBytes > 0 && kNopeBytes % 4 == 0, "kNopeBytes must be a positive multiple of 4");
|
||||
static_assert(kRopeBytes > 0 && kRopeBytes % 4 == 0, "kRopeBytes must be a positive multiple of 4");
|
||||
static_assert(
|
||||
(kNopeBytes + kRopeBytes) % 16 == 0, "TMA bulk store requires (kNopeBytes + kRopeBytes) to be a multiple of 16");
|
||||
|
||||
template <int kNumWarps, typename TLoc>
|
||||
static constexpr auto kernel = set_mla_kv_buffer_kernel<kNopeBytes, kRopeBytes, kNumWarps, kUsePDL, TLoc>;
|
||||
template <typename TLoc>
|
||||
static constexpr auto set_kernel = set_mla_kv_buffer_kernel<kNopeBytes, kRopeBytes, kUsePDL, TLoc>;
|
||||
|
||||
static void
|
||||
run(tvm::ffi::TensorView kv_buffer,
|
||||
tvm::ffi::TensorView loc,
|
||||
tvm::ffi::TensorView k_nope,
|
||||
tvm::ffi::TensorView k_rope,
|
||||
int64_t num_warps_per_block,
|
||||
int64_t,
|
||||
int64_t reserved_skip_index) {
|
||||
using namespace host;
|
||||
|
||||
auto B = SymbolicSize{"batch_size"};
|
||||
auto D_nope = SymbolicSize{"nope_dim"};
|
||||
auto D_rope = SymbolicSize{"rope_dim"};
|
||||
auto D_buf = SymbolicSize{"buffer_last_dim"};
|
||||
auto S_nope = SymbolicSize{"nope_stride"};
|
||||
auto S_rope = SymbolicSize{"rope_stride"};
|
||||
auto S_buf = SymbolicSize{"buffer_stride"};
|
||||
auto S_loc = SymbolicSize{"loc_stride"};
|
||||
auto dtype = SymbolicDType{};
|
||||
auto loc_dtype = SymbolicDType{};
|
||||
auto device = SymbolicDevice{};
|
||||
device.set_options<kDLCUDA>();
|
||||
auto device_ = SymbolicDevice{};
|
||||
device_.set_options<kDLCUDA>();
|
||||
|
||||
TensorMatcher({B, D_nope}) //
|
||||
.with_strides({S_nope, 1})
|
||||
using device::warp::LoadStorePattern;
|
||||
using enum LoadStorePattern::type;
|
||||
constexpr int64_t kAlignNope = LoadStorePattern::get_vec_bytes<kNopeBytes, WARP_UNIFORM_16B>();
|
||||
constexpr int64_t kAlignRope = LoadStorePattern::get_vec_bytes<kRopeBytes, WARP_UNIFORM_16B>();
|
||||
// The buffer row carries both halves, so it has to satisfy the WIDER of the
|
||||
// two -- the narrower one alone would let a nope-misaligned stride through.
|
||||
constexpr int64_t kAlignBuffer = std::max(kAlignNope, kAlignRope);
|
||||
// The rope half starts kNopeBytes into the row, so that offset must not
|
||||
// break the rope alignment the buffer was just checked for.
|
||||
static_assert(kNopeBytes % kAlignRope == 0, "nope width must not misalign the rope half");
|
||||
|
||||
TensorMatcher({B, -1}) //
|
||||
.with_strides({-1, 1})
|
||||
.with_dtype(dtype)
|
||||
.with_device(device)
|
||||
.with_device(device_)
|
||||
.ensure_alignment(kAlignNope)
|
||||
.verify(k_nope);
|
||||
TensorMatcher({B, D_rope}) //
|
||||
.with_strides({S_rope, 1})
|
||||
TensorMatcher({B, -1}) //
|
||||
.with_strides({-1, 1})
|
||||
.with_dtype(dtype)
|
||||
.with_device(device)
|
||||
.with_device(device_)
|
||||
.ensure_alignment(kAlignRope)
|
||||
.verify(k_rope);
|
||||
TensorMatcher({-1, D_buf}) //
|
||||
.with_strides({S_buf, 1})
|
||||
TensorMatcher({-1, -1}) //
|
||||
.with_strides({-1, 1})
|
||||
.with_dtype(dtype)
|
||||
.with_device(device)
|
||||
.with_device(device_)
|
||||
.ensure_alignment(kAlignBuffer)
|
||||
.verify(kv_buffer);
|
||||
TensorMatcher({B}) //
|
||||
.with_strides({S_loc})
|
||||
.with_strides({-1})
|
||||
.with_dtype<int32_t, int64_t>(loc_dtype)
|
||||
.with_device(device)
|
||||
.with_device(device_)
|
||||
.verify(loc);
|
||||
|
||||
const int64_t dtype_size = dtype_bytes(dtype.unwrap());
|
||||
RuntimeCheck(
|
||||
kNopeBytes == dtype_size * D_nope.unwrap(),
|
||||
"kNopeBytes mismatch: expected ",
|
||||
kNopeBytes,
|
||||
", got ",
|
||||
dtype_size * D_nope.unwrap());
|
||||
RuntimeCheck(
|
||||
kRopeBytes == dtype_size * D_rope.unwrap(),
|
||||
"kRopeBytes mismatch: expected ",
|
||||
kRopeBytes,
|
||||
", got ",
|
||||
dtype_size * D_rope.unwrap());
|
||||
RuntimeCheck(dtype_size * D_buf.unwrap() >= kNopeBytes + kRopeBytes, "kv_buffer last dim too small");
|
||||
RuntimeCheck(
|
||||
(S_buf.unwrap() * dtype_size) % 16 == 0,
|
||||
"kv_buffer row stride must be a multiple of 16 bytes for TMA bulk store; got ",
|
||||
S_buf.unwrap() * dtype_size);
|
||||
|
||||
const uint32_t batch = static_cast<uint32_t>(B.unwrap());
|
||||
if (batch == 0) return;
|
||||
const auto dtype_size = static_cast<int64_t>(dtype_bytes(dtype.unwrap()));
|
||||
CHECK_HOST(kv_buffer.size(1) >= k_nope.size(1) + k_rope.size(1));
|
||||
CHECK_HOST(k_nope.size(1) * dtype_size == kNopeBytes);
|
||||
CHECK_HOST(k_rope.size(1) * dtype_size == kRopeBytes);
|
||||
const auto batch_size = static_cast<uint32_t>(B.unwrap());
|
||||
if (batch_size == 0) return;
|
||||
|
||||
const auto params = SetMlaKVBufferParams{
|
||||
.k_nope = k_nope.data_ptr(),
|
||||
.k_rope = k_rope.data_ptr(),
|
||||
.kv_buffer = kv_buffer.data_ptr(),
|
||||
.loc = loc.data_ptr(),
|
||||
.stride_nope_bytes = S_nope.unwrap() * dtype_size,
|
||||
.stride_rope_bytes = S_rope.unwrap() * dtype_size,
|
||||
.stride_buffer_bytes = S_buf.unwrap() * dtype_size,
|
||||
.batch_size = batch,
|
||||
.stride_nope_bytes = k_nope.stride(0) * dtype_size,
|
||||
.stride_rope_bytes = k_rope.stride(0) * dtype_size,
|
||||
.stride_buffer_bytes = kv_buffer.stride(0) * dtype_size,
|
||||
.batch_size = batch_size,
|
||||
.reserved_skip_index = reserved_skip_index,
|
||||
};
|
||||
|
||||
const auto use_int32 = loc_dtype.is_type<int32_t>();
|
||||
|
||||
auto launch = [&]<int kNW>() {
|
||||
const auto kernel_ptr = use_int32 ? kernel<kNW, int32_t> : kernel<kNW, int64_t>;
|
||||
const uint32_t num_blocks = div_ceil(batch, static_cast<uint32_t>(kNW));
|
||||
const uint32_t threads_per_block = static_cast<uint32_t>(kNW) * device::kWarpThreads;
|
||||
LaunchKernel(num_blocks, threads_per_block, device.unwrap()) //
|
||||
.enable_pdl(kUsePDL)(kernel_ptr, params);
|
||||
};
|
||||
|
||||
switch (num_warps_per_block) {
|
||||
case 1:
|
||||
launch.template operator()<1>();
|
||||
break;
|
||||
case 2:
|
||||
launch.template operator()<2>();
|
||||
break;
|
||||
case 4:
|
||||
launch.template operator()<4>();
|
||||
break;
|
||||
case 8:
|
||||
launch.template operator()<8>();
|
||||
break;
|
||||
default:
|
||||
Panic("Unsupported num_warps_per_block=", num_warps_per_block);
|
||||
}
|
||||
const auto device = device_.unwrap();
|
||||
const auto kernel = loc_dtype.is_type<int32_t>() ? set_kernel<int32_t> : set_kernel<int64_t>;
|
||||
const auto num_warps = [&] {
|
||||
const auto sm_count = runtime::get_sm_count(device.device_id);
|
||||
#pragma unroll
|
||||
for (uint32_t n : {1, 2, 4}) {
|
||||
if (batch_size <= sm_count * n) return n;
|
||||
}
|
||||
return 8u;
|
||||
}();
|
||||
const auto num_blocks = div_ceil(batch_size, num_warps);
|
||||
LaunchKernel(num_blocks, {device::kWarpSize, num_warps}, device) //
|
||||
.enable_pdl(kUsePDL)(kernel, params);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#include <sgl_kernel/tile.cuh>
|
||||
#include <sgl_kernel/utils.cuh> // For LaunchKernel, SGL_DEVICE, PDL helpers
|
||||
#include <sgl_kernel/vec.cuh> // For AlignedVector
|
||||
#include <sgl_kernel/warp.cuh> // For warp::copy_bytes, elect_one_lane, inclusive_sum
|
||||
#include <sgl_kernel/warp.cuh> // For warp::load_bytes, store_bytes, elect_one_lane
|
||||
|
||||
#include <cuda/ptx>
|
||||
#include <dlpack/dlpack.h>
|
||||
@@ -52,9 +52,6 @@ __global__ void set_mla_kv_concat_q_kernel(const __grid_constant__ SetMlaKVConca
|
||||
constexpr int kQNopeDim = static_cast<int>(kNopeBytes / sizeof(bf16_t));
|
||||
constexpr int kQRopeDim = static_cast<int>(kRopeBytes / sizeof(bf16_t));
|
||||
|
||||
// Per-warp smem slots for the KV scatter role; concat warps leave theirs idle.
|
||||
__shared__ alignas(16) uint8_t smem[kNumWarps][kRowBytes];
|
||||
|
||||
const uint32_t warp_in_cta = threadIdx.x / kWarpThreads;
|
||||
const uint32_t lane_id = threadIdx.x % kWarpThreads;
|
||||
const uint32_t flat_warp = blockIdx.x * kNumWarps + warp_in_cta;
|
||||
@@ -64,33 +61,19 @@ __global__ void set_mla_kv_concat_q_kernel(const __grid_constant__ SetMlaKVConca
|
||||
if (flat_warp < params.batch_size) {
|
||||
// --- KV scatter role: one warp per token (smem staging + TMA bulk store) ---
|
||||
const uint32_t item_id = flat_warp;
|
||||
const int64_t loc = static_cast<int64_t>(static_cast<const TLoc*>(params.loc)[item_id]);
|
||||
const auto loc = static_cast<const TLoc*>(params.loc)[item_id];
|
||||
|
||||
const auto nope_src = pointer::offset(params.k_nope, item_id * params.stride_nope_bytes);
|
||||
const auto rope_src = pointer::offset(params.k_rope, item_id * params.stride_rope_bytes);
|
||||
void* const gmem_dst = pointer::offset(params.kv_buffer, loc * params.stride_buffer_bytes);
|
||||
|
||||
warp::copy_bytes<kNopeBytes>(nope_src, &smem[warp_in_cta][0]);
|
||||
warp::copy_bytes<kRopeBytes>(rope_src, &smem[warp_in_cta][kNopeBytes]);
|
||||
using enum warp::LoadStorePattern::type;
|
||||
const auto nope = warp::load_bytes<kNopeBytes, WARP_UNIFORM_16B>(nope_src);
|
||||
const auto rope = warp::load_bytes<kRopeBytes, WARP_UNIFORM_16B>(rope_src);
|
||||
|
||||
// TMA reads smem via the async proxy; fence so it can't observe stale sts.
|
||||
__syncwarp();
|
||||
asm volatile("fence.proxy.async.shared::cta;" ::: "memory");
|
||||
|
||||
// elect.sync rather than `lane_id == 0`: the TMA issue must not sit
|
||||
// behind a lane-index predicate (see PR review).
|
||||
if (device::warp::elect_one_lane()) {
|
||||
cuda::ptx::cp_async_bulk(
|
||||
cuda::ptx::space_global,
|
||||
cuda::ptx::space_shared,
|
||||
gmem_dst,
|
||||
&smem[warp_in_cta][0],
|
||||
static_cast<uint32_t>(kRowBytes));
|
||||
}
|
||||
|
||||
// ``wait_group`` (not ``_read``): waits for gmem commit, not just smem reuse.
|
||||
cuda::ptx::cp_async_bulk_commit_group();
|
||||
cuda::ptx::cp_async_bulk_wait_group(cuda::ptx::n32_t<0>{});
|
||||
const auto nope_dst = pointer::offset(params.kv_buffer, loc * params.stride_buffer_bytes);
|
||||
const auto rope_dst = pointer::offset(nope_dst, kNopeBytes);
|
||||
warp::store_bytes<kNopeBytes, WARP_UNIFORM_16B>(nope_dst, nope);
|
||||
warp::store_bytes<kRopeBytes, WARP_UNIFORM_16B>(rope_dst, rope);
|
||||
} else if (flat_warp - params.batch_size < params.num_q_items) {
|
||||
// --- Q concat role: one warp per (token, head) row ---
|
||||
const uint32_t q_item = flat_warp - params.batch_size;
|
||||
@@ -223,7 +206,7 @@ struct SetMlaKVConcatQKernel {
|
||||
// Alignment tripwires. The device code does 16-byte vector accesses on the
|
||||
// kv row / nope rows / q rows and 4-byte accesses on the rope rows; the
|
||||
// python-side ``covered()`` mirrors these so uncovered layouts fall back
|
||||
// instead of faulting (do NOT assume "PyTorch tensors are aligned" — views
|
||||
// instead of faulting (do NOT assume "PyTorch tensors are aligned" -- views
|
||||
// and odd pool pitches break that).
|
||||
const auto aligned = [](const void* ptr, int64_t align) {
|
||||
return reinterpret_cast<uintptr_t>(ptr) % static_cast<uintptr_t>(align) == 0;
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
#include <sgl_kernel/type.cuh> // For dtype_trait, bf16_t, fp32_t, cast
|
||||
#include <sgl_kernel/utils.cuh> // For LaunchKernel, SGL_DEVICE, PDL helpers
|
||||
#include <sgl_kernel/vec.cuh> // For AlignedVector
|
||||
#include <sgl_kernel/warp.cuh> // For warp::copy_bytes, elect_one_lane, inclusive_sum
|
||||
#include <sgl_kernel/warp.cuh> // For warp::inclusive_sum, reduce_sum, reduce_max
|
||||
|
||||
#include <sgl_kernel/deepseek_v4/fp8_utils.cuh>
|
||||
|
||||
@@ -247,7 +247,7 @@ SGL_DEVICE CTAWork get_work(const SituMulQuantVarlenParams& params) {
|
||||
const uint32_t val = tx < params.num_experts ? params.masked_m[tx] : 0u;
|
||||
|
||||
// Per-warp inclusive scan of masked_m.
|
||||
const uint32_t warp_inclusive = device::warp::inclusive_sum(lane_id, val);
|
||||
const uint32_t warp_inclusive = device::warp::inclusive_sum(val, lane_id);
|
||||
const uint32_t warp_exclusive = warp_inclusive - val;
|
||||
|
||||
// Write each warp total.
|
||||
|
||||
@@ -208,11 +208,16 @@ SGL_HICACHE_KERNEL void hicache_transfer_per_layer(const __grid_constant__ Hicac
|
||||
const auto src_k = pointer::offset(k_cache_src, pos_src * kv_cache_src_stride);
|
||||
const auto dst_k = pointer::offset(k_cache_dst, pos_dst * kv_cache_dst_stride);
|
||||
const auto vec_k = load_vec<kElementSize, kNumThreads>(src_k);
|
||||
store_vec<kElementSize, kNumThreads>(dst_k, vec_k);
|
||||
// Both loads are issued before either store: the compiler cannot prove
|
||||
// dst_k and src_v disjoint, so it will not hoist the V load on its own.
|
||||
std::decay_t<decltype(vec_k)> vec_v;
|
||||
if constexpr (!kIsMLA) {
|
||||
const auto src_v = pointer::offset(v_cache_src, pos_src * kv_cache_src_stride);
|
||||
vec_v = load_vec<kElementSize, kNumThreads>(src_v);
|
||||
}
|
||||
store_vec<kElementSize, kNumThreads>(dst_k, vec_k);
|
||||
if constexpr (!kIsMLA) {
|
||||
const auto dst_v = pointer::offset(v_cache_dst, pos_dst * kv_cache_dst_stride);
|
||||
const auto vec_v = load_vec<kElementSize, kNumThreads>(src_v);
|
||||
store_vec<kElementSize, kNumThreads>(dst_v, vec_v);
|
||||
}
|
||||
}
|
||||
@@ -253,13 +258,18 @@ SGL_HICACHE_KERNEL void hicache_transfer_all_layer(const __grid_constant__ Hicac
|
||||
const auto src_k = pointer::offset(k_cache_src, pos_src * kv_cache_src_stride);
|
||||
const auto dst_k = pointer::offset(k_cache_dst, pos_dst * kv_cache_dst_stride);
|
||||
const auto vec_k = load_vec<kElementSize, kNumThreads>(src_k);
|
||||
store_vec<kElementSize, kNumThreads>(dst_k, vec_k);
|
||||
// Both loads are issued before either store: the compiler cannot prove
|
||||
// dst_k and src_v disjoint, so it will not hoist the V load on its own.
|
||||
std::decay_t<decltype(vec_k)> vec_v;
|
||||
if constexpr (!kIsMLA) {
|
||||
const auto v_cache_src = static_cast<const src_ptr_t*>(v_ptr_src)[layer];
|
||||
const auto v_cache_dst = static_cast<const dst_ptr_t*>(v_ptr_dst)[layer];
|
||||
const auto src_v = pointer::offset(v_cache_src, pos_src * kv_cache_src_stride);
|
||||
vec_v = load_vec<kElementSize, kNumThreads>(src_v);
|
||||
}
|
||||
store_vec<kElementSize, kNumThreads>(dst_k, vec_k);
|
||||
if constexpr (!kIsMLA) {
|
||||
const auto v_cache_dst = static_cast<const dst_ptr_t*>(v_ptr_dst)[layer];
|
||||
const auto dst_v = pointer::offset(v_cache_dst, pos_dst * kv_cache_dst_stride);
|
||||
const auto vec_v = load_vec<kElementSize, kNumThreads>(src_v);
|
||||
store_vec<kElementSize, kNumThreads>(dst_v, vec_v);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,20 +67,6 @@ struct TopKTrait {
|
||||
constexpr auto is_greater = [](float x, float y, int32_t delta) {
|
||||
return (x > y) || ((x == y) && delta < 0); // lower block id wins
|
||||
};
|
||||
constexpr auto warp_inclusive_sum = [](uint32_t lane_id, uint32_t val) {
|
||||
#pragma unroll
|
||||
for (uint32_t offset = 1; offset < device::kWarpThreads; offset *= 2) {
|
||||
// Width-32 up-shuffle. On wave64 HIP the un-suffixed __shfl_up takes the
|
||||
// logical-warp width directly; CUDA needs the active mask.
|
||||
#ifdef USE_ROCM
|
||||
uint32_t n = __shfl_up(val, offset, device::kWarpThreads);
|
||||
#else
|
||||
uint32_t n = __shfl_up_sync(kWarpSyncMask, val, offset, device::kWarpThreads);
|
||||
#endif
|
||||
if (lane_id >= offset) val += n;
|
||||
}
|
||||
return val;
|
||||
};
|
||||
constexpr auto clip_nan = [](float x) { return x != x ? kNegInf : x; };
|
||||
constexpr auto score_to_key = [](float x) {
|
||||
uint32_t b = __float_as_uint(x);
|
||||
@@ -95,7 +81,7 @@ struct TopKTrait {
|
||||
uint32_t warp_inc = 0;
|
||||
if (tx < kRadixSize) {
|
||||
hist_val = histogram[tx];
|
||||
warp_inc = warp_inclusive_sum(lane_id, hist_val);
|
||||
warp_inc = warp::inclusive_sum(hist_val, lane_id);
|
||||
if (lane_id == kWarpThreads - 1) smem->warp_sum[warp_id] = warp_inc;
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
#include <sgl_kernel/type.cuh> // For dtype_trait, bf16_t, fp32_t, cast
|
||||
#include <sgl_kernel/utils.cuh> // For LaunchKernel, SGL_DEVICE, PDL helpers
|
||||
#include <sgl_kernel/vec.cuh> // For AlignedVector
|
||||
#include <sgl_kernel/warp.cuh> // For warp::copy_bytes, elect_one_lane, inclusive_sum
|
||||
#include <sgl_kernel/warp.cuh> // For warp::inclusive_sum, reduce_sum
|
||||
|
||||
#include <tvm/ffi/container/tensor.h>
|
||||
|
||||
@@ -60,7 +60,7 @@ SGL_DEVICE void bar_sync(uint32_t id, uint32_t num_threads) {
|
||||
// smem_warp_sum[kNumWarps]; syncs on entry (so the workspace can be reused
|
||||
// across calls) and before the cross-warp read.
|
||||
SGL_DEVICE uint32_t block_exclusive_sum(uint32_t cnt, uint32_t lane_id, uint32_t warp_id, uint32_t* smem_warp_sum) {
|
||||
const uint32_t inc = device::warp::inclusive_sum(lane_id, cnt);
|
||||
const uint32_t inc = device::warp::inclusive_sum(cnt, lane_id);
|
||||
if (lane_id == 31) smem_warp_sum[warp_id] = inc;
|
||||
__syncthreads();
|
||||
// TODO: replace `__reduce_add_sync` with `warp::reduce_sum`
|
||||
@@ -134,12 +134,12 @@ SGL_DEVICE void route_radix_block(const RouteRadixParams& params, typename Large
|
||||
|
||||
// ---- Load + key transform: thread tx owns experts [4*tx, 4*tx+4) ----
|
||||
uint32_t keys[kVecSize];
|
||||
float act[kVecSize]; // raw sigmoid (weight source) — never NaN-sanitized
|
||||
float act[kVecSize]; // raw sigmoid (weight source) -- never NaN-sanitized
|
||||
{
|
||||
const auto scores = static_cast<const TScore*>(params.scores) + bx * params.scores_stride;
|
||||
AlignedVector<fp32x2_t, kVecSize / 2> bias_vec;
|
||||
// bf16: 2x bf16x2 (8B row loads); fp32: 2x fp32x2 (16B row loads). The
|
||||
// radix math below is fp32 either way — only the load width differs.
|
||||
// radix math below is fp32 either way -- only the load width differs.
|
||||
AlignedVector<packed_t<TScore>, kVecSize / 2> scores_vec;
|
||||
|
||||
// Bias may be produced by a preceding cast or fill kernel (the caller
|
||||
@@ -208,7 +208,7 @@ SGL_DEVICE void route_radix_block(const RouteRadixParams& params, typename Large
|
||||
AlignedVector<uint32_t, 2> hist;
|
||||
hist.load(smem.histogram, tx);
|
||||
const auto local_val = hist[0] + hist[1];
|
||||
const auto warp_inc = device::warp::inclusive_sum(lane_id, local_val);
|
||||
const auto warp_inc = device::warp::inclusive_sum(local_val, lane_id);
|
||||
if (lane_id == kWarpThreads - 1) smem.warp_sum[0][warp_id] = warp_inc;
|
||||
moe::radix::bar_sync(BAR_SUM, kRadixLanes);
|
||||
const auto inter = __reduce_add_sync(0xFFFFFFFF, lane_id < warp_id ? smem.warp_sum[0][lane_id] : 0u);
|
||||
@@ -316,7 +316,7 @@ SGL_DEVICE void route_radix_block(const RouteRadixParams& params, typename Large
|
||||
params.out_w[bx * params.out_w_stride + rank] = w;
|
||||
params.out_i[bx * params.out_i_stride + rank] = id;
|
||||
if (params.out_packed != nullptr) {
|
||||
// (id << 16) | bf16(w) bits — RN float->bf16 matches the triton pack.
|
||||
// (id << 16) | bf16(w) bits -- RN float->bf16 matches the triton pack.
|
||||
const auto bits = static_cast<uint32_t>(__bfloat16_as_ushort(__float2bfloat16_rn(w)));
|
||||
params.out_packed[bx * params.out_packed_stride + rank] =
|
||||
static_cast<int32_t>((static_cast<uint32_t>(id) << 16) | bits);
|
||||
@@ -458,7 +458,7 @@ SGL_DEVICE void fgt_select_topk(
|
||||
device::AlignedVector<uint32_t, 2> hist;
|
||||
hist.load(smem.histogram, tx);
|
||||
const auto local_val = hist[0] + hist[1];
|
||||
const auto warp_inc = device::warp::inclusive_sum(lane_id, local_val);
|
||||
const auto warp_inc = device::warp::inclusive_sum(local_val, lane_id);
|
||||
if (lane_id == 31) smem.warp_sum[0][warp_id] = warp_inc;
|
||||
moe::radix::bar_sync(BAR_SUM, kRadixLanes);
|
||||
const auto inter = __reduce_add_sync(0xFFFFFFFF, lane_id < warp_id ? smem.warp_sum[0][lane_id] : 0u);
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <concepts>
|
||||
#include <mutex>
|
||||
#include <type_traits>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace sglang::host {
|
||||
|
||||
/**
|
||||
* \brief Allocate only once for a given function.
|
||||
* \tparam kThreadSafe Whether to make the allocation thread-safe.
|
||||
* \tparam Salt A salt type to avoid cache collision.
|
||||
* \param key The key to identify the allocation. It should be unique for each allocation.
|
||||
* \param callback The callback function to perform the allocation. It should return the allocated value.
|
||||
* \note The `Fn` type must be unique. It's typically a lambda type that's evaluated only once.
|
||||
* Otherwise, different call-sites may hit the same cache entry.
|
||||
* In case where `Fn` is not unique (e.g. std::function), make `Salt` unique to avoid cache collision.
|
||||
*/
|
||||
template <bool kThreadSafe = true, typename Salt = void, typename Key, std::invocable Fn>
|
||||
inline auto allocate_once(Key&& key, Fn&& callback) -> std::decay_t<std::invoke_result_t<Fn>>& {
|
||||
using Value = std::decay_t<std::invoke_result_t<Fn>>;
|
||||
static std::unordered_map<std::decay_t<Key>, Value> s_map;
|
||||
const auto alloc = [&]() -> Value& {
|
||||
const auto iter = s_map.find(key);
|
||||
if (iter != s_map.end()) return iter->second;
|
||||
// Evaluate the callback before inserting, so a throwing callback leaves no empty entry behind.
|
||||
auto value = std::forward<Fn>(callback)();
|
||||
return s_map.emplace(std::forward<Key>(key), std::move(value)).first->second;
|
||||
};
|
||||
if constexpr (kThreadSafe) {
|
||||
static std::mutex s_mutex;
|
||||
const auto lock = std::lock_guard{s_mutex};
|
||||
return alloc();
|
||||
} else {
|
||||
return alloc();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace sglang::host
|
||||
@@ -34,6 +34,136 @@ SGL_DEVICE float max(float* addr, float value) {
|
||||
#endif
|
||||
}
|
||||
|
||||
namespace ptx {
|
||||
|
||||
SGL_DEVICE void red_release_add_u32(uint32_t* ptr, uint32_t n) {
|
||||
asm volatile("red.release.gpu.global.add.u32 [%0], %1;" ::"l"(ptr), "r"(n) : "memory");
|
||||
}
|
||||
|
||||
SGL_DEVICE void red_relaxed_add_u32(uint32_t* ptr, uint32_t n) {
|
||||
asm volatile("red.relaxed.gpu.global.add.u32 [%0], %1;" ::"l"(ptr), "r"(n) : "memory");
|
||||
}
|
||||
|
||||
SGL_DEVICE uint32_t atom_acquire_cas_b32(uint32_t* addr, uint32_t compare, uint32_t swap) {
|
||||
uint32_t result;
|
||||
asm volatile("atom.acquire.gpu.global.cas.b32 %0, [%1], %2, %3;"
|
||||
: "=r"(result)
|
||||
: "l"(addr), "r"(compare), "r"(swap)
|
||||
: "memory");
|
||||
return result;
|
||||
}
|
||||
|
||||
SGL_DEVICE uint32_t load_acquire_u32(uint32_t* addr) {
|
||||
uint32_t result;
|
||||
asm volatile("ld.acquire.gpu.global.u32 %0, [%1];" : "=r"(result) : "l"(addr) : "memory");
|
||||
return result;
|
||||
}
|
||||
|
||||
SGL_DEVICE uint32_t atom_acquire_add_u32(uint32_t* addr, uint32_t n) {
|
||||
uint32_t result;
|
||||
asm volatile("atom.acquire.gpu.global.add.u32 %0, [%1], %2;" : "=r"(result) : "l"(addr), "r"(n) : "memory");
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace ptx
|
||||
|
||||
/**
|
||||
* \brief Cross-CTA arrive/wait counter packed into one 32-bit word.
|
||||
*
|
||||
* Producers call `arrive()`; consumers call `wait()` (exactly one consumer) or
|
||||
* `wait_multi()` (several) until every producer has. The word is split: the low
|
||||
* `32 - kConsumerBits` bits count producer arrivals, the high bits count
|
||||
* consumers that have already been released. The last consumer to be released
|
||||
* subtracts the whole thing, so one Event is reusable across launches without a
|
||||
* host-side re-zero.
|
||||
*
|
||||
* \note The handle must be ZERO before first use. Nothing constructs it on the
|
||||
* device, so zero the backing allocation from the host once.
|
||||
* \note The storage must be GLOBAL memory: the PTX below names the `.global`
|
||||
* state space, so a shared or local Event is an illegal address.
|
||||
* \note `arrive()` is a release and the waits are acquires, so a producer's
|
||||
* writes before `arrive()` are visible to a consumer after the wait.
|
||||
* \note Generations must be explicitly ordered: every consumer of generation N
|
||||
* has to be released before any producer arrives for generation N + 1.
|
||||
* The word carries no phase bit, so overlapping two generations on one
|
||||
* Event is undefined behavior.
|
||||
* \note `wait()` and `wait_multi()` lay the word out incompatibly. Mixing them
|
||||
* on one Event is undefined behavior.
|
||||
*/
|
||||
struct Event {
|
||||
public:
|
||||
using handle_type = uint32_t;
|
||||
|
||||
Event(const Event&) = delete;
|
||||
Event& operator=(const Event&) = delete;
|
||||
|
||||
/// \brief DON'T touch unless you know what you're doing.
|
||||
SGL_DEVICE handle_type& unsafe_get_handle() {
|
||||
return m_handle;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Increment the producer count by `n`.
|
||||
* \param n The number of producers to arrive. Defaults to 1.
|
||||
* \note This is a release operation, so any writes before `arrive()` are
|
||||
* visible to a consumer after `wait()`.
|
||||
*/
|
||||
SGL_DEVICE void arrive(uint32_t n = 1) {
|
||||
ptx::red_release_add_u32(&m_handle, n);
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Block until `num_producers` producers have arrived.
|
||||
* \param num_producers The number of producers to wait for.
|
||||
*
|
||||
* Single-consumer: simpler and faster than `wait_multi()`, but exactly one
|
||||
* thread in the whole grid may call it per generation.
|
||||
*/
|
||||
SGL_DEVICE void wait(uint32_t num_producers) {
|
||||
while (ptx::atom_acquire_cas_b32(&m_handle, num_producers, 0) != num_producers)
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Block until `num_producers` producers have arrived, with several
|
||||
* consumers sharing the Event.
|
||||
* \tparam kConsumerBits Bits reserved for the consumer half of the word.
|
||||
* \param num_producers Must be `< 1 << (32 - kConsumerBits)`.
|
||||
* \param num_consumers Must be in `[1, 1 << kConsumerBits)`, and the `n` of
|
||||
* all callers has to sum to exactly this, otherwise the
|
||||
* Event is never reset.
|
||||
* \param n How many of `num_consumers` this call stands for.
|
||||
* Defaults to 1, i.e. one calling thread per consumer.
|
||||
*/
|
||||
template <uint32_t kConsumerBits = 16u>
|
||||
SGL_DEVICE void wait_multi(uint32_t num_producers, uint32_t num_consumers, uint32_t n = 1) {
|
||||
static_assert(kConsumerBits > 0 && kConsumerBits < 32);
|
||||
constexpr uint32_t kProducerBits = 32 - kConsumerBits;
|
||||
constexpr uint32_t kProducerMask = (1u << kProducerBits) - 1;
|
||||
|
||||
__builtin_assume(num_producers < (1u << kProducerBits));
|
||||
__builtin_assume(num_consumers > 0 && num_consumers < (1u << kConsumerBits));
|
||||
|
||||
// Register and observe in the SAME atomic. ticket = consumers ahead of me.
|
||||
const auto ticket = ptx::atom_acquire_add_u32(&m_handle, n << kProducerBits);
|
||||
if ((ticket & kProducerMask) != num_producers) {
|
||||
/// NOTE: when v = 0, a reset has already happened.
|
||||
while (const auto v = ptx::load_acquire_u32(&m_handle)) {
|
||||
if ((v & kProducerMask) == num_producers) break;
|
||||
}
|
||||
}
|
||||
|
||||
// The last consumer to register should reset the counter to 0
|
||||
if ((ticket >> kProducerBits) + n == num_consumers) {
|
||||
const auto final_value = num_producers | (num_consumers << kProducerBits);
|
||||
ptx::red_relaxed_add_u32(&m_handle, -final_value);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
handle_type m_handle;
|
||||
};
|
||||
|
||||
} // namespace device::atomic
|
||||
|
||||
} // namespace sglang
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
#include <bit>
|
||||
#include <concepts>
|
||||
#include <cstdint>
|
||||
|
||||
namespace sglang {
|
||||
|
||||
namespace host {
|
||||
|
||||
template <std::unsigned_integral T>
|
||||
inline constexpr bool is_pow2(T x) {
|
||||
return std::has_single_bit(x);
|
||||
}
|
||||
|
||||
/// \brief `floor(log2(x))`; -1 for `x == 0`.
|
||||
template <std::unsigned_integral T>
|
||||
inline constexpr int32_t log2_floor(T x) {
|
||||
if (x == 0) return -1;
|
||||
return std::bit_width(x) - 1;
|
||||
}
|
||||
|
||||
/// \brief `ceil(log2(x))`; -1 for `x == 0`.
|
||||
template <std::unsigned_integral T>
|
||||
inline constexpr int32_t log2_ceil(T x) {
|
||||
if (x == 0) return -1;
|
||||
return std::bit_width(x - 1);
|
||||
}
|
||||
|
||||
template <std::unsigned_integral T>
|
||||
inline constexpr T round_up_pow2(T x) {
|
||||
return std::bit_ceil(x);
|
||||
}
|
||||
|
||||
template <std::unsigned_integral T>
|
||||
inline constexpr T round_down_pow2(T x) {
|
||||
return std::bit_floor(x);
|
||||
}
|
||||
|
||||
} // namespace host
|
||||
|
||||
} // namespace sglang
|
||||
@@ -58,9 +58,6 @@ SGL_DEVICE T broadcast(T value, uint32_t src = 0) {
|
||||
#endif
|
||||
}
|
||||
|
||||
/// sgl_kernel names the warp size `kWarpThreads`; alias it locally as `kWarpSize`.
|
||||
inline constexpr uint32_t kWarpSize = kWarpThreads;
|
||||
|
||||
template <typename... Smems>
|
||||
struct MaxSmem {
|
||||
static constexpr size_t kSize = std::max({sizeof(Smems)...});
|
||||
@@ -148,19 +145,6 @@ SGL_DEVICE float coarse_bin_lower_bound(uint32_t bin) {
|
||||
return extract_coarse_bin<kBits>(mid) < bin ? step_up(mid) : mid;
|
||||
}
|
||||
|
||||
SGL_DEVICE uint32_t warp_inclusive_sum(uint32_t lane_id, uint32_t val) {
|
||||
#pragma unroll
|
||||
for (uint32_t offset = 1; offset < 32; offset *= 2) {
|
||||
#ifndef USE_ROCM
|
||||
uint32_t n = __shfl_up_sync(0xFFFFFFFF, val, offset);
|
||||
#else
|
||||
uint32_t n = __shfl_up_sync(kFullMask, val, offset, kWarpThreads);
|
||||
#endif
|
||||
if (lane_id >= offset) val += n;
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
SGL_DEVICE uint32_t warp_sum_bool(bool pred, uint32_t mask = 0xFFFFFFFF) {
|
||||
#ifdef USE_ROCM
|
||||
// The ballot covers the whole hardware wave, which on wave64 holds two of
|
||||
@@ -377,7 +361,7 @@ struct TopKConfig {
|
||||
uint32_t warp_inc = 0;
|
||||
if (tx < kRadixSize) {
|
||||
hist_val = histogram[tx];
|
||||
warp_inc = warp_inclusive_sum(lane_id, hist_val);
|
||||
warp_inc = warp::inclusive_sum(hist_val, lane_id);
|
||||
if (lane_id == kWarpSize - 1) smem->warp_sum[warp_id] = warp_inc;
|
||||
}
|
||||
__syncthreads();
|
||||
@@ -516,7 +500,7 @@ struct TopKRadixBase : TopKConfig {
|
||||
const auto local_sum = local_exc_sum[kItems];
|
||||
const auto lane_id = tx % kWarpSize;
|
||||
const auto warp_id = broadcast(tx / kWarpSize);
|
||||
const auto warp_inc_sum = warp_inclusive_sum(lane_id, local_sum);
|
||||
const auto warp_inc_sum = warp::inclusive_sum(local_sum, lane_id);
|
||||
const auto warp_exc_sum = warp_inc_sum - local_sum;
|
||||
if (lane_id == kWarpSize - 1) smem->warp_sum[warp_id] = warp_inc_sum;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
/// \brief Host-side CUDA runtime query helpers.
|
||||
///
|
||||
/// Thin wrappers around CUDA occupancy and device-property APIs with
|
||||
/// automatic error checking via `RuntimeDeviceCheck`.
|
||||
/// automatic error checking via `CHECK_CUDA`.
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <utility>
|
||||
#ifndef USE_ROCM
|
||||
#include <cuda_runtime.h>
|
||||
#else
|
||||
@@ -66,45 +67,87 @@ inline void* get_device_accessible_ptr(const tvm::ffi::TensorView& tensor) {
|
||||
return device_ptr;
|
||||
}
|
||||
|
||||
namespace details {
|
||||
|
||||
template <typename T, T kDefault>
|
||||
struct DeviceCacheMap {
|
||||
public:
|
||||
// Generous bound on the device ordinals one process can see; a larger ordinal
|
||||
// is not an error, it just falls through to the driver query uncached.
|
||||
static constexpr uint32_t kNumStaticMaxDevice = 72;
|
||||
|
||||
constexpr DeviceCacheMap() {
|
||||
for (uint32_t i = 0; i < kNumStaticMaxDevice; ++i) {
|
||||
m_data[i] = kDefault;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Fn>
|
||||
T get_cached(int32_t device_, bool use_cache, Fn&& fn) {
|
||||
const auto device = static_cast<uint32_t>(device_);
|
||||
if (use_cache && device < kNumStaticMaxDevice && m_data[device] != kDefault) {
|
||||
return m_data[device];
|
||||
}
|
||||
const auto value = static_cast<T>(std::forward<Fn>(fn)(device_));
|
||||
if (device < kNumStaticMaxDevice) {
|
||||
m_data[device] = value;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private:
|
||||
T m_data[kNumStaticMaxDevice];
|
||||
};
|
||||
|
||||
} // namespace details
|
||||
|
||||
// Return the maximum number of active blocks per SM for the given kernel
|
||||
template <typename T>
|
||||
inline auto get_blocks_per_sm(T&& kernel, int32_t block_dim, std::size_t dynamic_smem = 0) -> uint32_t {
|
||||
int num_blocks_per_sm = 0;
|
||||
RuntimeDeviceCheck(
|
||||
cudaOccupancyMaxActiveBlocksPerMultiprocessor(&num_blocks_per_sm, kernel, block_dim, dynamic_smem));
|
||||
CHECK_CUDA(cudaOccupancyMaxActiveBlocksPerMultiprocessor(&num_blocks_per_sm, kernel, block_dim, dynamic_smem));
|
||||
return static_cast<uint32_t>(num_blocks_per_sm);
|
||||
}
|
||||
|
||||
// Return the number of SMs for the given device
|
||||
inline auto get_sm_count(int device_id) -> uint32_t {
|
||||
int sm_count;
|
||||
RuntimeDeviceCheck(cudaDeviceGetAttribute(&sm_count, cudaDevAttrMultiProcessorCount, device_id));
|
||||
return static_cast<uint32_t>(sm_count);
|
||||
inline auto get_sm_count(int device_id, bool use_cache = true) -> uint32_t {
|
||||
static details::DeviceCacheMap<uint32_t, 0> sm_count_cache;
|
||||
return sm_count_cache.get_cached(device_id, use_cache, [](int32_t device_id) {
|
||||
int sm_count;
|
||||
CHECK_CUDA(cudaDeviceGetAttribute(&sm_count, cudaDevAttrMultiProcessorCount, device_id));
|
||||
return sm_count;
|
||||
});
|
||||
}
|
||||
|
||||
// Return the Major compute capability for the given device
|
||||
inline auto get_cc_major(int device_id) -> int {
|
||||
int cc_major;
|
||||
RuntimeDeviceCheck(cudaDeviceGetAttribute(&cc_major, cudaDevAttrComputeCapabilityMajor, device_id));
|
||||
return cc_major;
|
||||
inline auto get_cc_major(int device_id, bool use_cache = true) -> int {
|
||||
static details::DeviceCacheMap<int, -1> cc_major_cache;
|
||||
return cc_major_cache.get_cached(device_id, use_cache, [](int32_t device_id) {
|
||||
int cc_major;
|
||||
CHECK_CUDA(cudaDeviceGetAttribute(&cc_major, cudaDevAttrComputeCapabilityMajor, device_id));
|
||||
return cc_major;
|
||||
});
|
||||
}
|
||||
|
||||
// Return the Minor compute capability for the given device
|
||||
inline auto get_cc_minor(int device_id) -> int {
|
||||
int cc_minor;
|
||||
RuntimeDeviceCheck(cudaDeviceGetAttribute(&cc_minor, cudaDevAttrComputeCapabilityMinor, device_id));
|
||||
return cc_minor;
|
||||
inline auto get_cc_minor(int device_id, bool use_cache = true) -> int {
|
||||
static details::DeviceCacheMap<int, -1> cc_minor_cache;
|
||||
return cc_minor_cache.get_cached(device_id, use_cache, [](int32_t device_id) {
|
||||
int cc_minor;
|
||||
CHECK_CUDA(cudaDeviceGetAttribute(&cc_minor, cudaDevAttrComputeCapabilityMinor, device_id));
|
||||
return cc_minor;
|
||||
});
|
||||
}
|
||||
|
||||
// Return the SM version (major * 10 + minor) for the given device
|
||||
inline auto get_sm_version(int device_id) -> int {
|
||||
return get_cc_major(device_id) * 10 + get_cc_minor(device_id);
|
||||
inline auto get_sm_version(int device_id, bool use_cache = true) -> int {
|
||||
return get_cc_major(device_id, use_cache) * 10 + get_cc_minor(device_id, use_cache);
|
||||
}
|
||||
|
||||
// Return the runtime version
|
||||
inline auto get_runtime_version() -> int {
|
||||
int runtime_version;
|
||||
RuntimeDeviceCheck(cudaRuntimeGetVersion(&runtime_version));
|
||||
CHECK_CUDA(cudaRuntimeGetVersion(&runtime_version));
|
||||
return runtime_version;
|
||||
}
|
||||
|
||||
@@ -112,7 +155,7 @@ inline auto get_runtime_version() -> int {
|
||||
template <typename T>
|
||||
inline auto get_available_dynamic_smem_per_block(T&& kernel, int num_blocks, int block_size) -> std::size_t {
|
||||
std::size_t smem_size;
|
||||
RuntimeDeviceCheck(cudaOccupancyAvailableDynamicSMemPerBlock(&smem_size, kernel, num_blocks, block_size));
|
||||
CHECK_CUDA(cudaOccupancyAvailableDynamicSMemPerBlock(&smem_size, kernel, num_blocks, block_size));
|
||||
return smem_size;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
/// usage examples.
|
||||
|
||||
#pragma once
|
||||
#include <sgl_kernel/bits.h>
|
||||
#include <sgl_kernel/utils.h>
|
||||
|
||||
#include <dlpack/dlpack.h>
|
||||
@@ -18,6 +19,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <concepts>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -527,6 +529,14 @@ struct TensorMatcher {
|
||||
return std::move(*this);
|
||||
}
|
||||
|
||||
/// Ensure alignment on all dimensions except for the last dimension.
|
||||
auto ensure_alignment(int64_t alignment) && -> TensorMatcher&& {
|
||||
RuntimeCheck(!m_alignment.has_value(), "Alignment already specified");
|
||||
RuntimeCheck(is_pow2<uint64_t>(alignment), "Alignment must be a power of 2");
|
||||
m_alignment = alignment;
|
||||
return std::move(*this);
|
||||
}
|
||||
|
||||
// once we start verification, we cannot modify anymore
|
||||
auto verify(tvm::ffi::TensorView view, DebugInfo info = {}) const&& -> const TensorMatcher&& {
|
||||
try {
|
||||
@@ -580,6 +590,18 @@ struct TensorMatcher {
|
||||
// since we may double verify, we will force to check
|
||||
m_dtype->verify(view.dtype());
|
||||
m_device->verify(view.device());
|
||||
if (m_alignment.has_value()) {
|
||||
const auto alignment = *m_alignment;
|
||||
CHECK_HOST(std::bit_cast<uintptr_t>(view.data_ptr()) % alignment == 0)
|
||||
<< "Tensor data pointer is not aligned to " << alignment << " bytes";
|
||||
if (dim > 0) [[likely]] {
|
||||
const auto bytes = static_cast<int64_t>(dtype_bytes(view.dtype()));
|
||||
for (const auto i : irange(dim - 1)) {
|
||||
CHECK_HOST(view.size(i) == 1 || (view.stride(i) * bytes) % alignment == 0)
|
||||
<< "Tensor stride for dimension " << i << " is not aligned to " << alignment << " bytes";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto m_init_dtype() -> void {
|
||||
@@ -602,6 +624,7 @@ struct TensorMatcher {
|
||||
DeviceRef m_device;
|
||||
bool m_has_dtype = false;
|
||||
bool m_has_device = false;
|
||||
std::optional<int64_t> m_alignment;
|
||||
};
|
||||
|
||||
} // namespace host
|
||||
|
||||
@@ -34,7 +34,11 @@ struct Memory {
|
||||
return Memory{0, 1};
|
||||
}
|
||||
/// \brief Create a Memory accessor distributed across warp threads.
|
||||
SGL_DEVICE static Memory warp(int warp_threads = kWarpThreads) {
|
||||
SGL_DEVICE static Memory warp() {
|
||||
return Memory{get_lane_id(), kWarpThreads};
|
||||
}
|
||||
/// \brief Create a Memory accessor over a narrower warp sub-group.
|
||||
SGL_DEVICE static Memory warp(int warp_threads) {
|
||||
return Memory{static_cast<uint32_t>(threadIdx.x % warp_threads), static_cast<uint32_t>(warp_threads)};
|
||||
}
|
||||
/// \brief Create a Memory accessor distributed across all CTA threads.
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <sgl_kernel/bits.h>
|
||||
#include <sgl_kernel/utils.h>
|
||||
|
||||
#include <dlpack/dlpack.h>
|
||||
@@ -22,6 +23,8 @@
|
||||
|
||||
#include <concepts>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <type_traits>
|
||||
#ifndef USE_ROCM
|
||||
#include <cuda_bf16.h>
|
||||
@@ -108,6 +111,7 @@ namespace device {
|
||||
|
||||
/// \brief Macro: forced-inline device function qualifier.
|
||||
#define SGL_DEVICE __forceinline__ __device__
|
||||
#define SGL_DEVICE_HOST __forceinline__ __device__ __host__
|
||||
|
||||
// Architecture detection: SGL_CUDA_ARCH is injected by load_jit() and is
|
||||
// available in both host and device compilation passes, whereas __CUDA_ARCH__
|
||||
@@ -133,13 +137,42 @@ static_assert(
|
||||
inline constexpr std::size_t kMaxVecBytes = SGL_ARCH_BLACKWELL_OR_GREATER ? 32 : 16;
|
||||
|
||||
/// \brief Number of threads per warp (always 32 on NVIDIA/AMD GPUs).
|
||||
inline constexpr auto kWarpThreads = 32u;
|
||||
/// \brief Full warp active mask (all 32 lanes).
|
||||
inline constexpr uint32_t kWarpThreads = 32u;
|
||||
/// \brief Most implementations prefer this name; keep the alias for them.
|
||||
inline constexpr uint32_t kWarpSize = kWarpThreads;
|
||||
|
||||
/**
|
||||
* \brief This thread's index within its logical `kNumThreads` group.
|
||||
*
|
||||
* \tparam kNumThreads Group width; a power of two, at most 32 on CUDA and at
|
||||
* most 64 (the wave) on HIP -- so `64` is a HIP-only instantiation.
|
||||
*
|
||||
* \note Equals the true in-warp lane only when `blockDim.x` is a multiple of
|
||||
* `kNumThreads`; every caller in this tree satisfies that.
|
||||
* \note On CUDA prefer this over `threadIdx.x % kNumThreads` when the value
|
||||
* feeds an address: `%laneid` is one register read that folds straight into
|
||||
* `IMAD.WIDE`, while the modulo makes ptxas re-derive the mask at every address
|
||||
* scale. Worth 8 instructions in a two-tile warp copy, measured on sm_100a.
|
||||
* That only holds at full width -- a narrower group needs the mask anyway and
|
||||
* ties with the modulo.
|
||||
*/
|
||||
template <uint32_t kNumThreads = kWarpThreads>
|
||||
SGL_DEVICE uint32_t get_lane_id() {
|
||||
#ifndef USE_ROCM
|
||||
inline constexpr auto kFullMask = 0xffffffffu;
|
||||
static_assert(kNumThreads <= 32 && host::is_pow2(kNumThreads));
|
||||
uint32_t lane_id;
|
||||
asm volatile("mov.u32 %0, %%laneid;" : "=r"(lane_id));
|
||||
if constexpr (kNumThreads != 32) lane_id %= kNumThreads;
|
||||
return lane_id;
|
||||
#else
|
||||
inline constexpr auto kFullMask = 0xffffffffffffffffULL;
|
||||
static_assert(kNumThreads <= 64 && host::is_pow2(kNumThreads));
|
||||
// AMD has no lane-id register: `__lane_id()` is computed from the exec mask as
|
||||
// a `v_mbcnt_lo`/`v_mbcnt_hi` pair, and the group mask is still needed on top.
|
||||
// Masking `threadIdx.x` -- already live in v0 -- is 2 instructions cheaper and
|
||||
// yields the same value (measured on gfx950, hipcc 7.0).
|
||||
return threadIdx.x % kNumThreads;
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief PDL (Programmatic Dependent Launch): wait for the primary kernel.
|
||||
@@ -147,6 +180,14 @@ inline constexpr auto kFullMask = 0xffffffffffffffffULL;
|
||||
* On Hopper (sm_90+), inserts a `griddepcontrol.wait` instruction to
|
||||
* synchronize with a preceding kernel in the same stream. On older
|
||||
* architectures or ROCm this is a no-op.
|
||||
*
|
||||
*\note This is the only thing that orders us against the producer. Per the PTX
|
||||
* ISA, `.wait` makes the executing thread wait until every prerequisite grid in
|
||||
* flight has COMPLETED and all of its memory operations are performed and made
|
||||
* visible to this grid -- so it is what a `PDLTriggerSecondary` upstream does
|
||||
* NOT give us. It acts per thread, so every thread that reads producer data has
|
||||
* to execute it; put it ahead of the first such load. Stores into our own output
|
||||
* buffers depend on nothing upstream and may be issued before it.
|
||||
*/
|
||||
template <bool kUsePDL>
|
||||
SGL_DEVICE void PDLWaitPrimary() {
|
||||
@@ -162,6 +203,22 @@ SGL_DEVICE void PDLWaitPrimary() {
|
||||
*
|
||||
* On Hopper (sm_90+), inserts a `griddepcontrol.launch_dependents`
|
||||
* instruction. On older architectures or ROCm this is a no-op.
|
||||
*
|
||||
* \note Scheduling only: this carries no memory ordering of its own. The
|
||||
* dependent becomes eligible to launch once every CTA in this grid has issued
|
||||
* the instruction or has exited, and it may then start before our writes are
|
||||
* visible -- making them visible is the job of `PDLWaitPrimary` on the dependent
|
||||
* side, which is why the programming guide requires the dependent to call it.
|
||||
*
|
||||
* Granularity is the CTA: the PTX ISA states that repeated invocations by
|
||||
* threads of the same CTA have no side effect past the first, so one thread
|
||||
* would do; we call it from all of them because it is free and needs no
|
||||
* predication. Leaving it out altogether is safe and merely late, since the
|
||||
* trigger is implied once every CTA exits (SASS code `PREEXIT`)
|
||||
*
|
||||
* Placing it early therefore costs nothing and only buys the dependent a head
|
||||
* start on the work that does not depend on us. Even that is opportunistic:
|
||||
* concurrent execution is never guaranteed, so nothing may rely on it.
|
||||
*/
|
||||
template <bool kUsePDL>
|
||||
SGL_DEVICE void PDLTriggerSecondary() {
|
||||
@@ -229,6 +286,41 @@ SGL_DEVICE void enable_smem_spilling() {
|
||||
#endif
|
||||
}
|
||||
|
||||
template <typename T, std::size_t N>
|
||||
struct DeviceArray {
|
||||
public:
|
||||
SGL_DEVICE constexpr static std::size_t size() {
|
||||
return N;
|
||||
}
|
||||
SGL_DEVICE constexpr auto operator[](std::size_t idx) -> T& {
|
||||
return m_data[idx];
|
||||
}
|
||||
SGL_DEVICE constexpr auto operator[](std::size_t idx) const -> const T& {
|
||||
return m_data[idx];
|
||||
}
|
||||
SGL_DEVICE constexpr auto data() const -> const T* {
|
||||
return m_data;
|
||||
}
|
||||
SGL_DEVICE constexpr auto data() -> T* {
|
||||
return m_data;
|
||||
}
|
||||
|
||||
private:
|
||||
T m_data[N];
|
||||
};
|
||||
|
||||
/**
|
||||
* Adapted from
|
||||
* https://github.com/deepseek-ai/DeepGEMM/blob/559d79fb6994a58b8a15b4b93bf13ccc16edf247/deep_gemm/include/deep_gemm/common/utils.cuh
|
||||
*/
|
||||
SGL_DEVICE_HOST constexpr uint32_t get_tmem_cols(uint32_t num_cols) {
|
||||
if (num_cols <= 32) return 32;
|
||||
if (num_cols <= 64) return 64;
|
||||
if (num_cols <= 128) return 128;
|
||||
if (num_cols <= 256) return 256;
|
||||
return 512;
|
||||
}
|
||||
|
||||
} // namespace device
|
||||
|
||||
namespace host {
|
||||
|
||||
@@ -118,6 +118,23 @@ struct AlignedVector {
|
||||
storage_t m_storage;
|
||||
};
|
||||
|
||||
/// \brief Maximum vector width for coalesced memory access on GPU.
|
||||
struct LoadStoreBytes {
|
||||
enum type : int64_t {
|
||||
MAX_GMEM = device::kMaxVecBytes, // architecture-dependent
|
||||
MAX_SMEM = 16, // smem only support 16B op
|
||||
MAX_PORTABLE = 16, // general across CUDA/HIP
|
||||
MIN_COALALESCED = 4, // minimum for coalesced access
|
||||
// some common vector widths for load/store
|
||||
RAW_1B = 1,
|
||||
RAW_2B = 2,
|
||||
RAW_4B = 4,
|
||||
RAW_8B = 8,
|
||||
RAW_16B = 16,
|
||||
RAW_32B = 32,
|
||||
}; // namespace device
|
||||
};
|
||||
|
||||
/// Sum `M` vectors element-wise into one, accumulating in fp32 regardless of
|
||||
/// the packed element type. Used by every collective that reduces peer
|
||||
/// contributions in registers.
|
||||
|
||||
@@ -2,55 +2,83 @@
|
||||
/// \brief Warp-level reduction and cooperative-copy primitives.
|
||||
|
||||
#pragma once
|
||||
#include <sgl_kernel/bits.h>
|
||||
#include <sgl_kernel/utils.h>
|
||||
|
||||
#include <sgl_kernel/math.cuh>
|
||||
#include <sgl_kernel/tile.cuh>
|
||||
#include <sgl_kernel/utils.cuh>
|
||||
#include <sgl_kernel/vec.cuh>
|
||||
|
||||
#include <cstdint>
|
||||
#include <numeric>
|
||||
#include <type_traits>
|
||||
|
||||
namespace sglang {
|
||||
|
||||
namespace device::warp {
|
||||
|
||||
/// \brief Full warp active mask.
|
||||
/// \brief Full warp active mask and lane count: 32 on CUDA, wave64 on HIP.
|
||||
#ifndef USE_ROCM
|
||||
static constexpr uint32_t kFullMask = 0xffffffffu;
|
||||
inline constexpr uint32_t kFullMask = 0xffffffffu;
|
||||
inline constexpr uint32_t kFullWidth = 32u;
|
||||
using mask_t = uint32_t;
|
||||
#else
|
||||
static constexpr uint64_t kFullMask = 0xffffffffffffffffULL;
|
||||
inline constexpr uint64_t kFullMask = 0xffffffffffffffffULL;
|
||||
inline constexpr uint32_t kFullWidth = 64u;
|
||||
using mask_t = uint64_t;
|
||||
#endif
|
||||
|
||||
using ::sglang::device::get_lane_id;
|
||||
|
||||
// One elected lane, via elect.sync. Raw PTX rather than cute::elect_one_sync,
|
||||
// which would drag the whole CuTe include path into elementwise JIT modules;
|
||||
// cuda::ptx has no elect_sync in CUDA 13.0. Use this to gate a single-thread
|
||||
// TMA issue instead of a lane-index predicate.
|
||||
SGL_DEVICE bool elect_one_lane() {
|
||||
uint32_t pred;
|
||||
asm volatile(
|
||||
"{\n"
|
||||
" .reg .pred p;\n"
|
||||
" .reg .b32 r;\n"
|
||||
" elect.sync r|p, 0xFFFFFFFF;\n"
|
||||
" selp.b32 %0, 1, 0, p;\n"
|
||||
"}\n"
|
||||
: "=r"(pred));
|
||||
return pred != 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Warp-level reduction.
|
||||
* \brief Warp-level reduction over the lane range spanned by two widths.
|
||||
*
|
||||
* `kStart` and `kFinish` bound a range of lane-index bits: lane `i` reduces with
|
||||
* every lane differing from it only in bits `[log2(lo), log2(hi))`, where `lo`
|
||||
* and `hi` are the smaller and larger of the two. So `<N, 1>` reduces within
|
||||
* contiguous groups of `N` lanes (`0..7, 8..15, ...` for `N = 8`), while
|
||||
* `<kFullWidth, N>` reduces across groups at the same offset (`{0, 8, 16, 24}`).
|
||||
*
|
||||
* On CUDA: uses __shfl_xor_sync with width=32. Full-warp reductions
|
||||
* use a single `redux.sync` instruction when the target supports it.
|
||||
* On HIP: uses __shfl_xor with explicit width parameter (supports wave64 sub-groups).
|
||||
* \tparam OP Reduction operation to perform (SUM, MAX, MIN).
|
||||
* \tparam kNumThreads Number of threads as a group.
|
||||
* \tparam kInner Whether to perform within a group or not.
|
||||
* \tparam kStart One end of the reduced lane range; power of two.
|
||||
* \tparam kFinish The other end of the reduced lane range; power of two.
|
||||
* \tparam T Type of the value to reduce.
|
||||
*
|
||||
* \param value The value to reduce.
|
||||
* \param active_mask The active mask of threads participating in the reduction.
|
||||
*
|
||||
* \note We will divide into groups of `kNumThreads`.
|
||||
* e.g. kNumThreads = 8, we have 0..7, 8..15, 16..23, 24..31 as groups.
|
||||
* By reduction is performed within a group. Inter-group reduction will reduce
|
||||
* over the same offset in different groups. e.g. {0, 8, 16, 24} in the above example.
|
||||
* \note Symmetric: `<kStart, kFinish>` and `<kFinish, kStart>` reduce the same
|
||||
* lane set, only walking the range in the opposite order.
|
||||
* \note On CUDA a whole-warp reduction lowers to a single `redux.sync` where the
|
||||
* target supports it. On HIP the shuffles use `__shfl_xor` with max width 64.
|
||||
*/
|
||||
template <ReductionOp OP, uint32_t kNumThreads = kWarpThreads, bool kInner = true, typename T>
|
||||
template <ReductionOp OP, uint32_t kStart = kWarpThreads, uint32_t kFinish = 1, typename T>
|
||||
SGL_DEVICE T reduce(T value, mask_t active_mask = kFullMask) {
|
||||
static_assert(kNumThreads >= 1 && kNumThreads <= kWarpThreads);
|
||||
static_assert(std::has_single_bit(kNumThreads), "must be pow of 2");
|
||||
static_assert(host::is_pow2(kStart) && host::is_pow2(kFinish));
|
||||
static_assert(kStart <= kFullWidth && kFinish <= kFullWidth);
|
||||
using Trait = ReductionTrait<OP, T>;
|
||||
|
||||
#ifdef SGL_CUDA_ARCH
|
||||
// CUDA target only
|
||||
constexpr bool kFullReduction = (kNumThreads == kWarpThreads && kInner) || (kNumThreads == 1 && !kInner);
|
||||
constexpr bool kFullReduction = (kStart == 1 && kFinish == kFullWidth) || (kStart == kFullWidth && kFinish == 1);
|
||||
if constexpr (kFullReduction) {
|
||||
#if SGL_CUDA_ARCH >= 800
|
||||
// 32 bit integer reduction
|
||||
@@ -81,24 +109,22 @@ SGL_DEVICE T reduce(T value, mask_t active_mask = kFullMask) {
|
||||
}
|
||||
#endif // redux.sync for CUDA only
|
||||
|
||||
if constexpr (kInner) {
|
||||
if constexpr (kStart > kFinish) {
|
||||
#pragma unroll
|
||||
for (uint32_t mask = kNumThreads / 2; mask >= 1; mask >>= 1) {
|
||||
for (uint32_t mask = kStart / 2; mask >= kFinish; mask >>= 1) {
|
||||
#ifndef USE_ROCM
|
||||
value = Trait::reduce(value, __shfl_xor_sync(active_mask, value, mask, 32));
|
||||
value = Trait::reduce(value, __shfl_xor_sync(active_mask, value, mask, kStart));
|
||||
#else
|
||||
value = Trait::reduce(value, __shfl_xor(value, mask, kNumThreads));
|
||||
value = Trait::reduce(value, __shfl_xor(value, mask, kStart));
|
||||
#endif
|
||||
}
|
||||
} else {
|
||||
#pragma unroll
|
||||
for (uint32_t mask = kNumThreads; mask <= kWarpThreads / 2; mask <<= 1) {
|
||||
for (uint32_t mask = kStart; mask <= kFinish / 2; mask <<= 1) {
|
||||
#ifndef USE_ROCM
|
||||
value = Trait::reduce(value, __shfl_xor_sync(active_mask, value, mask, 32));
|
||||
value = Trait::reduce(value, __shfl_xor_sync(active_mask, value, mask, kFinish));
|
||||
#else
|
||||
// Inter-group shuffle crosses kNumThreads-sized sub-groups, so the
|
||||
// shuffle width must span the whole warp.
|
||||
value = Trait::reduce(value, __shfl_xor(value, mask, kWarpThreads));
|
||||
value = Trait::reduce(value, __shfl_xor(value, mask, kFinish));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -106,108 +132,183 @@ SGL_DEVICE T reduce(T value, mask_t active_mask = kFullMask) {
|
||||
}
|
||||
|
||||
/** \brief Warp-level sum reduction. */
|
||||
template <uint32_t kNumThreads = kWarpThreads, bool kInner = true, typename T>
|
||||
template <uint32_t kStart = kWarpThreads, uint32_t kFinish = 1, typename T>
|
||||
SGL_DEVICE T reduce_sum(T value, mask_t active_mask = kFullMask) {
|
||||
return reduce<ReductionOp::SUM, kNumThreads, kInner>(value, active_mask);
|
||||
return reduce<ReductionOp::SUM, kStart, kFinish>(value, active_mask);
|
||||
}
|
||||
|
||||
/** \brief Warp-level max reduction. */
|
||||
template <uint32_t kNumThreads = kWarpThreads, bool kInner = true, typename T>
|
||||
template <uint32_t kStart = kWarpThreads, uint32_t kFinish = 1, typename T>
|
||||
SGL_DEVICE T reduce_max(T value, mask_t active_mask = kFullMask) {
|
||||
return reduce<ReductionOp::MAX, kNumThreads, kInner>(value, active_mask);
|
||||
return reduce<ReductionOp::MAX, kStart, kFinish>(value, active_mask);
|
||||
}
|
||||
|
||||
/** \brief Warp-level min reduction. */
|
||||
template <uint32_t kNumThreads = kWarpThreads, bool kInner = true, typename T>
|
||||
template <uint32_t kStart = kWarpThreads, uint32_t kFinish = 1, typename T>
|
||||
SGL_DEVICE T reduce_min(T value, mask_t active_mask = kFullMask) {
|
||||
return reduce<ReductionOp::MIN, kNumThreads, kInner>(value, active_mask);
|
||||
return reduce<ReductionOp::MIN, kStart, kFinish>(value, active_mask);
|
||||
}
|
||||
|
||||
/// \brief Warp-cooperative gmem -> smem copy of a compile-time byte count.
|
||||
///
|
||||
/// Picks the widest vector width that divides both the per-thread share and
|
||||
/// the byte total. The caller guarantees ``src`` is aligned to the picked
|
||||
/// width (16B for kBytes % (16*32) == 0, else 8/4) and ``dst`` is the start
|
||||
/// of a 16B-aligned per-warp smem slot.
|
||||
// Warp-cooperative byte copy between any two address spaces, vectorised to the
|
||||
// widest unit `kBytes` allows. Named for what it does rather than where it is
|
||||
// used: the MLA call sites happen to target shared memory, but nothing here is
|
||||
// global->shared specific -- no cp.async, no TMA, the payload moves through
|
||||
// registers.
|
||||
//
|
||||
// The strategy was measured against the two async alternatives on B300 (sm_103,
|
||||
// 148 SMs), copying one MLA row per warp out of a 512 MB pool so every row
|
||||
// streams from HBM (grid 296, 64 rows/warp, 50 launches):
|
||||
//
|
||||
// 1152 B/warp (bf16, nope 1024 + rope 128) 576 B/warp (fp8, 512 + 64)
|
||||
// this (generic) 47.3 us 3.69 TB/s 40.8 us 2.14 TB/s
|
||||
// cp.async (ldgsts) 73.9 us 2.36 TB/s 56.4 us 1.55 TB/s
|
||||
// cp.async.bulk/TMA 50.0 us 3.50 TB/s 43.2 us 2.02 TB/s
|
||||
//
|
||||
// The generic path wins at both sizes: a ~1 KB row is too small to amortise
|
||||
// cp.async's per-lane 16 B issues or TMA's fixed issue plus mbarrier round trip.
|
||||
// Revisit if a call site ever copies substantially more than one row per warp.
|
||||
template <int64_t kBytes>
|
||||
SGL_DEVICE void copy_bytes(const void* __restrict__ src, void* __restrict__ dst) {
|
||||
constexpr int64_t kAlignment = (kBytes % (16 * kWarpThreads) == 0) ? 16
|
||||
: (kBytes % (8 * kWarpThreads) == 0) ? 8
|
||||
: (kBytes % (4 * kWarpThreads) == 0) ? 4
|
||||
: (kBytes % 4 == 0) ? 4
|
||||
: 0;
|
||||
static_assert(kAlignment > 0, "kBytes must be a multiple of 4");
|
||||
|
||||
using vec_t = AlignedStorage<uint32_t, kAlignment / 4>;
|
||||
constexpr auto kLoopBytes = sizeof(vec_t) * kWarpThreads;
|
||||
constexpr auto kLoopCount = kBytes / kLoopBytes;
|
||||
constexpr int64_t kTailVecs = (kBytes - kLoopCount * kLoopBytes) / sizeof(vec_t);
|
||||
|
||||
const auto gmem = tile::Memory<vec_t>::warp();
|
||||
|
||||
/**
|
||||
* \brief Inclusive scan within each segment of `kWidth` lanes.
|
||||
*
|
||||
* Distinct from `reduce` above: every lane keeps its own running total rather
|
||||
* than the whole-group result. Scans forward when `kStart < kFinish` (lane `p`
|
||||
* accumulates lanes `<= p`) and backward when `kStart > kFinish` (lane `p`
|
||||
* accumulates lanes `>= p`). The default `<kWidth, 1, kWidth>` is a plain
|
||||
* forward scan over the whole segment.
|
||||
*
|
||||
* \tparam OP Reduction operation to combine with (SUM, MAX, MIN).
|
||||
* \tparam kWidth Segment size; a scan never crosses a segment boundary.
|
||||
* \tparam kStart First shuffle offset; power of two. `kStart > 1` assumes the
|
||||
* input is already scanned in blocks of `kStart`, and scans the strided
|
||||
* subsequences instead.
|
||||
* \tparam kFinish Exclusive bound on the shuffle offset; power of two.
|
||||
* \tparam T Type of the value to scan.
|
||||
*
|
||||
* \param val The value to scan.
|
||||
* \param lane_id This thread's index WITHIN its segment, i.e.
|
||||
* `threadIdx.x % kWidth` -- not `% kWarpThreads`. The shuffles are
|
||||
* segment-relative but this predicate is not, so a warp-relative id silently
|
||||
* corrupts every segment past the first whenever `kWidth < kWarpThreads`.
|
||||
* \param active_mask The active mask of threads participating in the scan.
|
||||
*
|
||||
* \note The backward direction accumulates over `[p, kStart)`, so it covers the
|
||||
* whole segment only when `kStart == kWidth`.
|
||||
*/
|
||||
template <ReductionOp OP, uint32_t kWidth = kWarpThreads, uint32_t kStart = 1, uint32_t kFinish = kWidth, typename T>
|
||||
SGL_DEVICE T inclusive_reduce(T val, uint32_t lane_id = get_lane_id<kWidth>(), mask_t active_mask = kFullMask) {
|
||||
static_assert(host::is_pow2(kStart) && host::is_pow2(kFinish));
|
||||
static_assert(kStart <= kWidth && kFinish <= kWidth && kWidth <= kFullWidth);
|
||||
using Trait = ReductionTrait<OP, T>;
|
||||
if constexpr (kStart < kFinish) {
|
||||
#pragma unroll
|
||||
for (int64_t i = 0; i < kLoopCount; ++i) {
|
||||
const auto v = gmem.load(src, i);
|
||||
gmem.store(dst, v, i);
|
||||
}
|
||||
if constexpr (kTailVecs > 0) {
|
||||
if (gmem.in_bound(kLoopCount * kWarpThreads + kTailVecs, kLoopCount)) {
|
||||
const auto v = gmem.load(src, kLoopCount);
|
||||
gmem.store(dst, v, kLoopCount);
|
||||
for (uint32_t offset = kStart; offset < kFinish; offset *= 2) {
|
||||
const auto n = __shfl_up_sync(active_mask, val, offset, kWidth);
|
||||
if (lane_id >= offset) val = Trait::reduce(val, n);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Inclusive prefix sum across one warp, thread-rank order. Distinct from
|
||||
/// reduce_sum above: every lane keeps its own running total rather than the
|
||||
/// whole-warp result.
|
||||
SGL_DEVICE uint32_t inclusive_sum(uint32_t lane_id, uint32_t val) {
|
||||
static_assert(kWarpThreads == 32);
|
||||
} else {
|
||||
#pragma unroll
|
||||
for (uint32_t offset = 1; offset < 32; offset *= 2) {
|
||||
#ifndef USE_ROCM
|
||||
uint32_t n = __shfl_up_sync(0xFFFFFFFF, val, offset);
|
||||
#else
|
||||
uint32_t n = __shfl_up_sync(kFullMask, val, offset, kWarpThreads);
|
||||
#endif
|
||||
if (lane_id >= offset) val += n;
|
||||
for (uint32_t offset = kFinish; offset < kStart; offset *= 2) {
|
||||
const auto n = __shfl_down_sync(active_mask, val, offset, kWidth);
|
||||
if (lane_id < kStart - offset) val = Trait::reduce(val, n);
|
||||
}
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
// One elected lane, via elect.sync. Raw PTX rather than cute::elect_one_sync,
|
||||
// which would drag the whole CuTe include path into elementwise JIT modules;
|
||||
// cuda::ptx has no elect_sync in CUDA 13.0. Use this to gate a single-thread
|
||||
// TMA issue instead of a lane-index predicate.
|
||||
SGL_DEVICE bool elect_one_lane() {
|
||||
uint32_t pred;
|
||||
asm volatile(
|
||||
"{\n"
|
||||
" .reg .pred p;\n"
|
||||
" .reg .b32 r;\n"
|
||||
" elect.sync r|p, 0xFFFFFFFF;\n"
|
||||
" selp.b32 %0, 1, 0, p;\n"
|
||||
"}\n"
|
||||
: "=r"(pred));
|
||||
return pred != 0;
|
||||
template <uint32_t kWidth = kWarpThreads, uint32_t kStart = 1, uint32_t kFinish = kWidth, typename T>
|
||||
SGL_DEVICE T inclusive_sum(T val, uint32_t lane_id = get_lane_id<kWidth>(), mask_t active_mask = kFullMask) {
|
||||
return inclusive_reduce<ReductionOp::SUM, kWidth, kStart, kFinish>(val, lane_id, active_mask);
|
||||
}
|
||||
|
||||
template <uint32_t kWidth = kWarpThreads, uint32_t kStart = 1, uint32_t kFinish = kWidth, typename T>
|
||||
SGL_DEVICE T inclusive_max(T val, uint32_t lane_id = get_lane_id<kWidth>(), mask_t active_mask = kFullMask) {
|
||||
return inclusive_reduce<ReductionOp::MAX, kWidth, kStart, kFinish>(val, lane_id, active_mask);
|
||||
}
|
||||
|
||||
template <uint32_t kWidth = kWarpThreads, uint32_t kStart = 1, uint32_t kFinish = kWidth, typename T>
|
||||
SGL_DEVICE T inclusive_min(T val, uint32_t lane_id = get_lane_id<kWidth>(), mask_t active_mask = kFullMask) {
|
||||
return inclusive_reduce<ReductionOp::MIN, kWidth, kStart, kFinish>(val, lane_id, active_mask);
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Broadcast one lane's value to every lane of its `kWidth` segment.
|
||||
* \param src_lane The source's index WITHIN the segment, i.e. in `[0, kWidth)`;
|
||||
* each segment reads its own lane `src_lane`, not a single warp-wide one.
|
||||
*/
|
||||
template <uint32_t kWidth = kWarpThreads, typename T>
|
||||
SGL_DEVICE T broadcast(T value, uint32_t src_lane, mask_t active_mask = kFullMask) {
|
||||
static_assert(host::is_pow2(kWidth) && kWidth <= kFullWidth);
|
||||
return __shfl_sync(active_mask, value, src_lane, kWidth);
|
||||
}
|
||||
|
||||
namespace details {
|
||||
|
||||
// array for load & store operations
|
||||
template <typename T, std::size_t N, int64_t kBytes>
|
||||
struct Array : public DeviceArray<T, N> {
|
||||
static_assert(alignof(T) == sizeof(T));
|
||||
static constexpr int64_t kVecBytes = static_cast<int64_t>(sizeof(T)) * N;
|
||||
};
|
||||
|
||||
} // namespace details
|
||||
|
||||
template <int64_t kBytes, int64_t kVecBytes>
|
||||
struct CopyTrait {
|
||||
static_assert(kBytes % kVecBytes == 0 && kBytes > 0);
|
||||
using vec_t = AlignedStorage<uint8_t, kVecBytes>;
|
||||
static constexpr int64_t kLoopBytes = sizeof(vec_t) * kWarpThreads;
|
||||
static constexpr int64_t kLoopCount = kBytes / kLoopBytes;
|
||||
static constexpr int64_t kTailBytes = kBytes - kLoopCount * kLoopBytes;
|
||||
static constexpr int64_t kTailVecs = kTailBytes / sizeof(vec_t);
|
||||
using result_t = details::Array<vec_t, kLoopCount + (kTailVecs > 0 ? 1 : 0), kBytes>;
|
||||
|
||||
template <typename F>
|
||||
SGL_DEVICE static void for_each(F&& f) {
|
||||
const auto mem = tile::Memory<vec_t>::warp();
|
||||
#pragma unroll
|
||||
for (int64_t i = 0; i < kLoopCount; ++i) {
|
||||
f(mem, i);
|
||||
}
|
||||
if constexpr (kTailVecs > 0) {
|
||||
if (mem.in_bound(kBytes / sizeof(vec_t), kLoopCount)) {
|
||||
f(mem, kLoopCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SGL_DEVICE static result_t load(const void* src) {
|
||||
result_t result;
|
||||
for_each([&](const auto& mem, int64_t i) { result[i] = mem.load(src, i); });
|
||||
return result;
|
||||
}
|
||||
|
||||
SGL_DEVICE static void store(void* dst, const result_t& result) {
|
||||
for_each([&](const auto& mem, int64_t i) { mem.store(dst, result[i], i); });
|
||||
}
|
||||
};
|
||||
|
||||
struct LoadStorePattern {
|
||||
using enum LoadStoreBytes::type;
|
||||
enum type : int64_t {
|
||||
WARP_UNIFORM_GMEM = -MAX_GMEM,
|
||||
WARP_UNIFORM_SMEM = -MAX_SMEM,
|
||||
WARP_UNIFORM_4B = -4,
|
||||
WARP_UNIFORM_8B = -8,
|
||||
WARP_UNIFORM_16B = -16,
|
||||
WARP_UNIFORM_32B = -32,
|
||||
};
|
||||
|
||||
template <int64_t kBytes, int64_t kMaxVecBytes>
|
||||
SGL_DEVICE_HOST static constexpr int64_t get_vec_bytes() {
|
||||
if constexpr (kMaxVecBytes < 0) { // best-effort warp uniform load/store
|
||||
if constexpr (kBytes % (4 * device::kWarpThreads) != 0) {
|
||||
// at least guarantee 128B coalesced for better performance
|
||||
return std::gcd(kBytes, 4);
|
||||
} else { // kBytes is at least 128B coalesced
|
||||
return std::gcd(kBytes / device::kWarpThreads, -kMaxVecBytes);
|
||||
}
|
||||
} else {
|
||||
return std::gcd(kBytes, kMaxVecBytes);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template <
|
||||
int64_t kBytes,
|
||||
int64_t kMaxVecBytes = LoadStorePattern::MAX_GMEM,
|
||||
int64_t kVecBytes = LoadStorePattern::get_vec_bytes<kBytes, kMaxVecBytes>()>
|
||||
SGL_DEVICE auto load_bytes(const void* src) {
|
||||
return CopyTrait<kBytes, kVecBytes>::load(src);
|
||||
}
|
||||
|
||||
template <
|
||||
int64_t kBytes,
|
||||
int64_t kMaxVecBytes = LoadStorePattern::MAX_GMEM,
|
||||
int64_t kVecBytes = LoadStorePattern::get_vec_bytes<kBytes, kMaxVecBytes>()>
|
||||
SGL_DEVICE void store_bytes(void* dst, const auto& result) {
|
||||
return CopyTrait<kBytes, kVecBytes>::store(dst, result);
|
||||
}
|
||||
|
||||
} // namespace device::warp
|
||||
|
||||
@@ -86,6 +86,14 @@ def load_jit(
|
||||
if flag not in ("--use_fast_math", "-use_fast_math")
|
||||
]
|
||||
|
||||
if envs.SGLANG_JIT_LOG_RESOURCE_USAGE.get():
|
||||
# nvcc reports through ptxas; hipcc through a clang remark pass.
|
||||
extra_cuda_cflags = list(extra_cuda_cflags or []) + (
|
||||
["-Rpass-analysis=kernel-resource-usage"]
|
||||
if is_hip_runtime()
|
||||
else ["-Xptxas=-v"]
|
||||
)
|
||||
|
||||
includes = list(DEFAULT_INCLUDE) + (extra_include_paths or [])
|
||||
for dep in sorted(set(extra_dependencies or [])):
|
||||
if dep not in REGISTERED_DEPENDENCIES:
|
||||
@@ -113,7 +121,7 @@ def load_jit(
|
||||
build_key = cache.compute_build_key(spec, build_file=build_file)
|
||||
scope = cache.build_key_dir(module_name=spec.module_name, build_key=build_key)
|
||||
|
||||
prebuilt = cache.find_prebuilt(scope=scope, module_name=spec.module_name)
|
||||
prebuilt = _find_prebuilt(spec=spec, scope=scope)
|
||||
if prebuilt is not None:
|
||||
try:
|
||||
return _load(prebuilt)
|
||||
@@ -141,7 +149,7 @@ def load_jit(
|
||||
# published exactly what we were about to build. This is what turns N
|
||||
# tensor-parallel ranks starting together into one compile plus N-1
|
||||
# cache hits instead of N identical compiles.
|
||||
prebuilt = cache.find_prebuilt(scope=scope, module_name=spec.module_name)
|
||||
prebuilt = _find_prebuilt(spec=spec, scope=scope)
|
||||
if prebuilt is not None:
|
||||
try:
|
||||
return _load(prebuilt)
|
||||
@@ -182,6 +190,18 @@ def load_jit(
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
|
||||
|
||||
def _find_prebuilt(*, spec: BuildSpec, scope: pathlib.Path) -> pathlib.Path | None:
|
||||
"""The cached build to reuse, or None when there is nothing to reuse.
|
||||
|
||||
Returns None unconditionally under `SGLANG_JIT_FORCE_RECOMPILE`, which is
|
||||
what makes the compiler run again. Both lookups go through here, so the flag
|
||||
cannot take effect on the fast path and not on the one behind the lock.
|
||||
"""
|
||||
if envs.SGLANG_JIT_FORCE_RECOMPILE.get():
|
||||
return None
|
||||
return cache.find_prebuilt(scope=scope, module_name=spec.module_name)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _build_lock(scope: pathlib.Path):
|
||||
"""Serialize builds of one module variant across processes.
|
||||
|
||||
@@ -28,6 +28,7 @@ from typing import List
|
||||
|
||||
from sglang.kernels.jit.utils.compile import toolchain
|
||||
from sglang.kernels.jit.utils.compile.spec import BuildSpec
|
||||
from sglang.srt.environ import envs
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -169,9 +170,38 @@ def build(*, spec: BuildSpec, build_dir: pathlib.Path, build_file: str) -> pathl
|
||||
f"Failed to build JIT module {spec.module_name} in {build_dir}\n"
|
||||
f"stdout:\n{completed.stdout}\nstderr:\n{completed.stderr}"
|
||||
)
|
||||
if envs.SGLANG_JIT_LOG_RESOURCE_USAGE.get():
|
||||
_log_resource_usage(
|
||||
spec.module_name, (completed.stdout or "") + (completed.stderr or "")
|
||||
)
|
||||
return build_dir / f"{spec.module_name}.so"
|
||||
|
||||
|
||||
# What the two device compilers call their resource report. nvcc routes it
|
||||
# through ptxas; hipcc emits clang remarks naming the analysis pass.
|
||||
_RESOURCE_MARKERS = ("ptxas info", "spill", "kernel-resource-usage")
|
||||
|
||||
|
||||
def _log_resource_usage(module_name: str, output: str) -> None:
|
||||
"""Replay the compiler's per-kernel resource report.
|
||||
|
||||
The build runs with `capture_output=True` and replays only on failure, so
|
||||
without this the report is produced and then dropped. Ninja forwards each
|
||||
subcommand's diagnostics onto its own stdout, so that is where this lands
|
||||
regardless of which stream the compiler wrote to.
|
||||
"""
|
||||
report = [
|
||||
line.rstrip()
|
||||
for line in output.splitlines()
|
||||
# The echoed compile command also contains the flag that asked for the
|
||||
# report, so match on the output's own markers, not on the flag.
|
||||
if not line.startswith("[") and any(m in line for m in _RESOURCE_MARKERS)
|
||||
]
|
||||
if not report:
|
||||
return
|
||||
logger.info("JIT resource usage for %s:\n%s", module_name, "\n".join(report))
|
||||
|
||||
|
||||
def scan_dependencies(build_dir: pathlib.Path) -> List[pathlib.Path]:
|
||||
"""Every file the compiler read, taken from the depfiles the build left.
|
||||
|
||||
|
||||
@@ -16,10 +16,30 @@ from sglang.srt.utils.custom_op import register_custom_op
|
||||
if TYPE_CHECKING:
|
||||
from tvm_ffi.module import Module
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Mirrors device::kWarpThreads in include/sgl_kernel/utils.cuh (32 on CUDA and HIP).
|
||||
_WARP_THREADS = 32
|
||||
|
||||
|
||||
@cache_once
|
||||
def _jit_kvcache_module(k_row_bytes: int, v_row_bytes: int) -> Module:
|
||||
args = make_cpp_args(k_row_bytes, v_row_bytes, is_arch_support_pdl())
|
||||
def _jit_kvcache_module(k_row_bytes: int, v_row_bytes: int, num_threads: int) -> Module:
|
||||
if num_threads == 0:
|
||||
num_threads = 32
|
||||
# rare case. just don't optimize it
|
||||
if k_row_bytes % num_threads != 0 or v_row_bytes % num_threads != 0:
|
||||
return _jit_kvcache_module(k_row_bytes, v_row_bytes, num_threads)
|
||||
k_bytes = k_row_bytes / num_threads
|
||||
v_bytes = v_row_bytes / num_threads
|
||||
# increase threads if row is too large
|
||||
while k_bytes % 8 == 0 and v_bytes % 8 == 0 and (k_bytes + v_bytes) >= 64:
|
||||
num_threads *= 2
|
||||
k_bytes /= 2
|
||||
v_bytes /= 2
|
||||
logger.debug(f"Heuristic {num_threads = } for {k_row_bytes = }, {v_row_bytes}")
|
||||
return _jit_kvcache_module(k_row_bytes, v_row_bytes, num_threads)
|
||||
|
||||
args = make_cpp_args(k_row_bytes, v_row_bytes, num_threads, is_arch_support_pdl())
|
||||
return load_jit(
|
||||
"kvcache",
|
||||
*args,
|
||||
@@ -29,20 +49,14 @@ def _jit_kvcache_module(k_row_bytes: int, v_row_bytes: int) -> Module:
|
||||
|
||||
|
||||
@cache_once
|
||||
def can_use_store_cache(k_row_bytes: int, v_row_bytes: int = 0) -> bool:
|
||||
def can_use_store_cache(
|
||||
k_row_bytes: int, v_row_bytes: int = 0, num_threads: int = 0
|
||||
) -> bool:
|
||||
"""Whether the JIT store_cache kernel can serve these row widths.
|
||||
v_row_bytes=0 means symmetric, i.e. it defaults to k_row_bytes."""
|
||||
logger = logging.getLogger(__name__)
|
||||
v_row_bytes = v_row_bytes or k_row_bytes
|
||||
for name, size in (("k_row_bytes", k_row_bytes), ("v_row_bytes", v_row_bytes)):
|
||||
if size % 4 != 0:
|
||||
logger.warning(
|
||||
f"Unsupported {name}={size} for JIT KV-Cache kernel:"
|
||||
" must be multiple of 4"
|
||||
)
|
||||
return False
|
||||
try:
|
||||
_jit_kvcache_module(k_row_bytes, v_row_bytes)
|
||||
_jit_kvcache_module(k_row_bytes, v_row_bytes, num_threads)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
@@ -62,7 +76,7 @@ def store_cache(
|
||||
*,
|
||||
row_bytes: int = 0,
|
||||
v_row_bytes: int = 0,
|
||||
num_split: int = 0, # can be tuned for performance
|
||||
num_split: int = 0,
|
||||
size_limit: int = 0,
|
||||
reserved_skip_index: int = 0,
|
||||
) -> None:
|
||||
@@ -77,6 +91,8 @@ def store_cache(
|
||||
row_bytes (int): Key row width in bytes. Inferred from k when 0.
|
||||
v_row_bytes (int): Value row width in bytes; differs from row_bytes for
|
||||
asymmetric KV (head_dim != v_head_dim). Inferred from v when 0.
|
||||
num_split (int): Warps cooperating on one row. A heuristic picks it
|
||||
when 0; it is the only knob here that exists purely for tuning.
|
||||
size_limit (int): Valid slot bound (cache row count = real slots + the
|
||||
reserved padding slot); an index outside [0, size_limit) fails fast
|
||||
(device assert) instead of an illegal memory access. Defaults to the
|
||||
@@ -87,15 +103,10 @@ def store_cache(
|
||||
"""
|
||||
row_bytes = row_bytes or k.shape[-1] * k.element_size()
|
||||
v_row_bytes = v_row_bytes or v.shape[-1] * v.element_size()
|
||||
module = _jit_kvcache_module(row_bytes, v_row_bytes)
|
||||
if num_split <= 0:
|
||||
# A split must divide BOTH rows, so require the alignment on each.
|
||||
if row_bytes % 2048 == 0 and v_row_bytes % 2048 == 0:
|
||||
num_split = 4
|
||||
elif row_bytes % 1024 == 0 and v_row_bytes % 1024 == 0:
|
||||
num_split = 2
|
||||
else:
|
||||
num_split = 1
|
||||
# One warp per split. The knob stays the split count it has always been:
|
||||
# renaming it changes the registered op schema, and a warm inductor cache
|
||||
# does not notice that -- it replays generated code carrying the old name.
|
||||
module = _jit_kvcache_module(row_bytes, v_row_bytes, num_split * _WARP_THREADS)
|
||||
if size_limit <= 0:
|
||||
size_limit = k_cache.shape[0]
|
||||
module.store_cache(
|
||||
@@ -104,7 +115,6 @@ def store_cache(
|
||||
k_cache,
|
||||
v_cache,
|
||||
indices,
|
||||
num_split,
|
||||
size_limit,
|
||||
reserved_skip_index,
|
||||
)
|
||||
|
||||
@@ -117,13 +117,6 @@ def set_mla_kv_buffer_kernel_norope(
|
||||
tl.extra.cuda.gdc_launch_dependents()
|
||||
|
||||
|
||||
# Above this loc count the TMA bulk-store path overtakes the single-CTA-per-loc
|
||||
# Triton kernel. Below it, Triton with BLOCK = next_pow2(total_dim) (one CTA
|
||||
# does the whole row in one tile, no boundary fan-out) is the winning fallback.
|
||||
# Tuned on GB300 with DSv4 row widths.
|
||||
_TMA_BULK_STORE_MIN_LOCS = 768
|
||||
|
||||
|
||||
def _set_mla_kv_buffer_impl(
|
||||
kv_buffer: torch.Tensor,
|
||||
loc: torch.Tensor,
|
||||
@@ -136,19 +129,16 @@ def _set_mla_kv_buffer_impl(
|
||||
):
|
||||
"""Dispatch MLA paged-KV scatter writes to the fastest available path.
|
||||
|
||||
Two paths, chosen on ``n_loc``:
|
||||
Two paths:
|
||||
|
||||
- ``n_loc >= 768`` (and SM90+ with TMA-compatible row widths): JIT CUDA
|
||||
kernel where each warp loads one (nope, rope) row into shared memory and
|
||||
issues a single ``cp.async.bulk.global.shared::cta`` store to scatter the
|
||||
row at ``kv_buffer[loc[item]]``. Wins at large bs because it packs 4-8
|
||||
items per CTA, drastically reducing the CTA count vs single-CTA-per-loc.
|
||||
- SM90+ with TMA-compatible row widths: JIT CUDA kernel where each warp
|
||||
loads one (nope, rope) row into shared memory and issues a single
|
||||
``cp.async.bulk.global.shared::cta`` store to scatter the row at
|
||||
``kv_buffer[loc[item]]``. It packs 4-8 items per CTA, so the CTA count
|
||||
falls well below single-CTA-per-loc.
|
||||
- Otherwise: Triton kernel with ``BLOCK = next_pow2(nope_dim + rope_dim)``,
|
||||
i.e. one CTA per loc covering the entire row in one tile. Wins at small
|
||||
bs because there's no per-loc CTA fan-out (5x fewer CTAs than the old
|
||||
BLOCK=128 dispatch) and the row-spanning block makes the boundary branch
|
||||
a one-shot per CTA. This is also the path for SM<90 and for shapes that
|
||||
violate the TMA 16-byte alignment.
|
||||
i.e. one CTA per loc covering the entire row in one tile. This is the
|
||||
path for SM<90 and for shapes that violate the TMA 16-byte alignment.
|
||||
|
||||
Speedup vs the legacy BLOCK=128 Triton kernel on GB300 (BF16, nope=512,
|
||||
rope=64): ~1.05x at bs=8, ~1.5x at bs=128, 3.5x at bs=512, **11.7x at
|
||||
@@ -195,8 +185,7 @@ def _set_mla_kv_buffer_impl(
|
||||
nope_bytes = cache_k_nope.shape[-1] * cache_k_nope.element_size()
|
||||
rope_bytes = cache_k_rope.shape[-1] * cache_k_rope.element_size()
|
||||
if (
|
||||
n_loc >= _TMA_BULK_STORE_MIN_LOCS
|
||||
and is_arch_support_pdl()
|
||||
is_arch_support_pdl()
|
||||
and can_use_set_mla_kv_buffer(nope_bytes, rope_bytes)
|
||||
and dcp_world_size == 1
|
||||
):
|
||||
|
||||
@@ -41,27 +41,7 @@ def set_mla_kv_buffer_module(nope_bytes: int, rope_bytes: int, use_pdl: bool) ->
|
||||
|
||||
@cache_once
|
||||
def can_use_set_mla_kv_buffer(nope_bytes: int, rope_bytes: int) -> bool:
|
||||
"""Whether the TMA path can be used for these row byte widths.
|
||||
|
||||
TMA bulk store requires ``(nope_bytes + rope_bytes)`` to be a multiple of
|
||||
16; both halves individually must also be a multiple of 4 (the warp-coop
|
||||
smem load lower bound).
|
||||
"""
|
||||
if nope_bytes % 4 != 0 or rope_bytes % 4 != 0:
|
||||
logger.warning(
|
||||
"Unsupported nope_bytes=%d rope_bytes=%d for JIT set_mla_kv_buffer:"
|
||||
" both must be multiples of 4",
|
||||
nope_bytes,
|
||||
rope_bytes,
|
||||
)
|
||||
return False
|
||||
if (nope_bytes + rope_bytes) % 16 != 0:
|
||||
logger.warning(
|
||||
"Unsupported nope_bytes=%d rope_bytes=%d for JIT set_mla_kv_buffer:"
|
||||
" (nope_bytes + rope_bytes) must be a multiple of 16 for TMA bulk store",
|
||||
nope_bytes,
|
||||
rope_bytes,
|
||||
)
|
||||
if (rope_bytes + nope_bytes) % 16 != 0:
|
||||
return False
|
||||
try:
|
||||
set_mla_kv_buffer_module(nope_bytes, rope_bytes, is_arch_support_pdl())
|
||||
|
||||
@@ -1186,6 +1186,13 @@ class Envs:
|
||||
# SGLANG_CACHE_DIR; set to an empty string to keep compilation
|
||||
# process-local. Must be trusted: cached objects are loaded into the process.
|
||||
SGLANG_CUTE_AOT_CACHE_DIR = EnvStr(lambda: _default_cache_subdir("cute_aot"))
|
||||
|
||||
# ===================================================================
|
||||
# Kernel development: JIT build cache, diagnostics and benchmarks
|
||||
# ===================================================================
|
||||
# Everything here is a developer knob for working ON kernels -- building
|
||||
# them, inspecting what the compiler produced, and benchmarking them. Flags
|
||||
# that select a kernel in production live with their own feature instead.
|
||||
# JIT kernel build cache. None = unset, resolving to ~/.cache/sglang/jit;
|
||||
# point it at a persistent mount to share builds across CI jobs.
|
||||
SGLANG_JIT_CACHE_DIR = EnvStr(None)
|
||||
@@ -1195,10 +1202,23 @@ class Envs:
|
||||
# is what makes reverting an edit an instant hit instead of a rebuild; set
|
||||
# it to trade that away for disk (1 keeps only the most recent build).
|
||||
SGLANG_JIT_CACHE_KEEP = EnvInt(None)
|
||||
# Skip the cache lookup and run the compiler for every module this process
|
||||
# loads. The result is still published, so the cost is one rebuild per
|
||||
# module, not one per load.
|
||||
SGLANG_JIT_FORCE_RECOMPILE = EnvBool(False)
|
||||
# Raise instead of compiling when a module misses the cache, so a
|
||||
# deployment that expects a pre-seeded cache fails loudly at startup
|
||||
# rather than silently eating a cold compile.
|
||||
SGLANG_CRASH_ON_JIT_COMPILE = EnvBool(False)
|
||||
# Ask the device compiler for per-kernel resource usage (registers, spills,
|
||||
# shared memory) and log it at INFO. Changes the build flags, so it compiles
|
||||
# into its own cache entry and leaves the normal one alone -- but that entry
|
||||
# is a hit on the second run, and a cache hit has nothing to report, so pair
|
||||
# this with SGLANG_JIT_FORCE_RECOMPILE to see the report every time.
|
||||
SGLANG_JIT_LOG_RESOURCE_USAGE = EnvBool(False)
|
||||
# Drop the GB/s and TFLOPS columns from the benchmark marker's table.
|
||||
SGLANG_JIT_BENCHMARK_DISABLE_LOG_BANDWIDTH = EnvBool(False)
|
||||
SGLANG_JIT_BENCHMARK_DISABLE_LOG_FLOPS = EnvBool(False)
|
||||
|
||||
# ===================================================================
|
||||
# Expert-parallel dispatch and MoE execution
|
||||
|
||||
@@ -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