[CPU] Support FP16/BF16 persisted GDN state on AMX (#52191)
Signed-off-by: Li, Tianmu <tianmu.li@intel.com> Co-authored-by: OpenAI Codex <noreply@openai.com>
Tianmu Li committed
Aug 31, 2026 at 01:32 UTC
c6c33f2b1facdcf6ace0d2f241f20507c6cc8157
4 files changed
+575
-141
csrc/cpu/sgl-kernels/fla.cpp
+292
-118
@@ -74,6 +74,20 @@ void pack_vnni2(scalar_t* __restrict__ dst, float* __restrict__ src, const float
74
#endif
75
}
76
77
+template <typename state_t>
78
+inline void store_state_vec2(
79
+ state_t* __restrict__ dst,
80
+ const at::vec::Vectorized<float>& src0,
81
+ const at::vec::Vectorized<float>& src1) {
82
+ if constexpr (std::is_same_v<state_t, float>) {
83
+ src0.store(dst);
84
+ src1.store(dst + at::vec::Vectorized<float>::size());
85
+ } else {
86
+ auto state_vec = convert_from_float_ext<state_t>(src0, src1);
87
+ state_vec.store(dst);
88
+ }
89
+}
90
+
91
template <typename scalar_t, int SIZE>
92
inline void fill_stub(scalar_t* __restrict__ out, float val) {
93
using Vec = at::vec::Vectorized<scalar_t>;
@@ -1255,10 +1269,10 @@ void chunk_gated_delta_rule_fwd_intra_kernel_impl(
1269
// d : [B, NT, Hv, C, C]
1270
// cu_seqlens : [num_seqs + 1]
1271
// chunk_offsets : [num_seqs + 1]
1258
-template <typename scalar_t, int D, int CHUNK_SIZE>
1272
+template <typename scalar_t, typename state_t, int D, int CHUNK_SIZE>
1273
void chunk_gated_delta_rule_fwd_inter_kernel_impl(
1274
scalar_t* __restrict__ out,
1261
- float* __restrict__ state,
1275
+ state_t* __restrict__ state,
1276
const int32_t* __restrict__ indices,
1277
const scalar_t* __restrict__ q,
1278
const scalar_t* __restrict__ k,
@@ -1297,6 +1311,7 @@ void chunk_gated_delta_rule_fwd_inter_kernel_impl(
1311
// thread local temp buffer
1312
DECL_ZERO_BUF(scalar_t, tmp, CHUNK_SIZE * D);
1313
DECL_ZERO_BUF(scalar_t, tmp2, D * D);
1314
+ DECL_ZERO_BUF(float, state_tmp, D * D);
1315
DECL_ZERO_BUF(float, tmp3, CHUNK_SIZE* D);
1316
DECL_ZERO_BUF(scalar_t, tmp4, CHUNK_SIZE * D);
1317
DECL_ZERO_BUF(float, attn, CHUNK_SIZE* CHUNK_SIZE);
@@ -1370,7 +1385,27 @@ void chunk_gated_delta_rule_fwd_inter_kernel_impl(
1385
apply_mask_kernel<scalar_t, CHUNK_SIZE, false>::apply(attn2, attn, nullptr, d_ptr, mb_size);
1386
1387
// step 2.a: v' = w @ state (fuse state *= exp(g_last) with packing)
1373
- float* __restrict__ s_ptr = state + indices[bs] * state_strideS + hv * (D * D);
1388
+ state_t* __restrict__ state_slot =
1389
+ state + indices[bs] * state_strideS + hv * (D * D);
1390
+ float* s_ptr;
1391
+ if constexpr (std::is_same_v<state_t, float>) {
1392
+ s_ptr = state_slot;
1393
+ } else {
1394
+ using bVec = at::vec::Vectorized<state_t>;
1395
+ using fVec = at::vec::Vectorized<float>;
1396
+ constexpr int kVecSize = bVec::size();
1397
+ constexpr int fVecSize = fVec::size();
1398
+ int64_t d0 = 0;
1399
+ for (; d0 + kVecSize <= D * D; d0 += kVecSize) {
1400
+ auto [state_vec0, state_vec1] = load_float_vec2(state_slot + d0);
1401
+ state_vec0.store(state_tmp + d0);
1402
+ state_vec1.store(state_tmp + d0 + fVecSize);
1403
+ }
1404
+ for (; d0 < D * D; ++d0) {
1405
+ state_tmp[d0] = static_cast<float>(state_slot[d0]);
1406
+ }
1407
+ s_ptr = state_tmp;
1408
+ }
1409
const float* __restrict__ g_ptr = g + nt * (Hv * CHUNK_SIZE) + hv * (CHUNK_SIZE);
1410
float g_last = g_ptr[mb_size - 1];
1411
const scalar_t* __restrict__ w_ptr = w + (batch_offset + mb_start) * w_strideT + hv * w_strideH;
@@ -1547,6 +1582,22 @@ void chunk_gated_delta_rule_fwd_inter_kernel_impl(
1582
s_ptr,
1583
D);
1584
}
1585
+
1586
+ if constexpr (!std::is_same_v<state_t, float>) {
1587
+ using bVec = at::vec::Vectorized<state_t>;
1588
+ using fVec = at::vec::Vectorized<float>;
1589
+ constexpr int kVecSize = bVec::size();
1590
+ constexpr int fVecSize = fVec::size();
1591
+ int64_t d0 = 0;
1592
+ for (; d0 + kVecSize <= D * D; d0 += kVecSize) {
1593
+ fVec state_vec0 = fVec::loadu(s_ptr + d0);
1594
+ fVec state_vec1 = fVec::loadu(s_ptr + d0 + fVecSize);
1595
+ store_state_vec2<state_t>(state_slot + d0, state_vec0, state_vec1);
1596
+ }
1597
+ for (; d0 < D * D; ++d0) {
1598
+ state_slot[d0] = static_cast<state_t>(s_ptr[d0]);
1599
+ }
1600
+ }
1601
}
1602
1603
// move to the next index
@@ -1576,7 +1627,7 @@ inline at::vec::Vectorized<float> softplus(const at::vec::Vectorized<float>& x,
1627
return Vec::blendv(Vec::blendv(log1pex, expx, mask_lo), x, mask_hi);
1628
}
1629
1579
-template <typename scalar_t, typename param_t>
1630
+template <typename scalar_t, typename param_t, typename state_t>
1631
void fused_sigmoid_gating_delta_rule_update_kernel_impl(
1632
const scalar_t* __restrict__ q_ptr,
1633
const scalar_t* __restrict__ k_ptr,
@@ -1586,7 +1637,7 @@ void fused_sigmoid_gating_delta_rule_update_kernel_impl(
1637
const scalar_t* __restrict__ dt_bias_ptr,
1638
const scalar_t* __restrict__ b_ptr,
1639
const int32_t* __restrict__ indices_ptr,
1589
- float* __restrict__ state_ptr,
1640
+ state_t* __restrict__ state_ptr,
1641
scalar_t* __restrict__ o_ptr,
1642
float* __restrict__ qk_scale_buf,
1643
int64_t seq_len,
@@ -1700,8 +1751,10 @@ void fused_sigmoid_gating_delta_rule_update_kernel_impl(
1751
state_vec1 = state_vec1 * g_val_exp_vec + k_vec * dt_vec1;
1752
o_vec0 = o_vec0 + state_vec0 * q_vec * scale_vec;
1753
o_vec1 = o_vec1 + state_vec1 * q_vec * scale_vec;
1703
- state_vec0.store(state_ptr + state_offset + di * v_head_dim + dvi);
1704
- state_vec1.store(state_ptr + state_offset + di * v_head_dim + dvi + fVecSize);
1754
+ store_state_vec2<state_t>(
1755
+ state_ptr + state_offset + di * v_head_dim + dvi,
1756
+ state_vec0,
1757
+ state_vec1);
1758
}
1759
bVec o_vec = at::vec::convert_from_float<scalar_t>(o_vec0, o_vec1);
1760
o_vec.store(o_ptr + o_offset + dvi);
@@ -1710,8 +1763,10 @@ void fused_sigmoid_gating_delta_rule_update_kernel_impl(
1763
float kv_mem_val = 0;
1764
for (int di = 0; di < head_dim; ++di) {
1765
float k_val = k_ptr[k_offset + di] * k_scale;
1713
- state_ptr[state_offset + di * v_head_dim + dvi] *= g_val_exp;
1714
- kv_mem_val += state_ptr[state_offset + di * v_head_dim + dvi] * k_val;
1766
+ float state_val =
1767
+ static_cast<float>(state_ptr[state_offset + di * v_head_dim + dvi]) *
1768
+ g_val_exp;
1769
+ kv_mem_val += state_val * k_val;
1770
}
1771
float v_val = v_ptr[v_offset + dvi];
1772
float dt_val = (v_val - kv_mem_val) * beta_val;
@@ -1719,8 +1774,13 @@ void fused_sigmoid_gating_delta_rule_update_kernel_impl(
1774
for (int di = 0; di < head_dim; ++di) {
1775
float q_val = q_ptr[q_offset + di] * q_scale;
1776
float k_val = k_ptr[k_offset + di] * k_scale;
1722
- state_ptr[state_offset + di * v_head_dim + dvi] += k_val * dt_val;
1723
- o_val += state_ptr[state_offset + di * v_head_dim + dvi] * q_val * scale;
1777
+ float state_val =
1778
+ static_cast<float>(state_ptr[state_offset + di * v_head_dim + dvi]) *
1779
+ g_val_exp;
1780
+ state_val += k_val * dt_val;
1781
+ state_ptr[state_offset + di * v_head_dim + dvi] =
1782
+ static_cast<state_t>(state_val);
1783
+ o_val += state_val * q_val * scale;
1784
}
1785
o_ptr[o_offset + dvi] = o_val;
1786
}
@@ -1736,7 +1796,7 @@ void fused_sigmoid_gating_delta_rule_update_kernel_impl(
1796
// token ``t`` into cache slot ``t`` (multi-slot rollback, matching the GPU
1797
// kernel). Parallelized over (sequence, v_head); the per-sequence token loop is
1798
// sequential as required by the recurrence.
1739
-template <typename scalar_t, typename param_t>
1799
+template <typename scalar_t, typename param_t, typename state_t>
1800
void fused_sigmoid_gating_delta_rule_update_spec_kernel_impl(
1801
const scalar_t* __restrict__ q_ptr, // [T, HK, EK]
1802
const scalar_t* __restrict__ k_ptr, // [T, HK, EK]
@@ -1748,7 +1808,7 @@ void fused_sigmoid_gating_delta_rule_update_spec_kernel_impl(
1808
const int32_t* __restrict__ spec_indices_ptr, // [N, S]
1809
const int32_t* __restrict__ num_accepted_ptr, // [N]
1810
const int32_t* __restrict__ cu_seqlens_ptr, // [N + 1]
1751
- float* __restrict__ state_ptr,
1811
+ state_t* __restrict__ state_ptr,
1812
scalar_t* __restrict__ o_ptr, // [T, HV, EV]
1813
float* __restrict__ qk_scale_buf, // [2, T, HK]
1814
int64_t total_tokens,
@@ -1770,7 +1830,6 @@ void fused_sigmoid_gating_delta_rule_update_spec_kernel_impl(
1830
using bVec = at::vec::Vectorized<scalar_t>;
1831
using fVec = at::vec::Vectorized<float>;
1832
constexpr int64_t VecSize = bVec::size();
1773
- constexpr int64_t fVecSize = fVec::size();
1833
int64_t group_size = v_num_heads / num_heads;
1834
double scale = 1 / std::sqrt((double)head_dim);
1835
fVec scale_vec = fVec((float)scale);
@@ -1815,8 +1874,10 @@ void fused_sigmoid_gating_delta_rule_update_spec_kernel_impl(
1874
for (int64_t t = 0; t < q_len; ++t) {
1875
int64_t cur_slot = (int64_t)spec_indices_ptr[bi * spec_stride + t];
1876
int64_t token = q_start + t;
1818
- const float* src = state_ptr + prev_slot * state_slot_stride + ni * head_dim * v_head_dim;
1819
- float* dst = state_ptr + cur_slot * state_slot_stride + ni * head_dim * v_head_dim;
1877
+ const state_t* src =
1878
+ state_ptr + prev_slot * state_slot_stride + ni * head_dim * v_head_dim;
1879
+ state_t* dst =
1880
+ state_ptr + cur_slot * state_slot_stride + ni * head_dim * v_head_dim;
1881
float g_val = -std::exp((float)A_log_ptr[ni]) *
1882
softplus((float)a_ptr[token * v_num_heads + ni] + (float)dt_bias_ptr[ni], softplus_threshold);
1883
float g_val_exp = std::exp(g_val);
@@ -1836,8 +1897,8 @@ void fused_sigmoid_gating_delta_rule_update_spec_kernel_impl(
1897
fVec kv_mem_vec1 = fVec(0.f);
1898
for (int di = 0; di < head_dim; ++di) {
1899
fVec k_val_vec = fVec((float)k_ptr[k_offset + di] * k_scale);
1839
- fVec sv0 = fVec::loadu(src + di * v_head_dim + dvi);
1840
- fVec sv1 = fVec::loadu(src + di * v_head_dim + dvi + fVecSize);
1900
+ auto [sv0, sv1] =
1901
+ load_float_vec2(src + di * v_head_dim + dvi);
1902
kv_mem_vec0 = kv_mem_vec0 + sv0 * g_val_exp_vec * k_val_vec;
1903
kv_mem_vec1 = kv_mem_vec1 + sv1 * g_val_exp_vec * k_val_vec;
1904
}
@@ -1851,14 +1912,14 @@ void fused_sigmoid_gating_delta_rule_update_spec_kernel_impl(
1912
for (int di = 0; di < head_dim; ++di) {
1913
fVec q_vec = fVec((float)q_ptr[q_offset + di] * q_scale);
1914
fVec k_vec = fVec((float)k_ptr[k_offset + di] * k_scale);
1854
- fVec sv0 = fVec::loadu(src + di * v_head_dim + dvi);
1855
- fVec sv1 = fVec::loadu(src + di * v_head_dim + dvi + fVecSize);
1915
+ auto [sv0, sv1] =
1916
+ load_float_vec2(src + di * v_head_dim + dvi);
1917
sv0 = sv0 * g_val_exp_vec + k_vec * dt_vec0;
1918
sv1 = sv1 * g_val_exp_vec + k_vec * dt_vec1;
1919
o_vec0 = o_vec0 + sv0 * q_vec * scale_vec;
1920
o_vec1 = o_vec1 + sv1 * q_vec * scale_vec;
1860
- sv0.store(dst + di * v_head_dim + dvi);
1861
- sv1.store(dst + di * v_head_dim + dvi + fVecSize);
1921
+ store_state_vec2<state_t>(
1922
+ dst + di * v_head_dim + dvi, sv0, sv1);
1923
}
1924
bVec o_vec = at::vec::convert_from_float<scalar_t>(o_vec0, o_vec1);
1925
o_vec.store(o_ptr + o_offset + dvi);
@@ -1867,7 +1928,8 @@ void fused_sigmoid_gating_delta_rule_update_spec_kernel_impl(
1928
float kv_mem_val = 0.f;
1929
for (int di = 0; di < head_dim; ++di) {
1930
float k_val = (float)k_ptr[k_offset + di] * k_scale;
1870
- kv_mem_val += src[di * v_head_dim + dvi] * g_val_exp * k_val;
1931
+ kv_mem_val += static_cast<float>(src[di * v_head_dim + dvi]) *
1932
+ g_val_exp * k_val;
1933
}
1934
float v_val = (float)v_ptr[v_offset + dvi];
1935
float dt_val = (v_val - kv_mem_val) * beta_val;
@@ -1875,8 +1937,10 @@ void fused_sigmoid_gating_delta_rule_update_spec_kernel_impl(
1937
for (int di = 0; di < head_dim; ++di) {
1938
float q_val = (float)q_ptr[q_offset + di] * q_scale;
1939
float k_val = (float)k_ptr[k_offset + di] * k_scale;
1878
- float ns = src[di * v_head_dim + dvi] * g_val_exp + k_val * dt_val;
1879
- dst[di * v_head_dim + dvi] = ns;
1940
+ float ns = static_cast<float>(src[di * v_head_dim + dvi]) *
1941
+ g_val_exp +
1942
+ k_val * dt_val;
1943
+ dst[di * v_head_dim + dvi] = static_cast<state_t>(ns);
1944
o_val += ns * q_val * scale;
1945
}
1946
o_ptr[o_offset + dvi] = (scalar_t)o_val;
@@ -2112,10 +2176,10 @@ std::tuple<at::Tensor, at::Tensor, at::Tensor> chunk_gated_delta_rule_fwd_intra(
2176
return std::make_tuple(w, u, decay_mask);
2177
}
2178
2115
-#define LAUNCH_CHUNK_GATED_DELTA_RULE_FWD_INTER_KERNEL(HD) \
2116
- chunk_gated_delta_rule_fwd_inter_kernel_impl<scalar_t, HD, CHUNK_SIZE>( \
2179
+#define LAUNCH_CHUNK_GATED_DELTA_RULE_FWD_INTER_KERNEL(HD) \
2180
+ chunk_gated_delta_rule_fwd_inter_kernel_impl<scalar_t, state_t, HD, CHUNK_SIZE>( \
2181
o.data_ptr<scalar_t>(), \
2118
- initial_state.data_ptr<float>(), \
2182
+ initial_state.data_ptr<state_t>(), \
2183
initial_state_indices.data_ptr<int32_t>(), \
2184
q.data_ptr<scalar_t>(), \
2185
k.data_ptr<scalar_t>(), \
@@ -2134,7 +2198,7 @@ std::tuple<at::Tensor, at::Tensor, at::Tensor> chunk_gated_delta_rule_fwd_intra(
2198
k.stride(2), \
2199
initial_state.stride(0));
2200
2137
-template <int CHUNK_SIZE>
2201
+template <typename state_t, int CHUNK_SIZE>
2202
std::tuple<at::Tensor, at::Tensor> chunk_gated_delta_rule_fwd_inter(
2203
const at::Tensor& q,
2204
const at::Tensor& k,
@@ -2172,7 +2236,7 @@ std::tuple<at::Tensor, at::Tensor> chunk_gated_delta_rule_fwd_inter(
2236
// value: [B, T, Hv, Dv]
2237
// g: [B, T, Hv] FP32
2238
// beta: [B, T, Hv]
2175
-// initial_state: [num_seqs, Hv, Dv, D] FP32
2239
+// initial_state: [num_seqs, Hv, Dv, D] FP16, BF16, or FP32 persisted state
2240
// cu_seqlens: [num_seqs + 1] INT32
2241
//
2242
std::tuple<at::Tensor, at::Tensor> chunk_gated_delta_rule_cpu(
@@ -2211,7 +2275,12 @@ std::tuple<at::Tensor, at::Tensor> chunk_gated_delta_rule_cpu(
2275
CHECK_INPUT_SHAPE_DTYPE<false>(cu_seqlens, {num_seqs + 1}, at::kInt);
2276
TORCH_CHECK(initial_state.sizes() == at::IntArrayRef({initial_state.size(0), Hv, Dv, D}),
2277
"chunk_gated_delta_rule_cpu: initial_state shape mismatch, got ", initial_state.sizes());
2214
- TORCH_CHECK(initial_state.scalar_type() == at::kFloat, "chunk_gated_delta_rule_cpu: initial_state dtype mismatch");
2278
+ TORCH_CHECK(
2279
+ initial_state.scalar_type() == at::kFloat ||
2280
+ initial_state.scalar_type() == at::kBFloat16 ||
2281
+ initial_state.scalar_type() == at::kHalf,
2282
+ "chunk_gated_delta_rule_cpu: initial_state dtype must be float16, "
2283
+ "bfloat16, or float32");
2284
CHECK_CPU(initial_state);
2285
// initial_state may be a pooled/paged buffer with padding between slots
2286
// (e.g. mamba cache-align mode), so only the per-slot (Hv, Dv, D) layout
@@ -2237,18 +2306,28 @@ std::tuple<at::Tensor, at::Tensor> chunk_gated_delta_rule_cpu(
2306
chunk_gated_delta_rule_fwd_intra<CHUNK_SIZE>(key_, value, g_, beta, cu_seqlens, chunk_indices);
2307
2308
// fused `chunk_gated_delta_rule_fwd_h` + `chunk_fwd_o`
2240
- auto [output, final_state] = chunk_gated_delta_rule_fwd_inter<CHUNK_SIZE>(
2241
- query_,
2242
- key_,
2243
- w,
2244
- u,
2245
- g_,
2246
- decay_mask,
2247
- initial_state,
2248
- output_final_state,
2249
- cu_seqlens,
2250
- chunk_offsets,
2251
- initial_state_indices);
2309
+ auto launch_inter = [&](auto state_tag) {
2310
+ using state_t = decltype(state_tag);
2311
+ return chunk_gated_delta_rule_fwd_inter<state_t, CHUNK_SIZE>(
2312
+ query_,
2313
+ key_,
2314
+ w,
2315
+ u,
2316
+ g_,
2317
+ decay_mask,
2318
+ initial_state,
2319
+ output_final_state,
2320
+ cu_seqlens,
2321
+ chunk_offsets,
2322
+ initial_state_indices);
2323
+ };
2324
+ auto output_and_state =
2325
+ initial_state.scalar_type() == at::kFloat
2326
+ ? launch_inter(float{})
2327
+ : initial_state.scalar_type() == at::kBFloat16
2328
+ ? launch_inter(at::BFloat16{})
2329
+ : launch_inter(at::Half{});
2330
+ auto [output, final_state] = output_and_state;
2331
2332
return std::make_tuple(output, final_state);
2333
}
@@ -2261,6 +2340,7 @@ std::tuple<at::Tensor, at::Tensor> chunk_gated_delta_rule_cpu(
2340
// a: [batch_size, v_num_heads]
2341
// b: [batch_size, v_num_heads]
2342
// initial_state_source:[num_tokens, v_num_heads, head_dim, v_head_dim]
2343
+// FP16, BF16, or FP32 persisted state; recurrent arithmetic remains FP32.
2344
// initial_state_indices: [batch_size]
2345
// cu_seqlens: [batch_size + 1]
2346
at::Tensor fused_sigmoid_gating_delta_rule_update_cpu(
@@ -2293,8 +2373,24 @@ at::Tensor fused_sigmoid_gating_delta_rule_update_cpu(
2373
CHECK_INPUT_SHAPE_DTYPE<true>(b, {batch_size, v_num_heads}, q.scalar_type());
2374
CHECK_INPUT_SHAPE_DTYPE<true>(initial_state_indices, {batch_size}, at::kInt);
2375
CHECK_INPUT_SHAPE_DTYPE<true>(cu_seqlens, {batch_size + 1}, at::kInt);
2296
- CHECK_INPUT_SHAPE_DTYPE<true>(
2297
- initial_state_source, {initial_state_source.size(0), v_num_heads, head_dim, v_head_dim}, at::kFloat);
2376
+ TORCH_CHECK(
2377
+ initial_state_source.scalar_type() == at::kFloat ||
2378
+ initial_state_source.scalar_type() == at::kBFloat16 ||
2379
+ initial_state_source.scalar_type() == at::kHalf,
2380
+ "fused_sigmoid_gating_delta_rule_update_cpu: initial_state dtype must "
2381
+ "be float16, bfloat16, or float32");
2382
+ TORCH_CHECK(
2383
+ initial_state_source.sizes() ==
2384
+ at::IntArrayRef(
2385
+ {initial_state_source.size(0), v_num_heads, head_dim, v_head_dim}),
2386
+ "Input tensor shape mismatch for initial_state_source");
2387
+ CHECK_CPU(initial_state_source);
2388
+ TORCH_CHECK(
2389
+ initial_state_source.stride(-1) == 1 &&
2390
+ initial_state_source.stride(-2) == v_head_dim &&
2391
+ initial_state_source.stride(-3) == head_dim * v_head_dim,
2392
+ "fused_sigmoid_gating_delta_rule_update_cpu: expect initial_state_source "
2393
+ "to be contiguous per pool slot.");
2394
CHECK(initial_state_source.size(0) >= batch_size);
2395
CHECK_EQ(v_num_heads % num_heads, 0);
2396
TORCH_CHECK(
@@ -2318,46 +2414,63 @@ at::Tensor fused_sigmoid_gating_delta_rule_update_cpu(
2414
at::Tensor core_attn_out = at::empty({batch_size, seq_len, v_num_heads, v_head_dim}, q.options());
2415
at::Tensor qk_scale_buf = at::empty({2 * batch_size, seq_len, num_heads}, at::kFloat);
2416
2321
- CPU_DISPATCH_REDUCED_FLOATING_TYPES_EXT(
2322
- q.scalar_type(), A_log.scalar_type(), "fused_sigmoid_gating_delta_rule_update_kernel_impl", [&] {
2323
- fused_sigmoid_gating_delta_rule_update_kernel_impl<scalar_t, param_t>(
2324
- q.data_ptr<scalar_t>(),
2325
- k.data_ptr<scalar_t>(),
2326
- v.data_ptr<scalar_t>(),
2327
- A_log.data_ptr<param_t>(),
2328
- a.data_ptr<scalar_t>(),
2329
- dt_bias.data_ptr<scalar_t>(),
2330
- b.data_ptr<scalar_t>(),
2331
- initial_state_indices.data_ptr<int32_t>(),
2332
- initial_state_source.data_ptr<float>(),
2333
- core_attn_out.data_ptr<scalar_t>(),
2334
- qk_scale_buf.data_ptr<float>(),
2335
- seq_len,
2336
- batch_size,
2337
- num_heads,
2338
- head_dim,
2339
- v_num_heads,
2340
- v_head_dim,
2341
- q_strideB,
2342
- q_strideS,
2343
- q_strideH,
2344
- k_strideB,
2345
- k_strideS,
2346
- k_strideH,
2347
- v_strideB,
2348
- v_strideS,
2349
- v_strideH,
2350
- state_slot_stride,
2351
- use_qk_l2norm_in_kernel,
2352
- softplus_threshold);
2353
- });
2417
+ auto launch = [&](auto state_tag) {
2418
+ using state_t = decltype(state_tag);
2419
+ CPU_DISPATCH_REDUCED_FLOATING_TYPES_EXT(
2420
+ q.scalar_type(), A_log.scalar_type(),
2421
+ "fused_sigmoid_gating_delta_rule_update_kernel_impl", [&] {
2422
+ fused_sigmoid_gating_delta_rule_update_kernel_impl<scalar_t, param_t, state_t>(
2423
+ q.data_ptr<scalar_t>(),
2424
+ k.data_ptr<scalar_t>(),
2425
+ v.data_ptr<scalar_t>(),
2426
+ A_log.data_ptr<param_t>(),
2427
+ a.data_ptr<scalar_t>(),
2428
+ dt_bias.data_ptr<scalar_t>(),
2429
+ b.data_ptr<scalar_t>(),
2430
+ initial_state_indices.data_ptr<int32_t>(),
2431
+ initial_state_source.data_ptr<state_t>(),
2432
+ core_attn_out.data_ptr<scalar_t>(),
2433
+ qk_scale_buf.data_ptr<float>(),
2434
+ seq_len,
2435
+ batch_size,
2436
+ num_heads,
2437
+ head_dim,
2438
+ v_num_heads,
2439
+ v_head_dim,
2440
+ q_strideB,
2441
+ q_strideS,
2442
+ q_strideH,
2443
+ k_strideB,
2444
+ k_strideS,
2445
+ k_strideH,
2446
+ v_strideB,
2447
+ v_strideS,
2448
+ v_strideH,
2449
+ state_slot_stride,
2450
+ use_qk_l2norm_in_kernel,
2451
+ softplus_threshold);
2452
+ });
2453
+ };
2454
+ if (initial_state_source.scalar_type() == at::kFloat) {
2455
+ launch(float());
2456
+ } else if (initial_state_source.scalar_type() == at::kBFloat16) {
2457
+ launch(at::BFloat16());
2458
+ } else if (initial_state_source.scalar_type() == at::kHalf) {
2459
+ launch(at::Half());
2460
+ } else {
2461
+ TORCH_CHECK(
2462
+ false,
2463
+ "fused_sigmoid_gating_delta_rule_update_cpu: initial_state dtype must "
2464
+ "be float16, bfloat16, or float32");
2465
+ }
2466
return core_attn_out;
2467
}
2468
2469
// Speculative-decode update (multi-token, multi-slot rollback).
2470
// q: [T, HK, EK] k: [T, HK, EK] v: [T, HV, EV]
2471
// a: [T, HV] b: [T, HV]
2360
-// initial_state_source: [N_slots, HV, EK, EV] FP32 (updated in place)
2472
+// initial_state_source: [N_slots, HV, EK, EV] FP16, BF16, or FP32 persisted state
2473
+// (updated in place; recurrent arithmetic remains FP32)
2474
// spec_state_indices: [batch, S] INT32 (S = num_spec + 1)
2475
// num_accepted_tokens: [batch] INT32
2476
// cu_seqlens: [batch + 1] INT32
@@ -2385,21 +2498,65 @@ at::Tensor fused_sigmoid_gating_delta_rule_update_spec_cpu(
2498
int64_t head_dim = q.size(2);
2499
int64_t v_num_heads = v.size(1);
2500
int64_t v_head_dim = v.size(2);
2388
- int64_t batch_size = cu_seqlens.size(0) - 1;
2389
- int64_t spec_stride = spec_state_indices.stride(0);
2501
CHECK_INPUT_SHAPE_DTYPE<true>(k, {total_tokens, num_heads, head_dim}, q.scalar_type());
2502
CHECK_INPUT_SHAPE_DTYPE<true>(v, {total_tokens, v_num_heads, v_head_dim}, q.scalar_type());
2503
CHECK_INPUT_SHAPE_DTYPE<true>(a, {total_tokens, v_num_heads}, q.scalar_type());
2504
CHECK_INPUT_SHAPE_DTYPE<true>(b, {total_tokens, v_num_heads}, q.scalar_type());
2505
CHECK_INPUT_SHAPE_DTYPE<true>(dt_bias, {v_num_heads}, q.scalar_type());
2506
+ CHECK_DIM(1, cu_seqlens);
2507
+ int64_t batch_size = cu_seqlens.size(0) - 1;
2508
CHECK_INPUT_SHAPE_DTYPE<true>(num_accepted_tokens, {batch_size}, at::kInt);
2509
CHECK_INPUT_SHAPE_DTYPE<true>(cu_seqlens, {batch_size + 1}, at::kInt);
2510
+ CHECK_DIM(2, spec_state_indices);
2511
+ CHECK_CPU(spec_state_indices);
2512
+ TORCH_CHECK(
2513
+ spec_state_indices.scalar_type() == at::kInt,
2514
+ "spec_state_indices must be an int32 CPU tensor");
2515
+ TORCH_CHECK(
2516
+ spec_state_indices.size(0) == batch_size,
2517
+ "spec_state_indices first dimension must equal batch_size");
2518
+ CHECK_LAST_DIM_CONTIGUOUS_INPUT(spec_state_indices);
2519
+ const int32_t* cu_seqlens_ptr = cu_seqlens.data_ptr<int32_t>();
2520
+ int64_t max_seq_len = 0;
2521
+ for (int64_t i = 0; i < batch_size; ++i) {
2522
+ int64_t span = static_cast<int64_t>(cu_seqlens_ptr[i + 1]) -
2523
+ static_cast<int64_t>(cu_seqlens_ptr[i]);
2524
+ TORCH_CHECK(
2525
+ span >= 0,
2526
+ "cu_seqlens must be non-decreasing at index ",
2527
+ i);
2528
+ if (span > max_seq_len) {
2529
+ max_seq_len = span;
2530
+ }
2531
+ }
2532
+ TORCH_CHECK(
2533
+ spec_state_indices.size(1) >= max_seq_len,
2534
+ "spec_state_indices second dimension must be at least the longest "
2535
+ "cu_seqlens span; got ",
2536
+ spec_state_indices.size(1),
2537
+ " but required ",
2538
+ max_seq_len);
2539
+ int64_t spec_stride = spec_state_indices.stride(0);
2540
CHECK_EQ(v_num_heads % num_heads, 0);
2541
TORCH_CHECK(A_log.sizes() == at::IntArrayRef({v_num_heads}));
2399
- CHECK_INPUT_SHAPE_DTYPE<true>(
2400
- initial_state_source,
2401
- {initial_state_source.size(0), v_num_heads, head_dim, v_head_dim},
2402
- at::kFloat);
2542
+ TORCH_CHECK(
2543
+ initial_state_source.scalar_type() == at::kFloat ||
2544
+ initial_state_source.scalar_type() == at::kBFloat16 ||
2545
+ initial_state_source.scalar_type() == at::kHalf,
2546
+ "fused_sigmoid_gating_delta_rule_update_spec_cpu: initial_state dtype "
2547
+ "must be float16, bfloat16, or float32");
2548
+ TORCH_CHECK(
2549
+ initial_state_source.sizes() ==
2550
+ at::IntArrayRef(
2551
+ {initial_state_source.size(0), v_num_heads, head_dim, v_head_dim}),
2552
+ "Input tensor shape mismatch for initial_state_source");
2553
+ CHECK_CPU(initial_state_source);
2554
+ TORCH_CHECK(
2555
+ initial_state_source.stride(-1) == 1 &&
2556
+ initial_state_source.stride(-2) == v_head_dim &&
2557
+ initial_state_source.stride(-3) == head_dim * v_head_dim,
2558
+ "fused_sigmoid_gating_delta_rule_update_spec_cpu: expect "
2559
+ "initial_state_source to be contiguous per pool slot.");
2560
TORCH_CHECK(initial_state_source.size(0) >= batch_size,
2561
"initial_state_source capacity too small: size(0)=",
2562
initial_state_source.size(0), ", batch_size=", batch_size);
@@ -2415,39 +2572,56 @@ at::Tensor fused_sigmoid_gating_delta_rule_update_spec_cpu(
2572
at::Tensor o = at::empty({total_tokens, v_num_heads, v_head_dim}, q.options());
2573
at::Tensor qk_scale_buf = at::empty({2, total_tokens, num_heads}, at::kFloat);
2574
2418
- CPU_DISPATCH_REDUCED_FLOATING_TYPES_EXT(
2419
- q.scalar_type(), A_log.scalar_type(), "fused_sigmoid_gating_delta_rule_update_spec_kernel_impl", [&] {
2420
- fused_sigmoid_gating_delta_rule_update_spec_kernel_impl<scalar_t, param_t>(
2421
- q.data_ptr<scalar_t>(),
2422
- k.data_ptr<scalar_t>(),
2423
- v.data_ptr<scalar_t>(),
2424
- A_log.data_ptr<param_t>(),
2425
- a.data_ptr<scalar_t>(),
2426
- dt_bias.data_ptr<scalar_t>(),
2427
- b.data_ptr<scalar_t>(),
2428
- spec_state_indices.data_ptr<int32_t>(),
2429
- num_accepted_tokens.data_ptr<int32_t>(),
2430
- cu_seqlens.data_ptr<int32_t>(),
2431
- initial_state_source.data_ptr<float>(),
2432
- o.data_ptr<scalar_t>(),
2433
- qk_scale_buf.data_ptr<float>(),
2434
- total_tokens,
2435
- batch_size,
2436
- spec_stride,
2437
- num_heads,
2438
- head_dim,
2439
- v_num_heads,
2440
- v_head_dim,
2441
- q_strideT,
2442
- q_strideH,
2443
- k_strideT,
2444
- k_strideH,
2445
- v_strideT,
2446
- v_strideH,
2447
- state_slot_stride,
2448
- use_qk_l2norm_in_kernel,
2449
- softplus_threshold);
2450
- });
2575
+ auto launch = [&](auto state_tag) {
2576
+ using state_t = decltype(state_tag);
2577
+ CPU_DISPATCH_REDUCED_FLOATING_TYPES_EXT(
2578
+ q.scalar_type(), A_log.scalar_type(),
2579
+ "fused_sigmoid_gating_delta_rule_update_spec_kernel_impl", [&] {
2580
+ fused_sigmoid_gating_delta_rule_update_spec_kernel_impl<
2581
+ scalar_t, param_t, state_t>(
2582
+ q.data_ptr<scalar_t>(),
2583
+ k.data_ptr<scalar_t>(),
2584
+ v.data_ptr<scalar_t>(),
2585
+ A_log.data_ptr<param_t>(),
2586
+ a.data_ptr<scalar_t>(),
2587
+ dt_bias.data_ptr<scalar_t>(),
2588
+ b.data_ptr<scalar_t>(),
2589
+ spec_state_indices.data_ptr<int32_t>(),
2590
+ num_accepted_tokens.data_ptr<int32_t>(),
2591
+ cu_seqlens.data_ptr<int32_t>(),
2592
+ initial_state_source.data_ptr<state_t>(),
2593
+ o.data_ptr<scalar_t>(),
2594
+ qk_scale_buf.data_ptr<float>(),
2595
+ total_tokens,
2596
+ batch_size,
2597
+ spec_stride,
2598
+ num_heads,
2599
+ head_dim,
2600
+ v_num_heads,
2601
+ v_head_dim,
2602
+ q_strideT,
2603
+ q_strideH,
2604
+ k_strideT,
2605
+ k_strideH,
2606
+ v_strideT,
2607
+ v_strideH,
2608
+ state_slot_stride,
2609
+ use_qk_l2norm_in_kernel,
2610
+ softplus_threshold);
2611
+ });
2612
+ };
2613
+ if (initial_state_source.scalar_type() == at::kFloat) {
2614
+ launch(float());
2615
+ } else if (initial_state_source.scalar_type() == at::kBFloat16) {
2616
+ launch(at::BFloat16());
2617
+ } else if (initial_state_source.scalar_type() == at::kHalf) {
2618
+ launch(at::Half());
2619
+ } else {
2620
+ TORCH_CHECK(
2621
+ false,
2622
+ "fused_sigmoid_gating_delta_rule_update_spec_cpu: initial_state "
2623
+ "dtype must be float16, bfloat16, or float32");
2624
+ }
2625
return o;
2626
}
2627
tests/kernels/mamba/cpu/test_cpu_gdn_ops.py
+59
-17
@@ -37,6 +37,7 @@ CHUNK_HEAD_DIMS = [
37
CHUNK_SIZE = 64
38
CONV_DIM = 128
39
CONV_KERNEL = 4
40
+DECODE_BATCH_SIZES = [1, 3, 5]
41
PREFILL_SEQ_LENS = [
42
[1],
43
[1, 2, 3],
@@ -47,7 +48,36 @@ PREFILL_SEQ_LENS = [
48
[2 * CHUNK_SIZE - 1, 2 * CHUNK_SIZE, 2 * CHUNK_SIZE + 1],
49
[4 * CHUNK_SIZE + 17],
50
]
50
-DECODE_BATCH_SIZES = [1, 3, 5]
51
+DECODE_DTYPE_CASES = [
52
+ *[
53
+ pytest.param(
54
+ batch_size,
55
+ num_heads,
56
+ head_dims,
57
+ torch.float32,
58
+ id=f"fp32-b{batch_size}-h{num_heads}-d{head_dims}",
59
+ )
60
+ for batch_size in [1, 3, 5]
61
+ for num_heads in NUM_HEADS
62
+ for head_dims in HEAD_DIMS
63
+ ],
64
+ pytest.param(3, (2, 4), (32, 32), torch.bfloat16, id="bf16-representative"),
65
+ pytest.param(3, (2, 4), (32, 32), torch.float16, id="fp16-representative"),
66
+]
67
+PREFILL_DTYPE_CASES = [
68
+ *[
69
+ pytest.param(
70
+ num_heads,
71
+ head_dims,
72
+ torch.float32,
73
+ id=f"fp32-h{num_heads}-d{head_dims}",
74
+ )
75
+ for num_heads in NUM_HEADS
76
+ for head_dims in CHUNK_HEAD_DIMS
77
+ ],
78
+ pytest.param((2, 4), (64, 64), torch.bfloat16, id="bf16-representative"),
79
+ pytest.param((2, 4), (64, 64), torch.float16, id="fp16-representative"),
80
+]
81
82
83
@functools.lru_cache(maxsize=128, typed=False)
@@ -91,6 +121,8 @@ def ref_gated_delta_rule(
121
initial_state: torch.Tensor,
122
cu_seqlens: torch.Tensor,
123
use_qk_l2norm_in_kernel: bool = False,
124
+ state_dtype: torch.dtype = torch.float32,
125
+ state_write_interval: int = 1,
126
) -> tuple[torch.Tensor, torch.Tensor]:
127
g, beta = ref_gdn_gating(A_log, a, b, dt_bias)
128
out = torch.empty_like(value)
@@ -144,6 +176,10 @@ def ref_gated_delta_rule(
176
delta = (v_t - kv_mem) * beta_t
177
state = state + delta.unsqueeze(-1) * k_t.unsqueeze(-2)
178
out_seq[:, :, token_idx] = (state * q_t.unsqueeze(-2)).sum(dim=-1)
179
+ if state_dtype != torch.float32 and (
180
+ (token_idx + 1) % state_write_interval == 0 or token_idx + 1 == seq_len
181
+ ):
182
+ state = state.to(state_dtype).float()
183
184
out[:, begin:end] = out_seq.transpose(1, 2).contiguous().to(initial_dtype)
185
final_state[seq_idx] = state.squeeze(0)
@@ -201,14 +237,15 @@ def test_fused_gdn_gating_cpu(
237
238
239
# decode path
204
-@pytest.mark.parametrize("batch_size", DECODE_BATCH_SIZES)
205
-@pytest.mark.parametrize("num_heads", NUM_HEADS)
206
-@pytest.mark.parametrize("head_dims", HEAD_DIMS)
240
+@pytest.mark.parametrize(
241
+ "batch_size,num_heads,head_dims,state_dtype", DECODE_DTYPE_CASES
242
+)
243
@torch.inference_mode()
244
def test_fused_sigmoid_gating_delta_rule_update_cpu(
245
batch_size: int,
246
num_heads: tuple[int, int],
247
head_dims: tuple[int, int],
248
+ state_dtype: torch.dtype,
249
) -> None:
250
q, k, v, a, b, A_log, dt_bias = gdn_inputs(
251
num_tokens=batch_size,
@@ -221,7 +258,7 @@ def test_fused_sigmoid_gating_delta_rule_update_cpu(
258
cu_seqlens = torch.arange(batch_size + 1, dtype=torch.int32)
259
state_shape = (batch_size, num_v_heads, head_dim, v_head_dim)
260
state = tensor_cache(
224
- batch_size * num_v_heads * head_dim * v_head_dim, torch.float32
261
+ batch_size * num_v_heads * head_dim * v_head_dim, state_dtype
262
).view(state_shape)
263
state_ref = state[state_indices].transpose(-1, -2).contiguous()
264
@@ -236,6 +273,7 @@ def test_fused_sigmoid_gating_delta_rule_update_cpu(
273
initial_state=state_ref,
274
cu_seqlens=cu_seqlens,
275
use_qk_l2norm_in_kernel=True,
276
+ state_dtype=state_dtype,
277
)
278
out_ref = out_ref.transpose(0, 1).contiguous()
279
@@ -256,8 +294,8 @@ def test_fused_sigmoid_gating_delta_rule_update_cpu(
294
295
torch.testing.assert_close(out, out_ref, atol=1e-2, rtol=1e-2)
296
torch.testing.assert_close(
259
- state_out[state_indices].transpose(-1, -2),
260
- final_state_ref,
297
+ state_out[state_indices].transpose(-1, -2).float(),
298
+ final_state_ref.float(),
299
atol=1e-2,
300
rtol=1e-2,
301
)
@@ -265,13 +303,13 @@ def test_fused_sigmoid_gating_delta_rule_update_cpu(
303
304
# prefill path
305
@pytest.mark.parametrize("seq_lens", PREFILL_SEQ_LENS)
268
-@pytest.mark.parametrize("num_heads", NUM_HEADS)
269
-@pytest.mark.parametrize("head_dims", CHUNK_HEAD_DIMS)
306
+@pytest.mark.parametrize("num_heads,head_dims,state_dtype", PREFILL_DTYPE_CASES)
307
@torch.inference_mode()
308
def test_chunk_gated_delta_rule_cpu(
309
seq_lens: list[int],
310
num_heads: tuple[int, int],
311
head_dims: tuple[int, int],
312
+ state_dtype: torch.dtype,
313
) -> None:
314
total_tokens = sum(seq_lens)
315
q, k, v, a, b, A_log, dt_bias = gdn_inputs(
@@ -286,7 +324,7 @@ def test_chunk_gated_delta_rule_cpu(
324
)
325
initial_state_shape = (len(seq_lens), num_v_heads, head_dim, v_head_dim)
326
initial_state = tensor_cache(
289
- len(seq_lens) * num_v_heads * head_dim * v_head_dim, torch.float32
327
+ len(seq_lens) * num_v_heads * head_dim * v_head_dim, state_dtype
328
).view(initial_state_shape)
329
initial_state_ref = initial_state.transpose(-1, -2).contiguous()
330
@@ -301,6 +339,8 @@ def test_chunk_gated_delta_rule_cpu(
339
initial_state=initial_state_ref,
340
cu_seqlens=cu_seqlens,
341
use_qk_l2norm_in_kernel=True,
342
+ state_dtype=state_dtype,
343
+ state_write_interval=CHUNK_SIZE,
344
)
345
346
g, beta = ref_gdn_gating(A_log, a, b, dt_bias)
@@ -320,8 +360,8 @@ def test_chunk_gated_delta_rule_cpu(
360
361
torch.testing.assert_close(out, out_ref, atol=1e-2, rtol=1e-2)
362
torch.testing.assert_close(
323
- final_state.transpose(-1, -2),
324
- final_state_ref,
363
+ final_state.transpose(-1, -2).float(),
364
+ final_state_ref.float(),
365
atol=1e-2,
366
rtol=1e-2,
367
)
@@ -339,14 +379,16 @@ TWO_CALL_SPLITS = [
379
380
381
@pytest.mark.parametrize("total_tokens, split", TWO_CALL_SPLITS)
342
-@pytest.mark.parametrize("num_heads", NUM_HEADS)
343
-@pytest.mark.parametrize("head_dims", CHUNK_HEAD_DIMS)
382
+@pytest.mark.parametrize("num_heads", [(2, 4)])
383
+@pytest.mark.parametrize("head_dims", [(64, 64)])
384
+@pytest.mark.parametrize("state_dtype", [torch.float32, torch.bfloat16, torch.float16])
385
@torch.inference_mode()
386
def test_chunk_gated_delta_rule_cpu_two_call_split(
387
total_tokens: int,
388
split: int,
389
num_heads: tuple[int, int],
390
head_dims: tuple[int, int],
391
+ state_dtype: torch.dtype,
392
) -> None:
393
"""A prefill split into two calls (the second seeded with the first's
394
``final_state`` and a rebased ``cu_seqlens``) must match the single-call
@@ -365,7 +407,7 @@ def test_chunk_gated_delta_rule_cpu_two_call_split(
407
g = g.unsqueeze(0) # [1, T, HV]
408
beta = beta.unsqueeze(0)
409
368
- zero_state = torch.zeros(1, num_v_heads, head_dim, v_head_dim, dtype=torch.float32)
410
+ zero_state = torch.zeros(1, num_v_heads, head_dim, v_head_dim, dtype=state_dtype)
411
412
# Reference: whole sequence in one call, no initial state.
413
out_full, final_full = ops.chunk_gated_delta_rule_cpu(
@@ -405,7 +447,7 @@ def test_chunk_gated_delta_rule_cpu_two_call_split(
447
value=v[:, split:],
448
g=g[:, split:],
449
beta=beta[:, split:],
408
- initial_state=state1.to(torch.float32),
450
+ initial_state=state1,
451
output_final_state=True,
452
cu_seqlens=torch.tensor([0, tail], dtype=torch.int32),
453
head_first=False,
@@ -416,7 +458,7 @@ def test_chunk_gated_delta_rule_cpu_two_call_split(
458
out_split = torch.cat([out1, out2], dim=1)
459
460
# State must be near-exact; output allows a looser bound for the bf16 round-trip.
419
- torch.testing.assert_close(state2, final_full, atol=1e-3, rtol=1e-3)
461
+ torch.testing.assert_close(state2.float(), final_full.float(), atol=1e-3, rtol=1e-3)
462
torch.testing.assert_close(out_split, out_full, atol=2e-2, rtol=2e-2)
463
464
tests/platforms/test_cpu.py
new
+152
@@ -0,0 +1,152 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+from types import SimpleNamespace
5
+
6
+import pytest
7
+import torch
8
+
9
+from vllm.config import CacheConfig, VllmConfig
10
+from vllm.platforms.cpu import CpuPlatform
11
+
12
+
13
+class _FakeModel:
14
+ @classmethod
15
+ def get_mamba_state_dtype_from_config(
16
+ cls, vllm_config: VllmConfig
17
+ ) -> tuple[torch.dtype, torch.dtype]:
18
+ state_dtype = {
19
+ "float16": torch.float16,
20
+ "bfloat16": torch.bfloat16,
21
+ }.get(vllm_config.cache_config.mamba_ssm_cache_dtype, torch.float32)
22
+ return torch.float32, state_dtype
23
+
24
+
25
+class _FakeRegistry:
26
+ @staticmethod
27
+ def resolve_model_cls(
28
+ architecture: str,
29
+ model_config: SimpleNamespace,
30
+ ) -> tuple[type[_FakeModel], str]:
31
+ return _FakeModel, architecture
32
+
33
+
34
+def _cpu_config(
35
+ cache_config: CacheConfig,
36
+ *,
37
+ model_type: str,
38
+ resolved_dtype: str,
39
+ architecture: str | None = None,
40
+ layer_types: tuple[str, ...] = ("linear_attention",),
41
+) -> SimpleNamespace:
42
+ model_config = SimpleNamespace(
43
+ disable_cascade_attn=False,
44
+ architecture=architecture or model_type,
45
+ has_inner_state=True,
46
+ use_mla=False,
47
+ hf_text_config=SimpleNamespace(
48
+ model_type=model_type,
49
+ layer_types=layer_types,
50
+ ),
51
+ registry=_FakeRegistry,
52
+ )
53
+ config = VllmConfig(cache_config=cache_config)
54
+ config.model_config = model_config
55
+ cache_config.mamba_ssm_cache_dtype = resolved_dtype
56
+ return config
57
+
58
+
59
+@pytest.mark.parametrize(
60
+ (
61
+ "model_type",
62
+ "requested_dtype",
63
+ "resolved_dtype",
64
+ "layer_types",
65
+ "expected_dtype",
66
+ ),
67
+ [
68
+ pytest.param(
69
+ "qwen3_5",
70
+ "bfloat16",
71
+ "bfloat16",
72
+ ("linear_attention",),
73
+ "bfloat16",
74
+ id="gdn-explicit-bf16",
75
+ ),
76
+ pytest.param(
77
+ "qwen3_5",
78
+ "float16",
79
+ "float16",
80
+ ("linear_attention",),
81
+ "float16",
82
+ id="gdn-explicit-fp16",
83
+ ),
84
+ pytest.param(
85
+ "qwen3_5",
86
+ "auto",
87
+ "bfloat16",
88
+ ("linear_attention",),
89
+ "bfloat16",
90
+ id="gdn-model-bf16",
91
+ ),
92
+ pytest.param(
93
+ "qwen3_5",
94
+ "auto",
95
+ "float16",
96
+ ("linear_attention",),
97
+ "float16",
98
+ id="gdn-model-fp16",
99
+ ),
100
+ pytest.param(
101
+ "nemotron_h",
102
+ "auto",
103
+ "bfloat16",
104
+ ("mamba",),
105
+ "float32",
106
+ id="unsupported-model-bf16",
107
+ ),
108
+ pytest.param(
109
+ "nemotron_h",
110
+ "auto",
111
+ "float16",
112
+ ("mamba",),
113
+ "float32",
114
+ id="unsupported-model-fp16",
115
+ ),
116
+ pytest.param(
117
+ "nemotron_h",
118
+ "bfloat16",
119
+ "bfloat16",
120
+ ("mamba",),
121
+ "float32",
122
+ id="unsupported-explicit-bf16",
123
+ ),
124
+ pytest.param(
125
+ "nemotron_h",
126
+ "float16",
127
+ "float16",
128
+ ("mamba",),
129
+ "float32",
130
+ id="unsupported-explicit-fp16",
131
+ ),
132
+ ],
133
+)
134
+def test_cpu_accelerated_gdn_dtype_policy(
135
+ monkeypatch: pytest.MonkeyPatch,
136
+ model_type: str,
137
+ requested_dtype: str,
138
+ resolved_dtype: str,
139
+ layer_types: tuple[str, ...],
140
+ expected_dtype: str,
141
+) -> None:
142
+ monkeypatch.setattr("torch.cpu._is_avx512_bf16_supported", lambda: True)
143
+ cache_config = CacheConfig(mamba_ssm_cache_dtype=requested_dtype)
144
+ config = _cpu_config(
145
+ cache_config,
146
+ model_type=model_type,
147
+ resolved_dtype=resolved_dtype,
148
+ layer_types=layer_types,
149
+ )
150
+
151
+ CpuPlatform.check_and_update_config(config)
152
+ assert cache_config.mamba_ssm_cache_dtype == expected_dtype
vllm/platforms/cpu.py
+72
-6
@@ -31,6 +31,61 @@ else:
31
VllmConfig = None
32
33
34
+def _cpu_mamba_backend(vllm_config: VllmConfig) -> str:
35
+ """Return the CPU state backend selected by the model configuration."""
36
+ model_config = vllm_config.model_config
37
+ if model_config is None:
38
+ return "none"
39
+
40
+ hf_config = getattr(model_config, "hf_text_config", None)
41
+ model_type = str(getattr(hf_config, "model_type", "")).lower()
42
+ architecture = str(getattr(model_config, "architecture", "")).lower()
43
+ layer_types = getattr(hf_config, "layer_types", None)
44
+ has_linear_attention = isinstance(layer_types, (list, tuple)) and (
45
+ "linear_attention" in layer_types
46
+ )
47
+
48
+ try:
49
+ has_inner_state = bool(model_config.has_inner_state)
50
+ except (AttributeError, RuntimeError):
51
+ has_inner_state = False
52
+
53
+ if not (has_inner_state or has_linear_attention):
54
+ return "none"
55
+
56
+ fallback_backend = model_type or architecture or "unknown"
57
+ if not has_linear_attention:
58
+ return fallback_backend
59
+
60
+ try:
61
+ model_cls, _ = model_config.registry.resolve_model_cls(
62
+ model_config.architecture,
63
+ model_config=model_config,
64
+ )
65
+ except Exception:
66
+ return fallback_backend
67
+
68
+ try:
69
+ state_dtypes = model_cls.get_mamba_state_dtype_from_config(vllm_config)
70
+ except Exception:
71
+ return fallback_backend
72
+
73
+ if not isinstance(state_dtypes, tuple) or len(state_dtypes) != 2:
74
+ return fallback_backend
75
+
76
+ if vllm_config.cache_config.mamba_ssm_cache_dtype == "float16":
77
+ cache_dtype = torch.float16
78
+ elif vllm_config.cache_config.mamba_ssm_cache_dtype == "bfloat16":
79
+ cache_dtype = torch.bfloat16
80
+ else:
81
+ return fallback_backend
82
+
83
+ if state_dtypes[1] == cache_dtype:
84
+ return "gdn"
85
+
86
+ return fallback_backend
87
+
88
+
89
def get_max_threads(pid=0):
90
if hasattr(os, "sched_getaffinity"):
91
return len(os.sched_getaffinity(pid))
@@ -181,16 +236,27 @@ class CpuPlatform(Platform):
236
"otherwise the performance is not optimized."
237
)
238
184
- # Accelerated GDN (AMX tiles or AVX-512BF16 VDPBF16PS) requires
185
- # float32 SSM state.
239
+ # Accelerated GDN uses AMX tiles or AVX-512BF16 VDPBF16PS.
240
if (
241
torch.cpu._is_avx512_bf16_supported()
242
and cache_config.mamba_ssm_cache_dtype != "float32"
243
):
190
- cache_config.mamba_ssm_cache_dtype = "float32"
191
- logger.warning(
192
- "Reset SSM cache type to float32 for accelerated GDN mamba attention."
193
- )
244
+ mamba_backend = _cpu_mamba_backend(vllm_config)
245
+ if (
246
+ cache_config.mamba_ssm_cache_dtype in ("float16", "bfloat16")
247
+ and mamba_backend == "gdn"
248
+ ):
249
+ logger.info(
250
+ "Using %s SSM state storage for the CPU accelerated GDN backend.",
251
+ cache_config.mamba_ssm_cache_dtype,
252
+ )
253
+ else:
254
+ cache_config.mamba_ssm_cache_dtype = "float32"
255
+ logger.warning(
256
+ "Reset SSM cache type to float32 for accelerated GDN mamba "
257
+ "backend '%s'.",
258
+ mamba_backend,
259
+ )
260
261
# Lagecy setting
262
env_key = "VLLM_CPU_KVCACHE_SPACE"