[Hardware][CPU] Enable granite-4 model on cpu (#47641)

Signed-off-by: Akash Kaothalkar <akashkaothalkar@akashs-mbp.bl1-in.ibm.com> Signed-off-by: Akash Kaothalkar <akashkaothalkar@dhcp-9-123-5-76.bl1-in.ibm.com> Signed-off-by: Akash Kaothalkar <akashkaothalkar@Akashs-MBP.lan> Signed-off-by: Akash kaothalkar <akash.kaothalkar@ibm.com> Co-authored-by: Akash Kaothalkar <akashkaothalkar@dhcp-9-123-5-76.bl1-in.ibm.com> Co-authored-by: Akash Kaothalkar <akashkaothalkar@Akashs-MBP.lan> Co-authored-by: Akash Kaothalkar <akashkaothalkar@akashs-mbp.bl1-in.ibm.com> Co-authored-by: Akash kaothalkar <akash.kaothalkar@ibm.com> Co-authored-by: Li, Jiang <jiang1.li@intel.com>

Akash kaothalkar committed Jul 20, 2026 at 11:45 UTC 5c9f6557d7845b52917ef013011c83618439b17f
21 files changed +1283 -62
.buildkite/hardware_tests/cpu.yaml
+5 -1
@@ -18,6 +18,8 @@ steps:
18 - tests/kernels/quantization/test_cpu_fp8_scaled_mm.py
19 - tests/kernels/mamba/cpu/test_cpu_gdn_ops.py
20 - tests/kernels/mamba/test_cpu_short_conv.py
21 + - tests/kernels/mamba/test_causal_conv1d.py
22 + - tests/kernels/mamba/test_mamba_ssm.py
23 commands:
24 - |
25 bash .buildkite/scripts/hardware_ci/run-cpu-test.sh 30m "
@@ -28,7 +30,9 @@ steps:
30 pytest -x -v -s tests/kernels/test_onednn.py
31 pytest -x -v -s tests/kernels/test_awq_int4_to_int8.py
32 pytest -x -v -s tests/kernels/quantization/test_cpu_fp8_scaled_mm.py
31 - pytest -x -v -s tests/kernels/mamba/cpu/test_cpu_gdn_ops.py"
33 + pytest -x -v -s tests/kernels/mamba/cpu/test_cpu_gdn_ops.py
34 + pytest -x -v -s tests/kernels/mamba/test_causal_conv1d.py
35 + pytest -x -v -s tests/kernels/mamba/test_mamba_ssm.py"
36
37 # Note: SDE can't be downloaded from CI host because of AWS WAF
38 # - label: CPU-Compatibility Tests
.buildkite/scripts/hardware_ci/run-cpu-test-arm.sh
+4 -1
@@ -40,7 +40,9 @@ function cpu_tests() {
40 pytest -x -v -s tests/kernels/moe/test_cpu_fused_moe.py
41 pytest -x -v -s tests/kernels/mamba/cpu/test_cpu_gdn_ops.py
42 pytest -x -v -s tests/kernels/moe/test_cpu_int4_moe.py
43 - pytest -x -v -s tests/kernels/mamba/test_cpu_short_conv.py"
43 + pytest -x -v -s tests/kernels/mamba/test_cpu_short_conv.py
44 + pytest -x -v -s tests/kernels/mamba/test_causal_conv1d.py
45 + pytest -x -v -s tests/kernels/mamba/test_mamba_ssm.py"
46
47 # skip tests requiring model downloads if HF_TOKEN is not set
48 # due to rate-limits
@@ -97,3 +99,4 @@ function cpu_tests() {
99 # All of CPU tests are expected to be finished less than 40 mins.
100 export -f cpu_tests
101 timeout 2h bash -c cpu_tests
102 +
cmake/cpu_extension.cmake
+3
@@ -430,6 +430,7 @@ set(VLLM_EXT_SRC
430 "csrc/cpu/layernorm.cpp"
431 "csrc/cpu/mla_decode.cpp"
432 "csrc/cpu/pos_encoding.cpp"
433 + "csrc/cpu/mamba_cpu.cpp"
434 "csrc/moe/dynamic_4bit_int_moe_cpu.cpp"
435 "csrc/cpu/cpu_attn.cpp"
436 "csrc/cpu/torch_bindings.cpp")
@@ -489,6 +490,7 @@ if (ENABLE_X86_ISA)
490 "csrc/cpu/spec_decode_utils.cpp"
491 "csrc/cpu/cpu_attn.cpp"
492 "csrc/cpu/dnnl_kernels.cpp"
493 + "csrc/cpu/mamba_cpu.cpp"
494 "csrc/cpu/torch_bindings.cpp"
495 # TODO: Remove these files
496 "csrc/cpu/activation.cpp"
@@ -502,6 +504,7 @@ if (ENABLE_X86_ISA)
504 "csrc/cpu/utils.cpp"
505 "csrc/cpu/spec_decode_utils.cpp"
506 "csrc/cpu/cpu_attn.cpp"
507 + "csrc/cpu/mamba_cpu.cpp"
508 "csrc/cpu/dnnl_kernels.cpp"
509 "csrc/cpu/torch_bindings.cpp"
510 # TODO: Remove these files
csrc/cpu/cpu_types_vsx.hpp
+8 -7
@@ -336,13 +336,14 @@ struct FP32Vec8 : public Vec<FP32Vec8> {
336 reg.val[1] = fp16_to_fp32_bits(raw_lo);
337 }
338 float reduce_sum() const {
339 - AliasReg ar;
340 - ar.reg = reg;
341 - float result = 0;
342 - unroll_loop<int, VEC_ELEM_NUM>(
343 - [&result, &ar](int i) { result += ar.values[i]; });
344 -
345 - return result;
339 + // VSX horizontal reduction: 3 vector ops instead of 8 scalar adds.
340 + // Step 1: pairwise sum of the two 4-wide halves
341 + __vector float s = vec_add(reg.val[0], reg.val[1]);
342 + // Step 2: rotate by 8 bytes (2 floats) and add
343 + s = vec_add(s, vec_sld(s, s, 8));
344 + // Step 3: rotate by 4 bytes (1 float) and add => all lanes hold total
345 + s = vec_add(s, vec_sld(s, s, 4));
346 + return vec_extract(s, 0);
347 }
348 FP32Vec8 exp() const {
349 f32x4x2_t out;
csrc/cpu/mamba_cpu.cpp new
+285
@@ -0,0 +1,285 @@
1 +// SPDX-License-Identifier: Apache-2.0
2 +// SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +//
4 +// CPU at::Tensor wrappers for Mamba decode-step kernels defined in
5 +// mamba_kernels.hpp.
6 +
7 +#include "cpu/mamba_kernels.hpp"
8 +
9 +#include <ATen/ATen.h>
10 +#include <torch/library.h>
11 +#include <c10/util/Optional.h>
12 +
13 +#include "cpu_types.hpp"
14 +
15 +// ---------------------------------------------------------------------------
16 +// causal_conv1d_update
17 +// ---------------------------------------------------------------------------
18 +at::Tensor causal_conv1d_update_cpu_impl(
19 + at::Tensor& x, at::Tensor& conv_state, const at::Tensor& weight,
20 + const c10::optional<at::Tensor>& bias,
21 + const c10::optional<std::string>& activation,
22 + const c10::optional<at::Tensor>& conv_state_indices,
23 + const c10::optional<at::Tensor>& query_start_loc, int64_t pad_slot_id) {
24 + bool do_silu = false;
25 + if (activation.has_value()) {
26 + const std::string& act = activation.value();
27 + do_silu = (act == "silu" || act == "swish");
28 + }
29 +
30 + at::ScalarType dtype = x.scalar_type();
31 +
32 + // Input x: contiguous in native dtype.
33 + at::Tensor x_c = x.is_contiguous() ? x : x.contiguous();
34 +
35 + // conv_state: NEVER copy the full paged tensor just for layout reasons.
36 + // If the dtype matches we work directly on conv_state (contiguous or not)
37 + // by extracting strides and passing them to the kernel.
38 + // Only a dtype-conversion copy is made when types differ (rare for BF16).
39 + bool state_type_ok = (conv_state.scalar_type() == dtype);
40 + at::Tensor state_c = state_type_ok ? conv_state : conv_state.to(dtype);
41 + // state_c and conv_state may be non-contiguous — that is intentional.
42 +
43 + // Weight: coerce to same dtype if needed (should match in practice)
44 + at::Tensor w_c =
45 + (weight.scalar_type() != dtype)
46 + ? weight.to(dtype).contiguous()
47 + : (weight.is_contiguous() ? weight : weight.contiguous());
48 +
49 + // Bias stays float32 (small scalar, used only for fp32 accumulation)
50 + at::Tensor bias_f32;
51 + if (bias.has_value() && bias.value().defined())
52 + bias_f32 = bias.value().to(at::kFloat).contiguous();
53 +
54 + int64_t batch = x_c.size(0);
55 + int64_t dim = x_c.size(1);
56 + int64_t seqlen = (x_c.dim() == 3) ? x_c.size(2) : 1;
57 + int64_t width = w_c.size(1);
58 + int64_t state_len = state_c.size(2);
59 +
60 + // Extract strides — works for contiguous AND non-contiguous (transposed)
61 + // state. stride(0): between cache slots (e.g. num_slots × dim × width-1 in
62 + // contiguous) stride(1): between conv channels (dim stride) stride(2):
63 + // between state elements (=1 when contiguous, =dim when transposed)
64 + int64_t stride_s_slot = state_c.stride(0);
65 + int64_t stride_s_dim = state_c.stride(1);
66 + int64_t stride_s_state = state_c.stride(2);
67 +
68 + at::Tensor out = x_c.clone(); // native dtype, no float32 alloc
69 +
70 + const int32_t* cache_idx_ptr = nullptr;
71 + at::Tensor cache_idx_int;
72 + if (conv_state_indices.has_value()) {
73 + cache_idx_int = conv_state_indices.value().to(at::kInt).contiguous();
74 + cache_idx_ptr = cache_idx_int.data_ptr<int32_t>();
75 + }
76 +
77 + VLLM_DISPATCH_FLOATING_TYPES(dtype, "causal_conv1d_update", [&] {
78 + mamba_cpu::causal_conv1d_update_kernel<scalar_t>(
79 + x_c.data_ptr<scalar_t>(), state_c.data_ptr<scalar_t>(), stride_s_slot,
80 + stride_s_dim, stride_s_state, w_c.data_ptr<scalar_t>(),
81 + bias_f32.defined() ? bias_f32.data_ptr<float>() : nullptr,
82 + out.data_ptr<scalar_t>(), cache_idx_ptr,
83 + static_cast<int32_t>(pad_slot_id), batch, dim, seqlen, width, state_len,
84 + do_silu);
85 + });
86 +
87 + // Write back only when a type-conversion copy was made.
88 + // Layout-only non-contiguity is handled via strides above — no copy needed.
89 + if (!state_type_ok) conv_state.copy_(state_c);
90 +
91 + return out;
92 +}
93 +
94 +// ---------------------------------------------------------------------------
95 +// selective_state_update
96 +// ---------------------------------------------------------------------------
97 +void selective_state_update_cpu_impl(
98 + at::Tensor& state, // (nstates, nheads, dim, dstate)
99 + const at::Tensor& x, // (N, nheads, dim)
100 + const at::Tensor& dt, const at::Tensor& A, const at::Tensor& B,
101 + const at::Tensor& C, const c10::optional<at::Tensor>& D,
102 + const c10::optional<at::Tensor>& z,
103 + const c10::optional<at::Tensor>& dt_bias, bool dt_softplus,
104 + const c10::optional<at::Tensor>& state_batch_indices,
105 + const c10::optional<at::Tensor>& dst_state_batch_indices,
106 + int64_t null_block_id, at::Tensor& out,
107 + const c10::optional<at::Tensor>& num_accepted_tokens,
108 + const c10::optional<at::Tensor>& cu_seqlens) {
109 + at::ScalarType state_type = state.scalar_type();
110 + at::ScalarType input_type = x.scalar_type();
111 +
112 + // x, B, C must be contiguous and match input_type
113 + auto ensure_input = [input_type](const at::Tensor& t) -> at::Tensor {
114 + at::Tensor r = (t.scalar_type() != input_type) ? t.to(input_type) : t;
115 + return r.is_contiguous() ? r : r.contiguous();
116 + };
117 + at::Tensor x_in = ensure_input(x);
118 + at::Tensor B_in = ensure_input(B);
119 + at::Tensor C_in = ensure_input(C);
120 + at::Tensor z_in;
121 + if (z.has_value() && z.value().defined()) z_in = ensure_input(z.value());
122 +
123 + // A, D, dt_bias are float32 model parameters that arrive here as expanded
124 + // tensors, e.g. A is (nheads, head_dim, dstate) with strides (1, 0, 0).
125 + // We need just the scalar value per head as a (nheads,) 1-D array so that
126 + // A_ptr[h] in the kernel correctly reads head h's value.
127 + //
128 + // Strategy: peel trailing expanded (stride=0) dims via .select(), which is
129 + // a zero-copy view. For A: (nheads, head_dim, dstate) strides (1,0,0)
130 + // → .select(2,0) → (nheads, head_dim) strides (1,0)
131 + // → .select(1,0) → (nheads,) stride (1,) ← contiguous, free.
132 + // No allocation, no type conversion (A is already float32).
133 + auto to_per_head_1d_f32 = [](const at::Tensor& t) -> at::Tensor {
134 + at::Tensor r = t;
135 + // Peel trailing dimensions that are broadcast (stride=0 or size=1)
136 + while (r.dim() > 1) r = r.select(r.dim() - 1, 0);
137 + if (r.scalar_type() != at::kFloat) r = r.to(at::kFloat);
138 + return r.is_contiguous() ? r : r.contiguous();
139 + };
140 +
141 + at::Tensor A_f32 = to_per_head_1d_f32(A); // (nheads,) float32
142 + at::Tensor D_f32, dt_bias_f32;
143 + if (D.has_value() && D.value().defined())
144 + D_f32 = to_per_head_1d_f32(D.value());
145 + if (dt_bias.has_value() && dt_bias.value().defined())
146 + dt_bias_f32 = to_per_head_1d_f32(dt_bias.value());
147 +
148 + // dt: reduce (N, nheads, head_dim) expanded tensor → (N, nheads) BEFORE
149 + // the type conversion so we convert head_dim x fewer elements.
150 + at::Tensor dt_f32;
151 + {
152 + // If dt was expanded to (N, nheads, head_dim) with stride-0 in dim 2,
153 + // take a zero-copy view of index 0 along that dim first.
154 + at::Tensor t2 = (dt.dim() == 3) ? dt.select(2, 0) : dt; // (N, nheads)
155 + at::Tensor t3 = (t2.scalar_type() != at::kFloat) ? t2.to(at::kFloat) : t2;
156 + dt_f32 = t3.is_contiguous() ? t3 : t3.contiguous();
157 + }
158 +
159 + int64_t nheads = state.size(1);
160 + int64_t dim = state.size(2);
161 + int64_t dstate = state.size(3);
162 + int64_t N = (cu_seqlens.has_value() && cu_seqlens.value().defined())
163 + ? cu_seqlens.value().size(0) - 1
164 + : x_in.size(0);
165 + int64_t ngroups = B_in.size(1);
166 +
167 + // Strides
168 + int64_t stride_state_n = state.stride(0);
169 + int64_t stride_state_h = state.stride(1);
170 + int64_t stride_state_d = state.stride(2);
171 + int64_t stride_x_n = x_in.stride(0);
172 + int64_t stride_x_h = x_in.stride(1);
173 + int64_t stride_dt_n = dt_f32.stride(0); // dt is (N, nheads)
174 + int64_t stride_BC_n = B_in.stride(0);
175 + int64_t stride_BC_g = B_in.stride(1);
176 + int64_t stride_out_n = out.stride(0);
177 + int64_t stride_out_h = out.stride(1);
178 +
179 + // Optional index pointers
180 + auto get_int32_ptr =
181 + [](const c10::optional<at::Tensor>& opt) -> const int32_t* {
182 + return (opt.has_value() && opt.value().defined())
183 + ? opt.value().data_ptr<int32_t>()
184 + : nullptr;
185 + };
186 + const int32_t* sbi_ptr = get_int32_ptr(state_batch_indices);
187 + const int32_t* dsbi_ptr = get_int32_ptr(dst_state_batch_indices);
188 + const int32_t* nat_ptr = get_int32_ptr(num_accepted_tokens);
189 + const int32_t* csl_ptr = get_int32_ptr(cu_seqlens);
190 +
191 + // Dispatch on (state_t, input_t, out_t): write directly into `out`
192 + // without any intermediate float32 buffer.
193 + VLLM_DISPATCH_FLOATING_TYPES(state_type, "ssu_state", [&] {
194 + using state_t = scalar_t;
195 + VLLM_DISPATCH_FLOATING_TYPES(input_type, "ssu_input", [&] {
196 + using input_t = scalar_t;
197 + VLLM_DISPATCH_FLOATING_TYPES(out.scalar_type(), "ssu_out", [&] {
198 + using out_t = scalar_t;
199 + mamba_cpu::selective_state_update_kernel<state_t, input_t, out_t>(
200 + state.data_ptr<state_t>(), stride_state_n, stride_state_h,
201 + stride_state_d, x_in.data_ptr<input_t>(), stride_x_n, stride_x_h,
202 + dt_f32.data_ptr<float>(), stride_dt_n, A_f32.data_ptr<float>(),
203 + B_in.data_ptr<input_t>(), C_in.data_ptr<input_t>(), stride_BC_n,
204 + stride_BC_g, D_f32.defined() ? D_f32.data_ptr<float>() : nullptr,
205 + z_in.defined() ? z_in.data_ptr<input_t>() : nullptr,
206 + dt_bias_f32.defined() ? dt_bias_f32.data_ptr<float>() : nullptr,
207 + out.data_ptr<out_t>(), stride_out_n, stride_out_h, sbi_ptr,
208 + dsbi_ptr, static_cast<int32_t>(null_block_id), nat_ptr, csl_ptr, N,
209 + nheads, ngroups, dim, dstate, dt_softplus);
210 + });
211 + });
212 + });
213 +}
214 +
215 +// ---------------------------------------------------------------------------
216 +// mamba_chunk_scan_fwd_cpu
217 +// ---------------------------------------------------------------------------
218 +void mamba_chunk_scan_fwd_cpu_impl(
219 + at::Tensor& out, // [seqlen, nheads, headdim] — pre-allocated by caller
220 + at::Tensor&
221 + final_states, // [batch, nheads, headdim, dstate] float32 contiguous
222 + const at::Tensor& x, // [seqlen, nheads, headdim]
223 + const at::Tensor&
224 + dt, // [seqlen, nheads] float32 (preprocessed: bias+softplus+clamp)
225 + const at::Tensor& A, // [nheads] float32
226 + const at::Tensor& B, // [seqlen, ngroups, dstate]
227 + const at::Tensor& C, // [seqlen, ngroups, dstate]
228 + const c10::optional<at::Tensor>& D, // [nheads] float32 (optional)
229 + const c10::optional<at::Tensor>& z, // [seqlen, nheads, headdim] (optional)
230 + const at::Tensor& cu_seqlens // [batch+1] int32
231 +) {
232 + const at::ScalarType input_type = x.scalar_type();
233 +
234 + auto ensure_contig = [input_type](const at::Tensor& t) -> at::Tensor {
235 + at::Tensor r = (t.scalar_type() != input_type) ? t.to(input_type) : t;
236 + return r.is_contiguous() ? r : r.contiguous();
237 + };
238 + at::Tensor x_in = ensure_contig(x);
239 + at::Tensor B_in = ensure_contig(B);
240 + at::Tensor C_in = ensure_contig(C);
241 + at::Tensor z_in;
242 + if (z.has_value() && z.value().defined()) z_in = ensure_contig(z.value());
243 +
244 + // A and D are float32 model parameters, potentially broadcast-expanded.
245 + // Strip trailing broadcast dims to get a contiguous (nheads,) array.
246 + auto to_per_head_f32 = [](const at::Tensor& t) -> at::Tensor {
247 + at::Tensor r = t;
248 + while (r.dim() > 1) r = r.select(r.dim() - 1, 0);
249 + if (r.scalar_type() != at::kFloat) r = r.to(at::kFloat);
250 + return r.is_contiguous() ? r : r.contiguous();
251 + };
252 + at::Tensor A_f32 = to_per_head_f32(A);
253 + at::Tensor D_f32;
254 + if (D.has_value() && D.value().defined()) D_f32 = to_per_head_f32(D.value());
255 +
256 + // dt: [seqlen, nheads] float32 — caller has applied bias+softplus+clamp in
257 + // Python.
258 + at::Tensor dt_c = dt.is_contiguous() ? dt : dt.contiguous();
259 + if (dt_c.scalar_type() != at::kFloat) dt_c = dt_c.to(at::kFloat);
260 +
261 + at::Tensor cu_int = cu_seqlens.to(at::kInt).contiguous();
262 +
263 + const int64_t batch = final_states.size(0);
264 + const int64_t nheads = final_states.size(1);
265 + const int64_t headdim = final_states.size(2);
266 + const int64_t dstate = final_states.size(3);
267 + const int64_t ngroups = B_in.size(1);
268 +
269 + TORCH_CHECK(final_states.is_contiguous(),
270 + "mamba_chunk_scan_fwd_cpu: final_states must be contiguous");
271 + TORCH_CHECK(out.is_contiguous(),
272 + "mamba_chunk_scan_fwd_cpu: out must be contiguous (writes via "
273 + "raw data_ptr)");
274 +
275 + VLLM_DISPATCH_FLOATING_TYPES(input_type, "mamba_chunk_scan_fwd_cpu", [&] {
276 + mamba_cpu::mamba_chunk_scan_fwd_kernel<scalar_t>(
277 + final_states.data_ptr<float>(), x_in.data_ptr<scalar_t>(),
278 + dt_c.data_ptr<float>(), A_f32.data_ptr<float>(),
279 + B_in.data_ptr<scalar_t>(), C_in.data_ptr<scalar_t>(),
280 + D_f32.defined() ? D_f32.data_ptr<float>() : nullptr,
281 + z_in.defined() ? z_in.data_ptr<scalar_t>() : nullptr,
282 + out.data_ptr<scalar_t>(), cu_int.data_ptr<int32_t>(), batch, nheads,
283 + ngroups, headdim, dstate);
284 + });
285 +}
csrc/cpu/mamba_kernels.hpp new
+382
@@ -0,0 +1,382 @@
1 +// SPDX-License-Identifier: Apache-2.0
2 +// SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +//
4 +// Fused CPU vector kernels for Mamba decode-step hotspots:
5 +// - causal_conv1d_update (depthwise 1-D conv state roll + compute)
6 +// - selective_state_update (SSM recurrence, single-step)
7 +
8 +#pragma once
9 +
10 +#include "cpu_types.hpp"
11 +#include <cmath>
12 +#include <cstring>
13 +#include <cstdint>
14 +#include <algorithm>
15 +
16 +namespace mamba_cpu {
17 +
18 +// ---------------------------------------------------------------------------
19 +// causal_conv1d_update — templated for native BF16/FP32
20 +//
21 +// state_ptr may point to a NON-CONTIGUOUS paged KV cache tensor.
22 +// Explicit strides are passed so the kernel writes directly into the
23 +// correct memory locations without making a contiguous copy of the full
24 +// paged tensor (which was the source of the 34-41% direct_copy_kernel).
25 +//
26 +// stride_s_slot = state.stride(0) — between cache slots
27 +// stride_s_dim = state.stride(1) — between conv_dim channels
28 +// stride_s_state = state.stride(2) — between state elements
29 +//
30 +// When stride_s_state == 1 (contiguous), the memmove fast path is used.
31 +// ---------------------------------------------------------------------------
32 +template <typename scalar_t>
33 +inline void causal_conv1d_update_kernel(
34 + const scalar_t* __restrict__ x_ptr, scalar_t* __restrict__ state_ptr,
35 + int64_t stride_s_slot, int64_t stride_s_dim, int64_t stride_s_state,
36 + const scalar_t* __restrict__ weight_ptr, const float* __restrict__ bias_ptr,
37 + scalar_t* __restrict__ out_ptr, const int32_t* __restrict__ cache_idxs,
38 + int32_t pad_slot_id, int64_t batch, int64_t dim, int64_t seqlen,
39 + int64_t width, int64_t state_len, bool do_silu) {
40 +#pragma omp parallel for
41 + for (int64_t b = 0; b < batch; ++b) {
42 + int64_t cache_idx = (cache_idxs != nullptr) ? cache_idxs[b] : b;
43 + if (cache_idx == pad_slot_id) continue;
44 +
45 + for (int64_t t = 0; t < seqlen; ++t) {
46 + const scalar_t* x_b = x_ptr + (b * dim * seqlen + t);
47 + scalar_t* out_b = out_ptr + (b * dim * seqlen + t);
48 + // Base of this slot in the (possibly non-contiguous) paged state
49 + scalar_t* s_base = state_ptr + cache_idx * stride_s_slot;
50 +
51 + for (int64_t d = 0; d < dim; ++d) {
52 + float x_val = static_cast<float>(x_b[d * seqlen]);
53 + scalar_t* sd = s_base + d * stride_s_dim; // start of this dim's state
54 + const scalar_t* w = weight_ptr + d * width;
55 +
56 + // Accumulate in float32 for precision
57 + float acc = (bias_ptr != nullptr) ? bias_ptr[d] : 0.0f;
58 + for (int64_t k = 0; k < state_len; ++k) {
59 + acc += static_cast<float>(w[k]) *
60 + static_cast<float>(sd[k * stride_s_state]);
61 + }
62 + acc += static_cast<float>(w[state_len]) * x_val;
63 +
64 + // Shift state left and append new input.
65 + // Use memmove when contiguous (stride==1); element loop otherwise.
66 + if (stride_s_state == 1) {
67 + if (state_len > 1)
68 + std::memmove(sd, sd + 1, (state_len - 1) * sizeof(scalar_t));
69 + if (state_len > 0) sd[state_len - 1] = static_cast<scalar_t>(x_val);
70 + } else {
71 + for (int64_t k = 0; k < state_len - 1; ++k)
72 + sd[k * stride_s_state] = sd[(k + 1) * stride_s_state];
73 + if (state_len > 0)
74 + sd[(state_len - 1) * stride_s_state] = static_cast<scalar_t>(x_val);
75 + }
76 +
77 + if (do_silu) {
78 + float sigmoid = (acc >= 0) ? 1.0f / (1.0f + std::exp(-acc))
79 + : std::exp(acc) / (1.0f + std::exp(acc));
80 + acc *= sigmoid;
81 + }
82 + out_b[d * seqlen] = static_cast<scalar_t>(acc);
83 + }
84 + }
85 + }
86 +}
87 +
88 +// ---------------------------------------------------------------------------
89 +// selective_state_update
90 +//
91 +// Template parameters:
92 +// state_t - dtype of ssm_state cache (typically BFloat16)
93 +// input_t - dtype of x, B, C (typically BFloat16)
94 +// out_t - dtype of output tensor (typically BFloat16)
95 +// Write directly — no float32 intermediate buffer needed.
96 +//
97 +// A, D, dt_bias are accepted as const float* (they are always float32
98 +// model parameters in Mamba2). This eliminates the per-call float32→BF16
99 +// conversion and the .contiguous() materialisation of the broadcast-expand.
100 +//
101 +// dt is accepted as a (N, nheads) scalar-per-head tensor, not as the
102 +// (N, nheads, head_dim) expansion, so no .contiguous() copy is needed.
103 +// ---------------------------------------------------------------------------
104 +template <typename state_t, typename input_t, typename out_t = float>
105 +inline void selective_state_update_kernel(
106 + state_t* __restrict__ state_ptr, int64_t stride_state_n,
107 + int64_t stride_state_h, int64_t stride_state_d,
108 + const input_t* __restrict__ x_ptr, int64_t stride_x_n, int64_t stride_x_h,
109 + // dt: (N, nheads) — scalar per head, NOT expanded to head_dim
110 + const float* __restrict__ dt_ptr, int64_t stride_dt_n,
111 + // A: (nheads,) float32 — scalar per head
112 + const float* __restrict__ A_ptr, const input_t* __restrict__ B_ptr,
113 + const input_t* __restrict__ C_ptr, int64_t stride_BC_n, int64_t stride_BC_g,
114 + // D: (nheads,) float32 — scalar per head (nullptr if not used)
115 + const float* __restrict__ D_ptr,
116 + // z: same shape as x (optional)
117 + const input_t* __restrict__ z_ptr,
118 + // dt_bias: (nheads,) float32 — scalar per head (nullptr if not used)
119 + const float* __restrict__ dt_bias_ptr, out_t* __restrict__ out_ptr,
120 + int64_t stride_out_n, int64_t stride_out_h,
121 + const int32_t* __restrict__ state_batch_indices,
122 + const int32_t* __restrict__ dst_state_batch_indices, int32_t null_block_id,
123 + const int32_t* __restrict__ num_accepted_tokens,
124 + const int32_t* __restrict__ cu_seqlens, int64_t N, int64_t nheads,
125 + int64_t ngroups, int64_t dim, int64_t dstate, bool dt_softplus) {
126 + using state_vec_t = vec_op::vec_t<state_t>;
127 + using input_vec_t = vec_op::vec_t<input_t>;
128 + constexpr int VEC_ELEM_NUM = 8;
129 +
130 + int64_t nheads_per_group = nheads / ngroups;
131 +
132 + for (int64_t seq_idx = 0; seq_idx < N; ++seq_idx) {
133 + int64_t bos, seq_len;
134 + if (cu_seqlens != nullptr) {
135 + bos = cu_seqlens[seq_idx];
136 + seq_len = cu_seqlens[seq_idx + 1] - bos;
137 + } else {
138 + bos = seq_idx;
139 + seq_len = 1;
140 + }
141 +
142 + int64_t state_read_idx = (state_batch_indices != nullptr)
143 + ? state_batch_indices[seq_idx]
144 + : seq_idx;
145 + if (state_read_idx == null_block_id) continue;
146 +
147 + int64_t state_write_idx = (num_accepted_tokens == nullptr)
148 + ? ((dst_state_batch_indices != nullptr)
149 + ? dst_state_batch_indices[seq_idx]
150 + : state_read_idx)
151 + : -1;
152 +
153 + state_t* s = state_ptr + state_read_idx * stride_state_n;
154 +
155 + for (int64_t t = 0; t < seq_len; ++t) {
156 + int64_t token_idx = bos + t;
157 + const input_t* x_tok = x_ptr + token_idx * stride_x_n;
158 + // dt: (N, nheads) — one float per head per token
159 + const float* dt_tok = dt_ptr + token_idx * stride_dt_n;
160 + const input_t* B_tok = B_ptr + token_idx * stride_BC_n;
161 + const input_t* C_tok = C_ptr + token_idx * stride_BC_n;
162 + out_t* out_tok = out_ptr + token_idx * stride_out_n;
163 +
164 +#pragma omp parallel for
165 + for (int64_t h = 0; h < nheads; ++h) {
166 + int64_t g = h / nheads_per_group;
167 + const input_t* x_h = x_tok + h * stride_x_h;
168 + const input_t* B_g = B_tok + g * stride_BC_g;
169 + const input_t* C_g = C_tok + g * stride_BC_g;
170 + out_t* out_h = out_tok + h * stride_out_h;
171 + state_t* s_h = s + h * stride_state_h;
172 +
173 + // Read scalars-per-head (A, dt, dt_bias, D) — no per-dim indexing
174 + float dt_val = dt_tok[h];
175 + if (dt_bias_ptr != nullptr) dt_val += dt_bias_ptr[h];
176 + if (dt_softplus) {
177 + dt_val = (dt_val <= 20.0f) ? std::log1p(std::exp(dt_val)) : dt_val;
178 + }
179 + const float A_val = A_ptr[h]; // scalar: same for all dim, dstate
180 + const float D_val = (D_ptr != nullptr) ? D_ptr[h] : 0.0f;
181 +
182 + const input_t* z_h =
183 + (z_ptr != nullptr) ? z_ptr + token_idx * stride_x_n + h * stride_x_h
184 + : nullptr;
185 +
186 + vec_op::FP32Vec8 dt_vec(dt_val);
187 + // dA = exp(A * dt): A and dt are SCALARS per head, so compute once
188 + // and broadcast. This saves 7 redundant std::exp() calls that
189 + // FP32Vec8::exp() would otherwise make on the broadcast vector.
190 + const float dA_scalar = std::exp(A_val * dt_val);
191 + vec_op::FP32Vec8 dA(dA_scalar); // broadcast
192 +
193 + for (int64_t d = 0; d < dim; ++d) {
194 + float x_val = static_cast<float>(x_h[d]);
195 +
196 + vec_op::FP32Vec8 out_vec(0.0f);
197 + state_t* s_hd = s_h + d * stride_state_d;
198 + const input_t* B_g_base = B_g;
199 + const input_t* C_g_base = C_g;
200 +
201 + vec_op::FP32Vec8 x_vec(x_val);
202 + // dBx = B * x * dt — same dA for all dstate (A is scalar)
203 + // s_new = s * dA + B * x * dt
204 +
205 + int64_t n = 0;
206 + for (; n <= dstate - VEC_ELEM_NUM; n += VEC_ELEM_NUM) {
207 + vec_op::FP32Vec8 B_v((input_vec_t(B_g_base + n)));
208 + vec_op::FP32Vec8 C_v((input_vec_t(C_g_base + n)));
209 + vec_op::FP32Vec8 s_v((state_vec_t(s_hd + n)));
210 +
211 + vec_op::FP32Vec8 dBx = B_v * x_vec * dt_vec;
212 + vec_op::FP32Vec8 s_new = s_v * dA + dBx;
213 +
214 + state_vec_t(s_new).save(s_hd + n);
215 + out_vec = out_vec + s_new * C_v;
216 + }
217 +
218 + float out_val = out_vec.reduce_sum();
219 + for (; n < dstate; ++n) {
220 + // Reuse dA_scalar computed once per head — no exp() re-call
221 + float dBx = static_cast<float>(B_g[n]) * x_val * dt_val;
222 + float s_new = static_cast<float>(s_hd[n]) * dA_scalar + dBx;
223 + s_hd[n] = static_cast<state_t>(s_new);
224 + out_val += s_new * static_cast<float>(C_g[n]);
225 + }
226 +
227 + if (D_ptr != nullptr) out_val += x_val * D_val;
228 + if (z_h != nullptr) {
229 + float z_val = static_cast<float>(z_h[d]);
230 + float sigmoid = (z_val >= 0)
231 + ? 1.0f / (1.0f + std::exp(-z_val))
232 + : std::exp(z_val) / (1.0f + std::exp(z_val));
233 + out_val *= z_val * sigmoid;
234 + }
235 + out_h[d] = static_cast<out_t>(out_val);
236 + }
237 + }
238 +
239 + if (num_accepted_tokens != nullptr &&
240 + dst_state_batch_indices != nullptr) {
241 + int64_t token_dst_idx = dst_state_batch_indices[seq_idx * seq_len + t];
242 + if (token_dst_idx != null_block_id && token_dst_idx != state_read_idx) {
243 + state_t* dst_s = state_ptr + token_dst_idx * stride_state_n;
244 + std::memmove(dst_s, s, nheads * stride_state_h * sizeof(state_t));
245 + }
246 + }
247 + }
248 +
249 + if (num_accepted_tokens == nullptr && state_write_idx != null_block_id &&
250 + state_write_idx != state_read_idx) {
251 + state_t* dst_s = state_ptr + state_write_idx * stride_state_n;
252 + std::memmove(dst_s, s, nheads * stride_state_h * sizeof(state_t));
253 + }
254 + }
255 +}
256 +
257 +// ---------------------------------------------------------------------------
258 +// mamba_chunk_scan_fwd
259 +//
260 +// Prefill SSM recurrence for Mamba2 / SSD models.
261 +//
262 +// Key difference from selective_state_update_kernel (decode path):
263 +// - #pragma omp parallel for collapse(2) is OUTSIDE the time loop.
264 +// Each thread owns a (batch, head) slice and runs the entire token
265 +// sequence without any per-token OpenMP synchronisation overhead.
266 +// For seqlen=256, this eliminates 256 thread-barrier launches per batch.
267 +//
268 +// `dt` arrives already processed (float32, after bias + softplus + clamp)
269 +// to keep this kernel simple. Preprocessing is done in the Python wrapper.
270 +//
271 +// `states_ptr` points to the [batch, nheads, headdim, dstate] float32 output
272 +// tensor, pre-initialised by the caller (zero or from initial_states).
273 +// Each (b, h) slice is private to exactly one thread via collapse(2), so
274 +// there are no write conflicts.
275 +//
276 +// D is treated as a scalar per head ([nheads] float32).
277 +// ---------------------------------------------------------------------------
278 +template <typename input_t>
279 +inline void mamba_chunk_scan_fwd_kernel(
280 + float* __restrict__ states_ptr, // [batch, nheads, headdim, dstate] f32
281 + const input_t* __restrict__ x_ptr, // [seqlen, nheads, headdim]
282 + const float* __restrict__ dt_ptr, // [seqlen, nheads] f32 (preprocessed)
283 + const float* __restrict__ A_ptr, // [nheads] f32
284 + const input_t* __restrict__ B_ptr, // [seqlen, ngroups, dstate]
285 + const input_t* __restrict__ C_ptr, // [seqlen, ngroups, dstate]
286 + const float* __restrict__ D_ptr, // [nheads] f32 (nullable)
287 + const input_t* __restrict__ z_ptr, // [seqlen, nheads, headdim] (nullable)
288 + input_t* __restrict__ out_ptr, // [seqlen, nheads, headdim]
289 + const int32_t* __restrict__ cu_seqlens, // [batch+1] int32
290 + int64_t batch, int64_t nheads, int64_t ngroups, int64_t headdim,
291 + int64_t dstate) {
292 + using input_vec_t = vec_op::vec_t<input_t>;
293 + constexpr int VEC_ELEM_NUM = 8;
294 +
295 + const int64_t nheads_per_group = nheads / ngroups;
296 + // states layout: [batch, nheads, headdim, dstate] contiguous (caller
297 + // guarantee)
298 + const int64_t stride_s_b = nheads * headdim * dstate;
299 + const int64_t stride_s_h = headdim * dstate;
300 + // stride_s_d = dstate, stride_s_n = 1
301 +
302 +#pragma omp parallel for collapse(2) schedule(static)
303 + for (int64_t b = 0; b < batch; ++b) {
304 + for (int64_t h = 0; h < nheads; ++h) {
305 + const int64_t seq_start = cu_seqlens[b];
306 + const int64_t seq_end = cu_seqlens[b + 1];
307 + const int64_t g = h / nheads_per_group;
308 +
309 + const float A_val = A_ptr[h];
310 + const float D_val = (D_ptr != nullptr) ? D_ptr[h] : 0.0f;
311 +
312 + // Working state slice: states[b, h, :, :] — float32, headdim * dstate.
313 + // Fits in L1/L2 for typical dims (e.g. 64*128*4 = 32 KB).
314 + float* s_bh = states_ptr + b * stride_s_b + h * stride_s_h;
315 +
316 + for (int64_t t = seq_start; t < seq_end; ++t) {
317 + const input_t* x_h = x_ptr + t * nheads * headdim + h * headdim;
318 + const float* dt_h = dt_ptr + t * nheads + h;
319 + const input_t* B_g = B_ptr + t * ngroups * dstate + g * dstate;
320 + const input_t* C_g = C_ptr + t * ngroups * dstate + g * dstate;
321 + const input_t* z_h = (z_ptr != nullptr)
322 + ? z_ptr + t * nheads * headdim + h * headdim
323 + : nullptr;
324 + input_t* out_h = out_ptr + t * nheads * headdim + h * headdim;
325 +
326 + const float dt_val = *dt_h;
327 + const float dA_val = std::exp(A_val * dt_val);
328 + const vec_op::FP32Vec8 dA_vec(dA_val); // broadcast scalar
329 + const vec_op::FP32Vec8 dt_vec(dt_val);
330 +
331 + for (int64_t d = 0; d < headdim; ++d) {
332 + const float x_val = static_cast<float>(x_h[d]);
333 + float* s_bhd = s_bh + d * dstate; // [dstate] contiguous float32
334 +
335 + // Vectorised SSM update + readout over dstate:
336 + // s_new = s * dA + x * dt * B
337 + // y += s_new * C
338 + int64_t n = 0;
339 + vec_op::FP32Vec8 y_vec(0.0f);
340 + const vec_op::FP32Vec8 x_vec(x_val);
341 +
342 + for (; n <= dstate - VEC_ELEM_NUM; n += VEC_ELEM_NUM) {
343 + const vec_op::FP32Vec8 B_v((input_vec_t(B_g + n)));
344 + const vec_op::FP32Vec8 C_v((input_vec_t(C_g + n)));
345 + const vec_op::FP32Vec8 s_v(s_bhd + n);
346 +
347 + const vec_op::FP32Vec8 s_new = s_v * dA_vec + x_vec * dt_vec * B_v;
348 + s_new.save(s_bhd + n);
349 + y_vec = y_vec + s_new * C_v;
350 + }
351 +
352 + float y_val = y_vec.reduce_sum();
353 +
354 + // Scalar tail for remaining dstate elements
355 + for (; n < dstate; ++n) {
356 + const float B_n = static_cast<float>(B_g[n]);
357 + const float C_n = static_cast<float>(C_g[n]);
358 + const float s_new = s_bhd[n] * dA_val + x_val * dt_val * B_n;
359 + s_bhd[n] = s_new;
360 + y_val += s_new * C_n;
361 + }
362 +
363 + // D skip connection (scalar per head)
364 + if (D_ptr != nullptr) y_val += x_val * D_val;
365 +
366 + // z gating: out = y * z * sigmoid(z) (SiLU)
367 + if (z_h != nullptr) {
368 + const float z_val = static_cast<float>(z_h[d]);
369 + const float sigmoid =
370 + (z_val >= 0.0f) ? 1.0f / (1.0f + std::exp(-z_val))
371 + : std::exp(z_val) / (1.0f + std::exp(z_val));
372 + y_val *= z_val * sigmoid;
373 + }
374 +
375 + out_h[d] = static_cast<input_t>(y_val);
376 + }
377 + }
378 + }
379 + }
380 +}
381 +
382 +} // namespace mamba_cpu
csrc/cpu/torch_bindings.cpp
+50
@@ -213,6 +213,32 @@ void compute_slot_mapping_kernel_impl(const torch::Tensor query_start_loc,
213 torch::Tensor slot_mapping,
214 const int64_t block_size);
215
216 +at::Tensor causal_conv1d_update_cpu_impl(
217 + at::Tensor& x, at::Tensor& conv_state, const at::Tensor& weight,
218 + const c10::optional<at::Tensor>& bias,
219 + const c10::optional<std::string>& activation,
220 + const c10::optional<at::Tensor>& conv_state_indices,
221 + const c10::optional<at::Tensor>& query_start_loc, int64_t pad_slot_id);
222 +
223 +void selective_state_update_cpu_impl(
224 + at::Tensor& state, const at::Tensor& x, const at::Tensor& dt,
225 + const at::Tensor& A, const at::Tensor& B, const at::Tensor& C,
226 + const c10::optional<at::Tensor>& D, const c10::optional<at::Tensor>& z,
227 + const c10::optional<at::Tensor>& dt_bias, bool dt_softplus,
228 + const c10::optional<at::Tensor>& state_batch_indices,
229 + const c10::optional<at::Tensor>& dst_state_batch_indices,
230 + int64_t null_block_id, at::Tensor& out,
231 + const c10::optional<at::Tensor>& num_accepted_tokens,
232 + const c10::optional<at::Tensor>& cu_seqlens);
233 +
234 +void mamba_chunk_scan_fwd_cpu_impl(at::Tensor& out, at::Tensor& final_states,
235 + const at::Tensor& x, const at::Tensor& dt,
236 + const at::Tensor& A, const at::Tensor& B,
237 + const at::Tensor& C,
238 + const c10::optional<at::Tensor>& D,
239 + const c10::optional<at::Tensor>& z,
240 + const at::Tensor& cu_seqlens);
241 +
242 void init_cpu_memory_env(std::vector<int64_t> node_ids);
243
244 namespace cpu_utils {
@@ -595,6 +621,30 @@ TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, ops) {
621 "block_size) -> ()",
622 &compute_slot_mapping_kernel_impl);
623
624 + // Mamba CPU kernels
625 + ops.def(
626 + "causal_conv1d_update_cpu_vec("
627 + "Tensor(a0!) x, Tensor(a1!) conv_state, Tensor weight, "
628 + "Tensor? bias, str? activation, Tensor? conv_state_indices, "
629 + "Tensor? query_start_loc, SymInt pad_slot_id) -> Tensor",
630 + &causal_conv1d_update_cpu_impl);
631 +
632 + ops.def(
633 + "selective_state_update_cpu("
634 + "Tensor(a0!) state, Tensor x, Tensor dt, Tensor A, Tensor B, Tensor C, "
635 + "Tensor? D, Tensor? z, Tensor? dt_bias, bool dt_softplus, "
636 + "Tensor? state_batch_indices, Tensor? dst_state_batch_indices, "
637 + "SymInt null_block_id, Tensor(a13!) out, "
638 + "Tensor? num_accepted_tokens, Tensor? cu_seqlens) -> ()",
639 + &selective_state_update_cpu_impl);
640 +
641 + ops.def(
642 + "mamba_chunk_scan_fwd_cpu("
643 + "Tensor(a0!) out, Tensor(a1!) final_states, "
644 + "Tensor x, Tensor dt, Tensor A, Tensor B, Tensor C, "
645 + "Tensor? D, Tensor? z, Tensor cu_seqlens) -> ()",
646 + &mamba_chunk_scan_fwd_cpu_impl);
647 +
648 ops.def("init_cpu_memory_env(SymInt[] node_ids) -> ()", &init_cpu_memory_env);
649
650 // Speculative decoding kernels
tests/kernels/mamba/cpu/test_cpu_gdn_ops.py
+1 -1
@@ -425,7 +425,7 @@ def test_causal_conv1d_torch_two_call_split(total_tokens: int, split: int) -> No
425 match the single-call result.
426 """
427 from vllm.model_executor.layers.mamba.ops.cpu.causal_conv1d import (
428 - causal_conv1d_torch,
428 + causal_conv1d_fn_cpu as causal_conv1d_torch,
429 )
430
431 x, weight, bias = _conv_inputs(total_tokens)
tests/kernels/mamba/test_causal_conv1d.py
+8 -3
@@ -18,8 +18,12 @@ from vllm.v1.attention.backends.utils import NULL_BLOCK_ID
18 DEVICE = current_platform.device_type
19
20 pytestmark = pytest.mark.skipif(
21 - not (current_platform.is_cuda_alike() or current_platform.is_xpu()),
22 - reason="causal_conv1d Triton kernels require CUDA-alike or XPU",
21 + not (
22 + current_platform.is_cuda_alike()
23 + or current_platform.is_xpu()
24 + or current_platform.is_cpu()
25 + ),
26 + reason="causal_conv1d Triton kernels require CUDA-alike, XPU, or CPU",
27 )
28
29
@@ -284,7 +288,8 @@ def test_causal_conv1d_varlen(
288 batch, with_padding, dim, seqlen, width, has_bias, silu_activation, itype
289 ):
290 device = DEVICE
287 - torch.accelerator.empty_cache()
291 + if not current_platform.is_cpu():
292 + torch.accelerator.empty_cache()
293 rtol, atol = (3e-4, 1e-3) if itype == torch.float32 else (3e-3, 5e-3)
294 if itype == torch.bfloat16:
295 rtol, atol = 1e-2, 5e-2
tests/kernels/mamba/test_mamba_ssm.py
+48 -2
@@ -20,8 +20,12 @@ from vllm.v1.attention.backends.utils import NULL_BLOCK_ID
20 DEVICE = current_platform.device_type
21
22 pytestmark = pytest.mark.skipif(
23 - not (current_platform.is_cuda_alike() or current_platform.is_xpu()),
24 - reason="mamba_ssm kernels require CUDA-alike or XPU",
23 + not (
24 + current_platform.is_cuda_alike()
25 + or current_platform.is_xpu()
26 + or current_platform.is_cpu()
27 + ),
28 + reason="mamba_ssm kernels require CUDA-alike, XPU, or CPU",
29 )
30
31 # selective_scan_fn is backed by the CUDA-only `ops.selective_scan_fwd` C++ op,
@@ -342,6 +346,13 @@ def test_selective_scan(
346 @pytest.mark.parametrize("has_z", [False, True])
347 @pytest.mark.parametrize("dstate", [16, 64])
348 @pytest.mark.parametrize("dim", [2048, 2048 + 16, 4096])
349 +@pytest.mark.skipif(
350 + current_platform.is_cpu(),
351 + reason=(
352 + "CPU kernel for selective_state_update only supports "
353 + "Mamba 2 (scalar A/dt), not Mamba 1."
354 + ),
355 +)
356 def test_selective_state_update(dim, dstate, has_z, itype):
357 device = DEVICE
358 rtol, atol = (3e-4, 1e-3) if itype == torch.float32 else (5e-3, 1e-2)
@@ -436,6 +447,13 @@ def test_selective_state_update_stochastic_rounding(dim, dstate, has_z, philox_r
447 @pytest.mark.parametrize("dstate", [16, 64])
448 @pytest.mark.parametrize("dim", [2048, 2048 + 16, 4096])
449 @pytest.mark.parametrize("max_seq_len", [1, 2, 4])
450 +@pytest.mark.skipif(
451 + current_platform.is_cpu(),
452 + reason=(
453 + "CPU kernel for selective_state_update only supports "
454 + "Mamba 2 (scalar A/dt), not Mamba 1."
455 + ),
456 +)
457 def test_selective_state_update_varlen(dim, dstate, has_z, itype, max_seq_len):
458 device = DEVICE
459 rtol, atol = (3e-4, 1e-3) if itype == torch.float32 else (5e-3, 1e-2)
@@ -697,6 +715,13 @@ def test_selective_scan_varlen(
715 @pytest.mark.parametrize("dim", [2048, 2048 + 16, 4096])
716 # tests correctness in case subset of the sequences are padded
717 @pytest.mark.parametrize("with_padding", [True, False])
718 +@pytest.mark.skipif(
719 + current_platform.is_cpu(),
720 + reason=(
721 + "CPU kernel for selective_state_update only supports "
722 + "Mamba 2 (scalar A/dt), not Mamba 1."
723 + ),
724 +)
725 def test_selective_state_update_with_batch_indices(
726 with_padding, dim, dstate, has_z, itype
727 ):
@@ -789,6 +814,13 @@ def test_selective_state_update_with_batch_indices(
814 @pytest.mark.parametrize("ngroups", [1, 4])
815 @pytest.mark.parametrize("dstate", [16, 64])
816 @pytest.mark.parametrize("dim", [2048, 4096])
817 +@pytest.mark.skipif(
818 + current_platform.is_cpu(),
819 + reason=(
820 + "CPU kernel for selective_state_update only supports "
821 + "Mamba 2 (scalar A/dt), not Mamba 1."
822 + ),
823 +)
824 def test_selective_state_update_with_heads_with_batch_indices(
825 dim, dstate, ngroups, has_z, tie_hdim, itype
826 ):
@@ -862,6 +894,13 @@ def test_selective_state_update_with_heads_with_batch_indices(
894 @pytest.mark.parametrize("dstate", [16, 64])
895 @pytest.mark.parametrize("dim", [2048, 4096])
896 @pytest.mark.parametrize("max_seq_len", [2, 4])
897 +@pytest.mark.skipif(
898 + current_platform.is_cpu(),
899 + reason=(
900 + "CPU kernel for selective_state_update only supports "
901 + "Mamba 2 (scalar A/dt), not Mamba 1."
902 + ),
903 +)
904 def test_selective_state_update_with_num_accepted_tokens(
905 dim, dstate, has_z, itype, max_seq_len
906 ):
@@ -988,6 +1027,13 @@ def test_selective_state_update_with_num_accepted_tokens(
1027 @pytest.mark.parametrize("dstate", [16, 64])
1028 @pytest.mark.parametrize("dim", [2048, 4096])
1029 @pytest.mark.parametrize("max_seq_len", [2, 4])
1030 +@pytest.mark.skipif(
1031 + current_platform.is_cpu(),
1032 + reason=(
1033 + "CPU kernel for selective_state_update only supports "
1034 + "Mamba 2 (scalar A/dt), not Mamba 1."
1035 + ),
1036 +)
1037 def test_selective_state_update_varlen_with_num_accepted(
1038 dim, dstate, has_z, itype, max_seq_len
1039 ):
vllm/_custom_ops.py
+87
@@ -2070,6 +2070,93 @@ def selective_scan_fwd(
2070 )
2071
2072
2073 +def causal_conv1d_update_cpu_vec(
2074 + x: torch.Tensor,
2075 + conv_state: torch.Tensor,
2076 + weight: torch.Tensor,
2077 + bias: torch.Tensor | None = None,
2078 + activation: str | None = None,
2079 + conv_state_indices: torch.Tensor | None = None,
2080 + query_start_loc: torch.Tensor | None = None,
2081 + pad_slot_id: int = 0,
2082 +) -> torch.Tensor:
2083 + return torch.ops._C.causal_conv1d_update_cpu_vec(
2084 + x,
2085 + conv_state,
2086 + weight,
2087 + bias,
2088 + activation,
2089 + conv_state_indices,
2090 + query_start_loc,
2091 + pad_slot_id,
2092 + )
2093 +
2094 +
2095 +def selective_state_update_cpu(
2096 + state: torch.Tensor,
2097 + x: torch.Tensor,
2098 + dt: torch.Tensor,
2099 + A: torch.Tensor,
2100 + B: torch.Tensor,
2101 + C: torch.Tensor,
2102 + D: torch.Tensor | None,
2103 + z: torch.Tensor | None,
2104 + dt_bias: torch.Tensor | None,
2105 + dt_softplus: bool,
2106 + state_batch_indices: torch.Tensor | None,
2107 + dst_state_batch_indices: torch.Tensor | None,
2108 + null_block_id: int,
2109 + out: torch.Tensor,
2110 + num_accepted_tokens: torch.Tensor | None,
2111 + cu_seqlens: torch.Tensor | None,
2112 +):
2113 + torch.ops._C.selective_state_update_cpu(
2114 + state,
2115 + x,
2116 + dt,
2117 + A,
2118 + B,
2119 + C,
2120 + D,
2121 + z,
2122 + dt_bias,
2123 + dt_softplus,
2124 + state_batch_indices,
2125 + dst_state_batch_indices,
2126 + null_block_id,
2127 + out,
2128 + num_accepted_tokens,
2129 + cu_seqlens,
2130 + )
2131 +
2132 +
2133 +def mamba_chunk_scan_fwd_cpu(
2134 + out: torch.Tensor,
2135 + final_states: torch.Tensor,
2136 + x: torch.Tensor,
2137 + dt: torch.Tensor,
2138 + A: torch.Tensor,
2139 + B: torch.Tensor,
2140 + C: torch.Tensor,
2141 + D: torch.Tensor | None,
2142 + z: torch.Tensor | None,
2143 + cu_seqlens: torch.Tensor,
2144 +) -> None:
2145 + """Prefill SSM scan kernel. out and final_states are written in-place."""
2146 + torch.ops._C.mamba_chunk_scan_fwd_cpu(
2147 + out,
2148 + final_states,
2149 + x,
2150 + dt,
2151 + A,
2152 + B,
2153 + C,
2154 + D,
2155 + z,
2156 + cu_seqlens,
2157 + )
2158 +
2159 +
2160 # ROCm skinny gemms
2161 def LLMM1(a: torch.Tensor, b: torch.Tensor, rows_per_block: int) -> torch.Tensor:
2162 return torch.ops._rocm_C.LLMM1(a, b, rows_per_block)
vllm/config/mamba.py
+1
@@ -27,6 +27,7 @@ class MambaBackendEnum(Enum, metaclass=_MambaBackendEnumMeta):
27
28 TRITON = "triton"
29 FLASHINFER = "flashinfer"
30 + CPU = "cpu"
31
32
33 @config
vllm/model_executor/layers/mamba/ops/causal_conv1d.py
+12
@@ -1237,3 +1237,15 @@ def causal_conv1d_update(
1237 if unsqueeze:
1238 out = out.squeeze(-1)
1239 return out.to(original_x_dtype)
1240 +
1241 +
1242 +from vllm.platforms import current_platform # noqa: E402
1243 +
1244 +if current_platform.is_cpu():
1245 + from vllm.model_executor.layers.mamba.ops.cpu.causal_conv1d import (
1246 + causal_conv1d_fn_cpu,
1247 + causal_conv1d_update_cpu,
1248 + )
1249 +
1250 + causal_conv1d_fn = causal_conv1d_fn_cpu # type: ignore
1251 + causal_conv1d_update = causal_conv1d_update_cpu # type: ignore
vllm/model_executor/layers/mamba/ops/cpu/causal_conv1d.py
+68 -8
@@ -6,18 +6,31 @@ from __future__ import annotations
6 import torch
7 import torch.nn.functional as F
8
9 +from vllm._custom_ops import causal_conv1d_update_cpu_vec
10 +from vllm.v1.attention.backends.utils import NULL_BLOCK_ID, PAD_SLOT_ID
11
10 -# for prefill
11 -def causal_conv1d_torch(
12 +
13 +def causal_conv1d_fn_cpu(
14 x: torch.Tensor,
15 weight: torch.Tensor,
16 bias: torch.Tensor | None,
17 conv_states: torch.Tensor,
18 query_start_loc: torch.Tensor,
17 - cache_indices: torch.Tensor,
18 - has_initial_state: torch.Tensor,
19 + cache_indices: torch.Tensor | None = None,
20 + has_initial_state: torch.Tensor | None = None,
21 activation: str | None = "silu",
22 + pad_slot_id: int = PAD_SLOT_ID,
23 + **kwargs,
24 ) -> torch.Tensor:
25 + """CPU implementation for causal_conv1d_fwd."""
26 + if isinstance(activation, bool) and activation:
27 + activation = "silu"
28 + elif isinstance(activation, bool):
29 + activation = None
30 +
31 + original_x_dtype = x.dtype
32 + x = x.to(conv_states.dtype)
33 +
34 out = torch.empty_like(x)
35 state_len = weight.shape[1] - 1
36 assert activation in {None, "silu", "swish"}
@@ -27,11 +40,21 @@ def causal_conv1d_torch(
40 for idx in range(query_start_loc.shape[0] - 1)
41 ]
42 weight = weight.unsqueeze(1)
43 +
44 for seq_idx, (bos, eos) in enumerate(seq_begin_end_idx):
31 - slot = int(cache_indices[seq_idx].item())
45 + if bos == eos:
46 + continue
47 +
48 + slot = (
49 + int(cache_indices[seq_idx].item()) if cache_indices is not None else seq_idx
50 + )
51 +
52 + if slot == pad_slot_id:
53 + continue
54
55 seq_x = x[:, bos:eos].unsqueeze(0)
34 - if bool(has_initial_state[seq_idx].item()):
56 +
57 + if has_initial_state is not None and bool(has_initial_state[seq_idx].item()):
58 initial_state = conv_states[slot, :, :state_len].unsqueeze(0)
59 else:
60 initial_state = torch.zeros(
@@ -51,16 +74,48 @@ def causal_conv1d_torch(
74 groups=weight.shape[0],
75 )
76 seq_out = seq_out[..., -seq_x.shape[-1] :].to(dtype=x.dtype)
77 +
78 if activation in ("silu", "swish"):
79 seq_out = F.silu(seq_out)
80
81 out[:, bos:eos] = seq_out.squeeze(0)
82 conv_states[slot, :, :state_len].copy_(conv_input[..., -state_len:].squeeze(0))
83
60 - return out
84 + return out.to(original_x_dtype)
85 +
86 +
87 +def causal_conv1d_update_cpu(
88 + x: torch.Tensor,
89 + conv_state: torch.Tensor,
90 + weight: torch.Tensor,
91 + bias: torch.Tensor | None = None,
92 + activation: bool | str | None = None,
93 + conv_state_indices: torch.Tensor | None = None,
94 + query_start_loc: torch.Tensor | None = None,
95 + pad_slot_id: int | None = None,
96 + **kwargs,
97 +) -> torch.Tensor:
98 + """CPU implementation for causal_conv1d_update."""
99 + if isinstance(activation, bool):
100 + activation = "silu" if activation else None
101 +
102 + if pad_slot_id is None:
103 + pad_slot_id = kwargs.get("null_block_id", NULL_BLOCK_ID)
104 + if pad_slot_id is None:
105 + pad_slot_id = NULL_BLOCK_ID
106 +
107 + return causal_conv1d_update_cpu_vec(
108 + x,
109 + conv_state,
110 + weight,
111 + bias,
112 + activation,
113 + conv_state_indices,
114 + query_start_loc,
115 + pad_slot_id,
116 + )
117
118
63 -# for decode
119 def causal_conv1d_update_torch(
120 x: torch.Tensor,
121 conv_state: torch.Tensor,
@@ -68,6 +123,11 @@ def causal_conv1d_update_torch(
123 bias: torch.Tensor | None = None,
124 activation: str | None = None,
125 ) -> torch.Tensor:
126 + """
127 + Pure PyTorch fallback for causal_conv1d_update.
128 + Currently used as a fallback for Arm (aarch64) to leverage
129 + oneDNN/ACL F.conv1d kernels for batched decoding.
130 + """
131 assert activation in {None, "silu", "swish"}
132
133 _, dim, seq_len = x.shape
vllm/model_executor/layers/mamba/ops/cpu/gdn_attention.py
+45 -23
@@ -10,9 +10,13 @@ import vllm._custom_ops as ops
10 from vllm.forward_context import ForwardContext, get_forward_context
11 from vllm.model_executor.layers.mamba.mamba_utils import is_conv_state_dim_first
12 from vllm.model_executor.layers.mamba.ops.cpu.causal_conv1d import (
13 - causal_conv1d_torch,
13 + causal_conv1d_fn_cpu as causal_conv1d_torch,
14 +)
15 +from vllm.model_executor.layers.mamba.ops.cpu.causal_conv1d import (
16 + causal_conv1d_update_cpu,
17 causal_conv1d_update_torch,
18 )
19 +from vllm.platforms import CpuArchEnum, current_platform
20 from vllm.utils.torch_utils import (
21 LayerNameType,
22 _resolve_layer_name,
@@ -140,21 +144,30 @@ def _cpu_gdn_attention_nonspec(
144 conv_states=conv_state,
145 weight=layer.conv1d.weight,
146 bias=layer.conv1d.bias,
143 - silu_activation=layer.activation == "silu",
147 + silu_activation=(layer.activation == "silu"),
148 conv_state_indices=decode_state_indices,
149 is_vnni=True,
150 )
151 else:
148 - decode_conv_state = conv_state[decode_state_indices].contiguous()
149 - decode_mixed_qkv = causal_conv1d_update_torch(
150 - # [B, dim] -> [B, dim, 1]
151 - x=decode_mixed_qkv.unsqueeze(-1),
152 - conv_state=decode_conv_state,
153 - weight=conv_weights,
154 - bias=layer.conv1d.bias,
155 - activation=layer.activation,
156 - ).squeeze(-1)
157 - conv_state[decode_state_indices] = decode_conv_state
152 + if current_platform.get_cpu_architecture() == CpuArchEnum.ARM:
153 + decode_conv_state = conv_state[decode_state_indices].contiguous()
154 + decode_mixed_qkv = causal_conv1d_update_torch(
155 + x=decode_mixed_qkv.unsqueeze(-1),
156 + conv_state=decode_conv_state,
157 + weight=conv_weights,
158 + bias=layer.conv1d.bias,
159 + activation=layer.activation,
160 + ).squeeze(-1)
161 + conv_state[decode_state_indices] = decode_conv_state
162 + else:
163 + decode_mixed_qkv = causal_conv1d_update_cpu(
164 + x=decode_mixed_qkv,
165 + conv_state=conv_state,
166 + weight=conv_weights,
167 + bias=layer.conv1d.bias,
168 + activation=layer.activation,
169 + conv_state_indices=decode_state_indices,
170 + )
171
172 query, key, value = layer.rearrange_mixed_qkv(decode_mixed_qkv)
173
@@ -495,17 +508,26 @@ def _spec_aware_nonspec(
508 decode_a = a[:num_decode_tokens]
509 decode_state_indices = state_indices_tensor[:num_decodes]
510 # 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
511 + if current_platform.get_cpu_architecture() == CpuArchEnum.ARM:
512 + conv_state_view = conv_buf[:, :, : width - 1]
513 + decode_conv_state = conv_state_view[decode_state_indices].contiguous()
514 + decode_mixed_qkv = causal_conv1d_update_torch(
515 + x=decode_mixed_qkv.unsqueeze(-1),
516 + conv_state=decode_conv_state,
517 + weight=conv_weights,
518 + bias=layer.conv1d.bias,
519 + activation=layer.activation,
520 + ).squeeze(-1)
521 + conv_state_view[decode_state_indices] = decode_conv_state
522 + else:
523 + decode_mixed_qkv = causal_conv1d_update_cpu(
524 + x=decode_mixed_qkv,
525 + conv_state=conv_buf[:, :, : width - 1],
526 + weight=conv_weights,
527 + bias=layer.conv1d.bias,
528 + activation=layer.activation,
529 + conv_state_indices=decode_state_indices,
530 + )
531
532 query, key, value = layer.rearrange_mixed_qkv(decode_mixed_qkv)
533 # rearrange_mixed_qkv can return views whose last dim is not
vllm/model_executor/layers/mamba/ops/cpu/mamba_ssm.py new
+144
@@ -0,0 +1,144 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +
4 +import torch
5 +
6 +import vllm._custom_ops as ops
7 +from vllm.v1.attention.backends.utils import NULL_BLOCK_ID
8 +
9 +
10 +def _mamba_chunk_scan_combined_fwd_cpu(
11 + x,
12 + dt,
13 + A,
14 + B,
15 + C,
16 + chunk_size,
17 + out,
18 + D=None,
19 + z=None,
20 + dt_bias=None,
21 + initial_states=None,
22 + return_intermediate_states=False,
23 + seq_idx=None,
24 + cu_seqlens=None,
25 + cu_chunk_seqlens=None,
26 + last_chunk_indices=None,
27 + dt_softplus=False,
28 + dt_limit=(0.0, float("inf")),
29 + state_dtype=None,
30 + **kwargs,
31 +):
32 + seqlen, nheads, headdim = x.shape
33 + _, ngroups, dstate = B.shape
34 +
35 + assert cu_seqlens is not None
36 + batch = cu_seqlens.size(0) - 1
37 +
38 + dt_f = dt.float()
39 + if dt_bias is not None:
40 + dt_f = dt_f + dt_bias.float().unsqueeze(0)
41 + if dt_softplus:
42 + dt_f = torch.nn.functional.softplus(dt_f)
43 + if dt_limit[0] > 0.0 or dt_limit[1] < float("inf"):
44 + dt_f = dt_f.clamp(min=dt_limit[0], max=dt_limit[1])
45 +
46 + all_states = torch.zeros(
47 + batch, nheads, headdim, dstate, dtype=torch.float32, device=x.device
48 + )
49 + if initial_states is not None:
50 + all_states.copy_(initial_states.float())
51 +
52 + assert out.is_contiguous(), (
53 + "_mamba_chunk_scan_combined_fwd_cpu: `out` must be "
54 + "pre-allocated as a contiguous tensor"
55 + )
56 +
57 + D_1d = None
58 + if D is not None:
59 + d = D.float()
60 + while d.dim() > 1 and d.stride(-1) == 0:
61 + d = d.squeeze(-1)
62 + D_1d = d.contiguous()
63 +
64 + ops.mamba_chunk_scan_fwd_cpu(
65 + out,
66 + all_states,
67 + x,
68 + dt_f,
69 + A,
70 + B,
71 + C,
72 + D_1d,
73 + z,
74 + cu_seqlens.to(torch.int32),
75 + )
76 +
77 + out_dtype = state_dtype if state_dtype is not None else x.dtype
78 + all_states = all_states.to(out_dtype)
79 +
80 + return all_states
81 +
82 +
83 +def selective_state_update(
84 + state,
85 + x,
86 + dt,
87 + A,
88 + B,
89 + C,
90 + D=None,
91 + z=None,
92 + dt_bias=None,
93 + dt_softplus=False,
94 + state_batch_indices=None,
95 + dst_state_batch_indices=None,
96 + null_block_id=NULL_BLOCK_ID,
97 + out=None,
98 + num_accepted_tokens=None,
99 + cu_seqlens=None,
100 + is_blackwell=False,
101 + enable_stochastic_rounding=False,
102 + cache_philox_rounds=0,
103 +):
104 + """CPU implementation for selective_state_update."""
105 + # Ensure out tensor exists
106 + if out is None:
107 + out = torch.empty_like(x if x.dim() == 2 else x)
108 +
109 + _state = state.unsqueeze(1) if state.dim() == 3 else state
110 + _x = x.unsqueeze(1) if x.dim() == 2 else x
111 + _dt = dt.unsqueeze(1) if dt.dim() == 2 else dt
112 + _A = A.unsqueeze(0) if A.dim() == 2 else A
113 + _B = B.unsqueeze(1) if B.dim() == 2 else B
114 + _C = C.unsqueeze(1) if C.dim() == 2 else C
115 + _D = D.unsqueeze(0) if (D is not None and D.dim() == 1) else D
116 + _z = z.unsqueeze(1) if (z is not None and z.dim() == 2) else z
117 + _dt_bias = (
118 + dt_bias.unsqueeze(0)
119 + if (dt_bias is not None and dt_bias.dim() == 1)
120 + else dt_bias
121 + )
122 + _out = out.unsqueeze(1) if out.dim() == 2 else out
123 +
124 + _sbi = state_batch_indices
125 + _dsbi = dst_state_batch_indices
126 + ops.selective_state_update_cpu(
127 + _state,
128 + _x,
129 + _dt,
130 + _A,
131 + _B,
132 + _C,
133 + _D,
134 + _z,
135 + _dt_bias,
136 + dt_softplus,
137 + _sbi,
138 + _dsbi,
139 + null_block_id,
140 + _out,
141 + num_accepted_tokens,
142 + cu_seqlens,
143 + )
144 + return _out.squeeze(1) if out.dim() == 2 else _out
vllm/model_executor/layers/mamba/ops/mamba_ssm.py
+10
@@ -845,3 +845,13 @@ def selective_scan_fn(
845 return delta # output written inplace to delta
846 else:
847 return z # output written inplace to z
848 +
849 +
850 +from vllm.platforms import current_platform # noqa: E402
851 +
852 +if current_platform.is_cpu():
853 + from vllm.model_executor.layers.mamba.ops.cpu.mamba_ssm import (
854 + selective_state_update as selective_state_update_cpu,
855 + )
856 +
857 + selective_state_update = selective_state_update_cpu # type: ignore
vllm/model_executor/layers/mamba/ops/ssd_combined.py
+8
@@ -225,3 +225,11 @@ def mamba_chunk_scan_combined_varlen(
225 )
226
227 return varlen_states
228 +
229 +
230 +from vllm.platforms import current_platform # noqa: E402
231 +
232 +if current_platform.is_cpu():
233 + import vllm.model_executor.layers.mamba.ops.cpu.mamba_ssm as cpu_mamba_ssm
234 +
235 + _mamba_chunk_scan_combined_fwd = cpu_mamba_ssm._mamba_chunk_scan_combined_fwd_cpu # type: ignore
vllm/model_executor/layers/mamba/ops/ssu_dispatch.py
+83 -2
@@ -4,8 +4,9 @@
4 Dispatch module for Mamba selective state update (SSU) backends.
5
6 Provides a unified `selective_state_update` function that dispatches to
7 -either the Triton or FlashInfer backend based on the configured
8 -`MambaBackendEnum`. Follows SGLang's dispatch pattern adapted for vLLM.
7 +the Triton, FlashInfer, or CPU backend based on the configured
8 +`MambaBackendEnum`. On CPU-only platforms (PowerPC, x86 without CUDA)
9 +the backend defaults to 'cpu'.
10 """
11
12 from abc import ABC, abstractmethod
@@ -182,9 +183,75 @@ class FlashInferSSUBackend(MambaSSUBackend):
183 )
184
185
186 +class CPUSSUBackend(MambaSSUBackend):
187 + """CPU SSU backend using the compiled C++ VSX/scalar kernel.
188 +
189 + On CPU-only platforms (PowerPC, x86 without CUDA) this dispatches to
190 + the vectorized C++ kernel registered as ``torch.ops._C.selective_state_update_cpu``.
191 + That kernel uses vec_op SIMD intrinsics (VSX on ppc64le, AVX2 on x86,
192 + scalar fallback elsewhere) and is parallelised with OpenMP across heads.
193 +
194 + Falls back to the pure-PyTorch implementation only if the C++ op is
195 + unavailable (e.g. a CPU-less build).
196 + """
197 +
198 + def __init__(self, mamba_config: MambaConfig):
199 + super().__init__(mamba_config)
200 + from vllm import _custom_ops as ops
201 +
202 + self._cpp_kernel = ops.selective_state_update_cpu
203 + logger.info("CPUSSUBackend: using compiled C++ selective_state_update kernel.")
204 +
205 + @property
206 + def name(self) -> str:
207 + return "cpu"
208 +
209 + def __call__(
210 + self,
211 + state: torch.Tensor,
212 + x: torch.Tensor,
213 + dt: torch.Tensor,
214 + A: torch.Tensor,
215 + B: torch.Tensor,
216 + C: torch.Tensor,
217 + D: torch.Tensor,
218 + dt_bias: torch.Tensor,
219 + z: torch.Tensor | None = None,
220 + dt_softplus: bool = False,
221 + state_batch_indices: torch.Tensor | None = None,
222 + dst_state_batch_indices: torch.Tensor | None = None,
223 + null_block_id: int = NULL_BLOCK_ID,
224 + out: torch.Tensor | None = None,
225 + num_accepted_tokens: torch.Tensor | None = None,
226 + cu_seqlens: torch.Tensor | None = None,
227 + is_blackwell: bool = False,
228 + ) -> None:
229 + # C++ kernel: state shape expected as (nstates, nheads, dim, dstate)
230 + # The kernel writes in-place into `out` and updates `state`.
231 + self._cpp_kernel(
232 + state,
233 + x,
234 + dt,
235 + A,
236 + B,
237 + C,
238 + D,
239 + z,
240 + dt_bias,
241 + dt_softplus,
242 + state_batch_indices,
243 + dst_state_batch_indices,
244 + null_block_id,
245 + out,
246 + num_accepted_tokens,
247 + cu_seqlens,
248 + )
249 +
250 +
251 _BACKEND_REGISTRY: dict[MambaBackendEnum, type[MambaSSUBackend]] = {
252 MambaBackendEnum.TRITON: TritonSSUBackend,
253 MambaBackendEnum.FLASHINFER: FlashInferSSUBackend,
254 + MambaBackendEnum.CPU: CPUSSUBackend,
255 }
256
257 _mamba_ssu_backend: MambaSSUBackend | None = None
@@ -210,6 +277,20 @@ def initialize_mamba_ssu_backend(
277 global _mamba_ssu_backend
278
279 backend = mamba_config.backend
280 +
281 + # On CPU-only platforms (PowerPC, x86 without CUDA) Triton JIT is
282 + # unstable or unavailable. Silently fall back to the CPU
283 + # backend unless the user explicitly chose something other than "triton".
284 + if backend == MambaBackendEnum.TRITON:
285 + from vllm.platforms import current_platform
286 +
287 + if current_platform.is_cpu():
288 + logger.info(
289 + "CPU platform detected: overriding Mamba SSU backend "
290 + "from 'triton' to 'cpu'."
291 + )
292 + backend = MambaBackendEnum.CPU
293 +
294 if backend not in _BACKEND_REGISTRY:
295 raise ValueError(
296 f"Unknown Mamba SSU backend: {backend}. "
vllm/model_executor/layers/mamba/short_conv.py
+25 -12
@@ -94,9 +94,13 @@ class ShortConv(MambaBase, CustomOp):
94 # Reference torch causal conv1d; runs on all CPU platforms. AMX kernels
95 # for causal conv can be plugged in here later.
96 from vllm.model_executor.layers.mamba.ops.cpu.causal_conv1d import (
97 - causal_conv1d_torch,
97 + causal_conv1d_fn_cpu as causal_conv1d_torch,
98 + )
99 + from vllm.model_executor.layers.mamba.ops.cpu.causal_conv1d import (
100 + causal_conv1d_update_cpu,
101 causal_conv1d_update_torch,
102 )
103 + from vllm.platforms import CpuArchEnum, current_platform
104
105 forward_context = get_forward_context()
106 attn_metadata_raw = forward_context.attn_metadata
@@ -164,17 +168,26 @@ class ShortConv(MambaBase, CustomOp):
168 if has_decode:
169 assert attn_metadata.state_indices_tensor_d is not None
170 state_indices_d = attn_metadata.state_indices_tensor_d.flatten()
167 - Bx_d = (B_d * x_d).unsqueeze(-1) # (num_decodes, dim, 1)
168 - # Advanced indexing returns a copy; update in-place then scatter back
169 - gathered = conv_state[state_indices_d] # (num_decodes, dim, state_len)
170 - out_d = causal_conv1d_update_torch(
171 - Bx_d,
172 - gathered,
173 - conv_weights,
174 - self.conv.bias,
175 - activation=None,
176 - ).squeeze(-1) # (num_decodes, dim)
177 - conv_state[state_indices_d] = gathered
171 + Bx_d = B_d * x_d # (num_decodes, dim)
172 + if current_platform.get_cpu_architecture() == CpuArchEnum.ARM:
173 + conv_state_view = conv_state[state_indices_d].contiguous()
174 + out_d = causal_conv1d_update_torch(
175 + Bx_d.unsqueeze(-1),
176 + conv_state_view,
177 + conv_weights,
178 + self.conv.bias,
179 + activation=None,
180 + ).squeeze(-1)
181 + conv_state[state_indices_d] = conv_state_view
182 + else:
183 + out_d = causal_conv1d_update_cpu(
184 + Bx_d,
185 + conv_state,
186 + conv_weights,
187 + self.conv.bias,
188 + activation=None,
189 + conv_state_indices=state_indices_d,
190 + )
191 conv_output_list.insert(0, C_d * out_d)
192
193 hidden_states_out = torch.vstack(conv_output_list)
vllm/model_executor/layers/utils.py
+6 -2
@@ -234,10 +234,14 @@ def dispatch_cpu_unquantized_gemm(
234 layer.cpu_linear = torch.nn.functional.linear
235 return
236
237 + # Skip CPU GEMM dispatch for non-2D weights (e.g. MoE 3D expert weights).
238 + # These layers are handled by their own specialized methods.
239 if layer.weight.ndim != 2:
240 # this is not a linear layer
239 - # For now it should be a causal_conv1d op
240 - if torch.cpu._is_amx_tile_supported():
241 + # For now it should be a causal_conv1d op or MoE 3D expert weights
242 + if torch.cpu._is_amx_tile_supported() and hasattr(
243 + ops, "causal_conv1d_weight_pack"
244 + ):
245 # prepack conv weight
246 unpacked = (
247 layer.weight.view(