alignatt: enable model sharing by removing hooks and centralizing session state. Solves #282
Co-authored-by: Emmanuel Schmidbauer <eschmidbauer@gmail.com>
This commit is contained in:
parent
4e9f951551
commit
7faa21f95f
9 changed files with 525 additions and 373 deletions
|
|
@ -185,7 +185,6 @@ async def websocket_endpoint(websocket: WebSocket):
|
||||||
| `--init-prompt` | Initial prompt for the model | `None` |
|
| `--init-prompt` | Initial prompt for the model | `None` |
|
||||||
| `--static-init-prompt` | Static prompt that doesn't scroll | `None` |
|
| `--static-init-prompt` | Static prompt that doesn't scroll | `None` |
|
||||||
| `--max-context-tokens` | Maximum context tokens | `None` |
|
| `--max-context-tokens` | Maximum context tokens | `None` |
|
||||||
| `--preload-model-count` | Optional. Number of models to preload in memory to speed up loading (set up to the expected number of concurrent users) | `1` |
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,6 @@ class TranscriptionEngine:
|
||||||
"init_prompt": None,
|
"init_prompt": None,
|
||||||
"static_init_prompt": None,
|
"static_init_prompt": None,
|
||||||
"max_context_tokens": None,
|
"max_context_tokens": None,
|
||||||
"preload_model_count": 1,
|
|
||||||
}
|
}
|
||||||
simulstreaming_params = update_with_kwargs(simulstreaming_params, kwargs)
|
simulstreaming_params = update_with_kwargs(simulstreaming_params, kwargs)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -296,14 +296,6 @@ def parse_args():
|
||||||
help="Direct path to the SimulStreaming Whisper .pt model file. Overrides --model for SimulStreaming backend.",
|
help="Direct path to the SimulStreaming Whisper .pt model file. Overrides --model for SimulStreaming backend.",
|
||||||
)
|
)
|
||||||
|
|
||||||
simulstreaming_group.add_argument(
|
|
||||||
"--preload-model-count",
|
|
||||||
type=int,
|
|
||||||
default=1,
|
|
||||||
dest="preload_model_count",
|
|
||||||
help="Optional. Number of models to preload in memory to speed up loading (set up to the expected number of concurrent instances).",
|
|
||||||
)
|
|
||||||
|
|
||||||
simulstreaming_group.add_argument(
|
simulstreaming_group.add_argument(
|
||||||
"--nllb-backend",
|
"--nllb-backend",
|
||||||
type=str,
|
type=str,
|
||||||
|
|
|
||||||
|
|
@ -49,20 +49,19 @@ class SimulStreamingOnlineProcessor:
|
||||||
self.buffer = []
|
self.buffer = []
|
||||||
self.committed: List[ASRToken] = []
|
self.committed: List[ASRToken] = []
|
||||||
self.last_result_tokens: List[ASRToken] = []
|
self.last_result_tokens: List[ASRToken] = []
|
||||||
self.load_new_backend()
|
self.load_new_alignatt_instance()
|
||||||
|
|
||||||
#can be moved
|
|
||||||
if asr.tokenizer:
|
if asr.tokenizer:
|
||||||
self.model.tokenizer = asr.tokenizer
|
self.model.tokenizer = asr.tokenizer
|
||||||
|
|
||||||
def load_new_backend(self):
|
def load_new_alignatt_instance(self):
|
||||||
model = self.asr.get_new_model_instance()
|
"""Initialize AlignAtt decoder using the shared model."""
|
||||||
self.model = AlignAtt(
|
self.model = AlignAtt(
|
||||||
cfg=self.asr.cfg,
|
cfg=self.asr.cfg,
|
||||||
loaded_model=model,
|
loaded_model=self.asr.shared_model,
|
||||||
mlx_encoder=self.asr.mlx_encoder,
|
mlx_encoder=self.asr.mlx_encoder,
|
||||||
fw_encoder=self.asr.fw_encoder,
|
fw_encoder=self.asr.fw_encoder,
|
||||||
)
|
)
|
||||||
|
|
||||||
def start_silence(self):
|
def start_silence(self):
|
||||||
tokens, processed_upto = self.process_iter(is_last=True)
|
tokens, processed_upto = self.process_iter(is_last=True)
|
||||||
|
|
@ -70,7 +69,10 @@ class SimulStreamingOnlineProcessor:
|
||||||
|
|
||||||
def end_silence(self, silence_duration, offset):
|
def end_silence(self, silence_duration, offset):
|
||||||
"""
|
"""
|
||||||
If silences are > MIN_DURATION_REAL_SILENCE, we do a complete context clear. Otherwise, we just insert a small silence and shift the last_attend_frame
|
Handle silence period.
|
||||||
|
|
||||||
|
If silence > MIN_DURATION_REAL_SILENCE, do a complete context clear.
|
||||||
|
Otherwise, insert a small silence and shift the last_attend_frame.
|
||||||
"""
|
"""
|
||||||
self.end += silence_duration
|
self.end += silence_duration
|
||||||
long_silence = silence_duration >= MIN_DURATION_REAL_SILENCE
|
long_silence = silence_duration >= MIN_DURATION_REAL_SILENCE
|
||||||
|
|
@ -83,21 +85,20 @@ class SimulStreamingOnlineProcessor:
|
||||||
self.model.refresh_segment(complete=True)
|
self.model.refresh_segment(complete=True)
|
||||||
self.model.global_time_offset = silence_duration + offset
|
self.model.global_time_offset = silence_duration + offset
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def insert_audio_chunk(self, audio: np.ndarray, audio_stream_end_time):
|
def insert_audio_chunk(self, audio: np.ndarray, audio_stream_end_time):
|
||||||
"""Append an audio chunk to be processed by SimulStreaming."""
|
"""Append an audio chunk to be processed by SimulStreaming."""
|
||||||
|
|
||||||
# Convert numpy array to torch tensor
|
# Convert numpy array to torch tensor
|
||||||
audio_tensor = torch.from_numpy(audio).float()
|
audio_tensor = torch.from_numpy(audio).float()
|
||||||
self.end = audio_stream_end_time #Only to be aligned with what happens in whisperstreaming backend.
|
self.end = audio_stream_end_time # Aligned with whisperstreaming backend behavior
|
||||||
self.model.insert_audio(audio_tensor)
|
self.model.insert_audio(audio_tensor)
|
||||||
|
|
||||||
def new_speaker(self, change_speaker: ChangeSpeaker):
|
def new_speaker(self, change_speaker: ChangeSpeaker):
|
||||||
self.process_iter(is_last=True)
|
"""Handle speaker change event."""
|
||||||
self.model.refresh_segment(complete=True)
|
self.process_iter(is_last=True)
|
||||||
self.model.speaker = change_speaker.speaker
|
self.model.refresh_segment(complete=True)
|
||||||
self.global_time_offset = change_speaker.start
|
self.model.speaker = change_speaker.speaker
|
||||||
|
self.model.global_time_offset = change_speaker.start
|
||||||
|
|
||||||
def get_buffer(self):
|
def get_buffer(self):
|
||||||
concat_buffer = Transcript.from_tokens(tokens= self.buffer, sep='')
|
concat_buffer = Transcript.from_tokens(tokens= self.buffer, sep='')
|
||||||
|
|
@ -122,8 +123,6 @@ class SimulStreamingOnlineProcessor:
|
||||||
self.committed.extend(timestamped_words)
|
self.committed.extend(timestamped_words)
|
||||||
self.buffer = []
|
self.buffer = []
|
||||||
return timestamped_words, self.end
|
return timestamped_words, self.end
|
||||||
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(f"SimulStreaming processing error: {e}")
|
logger.exception(f"SimulStreaming processing error: {e}")
|
||||||
return [], self.end
|
return [], self.end
|
||||||
|
|
@ -139,12 +138,8 @@ class SimulStreamingOnlineProcessor:
|
||||||
logger.exception(f"SimulStreaming warmup failed: {e}")
|
logger.exception(f"SimulStreaming warmup failed: {e}")
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
# free the model and add a new model to stack.
|
|
||||||
# del self.model
|
|
||||||
gc.collect()
|
gc.collect()
|
||||||
torch.cuda.empty_cache()
|
torch.cuda.empty_cache()
|
||||||
# self.asr.new_model_to_stack()
|
|
||||||
self.model.remove_hooks()
|
|
||||||
|
|
||||||
class SimulStreamingASR():
|
class SimulStreamingASR():
|
||||||
"""SimulStreaming backend with AlignAtt policy."""
|
"""SimulStreaming backend with AlignAtt policy."""
|
||||||
|
|
@ -229,10 +224,7 @@ class SimulStreamingASR():
|
||||||
self.tokenizer = self.set_translate_task()
|
self.tokenizer = self.set_translate_task()
|
||||||
else:
|
else:
|
||||||
self.tokenizer = None
|
self.tokenizer = None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
self.mlx_encoder, self.fw_encoder = None, None
|
self.mlx_encoder, self.fw_encoder = None, None
|
||||||
if self.encoder_backend == "mlx-whisper":
|
if self.encoder_backend == "mlx-whisper":
|
||||||
print('Simulstreaming will use MLX whisper to increase encoding speed.')
|
print('Simulstreaming will use MLX whisper to increase encoding speed.')
|
||||||
|
|
@ -256,8 +248,7 @@ class SimulStreamingASR():
|
||||||
device='auto',
|
device='auto',
|
||||||
compute_type='auto',
|
compute_type='auto',
|
||||||
)
|
)
|
||||||
|
self.shared_model = self.load_model()
|
||||||
self.models = [self.load_model() for i in range(self.preload_model_count)]
|
|
||||||
|
|
||||||
|
|
||||||
def _resolve_encoder_backend(self, preferred_backend, compatible_whisper_mlx, compatible_faster_whisper):
|
def _resolve_encoder_backend(self, preferred_backend, compatible_whisper_mlx, compatible_faster_whisper):
|
||||||
|
|
@ -306,11 +297,11 @@ class SimulStreamingASR():
|
||||||
download_root=self.model_path,
|
download_root=self.model_path,
|
||||||
decoder_only=self.fast_encoder,
|
decoder_only=self.fast_encoder,
|
||||||
custom_alignment_heads=self.custom_alignment_heads
|
custom_alignment_heads=self.custom_alignment_heads
|
||||||
)
|
)
|
||||||
warmup_audio = load_file(self.warmup_file)
|
warmup_audio = load_file(self.warmup_file)
|
||||||
if warmup_audio is not None:
|
if warmup_audio is not None:
|
||||||
warmup_audio = torch.from_numpy(warmup_audio).float()
|
warmup_audio = torch.from_numpy(warmup_audio).float()
|
||||||
if self.fast_encoder:
|
if self.fast_encoder:
|
||||||
temp_model = AlignAtt(
|
temp_model = AlignAtt(
|
||||||
cfg=self.cfg,
|
cfg=self.cfg,
|
||||||
loaded_model=whisper_model,
|
loaded_model=whisper_model,
|
||||||
|
|
@ -318,27 +309,9 @@ class SimulStreamingASR():
|
||||||
fw_encoder=self.fw_encoder,
|
fw_encoder=self.fw_encoder,
|
||||||
)
|
)
|
||||||
temp_model.warmup(warmup_audio)
|
temp_model.warmup(warmup_audio)
|
||||||
temp_model.remove_hooks()
|
|
||||||
else:
|
else:
|
||||||
# For standard encoder, use the original transcribe warmup
|
|
||||||
warmup_audio = load_file(self.warmup_file)
|
|
||||||
whisper_model.transcribe(warmup_audio, language=self.lan if self.lan != 'auto' else None)
|
whisper_model.transcribe(warmup_audio, language=self.lan if self.lan != 'auto' else None)
|
||||||
return whisper_model
|
return whisper_model
|
||||||
|
|
||||||
def get_new_model_instance(self):
|
|
||||||
"""
|
|
||||||
SimulStreaming cannot share the same backend because it uses global forward hooks on the attention layers.
|
|
||||||
Therefore, each user requires a separate model instance, which can be memory-intensive. To maintain speed, we preload the models into memory.
|
|
||||||
"""
|
|
||||||
if len(self.models) == 0:
|
|
||||||
self.models.append(self.load_model())
|
|
||||||
new_model = self.models.pop()
|
|
||||||
return new_model
|
|
||||||
# self.models[0]
|
|
||||||
|
|
||||||
def new_model_to_stack(self):
|
|
||||||
self.models.append(self.load_model())
|
|
||||||
|
|
||||||
|
|
||||||
def set_translate_task(self):
|
def set_translate_task(self):
|
||||||
"""Set up translation task."""
|
"""Set up translation task."""
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,32 @@
|
||||||
|
from torch import Tensor
|
||||||
|
|
||||||
from whisperlivekit.whisper.decoding import PyTorchInference
|
from whisperlivekit.whisper.decoding import PyTorchInference
|
||||||
|
|
||||||
|
|
||||||
# extention of PyTorchInference for beam search
|
|
||||||
class BeamPyTorchInference(PyTorchInference):
|
class BeamPyTorchInference(PyTorchInference):
|
||||||
|
"""Extension of PyTorchInference for beam search with cross-attention support."""
|
||||||
|
|
||||||
def _kv_modules(self):
|
def _kv_cache_ids(self):
|
||||||
key_modules = [block.attn.key.cache_id for block in self.model.decoder.blocks]
|
"""Get cache_id strings for self-attention key/value modules."""
|
||||||
value_modules = [block.attn.value.cache_id for block in self.model.decoder.blocks]
|
key_ids = [block.attn.key_cache_id for block in self.model.decoder.blocks]
|
||||||
return key_modules + value_modules
|
value_ids = [block.attn.value_cache_id for block in self.model.decoder.blocks]
|
||||||
|
return key_ids + value_ids
|
||||||
|
|
||||||
def rearrange_kv_cache(self, source_indices):
|
def rearrange_kv_cache(self, source_indices):
|
||||||
if source_indices != list(range(len(source_indices))):
|
if source_indices != list(range(len(source_indices))):
|
||||||
for module_cache_id in self._kv_modules():
|
for cache_id in self._kv_cache_ids():
|
||||||
self.kv_cache[module_cache_id] = self.kv_cache[module_cache_id][source_indices].detach()
|
if cache_id in self.kv_cache:
|
||||||
from torch import Tensor
|
self.kv_cache[cache_id] = self.kv_cache[cache_id][source_indices].detach()
|
||||||
def logits(self, tokens: Tensor, audio_features: Tensor) -> Tensor:
|
|
||||||
return self.model.decoder(tokens, audio_features, kv_cache=self.kv_cache)
|
def logits(
|
||||||
|
self,
|
||||||
|
tokens: Tensor,
|
||||||
|
audio_features: Tensor,
|
||||||
|
return_cross_attn: bool = False,
|
||||||
|
):
|
||||||
|
"""Get logits, optionally returning cross-attention weights."""
|
||||||
|
return self.model.decoder(
|
||||||
|
tokens, audio_features,
|
||||||
|
kv_cache=self.kv_cache,
|
||||||
|
return_cross_attn=return_cross_attn,
|
||||||
|
)
|
||||||
80
whisperlivekit/simul_whisper/decoder_state.py
Normal file
80
whisperlivekit/simul_whisper/decoder_state.py
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
import torch
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DecoderState:
|
||||||
|
|
||||||
|
kv_cache: Dict[str, torch.Tensor] = field(default_factory=dict)
|
||||||
|
|
||||||
|
tokenizer: Any = None
|
||||||
|
detected_language: Optional[str] = None
|
||||||
|
reset_tokenizer_to_auto_next_call: bool = False
|
||||||
|
|
||||||
|
tokens: List[torch.Tensor] = field(default_factory=list)
|
||||||
|
initial_tokens: Optional[torch.Tensor] = None
|
||||||
|
initial_token_length: int = 0
|
||||||
|
sot_index: int = 0
|
||||||
|
|
||||||
|
align_source: Dict[int, List[Tuple[int, int]]] = field(default_factory=dict)
|
||||||
|
num_align_heads: int = 0
|
||||||
|
|
||||||
|
segments: List[torch.Tensor] = field(default_factory=list)
|
||||||
|
|
||||||
|
context: Any = None
|
||||||
|
|
||||||
|
pending_incomplete_tokens: List[int] = field(default_factory=list)
|
||||||
|
|
||||||
|
global_time_offset: float = 0.0
|
||||||
|
cumulative_time_offset: float = 0.0
|
||||||
|
first_timestamp: Optional[float] = None
|
||||||
|
last_attend_frame: int = 0
|
||||||
|
|
||||||
|
speaker: int = -1
|
||||||
|
log_segments: int = 0
|
||||||
|
|
||||||
|
CIFLinear: Optional[torch.nn.Module] = None
|
||||||
|
always_fire: bool = False
|
||||||
|
never_fire: bool = False
|
||||||
|
|
||||||
|
suppress_tokens_fn: Any = None
|
||||||
|
|
||||||
|
token_decoder: Any = None
|
||||||
|
decoder_type: str = "greedy"
|
||||||
|
|
||||||
|
inference: Any = None
|
||||||
|
|
||||||
|
def clean_cache(self):
|
||||||
|
"""Clean the kv_cache after each inference step."""
|
||||||
|
self.kv_cache = {}
|
||||||
|
if self.decoder_type == "beam" and self.inference is not None:
|
||||||
|
self.inference.kv_cache = self.kv_cache
|
||||||
|
if self.token_decoder is not None:
|
||||||
|
self.token_decoder.reset()
|
||||||
|
|
||||||
|
def reset(self, rewind_threshold: int = 200):
|
||||||
|
"""
|
||||||
|
Reset transient state for a new segment.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
rewind_threshold: Value for resetting last_attend_frame
|
||||||
|
"""
|
||||||
|
self.last_attend_frame = -rewind_threshold
|
||||||
|
self.cumulative_time_offset = 0.0
|
||||||
|
self.pending_incomplete_tokens = []
|
||||||
|
self.log_segments += 1
|
||||||
|
|
||||||
|
def full_reset(self, rewind_threshold: int = 200):
|
||||||
|
"""
|
||||||
|
Full reset including audio segments and tokens.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
rewind_threshold: Value for resetting last_attend_frame
|
||||||
|
"""
|
||||||
|
self.reset(rewind_threshold)
|
||||||
|
self.segments = []
|
||||||
|
self.tokens = []
|
||||||
|
self.kv_cache = {}
|
||||||
|
self.first_timestamp = None
|
||||||
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from time import time
|
from time import time
|
||||||
|
from typing import List, Optional, Tuple
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import torch
|
import torch
|
||||||
|
|
@ -20,6 +21,7 @@ from whisperlivekit.whisper.timing import median_filter
|
||||||
from ..timed_objects import PUNCTUATION_MARKS
|
from ..timed_objects import PUNCTUATION_MARKS
|
||||||
from .beam import BeamPyTorchInference
|
from .beam import BeamPyTorchInference
|
||||||
from .config import AlignAttConfig
|
from .config import AlignAttConfig
|
||||||
|
from .decoder_state import DecoderState
|
||||||
from .eow_detection import fire_at_boundary, load_cif
|
from .eow_detection import fire_at_boundary, load_cif
|
||||||
from .token_buffer import TokenBuffer
|
from .token_buffer import TokenBuffer
|
||||||
|
|
||||||
|
|
@ -53,6 +55,30 @@ def load_coreml_encoder():
|
||||||
|
|
||||||
|
|
||||||
class AlignAtt:
|
class AlignAtt:
|
||||||
|
"""
|
||||||
|
Alignment-based Attention decoder for SimulStreaming.
|
||||||
|
|
||||||
|
This class is now hookless - the model can be shared across multiple
|
||||||
|
sessions, with each session maintaining its own DecoderState.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Property accessors for backward compatibility
|
||||||
|
@property
|
||||||
|
def speaker(self):
|
||||||
|
return self.state.speaker
|
||||||
|
|
||||||
|
@speaker.setter
|
||||||
|
def speaker(self, value):
|
||||||
|
self.state.speaker = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def global_time_offset(self):
|
||||||
|
return self.state.global_time_offset
|
||||||
|
|
||||||
|
@global_time_offset.setter
|
||||||
|
def global_time_offset(self, value):
|
||||||
|
self.state.global_time_offset = value
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
cfg: AlignAttConfig,
|
cfg: AlignAttConfig,
|
||||||
|
|
@ -60,8 +86,7 @@ class AlignAtt:
|
||||||
mlx_encoder=None,
|
mlx_encoder=None,
|
||||||
fw_encoder=None,
|
fw_encoder=None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.log_segments = 0
|
# Shared model reference (can be shared across sessions)
|
||||||
|
|
||||||
self.model = loaded_model
|
self.model = loaded_model
|
||||||
self.mlx_encoder = mlx_encoder
|
self.mlx_encoder = mlx_encoder
|
||||||
self.fw_encoder = fw_encoder
|
self.fw_encoder = fw_encoder
|
||||||
|
|
@ -75,119 +100,89 @@ class AlignAtt:
|
||||||
self.device = 'cuda' if torch.cuda.is_available() else 'cpu'
|
self.device = 'cuda' if torch.cuda.is_available() else 'cpu'
|
||||||
|
|
||||||
logger.info(f"Model dimensions: {self.model.dims}")
|
logger.info(f"Model dimensions: {self.model.dims}")
|
||||||
self.speaker = -1
|
|
||||||
self.decode_options = DecodingOptions(
|
self.decode_options = DecodingOptions(
|
||||||
language = cfg.language,
|
language=cfg.language,
|
||||||
without_timestamps = True,
|
without_timestamps=True,
|
||||||
task=cfg.task
|
task=cfg.task
|
||||||
)
|
)
|
||||||
self.tokenizer_is_multilingual = cfg.tokenizer_is_multilingual
|
self.tokenizer_is_multilingual = cfg.tokenizer_is_multilingual
|
||||||
self.create_tokenizer(cfg.language if cfg.language != "auto" else None)
|
|
||||||
# self.create_tokenizer('en')
|
|
||||||
self.detected_language = cfg.language if cfg.language != "auto" else None
|
|
||||||
self.global_time_offset = 0.0
|
|
||||||
self.reset_tokenizer_to_auto_next_call = False
|
|
||||||
|
|
||||||
self.max_text_len = self.model.dims.n_text_ctx
|
self.max_text_len = self.model.dims.n_text_ctx
|
||||||
self.num_decoder_layers = len(self.model.decoder.blocks)
|
self.num_decoder_layers = len(self.model.decoder.blocks)
|
||||||
self.cfg = cfg
|
self.cfg = cfg
|
||||||
self.l_hooks = []
|
|
||||||
|
|
||||||
# model to detect end-of-word boundary at the end of the segment
|
|
||||||
self.CIFLinear, self.always_fire, self.never_fire = load_cif(cfg,
|
|
||||||
n_audio_state=self.model.dims.n_audio_state,
|
|
||||||
device=self.model.device)
|
|
||||||
|
|
||||||
# install hooks to access encoder-decoder attention
|
|
||||||
self.dec_attns = []
|
|
||||||
def layer_hook(module, net_input, net_output):
|
|
||||||
# net_output[1]: B*num_head*token_len*audio_len
|
|
||||||
t = F.softmax(net_output[1], dim=-1)
|
|
||||||
self.dec_attns.append(t.squeeze(0))
|
|
||||||
for b in self.model.decoder.blocks:
|
|
||||||
hook = b.cross_attn.register_forward_hook(layer_hook)
|
|
||||||
self.l_hooks.append(hook)
|
|
||||||
|
|
||||||
self.kv_cache = {}
|
|
||||||
def kv_hook(module: torch.nn.Linear, _, net_output: torch.Tensor):
|
|
||||||
if module.cache_id not in self.kv_cache or net_output.shape[1] > self.max_text_len:
|
|
||||||
# save as-is, for the first token or cross attention
|
|
||||||
self.kv_cache[module.cache_id] = net_output
|
|
||||||
else:
|
|
||||||
x = self.kv_cache[module.cache_id]
|
|
||||||
self.kv_cache[module.cache_id] = torch.cat([x, net_output], dim=1).detach()
|
|
||||||
return self.kv_cache[module.cache_id]
|
|
||||||
|
|
||||||
for i,b in enumerate(self.model.decoder.blocks):
|
|
||||||
hooks = [
|
|
||||||
b.attn.key.register_forward_hook(kv_hook),
|
|
||||||
b.attn.value.register_forward_hook(kv_hook),
|
|
||||||
b.cross_attn.key.register_forward_hook(kv_hook),
|
|
||||||
b.cross_attn.value.register_forward_hook(kv_hook),
|
|
||||||
]
|
|
||||||
self.l_hooks.extend(hooks)
|
|
||||||
|
|
||||||
self.align_source = {}
|
|
||||||
self.num_align_heads = 0
|
|
||||||
for layer_rank, head_id in self.model.alignment_heads.indices().T:
|
|
||||||
layer_rank = layer_rank.item()
|
|
||||||
heads = self.align_source.get(layer_rank, [])
|
|
||||||
heads.append((self.num_align_heads, head_id.item()))
|
|
||||||
self.align_source[layer_rank] = heads
|
|
||||||
self.num_align_heads += 1
|
|
||||||
|
|
||||||
|
|
||||||
# tokens to be suppressed from decoding, to prevent hallucinations
|
|
||||||
suppress_tokens = [
|
|
||||||
self.tokenizer.transcribe,
|
|
||||||
self.tokenizer.translate,
|
|
||||||
self.tokenizer.sot,
|
|
||||||
self.tokenizer.sot_prev,
|
|
||||||
self.tokenizer.sot_lm,
|
|
||||||
# self.tokenizer.eot
|
|
||||||
self.tokenizer.no_timestamps, # added by DM
|
|
||||||
] + list(self.tokenizer.all_language_tokens) # added by DM
|
|
||||||
if self.tokenizer.no_speech is not None:
|
|
||||||
suppress_tokens.append(self.tokenizer.no_speech)
|
|
||||||
suppress_tokens = tuple(sorted(set(suppress_tokens)))
|
|
||||||
logger.debug(f"Suppress tokens: {suppress_tokens}")
|
|
||||||
sup_tokens = SuppressTokens(suppress_tokens)
|
|
||||||
self.suppress_tokens = lambda logits: sup_tokens.apply(logits, None)
|
|
||||||
# blank tokens are suppresed for new segments near the line 334
|
|
||||||
|
|
||||||
# it's going to be regenerated after lang id
|
|
||||||
self.segments = []
|
|
||||||
self.init_tokens()
|
|
||||||
|
|
||||||
self.last_attend_frame = -self.cfg.rewind_threshold
|
|
||||||
self.cumulative_time_offset = 0.0
|
|
||||||
self.first_timestamp = None
|
|
||||||
|
|
||||||
if self.cfg.max_context_tokens is None:
|
if self.cfg.max_context_tokens is None:
|
||||||
self.max_context_tokens = self.max_text_len
|
self.max_context_tokens = self.max_text_len
|
||||||
else:
|
else:
|
||||||
self.max_context_tokens = self.cfg.max_context_tokens
|
self.max_context_tokens = self.cfg.max_context_tokens
|
||||||
|
|
||||||
|
# Initialize per-session state
|
||||||
|
self.state = DecoderState()
|
||||||
|
self._init_state(cfg)
|
||||||
|
|
||||||
|
def _init_state(self, cfg: AlignAttConfig):
|
||||||
|
"""Initialize the per-session decoder state."""
|
||||||
|
# Create tokenizer
|
||||||
|
self.create_tokenizer(cfg.language if cfg.language != "auto" else None)
|
||||||
|
self.state.tokenizer = self.tokenizer
|
||||||
|
self.state.detected_language = cfg.language if cfg.language != "auto" else None
|
||||||
|
|
||||||
|
# Timing state
|
||||||
|
self.state.global_time_offset = 0.0
|
||||||
|
self.state.last_attend_frame = -cfg.rewind_threshold
|
||||||
|
self.state.speaker = -1
|
||||||
|
|
||||||
|
# CIF helpers for end-of-word boundary detection
|
||||||
|
self.state.CIFLinear, self.state.always_fire, self.state.never_fire = load_cif(
|
||||||
|
cfg,
|
||||||
|
n_audio_state=self.model.dims.n_audio_state,
|
||||||
|
device=self.model.device
|
||||||
|
)
|
||||||
|
|
||||||
|
# Build alignment source mapping from model's alignment_heads
|
||||||
|
self.state.align_source = {}
|
||||||
|
self.state.num_align_heads = 0
|
||||||
|
for layer_rank, head_id in self.model.alignment_heads.indices().T:
|
||||||
|
layer_rank = layer_rank.item()
|
||||||
|
heads = self.state.align_source.get(layer_rank, [])
|
||||||
|
heads.append((self.state.num_align_heads, head_id.item()))
|
||||||
|
self.state.align_source[layer_rank] = heads
|
||||||
|
self.state.num_align_heads += 1
|
||||||
|
|
||||||
|
# Build suppress tokens function
|
||||||
|
suppress_tokens = [
|
||||||
|
self.tokenizer.transcribe,
|
||||||
|
self.tokenizer.translate,
|
||||||
|
self.tokenizer.sot,
|
||||||
|
self.tokenizer.sot_prev,
|
||||||
|
self.tokenizer.sot_lm,
|
||||||
|
self.tokenizer.no_timestamps,
|
||||||
|
] + list(self.tokenizer.all_language_tokens)
|
||||||
|
if self.tokenizer.no_speech is not None:
|
||||||
|
suppress_tokens.append(self.tokenizer.no_speech)
|
||||||
|
suppress_tokens = tuple(sorted(set(suppress_tokens)))
|
||||||
|
logger.debug(f"Suppress tokens: {suppress_tokens}")
|
||||||
|
sup_tokens = SuppressTokens(suppress_tokens)
|
||||||
|
self.state.suppress_tokens_fn = lambda logits: sup_tokens.apply(logits, None)
|
||||||
|
|
||||||
|
# Initialize tokens
|
||||||
|
self.init_tokens()
|
||||||
self.init_context()
|
self.init_context()
|
||||||
|
|
||||||
# decoder type: greedy or beam
|
# Set up decoder type
|
||||||
|
self.state.decoder_type = cfg.decoder_type
|
||||||
if cfg.decoder_type == "greedy":
|
if cfg.decoder_type == "greedy":
|
||||||
logger.info("Using greedy decoder")
|
logger.info("Using greedy decoder")
|
||||||
self.token_decoder = GreedyDecoder(0.0, self.tokenizer.eot)
|
self.state.token_decoder = GreedyDecoder(0.0, self.tokenizer.eot)
|
||||||
self.decoder_type = "greedy"
|
|
||||||
|
|
||||||
elif cfg.decoder_type == "beam":
|
elif cfg.decoder_type == "beam":
|
||||||
self.decoder_type = "beam"
|
logger.info("Using beam decoder")
|
||||||
self.inference = BeamPyTorchInference(self.model, self.initial_token_length)
|
self.state.inference = BeamPyTorchInference(self.model, self.state.initial_token_length)
|
||||||
self.inference.kv_cache = self.kv_cache
|
self.state.inference.kv_cache = self.state.kv_cache
|
||||||
|
self.state.token_decoder = BeamSearchDecoder(
|
||||||
self.token_decoder = BeamSearchDecoder(inference=self.inference, eot=self.tokenizer.eot, beam_size=cfg.beam_size)
|
inference=self.state.inference,
|
||||||
|
eot=self.tokenizer.eot,
|
||||||
# Tokens to carry over to next chunk for incomplete UTF-8 characters
|
beam_size=cfg.beam_size
|
||||||
self.pending_incomplete_tokens = []
|
)
|
||||||
|
|
||||||
def remove_hooks(self):
|
|
||||||
for hook in self.l_hooks:
|
|
||||||
hook.remove()
|
|
||||||
|
|
||||||
def warmup(self, audio):
|
def warmup(self, audio):
|
||||||
try:
|
try:
|
||||||
|
|
@ -205,96 +200,100 @@ class AlignAtt:
|
||||||
num_languages=self.model.num_languages,
|
num_languages=self.model.num_languages,
|
||||||
task=self.decode_options.task
|
task=self.decode_options.task
|
||||||
)
|
)
|
||||||
|
self.state.tokenizer = self.tokenizer
|
||||||
|
|
||||||
def init_context(self):
|
def init_context(self):
|
||||||
kw = {'tokenizer': self.tokenizer,
|
kw = {'tokenizer': self.tokenizer,
|
||||||
'device': self.model.device,
|
'device': self.model.device,
|
||||||
'prefix_token_ids': [self.tokenizer.sot_prev]}
|
'prefix_token_ids': [self.tokenizer.sot_prev]}
|
||||||
self.context = TokenBuffer.empty(**kw)
|
self.state.context = TokenBuffer.empty(**kw)
|
||||||
if self.cfg.static_init_prompt is not None:
|
if self.cfg.static_init_prompt is not None:
|
||||||
self.context = TokenBuffer.from_text(self.cfg.static_init_prompt, **kw)
|
self.state.context = TokenBuffer.from_text(self.cfg.static_init_prompt, **kw)
|
||||||
if self.cfg.init_prompt is not None:
|
if self.cfg.init_prompt is not None:
|
||||||
self.context.text += self.cfg.init_prompt
|
self.state.context.text += self.cfg.init_prompt
|
||||||
|
|
||||||
def init_tokens(self):
|
def init_tokens(self):
|
||||||
logger.debug(f"init tokens, {len(self.segments)}")
|
logger.debug(f"init tokens, {len(self.state.segments)}")
|
||||||
# init tokens (mandatory prompt)
|
# init tokens (mandatory prompt)
|
||||||
self.initial_tokens = torch.tensor(
|
self.state.initial_tokens = torch.tensor(
|
||||||
self.tokenizer.sot_sequence_including_notimestamps,
|
self.tokenizer.sot_sequence_including_notimestamps,
|
||||||
dtype=torch.long,
|
dtype=torch.long,
|
||||||
device=self.model.device).unsqueeze(0)
|
device=self.model.device).unsqueeze(0)
|
||||||
self.initial_token_length = self.initial_tokens.shape[1]
|
self.state.initial_token_length = self.state.initial_tokens.shape[1]
|
||||||
self.sot_index = self.tokenizer.sot_sequence.index(self.tokenizer.sot)
|
self.state.sot_index = self.tokenizer.sot_sequence.index(self.tokenizer.sot)
|
||||||
# self.segments = []
|
logger.debug(f"init tokens after, {len(self.state.segments)}")
|
||||||
logger.debug(f"init tokens after, {len(self.segments)}")
|
self.state.tokens = [self.state.initial_tokens]
|
||||||
self.tokens = [self.initial_tokens]
|
|
||||||
|
|
||||||
def trim_context(self):
|
def trim_context(self):
|
||||||
logger.info("Trimming context")
|
logger.info("Trimming context")
|
||||||
c = len(self.context.as_token_ids()) - len(self.context.prefix_token_ids)
|
c = len(self.state.context.as_token_ids()) - len(self.state.context.prefix_token_ids)
|
||||||
# logger.debug(f"c= {len(self.context.as_token_ids())}, {len(self.context.prefix_token_ids)}")
|
logger.info(f"Context text: {self.state.context.as_text()}")
|
||||||
logger.info(f"Context text: {self.context.as_text()}")
|
l = sum(t.shape[1] for t in self.state.tokens) + c
|
||||||
# logger.debug(f"Context tensor: {self.context.as_tensor()}")
|
|
||||||
l = sum(t.shape[1] for t in self.tokens) + c
|
|
||||||
# logger.debug(f"len {l}, c {c}, max_context_tokens {self.max_context_tokens}")
|
|
||||||
if self.cfg.static_init_prompt is None:
|
if self.cfg.static_init_prompt is None:
|
||||||
after = 0
|
after = 0
|
||||||
else:
|
else:
|
||||||
after = len(self.cfg.static_init_prompt)
|
after = len(self.cfg.static_init_prompt)
|
||||||
# logger.debug(f"len {l}, c {c}, max_context_tokens {self.max_context_tokens}")
|
|
||||||
while c > self.max_context_tokens or l > self.max_text_len - 20:
|
while c > self.max_context_tokens or l > self.max_text_len - 20:
|
||||||
t = self.context.trim_words(after=after)
|
t = self.state.context.trim_words(after=after)
|
||||||
l -= t
|
l -= t
|
||||||
c -= t
|
c -= t
|
||||||
logger.debug(f"len {l}, c {c}, max_context_tokens {self.max_context_tokens}")
|
logger.debug(f"len {l}, c {c}, max_context_tokens {self.max_context_tokens}")
|
||||||
if t == 0:
|
if t == 0:
|
||||||
break
|
break
|
||||||
# logger.debug(f"len {l}, c {c}, max_context_tokens {self.max_context_tokens}")
|
logger.info(f"Context after trim: {self.state.context.text} (len: {l})")
|
||||||
logger.info(f"Context after trim: {self.context.text} (len: {l})")
|
|
||||||
|
|
||||||
|
|
||||||
def logits(self, tokens: torch.Tensor, audio_features: torch.Tensor) -> torch.Tensor:
|
def logits(
|
||||||
if self.cfg.decoder_type == "greedy":
|
self,
|
||||||
logit = self.model.decoder(tokens, audio_features, kv_cache=self.kv_cache)
|
tokens: torch.Tensor,
|
||||||
|
audio_features: torch.Tensor,
|
||||||
|
return_cross_attn: bool = False
|
||||||
|
):
|
||||||
|
"""Get logits from decoder, optionally returning cross-attention weights."""
|
||||||
|
if self.state.decoder_type == "greedy":
|
||||||
|
return self.model.decoder(
|
||||||
|
tokens, audio_features,
|
||||||
|
kv_cache=self.state.kv_cache,
|
||||||
|
return_cross_attn=return_cross_attn
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logger.debug(f"Logits shape: {tokens.shape}")
|
logger.debug(f"Logits shape: {tokens.shape}")
|
||||||
logit = self.inference.logits(tokens, audio_features)
|
return self.state.inference.logits(
|
||||||
return logit
|
tokens, audio_features,
|
||||||
|
return_cross_attn=return_cross_attn
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def refresh_segment(self, complete=False):
|
def refresh_segment(self, complete=False):
|
||||||
|
|
||||||
logger.debug("Refreshing segment:")
|
logger.debug("Refreshing segment:")
|
||||||
self.init_tokens()
|
self.init_tokens()
|
||||||
self.last_attend_frame = -self.cfg.rewind_threshold
|
self.state.last_attend_frame = -self.cfg.rewind_threshold
|
||||||
# self.detected_language = None
|
self.state.cumulative_time_offset = 0.0
|
||||||
self.cumulative_time_offset = 0.0
|
|
||||||
self.init_context()
|
self.init_context()
|
||||||
logger.debug(f"Context: {self.context}")
|
logger.debug(f"Context: {self.state.context}")
|
||||||
if not complete and len(self.segments) > 2:
|
if not complete and len(self.state.segments) > 2:
|
||||||
self.segments = self.segments[-2:]
|
self.state.segments = self.state.segments[-2:]
|
||||||
else:
|
else:
|
||||||
logger.debug("removing all segments.")
|
logger.debug("removing all segments.")
|
||||||
self.segments = []
|
self.state.segments = []
|
||||||
self.log_segments += 1
|
self.state.log_segments += 1
|
||||||
|
self.state.pending_incomplete_tokens = []
|
||||||
self.pending_incomplete_tokens = []
|
|
||||||
|
|
||||||
def fire_at_boundary(self, chunked_encoder_feature: torch.Tensor):
|
def fire_at_boundary(self, chunked_encoder_feature: torch.Tensor):
|
||||||
if self.always_fire: return True
|
if self.state.always_fire:
|
||||||
if self.never_fire: return False
|
return True
|
||||||
return fire_at_boundary(chunked_encoder_feature, self.CIFLinear)
|
if self.state.never_fire:
|
||||||
|
return False
|
||||||
|
return fire_at_boundary(chunked_encoder_feature, self.state.CIFLinear)
|
||||||
|
|
||||||
def _current_tokens(self):
|
def _current_tokens(self):
|
||||||
|
toks = self.state.tokens
|
||||||
toks = self.tokens
|
|
||||||
# very first infer: duplicate start of seq to beam_size
|
# very first infer: duplicate start of seq to beam_size
|
||||||
if toks[0].shape[0] == 1:
|
if toks[0].shape[0] == 1:
|
||||||
toks[0] = toks[0].repeat_interleave(self.cfg.beam_size,dim=0)
|
toks[0] = toks[0].repeat_interleave(self.cfg.beam_size, dim=0)
|
||||||
|
|
||||||
if not self.context.is_empty():
|
if not self.state.context.is_empty():
|
||||||
context_toks = self.context.as_tensor_beam(self.cfg.beam_size, device=self.model.device)
|
context_toks = self.state.context.as_tensor_beam(self.cfg.beam_size, device=self.model.device)
|
||||||
toks = [context_toks] + toks
|
toks = [context_toks] + toks
|
||||||
|
|
||||||
# make it one tensor
|
# make it one tensor
|
||||||
|
|
@ -314,7 +313,7 @@ class AlignAtt:
|
||||||
### audio buffer
|
### audio buffer
|
||||||
|
|
||||||
def segments_len(self):
|
def segments_len(self):
|
||||||
segments_len = sum(s.shape[0] for s in self.segments) / 16000
|
segments_len = sum(s.shape[0] for s in self.state.segments) / 16000
|
||||||
return segments_len
|
return segments_len
|
||||||
|
|
||||||
def _apply_minseglen(self):
|
def _apply_minseglen(self):
|
||||||
|
|
@ -327,42 +326,36 @@ class AlignAtt:
|
||||||
|
|
||||||
def insert_audio(self, segment=None):
|
def insert_audio(self, segment=None):
|
||||||
if segment is not None:
|
if segment is not None:
|
||||||
self.segments.append(segment)
|
self.state.segments.append(segment)
|
||||||
|
|
||||||
removed_len = 0
|
removed_len = 0
|
||||||
# len of audio is bigger than buffer_len. Going to remove the first segment
|
# len of audio is bigger than buffer_len. Going to remove the first segment
|
||||||
segments_len = self.segments_len()
|
segments_len = self.segments_len()
|
||||||
while len(self.segments) > 1 and segments_len > self.cfg.audio_max_len:
|
while len(self.state.segments) > 1 and segments_len > self.cfg.audio_max_len:
|
||||||
removed_len = self.segments[0].shape[0] / 16000
|
removed_len = self.state.segments[0].shape[0] / 16000
|
||||||
segments_len -= removed_len
|
segments_len -= removed_len
|
||||||
self.last_attend_frame -= int(TOKENS_PER_SECOND*removed_len)
|
self.state.last_attend_frame -= int(TOKENS_PER_SECOND * removed_len)
|
||||||
self.cumulative_time_offset += removed_len # Track cumulative time removed
|
self.state.cumulative_time_offset += removed_len # Track cumulative time removed
|
||||||
self.segments = self.segments[1:]
|
self.state.segments = self.state.segments[1:]
|
||||||
logger.debug(f"remove segments: {len(self.segments)} {len(self.tokens)}, cumulative offset: {self.cumulative_time_offset:.2f}s")
|
logger.debug(f"remove segments: {len(self.state.segments)} {len(self.state.tokens)}, cumulative offset: {self.state.cumulative_time_offset:.2f}s")
|
||||||
if len(self.tokens) > 1:
|
if len(self.state.tokens) > 1:
|
||||||
self.context.append_token_ids(self.tokens[1][0,:].tolist())
|
self.state.context.append_token_ids(self.state.tokens[1][0, :].tolist())
|
||||||
self.tokens = [self.initial_tokens] + self.tokens[2:]
|
self.state.tokens = [self.state.initial_tokens] + self.state.tokens[2:]
|
||||||
return removed_len
|
return removed_len
|
||||||
|
|
||||||
def _clean_cache(self):
|
def _clean_cache(self):
|
||||||
'''clean the cache that stores the attention matrices and kv_cache.
|
"""Clean the kv_cache after each inference step."""
|
||||||
It must be called every time after generation with the model.'''
|
self.state.clean_cache()
|
||||||
# cleaning cache
|
|
||||||
self.dec_attns = []
|
|
||||||
self.kv_cache = {}
|
|
||||||
if self.decoder_type == "beam":
|
|
||||||
self.inference.kv_cache = self.kv_cache
|
|
||||||
self.token_decoder.reset()
|
|
||||||
|
|
||||||
@torch.no_grad()
|
@torch.no_grad()
|
||||||
def lang_id(self, encoder_features):
|
def lang_id(self, encoder_features):
|
||||||
"""Language detection from encoder features.
|
"""Language detection from encoder features.
|
||||||
This code is trimmed and copy-pasted from whisper.decoding.detect_language .
|
This code is trimmed and copy-pasted from whisper.decoding.detect_language.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# forward pass using a single token, startoftranscript
|
# forward pass using a single token, startoftranscript
|
||||||
n_audio = encoder_features.shape[0]
|
n_audio = encoder_features.shape[0]
|
||||||
x = torch.tensor([[self.tokenizer.sot]] * n_audio).to(self.model.device) # [n_audio, 1]
|
x = torch.tensor([[self.tokenizer.sot]] * n_audio).to(self.model.device) # [n_audio, 1]
|
||||||
|
# Note: don't use kv_cache for language detection
|
||||||
logits = self.model.logits(x, encoder_features)[:, 0]
|
logits = self.model.logits(x, encoder_features)[:, 0]
|
||||||
|
|
||||||
# collect detected languages; suppress all non-language tokens
|
# collect detected languages; suppress all non-language tokens
|
||||||
|
|
@ -392,19 +385,19 @@ class AlignAtt:
|
||||||
@torch.no_grad()
|
@torch.no_grad()
|
||||||
def infer(self, is_last=False):
|
def infer(self, is_last=False):
|
||||||
new_segment = True
|
new_segment = True
|
||||||
if len(self.segments) == 0:
|
if len(self.state.segments) == 0:
|
||||||
logger.debug("No segments, nothing to do")
|
logger.debug("No segments, nothing to do")
|
||||||
return []
|
return []
|
||||||
if not self._apply_minseglen():
|
if not self._apply_minseglen():
|
||||||
logger.debug(f"applied minseglen {self.cfg.audio_min_len} > {self.segments_len()}.")
|
logger.debug(f"applied minseglen {self.cfg.audio_min_len} > {self.segments_len()}.")
|
||||||
input_segments = torch.cat(self.segments, dim=0)
|
input_segments = torch.cat(self.state.segments, dim=0)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# input_segments is concatenation of audio, it's one array
|
# input_segments is concatenation of audio, it's one array
|
||||||
if len(self.segments) > 1:
|
if len(self.state.segments) > 1:
|
||||||
input_segments = torch.cat(self.segments, dim=0)
|
input_segments = torch.cat(self.state.segments, dim=0)
|
||||||
else:
|
else:
|
||||||
input_segments = self.segments[0]
|
input_segments = self.state.segments[0]
|
||||||
|
|
||||||
beg_encode = time()
|
beg_encode = time()
|
||||||
if self.use_mlcore:
|
if self.use_mlcore:
|
||||||
|
|
@ -458,18 +451,18 @@ class AlignAtt:
|
||||||
end_encode = time()
|
end_encode = time()
|
||||||
# print('Encoder duration:', end_encode-beg_encode)
|
# print('Encoder duration:', end_encode-beg_encode)
|
||||||
|
|
||||||
if self.cfg.language == "auto" and self.detected_language is None and self.first_timestamp:
|
if self.cfg.language == "auto" and self.state.detected_language is None and self.state.first_timestamp:
|
||||||
seconds_since_start = self.segments_len() - self.first_timestamp
|
seconds_since_start = self.segments_len() - self.state.first_timestamp
|
||||||
if seconds_since_start >= 2.0:
|
if seconds_since_start >= 2.0:
|
||||||
language_tokens, language_probs = self.lang_id(encoder_feature)
|
language_tokens, language_probs = self.lang_id(encoder_feature)
|
||||||
top_lan, p = max(language_probs[0].items(), key=lambda x: x[1])
|
top_lan, p = max(language_probs[0].items(), key=lambda x: x[1])
|
||||||
print(f"Detected language: {top_lan} with p={p:.4f}")
|
print(f"Detected language: {top_lan} with p={p:.4f}")
|
||||||
self.create_tokenizer(top_lan)
|
self.create_tokenizer(top_lan)
|
||||||
self.last_attend_frame = -self.cfg.rewind_threshold
|
self.state.last_attend_frame = -self.cfg.rewind_threshold
|
||||||
self.cumulative_time_offset = 0.0
|
self.state.cumulative_time_offset = 0.0
|
||||||
self.init_tokens()
|
self.init_tokens()
|
||||||
self.init_context()
|
self.init_context()
|
||||||
self.detected_language = top_lan
|
self.state.detected_language = top_lan
|
||||||
logger.info(f"Tokenizer language: {self.tokenizer.language}, {self.tokenizer.sot_sequence_including_notimestamps}")
|
logger.info(f"Tokenizer language: {self.tokenizer.language}, {self.tokenizer.sot_sequence_including_notimestamps}")
|
||||||
|
|
||||||
self.trim_context()
|
self.trim_context()
|
||||||
|
|
@ -489,92 +482,80 @@ class AlignAtt:
|
||||||
|
|
||||||
l_absolute_timestamps = []
|
l_absolute_timestamps = []
|
||||||
|
|
||||||
while not completed and current_tokens.shape[1] < self.max_text_len: # bos is 3 tokens
|
accumulated_cross_attns = []
|
||||||
|
|
||||||
|
while not completed and current_tokens.shape[1] < self.max_text_len: # bos is 3 tokens
|
||||||
|
|
||||||
if new_segment:
|
if new_segment:
|
||||||
tokens_for_logits = current_tokens
|
tokens_for_logits = current_tokens
|
||||||
else:
|
else:
|
||||||
# only need to use the last token except in the first forward pass
|
# only need to use the last token except in the first forward pass
|
||||||
tokens_for_logits = current_tokens[:,-1:]
|
tokens_for_logits = current_tokens[:, -1:]
|
||||||
|
|
||||||
logits = self.logits(tokens_for_logits, encoder_feature) # B, len(tokens), token dict size
|
# Get logits and cross-attention weights from decoder
|
||||||
|
result = self.logits(tokens_for_logits, encoder_feature, return_cross_attn=True)
|
||||||
|
logits, cross_attns = result
|
||||||
|
|
||||||
|
# Accumulate cross-attention from this forward pass
|
||||||
|
accumulated_cross_attns.append(cross_attns)
|
||||||
|
|
||||||
if new_segment and self.tokenizer.no_speech is not None:
|
if new_segment and self.tokenizer.no_speech is not None:
|
||||||
probs_at_sot = logits[:, self.sot_index, :].float().softmax(dim=-1)
|
probs_at_sot = logits[:, self.state.sot_index, :].float().softmax(dim=-1)
|
||||||
no_speech_probs = probs_at_sot[:, self.tokenizer.no_speech].tolist()
|
no_speech_probs = probs_at_sot[:, self.tokenizer.no_speech].tolist()
|
||||||
if no_speech_probs[0] > self.cfg.nonspeech_prob:
|
if no_speech_probs[0] > self.cfg.nonspeech_prob:
|
||||||
logger.info("no speech, stop")
|
logger.info("no speech, stop")
|
||||||
break
|
break
|
||||||
|
|
||||||
logits = logits[:, -1, :] # logits for the last token
|
logits = logits[:, -1, :] # logits for the last token
|
||||||
|
|
||||||
# supress blank tokens only at the beginning of the segment
|
# suppress blank tokens only at the beginning of the segment
|
||||||
if new_segment:
|
if new_segment:
|
||||||
logits[:, self.tokenizer.encode(" ") + [self.tokenizer.eot]] = -np.inf
|
logits[:, self.tokenizer.encode(" ") + [self.tokenizer.eot]] = -np.inf
|
||||||
new_segment = False
|
new_segment = False
|
||||||
self.suppress_tokens(logits)
|
self.state.suppress_tokens_fn(logits)
|
||||||
current_tokens, completed = self.token_decoder.update(current_tokens, logits, sum_logprobs)
|
current_tokens, completed = self.state.token_decoder.update(current_tokens, logits, sum_logprobs)
|
||||||
|
|
||||||
logger.debug(f"Decoding completed: {completed}, sum_logprobs: {sum_logprobs.tolist()}, tokens: ")
|
logger.debug(f"Decoding completed: {completed}, sum_logprobs: {sum_logprobs.tolist()}, tokens: ")
|
||||||
self.debug_print_tokens(current_tokens)
|
self.debug_print_tokens(current_tokens)
|
||||||
|
|
||||||
attn_of_alignment_heads = [[] for _ in range(self.num_align_heads)]
|
# Process accumulated cross-attention weights for alignment
|
||||||
for i, attn_mat in enumerate(self.dec_attns):
|
attn_of_alignment_heads = self._process_cross_attention(accumulated_cross_attns, content_mel_len)
|
||||||
layer_rank = int(i % len(self.model.decoder.blocks))
|
|
||||||
align_heads_in_layer = self.align_source.get(layer_rank, [])
|
|
||||||
if len(align_heads_in_layer) == 0:
|
|
||||||
continue
|
|
||||||
for align_head_rank, head_id in align_heads_in_layer:
|
|
||||||
if self.cfg.beam_size == 1:
|
|
||||||
a = attn_mat[head_id, :, :]
|
|
||||||
a = a.unsqueeze(0)
|
|
||||||
else:
|
|
||||||
a = attn_mat[:, head_id, :, :]
|
|
||||||
attn_of_alignment_heads[align_head_rank].append(a)
|
|
||||||
tmp = []
|
|
||||||
for mat in attn_of_alignment_heads:
|
|
||||||
t = torch.cat(mat, dim=1)
|
|
||||||
tmp.append(t)
|
|
||||||
attn_of_alignment_heads = torch.stack(tmp, dim=1)
|
|
||||||
std, mean = torch.std_mean(attn_of_alignment_heads, dim=-2, keepdim=True, unbiased=False)
|
|
||||||
attn_of_alignment_heads = (attn_of_alignment_heads - mean) / std
|
|
||||||
attn_of_alignment_heads = median_filter(attn_of_alignment_heads, 7) # from whisper.timing
|
|
||||||
attn_of_alignment_heads = attn_of_alignment_heads.mean(dim=1)
|
|
||||||
attn_of_alignment_heads = attn_of_alignment_heads[:,:, :content_mel_len]
|
|
||||||
|
|
||||||
# for each beam, the most attended frame is:
|
# for each beam, the most attended frame is:
|
||||||
most_attended_frames = torch.argmax(attn_of_alignment_heads[:,-1,:], dim=-1)
|
most_attended_frames = torch.argmax(attn_of_alignment_heads[:, -1, :], dim=-1)
|
||||||
|
|
||||||
# Calculate absolute timestamps accounting for cumulative offset
|
# Calculate absolute timestamps accounting for cumulative offset
|
||||||
absolute_timestamps = [(frame * 0.02 + self.cumulative_time_offset) for frame in most_attended_frames.tolist()]
|
absolute_timestamps = [
|
||||||
|
(frame * 0.02 + self.state.cumulative_time_offset)
|
||||||
|
for frame in most_attended_frames.tolist()
|
||||||
|
]
|
||||||
|
|
||||||
logger.debug(str(most_attended_frames.tolist()) + " most att frames")
|
logger.debug(str(most_attended_frames.tolist()) + " most att frames")
|
||||||
logger.debug(f"Absolute timestamps: {absolute_timestamps} (offset: {self.cumulative_time_offset:.2f}s)")
|
logger.debug(f"Absolute timestamps: {absolute_timestamps} (offset: {self.state.cumulative_time_offset:.2f}s)")
|
||||||
|
|
||||||
most_attended_frame = most_attended_frames[0].item()
|
most_attended_frame = most_attended_frames[0].item()
|
||||||
l_absolute_timestamps.append(absolute_timestamps[0])
|
l_absolute_timestamps.append(absolute_timestamps[0])
|
||||||
|
|
||||||
logger.debug("current tokens" + str(current_tokens.shape))
|
logger.debug("current tokens" + str(current_tokens.shape))
|
||||||
if completed:
|
if completed:
|
||||||
# # stripping the last token, the eot
|
# stripping the last token, the eot
|
||||||
current_tokens = current_tokens[:, :-1]
|
current_tokens = current_tokens[:, :-1]
|
||||||
break
|
break
|
||||||
|
|
||||||
# for some rare cases where the attention fails
|
# for some rare cases where the attention fails
|
||||||
if not is_last and self.last_attend_frame - most_attended_frame > self.cfg.rewind_threshold:
|
if not is_last and self.state.last_attend_frame - most_attended_frame > self.cfg.rewind_threshold:
|
||||||
# TODO: check this
|
|
||||||
if current_tokens.shape[1] > 1 and current_tokens[0, -2] >= DEC_PAD:
|
if current_tokens.shape[1] > 1 and current_tokens[0, -2] >= DEC_PAD:
|
||||||
logger.debug("ommit rewinding from special tokens")
|
logger.debug("omit rewinding from special tokens")
|
||||||
self.last_attend_frame = most_attended_frame
|
self.state.last_attend_frame = most_attended_frame
|
||||||
else:
|
else:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"[rewind detected] current attention pos: {most_attended_frame}, "
|
f"[rewind detected] current attention pos: {most_attended_frame}, "
|
||||||
f"last attention pos: {self.last_attend_frame}; omit this segment")
|
f"last attention pos: {self.state.last_attend_frame}; omit this segment")
|
||||||
self.last_attend_frame = -self.cfg.rewind_threshold
|
self.state.last_attend_frame = -self.cfg.rewind_threshold
|
||||||
current_tokens = torch.cat(self.tokens, dim=1) if len(self.tokens) > 0 else self.tokens[0]
|
current_tokens = torch.cat(self.state.tokens, dim=1) if len(self.state.tokens) > 0 else self.state.tokens[0]
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
self.last_attend_frame = most_attended_frame
|
self.state.last_attend_frame = most_attended_frame
|
||||||
|
|
||||||
if content_mel_len - most_attended_frame <= (4 if is_last else self.cfg.frame_threshold):
|
if content_mel_len - most_attended_frame <= (4 if is_last else self.cfg.frame_threshold):
|
||||||
logger.debug(f"attention reaches the end: {most_attended_frame}/{content_mel_len}")
|
logger.debug(f"attention reaches the end: {most_attended_frame}/{content_mel_len}")
|
||||||
|
|
@ -594,12 +575,12 @@ class AlignAtt:
|
||||||
tokens_to_split = current_tokens[0, token_len_before_decoding:]
|
tokens_to_split = current_tokens[0, token_len_before_decoding:]
|
||||||
|
|
||||||
# Prepend pending tokens from previous chunk if any
|
# Prepend pending tokens from previous chunk if any
|
||||||
if self.pending_incomplete_tokens:
|
if self.state.pending_incomplete_tokens:
|
||||||
logger.debug(f"[UTF-8 Fix] Prepending {len(self.pending_incomplete_tokens)} pending tokens: {self.pending_incomplete_tokens}")
|
logger.debug(f"[UTF-8 Fix] Prepending {len(self.state.pending_incomplete_tokens)} pending tokens: {self.state.pending_incomplete_tokens}")
|
||||||
pending_tensor = torch.tensor(self.pending_incomplete_tokens, dtype=torch.long, device=self.device)
|
pending_tensor = torch.tensor(self.state.pending_incomplete_tokens, dtype=torch.long, device=self.device)
|
||||||
tokens_to_split = torch.cat([pending_tensor, tokens_to_split])
|
tokens_to_split = torch.cat([pending_tensor, tokens_to_split])
|
||||||
|
|
||||||
if fire_detected or is_last: #or punctuation_stop:
|
if fire_detected or is_last:
|
||||||
new_hypothesis = tokens_to_split.flatten().tolist()
|
new_hypothesis = tokens_to_split.flatten().tolist()
|
||||||
split_words, split_tokens = self.tokenizer.split_to_word_tokens(new_hypothesis)
|
split_words, split_tokens = self.tokenizer.split_to_word_tokens(new_hypothesis)
|
||||||
else:
|
else:
|
||||||
|
|
@ -610,20 +591,18 @@ class AlignAtt:
|
||||||
else:
|
else:
|
||||||
new_hypothesis = []
|
new_hypothesis = []
|
||||||
|
|
||||||
|
|
||||||
logger.debug(f"new_hypothesis: {new_hypothesis}")
|
logger.debug(f"new_hypothesis: {new_hypothesis}")
|
||||||
new_tokens = torch.tensor([new_hypothesis], dtype=torch.long).repeat_interleave(self.cfg.beam_size, dim=0).to(
|
new_tokens = torch.tensor([new_hypothesis], dtype=torch.long).repeat_interleave(self.cfg.beam_size, dim=0).to(
|
||||||
device=self.device,
|
device=self.device,
|
||||||
)
|
)
|
||||||
self.tokens.append(new_tokens)
|
self.state.tokens.append(new_tokens)
|
||||||
|
|
||||||
logger.info(f"Output: {self.tokenizer.decode(new_hypothesis)}")
|
logger.info(f"Output: {self.tokenizer.decode(new_hypothesis)}")
|
||||||
|
|
||||||
self._clean_cache()
|
self._clean_cache()
|
||||||
|
|
||||||
if len(l_absolute_timestamps) >=2 and self.first_timestamp is None:
|
if len(l_absolute_timestamps) >= 2 and self.state.first_timestamp is None:
|
||||||
self.first_timestamp = l_absolute_timestamps[0]
|
self.state.first_timestamp = l_absolute_timestamps[0]
|
||||||
|
|
||||||
|
|
||||||
timestamped_words = []
|
timestamped_words = []
|
||||||
timestamp_idx = 0
|
timestamp_idx = 0
|
||||||
|
|
@ -642,20 +621,85 @@ class AlignAtt:
|
||||||
timestamp_idx += len(word_tokens)
|
timestamp_idx += len(word_tokens)
|
||||||
|
|
||||||
timestamp_entry = ASRToken(
|
timestamp_entry = ASRToken(
|
||||||
start=round(current_timestamp, 2),
|
start=round(current_timestamp, 2),
|
||||||
end=round(current_timestamp + 0.1, 2),
|
end=round(current_timestamp + 0.1, 2),
|
||||||
text= word,
|
text=word,
|
||||||
speaker=self.speaker,
|
speaker=self.state.speaker,
|
||||||
detected_language=self.detected_language
|
detected_language=self.state.detected_language
|
||||||
).with_offset(
|
).with_offset(
|
||||||
self.global_time_offset
|
self.state.global_time_offset
|
||||||
)
|
)
|
||||||
timestamped_words.append(timestamp_entry)
|
timestamped_words.append(timestamp_entry)
|
||||||
|
|
||||||
# Hold incomplete tokens for next chunk
|
# Hold incomplete tokens for next chunk
|
||||||
self.pending_incomplete_tokens = []
|
self.state.pending_incomplete_tokens = []
|
||||||
if split_words and replacement_char in split_words[-1]:
|
if split_words and replacement_char in split_words[-1]:
|
||||||
self.pending_incomplete_tokens = split_tokens[-1]
|
self.state.pending_incomplete_tokens = split_tokens[-1]
|
||||||
logger.warning(f"[UTF-8 Fix] Holding {len(self.pending_incomplete_tokens)} incomplete tokens for next chunk: {self.pending_incomplete_tokens}")
|
logger.warning(f"[UTF-8 Fix] Holding {len(self.state.pending_incomplete_tokens)} incomplete tokens for next chunk: {self.state.pending_incomplete_tokens}")
|
||||||
|
|
||||||
return timestamped_words
|
return timestamped_words
|
||||||
|
|
||||||
|
def _process_cross_attention(
|
||||||
|
self,
|
||||||
|
cross_attns: List[torch.Tensor],
|
||||||
|
content_mel_len: int
|
||||||
|
) -> torch.Tensor:
|
||||||
|
"""
|
||||||
|
Process cross-attention weights from decoder layers for alignment.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cross_attns: List of cross-attention tensors from each decoder layer.
|
||||||
|
Each tensor has shape (batch, n_head, seq_len, audio_len)
|
||||||
|
content_mel_len: Length of actual audio content in mel frames
|
||||||
|
|
||||||
|
Returns processed attention tensor for alignment, shape (batch, seq_len, content_mel_len)
|
||||||
|
"""
|
||||||
|
attn_of_alignment_heads = [[] for _ in range(self.state.num_align_heads)]
|
||||||
|
num_decoder_layers = len(self.model.decoder.blocks)
|
||||||
|
|
||||||
|
if cross_attns and isinstance(cross_attns[0], list):
|
||||||
|
flattened_attns: List[torch.Tensor] = [attn for layer_list in cross_attns for attn in layer_list]
|
||||||
|
else:
|
||||||
|
flattened_attns = cross_attns
|
||||||
|
|
||||||
|
for idx, attn_mat in enumerate(flattened_attns):
|
||||||
|
layer_rank = idx % num_decoder_layers
|
||||||
|
# attn_mat shape: (batch, n_head, seq_len, audio_len) or (n_head, seq_len, audio_len) for batch=1
|
||||||
|
align_heads_in_layer = self.state.align_source.get(layer_rank, [])
|
||||||
|
if len(align_heads_in_layer) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
attn_mat = F.softmax(attn_mat, dim=-1)
|
||||||
|
|
||||||
|
for align_head_rank, head_id in align_heads_in_layer:
|
||||||
|
if self.cfg.beam_size == 1:
|
||||||
|
# (n_head, seq_len, audio_len) when squeezed
|
||||||
|
if attn_mat.dim() == 4:
|
||||||
|
a = attn_mat[0, head_id, :, :] # (seq_len, audio_len)
|
||||||
|
else:
|
||||||
|
a = attn_mat[head_id, :, :]
|
||||||
|
a = a.unsqueeze(0) # (1, seq_len, audio_len)
|
||||||
|
else:
|
||||||
|
# attn_mat: (batch, n_head, seq_len, audio_len)
|
||||||
|
a = attn_mat[:, head_id, :, :] # (batch, seq_len, audio_len)
|
||||||
|
attn_of_alignment_heads[align_head_rank].append(a)
|
||||||
|
|
||||||
|
tmp = []
|
||||||
|
for mat in attn_of_alignment_heads:
|
||||||
|
if mat:
|
||||||
|
t = torch.cat(mat, dim=1) # (batch, total_seq_len, audio_len)
|
||||||
|
tmp.append(t)
|
||||||
|
|
||||||
|
if not tmp:
|
||||||
|
return torch.zeros(self.cfg.beam_size, 1, content_mel_len, device=self.device)
|
||||||
|
|
||||||
|
# stck al heads: (batch, num_align_heads, seq_len, audio_len)
|
||||||
|
attn_of_alignment_heads = torch.stack(tmp, dim=1)
|
||||||
|
|
||||||
|
std, mean = torch.std_mean(attn_of_alignment_heads, dim=-2, keepdim=True, unbiased=False)
|
||||||
|
attn_of_alignment_heads = (attn_of_alignment_heads - mean) / (std + 1e-8)
|
||||||
|
|
||||||
|
attn_of_alignment_heads = median_filter(attn_of_alignment_heads, 7)
|
||||||
|
attn_of_alignment_heads = attn_of_alignment_heads.mean(dim=1)
|
||||||
|
attn_of_alignment_heads = attn_of_alignment_heads[:, :, :content_mel_len]
|
||||||
|
return attn_of_alignment_heads
|
||||||
|
|
|
||||||
|
|
@ -147,16 +147,13 @@ class PyTorchInference(Inference):
|
||||||
self.model: "Whisper" = model
|
self.model: "Whisper" = model
|
||||||
self.initial_token_length = initial_token_length
|
self.initial_token_length = initial_token_length
|
||||||
self.kv_cache = {}
|
self.kv_cache = {}
|
||||||
self.hooks = []
|
|
||||||
|
|
||||||
key_modules = [block.attn.key for block in self.model.decoder.blocks]
|
self.kv_cache_ids = []
|
||||||
value_modules = [block.attn.value for block in self.model.decoder.blocks]
|
for block in self.model.decoder.blocks:
|
||||||
self.kv_modules = key_modules + value_modules
|
self.kv_cache_ids.append(block.attn.key_cache_id)
|
||||||
|
self.kv_cache_ids.append(block.attn.value_cache_id)
|
||||||
|
|
||||||
def logits(self, tokens: Tensor, audio_features: Tensor) -> Tensor:
|
def logits(self, tokens: Tensor, audio_features: Tensor) -> Tensor:
|
||||||
if not self.kv_cache:
|
|
||||||
self.kv_cache, self.hooks = self.model.install_kv_cache_hooks()
|
|
||||||
|
|
||||||
if tokens.shape[-1] > self.initial_token_length:
|
if tokens.shape[-1] > self.initial_token_length:
|
||||||
# only need to use the last token except in the first forward pass
|
# only need to use the last token except in the first forward pass
|
||||||
tokens = tokens[:, -1:]
|
tokens = tokens[:, -1:]
|
||||||
|
|
@ -164,17 +161,14 @@ class PyTorchInference(Inference):
|
||||||
return self.model.decoder(tokens, audio_features, kv_cache=self.kv_cache)
|
return self.model.decoder(tokens, audio_features, kv_cache=self.kv_cache)
|
||||||
|
|
||||||
def cleanup_caching(self):
|
def cleanup_caching(self):
|
||||||
for hook in self.hooks:
|
|
||||||
hook.remove()
|
|
||||||
|
|
||||||
self.kv_cache = {}
|
self.kv_cache = {}
|
||||||
self.hooks = []
|
|
||||||
|
|
||||||
def rearrange_kv_cache(self, source_indices):
|
def rearrange_kv_cache(self, source_indices):
|
||||||
if source_indices != list(range(len(source_indices))):
|
if source_indices != list(range(len(source_indices))):
|
||||||
for module in self.kv_modules:
|
for cache_id in self.kv_cache_ids:
|
||||||
# update the key/value cache to contain the selected sequences
|
if cache_id in self.kv_cache:
|
||||||
self.kv_cache[module] = self.kv_cache[module][source_indices].detach()
|
# update the key/value cache to contain the selected sequences
|
||||||
|
self.kv_cache[cache_id] = self.kv_cache[cache_id][source_indices].detach()
|
||||||
|
|
||||||
|
|
||||||
class SequenceRanker:
|
class SequenceRanker:
|
||||||
|
|
|
||||||
|
|
@ -79,18 +79,23 @@ def disable_sdpa():
|
||||||
|
|
||||||
|
|
||||||
class MultiHeadAttention(nn.Module):
|
class MultiHeadAttention(nn.Module):
|
||||||
use_sdpa = False # Disable SDPA to ensure qk is always computed for hooks
|
use_sdpa = False # Disable SDPA to ensure qk is always computed when needed
|
||||||
|
|
||||||
def __init__(self, n_state: int, n_head: int, cache_id: str = ""):
|
def __init__(self, n_state: int, n_head: int, cache_id: str = "", n_text_ctx: int = 448):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.n_head = n_head
|
self.n_head = n_head
|
||||||
|
self.n_text_ctx = n_text_ctx
|
||||||
self.query = Linear(n_state, n_state)
|
self.query = Linear(n_state, n_state)
|
||||||
self.key = Linear(n_state, n_state, bias=False)
|
self.key = Linear(n_state, n_state, bias=False)
|
||||||
self.value = Linear(n_state, n_state)
|
self.value = Linear(n_state, n_state)
|
||||||
self.out = Linear(n_state, n_state)
|
self.out = Linear(n_state, n_state)
|
||||||
self.cache_id = cache_id
|
self.cache_id = cache_id
|
||||||
self.key.cache_id = f"{cache_id}_key"
|
# Cache IDs for key and value (used with dict-based kv_cache)
|
||||||
self.value.cache_id = f"{cache_id}_value"
|
self.key_cache_id = f"{cache_id}_key"
|
||||||
|
self.value_cache_id = f"{cache_id}_value"
|
||||||
|
# Keep these for backward compatibility with hook-based caching
|
||||||
|
self.key.cache_id = self.key_cache_id
|
||||||
|
self.value.cache_id = self.value_cache_id
|
||||||
|
|
||||||
def forward(
|
def forward(
|
||||||
self,
|
self,
|
||||||
|
|
@ -101,19 +106,45 @@ class MultiHeadAttention(nn.Module):
|
||||||
):
|
):
|
||||||
q = self.query(x)
|
q = self.query(x)
|
||||||
|
|
||||||
if kv_cache is None or xa is None or self.key not in kv_cache:
|
if xa is None:
|
||||||
# hooks, if installed (i.e. kv_cache is not None), will prepend the cached kv tensors;
|
# Self-attention
|
||||||
# otherwise, perform key/value projections for self- or cross-attention as usual.
|
k = self.key(x)
|
||||||
k = self.key(x if xa is None else xa)
|
v = self.value(x)
|
||||||
v = self.value(x if xa is None else xa)
|
if kv_cache is not None:
|
||||||
|
k, v = self._update_self_attn_cache(k, v, kv_cache)
|
||||||
else:
|
else:
|
||||||
# for cross-attention, calculate keys and values once and reuse in subsequent calls.
|
# Cross-attention: compute once and cache, or reuse from cache
|
||||||
k = kv_cache[self.key]
|
if kv_cache is not None and self.key_cache_id in kv_cache:
|
||||||
v = kv_cache[self.value]
|
k = kv_cache[self.key_cache_id]
|
||||||
|
v = kv_cache[self.value_cache_id]
|
||||||
|
else:
|
||||||
|
k = self.key(xa)
|
||||||
|
v = self.value(xa)
|
||||||
|
if kv_cache is not None:
|
||||||
|
kv_cache[self.key_cache_id] = k
|
||||||
|
kv_cache[self.value_cache_id] = v
|
||||||
|
|
||||||
wv, qk = self.qkv_attention(q, k, v, mask)
|
wv, qk = self.qkv_attention(q, k, v, mask)
|
||||||
return self.out(wv), qk
|
return self.out(wv), qk
|
||||||
|
|
||||||
|
def _update_self_attn_cache(
|
||||||
|
self, k: Tensor, v: Tensor, kv_cache: dict
|
||||||
|
) -> Tuple[Tensor, Tensor]:
|
||||||
|
"""Update self-attention kv cache by concatenating new k,v with cached values."""
|
||||||
|
if self.key_cache_id not in kv_cache or k.shape[1] > self.n_text_ctx:
|
||||||
|
# First token or context overflow: save as-is
|
||||||
|
kv_cache[self.key_cache_id] = k.detach()
|
||||||
|
kv_cache[self.value_cache_id] = v.detach()
|
||||||
|
else:
|
||||||
|
# Concatenate with existing cache
|
||||||
|
cached_k = kv_cache[self.key_cache_id]
|
||||||
|
cached_v = kv_cache[self.value_cache_id]
|
||||||
|
k = torch.cat([cached_k, k], dim=1).detach()
|
||||||
|
v = torch.cat([cached_v, v], dim=1).detach()
|
||||||
|
kv_cache[self.key_cache_id] = k
|
||||||
|
kv_cache[self.value_cache_id] = v
|
||||||
|
return k, v
|
||||||
|
|
||||||
def qkv_attention(
|
def qkv_attention(
|
||||||
self, q: Tensor, k: Tensor, v: Tensor, mask: Optional[Tensor] = None
|
self, q: Tensor, k: Tensor, v: Tensor, mask: Optional[Tensor] = None
|
||||||
) -> Tuple[torch.Tensor, Optional[torch.Tensor]]:
|
) -> Tuple[torch.Tensor, Optional[torch.Tensor]]:
|
||||||
|
|
@ -143,14 +174,21 @@ class MultiHeadAttention(nn.Module):
|
||||||
|
|
||||||
|
|
||||||
class ResidualAttentionBlock(nn.Module):
|
class ResidualAttentionBlock(nn.Module):
|
||||||
def __init__(self, n_state: int, n_head: int, cross_attention: bool = False, cache_id: str = ""):
|
def __init__(
|
||||||
|
self, n_state: int, n_head: int, cross_attention: bool = False,
|
||||||
|
cache_id: str = "", n_text_ctx: int = 448
|
||||||
|
):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self.attn = MultiHeadAttention(n_state, n_head, cache_id=f"{cache_id}_self_attn")
|
self.attn = MultiHeadAttention(
|
||||||
|
n_state, n_head, cache_id=f"{cache_id}_self_attn", n_text_ctx=n_text_ctx
|
||||||
|
)
|
||||||
self.attn_ln = LayerNorm(n_state)
|
self.attn_ln = LayerNorm(n_state)
|
||||||
|
|
||||||
self.cross_attn = (
|
self.cross_attn = (
|
||||||
MultiHeadAttention(n_state, n_head, cache_id=f"{cache_id}_cross_attn") if cross_attention else None
|
MultiHeadAttention(
|
||||||
|
n_state, n_head, cache_id=f"{cache_id}_cross_attn", n_text_ctx=n_text_ctx
|
||||||
|
) if cross_attention else None
|
||||||
)
|
)
|
||||||
self.cross_attn_ln = LayerNorm(n_state) if cross_attention else None
|
self.cross_attn_ln = LayerNorm(n_state) if cross_attention else None
|
||||||
|
|
||||||
|
|
@ -166,12 +204,21 @@ class ResidualAttentionBlock(nn.Module):
|
||||||
xa: Optional[Tensor] = None,
|
xa: Optional[Tensor] = None,
|
||||||
mask: Optional[Tensor] = None,
|
mask: Optional[Tensor] = None,
|
||||||
kv_cache: Optional[dict] = None,
|
kv_cache: Optional[dict] = None,
|
||||||
):
|
) -> Tuple[Tensor, Optional[Tensor]]:
|
||||||
|
"""
|
||||||
|
Returns:
|
||||||
|
x: The output tensor
|
||||||
|
cross_attn_qk: Cross-attention weights (if cross_attn exists), else None
|
||||||
|
"""
|
||||||
x = x + self.attn(self.attn_ln(x), mask=mask, kv_cache=kv_cache)[0]
|
x = x + self.attn(self.attn_ln(x), mask=mask, kv_cache=kv_cache)[0]
|
||||||
|
cross_attn_qk = None
|
||||||
if self.cross_attn:
|
if self.cross_attn:
|
||||||
x = x + self.cross_attn(self.cross_attn_ln(x), xa, kv_cache=kv_cache)[0]
|
cross_out, cross_attn_qk = self.cross_attn(
|
||||||
|
self.cross_attn_ln(x), xa, kv_cache=kv_cache
|
||||||
|
)
|
||||||
|
x = x + cross_out
|
||||||
x = x + self.mlp(self.mlp_ln(x))
|
x = x + self.mlp(self.mlp_ln(x))
|
||||||
return x
|
return x, cross_attn_qk
|
||||||
|
|
||||||
|
|
||||||
class AudioEncoder(nn.Module):
|
class AudioEncoder(nn.Module):
|
||||||
|
|
@ -201,7 +248,7 @@ class AudioEncoder(nn.Module):
|
||||||
x = (x + self.positional_embedding).to(x.dtype)
|
x = (x + self.positional_embedding).to(x.dtype)
|
||||||
|
|
||||||
for block in self.blocks:
|
for block in self.blocks:
|
||||||
x = block(x)
|
x, _ = block(x) # Encoder blocks don't have cross-attention
|
||||||
|
|
||||||
x = self.ln_post(x)
|
x = self.ln_post(x)
|
||||||
return x
|
return x
|
||||||
|
|
@ -212,13 +259,17 @@ class TextDecoder(nn.Module):
|
||||||
self, n_vocab: int, n_ctx: int, n_state: int, n_head: int, n_layer: int
|
self, n_vocab: int, n_ctx: int, n_state: int, n_head: int, n_layer: int
|
||||||
):
|
):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
self.n_ctx = n_ctx
|
||||||
|
|
||||||
self.token_embedding = nn.Embedding(n_vocab, n_state)
|
self.token_embedding = nn.Embedding(n_vocab, n_state)
|
||||||
self.positional_embedding = nn.Parameter(torch.empty(n_ctx, n_state))
|
self.positional_embedding = nn.Parameter(torch.empty(n_ctx, n_state))
|
||||||
|
|
||||||
self.blocks: Iterable[ResidualAttentionBlock] = nn.ModuleList(
|
self.blocks: Iterable[ResidualAttentionBlock] = nn.ModuleList(
|
||||||
[
|
[
|
||||||
ResidualAttentionBlock(n_state, n_head, cross_attention=True, cache_id=f"dec_layer{i}")
|
ResidualAttentionBlock(
|
||||||
|
n_state, n_head, cross_attention=True,
|
||||||
|
cache_id=f"dec_layer{i}", n_text_ctx=n_ctx
|
||||||
|
)
|
||||||
for i in range(n_layer)
|
for i in range(n_layer)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
@ -227,28 +278,57 @@ class TextDecoder(nn.Module):
|
||||||
mask = torch.empty(n_ctx, n_ctx).fill_(-np.inf).triu_(1)
|
mask = torch.empty(n_ctx, n_ctx).fill_(-np.inf).triu_(1)
|
||||||
self.register_buffer("mask", mask, persistent=False)
|
self.register_buffer("mask", mask, persistent=False)
|
||||||
|
|
||||||
def forward(self, x: Tensor, xa: Tensor, kv_cache: Optional[dict] = None):
|
def forward(
|
||||||
|
self,
|
||||||
|
x: Tensor,
|
||||||
|
xa: Tensor,
|
||||||
|
kv_cache: Optional[dict] = None,
|
||||||
|
return_cross_attn: bool = False,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
x : torch.LongTensor, shape = (batch_size, <= n_ctx)
|
x : torch.LongTensor, shape = (batch_size, <= n_ctx)
|
||||||
the text tokens
|
the text tokens
|
||||||
xa : torch.Tensor, shape = (batch_size, n_audio_ctx, n_audio_state)
|
xa : torch.Tensor, shape = (batch_size, n_audio_ctx, n_audio_state)
|
||||||
the encoded audio features to be attended on
|
the encoded audio features to be attended on
|
||||||
|
kv_cache : Optional[dict]
|
||||||
|
Dictionary to store/retrieve key-value cache for efficient decoding
|
||||||
|
return_cross_attn : bool
|
||||||
|
If True, return cross-attention weights from all decoder layers
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
logits : Tensor
|
||||||
|
The output logits
|
||||||
|
cross_attns : Optional[List[Tensor]]
|
||||||
|
List of cross-attention weights per layer (only if return_cross_attn=True)
|
||||||
"""
|
"""
|
||||||
offset = next(iter(kv_cache.values())).shape[1] if kv_cache else 0
|
# Calculate offset from self-attention cache (not cross-attention which has audio length)
|
||||||
|
offset = 0
|
||||||
|
if kv_cache:
|
||||||
|
# Use the first decoder block's self-attention key cache to get token position
|
||||||
|
first_self_attn_key = self.blocks[0].attn.key_cache_id
|
||||||
|
if first_self_attn_key in kv_cache:
|
||||||
|
offset = kv_cache[first_self_attn_key].shape[1]
|
||||||
|
|
||||||
x = (
|
x = (
|
||||||
self.token_embedding(x)
|
self.token_embedding(x)
|
||||||
+ self.positional_embedding[offset : offset + x.shape[-1]]
|
+ self.positional_embedding[offset : offset + x.shape[-1]]
|
||||||
)
|
)
|
||||||
x = x.to(xa.dtype)
|
x = x.to(xa.dtype)
|
||||||
|
|
||||||
|
cross_attns = [] if return_cross_attn else None
|
||||||
for block in self.blocks:
|
for block in self.blocks:
|
||||||
x = block(x, xa, mask=self.mask, kv_cache=kv_cache)
|
x, cross_attn_qk = block(x, xa, mask=self.mask, kv_cache=kv_cache)
|
||||||
|
if return_cross_attn and cross_attn_qk is not None:
|
||||||
|
cross_attns.append(cross_attn_qk)
|
||||||
|
|
||||||
x = self.ln(x)
|
x = self.ln(x)
|
||||||
logits = (
|
logits = (
|
||||||
x @ torch.transpose(self.token_embedding.weight.to(x.dtype), 0, 1)
|
x @ torch.transpose(self.token_embedding.weight.to(x.dtype), 0, 1)
|
||||||
).float()
|
).float()
|
||||||
|
|
||||||
|
if return_cross_attn:
|
||||||
|
return logits, cross_attns
|
||||||
return logits
|
return logits
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -292,8 +372,18 @@ class Whisper(nn.Module):
|
||||||
def embed_audio(self, mel: torch.Tensor):
|
def embed_audio(self, mel: torch.Tensor):
|
||||||
return self.encoder(mel)
|
return self.encoder(mel)
|
||||||
|
|
||||||
def logits(self, tokens: torch.Tensor, audio_features: torch.Tensor):
|
def logits(
|
||||||
return self.decoder(tokens, audio_features)
|
self,
|
||||||
|
tokens: torch.Tensor,
|
||||||
|
audio_features: torch.Tensor,
|
||||||
|
kv_cache: Optional[dict] = None,
|
||||||
|
return_cross_attn: bool = False,
|
||||||
|
):
|
||||||
|
return self.decoder(
|
||||||
|
tokens, audio_features,
|
||||||
|
kv_cache=kv_cache,
|
||||||
|
return_cross_attn=return_cross_attn
|
||||||
|
)
|
||||||
|
|
||||||
def forward(
|
def forward(
|
||||||
self, mel: torch.Tensor, tokens: torch.Tensor
|
self, mel: torch.Tensor, tokens: torch.Tensor
|
||||||
|
|
@ -312,39 +402,6 @@ class Whisper(nn.Module):
|
||||||
def num_languages(self):
|
def num_languages(self):
|
||||||
return self.dims.n_vocab - 51765 - int(self.is_multilingual)
|
return self.dims.n_vocab - 51765 - int(self.is_multilingual)
|
||||||
|
|
||||||
def install_kv_cache_hooks(self, cache: Optional[dict] = None):
|
|
||||||
"""
|
|
||||||
The `MultiHeadAttention` module optionally accepts `kv_cache` which stores the key and value
|
|
||||||
tensors calculated for the previous positions. This method returns a dictionary that stores
|
|
||||||
all caches, and the necessary hooks for the key and value projection modules that save the
|
|
||||||
intermediate tensors to be reused during later calculations.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
cache : Dict[nn.Module, torch.Tensor]
|
|
||||||
A dictionary object mapping the key/value projection modules to its cache
|
|
||||||
hooks : List[RemovableHandle]
|
|
||||||
List of PyTorch RemovableHandle objects to stop the hooks to be called
|
|
||||||
"""
|
|
||||||
cache = {**cache} if cache is not None else {}
|
|
||||||
hooks = []
|
|
||||||
|
|
||||||
def save_to_cache(module, _, output):
|
|
||||||
if module not in cache or output.shape[1] > self.dims.n_text_ctx:
|
|
||||||
# save as-is, for the first token or cross attention
|
|
||||||
cache[module] = output
|
|
||||||
else:
|
|
||||||
cache[module] = torch.cat([cache[module], output], dim=1).detach()
|
|
||||||
return cache[module]
|
|
||||||
|
|
||||||
def install_hooks(layer: nn.Module):
|
|
||||||
if isinstance(layer, MultiHeadAttention):
|
|
||||||
hooks.append(layer.key.register_forward_hook(save_to_cache))
|
|
||||||
hooks.append(layer.value.register_forward_hook(save_to_cache))
|
|
||||||
|
|
||||||
self.decoder.apply(install_hooks)
|
|
||||||
return cache, hooks
|
|
||||||
|
|
||||||
detect_language = detect_language_function
|
detect_language = detect_language_function
|
||||||
transcribe = transcribe_function
|
transcribe = transcribe_function
|
||||||
decode = decode_function
|
decode = decode_function
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue