[CPU][Spec Decode] Optimize GDN conv path for speculative decoding (#48577)
Signed-off-by: Li, Tianmu <tianmu.li@intel.com> Co-authored-by: Codex <codex@openai.com> Co-authored-by: Li, Jiang <jiang1.li@intel.com>
Tianmu Li committed
Jul 26, 2026 at 23:20 UTC
544cb724c888dda1eff06a02cb0baca783c3dd86
5 files changed
+767
-122
csrc/cpu/sgl-kernels/conv.cpp
+173
-12
@@ -451,6 +451,90 @@ void causal_conv1d_update_kernel_impl(
451
});
452
}
453
454
+template <typename scalar_t>
455
+void causal_conv1d_update_multi_kernel_impl(
456
+ scalar_t* __restrict__ out,
457
+ const scalar_t* __restrict__ input,
458
+ scalar_t* __restrict__ conv_states,
459
+ const scalar_t* __restrict__ weight,
460
+ const scalar_t* __restrict__ bias,
461
+ const int32_t* __restrict__ num_accepted_tokens,
462
+ const int32_t* __restrict__ conv_indices,
463
+ bool silu_activation,
464
+ int64_t batch,
465
+ int64_t dim,
466
+ int64_t seqlen,
467
+ int64_t width,
468
+ int64_t state_len,
469
+ int64_t conv_state_slot_stride) {
470
+ constexpr int64_t BLOCK_N = block_size_n() * 2;
471
+ const int64_t NB = div_up(dim, BLOCK_N);
472
+
473
+ AT_DISPATCH_BOOL2(bias != nullptr, has_bias, silu_activation, has_silu, [&] {
474
+ at::parallel_for(0, batch * NB, 0, [&](int64_t begin, int64_t end) {
475
+ int64_t bs{0}, nb{0};
476
+ data_index_init(begin, bs, batch, nb, NB);
477
+
478
+ for (int64_t i = begin; i < end; ++i) {
479
+ const int64_t nb_start = nb * BLOCK_N;
480
+ const int64_t nb_size = std::min(dim - nb_start, BLOCK_N);
481
+ const int32_t conv_state_index = conv_indices[bs];
482
+ const int32_t history_offset = num_accepted_tokens[bs] - 1;
483
+
484
+ switch (width << 4 | nb_size >> 4) {
485
+ case 0x42:
486
+ tinygemm_kernel<scalar_t, 4, 32, has_bias, has_silu>::apply(
487
+ input + bs * seqlen * dim + nb_start,
488
+ weight + nb_start * width,
489
+ out + bs * seqlen * dim + nb_start,
490
+ has_bias ? bias + nb_start : nullptr,
491
+ conv_states + conv_state_index * conv_state_slot_stride +
492
+ history_offset * dim + nb_start,
493
+ true,
494
+ seqlen,
495
+ dim,
496
+ true);
497
+ break;
498
+ case 0x44:
499
+ tinygemm_kernel<scalar_t, 4, 64, has_bias, has_silu>::apply(
500
+ input + bs * seqlen * dim + nb_start,
501
+ weight + nb_start * width,
502
+ out + bs * seqlen * dim + nb_start,
503
+ has_bias ? bias + nb_start : nullptr,
504
+ conv_states + conv_state_index * conv_state_slot_stride +
505
+ history_offset * dim + nb_start,
506
+ true,
507
+ seqlen,
508
+ dim,
509
+ true);
510
+ break;
511
+ default:
512
+ TORCH_CHECK(false, "Unexpected block size, ", width, " x ", nb_size);
513
+ }
514
+
515
+ data_index_step(bs, batch, nb, NB);
516
+ }
517
+ });
518
+ });
519
+
520
+ at::parallel_for(0, batch, 0, [&](int64_t begin, int64_t end) {
521
+ for (int64_t bs = begin; bs < end; ++bs) {
522
+ const int32_t conv_state_index = conv_indices[bs];
523
+ const int32_t num_accepted = num_accepted_tokens[bs];
524
+ scalar_t* state = conv_states + conv_state_index * conv_state_slot_stride;
525
+
526
+ std::memmove(
527
+ state,
528
+ state + num_accepted * dim,
529
+ (state_len - seqlen) * dim * sizeof(scalar_t));
530
+ std::memcpy(
531
+ state + (state_len - seqlen) * dim,
532
+ input + bs * seqlen * dim,
533
+ seqlen * dim * sizeof(scalar_t));
534
+ }
535
+ });
536
+}
537
+
538
} // anonymous namespace
539
540
// from [dim, width] or [N, K]
@@ -545,7 +629,7 @@ at::Tensor get_block_indices(const std::optional<at::Tensor>& offsets, int64_t n
629
// query_start_loc: (batch + 1) int32
630
// cache_indices: (batch) int32
631
// has_initial_state: (batch) bool
548
-// conv_states: (..., dim, width - 1) itype
632
+// conv_states: (..., dim, state_len) itype, where state_len >= width - 1
633
// activation: either None or "silu" or "swish"
634
// pad_slot_id: int
635
//
@@ -586,11 +670,14 @@ at::Tensor causal_conv1d_fwd_cpu(
670
CHECK_EQ(conv_states_val.scalar_type(), scalar_type);
671
CHECK_GE(padded_batch, batch);
672
CHECK_EQ(conv_states_val.size(1), dim);
589
- CHECK_EQ(conv_states_val.size(2), width - 1);
673
+ const int64_t state_len = conv_states_val.size(2);
674
+ CHECK_GE(state_len, width - 1);
675
676
// adjust `conv_states` to be contiguous on `dim`
677
// should happen only once
678
if (conv_states_val.stride(-2) != 1) {
679
+ TORCH_CHECK(state_len == width - 1,
680
+ "causal_conv1d_fwd_cpu: wide conv_states must be contiguous on dim.");
681
auto conv_states_copy = conv_states_val.clone();
682
conv_states_val.as_strided_({padded_batch, dim, width - 1}, {(width - 1) * dim, 1, dim});
683
conv_states_val.copy_(conv_states_copy);
@@ -651,14 +738,14 @@ at::Tensor causal_conv1d_fwd_cpu(
738
739
// API aligned with GPUs
740
//
654
-// x: (batch, dim) or (batch, dim, seqlen)
741
+// x: (batch, dim) or (batch, seqlen, dim)
742
// conv_state: (..., dim, state_len), where state_len >= width - 1
743
// weight: (dim, width)
744
// bias: (dim,)
658
-// cache_seqlens: (batch,), dtype int32.
745
+// num_accepted_tokens: (batch,), dtype int32.
746
// conv_state_indices: (batch,), dtype int32
747
// pad_slot_id: int
661
-// out: (batch, dim) or (batch, dim, seqlen)
748
+// out: (batch, dim) or (batch, seqlen, dim)
749
//
750
at::Tensor causal_conv1d_update_cpu(
751
const at::Tensor& x,
@@ -666,7 +753,7 @@ at::Tensor causal_conv1d_update_cpu(
753
const at::Tensor& weight,
754
const std::optional<at::Tensor>& bias,
755
bool silu_activation,
669
- const std::optional<at::Tensor>& cache_seqlens,
756
+ const std::optional<at::Tensor>& num_accepted_tokens,
757
const std::optional<at::Tensor>& conv_state_indices,
758
int64_t pad_slot_id,
759
bool is_vnni) {
@@ -674,13 +761,13 @@ at::Tensor causal_conv1d_update_cpu(
761
CHECK_CONTIGUOUS(weight);
762
auto packed_w = is_vnni ? weight : causal_conv1d_weight_pack(weight);
763
677
- // TODO: add multi-token prediction support
678
- TORCH_CHECK(x.dim() == 2, "causal_conv1d_update_cpu: expect x to be 2D tensor.");
679
- TORCH_CHECK(!cache_seqlens.has_value(), "causal_conv1d_update_cpu: don't support cache_seqlens.");
764
+ TORCH_CHECK(
765
+ x.dim() == 2 || x.dim() == 3,
766
+ "causal_conv1d_update_cpu: expect x to be 2D or 3D tensor.");
767
768
int64_t batch = x.size(0);
682
- int64_t dim = x.size(1);
683
- int64_t seqlen = 1;
769
+ int64_t dim = x.dim() == 2 ? x.size(1) : x.size(2);
770
+ int64_t seqlen = x.dim() == 2 ? 1 : x.size(1);
771
int64_t width = weight.size(-1);
772
773
const auto scalar_type = x.scalar_type();
@@ -690,10 +777,84 @@ at::Tensor causal_conv1d_update_cpu(
777
778
CHECK_EQ(conv_states.scalar_type(), scalar_type);
779
CHECK_EQ(conv_states.size(1), dim);
693
- CHECK_EQ(conv_states.size(2), width - 1);
780
+ const int64_t state_len = conv_states.size(2);
781
+ CHECK_GE(state_len, width - 1);
782
+
783
+ if (x.dim() == 3) {
784
+ TORCH_CHECK(
785
+ num_accepted_tokens.has_value(),
786
+ "causal_conv1d_update_cpu: num_accepted_tokens is required for 3D x.");
787
+ TORCH_CHECK(
788
+ conv_state_indices.has_value(),
789
+ "causal_conv1d_update_cpu: conv_state_indices is required for 3D x.");
790
+ CHECK_OPTIONAL_SHAPE_DTYPE(num_accepted_tokens, batch, at::kInt);
791
+ TORCH_CHECK(
792
+ width == 4,
793
+ "causal_conv1d_update_cpu: support only width of 4 for 3D x.");
794
+ TORCH_CHECK(
795
+ seqlen > 0,
796
+ "causal_conv1d_update_cpu: expect non-empty sequence for 3D x.");
797
+ TORCH_CHECK(
798
+ state_len >= seqlen,
799
+ "causal_conv1d_update_cpu: state_len must be >= seqlen for 3D x.");
800
+ TORCH_CHECK(
801
+ conv_states.stride(-2) == 1 && conv_states.stride(-1) == dim,
802
+ "causal_conv1d_update_cpu: 3D x requires SD conv_states layout.");
803
+
804
+ const int32_t* accepted_counts =
805
+ num_accepted_tokens.value().data_ptr<int32_t>();
806
+ const int32_t* indices = conv_state_indices.value().data_ptr<int32_t>();
807
+ const int64_t num_slots = conv_states.size(0);
808
+ for (int64_t bs = 0; bs < batch; ++bs) {
809
+ const int32_t num_accepted = accepted_counts[bs];
810
+ const int32_t conv_state_index = indices[bs];
811
+ TORCH_CHECK(
812
+ conv_state_index != pad_slot_id,
813
+ "causal_conv1d_update_cpu: 3D x does not support pad slots.");
814
+ TORCH_CHECK(
815
+ conv_state_index >= 0 && conv_state_index < num_slots,
816
+ "causal_conv1d_update_cpu: conv_state_indices out of range.");
817
+ TORCH_CHECK(
818
+ num_accepted >= 1 && num_accepted <= seqlen,
819
+ "causal_conv1d_update_cpu: num_accepted_tokens must be in [1, "
820
+ "seqlen].");
821
+ TORCH_CHECK(
822
+ num_accepted - 1 + width - 1 <= state_len,
823
+ "causal_conv1d_update_cpu: history window exceeds conv_states.");
824
+ }
825
+
826
+ int64_t conv_state_slot_stride = conv_states.stride(0);
827
+ at::Tensor out = at::empty_like(x);
828
+ AT_DISPATCH_REDUCED_FLOATING_TYPES(
829
+ scalar_type, "causal_conv1d_update_multi_kernel_impl", [&] {
830
+ causal_conv1d_update_multi_kernel_impl<scalar_t>(
831
+ out.data_ptr<scalar_t>(),
832
+ x.data_ptr<scalar_t>(),
833
+ conv_states.data_ptr<scalar_t>(),
834
+ packed_w.data_ptr<scalar_t>(),
835
+ conditional_data_ptr<scalar_t>(bias),
836
+ accepted_counts,
837
+ indices,
838
+ silu_activation,
839
+ batch,
840
+ dim,
841
+ seqlen,
842
+ width,
843
+ state_len,
844
+ conv_state_slot_stride);
845
+ });
846
+ return out;
847
+ }
848
+
849
+ TORCH_CHECK(
850
+ !num_accepted_tokens.has_value(),
851
+ "causal_conv1d_update_cpu: num_accepted_tokens is only supported for 3D "
852
+ "x.");
853
854
// adjust `conv_states` to be contiguous on `dim`
855
if (conv_states.stride(-2) != 1) {
856
+ TORCH_CHECK(state_len == width - 1,
857
+ "causal_conv1d_update_cpu: wide conv_states must be contiguous on dim.");
858
int64_t num_cache_lines = conv_states.size(0);
859
auto conv_states_copy = conv_states.clone();
860
conv_states.as_strided_({num_cache_lines, dim, width - 1}, {(width - 1) * dim, 1, dim});
csrc/cpu/torch_bindings.cpp
+3
-2
@@ -147,7 +147,7 @@ at::Tensor causal_conv1d_fwd_cpu(
147
at::Tensor causal_conv1d_update_cpu(
148
const at::Tensor& x, const at::Tensor& conv_states,
149
const at::Tensor& weight, const std::optional<at::Tensor>& bias,
150
- bool silu_activation, const std::optional<at::Tensor>& cache_seqlens,
150
+ bool silu_activation, const std::optional<at::Tensor>& num_accepted_tokens,
151
const std::optional<at::Tensor>& conv_state_indices, int64_t pad_slot_id,
152
bool is_vnni);
153
@@ -516,7 +516,8 @@ TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, ops) {
516
ops.def(
517
"causal_conv1d_update_cpu(Tensor x, Tensor(a!) conv_states, Tensor "
518
"weight, Tensor? bias, bool silu_activation,"
519
- "Tensor? cache_seqlens, Tensor? conv_state_indices, int pad_slot_id, "
519
+ "Tensor? num_accepted_tokens, Tensor? conv_state_indices, int "
520
+ "pad_slot_id, "
521
"bool is_vnni) -> Tensor");
522
ops.impl("causal_conv1d_update_cpu", torch::kCPU, &causal_conv1d_update_cpu);
523
#endif
tests/kernels/mamba/cpu/test_cpu_gdn_ops.py
+435
-1
@@ -2,12 +2,14 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
import functools
5
+import types
6
7
import pytest
8
import torch
9
import torch.nn.functional as F
10
11
import vllm._custom_ops as ops
12
+from vllm.model_executor.layers.mamba.ops.cpu import gdn_attention
13
from vllm.platforms import current_platform
14
from vllm.utils.torch_utils import set_random_seed
15
@@ -417,6 +419,207 @@ def _conv_inputs(total_tokens: int):
419
return x, weight, bias
420
421
422
+def _sd_conv_states(
423
+ num_slots: int, state_len: int, dim: int = CONV_DIM
424
+) -> torch.Tensor:
425
+ storage = torch.zeros(num_slots, state_len, dim, dtype=torch.bfloat16)
426
+ return storage.transpose(1, 2)
427
+
428
+
429
+def _maybe_pack_conv_weight(weight: torch.Tensor, is_vnni: bool) -> torch.Tensor:
430
+ return ops.causal_conv1d_weight_pack(weight) if is_vnni else weight
431
+
432
+
433
+@torch.inference_mode()
434
+def test_spec_aware_mixed_routing_preserves_token_order(
435
+ monkeypatch: pytest.MonkeyPatch,
436
+) -> None:
437
+ num_tokens = 4
438
+ projection = torch.arange(num_tokens * 16, dtype=torch.float32).view(num_tokens, 16)
439
+ mixed_qkv, b, a = projection[:, :4], projection[:, 4:6], projection[:, 6:8]
440
+ assert all(not tensor.is_contiguous() for tensor in (mixed_qkv, b, a))
441
+
442
+ spec_indices = torch.tensor([0, 2])
443
+ nonspec_indices = torch.tensor([1, 3])
444
+ metadata = types.SimpleNamespace(
445
+ spec_sequence_masks=torch.ones(1, dtype=torch.bool),
446
+ spec_token_indx=spec_indices,
447
+ non_spec_token_indx=nonspec_indices,
448
+ num_prefills=1,
449
+ num_decodes=0,
450
+ )
451
+ routed = []
452
+
453
+ def record(*args):
454
+ routed.append(args[2:5])
455
+ return args[2]
456
+
457
+ monkeypatch.setattr(gdn_attention, "is_conv_state_dim_first", lambda: True)
458
+ monkeypatch.setattr(gdn_attention, "_spec_forward", record)
459
+ monkeypatch.setattr(gdn_attention, "_spec_aware_nonspec_subset", record)
460
+
461
+ layer = types.SimpleNamespace(
462
+ kv_cache=[torch.empty(1, 4, 6), torch.empty(1, 1, 1, 1)]
463
+ )
464
+ core_attn_out = torch.empty_like(mixed_qkv)
465
+ gdn_attention._cpu_gdn_attention_spec_aware(
466
+ layer=layer,
467
+ attn_metadata_i=metadata,
468
+ mixed_qkv=mixed_qkv,
469
+ b=b,
470
+ a=a,
471
+ core_attn_out=core_attn_out,
472
+ width=CONV_KERNEL,
473
+ state_len=6,
474
+ )
475
+
476
+ expected_inputs = (mixed_qkv, b, a)
477
+ assert len(routed) == 2
478
+ for actual_inputs, indices in zip(routed, (spec_indices, nonspec_indices)):
479
+ for actual, expected in zip(actual_inputs, expected_inputs):
480
+ assert actual.is_contiguous()
481
+ torch.testing.assert_close(actual, expected.index_select(0, indices))
482
+ torch.testing.assert_close(core_attn_out, mixed_qkv)
483
+
484
+
485
+@torch.inference_mode()
486
+def test_spec_aware_nonspec_materializes_state_indices(
487
+ monkeypatch: pytest.MonkeyPatch,
488
+) -> None:
489
+ block_table = torch.arange(8, dtype=torch.int32).view(2, 4)
490
+ state_indices = block_table[:, 0]
491
+ assert not state_indices.is_contiguous()
492
+
493
+ metadata = types.SimpleNamespace(
494
+ non_spec_state_indices_tensor=state_indices,
495
+ non_spec_query_start_loc=torch.tensor([0, 2, 4], dtype=torch.int32),
496
+ num_decodes=0,
497
+ num_decode_tokens=0,
498
+ num_prefills=2,
499
+ num_prefill_tokens=4,
500
+ has_initial_state=torch.tensor([False, False]),
501
+ )
502
+
503
+ recorded_indices = None
504
+
505
+ def causal_conv1d_fwd_cpu(**kwargs):
506
+ nonlocal recorded_indices
507
+ recorded_indices = kwargs["cache_indices"]
508
+ return kwargs["x"]
509
+
510
+ def fused_gdn_gating_cpu(**kwargs):
511
+ return kwargs["a"], kwargs["b"]
512
+
513
+ def chunk_gated_delta_rule_cpu(**kwargs):
514
+ out = torch.zeros(1, 4, 1, 1)
515
+ return out, kwargs["initial_state"]
516
+
517
+ monkeypatch.setattr(torch.cpu, "_is_amx_tile_supported", lambda: True)
518
+ monkeypatch.setattr(gdn_attention, "is_conv_state_dim_first", lambda: False)
519
+ monkeypatch.setattr(
520
+ gdn_attention.ops, "causal_conv1d_fwd_cpu", causal_conv1d_fwd_cpu
521
+ )
522
+ monkeypatch.setattr(gdn_attention.ops, "fused_gdn_gating_cpu", fused_gdn_gating_cpu)
523
+ monkeypatch.setattr(
524
+ gdn_attention.ops,
525
+ "chunk_gated_delta_rule_cpu",
526
+ chunk_gated_delta_rule_cpu,
527
+ )
528
+
529
+ layer = types.SimpleNamespace(
530
+ activation="silu",
531
+ conv1d=types.SimpleNamespace(weight=torch.empty(0), bias=None),
532
+ A_log=torch.empty(0),
533
+ dt_bias=torch.empty(0),
534
+ rearrange_mixed_qkv=lambda x: (
535
+ x[:, :1].view(1, 4, 1, 1),
536
+ x[:, :1].view(1, 4, 1, 1),
537
+ x[:, :1].view(1, 4, 1, 1),
538
+ ),
539
+ )
540
+ gdn_attention._spec_aware_nonspec(
541
+ layer=layer,
542
+ attn_metadata_i=metadata,
543
+ mixed_qkv=torch.zeros(4, 4),
544
+ b=torch.zeros(4, 1),
545
+ a=torch.zeros(4, 1),
546
+ core_attn_out=torch.zeros(4, 1, 1),
547
+ conv_buf=torch.zeros(8, 1, 6),
548
+ ssm_state=torch.zeros(8, 1, 1, 1),
549
+ width=4,
550
+ )
551
+
552
+ assert recorded_indices is not None
553
+ assert recorded_indices.is_contiguous()
554
+ torch.testing.assert_close(
555
+ recorded_indices, torch.tensor([0, 4], dtype=torch.int32)
556
+ )
557
+
558
+
559
+@torch.inference_mode()
560
+def test_spec_forward_prepares_native_conv_metadata(
561
+ monkeypatch: pytest.MonkeyPatch,
562
+) -> None:
563
+ block_table = torch.tensor([[0, 1], [4, 5]], dtype=torch.int32)
564
+ state_indices = block_table[:, 0]
565
+ accepted_counts = torch.tensor([1, 4], dtype=torch.int32)
566
+ assert not state_indices.is_contiguous()
567
+ metadata = types.SimpleNamespace(
568
+ num_spec_decodes=2,
569
+ spec_state_indices_tensor=block_table,
570
+ spec_query_start_loc=torch.tensor([0, 4, 8], dtype=torch.int32),
571
+ num_accepted_tokens=accepted_counts,
572
+ )
573
+ forwarded_indices = None
574
+ forwarded_counts = None
575
+
576
+ def causal_conv1d_update_cpu(**kwargs):
577
+ nonlocal forwarded_counts, forwarded_indices
578
+ forwarded_indices = kwargs["conv_state_indices"]
579
+ forwarded_counts = kwargs["num_accepted_tokens"]
580
+ return kwargs["x"]
581
+
582
+ monkeypatch.setattr(torch.cpu, "_is_amx_tile_supported", lambda: True)
583
+ monkeypatch.setattr(gdn_attention, "is_conv_state_dim_first", lambda: False)
584
+ monkeypatch.setattr(
585
+ gdn_attention.ops, "causal_conv1d_update_cpu", causal_conv1d_update_cpu
586
+ )
587
+ monkeypatch.setattr(
588
+ gdn_attention.ops,
589
+ "fused_sigmoid_gating_delta_rule_update_spec_cpu",
590
+ lambda **kwargs: kwargs["q"],
591
+ )
592
+
593
+ layer = types.SimpleNamespace(
594
+ activation="silu",
595
+ conv1d=types.SimpleNamespace(weight=torch.empty(1, CONV_KERNEL), bias=None),
596
+ A_log=None,
597
+ dt_bias=None,
598
+ rearrange_mixed_qkv=lambda x: (x.unsqueeze(0),) * 3,
599
+ )
600
+ gdn_attention._spec_forward(
601
+ layer=layer,
602
+ attn_metadata_i=metadata,
603
+ mixed_qkv_spec=torch.zeros(8, 1, dtype=torch.bfloat16),
604
+ b_spec=torch.empty(0),
605
+ a_spec=torch.empty(0),
606
+ conv_buf=torch.empty(0),
607
+ ssm_state=torch.empty(0),
608
+ width=CONV_KERNEL,
609
+ state_len=0,
610
+ )
611
+
612
+ expected = (
613
+ (forwarded_indices, torch.tensor([0, 4], dtype=torch.int32)),
614
+ (forwarded_counts, torch.tensor([1, 4], dtype=torch.int32)),
615
+ )
616
+ for actual, reference in expected:
617
+ assert actual is not None
618
+ assert actual.is_contiguous()
619
+ assert actual.dtype == torch.int32
620
+ torch.testing.assert_close(actual, reference)
621
+
622
+
623
@pytest.mark.parametrize("total_tokens, split", TWO_CALL_SPLITS)
624
@torch.inference_mode()
625
def test_causal_conv1d_torch_two_call_split(total_tokens: int, split: int) -> None:
@@ -475,7 +678,182 @@ def test_causal_conv1d_torch_two_call_split(total_tokens: int, split: int) -> No
678
679
@pytest.mark.skipif(
680
not torch.cpu._is_amx_tile_supported(),
478
- reason="causal_conv1d_fwd_cpu requires AMX/AVX512",
681
+ reason="requires AMX support",
682
+)
683
+@torch.inference_mode()
684
+def test_causal_conv1d_update_cpu_accepts_wide_state() -> None:
685
+ state_len = CONV_KERNEL - 1
686
+ wide_state_len = state_len + 5
687
+ batch_size = 3
688
+ is_vnni = True
689
+ x, weight, bias = _conv_inputs(batch_size)
690
+ conv_state_indices = torch.tensor([2, 0, 1], dtype=torch.int32)
691
+
692
+ narrow_state = _sd_conv_states(batch_size, state_len)
693
+ narrow_state.copy_(
694
+ tensor_cache(narrow_state.numel(), torch.bfloat16).view_as(narrow_state)
695
+ )
696
+ wide_state = _sd_conv_states(batch_size, wide_state_len)
697
+ wide_state[:, :, :state_len].copy_(narrow_state)
698
+ wide_state[:, :, state_len:].fill_(7)
699
+ wide_tail = wide_state[:, :, state_len:].clone()
700
+
701
+ conv_weight = _maybe_pack_conv_weight(weight, is_vnni)
702
+ out_narrow = ops.causal_conv1d_update_cpu(
703
+ x=x,
704
+ conv_states=narrow_state,
705
+ weight=conv_weight,
706
+ bias=bias,
707
+ silu_activation=True,
708
+ conv_state_indices=conv_state_indices,
709
+ is_vnni=is_vnni,
710
+ )
711
+ out_wide = ops.causal_conv1d_update_cpu(
712
+ x=x,
713
+ conv_states=wide_state,
714
+ weight=conv_weight,
715
+ bias=bias,
716
+ silu_activation=True,
717
+ conv_state_indices=conv_state_indices,
718
+ is_vnni=is_vnni,
719
+ )
720
+
721
+ torch.testing.assert_close(out_wide, out_narrow, atol=1e-2, rtol=1e-2)
722
+ torch.testing.assert_close(
723
+ wide_state[:, :, :state_len], narrow_state, atol=0, rtol=0
724
+ )
725
+ torch.testing.assert_close(wide_state[:, :, state_len:], wide_tail, atol=0, rtol=0)
726
+
727
+
728
+def _ref_causal_conv1d_update_cpu_multi(
729
+ x: torch.Tensor,
730
+ conv_states: torch.Tensor,
731
+ weight: torch.Tensor,
732
+ bias: torch.Tensor | None,
733
+ silu_activation: bool,
734
+ conv_state_indices: torch.Tensor,
735
+ num_accepted_tokens: torch.Tensor,
736
+) -> torch.Tensor:
737
+ batch_size, seq_len, dim = x.shape
738
+ state_len = conv_states.size(2)
739
+ conv_out = torch.empty_like(x)
740
+ conv_weight = weight.unsqueeze(1)
741
+
742
+ for i in range(batch_size):
743
+ slot = int(conv_state_indices[i].item())
744
+ offset = int(num_accepted_tokens[i].item()) - 1
745
+ state = conv_states[slot]
746
+ x_seq = x[i].transpose(0, 1).to(state.dtype)
747
+ prior = state[:, offset : offset + CONV_KERNEL - 1]
748
+ conv_in = torch.cat([prior, x_seq], dim=-1).unsqueeze(0)
749
+ out = F.conv1d(conv_in, conv_weight, bias, groups=dim)[0]
750
+ if silu_activation:
751
+ out = F.silu(out)
752
+ conv_out[i] = out.transpose(0, 1).to(conv_out.dtype)
753
+ keep = state[:, offset + 1 : offset + 1 + (state_len - seq_len)]
754
+ state.copy_(torch.cat([keep, x_seq], dim=-1))
755
+
756
+ return conv_out
757
+
758
+
759
+@pytest.mark.skipif(
760
+ not torch.cpu._is_amx_tile_supported(),
761
+ reason="requires AMX support",
762
+)
763
+@pytest.mark.parametrize(
764
+ ("batch_size, seq_len, accepted_counts, has_bias, silu_activation, is_vnni"),
765
+ [
766
+ (1, 1, [1], False, False, False),
767
+ (1, 1, [1], True, True, True),
768
+ (4, 4, [1, 2, 3, 4], False, True, False),
769
+ (4, 4, [4, 3, 2, 1], True, False, True),
770
+ (4, 16, [1, 5, 10, 16], False, False, True),
771
+ (4, 16, [16, 10, 5, 1], True, True, False),
772
+ ],
773
+)
774
+@torch.inference_mode()
775
+def test_causal_conv1d_update_cpu_multi_token_matches_python(
776
+ batch_size: int,
777
+ seq_len: int,
778
+ accepted_counts: list[int],
779
+ has_bias: bool,
780
+ silu_activation: bool,
781
+ is_vnni: bool,
782
+) -> None:
783
+ dim = 96
784
+ state_len = seq_len + 2
785
+ x = tensor_cache(batch_size * seq_len * dim, torch.bfloat16).view(
786
+ batch_size, seq_len, dim
787
+ )
788
+ weight = tensor_cache(dim * CONV_KERNEL, torch.bfloat16).view(dim, CONV_KERNEL)
789
+ bias = tensor_cache(dim, torch.bfloat16) if has_bias else None
790
+ conv_state_indices = torch.arange(batch_size - 1, -1, -1, dtype=torch.int32)
791
+ num_accepted_tokens = torch.tensor(accepted_counts, dtype=torch.int32)
792
+
793
+ conv_states_ref = _sd_conv_states(batch_size, state_len, dim)
794
+ conv_states_ref.copy_(
795
+ tensor_cache(conv_states_ref.numel(), torch.bfloat16).view_as(conv_states_ref)
796
+ )
797
+ conv_states = conv_states_ref.clone()
798
+
799
+ conv_weight = _maybe_pack_conv_weight(weight, is_vnni)
800
+ out = ops.causal_conv1d_update_cpu(
801
+ x=x,
802
+ conv_states=conv_states,
803
+ weight=conv_weight,
804
+ bias=bias,
805
+ silu_activation=silu_activation,
806
+ conv_state_indices=conv_state_indices,
807
+ is_vnni=is_vnni,
808
+ num_accepted_tokens=num_accepted_tokens,
809
+ )
810
+ ref_out = _ref_causal_conv1d_update_cpu_multi(
811
+ x=x,
812
+ conv_states=conv_states_ref,
813
+ weight=weight,
814
+ bias=bias,
815
+ silu_activation=silu_activation,
816
+ conv_state_indices=conv_state_indices,
817
+ num_accepted_tokens=num_accepted_tokens,
818
+ )
819
+
820
+ torch.testing.assert_close(out, ref_out, atol=1e-2, rtol=1e-2)
821
+ torch.testing.assert_close(conv_states, conv_states_ref, atol=0, rtol=0)
822
+
823
+
824
+@pytest.mark.skipif(
825
+ not torch.cpu._is_amx_tile_supported(),
826
+ reason="requires AMX support",
827
+)
828
+@pytest.mark.parametrize("num_accepted", [0, 17])
829
+@torch.inference_mode()
830
+def test_causal_conv1d_update_cpu_rejects_invalid_accepted_count(
831
+ num_accepted: int,
832
+) -> None:
833
+ batch_size = 1
834
+ seq_len = 16
835
+ dim = 96
836
+ state_len = seq_len + 2
837
+ x = torch.zeros(batch_size, seq_len, dim, dtype=torch.bfloat16)
838
+ weight = torch.zeros(dim, CONV_KERNEL, dtype=torch.bfloat16)
839
+ conv_states = _sd_conv_states(batch_size, state_len, dim)
840
+
841
+ with pytest.raises(RuntimeError, match="num_accepted_tokens must be in.*seqlen"):
842
+ ops.causal_conv1d_update_cpu(
843
+ x=x,
844
+ conv_states=conv_states,
845
+ weight=weight,
846
+ bias=None,
847
+ silu_activation=True,
848
+ conv_state_indices=torch.tensor([0], dtype=torch.int32),
849
+ is_vnni=False,
850
+ num_accepted_tokens=torch.tensor([num_accepted], dtype=torch.int32),
851
+ )
852
+
853
+
854
+@pytest.mark.skipif(
855
+ not torch.cpu._is_amx_tile_supported(),
856
+ reason="requires AMX support",
857
)
858
@pytest.mark.parametrize("total_tokens, split", TWO_CALL_SPLITS)
859
@torch.inference_mode()
@@ -515,6 +893,62 @@ def test_causal_conv1d_fwd_cpu_two_call_split(total_tokens: int, split: int) ->
893
torch.testing.assert_close(out_split, out_full, atol=1e-2, rtol=1e-2)
894
895
896
+@pytest.mark.skipif(
897
+ not torch.cpu._is_amx_tile_supported(),
898
+ reason="requires AMX support",
899
+)
900
+@torch.inference_mode()
901
+def test_causal_conv1d_fwd_cpu_accepts_wide_state() -> None:
902
+ state_len = CONV_KERNEL - 1
903
+ wide_state_len = state_len + 5
904
+ is_vnni = True
905
+ seq_lens = [CHUNK_SIZE - 1, CHUNK_SIZE + 5]
906
+ total_tokens = sum(seq_lens)
907
+ x, weight, bias = _conv_inputs(total_tokens)
908
+ query_start_loc = torch.tensor([0, seq_lens[0], total_tokens], dtype=torch.int32)
909
+ cache_indices = torch.tensor([2, 0], dtype=torch.int32)
910
+ has_initial_state = torch.tensor([True, False])
911
+
912
+ narrow_state = _sd_conv_states(3, state_len)
913
+ narrow_state.copy_(
914
+ tensor_cache(narrow_state.numel(), torch.bfloat16).view_as(narrow_state)
915
+ )
916
+ wide_state = _sd_conv_states(3, wide_state_len)
917
+ wide_state[:, :, :state_len].copy_(narrow_state)
918
+ wide_state[:, :, state_len:].fill_(7)
919
+ wide_tail = wide_state[:, :, state_len:].clone()
920
+
921
+ conv_weight = _maybe_pack_conv_weight(weight, is_vnni)
922
+ out_narrow = ops.causal_conv1d_fwd_cpu(
923
+ x=x.transpose(0, 1),
924
+ weight=conv_weight,
925
+ bias=bias,
926
+ conv_states=narrow_state,
927
+ query_start_loc=query_start_loc,
928
+ cache_indices=cache_indices,
929
+ has_initial_state=has_initial_state,
930
+ silu_activation=True,
931
+ is_vnni=is_vnni,
932
+ )
933
+ out_wide = ops.causal_conv1d_fwd_cpu(
934
+ x=x.transpose(0, 1),
935
+ weight=conv_weight,
936
+ bias=bias,
937
+ conv_states=wide_state,
938
+ query_start_loc=query_start_loc,
939
+ cache_indices=cache_indices,
940
+ has_initial_state=has_initial_state,
941
+ silu_activation=True,
942
+ is_vnni=is_vnni,
943
+ )
944
+
945
+ torch.testing.assert_close(out_wide, out_narrow, atol=1e-2, rtol=1e-2)
946
+ torch.testing.assert_close(
947
+ wide_state[:, :, :state_len], narrow_state, atol=0, rtol=0
948
+ )
949
+ torch.testing.assert_close(wide_state[:, :, state_len:], wide_tail, atol=0, rtol=0)
950
+
951
+
952
@torch.inference_mode()
953
def test_batch_memcpy_cpu_fallback() -> None:
954
"""The ctypes batch_memcpy fallback (used when triton-cpu is absent) must
vllm/_custom_ops.py
+2
-1
@@ -3505,6 +3505,7 @@ def causal_conv1d_update_cpu(
3505
silu_activation: bool,
3506
conv_state_indices: torch.Tensor | None,
3507
is_vnni: bool,
3508
+ num_accepted_tokens: torch.Tensor | None = None,
3509
) -> torch.Tensor:
3510
return torch.ops._C.causal_conv1d_update_cpu(
3511
x,
@@ -3512,7 +3513,7 @@ def causal_conv1d_update_cpu(
3513
weight,
3514
bias,
3515
silu_activation,
3515
- None,
3516
+ num_accepted_tokens,
3517
conv_state_indices,
3518
-1,
3519
is_vnni,
vllm/model_executor/layers/mamba/ops/cpu/gdn_attention.py
+154
-106
@@ -306,25 +306,21 @@ def _cpu_gdn_attention_spec_aware(
306
width: int,
307
state_len: int,
308
) -> None:
309
- mixed_qkv = mixed_qkv.contiguous()
310
- a = a.contiguous()
311
- b = b.contiguous()
312
-
309
spec_sequence_masks = attn_metadata_i.spec_sequence_masks
310
conv_buf = _conv_buffer_view(layer) # (num_slots, dim, state_len)
311
ssm_state = _ssm_state_view(layer)
312
313
if spec_sequence_masks is None:
314
# No spec sequences in this batch (e.g. the prompt prefill step while
319
- # speculative decoding is configured). Process as prefill/decode using
320
- # torch conv (which only touches the first ``width-1`` columns of the
321
- # wide buffer, leaving the rolling history untouched).
315
+ # speculative decoding is configured). Process as ordinary
316
+ # prefill/decode while touching only the first ``width-1`` columns of
317
+ # the wide buffer, leaving the rolling history untouched.
318
_spec_aware_nonspec(
319
layer,
320
attn_metadata_i,
325
- mixed_qkv,
326
- b,
327
- a,
321
+ mixed_qkv.contiguous(),
322
+ b.contiguous(),
323
+ a.contiguous(),
324
core_attn_out,
325
conv_buf,
326
ssm_state,
@@ -339,9 +335,9 @@ def _cpu_gdn_attention_spec_aware(
335
num_decodes = attn_metadata_i.num_decodes
336
337
if num_prefills == 0 and num_decodes == 0:
342
- mixed_qkv_spec = mixed_qkv
343
- b_spec = b
344
- a_spec = a
338
+ mixed_qkv_spec = mixed_qkv.contiguous()
339
+ b_spec = b.contiguous()
340
+ a_spec = a.contiguous()
341
spec_out_indx = None
342
else:
343
assert spec_token_indx is not None
@@ -405,50 +401,71 @@ def _spec_forward(
401
assert spec_qsl is not None
402
assert num_accepted is not None
403
408
- spec_qsl_cpu = spec_qsl[: num_spec_decodes + 1].to("cpu", torch.int64)
409
- num_acc_cpu = num_accepted[:num_spec_decodes].to("cpu", torch.int64)
404
+ spec_qsl_cpu = spec_qsl[: num_spec_decodes + 1]
405
seq_starts = spec_qsl_cpu[:-1]
406
seq_lens = spec_qsl_cpu[1:] - spec_qsl_cpu[:-1]
407
408
# ---- 1. Convolution (per-sequence rolling buffer) ----
414
- w2d = _unpacked_conv_weight(layer) # (dim, width)
415
- dim = w2d.size(0)
416
- w = w2d.unsqueeze(1) # (dim, 1, width) for F.conv1d depthwise
409
+ dim = mixed_qkv_spec.size(-1)
410
bias = layer.conv1d.bias
411
silu = layer.activation == "silu"
412
420
- conv_out = torch.empty_like(mixed_qkv_spec)
421
- col0 = spec_state_indices[:, 0].to("cpu", torch.int64)
422
- for i in range(num_spec_decodes):
423
- q_i = int(seq_lens[i].item())
424
- if q_i == 0:
425
- continue
426
- start = int(seq_starts[i].item())
427
- slot0 = int(col0[i].item())
428
- a_prev = int(num_acc_cpu[i].item())
429
- offset = a_prev - 1
430
- B = conv_buf[slot0] # (dim, state_len)
431
- x_seq = mixed_qkv_spec[start : start + q_i].transpose(0, 1).to(B.dtype)
432
- prior = B[:, offset : offset + (width - 1)]
433
- conv_in = torch.cat([prior, x_seq], dim=-1).unsqueeze(0) # (1, dim, w-1+q)
434
- out = F.conv1d(conv_in, w, bias, groups=dim)[0] # (dim, q_i)
435
- if silu:
436
- out = F.silu(out)
437
- conv_out[start : start + q_i] = out.transpose(0, 1).to(conv_out.dtype)
438
- # Roll the buffer: drop ``a_prev`` from the front, append the new
439
- # draft tokens, keep total length == state_len.
440
- keep = B[:, offset + 1 : offset + 1 + (state_len - q_i)]
441
- new_B = torch.cat([keep, x_seq], dim=-1)
442
- B.copy_(new_B)
413
+ can_use_native_conv = (
414
+ torch.cpu._is_amx_tile_supported()
415
+ and not is_conv_state_dim_first()
416
+ and width == 4
417
+ and num_spec_decodes > 0
418
+ and bool(torch.all(seq_lens == seq_lens[0]).item())
419
+ and int(seq_lens[0].item()) > 0
420
+ )
421
+ if can_use_native_conv:
422
+ q_i = int(seq_lens[0].item())
423
+ conv_out = ops.causal_conv1d_update_cpu(
424
+ x=mixed_qkv_spec.view(num_spec_decodes, q_i, dim),
425
+ conv_states=conv_buf,
426
+ weight=layer.conv1d.weight,
427
+ bias=bias,
428
+ silu_activation=silu,
429
+ conv_state_indices=spec_state_indices[:num_spec_decodes, 0]
430
+ .to("cpu", torch.int32)
431
+ .contiguous(),
432
+ is_vnni=True,
433
+ num_accepted_tokens=num_accepted[:num_spec_decodes].to("cpu", torch.int32),
434
+ ).view_as(mixed_qkv_spec)
435
+ else:
436
+ w = _unpacked_conv_weight(layer).unsqueeze(1)
437
+ col0 = spec_state_indices[:num_spec_decodes, 0]
438
+ num_acc_cpu = num_accepted[:num_spec_decodes]
439
+ conv_out = torch.empty_like(mixed_qkv_spec)
440
+ for i in range(num_spec_decodes):
441
+ q_i = int(seq_lens[i].item())
442
+ if q_i == 0:
443
+ continue
444
+ start = int(seq_starts[i].item())
445
+ slot0 = int(col0[i].item())
446
+ offset = int(num_acc_cpu[i].item()) - 1
447
+ B = conv_buf[slot0] # (dim, state_len)
448
+ x_seq = mixed_qkv_spec[start : start + q_i].transpose(0, 1).to(B.dtype)
449
+ prior = B[:, offset : offset + (width - 1)]
450
+ conv_in = torch.cat([prior, x_seq], dim=-1).unsqueeze(0)
451
+ out = F.conv1d(conv_in, w, bias, groups=dim)[0] # (dim, q_i)
452
+ if silu:
453
+ out = F.silu(out)
454
+ conv_out[start : start + q_i] = out.transpose(0, 1).to(conv_out.dtype)
455
+ # Roll the buffer: drop the accepted history from the front, append
456
+ # the new draft tokens, keep total length == state_len.
457
+ keep = B[:, offset + 1 : offset + 1 + (state_len - q_i)]
458
+ new_B = torch.cat([keep, x_seq], dim=-1)
459
+ B.copy_(new_B)
460
461
# ---- 2. Recurrent (multi-slot SSM state) ----
462
# Single fused kernel call: it runs the recurrence over each sequence's
463
# draft tokens internally, resumes from slot ``num_accepted-1`` and stores
464
# the state after token ``t`` into slot ``t`` (rollback for the next step).
465
query, key, value = layer.rearrange_mixed_qkv(conv_out)
449
- query = query.squeeze(0).contiguous()
450
- key = key.squeeze(0).contiguous()
451
- value = value.squeeze(0).contiguous()
466
+ query = query.squeeze(0)
467
+ key = key.squeeze(0)
468
+ value = value.squeeze(0)
469
spec_idx = spec_state_indices[:num_spec_decodes].to(torch.int32).contiguous()
470
num_acc = num_accepted[:num_spec_decodes].to(torch.int32).contiguous()
471
cu = spec_qsl[: num_spec_decodes + 1].to(torch.int32).contiguous()
@@ -458,8 +475,8 @@ def _spec_forward(
475
q=query,
476
k=key,
477
v=value,
461
- a=a_spec.contiguous(),
462
- b=b_spec.contiguous(),
478
+ a=a_spec,
479
+ b=b_spec,
480
initial_state_source=ssm_state,
481
spec_state_indices=spec_idx,
482
num_accepted_tokens=num_acc,
@@ -469,15 +486,6 @@ def _spec_forward(
486
return out_spec
487
488
472
-def core_attn_out_like(layer, mixed_qkv_spec: torch.Tensor) -> torch.Tensor:
473
- num_tokens = mixed_qkv_spec.size(0)
474
- return torch.zeros(
475
- (num_tokens, layer.num_v_heads // layer.tp_size, layer.head_v_dim),
476
- dtype=mixed_qkv_spec.dtype,
477
- device=mixed_qkv_spec.device,
478
- )
479
-
480
-
489
def _spec_aware_nonspec(
490
layer,
491
attn_metadata_i: GDNAttentionMetadata,
@@ -489,13 +497,19 @@ def _spec_aware_nonspec(
497
ssm_state: torch.Tensor,
498
width: int,
499
) -> None:
492
- """Non-spec prefill/decode with a wide conv buffer (torch path)."""
500
+ """Non-spec prefill/decode with a wide conv buffer."""
501
state_indices_tensor = attn_metadata_i.non_spec_state_indices_tensor
502
query_start_loc = attn_metadata_i.non_spec_query_start_loc
503
assert state_indices_tensor is not None
504
assert query_start_loc is not None
505
+ state_indices_tensor = state_indices_tensor.contiguous()
506
+
507
+ is_amx = torch.cpu._is_amx_tile_supported()
508
+ if is_amx and is_conv_state_dim_first():
509
+ raise RuntimeError("AMX GDN attention requires `SD` conv_state layout.")
510
498
- conv_weights = _unpacked_conv_weight(layer)
511
+ if not is_amx:
512
+ conv_weights = _unpacked_conv_weight(layer)
513
514
num_decodes = attn_metadata_i.num_decodes
515
num_decode_tokens = attn_metadata_i.num_decode_tokens
@@ -507,42 +521,48 @@ def _spec_aware_nonspec(
521
decode_b = b[:num_decode_tokens]
522
decode_a = a[:num_decode_tokens]
523
decode_state_indices = state_indices_tensor[:num_decodes]
510
- # Only the first ``width-1`` columns hold the real 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
+ if is_amx:
525
+ decode_mixed_qkv = ops.causal_conv1d_update_cpu(
526
x=decode_mixed_qkv,
525
- conv_state=conv_buf[:, :, : width - 1],
526
- weight=conv_weights,
527
+ conv_states=conv_buf,
528
+ weight=layer.conv1d.weight,
529
bias=layer.conv1d.bias,
528
- activation=layer.activation,
530
+ silu_activation=layer.activation == "silu",
531
conv_state_indices=decode_state_indices,
532
+ is_vnni=True,
533
)
534
+ else:
535
+ # Only the first ``width-1`` columns hold the real conv state.
536
+ conv_state_view = conv_buf[:, :, : width - 1]
537
+ if current_platform.get_cpu_architecture() == CpuArchEnum.ARM:
538
+ decode_conv_state = conv_state_view[decode_state_indices].contiguous()
539
+ decode_mixed_qkv = causal_conv1d_update_torch(
540
+ x=decode_mixed_qkv.unsqueeze(-1),
541
+ conv_state=decode_conv_state,
542
+ weight=conv_weights,
543
+ bias=layer.conv1d.bias,
544
+ activation=layer.activation,
545
+ ).squeeze(-1)
546
+ conv_state_view[decode_state_indices] = decode_conv_state
547
+ else:
548
+ decode_mixed_qkv = causal_conv1d_update_cpu(
549
+ x=decode_mixed_qkv,
550
+ conv_state=conv_state_view,
551
+ weight=conv_weights,
552
+ bias=layer.conv1d.bias,
553
+ activation=layer.activation,
554
+ conv_state_indices=decode_state_indices,
555
+ )
556
557
query, key, value = layer.rearrange_mixed_qkv(decode_mixed_qkv)
533
- # rearrange_mixed_qkv can return views whose last dim is not
534
- # contiguous; the fused CPU kernel requires a contiguous last dim.
535
- query = query.contiguous()
536
- key = key.contiguous()
537
- value = value.contiguous()
558
attn_out = ops.fused_sigmoid_gating_delta_rule_update_cpu(
559
A_log=layer.A_log,
560
dt_bias=layer.dt_bias,
561
q=query,
562
k=key,
563
v=value,
544
- a=decode_a.contiguous(),
545
- b=decode_b.contiguous(),
564
+ a=decode_a,
565
+ b=decode_b,
566
initial_state_source=ssm_state,
567
initial_state_indices=decode_state_indices,
568
cu_seqlens=query_start_loc[: num_decodes + 1],
@@ -568,17 +588,29 @@ def _spec_aware_nonspec(
588
prefill_has_initial_state = has_initial_state[
589
num_decodes : num_decodes + num_prefills
590
]
571
- # ``causal_conv1d_torch`` only touches columns [:width-1] of the buffer.
572
- prefill_mixed_qkv = causal_conv1d_torch(
573
- x=prefill_mixed_qkv.transpose(0, 1),
574
- weight=conv_weights,
575
- bias=layer.conv1d.bias,
576
- conv_states=conv_buf,
577
- query_start_loc=prefill_query_start_loc,
578
- cache_indices=prefill_state_indices,
579
- has_initial_state=prefill_has_initial_state,
580
- activation=layer.activation,
581
- ).transpose(0, 1)
591
+ if is_amx:
592
+ prefill_mixed_qkv = ops.causal_conv1d_fwd_cpu(
593
+ x=prefill_mixed_qkv.transpose(0, 1),
594
+ weight=layer.conv1d.weight,
595
+ bias=layer.conv1d.bias,
596
+ conv_states=conv_buf,
597
+ query_start_loc=prefill_query_start_loc,
598
+ cache_indices=prefill_state_indices,
599
+ has_initial_state=prefill_has_initial_state,
600
+ silu_activation=layer.activation == "silu",
601
+ is_vnni=True,
602
+ ).transpose(0, 1)
603
+ else:
604
+ prefill_mixed_qkv = causal_conv1d_torch(
605
+ x=prefill_mixed_qkv.transpose(0, 1),
606
+ weight=conv_weights,
607
+ bias=layer.conv1d.bias,
608
+ conv_states=conv_buf,
609
+ query_start_loc=prefill_query_start_loc,
610
+ cache_indices=prefill_state_indices,
611
+ has_initial_state=prefill_has_initial_state,
612
+ activation=layer.activation,
613
+ ).transpose(0, 1)
614
615
query, key, value = layer.rearrange_mixed_qkv(prefill_mixed_qkv)
616
g, beta = ops.fused_gdn_gating_cpu(
@@ -618,24 +650,41 @@ def _spec_aware_nonspec_subset(
650
651
Returns outputs ordered like ``non_spec_token_indx``.
652
"""
621
- out = core_attn_out_like(layer, mixed_qkv)
653
has_initial_state = attn_metadata_i.has_initial_state
654
prefill_state_indices = attn_metadata_i.prefill_state_indices
655
prefill_qsl = attn_metadata_i.prefill_query_start_loc
656
assert prefill_state_indices is not None and prefill_qsl is not None
657
assert has_initial_state is not None
658
+ prefill_state_indices = prefill_state_indices.contiguous()
659
+
660
+ is_amx = torch.cpu._is_amx_tile_supported()
661
+ if is_amx and is_conv_state_dim_first():
662
+ raise RuntimeError("AMX GDN attention requires `SD` conv_state layout.")
663
628
- conv_weights = _unpacked_conv_weight(layer)
629
- conv_out = causal_conv1d_torch(
630
- x=mixed_qkv.transpose(0, 1),
631
- weight=conv_weights,
632
- bias=layer.conv1d.bias,
633
- conv_states=conv_buf,
634
- query_start_loc=prefill_qsl,
635
- cache_indices=prefill_state_indices,
636
- has_initial_state=has_initial_state,
637
- activation=layer.activation,
638
- ).transpose(0, 1)
664
+ if is_amx:
665
+ conv_out = ops.causal_conv1d_fwd_cpu(
666
+ x=mixed_qkv.transpose(0, 1),
667
+ weight=layer.conv1d.weight,
668
+ bias=layer.conv1d.bias,
669
+ conv_states=conv_buf,
670
+ query_start_loc=prefill_qsl,
671
+ cache_indices=prefill_state_indices,
672
+ has_initial_state=has_initial_state,
673
+ silu_activation=layer.activation == "silu",
674
+ is_vnni=True,
675
+ ).transpose(0, 1)
676
+ else:
677
+ conv_weights = _unpacked_conv_weight(layer)
678
+ conv_out = causal_conv1d_torch(
679
+ x=mixed_qkv.transpose(0, 1),
680
+ weight=conv_weights,
681
+ bias=layer.conv1d.bias,
682
+ conv_states=conv_buf,
683
+ query_start_loc=prefill_qsl,
684
+ cache_indices=prefill_state_indices,
685
+ has_initial_state=has_initial_state,
686
+ activation=layer.activation,
687
+ ).transpose(0, 1)
688
689
query, key, value = layer.rearrange_mixed_qkv(conv_out)
690
g, beta = ops.fused_gdn_gating_cpu(
@@ -658,8 +707,7 @@ def _spec_aware_nonspec_subset(
707
ssm_state[prefill_state_indices] = last_recurrent_state.to(
708
ssm_state.dtype, copy=False
709
)
661
- out[:] = attn_out.squeeze(0)
662
- return out
710
+ return attn_out.squeeze(0)
711
712
713
def cpu_gdn_attention_core_fake(