[CPU] Conv1d optimised kernel for aarch64 (#54093)

Signed-off-by: Anna Mayne <anna.mayne@arm.com> Co-authored-by: Li, Jiang <jiang1.li@intel.com>

almayne committed Sep 30, 2026 at 13:23 UTC 93dd80f63639453591e68891614729fb0bd44855
9 files changed +461 -206
cmake/cpu_extension.cmake
+1
@@ -524,6 +524,7 @@ if (ASIMD_FOUND AND NOT APPLE_SILICON_FOUND)
524 "csrc/cpu/cpu_tanhf_neon.hpp"
525 ${VLLM_EXT_SRC})
526 if (ARM_BF16_FOUND)
527 + set(VLLM_EXT_SRC "csrc/cpu/sgl-kernels/conv.cpp" ${VLLM_EXT_SRC})
528 if (ARM_I8MM_FOUND)
529 set(VLLM_EXT_SRC "csrc/cpu/cpu_fused_moe_int8.cpp" ${VLLM_EXT_SRC})
530 endif()
csrc/cpu/sgl-kernels/conv.cpp
+19 -7
@@ -66,6 +66,10 @@ struct tinygemm_kernel {
66 }
67 };
68
69 +#if defined(ARM_BF16_SUPPORT)
70 +#include "conv_arm.h"
71 +#endif
72 +
73 #if defined(CPU_CAPABILITY_AVX512)
74 template <int K, int BLOCK_N, bool has_bias, bool has_silu>
75 struct tinygemm_kernel<at::BFloat16, K, BLOCK_N, has_bias, has_silu> {
@@ -543,16 +547,24 @@ at::Tensor causal_conv1d_weight_pack(const at::Tensor& weight) {
547
548 int64_t dim = weight.size(0);
549 int64_t width = weight.size(1);
546 - constexpr int64_t BLOCK_N = block_size_n();
550 + constexpr int64_t BLOCK_N =
551 +#if defined(ARM_BF16_SUPPORT)
552 + 8;// 8 channel blocks for microtile
553 +#else
554 + block_size_n();
555 +#endif
556 TORCH_CHECK(width == 4, "causal_conv1d_weight_pack: support only width of 4");
548 - TORCH_CHECK(dim % BLOCK_N == 0, "causal_conv1d_weight_pack: invalid dim size ", dim);
557 + TORCH_CHECK(
558 + dim % block_size_n() == 0,
559 + "causal_conv1d_weight_pack: invalid dim size ",
560 + dim);
561
562 const int64_t N = dim, K2 = width >> 1;
563 const int64_t NB = div_up(N, BLOCK_N);
564
565 auto packed_weight = at::empty_like(weight);
566 AT_DISPATCH_REDUCED_FLOATING_TYPES(weight.scalar_type(), "causal_conv1d_fwd_kernel_impl", [&] {
555 - // cast to float32 as vnni size is 2
567 + // Copy each pair as an opaque 32-bit word. This is not a bf16->fp32 conversion.
568 const float* w_data = reinterpret_cast<float*>(weight.data_ptr<scalar_t>());
569 float* packed_data = reinterpret_cast<float*>(packed_weight.data_ptr<scalar_t>());
570
@@ -642,9 +654,9 @@ at::Tensor causal_conv1d_fwd_cpu(
654 const std::optional<at::Tensor>& has_initial_state,
655 bool silu_activation,
656 int64_t pad_slot_id,
645 - bool is_vnni) {
657 + bool is_weight_packed) {
658 CHECK_CONTIGUOUS(weight);
647 - auto packed_w = is_vnni ? weight : causal_conv1d_weight_pack(weight);
659 + auto packed_w = is_weight_packed ? weight : causal_conv1d_weight_pack(weight);
660
661 const bool is_var_seqlen = query_start_loc.has_value();
662 const int64_t input_ndim = is_var_seqlen ? 2 : 3;
@@ -755,10 +767,10 @@ at::Tensor causal_conv1d_update_cpu(
767 const std::optional<at::Tensor>& num_accepted_tokens,
768 const std::optional<at::Tensor>& conv_state_indices,
769 int64_t pad_slot_id,
758 - bool is_vnni) {
770 + bool is_weight_packed) {
771 CHECK_CONTIGUOUS(x);
772 CHECK_CONTIGUOUS(weight);
761 - auto packed_w = is_vnni ? weight : causal_conv1d_weight_pack(weight);
773 + auto packed_w = is_weight_packed ? weight : causal_conv1d_weight_pack(weight);
774
775 TORCH_CHECK(
776 x.dim() == 2 || x.dim() == 3,
csrc/cpu/sgl-kernels/conv_arm.h new
+126
@@ -0,0 +1,126 @@
1 +// SPDX-License-Identifier: Apache-2.0
2 +// SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +
4 +#pragma once
5 +
6 +#if defined(ARM_BF16_SUPPORT)
7 +
8 +template <int K, int BLOCK_N, bool has_bias, bool has_silu>
9 +struct tinygemm_kernel<at::BFloat16, K, BLOCK_N, has_bias, has_silu> {
10 + static inline bfloat16x8_t load_bf16x8(const at::BFloat16* ptr) {
11 + return vld1q_bf16(reinterpret_cast<const bfloat16_t*>(ptr));
12 + }
13 +
14 + // Input layout: A[m][channel]
15 + // Weight layout: B[N/MICRO_N, K/2, MICRO_N, 2]
16 + // The packed weights are reused across all M positions.
17 + static inline void tinygemm_neon_8(
18 + const at::BFloat16* __restrict__ A, const at::BFloat16* __restrict__ B,
19 + at::BFloat16* __restrict__ C, const at::BFloat16* __restrict__ bias,
20 + const at::BFloat16* __restrict__ conv_states, bool has_initial_state,
21 + int64_t M, int64_t lda, bool is_first_token) {
22 + bfloat16x8_t va[4];
23 + // Load 4 pairs of kernel weights into 4 registers
24 + const bfloat16x8_t vb0 = load_bf16x8(B); // channels 0-3, taps 0-1
25 + const bfloat16x8_t vb1 = load_bf16x8(B + 8); // channels 4-7, taps 0-1
26 + const bfloat16x8_t vb2 = load_bf16x8(B + 16); // channels 0-3, taps 2-3
27 + const bfloat16x8_t vb3 = load_bf16x8(B + 24); // channels 4-7, taps 2-3
28 +
29 + // Load the input data. A single register holds the values for a
30 + // single token across all 8 channels for this microtile.
31 + auto load_a = [&](int64_t m) {
32 + if (m < 0 && is_first_token) {
33 + if (!has_initial_state) {
34 + return vreinterpretq_bf16_u16(vdupq_n_u16(0));
35 + }
36 + return load_bf16x8(conv_states + (m + K - 1) * lda);
37 + }
38 + return load_bf16x8(A + m * lda);
39 + };
40 +
41 + // Load the previous inputs from the cached state if available.
42 + va[1] = load_a(-3);
43 + va[2] = load_a(-2);
44 + va[3] = load_a(-1);
45 +
46 + // Iterate over the sequence block, updating the previous conv inputs
47 + // and reading the next token's input data (8 channels).
48 + for (int64_t m = 0; m < M; ++m) {
49 + va[0] = va[1];
50 + va[1] = va[2];
51 + va[2] = va[3];
52 + va[3] = load_a(m);
53 +
54 + float32x4_t vc0;
55 + float32x4_t vc1;
56 + if constexpr (has_bias) {
57 + const bfloat16x8_t bias_vec = load_bf16x8(bias);
58 + vc0 = vcvtq_low_f32_bf16(bias_vec);
59 + vc1 = vcvtq_high_f32_bf16(bias_vec);
60 + } else {
61 + vc0 = vdupq_n_f32(0.0f);
62 + vc1 = vdupq_n_f32(0.0f);
63 + }
64 +
65 + // Use the zip operation to match the input layout to the weights layout
66 + bfloat16x8_t va_pair =
67 + vreinterpretq_bf16_u16( // channels 0-3, inputs for taps 0-1
68 + vzip1q_u16(vreinterpretq_u16_bf16(va[0]),
69 + vreinterpretq_u16_bf16(va[1])));
70 + vc0 = vbfdotq_f32(vc0, va_pair,
71 + vb0); // 4 independent length-two dot products
72 + va_pair = vreinterpretq_bf16_u16( // channels 4-7, inputs for taps 0-1
73 + vzip2q_u16(vreinterpretq_u16_bf16(va[0]),
74 + vreinterpretq_u16_bf16(va[1])));
75 + vc1 = vbfdotq_f32(vc1, va_pair, vb1);
76 + va_pair = vreinterpretq_bf16_u16( // channels 0-3, inputs for taps 2-3
77 + vzip1q_u16(vreinterpretq_u16_bf16(va[2]),
78 + vreinterpretq_u16_bf16(va[3])));
79 + // Add to the previous result for the same channels
80 + vc0 = vbfdotq_f32(vc0, va_pair, vb2);
81 + va_pair = vreinterpretq_bf16_u16( // channels 4-7, inputs for taps 2-3
82 + vzip2q_u16(vreinterpretq_u16_bf16(va[2]),
83 + vreinterpretq_u16_bf16(va[3])));
84 + vc1 = vbfdotq_f32(vc1, va_pair, vb3);
85 +
86 + using fVec = at::vec::Vectorized<float>;
87 + fVec out0(vc0);
88 + fVec out1(vc1);
89 + if constexpr (has_silu) {
90 + const fVec one(1.0f);
91 + out0 = out0 / (one + out0.neg().fexp_u20());
92 + out1 = out1 / (one + out1.neg().fexp_u20());
93 + }
94 +
95 + // Convert the 2 x 4 fp32 results to 1 x 8 bf16
96 + const bfloat16x8_t out =
97 + vcombine_bf16(vcvt_bf16_f32(static_cast<float32x4_t>(out0)),
98 + vcvt_bf16_f32(static_cast<float32x4_t>(out1)));
99 + vst1q_bf16(reinterpret_cast<bfloat16_t*>(C + m * lda), out);
100 + }
101 + }
102 +
103 + static inline void apply(const at::BFloat16* __restrict__ A,
104 + const at::BFloat16* __restrict__ B,
105 + at::BFloat16* __restrict__ C,
106 + const at::BFloat16* __restrict__ bias,
107 + const at::BFloat16* __restrict__ conv_states,
108 + bool has_initial_state, int64_t M, int64_t lda,
109 + bool is_first_token) {
110 + static_assert(K == 4);
111 + constexpr int64_t MICRO_N = 8;
112 + static_assert(BLOCK_N % MICRO_N == 0);
113 +
114 + // BLOCK_N is always 32 or 64. M is the block sequence length.
115 + // Input layout: A[m][channel]
116 + // Weight layout: B[N/MICRO_N, K/2, MICRO_N, 2]
117 + // Use an 8 channel microtile and iterate over the 8 channel blocks.
118 + for (int64_t n = 0; n < BLOCK_N; n += MICRO_N) {
119 + tinygemm_neon_8(A + n, B + n * K, C + n, has_bias ? bias + n : nullptr,
120 + conv_states ? conv_states + n : nullptr,
121 + has_initial_state, M, lda, is_first_token);
122 + }
123 + }
124 +};
125 +
126 +#endif
csrc/cpu/torch_bindings.cpp
+29 -20
@@ -281,14 +281,14 @@ at::Tensor causal_conv1d_fwd_cpu(
281 const std::optional<at::Tensor>& query_start_loc,
282 const std::optional<at::Tensor>& cache_indices,
283 const std::optional<at::Tensor>& has_initial_state, bool silu_activation,
284 - int64_t pad_slot_id, bool is_vnni);
284 + int64_t pad_slot_id, bool is_weight_packed);
285
286 at::Tensor causal_conv1d_update_cpu(
287 const at::Tensor& x, const at::Tensor& conv_states,
288 const at::Tensor& weight, const std::optional<at::Tensor>& bias,
289 bool silu_activation, const std::optional<at::Tensor>& num_accepted_tokens,
290 const std::optional<at::Tensor>& conv_state_indices, int64_t pad_slot_id,
291 - bool is_vnni);
291 + bool is_weight_packed);
292
293 void activation_lut_bf16(torch::Tensor& out, torch::Tensor& input,
294 const std::string& activation);
@@ -517,6 +517,33 @@ TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, ops) {
517
518 #endif // (defined(__aarch64__) && !defined(__APPLE__))
519
520 +#if (defined(__aarch64__) && defined(ARM_BF16_SUPPORT) && \
521 + !defined(__APPLE__)) || \
522 + (defined(__AVX512BF16__) && defined(__AVX512F__) && \
523 + defined(__AVX512VNNI__))
524 +
525 + // Adapted from sglang: casual_conv1d kernels
526 + ops.def("causal_conv1d_weight_pack(Tensor weight) -> Tensor");
527 + ops.impl("causal_conv1d_weight_pack", torch::kCPU,
528 + &causal_conv1d_weight_pack);
529 +
530 + ops.def(
531 + "causal_conv1d_fwd_cpu(Tensor x, Tensor weight, Tensor? bias, Tensor? "
532 + "conv_states, Tensor? query_start_loc,"
533 + "Tensor? cache_indices, Tensor? has_initial_state, bool silu_activation, "
534 + "int pad_slot_id, bool is_weight_packed) -> "
535 + "Tensor");
536 + ops.impl("causal_conv1d_fwd_cpu", torch::kCPU, &causal_conv1d_fwd_cpu);
537 +
538 + ops.def(
539 + "causal_conv1d_update_cpu(Tensor x, Tensor(a!) conv_states, Tensor "
540 + "weight, Tensor? bias, bool silu_activation,"
541 + "Tensor? num_accepted_tokens, Tensor? conv_state_indices, int "
542 + "pad_slot_id, "
543 + "bool is_weight_packed) -> Tensor");
544 + ops.impl("causal_conv1d_update_cpu", torch::kCPU, &causal_conv1d_update_cpu);
545 +#endif
546 +
547 // Layernorm
548 // Apply Root Mean Square (RMS) Normalization to the input tensor.
549 ops.def(
@@ -665,24 +692,6 @@ TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, ops) {
692 "-> (Tensor, Tensor)");
693 ops.impl("quantize_fp8e4m3_vec", torch::kCPU, &quantize_fp8e4m3_vec);
694
668 - ops.def("causal_conv1d_weight_pack(Tensor weight) -> Tensor");
669 - ops.impl("causal_conv1d_weight_pack", torch::kCPU,
670 - &causal_conv1d_weight_pack);
671 - ops.def(
672 - "causal_conv1d_fwd_cpu(Tensor x, Tensor weight, Tensor? bias, Tensor? "
673 - "conv_states, Tensor? query_start_loc,"
674 - "Tensor? cache_indices, Tensor? has_initial_state, bool silu_activation, "
675 - "int pad_slot_id, bool is_vnni) -> "
676 - "Tensor");
677 - ops.impl("causal_conv1d_fwd_cpu", torch::kCPU, &causal_conv1d_fwd_cpu);
678 - ops.def(
679 - "causal_conv1d_update_cpu(Tensor x, Tensor(a!) conv_states, Tensor "
680 - "weight, Tensor? bias, bool silu_activation,"
681 - "Tensor? num_accepted_tokens, Tensor? conv_state_indices, int "
682 - "pad_slot_id, "
683 - "bool is_vnni) -> Tensor");
684 - ops.impl("causal_conv1d_update_cpu", torch::kCPU, &causal_conv1d_update_cpu);
685 -
695 // Adapted from sglang: MLA CPU kernels (AMX-only, DeepSeek V2/V3/R1)
696 ops.def(
697 "decode_attention_cpu(Tensor query, Tensor k_buffer, Tensor v_buffer, "
tests/kernels/mamba/cpu/test_cpu_gdn_ops.py
+155 -103
@@ -494,8 +494,10 @@ def _sd_conv_states(
494 return storage.transpose(1, 2)
495
496
497 -def _maybe_pack_conv_weight(weight: torch.Tensor, is_vnni: bool) -> torch.Tensor:
498 - return ops.causal_conv1d_weight_pack(weight) if is_vnni else weight
497 +def _maybe_pack_conv_weight(
498 + weight: torch.Tensor, is_weight_packed: bool
499 +) -> torch.Tensor:
500 + return ops.causal_conv1d_weight_pack(weight) if is_weight_packed else weight
501
502
503 @torch.inference_mode()
@@ -523,7 +525,7 @@ def test_causal_conv1d_update_cpu_preserves_sd_state(
525 bias=None,
526 silu_activation=False,
527 conv_state_indices=state_indices,
526 - is_vnni=False,
528 + is_weight_packed=False,
529 )
530
531 assert out is x
@@ -666,7 +668,10 @@ def test_spec_aware_nonspec_materializes_state_indices(
668
669
670 @torch.inference_mode()
671 +@pytest.mark.parametrize(("has_amx", "has_arm_bf16"), [(True, False), (False, True)])
672 def test_spec_forward_prepares_native_conv_metadata(
673 + has_amx: bool,
674 + has_arm_bf16: bool,
675 monkeypatch: pytest.MonkeyPatch,
676 ) -> None:
677 block_table = torch.tensor([[0, 1], [4, 5]], dtype=torch.int32)
@@ -687,14 +692,17 @@ def test_spec_forward_prepares_native_conv_metadata(
692 )
693 forwarded_indices = None
694 forwarded_counts = None
695 + forwarded_is_weight_packed = None
696
697 def causal_conv1d_update_cpu(**kwargs):
692 - nonlocal forwarded_counts, forwarded_indices
698 + nonlocal forwarded_counts, forwarded_indices, forwarded_is_weight_packed
699 forwarded_indices = kwargs["conv_state_indices"]
700 forwarded_counts = kwargs["num_accepted_tokens"]
701 + forwarded_is_weight_packed = kwargs["is_weight_packed"]
702 return kwargs["x"]
703
697 - monkeypatch.setattr(torch.cpu, "_is_amx_tile_supported", lambda: True)
704 + monkeypatch.setattr(torch.cpu, "_is_amx_tile_supported", lambda: has_amx)
705 + monkeypatch.setattr(gdn_attention, "is_arm_bf16", lambda: has_arm_bf16)
706 monkeypatch.setattr(gdn_attention, "is_conv_state_dim_first", lambda: False)
707 monkeypatch.setattr(
708 gdn_attention.ops, "causal_conv1d_update_cpu", causal_conv1d_update_cpu
@@ -732,6 +740,7 @@ def test_spec_forward_prepares_native_conv_metadata(
740 assert actual is not None
741 assert actual.is_contiguous()
742 assert actual.dtype == torch.int32
743 + assert forwarded_is_weight_packed is True
744 torch.testing.assert_close(actual, reference)
745
746
@@ -792,15 +801,15 @@ def test_causal_conv1d_torch_two_call_split(total_tokens: int, split: int) -> No
801
802
803 @pytest.mark.skipif(
795 - not torch.cpu._is_amx_tile_supported(),
796 - reason="requires AMX support",
804 + not (torch.cpu._is_amx_tile_supported() or gdn_attention.is_arm_bf16()),
805 + reason="native causal conv1d requires AMX or Arm BF16 support",
806 )
807 @torch.inference_mode()
808 def test_causal_conv1d_update_cpu_accepts_wide_state() -> None:
809 state_len = CONV_KERNEL - 1
810 wide_state_len = state_len + 5
811 batch_size = 3
803 - is_vnni = True
812 + is_weight_packed = True
813 x, weight, bias = _conv_inputs(batch_size)
814 conv_state_indices = torch.tensor([2, 0, 1], dtype=torch.int32)
815
@@ -813,7 +822,7 @@ def test_causal_conv1d_update_cpu_accepts_wide_state() -> None:
822 wide_state[:, :, state_len:].fill_(7)
823 wide_tail = wide_state[:, :, state_len:].clone()
824
816 - conv_weight = _maybe_pack_conv_weight(weight, is_vnni)
825 + conv_weight = _maybe_pack_conv_weight(weight, is_weight_packed)
826 out_narrow = ops.causal_conv1d_update_cpu(
827 x=x,
828 conv_states=narrow_state,
@@ -821,7 +830,7 @@ def test_causal_conv1d_update_cpu_accepts_wide_state() -> None:
830 bias=bias,
831 silu_activation=True,
832 conv_state_indices=conv_state_indices,
824 - is_vnni=is_vnni,
833 + is_weight_packed=is_weight_packed,
834 )
835 out_wide = ops.causal_conv1d_update_cpu(
836 x=x,
@@ -830,7 +839,7 @@ def test_causal_conv1d_update_cpu_accepts_wide_state() -> None:
839 bias=bias,
840 silu_activation=True,
841 conv_state_indices=conv_state_indices,
833 - is_vnni=is_vnni,
842 + is_weight_packed=is_weight_packed,
843 )
844
845 torch.testing.assert_close(out_wide, out_narrow, atol=1e-2, rtol=1e-2)
@@ -872,11 +881,18 @@ def _ref_causal_conv1d_update_cpu_multi(
881
882
883 @pytest.mark.skipif(
875 - not torch.cpu._is_amx_tile_supported(),
876 - reason="requires AMX support",
884 + not (torch.cpu._is_amx_tile_supported() or gdn_attention.is_arm_bf16()),
885 + reason="native causal conv1d requires AMX or Arm BF16 support",
886 )
887 @pytest.mark.parametrize(
879 - ("batch_size, seq_len, accepted_counts, has_bias, silu_activation, is_vnni"),
888 + (
889 + "batch_size, "
890 + "seq_len, "
891 + "accepted_counts, "
892 + "has_bias, "
893 + "silu_activation, "
894 + "is_weight_packed"
895 + ),
896 [
897 (1, 1, [1], False, False, False),
898 (1, 1, [1], True, True, True),
@@ -893,7 +909,7 @@ def test_causal_conv1d_update_cpu_multi_token_matches_python(
909 accepted_counts: list[int],
910 has_bias: bool,
911 silu_activation: bool,
896 - is_vnni: bool,
912 + is_weight_packed: bool,
913 ) -> None:
914 dim = 96
915 state_len = seq_len + 2
@@ -911,7 +927,7 @@ def test_causal_conv1d_update_cpu_multi_token_matches_python(
927 )
928 conv_states = conv_states_ref.clone()
929
914 - conv_weight = _maybe_pack_conv_weight(weight, is_vnni)
930 + conv_weight = _maybe_pack_conv_weight(weight, is_weight_packed)
931 out = ops.causal_conv1d_update_cpu(
932 x=x,
933 conv_states=conv_states,
@@ -919,7 +935,7 @@ def test_causal_conv1d_update_cpu_multi_token_matches_python(
935 bias=bias,
936 silu_activation=silu_activation,
937 conv_state_indices=conv_state_indices,
922 - is_vnni=is_vnni,
938 + is_weight_packed=is_weight_packed,
939 num_accepted_tokens=num_accepted_tokens,
940 )
941 ref_out = _ref_causal_conv1d_update_cpu_multi(
@@ -937,8 +953,8 @@ def test_causal_conv1d_update_cpu_multi_token_matches_python(
953
954
955 @pytest.mark.skipif(
940 - not torch.cpu._is_amx_tile_supported(),
941 - reason="requires AMX support",
956 + not (torch.cpu._is_amx_tile_supported() or gdn_attention.is_arm_bf16()),
957 + reason="native causal conv1d requires AMX or Arm BF16 support",
958 )
959 @pytest.mark.parametrize("num_accepted", [0, 17])
960 @torch.inference_mode()
@@ -961,14 +977,15 @@ def test_causal_conv1d_update_cpu_rejects_invalid_accepted_count(
977 bias=None,
978 silu_activation=True,
979 conv_state_indices=torch.tensor([0], dtype=torch.int32),
964 - is_vnni=False,
980 + is_weight_packed=False,
981 num_accepted_tokens=torch.tensor([num_accepted], dtype=torch.int32),
982 )
983
984
985 @pytest.mark.skipif(
970 - not torch.cpu._is_avx512_bf16_supported(),
971 - reason="causal_conv1d_fwd_cpu requires AVX-512BF16 (Intel Xeon or AMD EPYC)",
986 + not (torch.cpu._is_avx512_bf16_supported() or gdn_attention.is_arm_bf16()),
987 + reason="causal_conv1d_fwd_cpu requires AVX-512BF16"
988 + " (Intel Xeon or AMD EPYC) or Arm BF16 support",
989 )
990 @pytest.mark.parametrize("total_tokens, split", TWO_CALL_SPLITS)
991 @torch.inference_mode()
@@ -977,13 +994,13 @@ def test_causal_conv1d_fwd_cpu_two_call_split(total_tokens: int, split: int) ->
994 matches the single-call result.
995
996 Regression test for ``causal_conv1d_fwd_varlen_kernel_impl`` (``conv.cpp``)
980 - ignoring the carried conv state on continued chunks. Runs on any
981 - AVX-512BF16 CPU since conv.cpp uses VDPBF16PS, not AMX tiles.
997 + ignoring the carried conv state on continued chunks. Runs on aarch64 cpus with
998 + bf16 support or any AVX-512BF16 CPU since conv.cpp uses VDPBF16PS, not AMX tiles.
999 """
1000 state_len = CONV_KERNEL - 1
1001 x, weight, bias = _conv_inputs(total_tokens)
1002
986 - def amx(x_seg, conv_states, has_init):
1003 + def native(x_seg, conv_states, has_init):
1004 seq = x_seg.shape[0]
1005 return ops.causal_conv1d_fwd_cpu(
1006 x=x_seg.transpose(0, 1), # [dim, seq]; stride(-2)==1 (view of [seq,dim])
@@ -994,30 +1011,30 @@ def test_causal_conv1d_fwd_cpu_two_call_split(total_tokens: int, split: int) ->
1011 cache_indices=torch.tensor([0], dtype=torch.int32),
1012 has_initial_state=torch.tensor([has_init]),
1013 silu_activation=True,
997 - is_vnni=False,
1014 + is_weight_packed=False,
1015 ).contiguous()
1016
1017 # conv_state layout passed by the AMX branch: [num_slots, dim, state_len].
1018 cs_full = torch.zeros(1, CONV_DIM, state_len, dtype=x.dtype)
1002 - out_full = amx(x, cs_full, False)
1019 + out_full = native(x, cs_full, False)
1020
1021 cs_split = torch.zeros(1, CONV_DIM, state_len, dtype=x.dtype)
1005 - out1 = amx(x[:split], cs_split, False)
1006 - out2 = amx(x[split:], cs_split, True)
1022 + out1 = native(x[:split], cs_split, False)
1023 + out2 = native(x[split:], cs_split, True)
1024 out_split = torch.cat([out1, out2], dim=1)
1025
1026 torch.testing.assert_close(out_split, out_full, atol=1e-2, rtol=1e-2)
1027
1028
1029 @pytest.mark.skipif(
1013 - not torch.cpu._is_amx_tile_supported(),
1014 - reason="requires AMX support",
1030 + not (torch.cpu._is_amx_tile_supported() or gdn_attention.is_arm_bf16()),
1031 + reason="native causal conv1d requires AMX or Arm BF16 support",
1032 )
1033 @torch.inference_mode()
1034 def test_causal_conv1d_fwd_cpu_accepts_wide_state() -> None:
1035 state_len = CONV_KERNEL - 1
1036 wide_state_len = state_len + 5
1020 - is_vnni = True
1037 + is_weight_packed = True
1038 seq_lens = [CHUNK_SIZE - 1, CHUNK_SIZE + 5]
1039 total_tokens = sum(seq_lens)
1040 x, weight, bias = _conv_inputs(total_tokens)
@@ -1034,7 +1051,7 @@ def test_causal_conv1d_fwd_cpu_accepts_wide_state() -> None:
1051 wide_state[:, :, state_len:].fill_(7)
1052 wide_tail = wide_state[:, :, state_len:].clone()
1053
1037 - conv_weight = _maybe_pack_conv_weight(weight, is_vnni)
1054 + conv_weight = _maybe_pack_conv_weight(weight, is_weight_packed)
1055 out_narrow = ops.causal_conv1d_fwd_cpu(
1056 x=x.transpose(0, 1),
1057 weight=conv_weight,
@@ -1044,7 +1061,7 @@ def test_causal_conv1d_fwd_cpu_accepts_wide_state() -> None:
1061 cache_indices=cache_indices,
1062 has_initial_state=has_initial_state,
1063 silu_activation=True,
1047 - is_vnni=is_vnni,
1064 + is_weight_packed=is_weight_packed,
1065 )
1066 out_wide = ops.causal_conv1d_fwd_cpu(
1067 x=x.transpose(0, 1),
@@ -1055,7 +1072,7 @@ def test_causal_conv1d_fwd_cpu_accepts_wide_state() -> None:
1072 cache_indices=cache_indices,
1073 has_initial_state=has_initial_state,
1074 silu_activation=True,
1058 - is_vnni=is_vnni,
1075 + is_weight_packed=is_weight_packed,
1076 )
1077
1078 torch.testing.assert_close(out_wide, out_narrow, atol=1e-2, rtol=1e-2)
@@ -1100,15 +1117,23 @@ def test_batch_memcpy_cpu_fallback() -> None:
1117
1118
1119 # ---------------------------------------------------------------------------
1103 -# C++ conv (conv.cpp) uses VDPBF16PS, not AMX tiles, so it runs on any
1104 -# AVX-512BF16 CPU; weight is VNNI-packed on this same predicate at load time.
1120 +# C++ conv (conv.cpp) uses AArch64BF16 or VDPBF16PS, not AMX tiles, so it can
1121 +# run on any AVX-512BF16 CPU; weight is packed on this same predicate at load time.
1122 # ---------------------------------------------------------------------------
1123
1124 _HAS_AVX512_BF16 = torch.cpu._is_avx512_bf16_supported()
1125
1126 _STATE_LEN = CONV_KERNEL - 1
1127
1111 -CONV_EQUIV_SEQ_LENS = [[1], [7], [64], [65], [1, 2, 3], [63, 64, 65], [128, 129]]
1128 +CONV_EQUIV_SEQ_LENS = [
1129 + [1],
1130 + [7],
1131 + [64],
1132 + [65],
1133 + [1, 2, 3],
1134 + [63, 64, 65],
1135 + [128, 129],
1136 +]
1137
1138
1139 def _conv_fp32_oracle(x, weight, bias, seq_lens, activation="silu"):
@@ -1153,10 +1178,10 @@ def _run_prefill_torch(x, weight, bias, seq_lens):
1178 return out.transpose(0, 1).contiguous(), conv_states
1179
1180
1156 -def _run_prefill_cpp(x, weight, bias, seq_lens, is_vnni=False):
1181 +def _run_prefill_cpp(x, weight, bias, seq_lens, is_weight_packed=False):
1182 num_seqs = len(seq_lens)
1158 - packed_w = ops.causal_conv1d_weight_pack(weight) if is_vnni else weight
1159 - if is_vnni:
1183 + packed_w = ops.causal_conv1d_weight_pack(weight) if is_weight_packed else weight
1184 + if is_weight_packed:
1185 # C++-branch layout: kv-cache "SD" [slots, state_len, dim] transposed to
1186 # [slots, dim, state_len] (a non-contiguous view).
1187 conv_state = torch.zeros(
@@ -1176,7 +1201,7 @@ def _run_prefill_cpp(x, weight, bias, seq_lens, is_vnni=False):
1201 cache_indices=torch.arange(num_seqs, dtype=torch.int32),
1202 has_initial_state=torch.zeros(num_seqs, dtype=torch.bool),
1203 silu_activation=True,
1179 - is_vnni=is_vnni,
1204 + is_weight_packed=is_weight_packed,
1205 )
1206 return out.transpose(0, 1).contiguous(), conv_state
1207
@@ -1245,7 +1270,7 @@ def test_causal_conv1d_fwd_cpu_ds_adapter_varlen_mixed_initial_state(
1270 cache_indices=state_indices,
1271 has_initial_state=has_initial_state,
1272 silu_activation=True,
1248 - is_vnni=False,
1273 + is_weight_packed=False,
1274 )
1275 monkeypatch.setattr(gdn_attention, "is_conv_state_dim_first", lambda: True)
1276 out_ds = gdn_attention._causal_conv1d_fwd_cpu(
@@ -1257,7 +1282,7 @@ def test_causal_conv1d_fwd_cpu_ds_adapter_varlen_mixed_initial_state(
1282 cache_indices=state_indices,
1283 has_initial_state=has_initial_state,
1284 silu_activation=True,
1260 - is_vnni=False,
1285 + is_weight_packed=False,
1286 )
1287
1288 torch.testing.assert_close(out_ds, out_sd)
@@ -1302,7 +1327,7 @@ def test_causal_conv1d_update_cpu_ds_adapter_wide_multi_token_copyback(
1327 bias=None,
1328 silu_activation=False,
1329 conv_state_indices=state_indices,
1305 - is_vnni=False,
1330 + is_weight_packed=False,
1331 num_accepted_tokens=num_accepted_tokens,
1332 )
1333
@@ -1321,7 +1346,8 @@ def test_causal_conv1d_update_cpu_ds_adapter_wide_multi_token_copyback(
1346
1347
1348 @pytest.mark.skipif(
1324 - not _HAS_AVX512_BF16, reason="C++ causal_conv1d requires AVX-512BF16"
1349 + not (_HAS_AVX512_BF16 or gdn_attention.is_arm_bf16()),
1350 + reason="C++ causal_conv1d requires AVX-512BF16 or Arm BF16 support",
1351 )
1352 @pytest.mark.parametrize("seq_lens", CONV_EQUIV_SEQ_LENS)
1353 @torch.inference_mode()
@@ -1335,7 +1361,8 @@ def test_conv_cpp_matches_torch(seq_lens):
1361
1362
1363 @pytest.mark.skipif(
1338 - not _HAS_AVX512_BF16, reason="C++ causal_conv1d requires AVX-512BF16"
1364 + not (_HAS_AVX512_BF16 or gdn_attention.is_arm_bf16()),
1365 + reason="C++ causal_conv1d requires AVX-512BF16 or Arm BF16 support",
1366 )
1367 @pytest.mark.parametrize("seq_lens", CONV_EQUIV_SEQ_LENS)
1368 @torch.inference_mode()
@@ -1354,89 +1381,114 @@ def test_conv_cpp_no_worse_than_torch_vs_fp32(seq_lens):
1381
1382
1383 @pytest.mark.skipif(
1357 - not _HAS_AVX512_BF16, reason="C++ causal_conv1d requires AVX-512BF16"
1384 + not (_HAS_AVX512_BF16 or gdn_attention.is_arm_bf16()),
1385 + reason="C++ causal_conv1d requires AVX-512BF16 or Arm BF16 support",
1386 )
1387 @pytest.mark.parametrize("seq_lens", CONV_EQUIV_SEQ_LENS)
1388 @torch.inference_mode()
1361 -def test_conv_cpp_vnni_packed_matches_torch(seq_lens):
1362 - """The exact runtime prefill sequence (VNNI-packed weight + SD-layout
1363 - conv_state view + is_vnni=True) must match the torch fallback. Validates
1364 - the packing + layout handoff on any AVX-512BF16 CPU."""
1389 +def test_conv_cpp_weight_packed_matches_torch(seq_lens):
1390 + """The exact runtime prefill sequence (packed weight + SD-layout
1391 + conv_state view + is_weight_packed=True) must match the torch fallback. Validates
1392 + the native packing + layout handoff."""
1393 x, weight, bias = _conv_inputs(sum(seq_lens))
1394 out_torch, _ = _run_prefill_torch(x, weight, bias, seq_lens)
1367 - out_vnni, _ = _run_prefill_cpp(x, weight, bias, seq_lens, is_vnni=True)
1368 - torch.testing.assert_close(out_vnni, out_torch, atol=1e-2, rtol=1e-2)
1395 + out_weight_packed, _ = _run_prefill_cpp(
1396 + x, weight, bias, seq_lens, is_weight_packed=True
1397 + )
1398 + torch.testing.assert_close(out_weight_packed, out_torch, atol=1e-2, rtol=1e-2)
1399
1400
1401 @pytest.mark.skipif(
1372 - not _HAS_AVX512_BF16, reason="C++ causal_conv1d requires AVX-512BF16"
1402 + not (_HAS_AVX512_BF16 or gdn_attention.is_arm_bf16()),
1403 + reason="C++ causal_conv1d requires AVX-512BF16 or Arm BF16 support",
1404 )
1405 @pytest.mark.parametrize("batch", DECODE_BATCH_SIZES)
1406 +@pytest.mark.parametrize(
1407 + ("dim", "has_bias", "silu_activation", "is_weight_packed"),
1408 + [
1409 + (32, False, False, False),
1410 + (32, True, True, True),
1411 + (96, False, True, True),
1412 + (96, True, False, False),
1413 + (8192, False, True, False),
1414 + (8192, True, True, True),
1415 + ],
1416 +)
1417 @torch.inference_mode()
1376 -def test_conv_update_cpp_matches_torch(batch):
1377 - """Decode conv: causal_conv1d_update_cpu matches causal_conv1d_update_torch,
1378 - including the in-place conv_state update (the next-step handoff)."""
1418 +def test_conv_update_cpp_matches_torch(
1419 + batch, dim, has_bias, silu_activation, is_weight_packed
1420 +):
1421 + """Decode preserves indexed, padded state across successive signed inputs."""
1422 from vllm.model_executor.layers.mamba.ops.cpu.causal_conv1d import (
1423 causal_conv1d_update_torch,
1424 )
1425
1383 - x = tensor_cache(batch * CONV_DIM, torch.bfloat16).view(batch, CONV_DIM)
1384 - weight = tensor_cache(CONV_DIM * CONV_KERNEL, torch.bfloat16).view(
1385 - CONV_DIM, CONV_KERNEL
1386 - )
1387 - bias = tensor_cache(CONV_DIM, torch.bfloat16)
1388 - conv_state = tensor_cache(batch * CONV_DIM * _STATE_LEN, torch.bfloat16).view(
1389 - batch, CONV_DIM, _STATE_LEN
1390 - )
1391 -
1392 - cs_torch = conv_state.clone()
1393 - out_torch = causal_conv1d_update_torch(
1394 - x=x.unsqueeze(-1),
1395 - conv_state=cs_torch,
1396 - weight=weight,
1397 - bias=bias,
1398 - activation="silu",
1399 - ).squeeze(-1)
1400 -
1401 - cs_cpp = conv_state.clone()
1402 - out_cpp = ops.causal_conv1d_update_cpu(
1403 - x=x.contiguous(),
1404 - conv_states=cs_cpp,
1405 - weight=weight,
1406 - bias=bias,
1407 - silu_activation=True,
1408 - conv_state_indices=torch.arange(batch, dtype=torch.int32),
1409 - is_vnni=False,
1410 - )
1411 - torch.testing.assert_close(out_cpp, out_torch, atol=1e-2, rtol=1e-2)
1412 - torch.testing.assert_close(cs_cpp, cs_torch, atol=1e-2, rtol=1e-2)
1426 + torch.manual_seed(0)
1427 + weight = torch.randn(dim, CONV_KERNEL, dtype=torch.bfloat16)
1428 + bias = torch.randn(dim, dtype=torch.bfloat16) if has_bias else None
1429 + conv_weight = _maybe_pack_conv_weight(weight, is_weight_packed)
1430 + indices = torch.arange(batch, 0, -1, dtype=torch.int32)
1431 + storage_ref = torch.randn(batch + 2, _STATE_LEN + 2, dim, dtype=torch.bfloat16)
1432 + storage_cpp = storage_ref.clone()
1433 + cs_cpp = storage_cpp[:, :_STATE_LEN].transpose(1, 2)
1434 +
1435 + for _ in range(3):
1436 + x = torch.randn(batch, dim, dtype=torch.bfloat16)
1437 + cs_torch = storage_ref[indices, :_STATE_LEN].transpose(1, 2).contiguous()
1438 + out_torch = causal_conv1d_update_torch(
1439 + x=x.unsqueeze(-1),
1440 + conv_state=cs_torch,
1441 + weight=weight,
1442 + bias=bias,
1443 + activation="silu" if silu_activation else None,
1444 + ).squeeze(-1)
1445 + storage_ref[indices, :_STATE_LEN] = cs_torch.transpose(1, 2)
1446 + out_cpp = ops.causal_conv1d_update_cpu(
1447 + x=x,
1448 + conv_states=cs_cpp,
1449 + weight=conv_weight,
1450 + bias=bias,
1451 + silu_activation=silu_activation,
1452 + conv_state_indices=indices,
1453 + is_weight_packed=is_weight_packed,
1454 + )
1455 + torch.testing.assert_close(out_cpp, out_torch, atol=1e-2, rtol=1e-2)
1456 + torch.testing.assert_close(storage_cpp, storage_ref, atol=0, rtol=0)
1457
1458
1459 @pytest.mark.skipif(
1416 - not _HAS_AVX512_BF16, reason="C++ causal_conv1d requires AVX-512BF16"
1460 + not (_HAS_AVX512_BF16 or gdn_attention.is_arm_bf16()),
1461 + reason="C++ causal_conv1d requires AVX-512BF16 or Arm BF16 support",
1462 )
1463 @torch.inference_mode()
1464 def test_conv_weight_pack_roundtrip_unpacked_matches():
1465 """`causal_conv1d_weight_pack` repacks the (dim, width) weight, so the C++ conv op
1421 - with packed weight + is_vnni=True must equal unpacked weight + is_vnni=False.
1422 - Guards the spec-decode contract: the runtime VNNI-packs `layer.conv1d.weight`
1423 - in place and stashes the original as `_cpu_unpacked_conv_weight` for the torch
1424 - spec-decode path, so both must produce identical math.
1466 + with packed weight + is_weight_packed=True must equal unpacked weight
1467 + + is_weight_packed=False. Guards the spec-decode contract: the runtime packs
1468 + `layer.conv1d.weight` in place and stashes the original as
1469 + `_cpu_unpacked_conv_weight` for the torch spec-decode path, so both must produce
1470 + identical math.
1471 """
1472 seq_lens = [7, 64, 65]
1473 x, weight, bias = _conv_inputs(sum(seq_lens))
1428 - out_unpacked, _ = _run_prefill_cpp(x, weight, bias, seq_lens, is_vnni=False)
1429 - out_packed, _ = _run_prefill_cpp(x, weight, bias, seq_lens, is_vnni=True)
1430 - torch.testing.assert_close(out_packed, out_unpacked, atol=1e-2, rtol=1e-2)
1474 + out_unpacked, state_unpacked = _run_prefill_cpp(
1475 + x, weight, bias, seq_lens, is_weight_packed=False
1476 + )
1477 + out_packed, state_packed = _run_prefill_cpp(
1478 + x, weight, bias, seq_lens, is_weight_packed=True
1479 + )
1480 + torch.testing.assert_close(out_packed, out_unpacked, atol=0, rtol=0)
1481 + torch.testing.assert_close(state_packed, state_unpacked, atol=0, rtol=0)
1482
1483
1484 @pytest.mark.skipif(
1434 - not _HAS_AVX512_BF16, reason="C++ causal_conv1d requires AVX-512BF16"
1485 + not (_HAS_AVX512_BF16 or gdn_attention.is_arm_bf16()),
1486 + reason="C++ causal_conv1d requires AVX-512BF16 or AArch64 BF16",
1487 )
1488 @torch.inference_mode()
1489 def test_spec_decode_unpacked_conv_weight_stash():
1438 - """Spec-decode correctness: AVX-512BF16 CPUs VNNI-pack ``conv1d.weight`` in place
1439 - at load time and stash the original ``(dim, width)`` tensor as
1490 + """Spec-decode correctness: AVX-512BF16 and AArch64 BF16 CPUs pack ``conv1d.weight``
1491 + in place at load time and stash the original ``(dim, width)`` tensor as
1492 ``_cpu_unpacked_conv_weight``; the spec-decode path must use that stash since
1493 reading the packed weight directly produces garbage. Verifies the recovered
1494 weight equals the original and that torch F.conv1d agrees with the C++ conv
@@ -1460,7 +1512,7 @@ def test_spec_decode_unpacked_conv_weight_stash():
1512 # 1. The stash must exist and equal the original (dim, width) weight.
1513 assert hasattr(conv, "_cpu_unpacked_conv_weight"), (
1514 "dispatch_cpu_unquantized_gemm did not stash _cpu_unpacked_conv_weight "
1463 - "on an AVX-512BF16 CPU"
1515 + "on an AArch64-BF16 or AVX-512BF16 CPU"
1516 )
1517 stash = conv._cpu_unpacked_conv_weight
1518 torch.testing.assert_close(stash, orig_2d, atol=0, rtol=0)
@@ -1469,10 +1521,10 @@ def test_spec_decode_unpacked_conv_weight_stash():
1521 recovered = getattr(conv, "_cpu_unpacked_conv_weight", None)
1522 assert recovered is not None
1523 # conv.weight is now packed; using it directly (the bug) would differ.
1472 - packed_weight = conv.weight # [dim, 1, width], VNNI-packed contents
1524 + packed_weight = conv.weight # [dim, 1, width], packed contents
1525
1526 # 3. Spec-path torch conv with the recovered unpacked weight must match the
1475 - # nonspec C++ conv with the packed weight (is_vnni=True).
1527 + # nonspec C++ conv with the packed weight (is_weight_packed=True).
1528 seq_lens = [7, 65]
1529 total = sum(seq_lens)
1530 x = tensor_cache(total * CONV_DIM, torch.bfloat16).view(total, CONV_DIM)
@@ -1494,7 +1546,7 @@ def test_spec_decode_unpacked_conv_weight_stash():
1546 cache_indices=torch.arange(num_seqs, dtype=torch.int32),
1547 has_initial_state=torch.zeros(num_seqs, dtype=torch.bool),
1548 silu_activation=True,
1497 - is_vnni=True,
1549 + is_weight_packed=True,
1550 )
1551 .transpose(0, 1)
1552 .contiguous()
tests/model_executor/test_cpu_unquantized_gemm_dispatch.py
+52 -1
@@ -6,10 +6,61 @@ import pytest
6 import torch
7
8 from vllm.model_executor.layers import utils
9 -from vllm.platforms import current_platform
9 +from vllm.platforms import CpuArchEnum, current_platform
10 from vllm.utils.torch_utils import set_default_torch_dtype
11
12
13 +def test_dispatch_prepacks_arm_bf16_causal_conv(monkeypatch):
14 + monkeypatch.setattr(
15 + current_platform, "get_cpu_architecture", lambda: CpuArchEnum.ARM
16 + )
17 + monkeypatch.setattr(torch.cpu, "get_capabilities", lambda: {"bf16": True})
18 + monkeypatch.setattr(torch.cpu, "_is_avx512_bf16_supported", lambda: False)
19 +
20 + packed = torch.randn(32, 4, dtype=torch.bfloat16)
21 + pack_inputs = []
22 +
23 + def pack(weight):
24 + pack_inputs.append(weight.clone())
25 + return packed
26 +
27 + monkeypatch.setattr(utils.ops, "causal_conv1d_weight_pack", pack)
28 + original = torch.randn(32, 1, 4, dtype=torch.bfloat16)
29 + layer = torch.nn.Module()
30 + layer.weight = torch.nn.Parameter(original.clone(), requires_grad=False)
31 +
32 + utils.dispatch_cpu_unquantized_gemm(layer, remove_weight=False)
33 +
34 + assert len(pack_inputs) == 1
35 + # Check causal_conv1d_weight_pack received the expected weights
36 + torch.testing.assert_close(pack_inputs[0], original.view(32, 4))
37 + # Check we have correctly stashed the unpacked weights
38 + torch.testing.assert_close(layer._cpu_unpacked_conv_weight, original.view(32, 4))
39 + # Check we have correctly stored the packed weights
40 + assert layer.weight.data_ptr() == packed.data_ptr()
41 +
42 +
43 +def test_dispatch_does_not_pack_3d_expert_weight(monkeypatch):
44 + monkeypatch.setattr(
45 + current_platform, "get_cpu_architecture", lambda: CpuArchEnum.ARM
46 + )
47 + monkeypatch.setattr(torch.cpu, "get_capabilities", lambda: {"bf16": True})
48 + monkeypatch.setattr(torch.cpu, "_is_avx512_bf16_supported", lambda: False)
49 + pack_calls = []
50 + monkeypatch.setattr(
51 + utils.ops,
52 + "causal_conv1d_weight_pack",
53 + lambda weight: pack_calls.append(weight),
54 + )
55 +
56 + layer = torch.nn.Module()
57 + layer.weight = torch.nn.Parameter(
58 + torch.randn(8, 32, 4, dtype=torch.bfloat16), requires_grad=False
59 + )
60 + utils.dispatch_cpu_unquantized_gemm(layer, remove_weight=False)
61 + assert pack_calls == []
62 +
63 +
64 @pytest.fixture(scope="module")
65 def _mock_zentorch_linear_unary():
66 """Register a mock zentorch_linear_unary op when zentorch is not installed.
vllm/_custom_ops.py
+4 -4
@@ -3666,7 +3666,7 @@ def causal_conv1d_fwd_cpu(
3666 cache_indices: torch.Tensor | None,
3667 has_initial_state: torch.Tensor | None,
3668 silu_activation: bool,
3669 - is_vnni: bool,
3669 + is_weight_packed: bool,
3670 ) -> torch.Tensor:
3671 return torch.ops._C.causal_conv1d_fwd_cpu(
3672 x,
@@ -3678,7 +3678,7 @@ def causal_conv1d_fwd_cpu(
3678 has_initial_state,
3679 silu_activation,
3680 -1,
3681 - is_vnni,
3681 + is_weight_packed,
3682 )
3683
3684
@@ -3689,7 +3689,7 @@ def causal_conv1d_update_cpu(
3689 bias: torch.Tensor | None,
3690 silu_activation: bool,
3691 conv_state_indices: torch.Tensor | None,
3692 - is_vnni: bool,
3692 + is_weight_packed: bool,
3693 num_accepted_tokens: torch.Tensor | None = None,
3694 ) -> torch.Tensor:
3695 return torch.ops._C.causal_conv1d_update_cpu(
@@ -3701,7 +3701,7 @@ def causal_conv1d_update_cpu(
3701 num_accepted_tokens,
3702 conv_state_indices,
3703 -1,
3704 - is_vnni,
3704 + is_weight_packed,
3705 )
3706
3707
vllm/model_executor/layers/mamba/ops/cpu/gdn_attention.py
+53 -63
@@ -14,7 +14,6 @@ from vllm.model_executor.layers.mamba.ops.cpu.causal_conv1d import (
14 )
15 from vllm.model_executor.layers.mamba.ops.cpu.causal_conv1d import (
16 causal_conv1d_update_cpu,
17 - causal_conv1d_update_torch,
17 )
18 from vllm.platforms import CpuArchEnum, current_platform
19 from vllm.utils.torch_utils import (
@@ -56,7 +55,7 @@ def _causal_conv1d_fwd_cpu(
55 cache_indices: torch.Tensor,
56 has_initial_state: torch.Tensor,
57 silu_activation: bool,
59 - is_vnni: bool,
58 + is_weight_packed: bool,
59 ) -> torch.Tensor:
60 if not is_conv_state_dim_first():
61 return ops.causal_conv1d_fwd_cpu(
@@ -68,7 +67,7 @@ def _causal_conv1d_fwd_cpu(
67 cache_indices=cache_indices,
68 has_initial_state=has_initial_state,
69 silu_activation=silu_activation,
71 - is_vnni=is_vnni,
70 + is_weight_packed=is_weight_packed,
71 )
72
73 scratch, identity_indices = _stage_ds_conv_states(conv_states, cache_indices)
@@ -81,7 +80,7 @@ def _causal_conv1d_fwd_cpu(
80 cache_indices=identity_indices,
81 has_initial_state=has_initial_state,
82 silu_activation=silu_activation,
84 - is_vnni=is_vnni,
83 + is_weight_packed=is_weight_packed,
84 )
85 _scatter_ds_conv_states(conv_states, cache_indices, scratch)
86 return out
@@ -95,7 +94,7 @@ def _causal_conv1d_update_cpu(
94 bias: torch.Tensor | None,
95 silu_activation: bool,
96 conv_state_indices: torch.Tensor,
98 - is_vnni: bool,
97 + is_weight_packed: bool,
98 num_accepted_tokens: torch.Tensor | None = None,
99 ) -> torch.Tensor:
100 if not is_conv_state_dim_first():
@@ -106,7 +105,7 @@ def _causal_conv1d_update_cpu(
105 bias=bias,
106 silu_activation=silu_activation,
107 conv_state_indices=conv_state_indices,
109 - is_vnni=is_vnni,
108 + is_weight_packed=is_weight_packed,
109 num_accepted_tokens=num_accepted_tokens,
110 )
111
@@ -118,13 +117,20 @@ def _causal_conv1d_update_cpu(
117 bias=bias,
118 silu_activation=silu_activation,
119 conv_state_indices=identity_indices,
121 - is_vnni=is_vnni,
120 + is_weight_packed=is_weight_packed,
121 num_accepted_tokens=num_accepted_tokens,
122 )
123 _scatter_ds_conv_states(conv_states, conv_state_indices, scratch)
124 return out
125
126
127 +def is_arm_bf16() -> bool:
128 + return (
129 + current_platform.get_cpu_architecture() == CpuArchEnum.ARM
130 + and torch.cpu.get_capabilities().get("bf16", False)
131 + )
132 +
133 +
134 def cpu_gdn_attention_core(
135 mixed_qkv: torch.Tensor,
136 b: torch.Tensor,
@@ -201,9 +207,10 @@ def _cpu_gdn_attention_nonspec(
207 assert state_indices_tensor is not None
208 assert query_start_loc is not None
209
204 - # C++ conv (conv.cpp) uses VDPBF16PS, not AMX tiles, so it runs on any
205 - # AVX-512BF16 CPU; weight is VNNI-packed on this same predicate at load time.
206 - use_cpp_conv = torch.cpu._is_avx512_bf16_supported()
210 + # C++ conv (conv.cpp) runs on aarch64 with bf16 support and on x86 uses VDPBF16PS,
211 + # not AMX tiles, so it runs on any AVX-512BF16 CPU. The weights are packed on this
212 + # same predicate at load time.
213 + use_cpp_conv = torch.cpu._is_avx512_bf16_supported() or is_arm_bf16()
214
215 conv_state = layer.kv_cache[0]
216 if use_cpp_conv:
@@ -244,28 +251,17 @@ def _cpu_gdn_attention_nonspec(
251 bias=layer.conv1d.bias,
252 silu_activation=(layer.activation == "silu"),
253 conv_state_indices=decode_state_indices,
247 - is_vnni=True,
254 + is_weight_packed=True,
255 )
256 else:
250 - if current_platform.get_cpu_architecture() == CpuArchEnum.ARM:
251 - decode_conv_state = conv_state[decode_state_indices].contiguous()
252 - decode_mixed_qkv = causal_conv1d_update_torch(
253 - x=decode_mixed_qkv.unsqueeze(-1),
254 - conv_state=decode_conv_state,
255 - weight=conv_weights,
256 - bias=layer.conv1d.bias,
257 - activation=layer.activation,
258 - ).squeeze(-1)
259 - conv_state[decode_state_indices] = decode_conv_state
260 - else:
261 - decode_mixed_qkv = causal_conv1d_update_cpu(
262 - x=decode_mixed_qkv,
263 - conv_state=conv_state,
264 - weight=conv_weights,
265 - bias=layer.conv1d.bias,
266 - activation=layer.activation,
267 - conv_state_indices=decode_state_indices,
268 - )
257 + decode_mixed_qkv = causal_conv1d_update_cpu(
258 + x=decode_mixed_qkv,
259 + conv_state=conv_state,
260 + weight=conv_weights,
261 + bias=layer.conv1d.bias,
262 + activation=layer.activation,
263 + conv_state_indices=decode_state_indices,
264 + )
265
266 query, key, value = layer.rearrange_mixed_qkv(decode_mixed_qkv)
267
@@ -315,7 +311,7 @@ def _cpu_gdn_attention_nonspec(
311 cache_indices=prefill_state_indices,
312 has_initial_state=prefill_has_initial_state,
313 silu_activation=layer.activation == "silu",
318 - is_vnni=True,
314 + is_weight_packed=True,
315 ).transpose(0, 1)
316 else:
317 prefill_mixed_qkv = causal_conv1d_torch(
@@ -383,9 +379,9 @@ def _ssm_state_view(layer) -> torch.Tensor:
379 def _unpacked_conv_weight(layer) -> torch.Tensor:
380 """Return the plain (dim, width) conv weight.
381
386 - On AMX the conv1d weight is VNNI-packed in place at load time (only usable
387 - by the AMX C++ kernel), so the torch spec-decode path relies on the
388 - un-packed copy stashed by ``dispatch_cpu_unquantized_gemm``.
382 + On AMX and aarch64+bf16 the conv1d weight is packed in place at load time,
383 + so the torch spec-decode path relies on the un-packed copy stashed by
384 + ``dispatch_cpu_unquantized_gemm``.
385 """
386 w = getattr(layer.conv1d, "_cpu_unpacked_conv_weight", None)
387 if w is not None:
@@ -505,13 +501,15 @@ def _spec_forward(
501 seq_starts = spec_qsl_cpu[:-1]
502 seq_lens = spec_qsl_cpu[1:] - spec_qsl_cpu[:-1]
503
504 + is_amx = torch.cpu._is_amx_tile_supported()
505 +
506 # ---- 1. Convolution (per-sequence rolling buffer) ----
507 dim = mixed_qkv_spec.size(-1)
508 bias = layer.conv1d.bias
509 silu = layer.activation == "silu"
510
511 can_use_native_conv = (
514 - torch.cpu._is_amx_tile_supported()
512 + (is_amx or is_arm_bf16())
513 and width == 4
514 and num_spec_decodes > 0
515 and bool(torch.all(seq_lens == seq_lens[0]).item())
@@ -528,7 +526,7 @@ def _spec_forward(
526 conv_state_indices=spec_state_indices[:num_spec_decodes, 0]
527 .to("cpu", torch.int32)
528 .contiguous(),
531 - is_vnni=True,
529 + is_weight_packed=True,
530 num_accepted_tokens=num_accepted[:num_spec_decodes].to("cpu", torch.int32),
531 ).view_as(mixed_qkv_spec)
532 else:
@@ -603,9 +601,9 @@ def _spec_aware_nonspec(
601 assert query_start_loc is not None
602 state_indices_tensor = state_indices_tensor.contiguous()
603
606 - is_amx = torch.cpu._is_amx_tile_supported()
604 + can_use_native_conv = torch.cpu._is_amx_tile_supported() or is_arm_bf16()
605
608 - if not is_amx:
606 + if not can_use_native_conv:
607 conv_weights = _unpacked_conv_weight(layer)
608
609 num_decodes = attn_metadata_i.num_decodes
@@ -618,7 +616,8 @@ def _spec_aware_nonspec(
616 decode_b = b[:num_decode_tokens]
617 decode_a = a[:num_decode_tokens]
618 decode_state_indices = state_indices_tensor[:num_decodes]
621 - if is_amx:
619 +
620 + if can_use_native_conv:
621 decode_mixed_qkv = _causal_conv1d_update_cpu(
622 x=decode_mixed_qkv,
623 conv_states=conv_buf,
@@ -626,30 +625,19 @@ def _spec_aware_nonspec(
625 bias=layer.conv1d.bias,
626 silu_activation=layer.activation == "silu",
627 conv_state_indices=decode_state_indices,
629 - is_vnni=True,
628 + is_weight_packed=True,
629 )
630 else:
631 # Only the first ``width-1`` columns hold the real conv state.
632 conv_state_view = conv_buf[:, :, : width - 1]
634 - if current_platform.get_cpu_architecture() == CpuArchEnum.ARM:
635 - decode_conv_state = conv_state_view[decode_state_indices].contiguous()
636 - decode_mixed_qkv = causal_conv1d_update_torch(
637 - x=decode_mixed_qkv.unsqueeze(-1),
638 - conv_state=decode_conv_state,
639 - weight=conv_weights,
640 - bias=layer.conv1d.bias,
641 - activation=layer.activation,
642 - ).squeeze(-1)
643 - conv_state_view[decode_state_indices] = decode_conv_state
644 - else:
645 - decode_mixed_qkv = causal_conv1d_update_cpu(
646 - x=decode_mixed_qkv,
647 - conv_state=conv_state_view,
648 - weight=conv_weights,
649 - bias=layer.conv1d.bias,
650 - activation=layer.activation,
651 - conv_state_indices=decode_state_indices,
652 - )
633 + decode_mixed_qkv = causal_conv1d_update_cpu(
634 + x=decode_mixed_qkv,
635 + conv_state=conv_state_view,
636 + weight=conv_weights,
637 + bias=layer.conv1d.bias,
638 + activation=layer.activation,
639 + conv_state_indices=decode_state_indices,
640 + )
641
642 query, key, value = layer.rearrange_mixed_qkv(decode_mixed_qkv)
643 attn_out = ops.fused_sigmoid_gating_delta_rule_update_cpu(
@@ -685,7 +673,8 @@ def _spec_aware_nonspec(
673 prefill_has_initial_state = has_initial_state[
674 num_decodes : num_decodes + num_prefills
675 ]
688 - if is_amx:
676 +
677 + if can_use_native_conv:
678 prefill_mixed_qkv = _causal_conv1d_fwd_cpu(
679 x=prefill_mixed_qkv.transpose(0, 1),
680 weight=layer.conv1d.weight,
@@ -695,7 +684,7 @@ def _spec_aware_nonspec(
684 cache_indices=prefill_state_indices,
685 has_initial_state=prefill_has_initial_state,
686 silu_activation=layer.activation == "silu",
698 - is_vnni=True,
687 + is_weight_packed=True,
688 ).transpose(0, 1)
689 else:
690 prefill_mixed_qkv = causal_conv1d_torch(
@@ -757,8 +746,9 @@ def _spec_aware_nonspec_subset(
746 prefill_state_indices = prefill_state_indices.contiguous()
747
748 is_amx = torch.cpu._is_amx_tile_supported()
749 + can_use_native_conv = is_amx or is_arm_bf16()
750
761 - if is_amx:
751 + if can_use_native_conv:
752 conv_out = _causal_conv1d_fwd_cpu(
753 x=mixed_qkv.transpose(0, 1),
754 weight=layer.conv1d.weight,
@@ -768,7 +758,7 @@ def _spec_aware_nonspec_subset(
758 cache_indices=prefill_state_indices,
759 has_initial_state=has_initial_state,
760 silu_activation=layer.activation == "silu",
771 - is_vnni=True,
761 + is_weight_packed=True,
762 ).transpose(0, 1)
763 else:
764 conv_weights = _unpacked_conv_weight(layer)
vllm/model_executor/layers/utils.py
+22 -8
@@ -475,13 +475,27 @@ def dispatch_cpu_unquantized_gemm(
475 # Skip CPU GEMM dispatch for non-2D weights (e.g. MoE 3D expert weights).
476 # These layers are handled by their own specialized methods.
477 if layer.weight.ndim != 2:
478 - # this is not a linear layer
479 - # For now it should be a causal_conv1d op or MoE 3D expert weights
480 - # The C++ causal_conv1d kernels use VDPBF16PS (no AMX tiles), so the
481 - # VNNI weight prepack applies to any AVX-512BF16 CPU, not just AMX
482 - # (e.g. AMD Zen5/Turin).
483 - if torch.cpu._is_avx512_bf16_supported() and hasattr(
484 - ops, "causal_conv1d_weight_pack"
478 + # This is not a linear layer.
479 + # For now it should be a causal_conv1d op or MoE 3D expert weights.
480 + # Causal-conv weights use [dim, 1, width].
481 + # The C++ causal_conv1d kernels support aarch64 with bf16 and
482 + # x86 CPUs that use VDPBF16PS (no AMX tiles). So the
483 + # weight prepack applies to aarch64 with bf16 and any AVX-512BF16 CPU,
484 + # not just AMX (e.g. AMD Zen5/Turin).
485 + is_causal_conv1d_weight = (
486 + layer.weight.ndim == 3
487 + and layer.weight.size(1) == 1
488 + and layer.weight.size(2) == 4
489 + and layer.weight.dtype == torch.bfloat16
490 + )
491 + is_arm_bf16 = (
492 + current_platform.get_cpu_architecture() == CpuArchEnum.ARM
493 + and torch.cpu.get_capabilities().get("bf16", False)
494 + )
495 + if (
496 + (is_arm_bf16 or torch.cpu._is_avx512_bf16_supported())
497 + and is_causal_conv1d_weight
498 + and hasattr(ops, "causal_conv1d_weight_pack")
499 ):
500 # prepack conv weight
501 unpacked = (
@@ -493,7 +507,7 @@ def dispatch_cpu_unquantized_gemm(
507 .clone()
508 )
509 # Stash the un-packed (dim, width) weight so the speculative-decode
496 - # GDN path (which uses torch conv, not the C++ kernel) can use it.
510 + # and unsupported-layout fallbacks can still use ordinary weights.
511 layer._cpu_unpacked_conv_weight = unpacked
512 layer.weight.data = ops.causal_conv1d_weight_pack(unpacked)
513 return