first tests
This commit is contained in:
1 parent
50ef195c35
commit
203df3b7d6
12 files changed
+4228
No files matched your search
+139
@@ -0,0 +1,139 @@
|
||||
"""A conversation held in the model's KV cache, with audio prefilled while the user is still speaking.
|
||||
|
||||
The cache always ends right after a complete chat-template segment, so each step only appends:
|
||||
|
||||
begin_turn(): [<bos>system…<|im_end|>\\n] <|im_start|>user\\n
|
||||
feed_audio(): audio tokens, encoded and prefilled as the mic chunks arrive
|
||||
generate(): <|im_end|>\\n<|im_start|>assistant\\n<|mixed_start|> → generated text + speech frames
|
||||
|
||||
The model's own generated tokens (including its speech frames) stay in the cache, so the next turn starts from
|
||||
there instead of re-encoding the whole conversation.
|
||||
"""
|
||||
|
||||
import copy
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
import soxr
|
||||
import torch
|
||||
|
||||
MAX_CONTEXT = 16_000 # tokens; past this the conversation is reset at the next turn
|
||||
|
||||
|
||||
class StreamingSession:
|
||||
def __init__(self, engine):
|
||||
self.engine = engine
|
||||
self.model = engine.model
|
||||
self.tok = engine.tokenizer
|
||||
self.fe = engine.processor.feature_extractor
|
||||
self.im_end = self.tok.convert_tokens_to_ids("<|im_end|>")
|
||||
self.reset()
|
||||
|
||||
# ---- state -------------------------------------------------------------------------------------------------
|
||||
|
||||
def reset(self):
|
||||
self.cache = None
|
||||
self.mask = None
|
||||
self.key = None # (mode, system prompt): a change starts a fresh context
|
||||
self.needs_close = False # last generation stopped without <|im_end|>
|
||||
self.turn_snapshot = None
|
||||
self._end_audio()
|
||||
|
||||
def _end_audio(self):
|
||||
self.resampler = None
|
||||
self.feature_cache = None
|
||||
self.encoder_cache = None
|
||||
self.audio_in_s = 0.0
|
||||
self.audio_tokens = 0
|
||||
|
||||
@property
|
||||
def length(self) -> int:
|
||||
return 0 if self.cache is None else self.cache.get_seq_length()
|
||||
|
||||
def snapshot(self):
|
||||
return copy.deepcopy((self.cache, self.mask, self.needs_close))
|
||||
|
||||
def restore(self, snap):
|
||||
self.cache, self.mask, self.needs_close = snap
|
||||
self._end_audio()
|
||||
|
||||
# ---- appending -----------------------------------------------------------------------------------------------
|
||||
|
||||
def _append_embeds(self, embeds: torch.Tensor):
|
||||
n = embeds.shape[1]
|
||||
if self.cache is None:
|
||||
from transformers import DynamicCache
|
||||
|
||||
self.cache = DynamicCache(config=self.model.config.text_config)
|
||||
self.mask = torch.ones((1, 0), dtype=torch.long, device=self.model.device)
|
||||
self.mask = torch.cat((self.mask, self.mask.new_ones((1, n))), dim=-1)
|
||||
position_ids = self.mask.cumsum(-1).sub(1)[:, -n:]
|
||||
self.model.model.language_model(
|
||||
inputs_embeds=embeds,
|
||||
past_key_values=self.cache,
|
||||
attention_mask=self.mask,
|
||||
position_ids=position_ids,
|
||||
use_cache=True,
|
||||
)
|
||||
|
||||
def append_text(self, text: str):
|
||||
ids = self.tok(text, add_special_tokens=False, return_tensors="pt").input_ids.to(self.model.device)
|
||||
self._append_embeds(self.model.get_input_embeddings()(ids))
|
||||
|
||||
def close_prefix(self) -> str:
|
||||
"""Text that closes the previous assistant turn, if generation left it open."""
|
||||
return "<|im_end|>\n" if self.needs_close else "\n" if self.cache is not None else ""
|
||||
|
||||
# ---- a turn --------------------------------------------------------------------------------------------------
|
||||
|
||||
@torch.inference_mode()
|
||||
def begin_turn(self, key, system_block: str, keep_history: bool) -> dict:
|
||||
"""Open a user turn. `system_block` is the rendered `<bos><|im_start|>system…<|im_end|>\\n` prefix."""
|
||||
info = {}
|
||||
if not keep_history or key != self.key or self.length > MAX_CONTEXT:
|
||||
if self.cache is not None:
|
||||
info["context_reset"] = "history off" if not keep_history else "settings changed" if key != self.key else "context full"
|
||||
self.reset()
|
||||
self.key = key
|
||||
self._end_audio()
|
||||
t = time.perf_counter()
|
||||
self.turn_snapshot = self.snapshot()
|
||||
info["snapshot_ms"] = round((time.perf_counter() - t) * 1000, 1)
|
||||
prefix = self.close_prefix() if self.cache is not None else system_block
|
||||
self.append_text(prefix + "<|im_start|>user\n")
|
||||
self.needs_close = False
|
||||
info["context_tokens"] = self.length
|
||||
return info
|
||||
|
||||
@torch.inference_mode()
|
||||
def feed_audio(self, pcm: np.ndarray, rate: int, final: bool = False):
|
||||
"""Resample, extract mel frames and prefill whatever audio tokens they complete."""
|
||||
if self.resampler is None:
|
||||
self.resampler = soxr.ResampleStream(rate, self.fe.sampling_rate, 1, dtype="float32")
|
||||
pcm16 = self.resampler.resample_chunk(np.asarray(pcm, dtype=np.float32), last=final)
|
||||
self.audio_in_s += len(pcm16) / self.fe.sampling_rate
|
||||
features, self.feature_cache = self.fe.stream(pcm16, self.feature_cache, is_final=final)
|
||||
mel = features["input_features"].to(self.model.device)
|
||||
if not mel.shape[-1] and not final:
|
||||
return
|
||||
out = self.model.prefill_audio(
|
||||
mel, past_key_values=self.cache, encoder_cache=self.encoder_cache, attention_mask=self.mask, is_final=final
|
||||
)
|
||||
self.audio_tokens += out.attention_mask.shape[1] - self.mask.shape[1]
|
||||
self.cache, self.mask, self.encoder_cache = out.past_key_values, out.attention_mask, out.encoder_cache
|
||||
|
||||
@torch.inference_mode()
|
||||
def generate(self, suffix: str, **kwargs):
|
||||
"""Append `suffix` and generate from the cache; the cache then covers the whole reply."""
|
||||
ids = self.tok(suffix, add_special_tokens=False, return_tensors="pt").input_ids.to(self.model.device)
|
||||
mask = torch.cat((self.mask, self.mask.new_ones((1, ids.shape[1]))), dim=-1)
|
||||
try:
|
||||
out = self.model.generate(
|
||||
input_ids=ids, attention_mask=mask, past_key_values=self.cache, return_audio_eos=True,
|
||||
return_dict_in_generate=True, **kwargs,
|
||||
)
|
||||
finally:
|
||||
# the cache was advanced in place, whether generation finished or was interrupted
|
||||
self.mask = torch.ones((1, self.length), dtype=torch.long, device=self.model.device)
|
||||
self.needs_close = int(out.sequences[0, -1]) != self.im_end
|
||||
return out
|
||||
Reference in new issue
Block a user