| import pytest |
| import torch |
|
|
|
|
| def do_test(): |
| try: |
| import causal_conv1d |
| import tilelang |
| causal_conv1d_fn = causal_conv1d.causal_conv1d_fn |
| causal_conv1d_update = causal_conv1d.causal_conv1d_update |
| return True |
| except Exception: |
| return False |
|
|
|
|
| @pytest.mark.skipif(not do_test(), reason='tilelang or causal_conv1d is not available') |
| class TestCausalConv1dUpdate: |
|
|
| @pytest.fixture |
| def device(self): |
| yield 'cuda' |
|
|
| @pytest.fixture |
| def batch(self): |
| yield 128 |
|
|
| @pytest.fixture |
| def hidden_size(self): |
| yield 2048 |
|
|
| @pytest.fixture |
| def width(self): |
| yield 4 |
|
|
| @pytest.fixture(params=[1, 4]) |
| def seqlen(self, request): |
| yield request.param |
|
|
| @pytest.fixture |
| def x(self, batch, hidden_size, seqlen, device): |
| yield torch.randn(batch, hidden_size, seqlen, device=device) |
|
|
| @pytest.fixture |
| def weight(self, hidden_size, width, device): |
| yield torch.randn(hidden_size, width, device=device) |
|
|
| @pytest.fixture |
| def bias(self, hidden_size, device): |
| yield torch.randn(hidden_size, device=device) |
|
|
| @pytest.fixture(params=[True, False]) |
| def cache_seqlens(self, request, batch, device): |
| if request.param: |
| yield torch.randint(0, 4096, (batch, ), dtype=torch.int32, device=device) |
| else: |
| yield None |
|
|
| @pytest.fixture(params=[True, False]) |
| def conv_state_indices(self, request, batch, device): |
| if request.param: |
| conv_state_indices = batch * 2 - 1 - torch.arange(0, batch * 2, 2, device=device) |
| yield conv_state_indices.to(torch.int32) |
| else: |
| yield None |
|
|
| @pytest.fixture |
| def conv_state(self, batch, hidden_size, width, device, conv_state_indices): |
| if conv_state_indices is not None: |
| conv_state = torch.randn(batch * 4, hidden_size, width, device=device) |
| conv_state = conv_state[::2] |
| else: |
| conv_state = torch.randn(batch, hidden_size, width, device=device) |
| yield conv_state |
|
|
| @pytest.fixture(params=[None, 'silu']) |
| def activation(self, request): |
| yield request.param |
|
|
| def test_causal_conv1d_update(self, x, conv_state, weight, bias, activation, cache_seqlens, conv_state_indices): |
| from causal_conv1d import causal_conv1d_update as causal_conv1d_update_gt |
|
|
| from lmdeploy.pytorch.kernels.cuda.causal_conv1d import causal_conv1d_update |
|
|
| conv_state_clone = conv_state.clone() |
| out = causal_conv1d_update(x=x, |
| conv_state=conv_state_clone, |
| weight=weight, |
| bias=bias, |
| activation=activation, |
| cache_seqlens=cache_seqlens, |
| conv_state_indices=conv_state_indices) |
| out_gt = causal_conv1d_update_gt(x=x, |
| conv_state=conv_state, |
| weight=weight, |
| bias=bias, |
| activation=activation, |
| cache_seqlens=cache_seqlens, |
| conv_state_indices=conv_state_indices) |
| torch.testing.assert_close(out, out_gt, rtol=1e-3, atol=1e-3) |
| torch.testing.assert_close(conv_state_clone, conv_state, rtol=1e-3, atol=1e-3) |
|
|
|
|
| @pytest.mark.skipif(not do_test(), reason='tilelang or causal_conv1d is not available') |
| class TestCausalConv1dFn: |
|
|
| @pytest.fixture |
| def device(self): |
| yield 'cuda' |
|
|
| @pytest.fixture |
| def hidden_size(self): |
| yield 2048 |
|
|
| @pytest.fixture |
| def seqlen(self): |
| yield 4096 |
|
|
| @pytest.fixture |
| def seq_idx(self, seqlen, device): |
| seq_idx = torch.zeros(seqlen, dtype=torch.int32, device=device) |
| seq_idx[seqlen // 4 * 3:] = 1 |
| seq_idx = seq_idx.view(1, -1) |
| yield seq_idx |
|
|
| @pytest.fixture |
| def x(self, hidden_size, seqlen, device): |
| yield torch.randn(1, hidden_size, seqlen, device=device).transpose(1, 2).contiguous().transpose(1, 2) |
|
|
| @pytest.fixture |
| def weight(self, hidden_size, device): |
| yield torch.randn(hidden_size, 4, device=device) |
|
|
| @pytest.fixture |
| def bias(self, hidden_size, device): |
| yield torch.randn(hidden_size, device=device) |
|
|
| @pytest.fixture(params=[None, 'silu']) |
| def activation(self, request): |
| yield request.param |
|
|
| def test_causal_conv1d_fn(self, x, weight, bias, activation, seq_idx): |
| from causal_conv1d import causal_conv1d_fn as causal_conv1d_fn_gt |
|
|
| from lmdeploy.pytorch.kernels.cuda.causal_conv1d import causal_conv1d_fn |
|
|
| out = causal_conv1d_fn(x=x, |
| weight=weight, |
| bias=bias, |
| activation=activation, |
| return_final_states=False, |
| seq_idx=seq_idx) |
| out_gt = causal_conv1d_fn_gt(x=x, |
| weight=weight, |
| bias=bias, |
| activation=activation, |
| return_final_states=False, |
| seq_idx=seq_idx) |
| torch.testing.assert_close(out, out_gt, rtol=1e-3, atol=1e-3) |
|
|
|
|
| @pytest.mark.skipif(not do_test(), reason='tilelang or causal_conv1d is not available') |
| class TestCausalConv1dFnInitStates: |
| """Test causal_conv1d_fn with per-sequence initial_states.""" |
|
|
| @pytest.fixture |
| def device(self): |
| yield 'cuda' |
|
|
| @pytest.fixture |
| def hidden_size(self): |
| yield 2048 |
|
|
| @pytest.fixture |
| def width(self): |
| yield 4 |
|
|
| @pytest.fixture(params=[None, 'silu']) |
| def activation(self, request): |
| yield request.param |
|
|
| @pytest.fixture |
| def weight(self, hidden_size, width, device): |
| yield torch.randn(hidden_size, width, device=device) |
|
|
| @pytest.fixture |
| def bias(self, hidden_size, device): |
| yield torch.randn(hidden_size, device=device) |
|
|
| def _ref_conv1d(self, x_seq, weight, bias, init_state, activation): |
| """Reference: prepend init_state and run F.conv1d per sequence.""" |
| dim = weight.shape[0] |
| x_cat = torch.cat([init_state, x_seq], dim=-1).float() |
| w = weight.float().unsqueeze(1) |
| b = bias.float() if bias is not None else None |
| out = torch.nn.functional.conv1d(x_cat, w, b, padding=0, groups=dim) |
| out = out[..., :x_seq.shape[-1]] |
| if activation in ('silu', 'swish'): |
| out = torch.nn.functional.silu(out) |
| return out.to(x_seq.dtype) |
|
|
| def test_single_seq(self, hidden_size, width, weight, bias, activation, device): |
| """Single sequence with non-zero initial states.""" |
| seqlen = 128 |
| x = torch.randn(1, hidden_size, seqlen, device=device).transpose(1, 2).contiguous().transpose(1, 2) |
| seq_idx = torch.zeros(1, seqlen, dtype=torch.int32, device=device) |
| init_states = torch.randn(1, hidden_size, width - 1, device=device, dtype=x.dtype) |
|
|
| from lmdeploy.pytorch.kernels.cuda.causal_conv1d import causal_conv1d_fn |
|
|
| out = causal_conv1d_fn(x=x, weight=weight, bias=bias, seq_idx=seq_idx, initial_states=init_states, |
| activation=activation) |
| out_ref = self._ref_conv1d(x, weight, bias, init_states, activation) |
| torch.testing.assert_close(out, out_ref, rtol=1e-3, atol=1e-3) |
|
|
| def test_multi_seq(self, hidden_size, width, weight, bias, activation, device): |
| """Multiple sequences packed, each with its own initial state.""" |
| seqlens = [100, 200, 50] |
| n_seqs = len(seqlens) |
|
|
| x_parts = [torch.randn(1, hidden_size, sl, device=device) for sl in seqlens] |
| |
| x_packed = torch.cat(x_parts, dim=-1).transpose(1, 2).contiguous().transpose(1, 2) |
|
|
| seq_idx_parts = [torch.full((sl, ), i, dtype=torch.int32, device=device) for i, sl in enumerate(seqlens)] |
| seq_idx = torch.cat(seq_idx_parts).unsqueeze(0) |
|
|
| init_states = torch.randn(n_seqs, hidden_size, width - 1, device=device, dtype=x_packed.dtype) |
|
|
| from lmdeploy.pytorch.kernels.cuda.causal_conv1d import causal_conv1d_fn |
|
|
| out = causal_conv1d_fn(x=x_packed, weight=weight, bias=bias, seq_idx=seq_idx, initial_states=init_states, |
| activation=activation) |
|
|
| |
| offset = 0 |
| for i, sl in enumerate(seqlens): |
| x_seq = x_packed[:, :, offset:offset + sl] |
| ref = self._ref_conv1d(x_seq, weight, bias, init_states[i:i + 1], activation) |
| torch.testing.assert_close(out[:, :, offset:offset + sl], ref, rtol=1e-3, atol=1e-3) |
| offset += sl |
|
|
| def test_zero_init_matches_no_init(self, hidden_size, width, weight, bias, activation, device): |
| """Zero initial_states should produce the same result as no |
| initial_states.""" |
| seqlens = [80, 120] |
| n_seqs = len(seqlens) |
|
|
| x_packed = torch.randn(1, hidden_size, sum(seqlens), device=device).transpose(1, 2).contiguous().transpose( |
| 1, 2) |
| seq_idx_parts = [torch.full((sl, ), i, dtype=torch.int32, device=device) for i, sl in enumerate(seqlens)] |
| seq_idx = torch.cat(seq_idx_parts).unsqueeze(0) |
|
|
| zero_states = torch.zeros(n_seqs, hidden_size, width - 1, device=device, dtype=x_packed.dtype) |
|
|
| from lmdeploy.pytorch.kernels.cuda.causal_conv1d import causal_conv1d_fn |
|
|
| out_with_init = causal_conv1d_fn(x=x_packed, weight=weight, bias=bias, seq_idx=seq_idx, |
| initial_states=zero_states, activation=activation) |
| out_no_init = causal_conv1d_fn(x=x_packed, weight=weight, bias=bias, seq_idx=seq_idx, initial_states=None, |
| activation=activation) |
| torch.testing.assert_close(out_with_init, out_no_init, rtol=1e-3, atol=1e-3) |
|
|