[CPU][Spec Decode] Support DFlash speculative decoding for GDN models on CPU (#46090)
Signed-off-by: guybd <guy.boudoukh@intel.com> Signed-off-by: Guy Boudoukh <guy.boudoukh@intel.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Li, Jiang <jiang1.li@intel.com>
guybd committed
Jul 13, 2026 at 07:16 UTC
05fa8183a6e2c1e1c6e4643d54b9709fa29ff753
5 files changed
+755
-10
csrc/cpu/sgl-kernels/fla.cpp
+255
@@ -1116,6 +1116,164 @@ void fused_sigmoid_gating_delta_rule_update_kernel_impl(
1116
});
1117
}
1118
1119
+// Speculative-decode variant: processes a varlen batch where each sequence has
1120
+// ``q_len`` draft tokens, runs the recurrence sequentially over those tokens
1121
+// (inside the kernel, so one dispatch handles the whole draft block), reads the
1122
+// initial state from cache slot ``num_accepted-1`` and stores the state *after*
1123
+// token ``t`` into cache slot ``t`` (multi-slot rollback, matching the GPU
1124
+// kernel). Parallelized over (sequence, v_head); the per-sequence token loop is
1125
+// sequential as required by the recurrence.
1126
+template <typename scalar_t, typename param_t>
1127
+void fused_sigmoid_gating_delta_rule_update_spec_kernel_impl(
1128
+ const scalar_t* __restrict__ q_ptr, // [T, HK, EK]
1129
+ const scalar_t* __restrict__ k_ptr, // [T, HK, EK]
1130
+ const scalar_t* __restrict__ v_ptr, // [T, HV, EV]
1131
+ const param_t* __restrict__ A_log_ptr,
1132
+ const scalar_t* __restrict__ a_ptr, // [T, HV]
1133
+ const scalar_t* __restrict__ dt_bias_ptr,
1134
+ const scalar_t* __restrict__ b_ptr, // [T, HV]
1135
+ const int32_t* __restrict__ spec_indices_ptr, // [N, S]
1136
+ const int32_t* __restrict__ num_accepted_ptr, // [N]
1137
+ const int32_t* __restrict__ cu_seqlens_ptr, // [N + 1]
1138
+ float* __restrict__ state_ptr,
1139
+ scalar_t* __restrict__ o_ptr, // [T, HV, EV]
1140
+ float* __restrict__ qk_scale_buf, // [2, T, HK]
1141
+ int64_t total_tokens,
1142
+ int64_t batch_size,
1143
+ int64_t spec_stride,
1144
+ int64_t num_heads,
1145
+ int64_t head_dim,
1146
+ int64_t v_num_heads,
1147
+ int64_t v_head_dim,
1148
+ int64_t q_strideT,
1149
+ int64_t q_strideH,
1150
+ int64_t k_strideT,
1151
+ int64_t k_strideH,
1152
+ int64_t v_strideT,
1153
+ int64_t v_strideH,
1154
+ int64_t state_slot_stride,
1155
+ bool use_qk_l2norm_in_kernel,
1156
+ double softplus_threshold) {
1157
+ using bVec = at::vec::Vectorized<scalar_t>;
1158
+ using fVec = at::vec::Vectorized<float>;
1159
+ constexpr int64_t VecSize = bVec::size();
1160
+ constexpr int64_t fVecSize = fVec::size();
1161
+ int64_t group_size = v_num_heads / num_heads;
1162
+ double scale = 1 / std::sqrt((double)head_dim);
1163
+ fVec scale_vec = fVec((float)scale);
1164
+
1165
+ if (use_qk_l2norm_in_kernel) {
1166
+ float eps = 1e-5f;
1167
+ at::parallel_for(0, total_tokens * num_heads, 0, [&](int64_t begin, int64_t end) {
1168
+ for (int64_t i = begin; i < end; ++i) {
1169
+ int64_t ti = i / num_heads;
1170
+ int64_t ni = i % num_heads;
1171
+ const scalar_t* qp = q_ptr + ti * q_strideT + ni * q_strideH;
1172
+ const scalar_t* kp = k_ptr + ti * k_strideT + ni * k_strideH;
1173
+ float sq = 0.f, sk = 0.f;
1174
+ for (int64_t d = 0; d < head_dim; ++d) {
1175
+ float qv = (float)qp[d];
1176
+ sq += qv * qv;
1177
+ float kv = (float)kp[d];
1178
+ sk += kv * kv;
1179
+ }
1180
+ qk_scale_buf[ti * num_heads + ni] = 1.f / std::sqrt(sq + eps);
1181
+ qk_scale_buf[total_tokens * num_heads + ti * num_heads + ni] = 1.f / std::sqrt(sk + eps);
1182
+ }
1183
+ });
1184
+ }
1185
+
1186
+ at::parallel_for(0, batch_size * v_num_heads, 0, [&](int64_t begin, int64_t end) {
1187
+ for (int64_t idx = begin; idx < end; ++idx) {
1188
+ int64_t bi = idx / v_num_heads;
1189
+ int64_t ni = idx % v_num_heads;
1190
+ int64_t kh = ni / group_size;
1191
+ int64_t q_start = cu_seqlens_ptr[bi];
1192
+ int64_t q_len = cu_seqlens_ptr[bi + 1] - q_start;
1193
+ if (q_len <= 0) {
1194
+ continue;
1195
+ }
1196
+ int64_t acc = (int64_t)num_accepted_ptr[bi];
1197
+ // Clamp acc-1 to >=0: when num_accepted is 0 the unclamped index reads
1198
+ // out of bounds and yields an arbitrary prev_slot used to index the SSM
1199
+ // state. Mirrors the GPU guard tl.maximum(num_accepted - 1, 0).
1200
+ int64_t prev_slot =
1201
+ (int64_t)spec_indices_ptr[bi * spec_stride + (acc > 0 ? acc - 1 : 0)];
1202
+ for (int64_t t = 0; t < q_len; ++t) {
1203
+ int64_t cur_slot = (int64_t)spec_indices_ptr[bi * spec_stride + t];
1204
+ int64_t token = q_start + t;
1205
+ const float* src = state_ptr + prev_slot * state_slot_stride + ni * head_dim * v_head_dim;
1206
+ float* dst = state_ptr + cur_slot * state_slot_stride + ni * head_dim * v_head_dim;
1207
+ float g_val = -std::exp((float)A_log_ptr[ni]) *
1208
+ softplus((float)a_ptr[token * v_num_heads + ni] + (float)dt_bias_ptr[ni], softplus_threshold);
1209
+ float g_val_exp = std::exp(g_val);
1210
+ fVec g_val_exp_vec = fVec(g_val_exp);
1211
+ float beta_val = 1.f / (1.f + std::exp(-(float)b_ptr[token * v_num_heads + ni]));
1212
+ fVec beta_vec = fVec(beta_val);
1213
+ int64_t q_offset = token * q_strideT + kh * q_strideH;
1214
+ int64_t k_offset = token * k_strideT + kh * k_strideH;
1215
+ float q_scale = use_qk_l2norm_in_kernel ? qk_scale_buf[token * num_heads + kh] : 1.f;
1216
+ float k_scale =
1217
+ use_qk_l2norm_in_kernel ? qk_scale_buf[total_tokens * num_heads + token * num_heads + kh] : 1.f;
1218
+ int64_t v_offset = token * v_strideT + ni * v_strideH;
1219
+ int64_t o_offset = (token * v_num_heads + ni) * v_head_dim;
1220
+ int64_t dvi = 0;
1221
+ for (; dvi <= v_head_dim - VecSize; dvi += VecSize) {
1222
+ fVec kv_mem_vec0 = fVec(0.f);
1223
+ fVec kv_mem_vec1 = fVec(0.f);
1224
+ for (int di = 0; di < head_dim; ++di) {
1225
+ fVec k_val_vec = fVec((float)k_ptr[k_offset + di] * k_scale);
1226
+ fVec sv0 = fVec::loadu(src + di * v_head_dim + dvi);
1227
+ fVec sv1 = fVec::loadu(src + di * v_head_dim + dvi + fVecSize);
1228
+ kv_mem_vec0 = kv_mem_vec0 + sv0 * g_val_exp_vec * k_val_vec;
1229
+ kv_mem_vec1 = kv_mem_vec1 + sv1 * g_val_exp_vec * k_val_vec;
1230
+ }
1231
+ bVec v_bvec = bVec::loadu(v_ptr + v_offset + dvi);
1232
+ fVec v_vec0, v_vec1;
1233
+ std::tie(v_vec0, v_vec1) = at::vec::convert_to_float(v_bvec);
1234
+ fVec dt_vec0 = (v_vec0 - kv_mem_vec0) * beta_vec;
1235
+ fVec dt_vec1 = (v_vec1 - kv_mem_vec1) * beta_vec;
1236
+ fVec o_vec0 = fVec(0.f);
1237
+ fVec o_vec1 = fVec(0.f);
1238
+ for (int di = 0; di < head_dim; ++di) {
1239
+ fVec q_vec = fVec((float)q_ptr[q_offset + di] * q_scale);
1240
+ fVec k_vec = fVec((float)k_ptr[k_offset + di] * k_scale);
1241
+ fVec sv0 = fVec::loadu(src + di * v_head_dim + dvi);
1242
+ fVec sv1 = fVec::loadu(src + di * v_head_dim + dvi + fVecSize);
1243
+ sv0 = sv0 * g_val_exp_vec + k_vec * dt_vec0;
1244
+ sv1 = sv1 * g_val_exp_vec + k_vec * dt_vec1;
1245
+ o_vec0 = o_vec0 + sv0 * q_vec * scale_vec;
1246
+ o_vec1 = o_vec1 + sv1 * q_vec * scale_vec;
1247
+ sv0.store(dst + di * v_head_dim + dvi);
1248
+ sv1.store(dst + di * v_head_dim + dvi + fVecSize);
1249
+ }
1250
+ bVec o_vec = at::vec::convert_from_float<scalar_t>(o_vec0, o_vec1);
1251
+ o_vec.store(o_ptr + o_offset + dvi);
1252
+ }
1253
+ for (; dvi < v_head_dim; ++dvi) {
1254
+ float kv_mem_val = 0.f;
1255
+ for (int di = 0; di < head_dim; ++di) {
1256
+ float k_val = (float)k_ptr[k_offset + di] * k_scale;
1257
+ kv_mem_val += src[di * v_head_dim + dvi] * g_val_exp * k_val;
1258
+ }
1259
+ float v_val = (float)v_ptr[v_offset + dvi];
1260
+ float dt_val = (v_val - kv_mem_val) * beta_val;
1261
+ float o_val = 0.f;
1262
+ for (int di = 0; di < head_dim; ++di) {
1263
+ float q_val = (float)q_ptr[q_offset + di] * q_scale;
1264
+ float k_val = (float)k_ptr[k_offset + di] * k_scale;
1265
+ float ns = src[di * v_head_dim + dvi] * g_val_exp + k_val * dt_val;
1266
+ dst[di * v_head_dim + dvi] = ns;
1267
+ o_val += ns * q_val * scale;
1268
+ }
1269
+ o_ptr[o_offset + dvi] = (scalar_t)o_val;
1270
+ }
1271
+ prev_slot = cur_slot;
1272
+ }
1273
+ }
1274
+ });
1275
+}
1276
+
1277
template <typename scalar_t>
1278
void fused_gdn_gating_kernel_impl(
1279
float* __restrict__ A_log,
@@ -1500,6 +1658,103 @@ at::Tensor fused_sigmoid_gating_delta_rule_update_cpu(
1658
return core_attn_out;
1659
}
1660
1661
+// Speculative-decode update (multi-token, multi-slot rollback).
1662
+// q: [T, HK, EK] k: [T, HK, EK] v: [T, HV, EV]
1663
+// a: [T, HV] b: [T, HV]
1664
+// initial_state_source: [N_slots, HV, EK, EV] FP32 (updated in place)
1665
+// spec_state_indices: [batch, S] INT32 (S = num_spec + 1)
1666
+// num_accepted_tokens: [batch] INT32
1667
+// cu_seqlens: [batch + 1] INT32
1668
+// Returns output: [T, HV, EV]
1669
+at::Tensor fused_sigmoid_gating_delta_rule_update_spec_cpu(
1670
+ const at::Tensor& A_log,
1671
+ const at::Tensor& dt_bias,
1672
+ const at::Tensor& q,
1673
+ const at::Tensor& k,
1674
+ const at::Tensor& v,
1675
+ const at::Tensor& a,
1676
+ const at::Tensor& b,
1677
+ at::Tensor& initial_state_source,
1678
+ const at::Tensor& spec_state_indices,
1679
+ const at::Tensor& num_accepted_tokens,
1680
+ const at::Tensor& cu_seqlens,
1681
+ bool use_qk_l2norm_in_kernel,
1682
+ double softplus_beta = 1.0,
1683
+ double softplus_threshold = 20.0) {
1684
+ CHECK_DIM(3, q);
1685
+ CHECK_DIM(3, v);
1686
+ CHECK_LAST_DIM_CONTIGUOUS_INPUT(q);
1687
+ int64_t total_tokens = q.size(0);
1688
+ int64_t num_heads = q.size(1);
1689
+ int64_t head_dim = q.size(2);
1690
+ int64_t v_num_heads = v.size(1);
1691
+ int64_t v_head_dim = v.size(2);
1692
+ int64_t batch_size = cu_seqlens.size(0) - 1;
1693
+ int64_t spec_stride = spec_state_indices.stride(0);
1694
+ CHECK_INPUT_SHAPE_DTYPE<true>(k, {total_tokens, num_heads, head_dim}, q.scalar_type());
1695
+ CHECK_INPUT_SHAPE_DTYPE<true>(v, {total_tokens, v_num_heads, v_head_dim}, q.scalar_type());
1696
+ CHECK_INPUT_SHAPE_DTYPE<true>(a, {total_tokens, v_num_heads}, q.scalar_type());
1697
+ CHECK_INPUT_SHAPE_DTYPE<true>(b, {total_tokens, v_num_heads}, q.scalar_type());
1698
+ CHECK_INPUT_SHAPE_DTYPE<true>(dt_bias, {v_num_heads}, q.scalar_type());
1699
+ CHECK_INPUT_SHAPE_DTYPE<true>(num_accepted_tokens, {batch_size}, at::kInt);
1700
+ CHECK_INPUT_SHAPE_DTYPE<true>(cu_seqlens, {batch_size + 1}, at::kInt);
1701
+ CHECK_EQ(v_num_heads % num_heads, 0);
1702
+ TORCH_CHECK(A_log.sizes() == at::IntArrayRef({v_num_heads}));
1703
+ CHECK_INPUT_SHAPE_DTYPE<true>(
1704
+ initial_state_source,
1705
+ {initial_state_source.size(0), v_num_heads, head_dim, v_head_dim},
1706
+ at::kFloat);
1707
+ TORCH_CHECK(initial_state_source.size(0) >= batch_size,
1708
+ "initial_state_source capacity too small: size(0)=",
1709
+ initial_state_source.size(0), ", batch_size=", batch_size);
1710
+
1711
+ int64_t q_strideT = q.stride(0);
1712
+ int64_t q_strideH = q.stride(1);
1713
+ int64_t k_strideT = k.stride(0);
1714
+ int64_t k_strideH = k.stride(1);
1715
+ int64_t v_strideT = v.stride(0);
1716
+ int64_t v_strideH = v.stride(1);
1717
+ int64_t state_slot_stride = initial_state_source.stride(0);
1718
+
1719
+ at::Tensor o = at::empty({total_tokens, v_num_heads, v_head_dim}, q.options());
1720
+ at::Tensor qk_scale_buf = at::empty({2, total_tokens, num_heads}, at::kFloat);
1721
+
1722
+ CPU_DISPATCH_REDUCED_FLOATING_TYPES_EXT(
1723
+ q.scalar_type(), A_log.scalar_type(), "fused_sigmoid_gating_delta_rule_update_spec_kernel_impl", [&] {
1724
+ fused_sigmoid_gating_delta_rule_update_spec_kernel_impl<scalar_t, param_t>(
1725
+ q.data_ptr<scalar_t>(),
1726
+ k.data_ptr<scalar_t>(),
1727
+ v.data_ptr<scalar_t>(),
1728
+ A_log.data_ptr<param_t>(),
1729
+ a.data_ptr<scalar_t>(),
1730
+ dt_bias.data_ptr<scalar_t>(),
1731
+ b.data_ptr<scalar_t>(),
1732
+ spec_state_indices.data_ptr<int32_t>(),
1733
+ num_accepted_tokens.data_ptr<int32_t>(),
1734
+ cu_seqlens.data_ptr<int32_t>(),
1735
+ initial_state_source.data_ptr<float>(),
1736
+ o.data_ptr<scalar_t>(),
1737
+ qk_scale_buf.data_ptr<float>(),
1738
+ total_tokens,
1739
+ batch_size,
1740
+ spec_stride,
1741
+ num_heads,
1742
+ head_dim,
1743
+ v_num_heads,
1744
+ v_head_dim,
1745
+ q_strideT,
1746
+ q_strideH,
1747
+ k_strideT,
1748
+ k_strideH,
1749
+ v_strideT,
1750
+ v_strideH,
1751
+ state_slot_stride,
1752
+ use_qk_l2norm_in_kernel,
1753
+ softplus_threshold);
1754
+ });
1755
+ return o;
1756
+}
1757
+
1758
// A_log: [num_v_heads]
1759
// a: [batch, num_v_heads]
1760
// b: [batch, num_v_heads]
csrc/cpu/torch_bindings.cpp
+17
@@ -120,6 +120,14 @@ at::Tensor fused_sigmoid_gating_delta_rule_update_cpu(
120
bool use_qk_l2norm_in_kernel, double softplus_beta = 1.0,
121
double softplus_threshold = 20.0);
122
123
+at::Tensor fused_sigmoid_gating_delta_rule_update_spec_cpu(
124
+ const at::Tensor& A_log, const at::Tensor& dt_bias, const at::Tensor& q,
125
+ const at::Tensor& k, const at::Tensor& v, const at::Tensor& a,
126
+ const at::Tensor& b, at::Tensor& initial_state_source,
127
+ const at::Tensor& spec_state_indices, const at::Tensor& num_accepted_tokens,
128
+ const at::Tensor& cu_seqlens, bool use_qk_l2norm_in_kernel,
129
+ double softplus_beta = 1.0, double softplus_threshold = 20.0);
130
+
131
std::tuple<at::Tensor, at::Tensor> fused_gdn_gating_cpu(
132
const at::Tensor& A_log, const at::Tensor& a, const at::Tensor& b,
133
const at::Tensor& dt_bias);
@@ -508,6 +516,15 @@ TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, ops) {
516
"softplus_threshold=20.0) -> Tensor");
517
ops.impl("fused_sigmoid_gating_delta_rule_update_cpu", torch::kCPU,
518
&fused_sigmoid_gating_delta_rule_update_cpu);
519
+ ops.def(
520
+ "fused_sigmoid_gating_delta_rule_update_spec_cpu(Tensor A_log, Tensor "
521
+ "dt_bias, Tensor q, Tensor k, Tensor v, Tensor a, Tensor b, "
522
+ "Tensor(a!) initial_state_source, Tensor spec_state_indices, "
523
+ "Tensor num_accepted_tokens, Tensor cu_seqlens, bool "
524
+ "use_qk_l2norm_in_kernel, float softplus_beta=1.0, float "
525
+ "softplus_threshold=20.0) -> Tensor");
526
+ ops.impl("fused_sigmoid_gating_delta_rule_update_spec_cpu", torch::kCPU,
527
+ &fused_sigmoid_gating_delta_rule_update_spec_cpu);
528
ops.def(
529
"fused_gdn_gating_cpu(Tensor A_log, Tensor a, Tensor b, Tensor dt_bias) "
530
"-> (Tensor, Tensor)");
vllm/_custom_ops.py
+34
@@ -3252,6 +3252,40 @@ def fused_sigmoid_gating_delta_rule_update_cpu(
3252
)
3253
3254
3255
+def fused_sigmoid_gating_delta_rule_update_spec_cpu(
3256
+ A_log: torch.Tensor,
3257
+ dt_bias: torch.Tensor,
3258
+ q: torch.Tensor,
3259
+ k: torch.Tensor,
3260
+ v: torch.Tensor,
3261
+ a: torch.Tensor,
3262
+ b: torch.Tensor,
3263
+ initial_state_source: torch.Tensor,
3264
+ spec_state_indices: torch.Tensor,
3265
+ num_accepted_tokens: torch.Tensor,
3266
+ cu_seqlens: torch.Tensor,
3267
+ use_qk_l2norm_in_kernel: bool,
3268
+ softplus_beta: float = 1.0,
3269
+ softplus_threshold: float = 20.0,
3270
+) -> torch.Tensor:
3271
+ return torch.ops._C.fused_sigmoid_gating_delta_rule_update_spec_cpu(
3272
+ A_log,
3273
+ dt_bias,
3274
+ q,
3275
+ k,
3276
+ v,
3277
+ a,
3278
+ b,
3279
+ initial_state_source,
3280
+ spec_state_indices,
3281
+ num_accepted_tokens,
3282
+ cu_seqlens,
3283
+ use_qk_l2norm_in_kernel,
3284
+ softplus_beta,
3285
+ softplus_threshold,
3286
+ )
3287
+
3288
+
3289
def fused_gdn_gating_cpu(
3290
A_log: torch.Tensor,
3291
a: torch.Tensor,
vllm/model_executor/layers/mamba/ops/cpu/gdn_attention.py
+442
-9
@@ -4,6 +4,7 @@
4
from __future__ import annotations
5
6
import torch
7
+import torch.nn.functional as F
8
9
import vllm._custom_ops as ops
10
from vllm.forward_context import ForwardContext, get_forward_context
@@ -46,11 +47,52 @@ def cpu_gdn_attention_core(
47
if attn_metadata_i.num_actual_tokens == 0:
48
return
49
50
+ assert mixed_qkv.dtype == torch.bfloat16, "CPU GDN attention requires BF16."
51
+
52
+ # The conv-state cache temporal dim is ``width - 1`` when speculative
53
+ # decoding is disabled, and ``width - 1 + num_spec`` when it is enabled
54
+ # (vLLM allocates a wider rolling buffer to support draft-token rollback).
55
+ width = layer.conv1d.weight.size(-1)
56
+ conv_cache = layer.kv_cache[0]
57
+ if is_conv_state_dim_first():
58
+ # DS layout: (num_slots, dim, state_len)
59
+ state_len = conv_cache.size(-1)
60
+ else:
61
+ # SD layout: (num_slots, state_len, dim)
62
+ state_len = conv_cache.size(-2)
63
+
64
+ spec_decode_cache = state_len > (width - 1)
65
+
66
+ if not spec_decode_cache:
67
+ # ===== Fast path: speculative decoding NOT configured =====
68
+ # conv-state is exactly (..., dim, width-1); use the original AMX /
69
+ # torch implementation unchanged.
70
+ _cpu_gdn_attention_nonspec(
71
+ layer, attn_metadata_i, mixed_qkv, b, a, core_attn_out
72
+ )
73
+ return
74
+
75
+ # ===== Speculative-decoding-aware path (wide rolling conv buffer) =====
76
+ _cpu_gdn_attention_spec_aware(
77
+ layer, attn_metadata_i, mixed_qkv, b, a, core_attn_out, width, state_len
78
+ )
79
+
80
+
81
+# ---------------------------------------------------------------------------
82
+# Non-speculative implementation: conv-state is exactly width-1.
83
+# ---------------------------------------------------------------------------
84
+def _cpu_gdn_attention_nonspec(
85
+ layer,
86
+ attn_metadata_i: GDNAttentionMetadata,
87
+ mixed_qkv: torch.Tensor,
88
+ b: torch.Tensor,
89
+ a: torch.Tensor,
90
+ core_attn_out: torch.Tensor,
91
+) -> None:
92
assert (
93
attn_metadata_i.spec_sequence_masks is None
94
and attn_metadata_i.num_accepted_tokens is None
52
- ), "speculative decode not supported in CPU GDN attention."
53
- assert mixed_qkv.dtype == torch.bfloat16, "CPU GDN attention requires BF16."
95
+ ), "speculative decode not supported without a wide conv-state cache."
96
97
state_indices_tensor = attn_metadata_i.non_spec_state_indices_tensor
98
query_start_loc = attn_metadata_i.non_spec_query_start_loc
@@ -79,12 +121,7 @@ def cpu_gdn_attention_core(
121
b = b.contiguous()
122
123
num_allocated_slots, head_num, v_dim, k_dim = ssm_state.size()
82
- ssm_state = ssm_state.view(
83
- num_allocated_slots,
84
- head_num,
85
- k_dim,
86
- v_dim,
87
- )
124
+ ssm_state = ssm_state.view(num_allocated_slots, head_num, k_dim, v_dim)
125
126
num_decodes = attn_metadata_i.num_decodes
127
num_decode_tokens = attn_metadata_i.num_decode_tokens
@@ -109,7 +146,6 @@ def cpu_gdn_attention_core(
146
)
147
else:
148
decode_conv_state = conv_state[decode_state_indices].contiguous()
112
-
149
decode_mixed_qkv = causal_conv1d_update_torch(
150
# [B, dim] -> [B, dim, 1]
151
x=decode_mixed_qkv.unsqueeze(-1),
@@ -207,6 +243,403 @@ def cpu_gdn_attention_core(
243
core_attn_out[prefill_token_start:prefill_token_end] = attn_out.squeeze(0)
244
245
246
+# ---------------------------------------------------------------------------
247
+# Speculative-decoding-aware implementation (wide rolling conv buffer).
248
+#
249
+# Mirrors the GPU `_forward_core` spec path using torch + the existing CPU
250
+# kernels. The conv-state is a per-sequence rolling buffer of length
251
+# ``state_len = width - 1 + num_spec`` (single cache slot, column 0 of the
252
+# spec block table), and the SSM recurrent state is stored multi-slot: the
253
+# state *after* draft token ``t`` lives in block-table column ``t``, so the
254
+# next step can resume from the slot of the last accepted token.
255
+# ---------------------------------------------------------------------------
256
+def _conv_buffer_view(layer) -> torch.Tensor:
257
+ """Return the conv-state cache as (num_slots, dim, state_len)."""
258
+ conv_cache = layer.kv_cache[0]
259
+ if is_conv_state_dim_first():
260
+ return conv_cache # (num_slots, dim, state_len)
261
+ return conv_cache.transpose(-1, -2) # view -> (num_slots, dim, state_len)
262
+
263
+
264
+def _ssm_state_view(layer) -> torch.Tensor:
265
+ ssm_state = layer.kv_cache[1]
266
+ num_slots, head_num, v_dim, k_dim = ssm_state.size()
267
+ return ssm_state.view(num_slots, head_num, k_dim, v_dim)
268
+
269
+
270
+def _unpacked_conv_weight(layer) -> torch.Tensor:
271
+ """Return the plain (dim, width) conv weight.
272
+
273
+ On AMX the conv1d weight is VNNI-packed in place at load time (only usable
274
+ by the AMX C++ kernel), so the torch spec-decode path relies on the
275
+ un-packed copy stashed by ``dispatch_cpu_unquantized_gemm``.
276
+ """
277
+ w = getattr(layer.conv1d, "_cpu_unpacked_conv_weight", None)
278
+ if w is not None:
279
+ return w
280
+ w = layer.conv1d.weight
281
+ if w.dim() == 3:
282
+ return w.view(w.size(0), w.size(2))
283
+ return w
284
+
285
+
286
+def _cpu_gdn_attention_spec_aware(
287
+ layer,
288
+ attn_metadata_i: GDNAttentionMetadata,
289
+ mixed_qkv: torch.Tensor,
290
+ b: torch.Tensor,
291
+ a: torch.Tensor,
292
+ core_attn_out: torch.Tensor,
293
+ width: int,
294
+ state_len: int,
295
+) -> None:
296
+ mixed_qkv = mixed_qkv.contiguous()
297
+ a = a.contiguous()
298
+ b = b.contiguous()
299
+
300
+ spec_sequence_masks = attn_metadata_i.spec_sequence_masks
301
+ conv_buf = _conv_buffer_view(layer) # (num_slots, dim, state_len)
302
+ ssm_state = _ssm_state_view(layer)
303
+
304
+ if spec_sequence_masks is None:
305
+ # No spec sequences in this batch (e.g. the prompt prefill step while
306
+ # speculative decoding is configured). Process as prefill/decode using
307
+ # torch conv (which only touches the first ``width-1`` columns of the
308
+ # wide buffer, leaving the rolling history untouched).
309
+ _spec_aware_nonspec(
310
+ layer,
311
+ attn_metadata_i,
312
+ mixed_qkv,
313
+ b,
314
+ a,
315
+ core_attn_out,
316
+ conv_buf,
317
+ ssm_state,
318
+ width,
319
+ )
320
+ return
321
+
322
+ # Split spec vs non-spec tokens.
323
+ spec_token_indx = attn_metadata_i.spec_token_indx
324
+ non_spec_token_indx = attn_metadata_i.non_spec_token_indx
325
+ num_prefills = attn_metadata_i.num_prefills
326
+ num_decodes = attn_metadata_i.num_decodes
327
+
328
+ if num_prefills == 0 and num_decodes == 0:
329
+ mixed_qkv_spec = mixed_qkv
330
+ b_spec = b
331
+ a_spec = a
332
+ spec_out_indx = None
333
+ else:
334
+ assert spec_token_indx is not None
335
+ mixed_qkv_spec = mixed_qkv.index_select(0, spec_token_indx)
336
+ b_spec = b.index_select(0, spec_token_indx)
337
+ a_spec = a.index_select(0, spec_token_indx)
338
+ spec_out_indx = spec_token_indx
339
+
340
+ spec_out = _spec_forward(
341
+ layer,
342
+ attn_metadata_i,
343
+ mixed_qkv_spec,
344
+ b_spec,
345
+ a_spec,
346
+ conv_buf,
347
+ ssm_state,
348
+ width,
349
+ state_len,
350
+ )
351
+
352
+ # Process the (rare) non-spec subset (prefill) tokens, if any.
353
+ nonspec_out = None
354
+ if (num_prefills > 0 or num_decodes > 0) and non_spec_token_indx is not None:
355
+ mixed_qkv_ns = mixed_qkv.index_select(0, non_spec_token_indx)
356
+ b_ns = b.index_select(0, non_spec_token_indx)
357
+ a_ns = a.index_select(0, non_spec_token_indx)
358
+ nonspec_out = _spec_aware_nonspec_subset(
359
+ layer, attn_metadata_i, mixed_qkv_ns, b_ns, a_ns, conv_buf, ssm_state, width
360
+ )
361
+
362
+ # Scatter outputs back to their token positions.
363
+ if spec_out_indx is None:
364
+ core_attn_out[: spec_out.size(0)] = spec_out
365
+ else:
366
+ core_attn_out.index_copy_(0, spec_out_indx, spec_out)
367
+ if nonspec_out is not None:
368
+ core_attn_out.index_copy_(0, non_spec_token_indx, nonspec_out)
369
+
370
+
371
+def _spec_forward(
372
+ layer,
373
+ attn_metadata_i: GDNAttentionMetadata,
374
+ mixed_qkv_spec: torch.Tensor,
375
+ b_spec: torch.Tensor,
376
+ a_spec: torch.Tensor,
377
+ conv_buf: torch.Tensor,
378
+ ssm_state: torch.Tensor,
379
+ width: int,
380
+ state_len: int,
381
+) -> torch.Tensor:
382
+ """Run the GDN core for the multi-query (speculative) tokens.
383
+
384
+ Returns the core attention output for the spec tokens, in the same token
385
+ order as ``mixed_qkv_spec`` (i.e. ordered by ``spec_query_start_loc``).
386
+ """
387
+ num_spec_decodes = attn_metadata_i.num_spec_decodes
388
+ spec_state_indices = attn_metadata_i.spec_state_indices_tensor
389
+ spec_qsl = attn_metadata_i.spec_query_start_loc
390
+ num_accepted = attn_metadata_i.num_accepted_tokens
391
+ assert spec_state_indices is not None
392
+ assert spec_qsl is not None
393
+ assert num_accepted is not None
394
+
395
+ spec_qsl_cpu = spec_qsl[: num_spec_decodes + 1].to("cpu", torch.int64)
396
+ num_acc_cpu = num_accepted[:num_spec_decodes].to("cpu", torch.int64)
397
+ seq_starts = spec_qsl_cpu[:-1]
398
+ seq_lens = spec_qsl_cpu[1:] - spec_qsl_cpu[:-1]
399
+
400
+ # ---- 1. Convolution (per-sequence rolling buffer) ----
401
+ w2d = _unpacked_conv_weight(layer) # (dim, width)
402
+ dim = w2d.size(0)
403
+ w = w2d.unsqueeze(1) # (dim, 1, width) for F.conv1d depthwise
404
+ bias = layer.conv1d.bias
405
+ silu = layer.activation == "silu"
406
+
407
+ conv_out = torch.empty_like(mixed_qkv_spec)
408
+ col0 = spec_state_indices[:, 0].to("cpu", torch.int64)
409
+ for i in range(num_spec_decodes):
410
+ q_i = int(seq_lens[i].item())
411
+ if q_i == 0:
412
+ continue
413
+ start = int(seq_starts[i].item())
414
+ slot0 = int(col0[i].item())
415
+ a_prev = int(num_acc_cpu[i].item())
416
+ offset = a_prev - 1
417
+ B = conv_buf[slot0] # (dim, state_len)
418
+ x_seq = mixed_qkv_spec[start : start + q_i].transpose(0, 1).to(B.dtype)
419
+ prior = B[:, offset : offset + (width - 1)]
420
+ conv_in = torch.cat([prior, x_seq], dim=-1).unsqueeze(0) # (1, dim, w-1+q)
421
+ out = F.conv1d(conv_in, w, bias, groups=dim)[0] # (dim, q_i)
422
+ if silu:
423
+ out = F.silu(out)
424
+ conv_out[start : start + q_i] = out.transpose(0, 1).to(conv_out.dtype)
425
+ # Roll the buffer: drop ``a_prev`` from the front, append the new
426
+ # draft tokens, keep total length == state_len.
427
+ keep = B[:, offset + 1 : offset + 1 + (state_len - q_i)]
428
+ new_B = torch.cat([keep, x_seq], dim=-1)
429
+ B.copy_(new_B)
430
+
431
+ # ---- 2. Recurrent (multi-slot SSM state) ----
432
+ # Single fused kernel call: it runs the recurrence over each sequence's
433
+ # draft tokens internally, resumes from slot ``num_accepted-1`` and stores
434
+ # the state after token ``t`` into slot ``t`` (rollback for the next step).
435
+ query, key, value = layer.rearrange_mixed_qkv(conv_out)
436
+ query = query.squeeze(0).contiguous()
437
+ key = key.squeeze(0).contiguous()
438
+ value = value.squeeze(0).contiguous()
439
+ spec_idx = spec_state_indices[:num_spec_decodes].to(torch.int32).contiguous()
440
+ num_acc = num_accepted[:num_spec_decodes].to(torch.int32).contiguous()
441
+ cu = spec_qsl[: num_spec_decodes + 1].to(torch.int32).contiguous()
442
+ out_spec = ops.fused_sigmoid_gating_delta_rule_update_spec_cpu(
443
+ A_log=layer.A_log,
444
+ dt_bias=layer.dt_bias,
445
+ q=query,
446
+ k=key,
447
+ v=value,
448
+ a=a_spec.contiguous(),
449
+ b=b_spec.contiguous(),
450
+ initial_state_source=ssm_state,
451
+ spec_state_indices=spec_idx,
452
+ num_accepted_tokens=num_acc,
453
+ cu_seqlens=cu,
454
+ use_qk_l2norm_in_kernel=True,
455
+ )
456
+ return out_spec
457
+
458
+
459
+def core_attn_out_like(layer, mixed_qkv_spec: torch.Tensor) -> torch.Tensor:
460
+ num_tokens = mixed_qkv_spec.size(0)
461
+ return torch.zeros(
462
+ (num_tokens, layer.num_v_heads // layer.tp_size, layer.head_v_dim),
463
+ dtype=mixed_qkv_spec.dtype,
464
+ device=mixed_qkv_spec.device,
465
+ )
466
+
467
+
468
+def _spec_aware_nonspec(
469
+ layer,
470
+ attn_metadata_i: GDNAttentionMetadata,
471
+ mixed_qkv: torch.Tensor,
472
+ b: torch.Tensor,
473
+ a: torch.Tensor,
474
+ core_attn_out: torch.Tensor,
475
+ conv_buf: torch.Tensor,
476
+ ssm_state: torch.Tensor,
477
+ width: int,
478
+) -> None:
479
+ """Non-spec prefill/decode with a wide conv buffer (torch path)."""
480
+ state_indices_tensor = attn_metadata_i.non_spec_state_indices_tensor
481
+ query_start_loc = attn_metadata_i.non_spec_query_start_loc
482
+ assert state_indices_tensor is not None
483
+ assert query_start_loc is not None
484
+
485
+ conv_weights = _unpacked_conv_weight(layer)
486
+
487
+ num_decodes = attn_metadata_i.num_decodes
488
+ num_decode_tokens = attn_metadata_i.num_decode_tokens
489
+ num_prefills = attn_metadata_i.num_prefills
490
+ num_prefill_tokens = attn_metadata_i.num_prefill_tokens
491
+
492
+ if num_decodes > 0:
493
+ decode_mixed_qkv = mixed_qkv[:num_decode_tokens]
494
+ decode_b = b[:num_decode_tokens]
495
+ decode_a = a[:num_decode_tokens]
496
+ decode_state_indices = state_indices_tensor[:num_decodes]
497
+ # Only the first ``width-1`` columns hold the real conv state.
498
+ decode_conv_state = conv_buf[decode_state_indices][
499
+ :, :, : width - 1
500
+ ].contiguous()
501
+ decode_mixed_qkv = causal_conv1d_update_torch(
502
+ x=decode_mixed_qkv.unsqueeze(-1),
503
+ conv_state=decode_conv_state,
504
+ weight=conv_weights,
505
+ bias=layer.conv1d.bias,
506
+ activation=layer.activation,
507
+ ).squeeze(-1)
508
+ conv_buf[decode_state_indices, :, : width - 1] = decode_conv_state
509
+
510
+ query, key, value = layer.rearrange_mixed_qkv(decode_mixed_qkv)
511
+ # rearrange_mixed_qkv can return views whose last dim is not
512
+ # contiguous; the fused CPU kernel requires a contiguous last dim.
513
+ query = query.contiguous()
514
+ key = key.contiguous()
515
+ value = value.contiguous()
516
+ attn_out = ops.fused_sigmoid_gating_delta_rule_update_cpu(
517
+ A_log=layer.A_log,
518
+ dt_bias=layer.dt_bias,
519
+ q=query,
520
+ k=key,
521
+ v=value,
522
+ a=decode_a.contiguous(),
523
+ b=decode_b.contiguous(),
524
+ initial_state_source=ssm_state,
525
+ initial_state_indices=decode_state_indices,
526
+ cu_seqlens=query_start_loc[: num_decodes + 1],
527
+ use_qk_l2norm_in_kernel=True,
528
+ )
529
+ core_attn_out[:num_decode_tokens] = attn_out.squeeze(1)
530
+
531
+ if num_prefills > 0:
532
+ has_initial_state = attn_metadata_i.has_initial_state
533
+ assert has_initial_state is not None
534
+ prefill_token_start = num_decode_tokens
535
+ prefill_token_end = prefill_token_start + num_prefill_tokens
536
+ prefill_mixed_qkv = mixed_qkv[prefill_token_start:prefill_token_end]
537
+ prefill_b = b[prefill_token_start:prefill_token_end]
538
+ prefill_a = a[prefill_token_start:prefill_token_end]
539
+ prefill_state_indices = state_indices_tensor[
540
+ num_decodes : num_decodes + num_prefills
541
+ ]
542
+ prefill_query_start_loc = (
543
+ query_start_loc[num_decodes : num_decodes + num_prefills + 1]
544
+ - num_decode_tokens
545
+ )
546
+ prefill_has_initial_state = has_initial_state[
547
+ num_decodes : num_decodes + num_prefills
548
+ ]
549
+ # ``causal_conv1d_torch`` only touches columns [:width-1] of the buffer.
550
+ prefill_mixed_qkv = causal_conv1d_torch(
551
+ x=prefill_mixed_qkv.transpose(0, 1),
552
+ weight=conv_weights,
553
+ bias=layer.conv1d.bias,
554
+ conv_states=conv_buf,
555
+ query_start_loc=prefill_query_start_loc,
556
+ cache_indices=prefill_state_indices,
557
+ has_initial_state=prefill_has_initial_state,
558
+ activation=layer.activation,
559
+ ).transpose(0, 1)
560
+
561
+ query, key, value = layer.rearrange_mixed_qkv(prefill_mixed_qkv)
562
+ g, beta = ops.fused_gdn_gating_cpu(
563
+ A_log=layer.A_log, a=prefill_a, b=prefill_b, dt_bias=layer.dt_bias
564
+ )
565
+ initial_state = ssm_state[prefill_state_indices]
566
+ initial_state[~prefill_has_initial_state, ...] = 0
567
+ attn_out, last_recurrent_state = ops.chunk_gated_delta_rule_cpu(
568
+ query=query,
569
+ key=key,
570
+ value=value,
571
+ g=g,
572
+ beta=beta,
573
+ initial_state=initial_state,
574
+ output_final_state=True,
575
+ cu_seqlens=prefill_query_start_loc,
576
+ head_first=False,
577
+ use_qk_l2norm_in_kernel=True,
578
+ )
579
+ ssm_state[prefill_state_indices] = last_recurrent_state.to(
580
+ ssm_state.dtype, copy=False
581
+ )
582
+ core_attn_out[prefill_token_start:prefill_token_end] = attn_out.squeeze(0)
583
+
584
+
585
+def _spec_aware_nonspec_subset(
586
+ layer,
587
+ attn_metadata_i: GDNAttentionMetadata,
588
+ mixed_qkv: torch.Tensor,
589
+ b: torch.Tensor,
590
+ a: torch.Tensor,
591
+ conv_buf: torch.Tensor,
592
+ ssm_state: torch.Tensor,
593
+ width: int,
594
+) -> torch.Tensor:
595
+ """Process non-spec (prefill) tokens that coexist with spec sequences.
596
+
597
+ Returns outputs ordered like ``non_spec_token_indx``.
598
+ """
599
+ out = core_attn_out_like(layer, mixed_qkv)
600
+ has_initial_state = attn_metadata_i.has_initial_state
601
+ prefill_state_indices = attn_metadata_i.prefill_state_indices
602
+ prefill_qsl = attn_metadata_i.prefill_query_start_loc
603
+ assert prefill_state_indices is not None and prefill_qsl is not None
604
+ assert has_initial_state is not None
605
+
606
+ conv_weights = _unpacked_conv_weight(layer)
607
+ conv_out = causal_conv1d_torch(
608
+ x=mixed_qkv.transpose(0, 1),
609
+ weight=conv_weights,
610
+ bias=layer.conv1d.bias,
611
+ conv_states=conv_buf,
612
+ query_start_loc=prefill_qsl,
613
+ cache_indices=prefill_state_indices,
614
+ has_initial_state=has_initial_state,
615
+ activation=layer.activation,
616
+ ).transpose(0, 1)
617
+
618
+ query, key, value = layer.rearrange_mixed_qkv(conv_out)
619
+ g, beta = ops.fused_gdn_gating_cpu(
620
+ A_log=layer.A_log, a=a, b=b, dt_bias=layer.dt_bias
621
+ )
622
+ initial_state = ssm_state[prefill_state_indices]
623
+ initial_state[~has_initial_state, ...] = 0
624
+ attn_out, last_recurrent_state = ops.chunk_gated_delta_rule_cpu(
625
+ query=query,
626
+ key=key,
627
+ value=value,
628
+ g=g,
629
+ beta=beta,
630
+ initial_state=initial_state,
631
+ output_final_state=True,
632
+ cu_seqlens=prefill_qsl,
633
+ head_first=False,
634
+ use_qk_l2norm_in_kernel=True,
635
+ )
636
+ ssm_state[prefill_state_indices] = last_recurrent_state.to(
637
+ ssm_state.dtype, copy=False
638
+ )
639
+ out[:] = attn_out.squeeze(0)
640
+ return out
641
+
642
+
643
def cpu_gdn_attention_core_fake(
644
mixed_qkv: torch.Tensor,
645
b: torch.Tensor,
vllm/model_executor/layers/utils.py
+7
-1
@@ -239,12 +239,18 @@ def dispatch_cpu_unquantized_gemm(
239
# For now it should be a causal_conv1d op
240
if torch.cpu._is_amx_tile_supported():
241
# prepack conv weight
242
- layer.weight.data = ops.causal_conv1d_weight_pack(
242
+ unpacked = (
243
layer.weight.view(
244
layer.weight.size(0),
245
layer.weight.size(2),
246
)
247
+ .contiguous()
248
+ .clone()
249
)
250
+ # Stash the un-packed (dim, width) weight so the speculative-decode
251
+ # GDN path (which uses torch conv, not the AMX kernel) can use it.
252
+ layer._cpu_unpacked_conv_weight = unpacked
253
+ layer.weight.data = ops.causal_conv1d_weight_pack(unpacked)
254
return
255
256
N, K = layer.weight.size()