[ROCm][Bugfix][Perf] Tune multi-stream shared experts use; wvSplitKrc fixes (#56098)
Signed-off-by: Matthew Wong <Matthew.Wong2@amd.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Matt committed
Sep 10, 2026 at 15:14 UTC
9163190dda009a310d8c175d63860ac7c671ca90
6 files changed
+369
-77
csrc/rocm/skinny_gemms.cu
+216
-32
@@ -8,12 +8,20 @@
8
9
#include <stdexcept>
10
#include <algorithm>
11
+#include <map>
12
+#include <mutex>
13
+#include <utility>
14
15
#include "../cuda_compat.h"
16
#include "dispatch_utils.h"
17
#include "quantization/w8a8/fp8/common.cuh"
18
#include "core/batch_invariant.hpp"
19
20
+// Number of streams the pre-allocated split-K pool covers. ~7.5 MiB per slot.
21
+static constexpr int64_t kWvSlots = 8;
22
+// Slots held back for capturing streams, which cannot allocate one themselves.
23
+static constexpr int64_t kWvCaptureSlots = 8;
24
+
25
// TODO(rasmith): The kernels in this file are susceptible to integer overflow
26
// issues, do not take strides, and are unable to handle PyTorch tensors that
27
// return is_contiguous() as False (the tensors may actually be contiguous
@@ -1364,9 +1372,18 @@ __global__ void __launch_bounds__(WvPrGrp* THRDS)
1372
};
1373
using big4 = __attribute__((__vector_size__(4 * sizeof(bigType)))) __bf16;
1374
1367
- __shared__ scalar_t stg[WvPrGrp * WVLDS / GrpsShrB];
1375
+ // The main-loop staging areas are separated from the split-K readback by a
1376
+ // __syncthreads(), so the readback may reuse all of LDS.
1377
+ __shared__ union {
1378
+ struct {
1379
+ scalar_t stg[WvPrGrp * WVLDS / GrpsShrB];
1380
+ scalar_t s[max_lds_len - WvPrGrp * WVLDS / GrpsShrB];
1381
+ } mainloop;
1382
+ scalar_t all[max_lds_len];
1383
+ } lds;
1384
+ auto& stg = lds.mainloop.stg;
1385
+ auto& s = lds.mainloop.s;
1386
unsigned int* myStg = (unsigned int*)(&stg[WVLDS * (threadIdx.y / GrpsShrB)]);
1369
- __shared__ scalar_t s[max_lds_len - WvPrGrp * WVLDS / GrpsShrB];
1387
1388
#ifndef WVSPLITKRC_1KPASS
1389
constexpr int TUC_ = (THRDS * UNRL * A_CHUNK);
@@ -1682,33 +1699,69 @@ __global__ void __launch_bounds__(WvPrGrp* THRDS)
1699
if (my_cntr + 1 == k_rnd) {
1700
cntr[adr_] = 0; // clear for next round
1701
if constexpr (DTRMNSTC) {
1702
+ constexpr uint32_t NT = N / NTILE / GrpsShrB;
1703
+ // The readback stages k-shard partials through LDS, which bounds how
1704
+ // many shards may be in flight at once.
1705
+ constexpr uint32_t LDS_F4 =
1706
+ max_lds_len * sizeof(scalar_t) / sizeof(float4);
1707
+ constexpr uint32_t KSBMAX = LDS_F4 / (THRDS * 4 * NT);
1708
+ static_assert(KSBMAX >= 1, "LDS cannot stage even one k-shard");
1709
+ const uint32_t KSB = (k_rnd < KSBMAX) ? k_rnd : KSBMAX;
1710
+
1711
+ // First batch is peeled; it is the only batch for most shapes.
1712
#pragma unroll
1686
- for (int ks = 0; ks < k_rnd; ks++) {
1687
- for (uint32_t nt = 0; nt < N / NTILE / GrpsShrB; nt++) {
1713
+ for (uint32_t kb = 0; kb < KSB; kb++) {
1714
+ for (uint32_t nt = 0; nt < NT; nt++) {
1715
int g_nindx =
1716
(nt * NTILE + (N / GrpsShrB) * (threadIdx.y % GrpsShrB)) / 4;
1717
int g_adr = g_mindx * 4 + 0 + M * g_nindx * 4;
1718
__builtin_amdgcn_global_load_lds(
1692
- (float4*)(&glbl[g_adr + M * N * ks]),
1693
- &(((float4*)s)[(threadIdx.y * THRDS) + ks * THRDS * 4 +
1694
- nt * THRDS * 4 * k_rnd]),
1719
+ (float4*)(&glbl[g_adr + M * N * kb]),
1720
+ &(((float4*)lds.all)[(threadIdx.y * THRDS) + kb * THRDS * 4 +
1721
+ nt * THRDS * 4 * KSB]),
1722
16, 0, 0);
1723
}
1724
}
1698
- if (BIAS)
1699
- for (uint32_t nt = 0; nt < N / NTILE / GrpsShrB; nt++) {
1725
+ if (BIAS) // overlaps the DMAs above
1726
+ for (uint32_t nt = 0; nt < NT; nt++) {
1727
for (uint32_t j = 0; j < 4; j++) {
1728
int nindx = (j + (threadIdx.x / 16) * 4) + nt * NTILE +
1729
(N / GrpsShrB) * (threadIdx.y % GrpsShrB);
1730
biases[nt][j] = BIAS[(mindx % Bx) + (nindx % By) * Bx];
1731
}
1732
}
1706
- asm volatile("s_waitcnt 0");
1707
- for (int ks = 0; ks < k_rnd; ks++) {
1708
- for (uint32_t nt = 0; nt < N / NTILE / GrpsShrB; nt++) {
1709
- float4 eval = ((float4*)s)[(threadIdx.x + threadIdx.y * THRDS) +
1710
- ks * THRDS * 4 + nt * THRDS * 4 * k_rnd];
1711
- vals[nt].f4 += eval;
1733
+ asm volatile("s_waitcnt vmcnt(0)" ::: "memory");
1734
+ for (uint32_t kb = 0; kb < KSB; kb++) {
1735
+ for (uint32_t nt = 0; nt < NT; nt++) {
1736
+ vals[nt].f4 +=
1737
+ ((float4*)lds.all)[(threadIdx.x + threadIdx.y * THRDS) +
1738
+ kb * THRDS * 4 + nt * THRDS * 4 * KSB];
1739
+ }
1740
+ }
1741
+
1742
+ for (uint32_t ks0 = KSB; ks0 < k_rnd; ks0 += KSB) {
1743
+ const uint32_t ksn = (k_rnd - ks0 < KSB) ? (k_rnd - ks0) : KSB;
1744
+ // prior reads must retire before the staging is overwritten
1745
+ asm volatile("s_waitcnt lgkmcnt(0)" ::: "memory");
1746
+ for (uint32_t kb = 0; kb < ksn; kb++) {
1747
+ for (uint32_t nt = 0; nt < NT; nt++) {
1748
+ int g_nindx =
1749
+ (nt * NTILE + (N / GrpsShrB) * (threadIdx.y % GrpsShrB)) / 4;
1750
+ int g_adr = g_mindx * 4 + 0 + M * g_nindx * 4;
1751
+ __builtin_amdgcn_global_load_lds(
1752
+ (float4*)(&glbl[g_adr + M * N * (ks0 + kb)]),
1753
+ &(((float4*)lds.all)[(threadIdx.y * THRDS) + kb * THRDS * 4 +
1754
+ nt * THRDS * 4 * KSB]),
1755
+ 16, 0, 0);
1756
+ }
1757
+ }
1758
+ asm volatile("s_waitcnt vmcnt(0)" ::: "memory");
1759
+ for (uint32_t kb = 0; kb < ksn; kb++) {
1760
+ for (uint32_t nt = 0; nt < NT; nt++) {
1761
+ vals[nt].f4 +=
1762
+ ((float4*)lds.all)[(threadIdx.x + threadIdx.y * THRDS) +
1763
+ kb * THRDS * 4 + nt * THRDS * 4 * KSB];
1764
+ }
1765
}
1766
}
1767
} else {
@@ -1771,7 +1824,7 @@ __global__ void wvSplitKrc_(const int actlN, const int K, const int Kap,
1824
torch::Tensor wvSplitKrc(const at::Tensor& in_a, const at::Tensor& in_b,
1825
const std::optional<at::Tensor>& in_bias,
1826
const int64_t CuCount) {
1774
- int _DTRMNSTC = 1; // vllm::vllm_is_batch_invariant();
1827
+ static constexpr bool _DTRMNSTC = true;
1828
1829
auto M_in = in_b.size(0);
1830
auto N_in = in_a.size(0);
@@ -1819,26 +1872,157 @@ torch::Tensor wvSplitKrc(const at::Tensor& in_a, const at::Tensor& in_b,
1872
1873
if (CuNeeded > CuCount) throw std::runtime_error("Invalid wvSplitKrc size");
1874
1822
- // Can we increase SplitK by shrinking the K-shared to 256?
1823
- int chunkk = (CuNeeded * 2 <= CuCount) ? 2 : 1;
1824
-
1825
- static torch::Tensor axl_glbl =
1826
- torch::zeros(
1827
- 128 * 1024 * (_DTRMNSTC ? 12 : 1),
1828
- torch::TensorOptions().dtype(torch::kFloat32).device(in_a.device()))
1829
- .detach();
1830
- static torch::Tensor axl_cntr =
1831
- torch::zeros(
1832
- 128 * 1024 * (_DTRMNSTC ? 12 : 1) / 4,
1833
- torch::TensorOptions().dtype(torch::kInt).device(in_a.device()))
1834
- .detach();
1835
- auto glbl = axl_glbl.data_ptr<float>();
1836
- auto cntr = axl_cntr.data_ptr<int>();
1875
+ // Can we increase SplitK by shrinking the K-shard to 256? That doubles both
1876
+ // the CUs needed and the k-shards to read back, and the readback is serial in
1877
+ // the shard count, so it pays only up to a crossover. The crossover is not a
1878
+ // constant -- it falls as M grows -- so one cap cannot sit on it for every
1879
+ // shape; this value minimizes total time over the admitted shapes. Retune it
1880
+ // if the readback cost changes, and note the directions are not symmetric:
1881
+ // capping too high costs about twice what capping too low does.
1882
+ // Mirrored by rocm_unquantized_gemm_impl() in
1883
+ // vllm/model_executor/layers/utils.py; both must pick the same chunkk or the
1884
+ // host-side fit check bounds the wrong k_rnd.
1885
+ constexpr int64_t CHUNKK2_MAX_SHARDS = 11;
1886
+ const int64_t shards_chunkk2 = (K_in + 256 - 1) / 256; // 256 == 512 / 2
1887
+ const int chunkk = (N_p2 != 16 && CuNeeded * 2 <= CuCount &&
1888
+ shards_chunkk2 <= CHUNKK2_MAX_SHARDS)
1889
+ ? 2
1890
+ : 1;
1891
+
1892
+ // One fp32 partial per (M, N, k-shard) must fit the split-K workspace.
1893
+ const int64_t k_rnd = (K_in + 512 / chunkk - 1) / (512 / chunkk);
1894
+ constexpr int64_t glbl_cap = 128 * 1024 * (_DTRMNSTC ? 12 : 1);
1895
+ const int64_t glbl_needed =
1896
+ _DTRMNSTC ? (int64_t)N_p2 * M_in * k_rnd : (int64_t)N_p2 * M_in;
1897
+ TORCH_CHECK(glbl_needed <= glbl_cap,
1898
+ "wvSplitKrc: split-K workspace too small", " (need ", glbl_needed,
1899
+ " > ", glbl_cap, " floats) for N=", N_in, " K=", K_in,
1900
+ " M=", M_in);
1901
+
1902
+ // Split-K workspaces: one pre-partitioned pool per device.
1903
+ // These must be zeroed before first use, and the kernel re-zeros them after
1904
+ // each use.
1905
+ // To avoid unnecessary overhead, we do not allocate them zeroed
1906
+ // per-invocation. However, a simple static allocation does not work with
1907
+ // multi-streams, because concurrent streams can alias into the same
1908
+ // workspace. Solution: pre-allocate kWvSlots = 8 slots per device, plus
1909
+ // kWvCaptureSlots reserved for capturing streams, and zero the whole pool
1910
+ // once at creation. On first use, a stream takes one of the slots if any are
1911
+ // still available. On subsequent uses, it re-uses the slot. If no slots are
1912
+ // available, fall back to allocating a zeroed one per call, which is safe but
1913
+ // adds overhead.
1914
+ // A capturing stream must neither allocate nor enqueue anything -- either
1915
+ // would be captured into the graph -- so it is only handed a reserved slot.
1916
+ struct WvSplitKrcPool {
1917
+ torch::Tensor glbl, cntr;
1918
+ int64_t glbl_stride = 0, cntr_stride = 0;
1919
+ int next_slot = 0;
1920
+ int next_capture_slot = 0;
1921
+ std::map<cudaStream_t, int> slot_of;
1922
+ std::map<cudaStream_t, int> capture_slot_of;
1923
+ };
1924
+ // mutex not strictly necessary if caller is always single-threaded (Python)
1925
+ // but this preserves Torch thread_local semantics for the C++ API
1926
+ static std::mutex wv_pool_mu;
1927
+ static std::map<int, WvSplitKrcPool> wv_pools;
1928
+
1929
+ float* glbl;
1930
+ int* cntr;
1931
+ // Fallback space
1932
+ torch::Tensor spill_glbl, spill_cntr;
1933
+ {
1934
+ std::lock_guard<std::mutex> wv_lk(wv_pool_mu);
1935
+ const int wv_dev = static_cast<int>(in_a.device().index());
1936
+ WvSplitKrcPool& P = wv_pools[wv_dev];
1937
+ constexpr int64_t wv_gf = 128 * 1024 * (_DTRMNSTC ? 12 : 1);
1938
+ constexpr int64_t wv_ci = wv_gf / 4;
1939
+ constexpr int64_t wv_all_slots = kWvSlots + kWvCaptureSlots;
1940
+
1941
+ // A stream whose capture was already invalidated reports an error; treat
1942
+ // that as not capturing, since the capture is lost either way.
1943
+ cudaStreamCaptureStatus capture_status = cudaStreamCaptureStatusNone;
1944
+ unsigned long long capture_id = 0;
1945
+ if (cudaStreamGetCaptureInfo(stream, &capture_status, &capture_id) !=
1946
+ cudaSuccess) {
1947
+ capture_status = cudaStreamCaptureStatusNone;
1948
+ }
1949
+ const bool wv_capturing = capture_status != cudaStreamCaptureStatusNone;
1950
+
1951
+ if (!P.glbl.defined()) {
1952
+ TORCH_CHECK(!wv_capturing,
1953
+ "wvSplitKrc: the split-K workspace pool cannot be allocated "
1954
+ "during graph capture. Call "
1955
+ "warmup_rocm_skinny_gemm_workspaces() "
1956
+ "(vllm/model_executor/layers/utils.py) eagerly first.");
1957
+ P.glbl_stride = wv_gf;
1958
+ P.cntr_stride = wv_ci;
1959
+ P.glbl = torch::zeros(wv_gf * wv_all_slots, torch::TensorOptions()
1960
+ .dtype(torch::kFloat32)
1961
+ .device(in_a.device()))
1962
+ .detach();
1963
+ P.cntr =
1964
+ torch::zeros(
1965
+ wv_ci * wv_all_slots,
1966
+ torch::TensorOptions().dtype(torch::kInt).device(in_a.device()))
1967
+ .detach();
1968
+ // The mutex orders host access to the map, not device execution, so it
1969
+ // alone would let another stream be handed a slot the fill above is
1970
+ // still clearing. Synchronizing here, inside the lock, is what makes
1971
+ // every slot safe to use without any further per-slot zeroing.
1972
+ const cudaError_t wv_err = cudaStreamSynchronize(stream);
1973
+ TORCH_CHECK(wv_err == cudaSuccess,
1974
+ "wvSplitKrc: failed to zero the split-K workspace pool: ",
1975
+ cudaGetErrorString(wv_err));
1976
+ }
1977
+
1978
+ int wv_slot = -1;
1979
+ if (wv_capturing) {
1980
+ // One workspace per capturing stream, so graphs captured on the same
1981
+ // stream share it -- safe because at most one of them replays at a time
1982
+ // (vLLM captures one graph per padded batch size, and runs one per step).
1983
+ auto cap_it = P.capture_slot_of.find(stream);
1984
+ if (cap_it == P.capture_slot_of.end()) {
1985
+ TORCH_CHECK(P.next_capture_slot < kWvCaptureSlots,
1986
+ "wvSplitKrc: no free capture workspace slot; more than ",
1987
+ kWvCaptureSlots,
1988
+ " distinct streams have captured a graph containing "
1989
+ "wvSplitKrc. Raise kWvCaptureSlots in "
1990
+ "csrc/rocm/skinny_gemms.cu.");
1991
+ cap_it = P.capture_slot_of
1992
+ .emplace(stream, static_cast<int>(kWvSlots) +
1993
+ P.next_capture_slot++)
1994
+ .first;
1995
+ }
1996
+ wv_slot = cap_it->second;
1997
+ } else {
1998
+ auto slot_it = P.slot_of.find(stream);
1999
+ if (slot_it != P.slot_of.end()) {
2000
+ wv_slot = slot_it->second;
2001
+ } else if (P.next_slot < kWvSlots) {
2002
+ wv_slot = P.next_slot++;
2003
+ P.slot_of.emplace(stream, wv_slot);
2004
+ }
2005
+ }
2006
+
2007
+ if (wv_slot >= 0) {
2008
+ glbl = P.glbl.data_ptr<float>() + wv_slot * P.glbl_stride;
2009
+ cntr = P.cntr.data_ptr<int>() + wv_slot * P.cntr_stride;
2010
+ } else {
2011
+ spill_glbl = torch::zeros(
2012
+ wv_gf,
2013
+ torch::TensorOptions().dtype(torch::kFloat32).device(in_a.device()));
2014
+ spill_cntr = torch::zeros(
2015
+ wv_ci,
2016
+ torch::TensorOptions().dtype(torch::kInt).device(in_a.device()));
2017
+ glbl = spill_glbl.data_ptr<float>();
2018
+ cntr = spill_cntr.data_ptr<int>();
2019
+ }
2020
+ }
2021
2022
#define WVSPLITKrc(_N, _GrpsShrB, _CHUNKK) \
2023
{ \
2024
dim3 block(64, 4); \
1841
- if (_DTRMNSTC) \
2025
+ if constexpr (_DTRMNSTC) \
2026
wvSplitKrc_<fptype, 64, 16, 4, 8, 1, _N, _GrpsShrB, _CHUNKK, 1> \
2027
<<<grid, block, 0, stream>>>(N_in, K_in, Kap_in, M_in, Bx_in, By_in, \
2028
af4, bf4, biasf4, glbl, cntr, c, \
tests/kernels/quantization/test_rocm_skinny_gemms.py
+97
@@ -53,6 +53,17 @@ OPTIONS_WVSPLITK_FP8 = [
53
54
DTYPES = [torch.bfloat16, torch.float16]
55
56
+# kWvSlots in csrc/rocm/skinny_gemms.cu; beyond it streams take overflow
57
+# workspaces.
58
+WVSPLITKRC_SLOTS = 8
59
+
60
+# The CU count is the grid size. Production passes num_compute_units(); a small
61
+# value manufactures better concurrency for these tests.
62
+WVSPLITKRC_TEST_CU = 16
63
+
64
+# Gate spin, ~125 ms on gfx950 -- must outlast the host enqueue loop it gates.
65
+_GATE_CYCLES = 300_000_000
66
+
67
# Specific (N, K, M) combinations for targeted testing
68
NKM_FACTORS_LLMM1 = [
69
# Small, medium, large cases
@@ -116,6 +127,15 @@ K_FACTORS_WVSPLITKRC = [2880, 2880 + 8, 3072, 3072 + 8]
127
# M tiles are 64 rows, +16 for a partial tile
128
M_FACTORS_WVSPLITKRC = [128, 128 + 16, 256, 256 + 16, 640, 640 + 16]
129
130
+# (N, K, M) with more K-shards than the readback can stage in one LDS pass.
131
+# Spans both CHUNKK values and both N-tile counts.
132
+NKM_FACTORS_WVSPLITKRC_LARGE_K = [
133
+ (128, 6144, 128),
134
+ (96, 8192, 128),
135
+ (128, 12288, 128),
136
+ (32, 12288, 128),
137
+]
138
+
139
NKM_FACTORS_WVSPLITK_FP8 = [
140
# FP8-specific cases with K % 16 == 0
141
(1, 16, 16),
@@ -222,6 +242,83 @@ def test_rocm_wvsplitkrc_kernel(n, k, m, dtype, padded_a, bias_mode, xnorm, seed
242
torch.testing.assert_close(out, ref_out, atol=1e-3, rtol=1e-2)
243
244
245
+@pytest.mark.parametrize("n,k,m", NKM_FACTORS_WVSPLITKRC_LARGE_K)
246
+@pytest.mark.parametrize("dtype", DTYPES)
247
+@pytest.mark.parametrize("seed", SEEDS)
248
+@pytest.mark.skipif(not current_platform.is_rocm(), reason="only test for rocm")
249
+@pytest.mark.skipif(not on_gfx950(), reason="only meant for gfx950")
250
+def test_rocm_wvsplitkrc_large_k(n, k, m, dtype, seed):
251
+ """K large enough that the split-K readback must stage LDS in batches."""
252
+ torch.manual_seed(seed)
253
+
254
+ xavier = math.sqrt(2 / k)
255
+ A = (torch.rand(n, k, dtype=dtype, device="cuda") * 2 - 1) * xavier
256
+ B = (torch.rand(m, k, dtype=dtype, device="cuda") * 2 - 1) * xavier
257
+
258
+ ref_out = torch.nn.functional.linear(A, B, None)
259
+ out = ops.wvSplitKrc(A, B, num_compute_units(), None)
260
+
261
+ torch.testing.assert_close(out, ref_out, atol=1e-3, rtol=1e-2)
262
+
263
+
264
+@pytest.mark.parametrize("n_streams", [WVSPLITKRC_SLOTS, WVSPLITKRC_SLOTS + 4])
265
+@pytest.mark.parametrize("dtype", DTYPES)
266
+@pytest.mark.parametrize("seed", SEEDS)
267
+@pytest.mark.skipif(not current_platform.is_rocm(), reason="only test for rocm")
268
+@pytest.mark.skipif(not on_gfx950(), reason="only meant for gfx950")
269
+def test_rocm_wvsplitkrc_multistream(n_streams, dtype, seed):
270
+ """Concurrent streams must not share a split-K workspace.
271
+
272
+ The workspace is cleared by protocol rather than per invocation, so sharing
273
+ one corrupts the reduction and returns wrong numbers rather than failing.
274
+ n_streams > WVSPLITKRC_SLOTS additionally exercises the overflow path.
275
+ """
276
+ torch.manual_seed(seed)
277
+
278
+ n, k, m = 16, 2048, 256
279
+ iters = 4
280
+ xavier = math.sqrt(2 / k)
281
+
282
+ B = torch.randn(m, k, dtype=dtype, device="cuda") * xavier
283
+ As = [
284
+ torch.randn(n, k, dtype=dtype, device="cuda") * xavier for _ in range(n_streams)
285
+ ]
286
+ refs = [torch.nn.functional.linear(A, B, None) for A in As]
287
+ torch.accelerator.synchronize()
288
+
289
+ streams = [torch.Stream() for _ in range(n_streams)]
290
+ outs: list[list[torch.Tensor]] = [[] for _ in range(n_streams)]
291
+ _release_together(streams)
292
+ # Round-robin the enqueues so kernels from different streams are in flight
293
+ # at the same time.
294
+ for _ in range(iters):
295
+ for i, s in enumerate(streams):
296
+ with torch.cuda.stream(s):
297
+ outs[i].append(ops.wvSplitKrc(As[i], B, WVSPLITKRC_TEST_CU, None))
298
+ for s in streams:
299
+ torch.accelerator.current_stream().wait_stream(s)
300
+ torch.accelerator.synchronize()
301
+
302
+ for i in range(n_streams):
303
+ for out in outs[i]:
304
+ torch.testing.assert_close(out, refs[i], atol=1e-3, rtol=1e-2)
305
+
306
+
307
+def _release_together(streams):
308
+ """Release every stream at once, once everything below is enqueued.
309
+
310
+ An event recorded on an idle stream is already complete, so waiting on it
311
+ gates nothing -- the spin, not the event, is what holds the streams.
312
+ """
313
+ blocker = torch.Stream()
314
+ with torch.cuda.stream(blocker):
315
+ torch.cuda._sleep(_GATE_CYCLES)
316
+ gate = torch.Event()
317
+ gate.record()
318
+ for s in streams:
319
+ s.wait_event(gate)
320
+
321
+
322
@pytest.mark.parametrize("n,k,m", NKM_FACTORS_LLMM1)
323
@pytest.mark.parametrize("dtype", DTYPES)
324
@pytest.mark.parametrize("rows_per_block", [2, 4, 8, 16])
vllm/distributed/parallel_state.py
+6
@@ -2357,6 +2357,12 @@ def cleanup_dist_env_and_memory(shutdown_ray: bool = False):
2357
from vllm.platforms import current_platform
2358
2359
if not current_platform.is_cpu():
2360
+ from vllm.triton_utils import HAS_TRITON
2361
+
2362
+ if HAS_TRITON:
2363
+ from vllm.v1.sample.ops.topk_topp_triton import reset_buffer_cache
2364
+
2365
+ reset_buffer_cache()
2366
torch.accelerator.empty_cache()
2367
try:
2368
torch.accelerator.empty_host_cache()
vllm/model_executor/layers/fused_moe/runner/moe_runner.py
-13
@@ -283,24 +283,11 @@ class MoERunner(MoERunnerInterface):
283
self._shared_experts: SharedExperts | None = None
284
if shared_experts is not None:
285
can_overlap = lambda: self._quant_method.mk_can_overlap_shared_experts
286
- # When unquantized, shared expert inputs alias the hidden states,
287
- # which can lead to race condition in multi-stream mode. Quantized
288
- # routed experts copy the input into a fresh buffer first, breaking
289
- # the alias, so overlap is safe. Only observed on ROCm.
290
- routed_input_is_quantized = lambda: (
291
- self.routed_experts.quant_method.moe_quant_config is not None
292
- and self.routed_experts.quant_method.moe_quant_config.quant_dtype
293
- is not None
294
- )
295
- is_multistream_safe = lambda: (
296
- not current_platform.is_rocm() or routed_input_is_quantized()
297
- )
286
self._shared_experts = SharedExperts(
287
shared_experts,
288
moe_config=moe_config,
289
enable_dbo=enable_dbo,
290
mk_can_overlap_shared_experts=can_overlap,
303
- is_multistream_safe=is_multistream_safe,
291
)
292
293
# Needed for string -> MoERunner layer lookup in custom ops.
vllm/model_executor/layers/fused_moe/runner/shared_experts.py
-13
@@ -43,7 +43,6 @@ class SharedExperts(torch.nn.Module):
43
moe_config: FusedMoEConfig,
44
enable_dbo: bool,
45
mk_can_overlap_shared_experts: Callable[[], bool],
46
- is_multistream_safe: Callable[[], bool],
46
):
47
super().__init__()
48
@@ -58,10 +57,6 @@ class SharedExperts(torch.nn.Module):
57
58
self._mk_can_overlap_shared_experts = mk_can_overlap_shared_experts
59
61
- # Might not be safe to run multi-stream mode if routed and shared experts
62
- # alias the same inputs
63
- self._is_multistream_safe = is_multistream_safe
64
-
60
# Allow disabling of the separate shared experts stream for
61
# debug purposes.
62
# TODO: Remove this after more extensive testings with TP/DP
@@ -112,19 +107,11 @@ class SharedExperts(torch.nn.Module):
107
if self._mk_can_overlap_shared_experts():
108
return SharedExpertsOrder.MK_INTERNAL_OVERLAPPED
109
115
- # On ROCm, empirically only DP-only deployments benefit from the overlap.
116
- overlap_is_beneficial = not current_platform.is_rocm() or (
117
- self._moe_config.moe_parallel_config.dp_size > 1
118
- and self._moe_config.moe_parallel_config.tp_size == 1
119
- )
120
-
110
should_run_shared_in_aux_stream = (
111
current_platform.is_cuda_alike()
112
and self._stream is not None
113
and hidden_states.shape[0]
114
<= envs.VLLM_SHARED_EXPERTS_STREAM_TOKEN_THRESHOLD
126
- and overlap_is_beneficial
127
- and self._is_multistream_safe()
115
)
116
117
if should_run_shared_in_aux_stream:
vllm/model_executor/layers/utils.py
+50
-19
@@ -249,6 +249,45 @@ def use_aiter_triton_gemm(n, m, k, dtype):
249
)
250
251
252
+def wvsplitkrc_dispatch(n: int, k: int, m: int, cu_count: int) -> tuple[int, bool]:
253
+ """Pick the K-shard split for wvSplitKrc and say whether the shape fits.
254
+
255
+ Mirrors wvSplitKrc() in csrc/rocm/skinny_gemms.cu, which is also where the
256
+ shard cap is explained. Both must pick the same chunkk or the workspace
257
+ check here bounds the wrong k_rnd.
258
+
259
+ Returns:
260
+ The CHUNKK the kernel will dispatch with, and whether the CU budget and
261
+ split-K workspace admit the shape at all.
262
+ """
263
+ # Next ^2 of n
264
+ N_p2 = 1 << (n - 1).bit_length()
265
+ # How many of 4 waves in a group can work on same 16 Ms at same time?
266
+ # This reduces the Ms each group works on, i.e. increasing the CUs needed.
267
+ GrpsShrB = min(N_p2 // 16, 4)
268
+ # With 64 Ms per CU (each of 4 SIMDs working on a 16x16 tile), and each
269
+ # working on a 512-shard of K, how many CUs would we need?
270
+ CuNeeded = ((m + 64 - 1) // 64) * ((k + 512 - 1) // 512) * GrpsShrB
271
+
272
+ CHUNKK2_MAX_SHARDS = 11
273
+ shards_chunkk2 = (k + 256 - 1) // 256 # 256-wide shards
274
+ chunkk = (
275
+ 2
276
+ if (
277
+ N_p2 != 16
278
+ and CuNeeded * 2 <= cu_count
279
+ and shards_chunkk2 <= CHUNKK2_MAX_SHARDS
280
+ )
281
+ else 1
282
+ )
283
+
284
+ # Deterministic reduction stores one fp32 partial per (M, N, k-shard); all
285
+ # of them must fit the split-K workspace.
286
+ k_rnd = (k + 512 // chunkk - 1) // (512 // chunkk)
287
+ fits = N_p2 * m * k_rnd <= 128 * 1024 * 12 and CuNeeded <= cu_count
288
+ return chunkk, fits
289
+
290
+
291
def rocm_unquantized_gemm_impl(
292
x: torch.Tensor, weight: torch.Tensor, bias: torch.Tensor | None = None
293
) -> torch.Tensor:
@@ -260,21 +299,7 @@ def rocm_unquantized_gemm_impl(
299
300
cu_count = num_compute_units()
301
263
- # Next ^2 of n
264
- N_p2 = 1 << (n - 1).bit_length()
265
- # With 64 Ms per CU (each of 4 SIMDs working on a 16x16 tile),
266
- # and each working on a 512-shard of K, how many CUs would we need?
267
- rndup_cus = ((m + 64 - 1) // 64) * ((k + 512 - 1) // 512)
268
- # How many of 4 waves in a group can work on same 16 Ms at same time?
269
- # This reduces the Ms each group works on, i.e. increasing the number of CUs needed.
270
- GrpsShrB = min(N_p2 // 16, 4)
271
- # Given the above, how many CUs would we need?
272
- CuNeeded = rndup_cus * GrpsShrB
273
- # Deterministic reduction stores one float workspace value per K shard.
274
- fits_wvsplitkrc = (
275
- N_p2 * m * ((k + 512 - 1) // 512)
276
- ) <= 128 * 1024 * 12 # deterministic
277
- fits_wvsplitkrc &= CuNeeded <= cu_count
302
+ _, fits_wvsplitkrc = wvsplitkrc_dispatch(n, k, m, cu_count)
303
304
skinny_operands_compatible = weight.is_contiguous() and (
305
bias is None or bias.is_contiguous()
@@ -362,12 +387,18 @@ direct_register_custom_op(
387
388
@functools.cache
389
def warmup_rocm_skinny_gemm_workspaces(device: torch.device) -> None:
365
- """Eagerly allocate wvSplitKrc's process-lifetime static workspaces.
390
+ """Eagerly allocate wvSplitKrc's per-device split-K workspace pool.
391
+
392
+ wvSplitKrc partitions one per-device allocation into ``kWvSlots`` slots
393
+ (csrc/rocm/skinny_gemms.cu) and hands each stream one on first use, so that
394
+ two streams never share the split-K partials and counters.
395
367
- They are otherwise created lazily on the first qualifying GEMM
396
+ The pool is otherwise created lazily on the first qualifying GEMM
397
(csrc/rocm/skinny_gemms.cu), which can be the first real request — after
369
- the KV cache backing buffer exists. If one landed in that segment's
370
- rounding tail, it would pin the entire segment at engine shutdown.
398
+ the KV cache backing buffer exists. If it landed in that segment's rounding
399
+ tail, it would pin the entire segment at engine shutdown; it could also land
400
+ inside a cudagraph capture, where it would be taken from the graph's private
401
+ pool and its zero-fill would become a replayed graph node.
402
"""
403
from vllm.platforms.rocm import on_gfx950
404