main
py 1,153 lines 38.2 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import pytest
5 import torch
6 import torch.nn.functional as F
7 from einops import rearrange, repeat
8
9 from tests.kernels.mamba.utils import selective_state_update_ref
10 from tests.kernels.utils import opcheck
11 from vllm import _custom_ops as ops # noqa: F401
12 from vllm.model_executor.layers.mamba.ops.mamba_ssm import (
13 selective_scan_fn,
14 selective_state_update,
15 )
16 from vllm.platforms import current_platform
17 from vllm.utils.torch_utils import set_random_seed
18 from vllm.v1.attention.backends.utils import NULL_BLOCK_ID
19
20 DEVICE = current_platform.device_type
21
22 pytestmark = pytest.mark.skipif(
23 not (
24 current_platform.is_cuda_alike()
25 or current_platform.is_xpu()
26 or current_platform.is_cpu()
27 ),
28 reason="mamba_ssm kernels require CUDA-alike, XPU, or CPU",
29 )
30
31 # selective_scan_fn is backed by the CUDA-only `ops.selective_scan_fwd` C++ op,
32 # so tests exercising it must be skipped on XPU. selective_state_update is
33 # pure Triton and runs on both CUDA-alike and XPU.
34 skip_unless_cuda_alike = pytest.mark.skipif(
35 not current_platform.is_cuda_alike(),
36 reason="selective_scan_fn uses CUDA-only custom op",
37 )
38
39
40 def selective_scan_ref(
41 u,
42 delta,
43 A,
44 B,
45 C,
46 D=None,
47 z=None,
48 delta_bias=None,
49 delta_softplus=False,
50 return_last_state=False,
51 prev_state=None,
52 final_state_out=None,
53 ):
54 """u: r(B D L)
55 delta: r(B D L)
56 A: c(D N) or r(D N)
57 B: c(D N) or r(B N L) or r(B N 2L) or r(B G N L) or (B G N L)
58 C: c(D N) or r(B N L) or r(B N 2L) or r(B G N L) or (B G N L)
59 D: r(D)
60 z: r(B D L)
61 delta_bias: r(D), fp32
62 prev_state: r(B D N), fp32
63
64 out: r(B D L)
65 last_state (optional): r(B D dstate) or c(B D dstate)
66 """
67 dtype_in = u.dtype
68 u = u.float()
69 delta = delta.float()
70 if delta_bias is not None:
71 delta = delta + delta_bias[..., None].float()
72 if delta_softplus:
73 delta = F.softplus(delta)
74 batch, dim, dstate = u.shape[0], A.shape[0], A.shape[1]
75 is_variable_B = B.dim() >= 3
76 is_variable_C = C.dim() >= 3
77 B = B.float()
78 C = C.float()
79 x = A.new_zeros((batch, dim, dstate)) if prev_state is None else prev_state
80 ys = []
81 deltaA = torch.exp(torch.einsum("bdl,dn->bdln", delta, A))
82 if not is_variable_B:
83 deltaB_u = torch.einsum("bdl,dn,bdl->bdln", delta, B, u)
84 else:
85 if B.dim() == 3:
86 deltaB_u = torch.einsum("bdl,bnl,bdl->bdln", delta, B, u)
87 else:
88 B = repeat(B, "B G N L -> B (G H) N L", H=dim // B.shape[1])
89 deltaB_u = torch.einsum("bdl,bdnl,bdl->bdln", delta, B, u)
90 if is_variable_C and C.dim() == 4:
91 C = repeat(C, "B G N L -> B (G H) N L", H=dim // C.shape[1])
92 for i in range(u.shape[2]):
93 x = deltaA[:, :, i] * x + deltaB_u[:, :, i]
94 if not is_variable_C:
95 y = torch.einsum("bdn,dn->bd", x, C)
96 else:
97 if C.dim() == 3:
98 y = torch.einsum("bdn,bn->bd", x, C[:, :, i])
99 else:
100 y = torch.einsum("bdn,bdn->bd", x, C[:, :, :, i])
101 if i == u.shape[2] - 1:
102 if final_state_out is None:
103 final_state_out = x
104 else:
105 final_state_out.copy_(x)
106 ys.append(y)
107 y = torch.stack(ys, dim=2) # (batch dim L)
108 out = y if D is None else y + u * rearrange(D, "d -> d 1")
109 if z is not None:
110 out = out * F.silu(z)
111 out = out.to(dtype=dtype_in)
112 return out if not return_last_state else (out, final_state_out)
113
114
115 def selective_scan_opcheck_fn(
116 u,
117 delta,
118 A,
119 B,
120 C,
121 D=None,
122 z=None,
123 delta_bias=None,
124 delta_softplus=False,
125 cu_seq_len=None,
126 cache_indices=None,
127 has_initial_state=None,
128 ssm_states=None,
129 null_block_id=NULL_BLOCK_ID,
130 block_size=2048,
131 block_idx_first_scheduled_token=None,
132 block_idx_last_scheduled_token=None,
133 initial_state_idx=None,
134 cu_chunk_seqlen=None,
135 last_chunk_indices=None,
136 ):
137 """If return_last_state is True, returns (out, last_state)
138 last_state has shape (batch, dim, dstate).
139 """
140 if u.stride(-1) != 1:
141 u = u.contiguous()
142 if delta.stride(-1) != 1:
143 delta = delta.contiguous()
144 if D is not None:
145 D = D.contiguous()
146 if B.stride(-1) != 1:
147 B = B.contiguous()
148 if C.stride(-1) != 1:
149 C = C.contiguous()
150 if z is not None and z.stride(-1) != 1:
151 z = z.contiguous()
152 if B.dim() == 3 and cu_seq_len is None:
153 B = B.unsqueeze(1)
154 if B.dim() == 2 and cu_seq_len is not None:
155 B = B.unsqueeze(0)
156 if C.dim() == 3 and cu_seq_len is None:
157 C = C.unsqueeze(1)
158 if C.dim() == 2 and cu_seq_len is not None:
159 C = C.unsqueeze(0)
160
161 # Disable test_autograd_registration for now as it seems to trigger
162 # a bogus error.
163 opcheck(
164 torch.ops._C.selective_scan_fwd,
165 (
166 u,
167 delta,
168 A,
169 B,
170 C,
171 D,
172 z,
173 delta_bias,
174 delta_softplus,
175 cu_seq_len,
176 cache_indices,
177 has_initial_state,
178 ssm_states,
179 null_block_id,
180 block_size,
181 block_idx_first_scheduled_token,
182 block_idx_last_scheduled_token,
183 initial_state_idx,
184 cu_chunk_seqlen,
185 last_chunk_indices,
186 ),
187 test_utils=["test_schema", "test_faketensor"],
188 )
189
190
191 @pytest.mark.parametrize("wtype", [torch.float32])
192 @pytest.mark.parametrize("itype", [torch.float32, torch.bfloat16])
193 @pytest.mark.parametrize("seqlen", [128, 1024, 4096])
194 @pytest.mark.parametrize("has_delta_bias", [True])
195 @pytest.mark.parametrize("delta_softplus", [True])
196 @pytest.mark.parametrize("has_z", [True])
197 @pytest.mark.parametrize("has_D", [True])
198 @pytest.mark.parametrize("varBC_groups", [1, 2])
199 @pytest.mark.parametrize("is_variable_C", [True])
200 @pytest.mark.parametrize("is_variable_B", [True])
201 @pytest.mark.parametrize("scan_chunks", [1, 3])
202 @skip_unless_cuda_alike
203 def test_selective_scan(
204 is_variable_B,
205 is_variable_C,
206 varBC_groups,
207 has_D,
208 has_z,
209 has_delta_bias,
210 delta_softplus,
211 seqlen,
212 itype,
213 wtype,
214 scan_chunks,
215 ):
216 if varBC_groups > 1 and (not is_variable_B or not is_variable_C):
217 pytest.skip() # This config is not applicable
218 device = "cuda"
219 rtol, atol = (6e-4, 2e-3) if itype == torch.float32 else (3e-3, 5e-3)
220 if itype == torch.bfloat16:
221 rtol, atol = 3e-2, 5e-2
222 rtolw, atolw = (1e-3, 1e-3)
223 if has_z: # If we have z, the errors on the weights seem higher
224 rtolw = max(rtolw, rtol)
225 atolw = max(atolw, atol)
226 # set seed
227 set_random_seed(0)
228 batch_size = 1
229 dim = 4
230 dstate = 8
231 A = -0.5 * torch.rand(dim, dstate, device=device, dtype=wtype)
232 A_ref = A.clone()
233 if not is_variable_B:
234 B_shape = [dim, dstate]
235 elif varBC_groups == 1:
236 B_shape = [batch_size, dstate, seqlen]
237 else:
238 B_shape = [batch_size, varBC_groups, dstate, seqlen]
239 B = torch.randn(B_shape, device=device, dtype=wtype if not is_variable_B else itype)
240 B_ref = B.clone()
241 if not is_variable_C:
242 C_shape = [dim, dstate]
243 elif varBC_groups == 1:
244 C_shape = [batch_size, dstate, seqlen]
245 else:
246 C_shape = [batch_size, varBC_groups, dstate, seqlen]
247 C = torch.randn(C_shape, device=device, dtype=wtype if not is_variable_C else itype)
248 C_ref = C.clone()
249 D = torch.randn(dim, device=device, dtype=torch.float32) if has_D else None
250 D_ref = D.clone() if D is not None else None
251 z = (
252 torch.randn(batch_size, dim, seqlen, device=device, dtype=itype)
253 if has_z
254 else None
255 )
256 z_ref = z.clone() if z is not None else None
257 delta_bias = (
258 (0.5 * torch.rand(dim, device=device, dtype=torch.float32))
259 if has_delta_bias
260 else None
261 )
262 u = torch.randn(batch_size, dim, seqlen, device=device, dtype=itype)
263 u_ref = u.clone()
264 delta = 0.5 * torch.rand(batch_size, dim, seqlen, device=device, dtype=itype)
265 delta_ref = delta.clone()
266 state_shape = (batch_size, u.shape[1], int(A.shape[1]))
267 state = torch.randn(state_shape, device=u.device, dtype=itype, requires_grad=False)
268 state_ref = state.clone()
269 out = None
270 out_ref = None
271 outs = []
272 for c in range(scan_chunks):
273 chunked_prompt_len = seqlen // scan_chunks
274 chunk_start = chunked_prompt_len * c
275 chunk_end = chunked_prompt_len * (c + 1)
276 if c == scan_chunks - 1:
277 chunk_end = seqlen
278 _B = B
279 if is_variable_B:
280 _B = B[..., chunk_start:chunk_end]
281 _C = C
282 if is_variable_B:
283 _C = C[..., chunk_start:chunk_end]
284 _z = z
285 if has_z:
286 assert z is not None
287 _z = z[..., chunk_start:chunk_end]
288 out = selective_scan_fn(
289 u[..., chunk_start:chunk_end],
290 state,
291 delta[..., chunk_start:chunk_end],
292 A,
293 _B,
294 _C,
295 D,
296 z=_z,
297 delta_bias=delta_bias,
298 delta_softplus=delta_softplus,
299 has_initial_state=torch.ones(batch_size, device=u.device, dtype=torch.bool)
300 if c > 0
301 else None,
302 block_size=2048,
303 block_idx_first_scheduled_token=None,
304 block_idx_last_scheduled_token=None,
305 initial_state_idx=None,
306 )
307 outs.append(out)
308 if len(outs) > 1:
309 out = torch.cat(outs, dim=-1)
310
311 out_ref, state_ref, *rest = selective_scan_ref(
312 u_ref,
313 delta_ref,
314 A_ref,
315 B_ref,
316 C_ref,
317 D_ref,
318 z=z_ref,
319 delta_bias=delta_bias,
320 delta_softplus=delta_softplus,
321 return_last_state=True,
322 )
323
324 assert out is not None and out_ref is not None
325 assert torch.allclose(out, out_ref, rtol=rtol, atol=atol)
326 assert state is not None and state_ref is not None
327 assert torch.allclose(state, state_ref.to(itype), rtol=rtol, atol=atol)
328
329 selective_scan_opcheck_fn(
330 u,
331 delta,
332 A,
333 B,
334 C,
335 D,
336 z,
337 delta_bias=delta_bias,
338 delta_softplus=delta_softplus,
339 ssm_states=state,
340 block_size=2048,
341 )
342
343
344 @pytest.mark.parametrize("itype", [torch.float32, torch.bfloat16])
345 @pytest.mark.parametrize("has_z", [False, True])
346 @pytest.mark.parametrize("dstate", [16, 64])
347 @pytest.mark.parametrize("dim", [2048, 2048 + 16, 4096])
348 @pytest.mark.skipif(
349 current_platform.is_cpu(),
350 reason=(
351 "CPU kernel for selective_state_update only supports "
352 "Mamba 2 (scalar A/dt), not Mamba 1."
353 ),
354 )
355 def test_selective_state_update(dim, dstate, has_z, itype):
356 device = DEVICE
357 rtol, atol = (3e-4, 1e-3) if itype == torch.float32 else (5e-3, 1e-2)
358 if itype == torch.bfloat16:
359 rtol, atol = 1e-2, 5e-2
360 if (
361 current_platform.is_rocm()
362 or current_platform.is_xpu()
363 or current_platform.is_device_capability_family(90)
364 ):
365 atol *= 2
366 # set seed
367 set_random_seed(0)
368 batch_size = 1
369 state = torch.randn(batch_size, dim, dstate, dtype=itype, device=device)
370 x = torch.randn(batch_size, dim, device=device, dtype=itype)
371 out = torch.empty_like(x)
372 dt = torch.randn(batch_size, dim, device=device, dtype=itype)
373 dt_bias = torch.rand(dim, device=device) - 4.0
374 A = -torch.rand(dim, dstate, device=device) - 1.0
375 B = torch.randn(batch_size, dstate, device=device)
376 C = torch.randn(batch_size, dstate, device=device)
377 D = torch.randn(dim, device=device)
378 z = torch.randn_like(x) if has_z else None
379 state_ref = state.detach().clone()
380 selective_state_update(
381 state, x, dt, A, B, C, D=D, z=z, dt_bias=dt_bias, dt_softplus=True, out=out
382 )
383 out_ref = selective_state_update_ref(
384 state_ref, x, dt, A, B, C, D=D, z=z, dt_bias=dt_bias, dt_softplus=True
385 )
386
387 assert torch.allclose(state, state_ref, rtol=rtol, atol=atol)
388 assert torch.allclose(out, out_ref, rtol=rtol, atol=atol)
389
390
391 @pytest.mark.parametrize("philox_rounds", [0, 4])
392 @pytest.mark.parametrize("has_z", [False, True])
393 @pytest.mark.parametrize("dstate", [16, 64])
394 @pytest.mark.parametrize("dim", [2048, 4096])
395 @pytest.mark.skipif(
396 not (
397 current_platform.is_cuda() and current_platform.is_device_capability_family(100)
398 ),
399 reason="Stochastic rounding in triton is only supported"
400 " on compute capability 10.0 CUDA devices.",
401 )
402 def test_selective_state_update_stochastic_rounding(dim, dstate, has_z, philox_rounds):
403 device = DEVICE
404 rtol, atol = 5e-3, 1e-1
405 # set seed
406 set_random_seed(0)
407 batch_size = 1
408 state = torch.randn(batch_size, dim, dstate, dtype=torch.float16, device=device)
409 x = torch.randn(batch_size, dim, device=device, dtype=torch.bfloat16)
410 out = torch.empty_like(x)
411 dt = torch.randn(batch_size, dim, device=device, dtype=torch.bfloat16)
412 dt_bias = torch.rand(dim, device=device) - 4.0
413 A = -torch.rand(dim, dstate, device=device) - 1.0
414 B = torch.randn(batch_size, dstate, device=device)
415 C = torch.randn(batch_size, dstate, device=device)
416 D = torch.randn(dim, device=device)
417 z = torch.randn_like(x) if has_z else None
418 # Reference uses fp32 state to get ground truth
419 state_ref = state.float()
420 selective_state_update(
421 state,
422 x,
423 dt,
424 A,
425 B,
426 C,
427 D=D,
428 z=z,
429 dt_bias=dt_bias,
430 dt_softplus=True,
431 out=out,
432 enable_stochastic_rounding=True,
433 cache_philox_rounds=philox_rounds,
434 )
435 out_ref = selective_state_update_ref(
436 state_ref, x, dt, A, B, C, D=D, z=z, dt_bias=dt_bias, dt_softplus=True
437 )
438
439 assert state.dtype == torch.float16
440 assert torch.allclose(state, state_ref.to(torch.float16), rtol=rtol, atol=atol)
441 assert torch.allclose(out, out_ref, rtol=rtol, atol=atol)
442
443
444 @pytest.mark.parametrize("itype", [torch.float32, torch.bfloat16])
445 @pytest.mark.parametrize("has_z", [False, True])
446 @pytest.mark.parametrize("dstate", [16, 64])
447 @pytest.mark.parametrize("dim", [2048, 2048 + 16, 4096])
448 @pytest.mark.parametrize("max_seq_len", [1, 2, 4])
449 @pytest.mark.skipif(
450 current_platform.is_cpu(),
451 reason=(
452 "CPU kernel for selective_state_update only supports "
453 "Mamba 2 (scalar A/dt), not Mamba 1."
454 ),
455 )
456 def test_selective_state_update_varlen(dim, dstate, has_z, itype, max_seq_len):
457 device = DEVICE
458 rtol, atol = (3e-4, 1e-3) if itype == torch.float32 else (5e-3, 1e-2)
459 if itype == torch.bfloat16:
460 rtol, atol = 5e-2, 1.5e-1
461 if current_platform.is_rocm() or current_platform.is_xpu():
462 atol *= 2
463 # set seed
464 set_random_seed(0)
465 batch_size = 4
466 token_counts = torch.randint(1, max_seq_len + 1, (batch_size,), device=device)
467 total_tokens = int(token_counts.sum().item())
468 cu_seqlens = torch.tensor(
469 [0] + torch.cumsum(token_counts, dim=0).tolist(),
470 dtype=torch.int32,
471 device=device,
472 )
473 state = torch.randn(batch_size, dim, dstate, dtype=itype, device=device)
474 x = torch.randn(total_tokens, dim, device=device, dtype=itype)
475 out = torch.empty_like(x)
476 dt = torch.randn(total_tokens, dim, device=device, dtype=itype)
477 dt_bias = torch.rand(dim, device=device) - 4.0
478 A = -torch.rand(dim, dstate, device=device) - 1.0
479 B = torch.randn(total_tokens, dstate, device=device)
480 C = torch.randn(total_tokens, dstate, device=device)
481 D = torch.randn(dim, device=device)
482 z = torch.randn_like(x) if has_z else None
483 state_ref = state.detach().clone()
484 selective_state_update(
485 state,
486 x,
487 dt,
488 A,
489 B,
490 C,
491 D=D,
492 z=z,
493 dt_bias=dt_bias,
494 dt_softplus=True,
495 out=out,
496 cu_seqlens=cu_seqlens,
497 )
498
499 out_ref_list = []
500 for seq_idx in range(batch_size):
501 start_idx = cu_seqlens[seq_idx].item()
502 end_idx = cu_seqlens[seq_idx + 1].item()
503 num_tokens = end_idx - start_idx
504 for token_idx in range(num_tokens):
505 idx = start_idx + token_idx
506 out_ref_list.append(
507 selective_state_update_ref(
508 state_ref[seq_idx : seq_idx + 1],
509 x[idx : idx + 1],
510 dt[idx : idx + 1],
511 A,
512 B[idx : idx + 1],
513 C[idx : idx + 1],
514 D=D,
515 z=z[idx : idx + 1] if z is not None else None,
516 dt_bias=dt_bias,
517 dt_softplus=True,
518 )
519 )
520 out_ref = torch.cat(out_ref_list, dim=0)
521 assert torch.allclose(state, state_ref, rtol=rtol, atol=atol)
522 assert torch.allclose(out, out_ref, rtol=rtol, atol=atol)
523
524
525 @pytest.mark.parametrize("wtype", [torch.float32])
526 @pytest.mark.parametrize("itype", [torch.float32])
527 @pytest.mark.parametrize("seqlen", [1, 256, 1024, 4096])
528 @pytest.mark.parametrize("return_last_state", [True])
529 @pytest.mark.parametrize("has_delta_bias", [True])
530 @pytest.mark.parametrize("delta_softplus", [True])
531 @pytest.mark.parametrize("has_z", [True])
532 @pytest.mark.parametrize("has_D", [True])
533 @pytest.mark.parametrize("varBC_groups", [1, 2])
534 @pytest.mark.parametrize("is_variable_C", [True])
535 @pytest.mark.parametrize("is_variable_B", [True])
536 # tests correctness in case subset of the sequences are padded
537 @pytest.mark.parametrize("with_padding", [False, True])
538 @skip_unless_cuda_alike
539 def test_selective_scan_varlen(
540 with_padding,
541 is_variable_B,
542 is_variable_C,
543 varBC_groups,
544 has_D,
545 has_z,
546 has_delta_bias,
547 delta_softplus,
548 return_last_state,
549 seqlen,
550 itype,
551 wtype,
552 ):
553 if varBC_groups > 1 and (not is_variable_B or not is_variable_C):
554 pytest.skip() # This config is not applicable
555 device = "cuda"
556 rtol, atol = (6e-4, 2e-3) if itype == torch.float32 else (3e-3, 5e-3)
557 if itype == torch.bfloat16:
558 rtol, atol = 3e-2, 5e-2
559 rtolw, atolw = (1e-3, 1e-3)
560 if has_z: # If we have z, the errors on the weights seem higher
561 rtolw = max(rtolw, rtol)
562 atolw = max(atolw, atol)
563 # set seed
564 torch.random.manual_seed(0)
565 seqlens = []
566 batch_size = 4
567 if seqlen < 10:
568 batch_size = 1
569 padding = 3 if with_padding else 0
570 padded_batch_size = batch_size + padding
571
572 if with_padding and seqlen < padded_batch_size:
573 pytest.skip()
574
575 nsplits = padded_batch_size - 1
576 eos_pos = torch.randperm(seqlen - 1)[:nsplits].sort().values
577 seqlens.append(
578 torch.diff(
579 torch.cat([torch.tensor([-1]), eos_pos, torch.tensor([seqlen - 1])])
580 ).tolist()
581 )
582
583 assert sum(seqlens[-1]) == seqlen
584 assert all(s > 0 for s in seqlens[-1])
585
586 total_entries = batch_size * 10
587 cumsum = torch.cumsum(torch.tensor(seqlens[0]), dim=0).to(torch.int32)
588 cumsum = torch.concat([torch.tensor([0], dtype=torch.int32), cumsum], dim=0).cuda()
589
590 dim = 4
591 dstate = 8
592 A = -0.5 * torch.rand(dim, dstate, device=device, dtype=wtype)
593 A_ref = A.clone()
594 B_shape = [varBC_groups, dstate, seqlen]
595 B = torch.randn(B_shape, device=device, dtype=wtype if not is_variable_B else itype)
596 B_ref = B.clone()
597 C_shape = [varBC_groups, dstate, seqlen]
598 C = torch.randn(C_shape, device=device, dtype=wtype if not is_variable_C else itype)
599 C_ref = C.clone()
600 D = torch.randn(dim, device=device, dtype=torch.float32) if has_D else None
601 D_ref = D.clone() if D is not None else None
602 z = torch.randn(dim, seqlen, device=device, dtype=itype)
603 z_ref = z.clone()
604 delta_bias = (
605 (0.5 * torch.rand(dim, device=device, dtype=torch.float32))
606 if has_delta_bias
607 else None
608 )
609 u = torch.randn(dim, seqlen, device=device, dtype=itype)
610 u_ref = u.clone()
611 delta = 0.5 * torch.rand(dim, seqlen, device=device, dtype=itype)
612 delta_ref = delta.clone()
613 out = None
614 out_ref = None
615
616 prev_state_shape = (total_entries, u.shape[0], int(A.shape[1]))
617 prev_state = torch.randn(
618 prev_state_shape, device=u.device, dtype=itype, requires_grad=False
619 )
620 prev_state_ref = prev_state.clone()
621 # +1 to exclude index 0 (null block)
622 state_indices = (
623 torch.randperm(total_entries - 1, dtype=torch.int32, device=u.device)[
624 :batch_size
625 ]
626 + 1
627 )
628 unused_states_bool = torch.ones(total_entries, dtype=torch.bool, device=device)
629 unused_states_bool[state_indices] = False
630 padded_state_indices = torch.concat(
631 [
632 state_indices,
633 torch.as_tensor(
634 [NULL_BLOCK_ID] * padding, dtype=torch.int32, device=device
635 ),
636 ],
637 dim=-1,
638 )
639
640 has_initial_state = torch.randint(
641 0, 2, (cumsum.shape[0] - 1,), dtype=torch.bool, device=u.device
642 )
643 out = selective_scan_fn(
644 u,
645 prev_state,
646 delta,
647 A,
648 B,
649 C,
650 D,
651 z,
652 delta_bias,
653 delta_softplus,
654 cumsum,
655 padded_state_indices,
656 has_initial_state,
657 )
658 outs_ref = []
659 splits = [
660 torch.split(var, seqlens[0], dim=-1)
661 for var in (u_ref, delta_ref, B_ref, C_ref, z_ref)
662 ]
663 for i in range(len(seqlens[0])):
664 u_s, delta_s, B_s, C_s, z_s = (v[i].unsqueeze(0) for v in splits)
665 if padded_state_indices[i] == NULL_BLOCK_ID:
666 continue
667 out_ref_s, _ = selective_scan_ref(
668 u_s,
669 delta_s,
670 A_ref,
671 B_s,
672 C_s,
673 D_ref,
674 z=z_s,
675 delta_bias=delta_bias,
676 delta_softplus=delta_softplus,
677 return_last_state=return_last_state,
678 prev_state=prev_state_ref[padded_state_indices[i]].unsqueeze(0)
679 if has_initial_state[i]
680 else None,
681 final_state_out=prev_state_ref[padded_state_indices[i]].unsqueeze(0),
682 )
683 outs_ref.append(out_ref_s)
684 out_ref = torch.cat(outs_ref, dim=-1)[0]
685
686 unpadded_out = out[:, : out_ref[0].shape[-1]]
687 print("Output diff max", (unpadded_out - out_ref).max())
688 print("Output diff mean", (unpadded_out - out_ref).mean())
689 print("Output state diff max", (prev_state - prev_state_ref).max())
690 print("Output state diff mean", (prev_state - prev_state_ref).mean())
691 assert torch.allclose(prev_state, prev_state_ref, rtol=rtol, atol=atol)
692 assert torch.allclose(unpadded_out, out_ref, rtol=rtol, atol=atol)
693 selective_scan_opcheck_fn(
694 u,
695 delta,
696 A,
697 B,
698 C,
699 D,
700 z,
701 delta_bias,
702 delta_softplus,
703 cumsum,
704 padded_state_indices,
705 has_initial_state,
706 prev_state,
707 block_size=2048,
708 )
709
710
711 @pytest.mark.parametrize("itype", [torch.float32, torch.bfloat16])
712 @pytest.mark.parametrize("has_z", [True])
713 @pytest.mark.parametrize("dstate", [16, 64])
714 @pytest.mark.parametrize("dim", [2048, 2048 + 16, 4096])
715 # tests correctness in case subset of the sequences are padded
716 @pytest.mark.parametrize("with_padding", [True, False])
717 @pytest.mark.skipif(
718 current_platform.is_cpu(),
719 reason=(
720 "CPU kernel for selective_state_update only supports "
721 "Mamba 2 (scalar A/dt), not Mamba 1."
722 ),
723 )
724 def test_selective_state_update_with_batch_indices(
725 with_padding, dim, dstate, has_z, itype
726 ):
727 device = DEVICE
728 rtol, atol = (3e-4, 1e-3) if itype == torch.float32 else (5e-3, 1e-2)
729 if itype == torch.bfloat16:
730 rtol, atol = 1e-1, 1e-1
731 if current_platform.is_rocm() or current_platform.is_xpu():
732 atol *= 2
733 # set seed
734 torch.random.manual_seed(0)
735 batch_size = 3
736 padding = 5 if with_padding else 0
737 padded_batch_size = batch_size + padding
738 total_entries = 10 * batch_size
739 state = torch.randn(total_entries, dim, dstate, dtype=itype, device=device)
740 # +1 to exclude index 0 (null block)
741 state_indices = (torch.randperm(total_entries - 1)[:batch_size] + 1).to(
742 dtype=torch.int32, device=device
743 )
744 unused_states_bool = torch.ones(total_entries, dtype=torch.bool, device=device)
745 unused_states_bool[state_indices] = False
746 padded_state_indices = torch.concat(
747 [
748 state_indices,
749 torch.as_tensor(
750 [NULL_BLOCK_ID] * padding, dtype=torch.int32, device=device
751 ),
752 ],
753 dim=0,
754 )
755 x = torch.randn(padded_batch_size, dim, device=device, dtype=itype)
756 out = torch.empty_like(x)
757 dt = torch.randn(padded_batch_size, dim, device=device, dtype=itype)
758 dt_bias = torch.rand(dim, device=device) - 4.0
759 A = -torch.rand(dim, dstate, device=device) - 1.0
760 B = torch.randn(padded_batch_size, dstate, device=device)
761 C = torch.randn(padded_batch_size, dstate, device=device)
762 D = torch.randn(dim, device=device)
763 z = torch.randn_like(x) if has_z else None
764 state_ref = state[state_indices, :].clone()
765 state_before = state.clone()
766 selective_state_update(
767 state,
768 x,
769 dt,
770 A,
771 B,
772 C,
773 D=D,
774 z=z,
775 dt_bias=dt_bias,
776 dt_softplus=True,
777 state_batch_indices=padded_state_indices,
778 out=out,
779 )
780 out_ref = selective_state_update_ref(
781 state_ref,
782 x[:batch_size],
783 dt[:batch_size],
784 A,
785 B[:batch_size],
786 C[:batch_size],
787 D=D,
788 z=z[:batch_size] if z is not None else None,
789 dt_bias=dt_bias,
790 dt_softplus=True,
791 )
792
793 print("Output diff max", (out[:batch_size] - out_ref).max())
794 print("Output diff mean", (out[:batch_size] - out_ref).mean())
795 print("Output state diff max", (state[state_indices, :] - state_ref).max())
796 print("Output state diff mean", (state[state_indices, :] - state_ref).mean())
797 # test padded entries stay the same
798 if with_padding:
799 assert torch.equal(state_before[unused_states_bool], state[unused_states_bool])
800 assert torch.equal(x[batch_size + 1 :], x[batch_size + 1 :])
801 assert torch.equal(dt[batch_size + 1 :], dt[batch_size + 1 :])
802 assert torch.equal(B[batch_size + 1 :], B[batch_size + 1 :])
803 assert torch.equal(C[batch_size + 1 :], C[batch_size + 1 :])
804
805 # test "real" entries
806 assert torch.allclose(state[state_indices, :], state_ref, rtol=rtol, atol=atol)
807 assert torch.allclose(out[:batch_size], out_ref, rtol=rtol, atol=atol)
808
809
810 @pytest.mark.parametrize("itype", [torch.float32, torch.bfloat16])
811 @pytest.mark.parametrize("has_z", [False, True])
812 @pytest.mark.parametrize("tie_hdim", [False, True])
813 @pytest.mark.parametrize("ngroups", [1, 4])
814 @pytest.mark.parametrize("dstate", [16, 64])
815 @pytest.mark.parametrize("dim", [2048, 4096])
816 @pytest.mark.skipif(
817 current_platform.is_cpu(),
818 reason=(
819 "CPU kernel for selective_state_update only supports "
820 "Mamba 2 (scalar A/dt), not Mamba 1."
821 ),
822 )
823 def test_selective_state_update_with_heads_with_batch_indices(
824 dim, dstate, ngroups, has_z, tie_hdim, itype
825 ):
826 device = DEVICE
827 rtol, atol = (3e-4, 1e-3) if itype == torch.float32 else (5e-3, 3e-2)
828 if itype == torch.bfloat16:
829 rtol, atol = 1e-1, 1e-1
830 # set seed
831 torch.random.manual_seed(0)
832 batch_size = 3
833 headdim = 64
834 nheads = dim // headdim
835
836 total_entries = 10 * batch_size
837 state = torch.randn(
838 total_entries, nheads, headdim, dstate, dtype=itype, device=device
839 )
840 # +1 to exclude index 0 (null block)
841 state_indices = (torch.randperm(total_entries - 1)[:batch_size] + 1).to(
842 dtype=torch.int32, device=device
843 )
844
845 x = torch.randn(batch_size, nheads, headdim, device=device, dtype=itype)
846 out = torch.empty_like(x)
847 if not tie_hdim:
848 dt = torch.randn(batch_size, nheads, headdim, device=device, dtype=itype)
849 dt_bias = torch.rand(nheads, headdim, device=device) - 4.0
850 A = -torch.rand(nheads, headdim, dstate, device=device) - 1.0
851 D = torch.randn(nheads, headdim, device=device)
852 else:
853 dt = repeat(
854 torch.randn(batch_size, nheads, device=device, dtype=itype),
855 "b h -> b h p",
856 p=headdim,
857 )
858 dt_bias = repeat(torch.rand(nheads, device=device) - 4.0, "h -> h p", p=headdim)
859 A = repeat(
860 -torch.rand(nheads, device=device) - 1.0, "h -> h p n", p=headdim, n=dstate
861 )
862 D = repeat(torch.randn(nheads, device=device), "h -> h p", p=headdim)
863 B = torch.randn(batch_size, ngroups, dstate, device=device)
864 C = torch.randn(batch_size, ngroups, dstate, device=device)
865 z = torch.randn_like(x) if has_z else None
866 state_ref = state[state_indices, :].detach().clone()
867 selective_state_update(
868 state,
869 x,
870 dt,
871 A,
872 B,
873 C,
874 D=D,
875 z=z,
876 dt_bias=dt_bias,
877 dt_softplus=True,
878 state_batch_indices=state_indices,
879 out=out,
880 )
881 out_ref = selective_state_update_ref(
882 state_ref, x, dt, A, B, C, D=D, z=z, dt_bias=dt_bias, dt_softplus=True
883 )
884
885 print(f"Output max diff: {(out - out_ref).abs().max().item()}")
886 print(f"Output mean diff: {(out - out_ref).abs().mean().item()}")
887 assert torch.allclose(state[state_indices, :], state_ref, rtol=rtol, atol=atol)
888 assert torch.allclose(out, out_ref, rtol=rtol, atol=atol)
889
890
891 @pytest.mark.parametrize("itype", [torch.float32, torch.bfloat16])
892 @pytest.mark.parametrize("has_z", [False, True])
893 @pytest.mark.parametrize("dstate", [16, 64])
894 @pytest.mark.parametrize("dim", [2048, 4096])
895 @pytest.mark.parametrize("max_seq_len", [2, 4])
896 @pytest.mark.skipif(
897 current_platform.is_cpu(),
898 reason=(
899 "CPU kernel for selective_state_update only supports "
900 "Mamba 2 (scalar A/dt), not Mamba 1."
901 ),
902 )
903 def test_selective_state_update_with_num_accepted_tokens(
904 dim, dstate, has_z, itype, max_seq_len
905 ):
906 device = DEVICE
907 rtol, atol = (3e-4, 1e-3) if itype == torch.float32 else (5e-3, 1e-2)
908 if itype == torch.bfloat16:
909 rtol, atol = 5e-2, 1.5e-1
910 if current_platform.is_rocm() or current_platform.is_xpu():
911 atol *= 2
912
913 set_random_seed(0)
914 batch_size = 4
915
916 tokens_per_seq = torch.randint(1, max_seq_len + 1, (batch_size,), device=device)
917 total_tokens = int(tokens_per_seq.sum().item())
918
919 num_accepted_tokens = torch.randint(0, max_seq_len, (batch_size,), device=device)
920 num_accepted_tokens[0] = 0 # Add edge-case of no accepted tokens
921 num_accepted_tokens[1] = max_seq_len # Add edge-case of all tokens accepted
922
923 cu_seqlens = torch.tensor(
924 [0] + torch.cumsum(tokens_per_seq, dim=0).tolist(),
925 dtype=torch.int32,
926 device=device,
927 )
928
929 total_state_slots = 50
930 state = torch.randn(total_state_slots, dim, dstate, dtype=itype, device=device)
931
932 state_batch_indices = torch.full(
933 (batch_size, max_seq_len), NULL_BLOCK_ID, dtype=torch.int32, device=device
934 )
935 # Start from 1 to exclude null block at index 0
936 initial_state_slots = torch.randint(
937 1, 15, (batch_size,), device=device, dtype=torch.int32
938 )
939 for seq_idx in range(batch_size):
940 token_pos = max(num_accepted_tokens[seq_idx].item() - 1, 0)
941 state_batch_indices[seq_idx, token_pos] = initial_state_slots[seq_idx]
942
943 dst_state_batch_indices = torch.full(
944 (batch_size, max_seq_len), NULL_BLOCK_ID, dtype=torch.int32, device=device
945 )
946 slot_offset = 15
947 dst_slots_map = {}
948 for seq_idx in range(batch_size):
949 for token_idx in range(tokens_per_seq[seq_idx].item()):
950 dst_state_batch_indices[seq_idx, token_idx] = slot_offset
951 dst_slots_map[(seq_idx, token_idx)] = slot_offset
952 slot_offset += 1
953
954 x = torch.randn(total_tokens, dim, device=device, dtype=itype)
955 out = torch.empty_like(x)
956 dt = torch.randn(total_tokens, dim, device=device, dtype=itype)
957 dt_bias = torch.rand(dim, device=device) - 4.0
958 A = -torch.rand(dim, dstate, device=device) - 1.0
959 B = torch.randn(total_tokens, dstate, device=device)
960 C = torch.randn(total_tokens, dstate, device=device)
961 D = torch.randn(dim, device=device)
962 z = torch.randn_like(x) if has_z else None
963
964 state_ref_intermediate = {}
965 out_ref_list = []
966
967 for seq_idx in range(batch_size):
968 seq_start = cu_seqlens[seq_idx].item()
969 seq_end = cu_seqlens[seq_idx + 1].item()
970 num_tokens = seq_end - seq_start
971
972 token_pos = max(num_accepted_tokens[seq_idx].item() - 1, 0)
973 initial_slot = state_batch_indices[seq_idx, token_pos].item()
974 state_seq = state[initial_slot : initial_slot + 1].clone()
975
976 for token_idx in range(num_tokens):
977 global_idx = seq_start + token_idx
978
979 out_token = selective_state_update_ref(
980 state_seq,
981 x[global_idx : global_idx + 1],
982 dt[global_idx : global_idx + 1],
983 A,
984 B[global_idx : global_idx + 1],
985 C[global_idx : global_idx + 1],
986 D=D,
987 z=z[global_idx : global_idx + 1] if z is not None else None,
988 dt_bias=dt_bias,
989 dt_softplus=True,
990 )
991 out_ref_list.append(out_token)
992 state_ref_intermediate[(seq_idx, token_idx)] = state_seq.clone()
993
994 out_ref = torch.cat(out_ref_list, dim=0)
995
996 selective_state_update(
997 state,
998 x,
999 dt,
1000 A,
1001 B,
1002 C,
1003 D=D,
1004 z=z,
1005 dt_bias=dt_bias,
1006 dt_softplus=True,
1007 out=out,
1008 cu_seqlens=cu_seqlens,
1009 state_batch_indices=state_batch_indices,
1010 dst_state_batch_indices=dst_state_batch_indices,
1011 num_accepted_tokens=num_accepted_tokens,
1012 )
1013
1014 assert torch.allclose(out, out_ref, rtol=rtol, atol=atol)
1015
1016 for seq_idx in range(batch_size):
1017 num_tokens = tokens_per_seq[seq_idx].item()
1018 for token_idx in range(num_tokens):
1019 dst_slot = dst_slots_map[(seq_idx, token_idx)]
1020 state_ref = state_ref_intermediate[(seq_idx, token_idx)].squeeze(0)
1021 assert torch.allclose(state[dst_slot], state_ref, rtol=rtol, atol=atol)
1022
1023
1024 @pytest.mark.parametrize("itype", [torch.float32, torch.bfloat16])
1025 @pytest.mark.parametrize("has_z", [False, True])
1026 @pytest.mark.parametrize("dstate", [16, 64])
1027 @pytest.mark.parametrize("dim", [2048, 4096])
1028 @pytest.mark.parametrize("max_seq_len", [2, 4])
1029 @pytest.mark.skipif(
1030 current_platform.is_cpu(),
1031 reason=(
1032 "CPU kernel for selective_state_update only supports "
1033 "Mamba 2 (scalar A/dt), not Mamba 1."
1034 ),
1035 )
1036 def test_selective_state_update_varlen_with_num_accepted(
1037 dim, dstate, has_z, itype, max_seq_len
1038 ):
1039 device = DEVICE
1040 rtol, atol = (3e-4, 1e-3) if itype == torch.float32 else (5e-3, 1e-2)
1041 if itype == torch.bfloat16:
1042 rtol, atol = 5e-2, 1.5e-1
1043 if current_platform.is_rocm() or current_platform.is_xpu():
1044 atol *= 2
1045
1046 set_random_seed(0)
1047 batch_size = 4
1048
1049 tokens_per_seq = torch.randint(1, max_seq_len + 1, (batch_size,), device=device)
1050 total_tokens = int(tokens_per_seq.sum().item())
1051
1052 num_accepted_tokens = torch.randint(0, max_seq_len, (batch_size,), device=device)
1053 num_accepted_tokens[0] = 0 # Add edge-case of no accepted tokens
1054 num_accepted_tokens[1] = max_seq_len # Add edge-case of all tokens accepted
1055
1056 cu_seqlens = torch.tensor(
1057 [0] + torch.cumsum(tokens_per_seq, dim=0).tolist(),
1058 dtype=torch.int32,
1059 device=device,
1060 )
1061
1062 total_state_slots = 50
1063 state = torch.randn(total_state_slots, dim, dstate, dtype=itype, device=device)
1064
1065 state_batch_indices = torch.full(
1066 (batch_size, max_seq_len), NULL_BLOCK_ID, dtype=torch.int32, device=device
1067 )
1068
1069 # Start from 1 to exclude null block at index 0
1070 initial_state_slots = torch.randint(
1071 1, 15, (batch_size,), device=device, dtype=torch.int32
1072 )
1073 for seq_idx in range(batch_size):
1074 token_pos = max(num_accepted_tokens[seq_idx].item() - 1, 0)
1075 state_batch_indices[seq_idx, token_pos] = initial_state_slots[seq_idx]
1076
1077 dst_state_batch_indices = torch.full(
1078 (batch_size, max_seq_len), NULL_BLOCK_ID, dtype=torch.int32, device=device
1079 )
1080
1081 slot_offset = 15
1082 dst_slots_map = {}
1083 for seq_idx in range(batch_size):
1084 for token_idx in range(tokens_per_seq[seq_idx].item()):
1085 dst_state_batch_indices[seq_idx, token_idx] = slot_offset
1086 dst_slots_map[(seq_idx, token_idx)] = slot_offset
1087 slot_offset += 1
1088
1089 x = torch.randn(total_tokens, dim, device=device, dtype=itype)
1090 out = torch.empty_like(x)
1091 dt = torch.randn(total_tokens, dim, device=device, dtype=itype)
1092 dt_bias = torch.rand(dim, device=device) - 4.0
1093 A = -torch.rand(dim, dstate, device=device) - 1.0
1094 B = torch.randn(total_tokens, dstate, device=device)
1095 C = torch.randn(total_tokens, dstate, device=device)
1096 D = torch.randn(dim, device=device)
1097 z = torch.randn_like(x) if has_z else None
1098
1099 state_ref_intermediate = {}
1100
1101 for seq_idx in range(batch_size):
1102 seq_start = cu_seqlens[seq_idx].item()
1103 seq_end = cu_seqlens[seq_idx + 1].item()
1104 num_tokens = seq_end - seq_start
1105
1106 token_pos = max(num_accepted_tokens[seq_idx].item() - 1, 0)
1107 initial_slot = state_batch_indices[seq_idx, token_pos].item()
1108 state_seq = state[initial_slot : initial_slot + 1].clone()
1109
1110 for token_idx in range(num_tokens):
1111 global_idx = seq_start + token_idx
1112
1113 selective_state_update_ref(
1114 state_seq,
1115 x[global_idx : global_idx + 1],
1116 dt[global_idx : global_idx + 1],
1117 A,
1118 B[global_idx : global_idx + 1],
1119 C[global_idx : global_idx + 1],
1120 D=D,
1121 z=z[global_idx : global_idx + 1] if z is not None else None,
1122 dt_bias=dt_bias,
1123 dt_softplus=True,
1124 )
1125
1126 state_ref_intermediate[(seq_idx, token_idx)] = state_seq.clone()
1127
1128 selective_state_update(
1129 state,
1130 x,
1131 dt,
1132 A,
1133 B,
1134 C,
1135 D=D,
1136 z=z,
1137 dt_bias=dt_bias,
1138 dt_softplus=True,
1139 out=out,
1140 cu_seqlens=cu_seqlens,
1141 state_batch_indices=state_batch_indices,
1142 dst_state_batch_indices=dst_state_batch_indices,
1143 num_accepted_tokens=num_accepted_tokens,
1144 )
1145
1146 for seq_idx in range(batch_size):
1147 num_tokens = tokens_per_seq[seq_idx].item()
1148
1149 for token_idx in range(num_tokens):
1150 dst_slot = dst_slots_map[(seq_idx, token_idx)]
1151 state_ref = state_ref_intermediate[(seq_idx, token_idx)].squeeze(0)
1152
1153 assert torch.allclose(state[dst_slot], state_ref, rtol=rtol, atol=atol)