main
py 90 lines 3.32 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 from types import SimpleNamespace
5
6 import pytest
7 import torch
8
9 from vllm.config import SpeechToTextConfig
10 from vllm.model_executor.models.cohere_asr import (
11 CohereAsrForConditionalGeneration,
12 RelPositionMultiHeadAttention,
13 )
14
15
16 class _ReferenceRelPositionMultiHeadAttention(RelPositionMultiHeadAttention):
17 """Relative attention implementation before score accumulation is fused."""
18
19 def forward(self, query, key, value, mask, pos_emb=None):
20 q, k, v = self.forward_qkv(query, key, value)
21 q = q.transpose(1, 2)
22
23 assert pos_emb is not None
24 n_batch_pos = pos_emb.size(0)
25 p = self.linear_pos(pos_emb).view(n_batch_pos, -1, self.h, self.d_k)
26 p = p.transpose(1, 2)
27
28 q_with_bias_u = (q + self.pos_bias_u).transpose(1, 2)
29 q_with_bias_v = (q + self.pos_bias_v).transpose(1, 2)
30
31 matrix_bd = torch.matmul(q_with_bias_v, p.transpose(-2, -1))
32 matrix_bd = self.rel_shift(matrix_bd)
33 matrix_ac = torch.matmul(q_with_bias_u, k.transpose(-2, -1))
34 matrix_bd = matrix_bd[:, :, :, : matrix_ac.size(-1)]
35 scores = (matrix_ac + matrix_bd) / self.s_d_k
36 return self.forward_attention(v, scores, mask)
37
38
39 @pytest.mark.parametrize(("query_len", "key_len"), [(2, 2), (3, 5)])
40 def test_relative_attention_fuses_score_accumulation(monkeypatch, query_len, key_len):
41 torch.manual_seed(7)
42 batch, heads, model_dim = 2, 4, 32
43 optimized = RelPositionMultiHeadAttention(heads, model_dim, None, None)
44 reference = _ReferenceRelPositionMultiHeadAttention(heads, model_dim, None, None)
45 reference.load_state_dict(optimized.state_dict())
46
47 query = torch.randn(batch, query_len, model_dim)
48 key = torch.randn(batch, key_len, model_dim)
49 value = torch.randn(batch, key_len, model_dim)
50 pos_emb = torch.randn(1, query_len + key_len - 1, model_dim)
51 expected = reference(query, key, value, None, pos_emb)
52
53 original_baddbmm = torch.baddbmm
54 calls = 0
55
56 def tracked_baddbmm(*args, **kwargs):
57 nonlocal calls
58 calls += 1
59 return original_baddbmm(*args, **kwargs)
60
61 monkeypatch.setattr(torch, "baddbmm", tracked_baddbmm)
62 actual = optimized(query, key, value, None, pos_emb)
63
64 torch.testing.assert_close(actual, expected)
65 assert calls == 1
66
67
68 @pytest.mark.parametrize(
69 ("audio_duration_s", "expected_tokens"),
70 # window_stride=0.01s @ 16kHz -> 160-sample hop; subsampling_factor=8.
71 # frames = floor(duration * 16000 / 160); tokens = ceil(frames / 8).
72 [(1.0, 13), (10.0, 125), (30.0, 375)],
73 )
74 def test_get_num_audio_tokens_streaming_estimate(audio_duration_s, expected_tokens):
75 """The duration-based estimate must convert ``window_stride`` (seconds) to a
76 sample hop and divide by the encoder subsampling factor. Values are pinned to
77 concrete numbers so a rounding regression is actually caught."""
78 model_config = SimpleNamespace(
79 hf_config=SimpleNamespace(
80 preprocessor={"window_stride": 0.01, "sample_rate": 16000},
81 encoder={"subsampling_factor": 8},
82 )
83 )
84 stt_config = SpeechToTextConfig(sample_rate=16000)
85
86 got = CohereAsrForConditionalGeneration.get_num_audio_tokens(
87 audio_duration_s, stt_config, model_config
88 )
89
90 assert got == expected_tokens