A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 11, 20:00 UTC
Add SpecDec support to `selective_state_update` (#29488)
Signed-off-by: Roi Koren <roik@nvidia.com>
roikoren755 committed
Dec 8, 2025 at 23:45 UTC
ae0f69b16aaa54197257267a8262555beb7d5ae3
2 files changed
+507
-74
tests/kernels/mamba/test_mamba_ssm.py
+325
@@ -425,6 +425,80 @@ def test_selective_state_update(dim, dstate, has_z, itype):
425
assert torch.allclose(out, out_ref, rtol=rtol, atol=atol)
426
427
428
+@pytest.mark.parametrize("itype", [torch.float32, torch.bfloat16])
429
+@pytest.mark.parametrize("has_z", [False, True])
430
+@pytest.mark.parametrize("dstate", [16, 64])
431
+@pytest.mark.parametrize("dim", [2048, 2048 + 16, 4096])
432
+@pytest.mark.parametrize("max_seq_len", [1, 2, 4])
433
+def test_selective_state_update_varlen(dim, dstate, has_z, itype, max_seq_len):
434
+ device = "cuda"
435
+ rtol, atol = (3e-4, 1e-3) if itype == torch.float32 else (5e-3, 1e-2)
436
+ if itype == torch.bfloat16:
437
+ rtol, atol = 5e-2, 1.5e-1
438
+ if torch.version.hip:
439
+ atol *= 2
440
+ # set seed
441
+ current_platform.seed_everything(0)
442
+ batch_size = 4
443
+ token_counts = torch.randint(1, max_seq_len + 1, (batch_size,), device=device)
444
+ total_tokens = int(token_counts.sum().item())
445
+ cu_seqlens = torch.tensor(
446
+ [0] + torch.cumsum(token_counts, dim=0).tolist(),
447
+ dtype=torch.int32,
448
+ device=device,
449
+ )
450
+ state = torch.randn(batch_size, dim, dstate, dtype=itype, device=device)
451
+ x = torch.randn(total_tokens, dim, device=device, dtype=itype)
452
+ out = torch.empty_like(x)
453
+ dt = torch.randn(total_tokens, dim, device=device, dtype=itype)
454
+ dt_bias = torch.rand(dim, device=device) - 4.0
455
+ A = -torch.rand(dim, dstate, device=device) - 1.0
456
+ B = torch.randn(total_tokens, dstate, device=device)
457
+ C = torch.randn(total_tokens, dstate, device=device)
458
+ D = torch.randn(dim, device=device)
459
+ z = torch.randn_like(x) if has_z else None
460
+ state_ref = state.detach().clone()
461
+ selective_state_update(
462
+ state,
463
+ x,
464
+ dt,
465
+ A,
466
+ B,
467
+ C,
468
+ D=D,
469
+ z=z,
470
+ dt_bias=dt_bias,
471
+ dt_softplus=True,
472
+ out=out,
473
+ cu_seqlens=cu_seqlens,
474
+ )
475
+
476
+ out_ref_list = []
477
+ for seq_idx in range(batch_size):
478
+ start_idx = cu_seqlens[seq_idx].item()
479
+ end_idx = cu_seqlens[seq_idx + 1].item()
480
+ num_tokens = end_idx - start_idx
481
+ for token_idx in range(num_tokens):
482
+ idx = start_idx + token_idx
483
+ out_ref_list.append(
484
+ selective_state_update_ref(
485
+ state_ref[seq_idx : seq_idx + 1],
486
+ x[idx : idx + 1],
487
+ dt[idx : idx + 1],
488
+ A,
489
+ B[idx : idx + 1],
490
+ C[idx : idx + 1],
491
+ D=D,
492
+ z=z[idx : idx + 1] if has_z else None,
493
+ dt_bias=dt_bias,
494
+ dt_softplus=True,
495
+ )
496
+ )
497
+ out_ref = torch.cat(out_ref_list, dim=0)
498
+ assert torch.allclose(state, state_ref, rtol=rtol, atol=atol)
499
+ assert torch.allclose(out, out_ref, rtol=rtol, atol=atol)
500
+
501
+
502
@pytest.mark.parametrize("wtype", [torch.float32])
503
@pytest.mark.parametrize("itype", [torch.float32])
504
@pytest.mark.parametrize("seqlen", [1, 256, 1024, 4096])
@@ -766,3 +840,254 @@ def test_selective_state_update_with_heads_with_batch_indices(
840
print(f"Output mean diff: {(out - out_ref).abs().mean().item()}")
841
assert torch.allclose(state[state_indices, :], state_ref, rtol=rtol, atol=atol)
842
assert torch.allclose(out, out_ref, rtol=rtol, atol=atol)
843
+
844
+
845
+@pytest.mark.parametrize("itype", [torch.float32, torch.bfloat16])
846
+@pytest.mark.parametrize("has_z", [False, True])
847
+@pytest.mark.parametrize("dstate", [16, 64])
848
+@pytest.mark.parametrize("dim", [2048, 4096])
849
+@pytest.mark.parametrize("max_seq_len", [2, 4])
850
+def test_selective_state_update_with_num_accepted_tokens(
851
+ dim, dstate, has_z, itype, max_seq_len
852
+):
853
+ device = "cuda"
854
+ rtol, atol = (3e-4, 1e-3) if itype == torch.float32 else (5e-3, 1e-2)
855
+ if itype == torch.bfloat16:
856
+ rtol, atol = 5e-2, 1.5e-1
857
+ if torch.version.hip:
858
+ atol *= 2
859
+
860
+ current_platform.seed_everything(0)
861
+ batch_size = 4
862
+
863
+ tokens_per_seq = torch.randint(1, max_seq_len + 1, (batch_size,), device=device)
864
+ total_tokens = int(tokens_per_seq.sum().item())
865
+
866
+ num_accepted_tokens = torch.randint(0, max_seq_len, (batch_size,), device=device)
867
+ num_accepted_tokens[0] = 0 # Add edge-case of no accepted tokens
868
+ num_accepted_tokens[1] = max_seq_len # Add edge-case of all tokens accepted
869
+
870
+ cu_seqlens = torch.tensor(
871
+ [0] + torch.cumsum(tokens_per_seq, dim=0).tolist(),
872
+ dtype=torch.int32,
873
+ device=device,
874
+ )
875
+
876
+ total_state_slots = 50
877
+ state = torch.randn(total_state_slots, dim, dstate, dtype=itype, device=device)
878
+
879
+ state_batch_indices = torch.full(
880
+ (batch_size, max_seq_len), PAD_SLOT_ID, dtype=torch.int32, device=device
881
+ )
882
+ initial_state_slots = torch.randint(
883
+ 0, 15, (batch_size,), device=device, dtype=torch.int32
884
+ )
885
+ for seq_idx in range(batch_size):
886
+ token_pos = max(num_accepted_tokens[seq_idx].item() - 1, 0)
887
+ state_batch_indices[seq_idx, token_pos] = initial_state_slots[seq_idx]
888
+
889
+ dst_state_batch_indices = torch.full(
890
+ (batch_size, max_seq_len), PAD_SLOT_ID, dtype=torch.int32, device=device
891
+ )
892
+ slot_offset = 15
893
+ dst_slots_map = {}
894
+ for seq_idx in range(batch_size):
895
+ for token_idx in range(tokens_per_seq[seq_idx].item()):
896
+ dst_state_batch_indices[seq_idx, token_idx] = slot_offset
897
+ dst_slots_map[(seq_idx, token_idx)] = slot_offset
898
+ slot_offset += 1
899
+
900
+ x = torch.randn(total_tokens, dim, device=device, dtype=itype)
901
+ out = torch.empty_like(x)
902
+ dt = torch.randn(total_tokens, dim, device=device, dtype=itype)
903
+ dt_bias = torch.rand(dim, device=device) - 4.0
904
+ A = -torch.rand(dim, dstate, device=device) - 1.0
905
+ B = torch.randn(total_tokens, dstate, device=device)
906
+ C = torch.randn(total_tokens, dstate, device=device)
907
+ D = torch.randn(dim, device=device)
908
+ z = torch.randn_like(x) if has_z else None
909
+
910
+ state_ref_intermediate = {}
911
+ out_ref_list = []
912
+
913
+ for seq_idx in range(batch_size):
914
+ seq_start = cu_seqlens[seq_idx].item()
915
+ seq_end = cu_seqlens[seq_idx + 1].item()
916
+ num_tokens = seq_end - seq_start
917
+
918
+ token_pos = max(num_accepted_tokens[seq_idx].item() - 1, 0)
919
+ initial_slot = state_batch_indices[seq_idx, token_pos].item()
920
+ state_seq = state[initial_slot : initial_slot + 1].clone()
921
+
922
+ for token_idx in range(num_tokens):
923
+ global_idx = seq_start + token_idx
924
+
925
+ out_token = selective_state_update_ref(
926
+ state_seq,
927
+ x[global_idx : global_idx + 1],
928
+ dt[global_idx : global_idx + 1],
929
+ A,
930
+ B[global_idx : global_idx + 1],
931
+ C[global_idx : global_idx + 1],
932
+ D=D,
933
+ z=z[global_idx : global_idx + 1] if has_z else None,
934
+ dt_bias=dt_bias,
935
+ dt_softplus=True,
936
+ )
937
+ out_ref_list.append(out_token)
938
+ state_ref_intermediate[(seq_idx, token_idx)] = state_seq.clone()
939
+
940
+ out_ref = torch.cat(out_ref_list, dim=0)
941
+
942
+ selective_state_update(
943
+ state,
944
+ x,
945
+ dt,
946
+ A,
947
+ B,
948
+ C,
949
+ D=D,
950
+ z=z,
951
+ dt_bias=dt_bias,
952
+ dt_softplus=True,
953
+ out=out,
954
+ cu_seqlens=cu_seqlens,
955
+ state_batch_indices=state_batch_indices,
956
+ dst_state_batch_indices=dst_state_batch_indices,
957
+ num_accepted_tokens=num_accepted_tokens,
958
+ pad_slot_id=PAD_SLOT_ID,
959
+ )
960
+
961
+ assert torch.allclose(out, out_ref, rtol=rtol, atol=atol)
962
+
963
+ for seq_idx in range(batch_size):
964
+ num_tokens = tokens_per_seq[seq_idx].item()
965
+ for token_idx in range(num_tokens):
966
+ dst_slot = dst_slots_map[(seq_idx, token_idx)]
967
+ state_ref = state_ref_intermediate[(seq_idx, token_idx)].squeeze(0)
968
+ assert torch.allclose(state[dst_slot], state_ref, rtol=rtol, atol=atol)
969
+
970
+
971
+@pytest.mark.parametrize("itype", [torch.float32, torch.bfloat16])
972
+@pytest.mark.parametrize("has_z", [False, True])
973
+@pytest.mark.parametrize("dstate", [16, 64])
974
+@pytest.mark.parametrize("dim", [2048, 4096])
975
+@pytest.mark.parametrize("max_seq_len", [2, 4])
976
+def test_selective_state_update_varlen_with_num_accepted(
977
+ dim, dstate, has_z, itype, max_seq_len
978
+):
979
+ device = "cuda"
980
+ rtol, atol = (3e-4, 1e-3) if itype == torch.float32 else (5e-3, 1e-2)
981
+ if itype == torch.bfloat16:
982
+ rtol, atol = 5e-2, 1.5e-1
983
+ if torch.version.hip:
984
+ atol *= 2
985
+
986
+ current_platform.seed_everything(0)
987
+ batch_size = 4
988
+
989
+ tokens_per_seq = torch.randint(1, max_seq_len + 1, (batch_size,), device=device)
990
+ total_tokens = int(tokens_per_seq.sum().item())
991
+
992
+ num_accepted_tokens = torch.randint(0, max_seq_len, (batch_size,), device=device)
993
+ num_accepted_tokens[0] = 0 # Add edge-case of no accepted tokens
994
+ num_accepted_tokens[1] = max_seq_len # Add edge-case of all tokens accepted
995
+
996
+ cu_seqlens = torch.tensor(
997
+ [0] + torch.cumsum(tokens_per_seq, dim=0).tolist(),
998
+ dtype=torch.int32,
999
+ device=device,
1000
+ )
1001
+
1002
+ total_state_slots = 50
1003
+ state = torch.randn(total_state_slots, dim, dstate, dtype=itype, device=device)
1004
+
1005
+ state_batch_indices = torch.full(
1006
+ (batch_size, max_seq_len), PAD_SLOT_ID, dtype=torch.int32, device=device
1007
+ )
1008
+
1009
+ initial_state_slots = torch.randint(
1010
+ 0, 15, (batch_size,), device=device, dtype=torch.int32
1011
+ )
1012
+ for seq_idx in range(batch_size):
1013
+ token_pos = max(num_accepted_tokens[seq_idx].item() - 1, 0)
1014
+ state_batch_indices[seq_idx, token_pos] = initial_state_slots[seq_idx]
1015
+
1016
+ dst_state_batch_indices = torch.full(
1017
+ (batch_size, max_seq_len), PAD_SLOT_ID, dtype=torch.int32, device=device
1018
+ )
1019
+
1020
+ slot_offset = 15
1021
+ dst_slots_map = {}
1022
+ for seq_idx in range(batch_size):
1023
+ for token_idx in range(tokens_per_seq[seq_idx].item()):
1024
+ dst_state_batch_indices[seq_idx, token_idx] = slot_offset
1025
+ dst_slots_map[(seq_idx, token_idx)] = slot_offset
1026
+ slot_offset += 1
1027
+
1028
+ x = torch.randn(total_tokens, dim, device=device, dtype=itype)
1029
+ out = torch.empty_like(x)
1030
+ dt = torch.randn(total_tokens, dim, device=device, dtype=itype)
1031
+ dt_bias = torch.rand(dim, device=device) - 4.0
1032
+ A = -torch.rand(dim, dstate, device=device) - 1.0
1033
+ B = torch.randn(total_tokens, dstate, device=device)
1034
+ C = torch.randn(total_tokens, dstate, device=device)
1035
+ D = torch.randn(dim, device=device)
1036
+ z = torch.randn_like(x) if has_z else None
1037
+
1038
+ state_ref_intermediate = {}
1039
+
1040
+ for seq_idx in range(batch_size):
1041
+ seq_start = cu_seqlens[seq_idx].item()
1042
+ seq_end = cu_seqlens[seq_idx + 1].item()
1043
+ num_tokens = seq_end - seq_start
1044
+
1045
+ token_pos = max(num_accepted_tokens[seq_idx].item() - 1, 0)
1046
+ initial_slot = state_batch_indices[seq_idx, token_pos].item()
1047
+ state_seq = state[initial_slot : initial_slot + 1].clone()
1048
+
1049
+ for token_idx in range(num_tokens):
1050
+ global_idx = seq_start + token_idx
1051
+
1052
+ selective_state_update_ref(
1053
+ state_seq,
1054
+ x[global_idx : global_idx + 1],
1055
+ dt[global_idx : global_idx + 1],
1056
+ A,
1057
+ B[global_idx : global_idx + 1],
1058
+ C[global_idx : global_idx + 1],
1059
+ D=D,
1060
+ z=z[global_idx : global_idx + 1] if has_z else None,
1061
+ dt_bias=dt_bias,
1062
+ dt_softplus=True,
1063
+ )
1064
+
1065
+ state_ref_intermediate[(seq_idx, token_idx)] = state_seq.clone()
1066
+
1067
+ selective_state_update(
1068
+ state,
1069
+ x,
1070
+ dt,
1071
+ A,
1072
+ B,
1073
+ C,
1074
+ D=D,
1075
+ z=z,
1076
+ dt_bias=dt_bias,
1077
+ dt_softplus=True,
1078
+ out=out,
1079
+ cu_seqlens=cu_seqlens,
1080
+ state_batch_indices=state_batch_indices,
1081
+ dst_state_batch_indices=dst_state_batch_indices,
1082
+ num_accepted_tokens=num_accepted_tokens,
1083
+ pad_slot_id=PAD_SLOT_ID,
1084
+ )
1085
+
1086
+ for seq_idx in range(batch_size):
1087
+ num_tokens = tokens_per_seq[seq_idx].item()
1088
+
1089
+ for token_idx in range(num_tokens):
1090
+ dst_slot = dst_slots_map[(seq_idx, token_idx)]
1091
+ state_ref = state_ref_intermediate[(seq_idx, token_idx)].squeeze(0)
1092
+
1093
+ assert torch.allclose(state[dst_slot], state_ref, rtol=rtol, atol=atol)
vllm/model_executor/layers/mamba/ops/mamba_ssm.py
+182
-74
@@ -36,10 +36,14 @@ else:
36
is not None
37
}
38
)
39
+@triton.heuristics(
40
+ {"IS_SPEC_DECODING": lambda args: args["num_accepted_tokens_ptr"] is not None}
41
+)
42
+@triton.heuristics({"IS_VARLEN": lambda args: args["cu_seqlens_ptr"] is not None})
43
@triton.heuristics(
44
{"BLOCK_SIZE_DSTATE": lambda args: triton.next_power_of_2(args["dstate"])}
45
)
42
-@triton.jit
46
+@triton.jit(do_not_specialize=["N"])
47
def _selective_scan_update_kernel(
48
# Pointers to matrices
49
state_ptr,
@@ -55,8 +59,10 @@ def _selective_scan_update_kernel(
59
state_batch_indices_ptr,
60
dst_state_batch_indices_ptr,
61
pad_slot_id,
62
+ num_accepted_tokens_ptr,
63
+ cu_seqlens_ptr,
64
# Matrix dimensions
59
- batch,
65
+ N,
66
nheads,
67
dim,
68
dstate,
@@ -91,6 +97,10 @@ def _selective_scan_update_kernel(
97
stride_out_batch,
98
stride_out_head,
99
stride_out_dim,
100
+ stride_state_indices_batch,
101
+ stride_state_indices_T,
102
+ stride_dst_state_indices_batch,
103
+ stride_dst_state_indices_T,
104
# Meta-parameters
105
DT_SOFTPLUS: tl.constexpr,
106
TIE_HDIM: tl.constexpr,
@@ -99,22 +109,50 @@ def _selective_scan_update_kernel(
109
HAS_D: tl.constexpr,
110
HAS_Z: tl.constexpr,
111
HAS_STATE_BATCH_INDICES: tl.constexpr,
112
+ IS_SPEC_DECODING: tl.constexpr,
113
+ IS_VARLEN: tl.constexpr,
114
BLOCK_SIZE_DSTATE: tl.constexpr,
115
):
116
pid_m = tl.program_id(axis=0)
117
pid_b = tl.program_id(axis=1)
118
pid_h = tl.program_id(axis=2)
119
120
+ if IS_VARLEN:
121
+ bos = tl.load(cu_seqlens_ptr + pid_b).to(tl.int64)
122
+ eos = tl.load(cu_seqlens_ptr + pid_b + 1).to(tl.int64)
123
+ seq_len = eos - bos
124
+
125
+ if seq_len == 0:
126
+ return
127
+ else:
128
+ bos = pid_b
129
+ seq_len = 1
130
+
131
+ state_ptr_base = state_ptr
132
+
133
# If HAS_STATE_BATCH_INDICES is true, then the ssm state's batch coordinate
134
# is taken from the state_batch_indices_ptr Otherwise, the state coordinate
135
# is the same as the batch id.
136
if HAS_STATE_BATCH_INDICES:
112
- dst_state_batch_indices_ptr += pid_b
113
- dst_state_batch_idx = tl.load(dst_state_batch_indices_ptr).to(tl.int64)
114
- dst_state_ptr = state_ptr + (
115
- dst_state_batch_idx * stride_state_batch + pid_h * stride_state_head
137
+ if IS_SPEC_DECODING:
138
+ num_accepted = tl.load(num_accepted_tokens_ptr + pid_b).to(tl.int64)
139
+ init_token_idx = tl.maximum(num_accepted - 1, 0)
140
+ else:
141
+ init_token_idx = 0
142
+
143
+ dst_state_batch_indices_ptr += pid_b * stride_dst_state_indices_batch
144
+ if not IS_SPEC_DECODING:
145
+ dst_state_batch_idx = tl.load(
146
+ dst_state_batch_indices_ptr
147
+ + init_token_idx * stride_dst_state_indices_T
148
+ ).to(tl.int64)
149
+ dst_state_ptr = state_ptr + (
150
+ dst_state_batch_idx * stride_state_batch + pid_h * stride_state_head
151
+ )
152
+
153
+ state_batch_indices_ptr += (
154
+ pid_b * stride_state_indices_batch + init_token_idx * stride_state_indices_T
155
)
117
- state_batch_indices_ptr += pid_b
156
state_batch_idx = tl.load(state_batch_indices_ptr).to(tl.int64)
157
state_ptr += state_batch_idx * stride_state_batch + pid_h * stride_state_head
158
else:
@@ -123,86 +161,112 @@ def _selective_scan_update_kernel(
161
)
162
state_ptr += pid_b * stride_state_batch + pid_h * stride_state_head
163
126
- x_ptr += pid_b * stride_x_batch + pid_h * stride_x_head
127
- dt_ptr += pid_b * stride_dt_batch + pid_h * stride_dt_head
164
+ x_ptr += bos * stride_x_batch + pid_h * stride_x_head
165
+ dt_ptr += bos * stride_dt_batch + pid_h * stride_dt_head
166
if HAS_DT_BIAS:
167
dt_bias_ptr += pid_h * stride_dt_bias_head
168
A_ptr += pid_h * stride_A_head
131
- B_ptr += pid_b * stride_B_batch + (pid_h // nheads_ngroups_ratio) * stride_B_group
132
- C_ptr += pid_b * stride_C_batch + (pid_h // nheads_ngroups_ratio) * stride_C_group
169
+ B_ptr += bos * stride_B_batch + (pid_h // nheads_ngroups_ratio) * stride_B_group
170
+ C_ptr += bos * stride_C_batch + (pid_h // nheads_ngroups_ratio) * stride_C_group
171
if HAS_Z:
134
- z_ptr += pid_b * stride_z_batch + pid_h * stride_z_head
135
- out_ptr += pid_b * stride_out_batch + pid_h * stride_out_head
172
+ z_ptr += bos * stride_z_batch + pid_h * stride_z_head
173
+ out_ptr += bos * stride_out_batch + pid_h * stride_out_head
174
175
offs_m = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M)
176
offs_n = tl.arange(0, BLOCK_SIZE_DSTATE)
177
state_ptrs = state_ptr + (
178
offs_m[:, None] * stride_state_dim + offs_n[None, :] * stride_state_dstate
179
)
142
- dst_state_ptrs = dst_state_ptr + (
143
- offs_m[:, None] * stride_state_dim + offs_n[None, :] * stride_state_dstate
144
- )
145
- x_ptrs = x_ptr + offs_m * stride_x_dim
146
- dt_ptrs = dt_ptr + offs_m * stride_dt_dim
180
+ if not IS_SPEC_DECODING:
181
+ dst_state_ptrs = dst_state_ptr + (
182
+ offs_m[:, None] * stride_state_dim + offs_n[None, :] * stride_state_dstate
183
+ )
184
+
185
+ mask = (offs_m[:, None] < dim) & (offs_n[None, :] < dstate)
186
+ if HAS_STATE_BATCH_INDICES:
187
+ mask &= state_batch_idx != pad_slot_id
188
+ state = tl.load(state_ptrs, mask=mask, other=0.0).to(tl.float32)
189
+
190
if HAS_DT_BIAS:
191
dt_bias_ptrs = dt_bias_ptr + offs_m * stride_dt_bias_dim
192
if HAS_D:
193
D_ptr += pid_h * stride_D_head
151
- A_ptrs = A_ptr + (
152
- offs_m[:, None] * stride_A_dim + offs_n[None, :] * stride_A_dstate
153
- )
154
- B_ptrs = B_ptr + offs_n * stride_B_dstate
155
- C_ptrs = C_ptr + offs_n * stride_C_dstate
156
- if HAS_D:
194
D_ptrs = D_ptr + offs_m * stride_D_dim
158
- if HAS_Z:
159
- z_ptrs = z_ptr + offs_m * stride_z_dim
160
- out_ptrs = out_ptr + offs_m * stride_out_dim
161
- mask = (offs_m[:, None] < dim) & (offs_n[None, :] < dstate)
162
- if HAS_STATE_BATCH_INDICES:
163
- mask &= state_batch_idx != pad_slot_id
164
- state = tl.load(state_ptrs, mask=mask, other=0.0)
165
-
166
- x = tl.load(x_ptrs, mask=offs_m < dim, other=0.0).to(tl.float32)
167
- if not TIE_HDIM:
168
- dt = tl.load(dt_ptrs, mask=offs_m < dim, other=0.0).to(tl.float32)
169
- if HAS_DT_BIAS:
170
- dt += tl.load(dt_bias_ptrs, mask=offs_m < dim, other=0.0).to(tl.float32)
171
- if DT_SOFTPLUS:
172
- dt = softplus(dt)
173
- A = tl.load(
174
- A_ptrs, mask=(offs_m[:, None] < dim) & (offs_n[None, :] < dstate), other=0.0
175
- ).to(tl.float32)
176
- dA = tl.exp(A * dt[:, None])
177
- else:
178
- dt = tl.load(dt_ptr).to(tl.float32)
179
- if HAS_DT_BIAS:
180
- dt += tl.load(dt_bias_ptr).to(tl.float32)
181
- if DT_SOFTPLUS:
182
- dt = softplus(dt)
183
- A = tl.load(A_ptr).to(tl.float32)
184
- dA = tl.exp(A * dt) # scalar, not a matrix
185
-
186
- B = tl.load(B_ptrs, mask=offs_n < dstate, other=0.0).to(tl.float32)
187
- C = tl.load(C_ptrs, mask=offs_n < dstate, other=0.0).to(tl.float32)
188
- if HAS_D:
189
- D = tl.load(D_ptrs, mask=offs_m < dim, other=0.0).to(tl.float32)
190
- if HAS_Z:
191
- z = tl.load(z_ptrs, mask=offs_m < dim, other=0.0).to(tl.float32)
195
+ A_ptrs = A_ptr + offs_m[:, None] * stride_A_dim + offs_n[None, :] * stride_A_dstate
196
193
- dB = B[None, :] * dt[:, None] if not TIE_HDIM else B * dt
194
- state = state * dA + dB * x[:, None]
197
+ for i_t in range(seq_len):
198
+ x_ptrs = x_ptr + offs_m * stride_x_dim
199
+ dt_ptrs = dt_ptr + offs_m * stride_dt_dim
200
+ B_ptrs = B_ptr + offs_n * stride_B_dstate
201
+ C_ptrs = C_ptr + offs_n * stride_C_dstate
202
+ if HAS_Z:
203
+ z_ptrs = z_ptr + offs_m * stride_z_dim
204
+ out_ptrs = out_ptr + offs_m * stride_out_dim
205
196
- mask = (offs_m[:, None] < dim) & (offs_n[None, :] < dstate)
197
- if HAS_STATE_BATCH_INDICES:
198
- mask &= state_batch_idx != pad_slot_id
199
- tl.store(dst_state_ptrs, state, mask=mask)
200
- out = tl.sum(state * C[None, :], axis=1)
201
- if HAS_D:
202
- out += x * D
203
- if HAS_Z:
204
- out *= z * tl.sigmoid(z)
205
- tl.store(out_ptrs, out, mask=offs_m < dim)
206
+ x = tl.load(x_ptrs, mask=offs_m < dim, other=0.0).to(tl.float32)
207
+ if not TIE_HDIM:
208
+ dt = tl.load(dt_ptrs, mask=offs_m < dim, other=0.0).to(tl.float32)
209
+ if HAS_DT_BIAS:
210
+ dt += tl.load(dt_bias_ptrs, mask=offs_m < dim, other=0.0).to(tl.float32)
211
+ if DT_SOFTPLUS:
212
+ dt = softplus(dt)
213
+ A = tl.load(
214
+ A_ptrs,
215
+ mask=(offs_m[:, None] < dim) & (offs_n[None, :] < dstate),
216
+ other=0.0,
217
+ ).to(tl.float32)
218
+ dA = tl.exp(A * dt[:, None])
219
+ else:
220
+ dt = tl.load(dt_ptr).to(tl.float32)
221
+ if HAS_DT_BIAS:
222
+ dt += tl.load(dt_bias_ptr).to(tl.float32)
223
+ if DT_SOFTPLUS:
224
+ dt = softplus(dt)
225
+ A = tl.load(A_ptr).to(tl.float32)
226
+ dA = tl.exp(A * dt) # scalar, not a matrix
227
+
228
+ B = tl.load(B_ptrs, mask=offs_n < dstate, other=0.0).to(tl.float32)
229
+ C = tl.load(C_ptrs, mask=offs_n < dstate, other=0.0).to(tl.float32)
230
+ if HAS_D:
231
+ D = tl.load(D_ptrs, mask=offs_m < dim, other=0.0).to(tl.float32)
232
+ if HAS_Z:
233
+ z = tl.load(z_ptrs, mask=offs_m < dim, other=0.0).to(tl.float32)
234
+
235
+ dB = B[None, :] * dt[:, None] if not TIE_HDIM else B * dt
236
+ state = state * dA + dB * x[:, None]
237
+
238
+ if IS_SPEC_DECODING:
239
+ dst_idx_ptr = dst_state_batch_indices_ptr + i_t * stride_dst_state_indices_T
240
+ token_dst_idx = tl.load(dst_idx_ptr).to(tl.int64)
241
+ if token_dst_idx != pad_slot_id:
242
+ token_dst_ptrs = (
243
+ state_ptr_base
244
+ + token_dst_idx * stride_state_batch
245
+ + pid_h * stride_state_head
246
+ + offs_m[:, None] * stride_state_dim
247
+ + offs_n[None, :] * stride_state_dstate
248
+ )
249
+ tl.store(
250
+ token_dst_ptrs, state.to(token_dst_ptrs.dtype.element_ty), mask=mask
251
+ )
252
+
253
+ out = tl.sum(state * C[None, :], axis=1)
254
+ if HAS_D:
255
+ out += x * D
256
+ if HAS_Z:
257
+ out *= z * tl.sigmoid(z)
258
+ tl.store(out_ptrs, out, mask=offs_m < dim)
259
+
260
+ x_ptr += stride_x_batch
261
+ dt_ptr += stride_dt_batch
262
+ B_ptr += stride_B_batch
263
+ C_ptr += stride_C_batch
264
+ out_ptr += stride_out_batch
265
+ if HAS_Z:
266
+ z_ptr += stride_z_batch
267
+
268
+ if not IS_SPEC_DECODING:
269
+ tl.store(dst_state_ptrs, state.to(dst_state_ptrs.dtype.element_ty), mask=mask)
270
271
272
def selective_state_update(
@@ -220,6 +284,8 @@ def selective_state_update(
284
dst_state_batch_indices=None,
285
pad_slot_id=PAD_SLOT_ID,
286
out=None,
287
+ num_accepted_tokens=None,
288
+ cu_seqlens=None,
289
):
290
"""
291
Argument:
@@ -240,6 +306,11 @@ def selective_state_update(
306
indices 0 and 3
307
out: Preallocated ssm output tensor. Assume same shape as x.
308
In-place updated.
309
+ num_accepted_tokens: (batch,)
310
+ number of accepted tokens from previous verification step,
311
+ tells the kernel which initial state to use
312
+ cu_seqlens: (batch,)
313
+ length per sequence, for variable length in speculative decoding cases
314
"""
315
if state.dim() == 3:
316
state = state.unsqueeze(1)
@@ -261,9 +332,26 @@ def selective_state_update(
332
dt_bias = dt_bias.unsqueeze(0)
333
if out.dim() == 2:
334
out = out.unsqueeze(1)
335
+ if num_accepted_tokens is not None:
336
+ assert state_batch_indices is not None and state_batch_indices.dim() == 2
337
+ assert dst_state_batch_indices is None or dst_state_batch_indices.dim() == 2
338
+ if state_batch_indices is not None and state_batch_indices.dim() == 1:
339
+ state_batch_indices = state_batch_indices.unsqueeze(1)
340
+ if dst_state_batch_indices is not None and dst_state_batch_indices.dim() == 1:
341
+ dst_state_batch_indices = dst_state_batch_indices.unsqueeze(1)
342
343
_, nheads, dim, dstate = state.shape
344
batch = x.shape[0]
345
+ if cu_seqlens is not None:
346
+ N = len(cu_seqlens) - 1
347
+ # Only used to verify the shape of
348
+ # state_batch_indices and dst_state_batch_indices
349
+ max_seqlen = (
350
+ state_batch_indices.size(-1) if state_batch_indices is not None else 1
351
+ )
352
+ else:
353
+ N = batch
354
+ max_seqlen = 1
355
356
assert x.shape == (batch, nheads, dim)
357
assert dt.shape == x.shape
@@ -279,16 +367,30 @@ def selective_state_update(
367
if dt_bias is not None:
368
assert dt_bias.shape == (nheads, dim)
369
if state_batch_indices is not None:
282
- assert state_batch_indices.shape == (batch,)
370
+ assert state_batch_indices.shape[0] >= N
371
+ assert state_batch_indices.shape[1] >= max_seqlen
372
if dst_state_batch_indices is not None:
284
- assert dst_state_batch_indices.shape == (batch,)
373
+ assert dst_state_batch_indices.shape[0] >= N
374
+ assert dst_state_batch_indices.shape[1] >= max_seqlen
375
else:
376
# revert to the default behavior of in-place state updates
377
dst_state_batch_indices = state_batch_indices
378
assert out.shape == x.shape
379
+ if num_accepted_tokens is not None:
380
+ assert num_accepted_tokens.shape == (N,)
381
290
- grid = lambda META: (triton.cdiv(dim, META["BLOCK_SIZE_M"]), batch, nheads)
382
+ grid = lambda META: (triton.cdiv(dim, META["BLOCK_SIZE_M"]), N, nheads)
383
z_strides = (z.stride(0), z.stride(1), z.stride(2)) if z is not None else (0, 0, 0)
384
+ state_batch_indices_strides = (
385
+ (state_batch_indices.stride(0), state_batch_indices.stride(1))
386
+ if state_batch_indices is not None
387
+ else (0, 0)
388
+ )
389
+ dst_state_batch_indices_strides = (
390
+ (dst_state_batch_indices.stride(0), dst_state_batch_indices.stride(1))
391
+ if dst_state_batch_indices is not None
392
+ else (0, 0)
393
+ )
394
# We don't want autotune since it will overwrite the state
395
# We instead tune by hand.
396
BLOCK_SIZE_M, num_warps = (
@@ -321,7 +423,9 @@ def selective_state_update(
423
state_batch_indices,
424
dst_state_batch_indices,
425
pad_slot_id,
324
- batch,
426
+ num_accepted_tokens,
427
+ cu_seqlens,
428
+ N,
429
nheads,
430
dim,
431
dstate,
@@ -353,6 +457,10 @@ def selective_state_update(
457
out.stride(0),
458
out.stride(1),
459
out.stride(2),
460
+ state_batch_indices_strides[0],
461
+ state_batch_indices_strides[1],
462
+ dst_state_batch_indices_strides[0],
463
+ dst_state_batch_indices_strides[1],
464
dt_softplus,
465
tie_hdim,
466
BLOCK_SIZE_M,