openbmb/MiniCPM-o-4_5
1.5k710k
1#!/usr/bin/env python2# -*- coding: utf-8 -*-3# Copyright 2026 The OpenBMB Team. All rights reserved.4#5# Licensed under the Apache License, Version 2.0 (the "License");6# you may not use this file except in compliance with the License.7# You may obtain a copy of the License at8#9# http://www.apache.org/licenses/LICENSE-2.010#11# Unless required by applicable law or agreed to in writing, software12# distributed under the License is distributed on an "AS IS" BASIS,13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.14# See the License for the specific language governing permissions and15# limitations under the License.16 17import logging18from dataclasses import dataclass19from typing import Any20from typing import Dict21from typing import List22from typing import Literal23from typing import Optional24from typing import Tuple25from typing import Union26 27import torch28import torch.nn.functional as F29import torch.nn.utils.parametrize as P30from transformers.cache_utils import DynamicCache31 32logger = logging.getLogger(__name__)33 34 35# text36@dataclass37class GenerateChunkOutput:38 chunk_token_ids: torch.Tensor39 current_inputs_embeds: torch.Tensor40 input_last_hidden_states: Optional[torch.Tensor] # for tts use_speaker_embedding41 last_hidden_states: Optional[torch.Tensor] # for tts input feature (projector_semantic)42 past_key_values: Optional[torch.Tensor]43 finished: bool44 45 46class ChunkPrefillChunkGenerate:47 def __init__(self, model, tokenizer, terminators):48 self.tokenizer = tokenizer49 self.model = model50 self.terminators = terminators51 self.terminators_ids = [tokenizer.convert_tokens_to_ids(i) for i in self.terminators]52 self.embedding_layer = self.model.get_input_embeddings()53 54 self.forbidden_tokens = [55 ":",56 ":",57 ";",58 "#",59 "“",60 "”",61 "‘",62 "’",63 "@",64 "*",65 "【",66 "】",67 "「",68 "」",69 "(",70 ")",71 "(",72 ")",73 "[",74 "]",75 "&",76 "/",77 "$",78 ]79 80 self.forbidden_token_ids = [tokenizer.convert_tokens_to_ids(i) for i in self.forbidden_tokens]81 bad_token_ids = getattr(tokenizer, "bad_token_ids", [])82 if bad_token_ids:83 self.forbidden_token_ids.extend(bad_token_ids)84 85 @staticmethod86 def prepare_generation_config(do_sample, max_new_tokens=50, min_new_tokens=0, **kwargs):87 num_beams = kwargs.get("num_beams", 3)88 generation_config = {89 "num_beams": num_beams,90 "top_p": 0.8,91 "top_k": 100,92 "temperature": 0.7,93 "do_sample": True,94 "repetition_penalty": 1.05,95 }96 97 if do_sample:98 generation_config.update(99 {100 "top_p": 0.8,101 "top_k": 100,102 "temperature": 0.7,103 "do_sample": True,104 "repetition_penalty": 1.05,105 }106 )107 elif num_beams > 1:108 generation_config.update({"num_beams": num_beams, "repetition_penalty": 1.2, "do_sample": False})109 else:110 generation_config.update({"do_sample": False, "repetition_penalty": 1.05})111 112 generation_config.update((k, kwargs[k]) for k in generation_config.keys() & kwargs.keys())113 generation_config["min_new_tokens"] = min_new_tokens114 generation_config["max_new_tokens"] = max_new_tokens115 116 return generation_config117 118 def chunk_generate(119 self,120 inputs_embeds: torch.Tensor,121 past_key_values,122 is_first_generate_chunk: bool,123 chunk_size: int,124 return_hidden_states: bool,125 do_sample: bool,126 temperature: float,127 top_p: float,128 top_k: int,129 repetition_penalty: float = 1.05,130 length_penalty: float = 1.0,131 all_input_ids: Optional[torch.Tensor] = None,132 ) -> GenerateChunkOutput:133 """134 Args:135 inputs_embeds: [1, seq_len, hidden_dim], Input embeddings of current chunk.136 past_key_values: [num_layers, 2, batch_size, num_heads, seq_len, head_dim], Past key values for llm.137 is_first_generate_chunk: bool, Whether this is the first generate chunk.138 chunk_size: int, The size of the current chunk, default is 10, and it is fixed during training.139 return_hidden_states: bool Whether to return the hidden states, default is True.140 do_sample: bool Whether to sample from the model, default is True.141 temperature: float The temperature for the model, default is 0.7.142 top_p: float The top-p for the model, default is 0.8.143 top_k: int The top-k for the model, default is 100.144 repetition_penalty: float, The repetition penalty for the model, default is 1.05.145 length_penalty: float, The length penalty for the model, default is 1.0. Higher value means more detailed generation.146 all_input_ids: Optional[torch.Tensor], The input ids for the current chunk.147 """148 149 finished = False150 current_inputs_embeds = inputs_embeds.clone()151 input_last_hidden_states = []152 last_hidden_states = []153 generated_tokens = []154 155 for token_idx in range(chunk_size):156 if is_first_generate_chunk and token_idx == 0:157 # first generate chunk, prefill inputs_embeds158 model_inputs = {159 "inputs_embeds": current_inputs_embeds,160 "past_key_values": past_key_values,161 "use_cache": True,162 "output_hidden_states": return_hidden_states,163 }164 else: # for all other cases: prefill the latest generated token165 model_inputs = {166 "inputs_embeds": current_inputs_embeds[:, -1:, :],167 "past_key_values": past_key_values,168 "use_cache": True,169 "output_hidden_states": return_hidden_states,170 }171 172 with torch.no_grad():173 outputs = self.model(**model_inputs)174 175 # last token's logits176 logits = outputs.logits[:, -1, :].to(copy=True, dtype=torch.float32, device=inputs_embeds.device)177 178 # forbid specific tokens decoding = model.generate@suppress_tokens179 if self.forbidden_token_ids:180 logits[:, self.forbidden_token_ids] = float("-inf")181 182 past_key_values = outputs.past_key_values183 184 PENALTY_WINDOW_SIZE = 128185 186 # apply repetition penalty187 if repetition_penalty != 1.0:188 # get token ids for repetition penalty189 if all_input_ids is not None:190 # use global input ids (including original input and generated part)191 if len(generated_tokens) > 0:192 generated_token_ids = torch.cat(generated_tokens, dim=1)193 current_sequence = torch.cat(194 [195 all_input_ids[:, -PENALTY_WINDOW_SIZE:],196 generated_token_ids,197 ],198 dim=1,199 )200 else:201 current_sequence = all_input_ids[:, -PENALTY_WINDOW_SIZE:]202 unique_token_ids = torch.unique(current_sequence.squeeze(0))203 elif len(generated_tokens) > 0:204 # revert to original logic: only use generated tokens205 generated_token_ids = torch.cat(generated_tokens, dim=1).squeeze(0)206 unique_token_ids = torch.unique(generated_token_ids)207 else:208 unique_token_ids = torch.tensor([], dtype=torch.long, device=logits.device)209 210 # apply repetition penalty211 for token_id in unique_token_ids:212 if logits[0, token_id] > 0:213 logits[0, token_id] = logits[0, token_id] / repetition_penalty214 else:215 logits[0, token_id] = logits[0, token_id] * repetition_penalty216 217 # apply length penalty, higher value means more detailed generation218 if length_penalty != 1.0:219 for eos_token_id in self.terminators_ids:220 if logits[0, eos_token_id] > 0:221 logits[0, eos_token_id] = logits[0, eos_token_id] / length_penalty222 else:223 logits[0, eos_token_id] = logits[0, eos_token_id] * length_penalty224 225 # apply temperature226 if temperature != 1.0:227 logits = logits / temperature228 229 if do_sample:230 # Top-k filtering231 if top_k > 0:232 top_k_logits, top_k_indices = torch.topk(logits, min(top_k, logits.size(-1)))233 logits_filtered = torch.full_like(logits, float("-inf"))234 logits_filtered.scatter_(1, top_k_indices, top_k_logits)235 logits = logits_filtered236 237 # Top-p filtering238 if top_p < 1.0:239 sorted_logits, sorted_indices = torch.sort(logits, descending=True)240 cumulative_probs = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1)241 242 # remove tokens with cumulative probability greater than top_p243 sorted_indices_to_remove = cumulative_probs > top_p244 sorted_indices_to_remove[..., 1:] = sorted_indices_to_remove[..., :-1].clone()245 sorted_indices_to_remove[..., 0] = 0246 247 indices_to_remove = sorted_indices_to_remove.scatter(1, sorted_indices, sorted_indices_to_remove)248 logits[indices_to_remove] = float("-inf")249 250 # sampling251 probs = F.softmax(logits, dim=-1)252 next_token = torch.multinomial(probs, num_samples=1)253 else:254 next_token = torch.argmax(logits, dim=-1, keepdim=True)255 256 if return_hidden_states:257 if is_first_generate_chunk and token_idx == 0:258 input_last_hidden_states.append(outputs.hidden_states[-1])259 else:260 last_hidden_states.append(outputs.hidden_states[-1])261 262 # if terminator token, stop generating263 if next_token.item() in self.terminators_ids:264 finished = True265 break266 267 generated_tokens.append(next_token)268 269 # convert new token to embeddings and concatenate270 next_token_embed = self.embedding_layer(next_token)271 272 # update inputs_embeds, add one273 current_inputs_embeds = torch.cat([current_inputs_embeds, next_token_embed], dim=1)274 275 if len(generated_tokens) > 0:276 chunk_token_ids = torch.cat(generated_tokens, dim=1)277 else:278 # special case: if last chunk and first predict is eos token, return last token of previous chunk. return a tensor with shape (1, 0)279 if finished:280 chunk_token_ids = torch.zeros((1, 0), dtype=torch.long, device=current_inputs_embeds.device)281 else:282 raise Exception("this should not happen")283 284 if len(last_hidden_states) > 0:285 last_hidden_states = torch.cat(last_hidden_states, dim=1)286 else:287 # special case: if last chunk, return last token of previous chunk.288 if finished:289 last_hidden_states = torch.cat(last_hidden_states, dim=1)290 else:291 raise Exception("this should not happen")292 293 if len(input_last_hidden_states) > 0:294 input_last_hidden_states = torch.cat(input_last_hidden_states, dim=1)295 else:296 input_last_hidden_states = None297 298 return GenerateChunkOutput(299 chunk_token_ids=chunk_token_ids,300 current_inputs_embeds=current_inputs_embeds,301 input_last_hidden_states=input_last_hidden_states,302 last_hidden_states=last_hidden_states,303 past_key_values=past_key_values,304 finished=finished,305 )306 307 308def streaming_token_decoder(token_iterator, tokenizer, skip_special_tokens=False):309 """310 Incrementally decode tokens from an iterator, handling partial multi-byte characters.311 312 When streaming tokens, multi-byte characters (like Chinese) may be split across multiple313 tokens. Decoding partial tokens results in replacement characters (U+FFFD). This function314 buffers tokens and only yields complete characters.315 316 Args:317 token_iterator: An iterator yielding (token_ids, is_finished) tuples.318 token_ids can be torch.Tensor or any iterable of integers.319 tokenizer: The tokenizer to use for decoding.320 skip_special_tokens: Whether to skip special tokens during decoding.321 322 Yields:323 (decoded_text, is_finished) tuples where decoded_text is the new text since last yield.324 """325 accumulated_token_ids = []326 yielded_text_len = 0327 328 for token_ids, is_finished in token_iterator:329 # Accumulate token IDs330 if torch.is_tensor(token_ids):331 accumulated_token_ids.extend(token_ids.reshape(-1).tolist())332 else:333 accumulated_token_ids.extend(list(token_ids) if hasattr(token_ids, "__iter__") else [token_ids])334 335 # Decode all accumulated tokens336 full_decoded = tokenizer.decode(accumulated_token_ids, skip_special_tokens=skip_special_tokens)337 338 if is_finished:339 # Final chunk - yield all remaining text340 new_text = full_decoded[yielded_text_len:]341 yield new_text, is_finished342 else:343 # Find safe prefix without incomplete multi-byte characters344 # The replacement character '�' (U+FFFD) indicates incomplete decoding345 new_text = full_decoded[yielded_text_len:]346 347 # Hold back text ending with replacement character (incomplete UTF-8 sequence)348 safe_end = len(new_text)349 while safe_end > 0 and new_text[safe_end - 1] == "\ufffd":350 safe_end -= 1351 352 safe_text = new_text[:safe_end] if safe_end > 0 else ""353 yielded_text_len += len(safe_text)354 yield safe_text, is_finished355 356 357def torch_clone_recursive(obj):358 """Recursively clone nested containers of torch.Tensors.359 360 Supported container types: dict, list, tuple. Non-container non-Tensor361 objects are returned as-is.362 """363 if torch.is_tensor(obj):364 return obj.clone()365 elif isinstance(obj, dict):366 return {k: torch_clone_recursive(v) for k, v in obj.items()}367 elif isinstance(obj, list):368 return [torch_clone_recursive(v) for v in obj]369 elif isinstance(obj, tuple):370 return tuple(torch_clone_recursive(v) for v in obj)371 else:372 raise ValueError(f"Unsupported type: {type(obj)}")373 374 375def rotate_half(x: torch.Tensor) -> torch.Tensor:376 """Rotate half the hidden dims of the input for RoPE."""377 dim = x.shape[-1]378 x1 = x[..., : dim // 2]379 x2 = x[..., dim // 2 :]380 return torch.cat((-x2, x1), dim=-1)381 382 383@dataclass384class SpeculativeSnapshot:385 """Speculative snapshot for VAD speculative rollback.386 387 Used in VAD speculative execution: creates a snapshot after streaming_prefill388 and before streaming_generate. If speculation fails (user continues speaking),389 the state can be restored to continue streaming_prefill.390 391 Implementation:392 - LLM KV Cache: only record length, restore by truncation (zero extra VRAM)393 - Audio KV Cache: requires cloning, as generate sets it to None394 - Mel processor: save full state snapshot (including buffer)395 """396 397 # KV Cache length (for truncation recovery)398 llm_cache_length: int399 audio_cache_length: int400 401 # session state402 new_user_msg: bool403 llm_generated: bool404 llm_generate_completed: bool405 406 # Round management407 next_round_id: int408 pending_round_id: Optional[int]409 omni_chunk_history_length: int410 411 # TTS state (requires cloning, but usually small)412 tts_last_turn_tokens: Optional[torch.Tensor]413 414 # Streaming processor state415 audio_chunk_idx: int416 417 # Mel processor state snapshot (including buffer)418 mel_processor_snapshot: Optional[dict] = None419 420 # Audio encoder KV cache (requires cloning to ensure determinism after recovery)421 audio_past_key_values: Optional[tuple] = None422 423 # timestamp (for debugging)424 timestamp: float = 0.0425 426 # debug field: for verifying correctness of recovery427 llm_cache_checksum: Optional[float] = None # LLM KV Cache first layer K sum428 audio_cache_checksum: Optional[float] = None # Audio KV Cache first layer K sum429 mel_buffer_checksum: Optional[float] = None # Mel buffer sum430 431 # RNG state (key: for ensuring determinism of dithering etc. after recovery)432 rng_state_cpu: Optional[torch.Tensor] = None # torch CPU RNG state433 rng_state_cuda: Optional[torch.Tensor] = None # torch CUDA RNG state (if on GPU)434 435 def summary(self) -> str:436 mel_buf_len = 0437 if self.mel_processor_snapshot:438 buf = self.mel_processor_snapshot.get("buffer")439 if buf is not None:440 mel_buf_len = len(buf)441 return (442 f"llm_cache={self.llm_cache_length}, "443 f"audio_cache={self.audio_cache_length}, "444 f"audio_chunk_idx={self.audio_chunk_idx}, "445 f"mel_buffer={mel_buf_len}, "446 f"history_len={self.omni_chunk_history_length}, "447 f"new_user_msg={self.new_user_msg}, "448 f"llm_generated={self.llm_generated}"449 )450 451 452# tts453@dataclass454class TTSSamplingParams:455 top_p: float = 0.85456 min_p: float = 0.01457 top_k: int = 25458 repetition_penalty: float = 1.05459 temperature: float = 0.8460 win_size: int = 16461 tau_r: float = 0.1462 463 464class TTSStreamingGenerator:465 """466 Streaming generator for TTS that processes chunks and yields audio tokens in real-time.467 468 Supported attention types:469 - full_attention: Full attention, all tokens can attend to each other470 - sliding_window: Sliding window attention, KV cache is truncated to fixed size (token_window_size)471 - sliding_recompute: Sliding recompute, only keep previous chunk and recompute with current chunk472 - reindex: Keep first chunk as sink, reindex sliding window positions via RoPE rotation473 """474 475 def __init__(476 self,477 model,478 temperature: float,479 eos_token: Union[int, torch.Tensor],480 chunk_size: int = 25, # s3tokenizer 1s = 25token481 tts_last_turn_tokens: torch.Tensor = None,482 logits_processors=None,483 logits_warpers=None,484 ):485 self.tts = model486 self.device = model.device487 self.temperature = torch.tensor([temperature], dtype=torch.float, device=self.device)488 self.eos_token = (489 torch.tensor(eos_token, device=self.device) if isinstance(eos_token, int) else eos_token.to(self.device)490 )491 492 self.num_vq = model.num_vq493 self.num_audio_tokens = model.num_audio_tokens494 self.recomputed_chunks = model.recomputed_chunks495 self.emb_code = model.emb_code496 self.head_code = model.head_code497 498 # Attention type and window sizes499 self.attention_type = model.attention_type # "full_attention", "sliding_window", "sliding_recompute", "reindex"500 self.chunk_window_size = model.chunk_window_size # chunk-level window for sliding_recompute (default 2)501 self.token_window_size = model.token_window_size # token-level window for sliding_window/reindex (default 300)502 503 # RoPE config (for reindex mode)504 self.rope_theta = model.model.config.rope_theta505 self.head_dim = model.model.config.hidden_size // model.model.config.num_attention_heads506 507 # Logits processors508 self.logits_processors = logits_processors if logits_processors is not None else []509 # Logits warpers (like TopP/TopK), separate from processors510 self.logits_warpers = logits_warpers if logits_warpers is not None else []511 512 # initialize state513 self.past_key_values = None514 self.text_start_pos = 0515 self.idx = -1 # start from -1, become 0 when first called516 self.all_conditions = []517 self.all_generated_tokens = []518 self.tts_last_turn_tokens = tts_last_turn_tokens519 self.spk_emb = None520 521 audio_bos = [self.tts.audio_bos_token_id]522 audio_bos = torch.Tensor(audio_bos).to(self.tts.emb_text.weight.device, dtype=torch.long)523 524 self.audio_bos_embeds = self.tts.emb_text(audio_bos).unsqueeze(0)525 self.text_eos_embed = self.tts.emb_text(526 torch.tensor(527 [self.tts.config.text_eos_token_id],528 device=self.tts.emb_text.weight.device,529 dtype=torch.long,530 )531 ).unsqueeze(0)532 533 # buffer related, used to fill up chunk_size and yield to outside534 self.chunk_size = chunk_size535 self._token_buffer: List[torch.Tensor] = []536 537 # Chunk info tracking for sliding_recompute and reindex538 self._chunk_info: List[dict] = []539 self._total_seq_len = 0540 541 # Reindex mode: track sink (first chunk) length542 self._sink_kv_len = 0543 544 def _build_recompute_inputs(self, current_condition: torch.Tensor) -> torch.Tensor:545 """Build recompute inputs for sliding_recompute mode."""546 if len(self._chunk_info) == 0:547 return current_condition548 549 prev_chunk = self._chunk_info[-1]550 prev_condition = prev_chunk["condition"]551 prev_audio_tokens = prev_chunk["audio_tokens"]552 553 recompute_list = [prev_condition]554 if len(prev_audio_tokens) > 0:555 prev_audio_embeds = torch.cat([self.emb_code[0](tok) for tok in prev_audio_tokens], dim=1)556 recompute_list.append(prev_audio_embeds)557 558 recompute_list.append(current_condition)559 return torch.cat(recompute_list, dim=1)560 561 def _truncate_kv_cache_sliding_window(self):562 """Truncate KV cache for sliding_window mode."""563 if self.past_key_values is None:564 return565 566 if hasattr(self.past_key_values, "get_seq_length"):567 current_kv_len = self.past_key_values.get_seq_length()568 else:569 current_kv_len = self.past_key_values[0][0].shape[2]570 571 if current_kv_len <= self.token_window_size:572 return573 574 new_cache = DynamicCache()575 num_layers = (576 len(self.past_key_values.key_cache)577 if hasattr(self.past_key_values, "key_cache")578 else len(self.past_key_values)579 )580 581 for layer_idx in range(num_layers):582 if hasattr(self.past_key_values, "key_cache"):583 key = self.past_key_values.key_cache[layer_idx][:, :, -self.token_window_size :, :]584 value = self.past_key_values.value_cache[layer_idx][:, :, -self.token_window_size :, :]585 else:586 key = self.past_key_values[layer_idx][0][:, :, -self.token_window_size :, :]587 value = self.past_key_values[layer_idx][1][:, :, -self.token_window_size :, :]588 new_cache.update(key, value, layer_idx)589 590 self.past_key_values = new_cache591 592 @staticmethod593 def _apply_rope_rotation(x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:594 """Apply RoPE rotation to tensor."""595 return x * cos + rotate_half(x) * sin596 597 def _compute_rope_cos_sin(self, positions: torch.Tensor, device: torch.device, dtype: torch.dtype):598 """Compute RoPE cos and sin for given positions."""599 dim_half = self.head_dim // 2600 freq_seq = torch.arange(0, dim_half, dtype=torch.float32, device=device)601 inv_freq = 1.0 / (self.rope_theta ** (freq_seq / dim_half))602 603 # positions: [seq_len]604 angles = positions.float().unsqueeze(-1) * inv_freq.unsqueeze(0) # [seq_len, dim_half]605 angles = torch.cat([angles, angles], dim=-1) # [seq_len, head_dim]606 607 cos = angles.cos().to(dtype)608 sin = angles.sin().to(dtype)609 return cos, sin610 611 def _reindex_kv_cache(self):612 """613 Reindex KV cache for reindex mode:614 1. Keep first chunk as attention sink615 2. Keep last chunk616 3. Discard middle chunks617 4. Reindex the last chunk's key positions to be right after sink via RoPE rotation618 """619 if self.past_key_values is None or len(self._chunk_info) < 2:620 return621 622 # Get current KV cache length623 if hasattr(self.past_key_values, "get_seq_length"):624 current_kv_len = self.past_key_values.get_seq_length()625 else:626 current_kv_len = self.past_key_values[0][0].shape[2]627 628 # Calculate sink length (first chunk)629 sink_len = self._chunk_info[0]["condition_len"] + self._chunk_info[0]["audio_token_count"]630 631 # Last chunk length632 last_chunk = self._chunk_info[-1]633 last_chunk_len = last_chunk["condition_len"] + last_chunk["audio_token_count"]634 635 keep_len = sink_len + last_chunk_len636 637 # Get device and dtype638 device = self.past_key_values.key_cache[0].device639 dtype = self.past_key_values.key_cache[0].dtype640 641 if current_kv_len <= keep_len:642 last_chunk_kv_len = current_kv_len - sink_len643 if last_chunk_kv_len <= 0:644 return645 self.text_start_pos = current_kv_len646 return647 648 # Step 1: Truncate KV cache - keep sink and last chunk649 new_cache = DynamicCache()650 num_layers = len(self.past_key_values.key_cache)651 652 original_start_pos = current_kv_len - last_chunk_len653 new_start_pos = sink_len654 delta = new_start_pos - original_start_pos # This is a scalar constant655 delta_positions = torch.full((last_chunk_len,), delta, dtype=torch.float32, device=device)656 657 # Compute rotation cos/sin658 cos, sin = self._compute_rope_cos_sin(delta_positions, device, dtype)659 cos = cos.unsqueeze(0).unsqueeze(0) # [1, 1, seq_len, head_dim]660 sin = sin.unsqueeze(0).unsqueeze(0)661 662 for layer_idx in range(num_layers):663 key_full = self.past_key_values.key_cache[layer_idx]664 value_full = self.past_key_values.value_cache[layer_idx]665 666 # Extract sink and last chunk667 key_sink = key_full[:, :, :sink_len, :]668 value_sink = value_full[:, :, :sink_len, :]669 key_last = key_full[:, :, -last_chunk_len:, :]670 value_last = value_full[:, :, -last_chunk_len:, :]671 672 # Apply RoPE rotation to reindex key positions673 key_last_reindexed = self._apply_rope_rotation(key_last, cos, sin)674 675 # Concatenate sink and reindexed last chunk676 key = torch.cat([key_sink, key_last_reindexed], dim=2)677 value = torch.cat([value_sink, value_last], dim=2)678 679 new_cache.update(key, value, layer_idx)680 681 self.past_key_values = new_cache682 683 # Update text_start_pos to reflect new positions684 self.text_start_pos = sink_len + last_chunk_len685 686 @torch.inference_mode()687 def generate_with_buffer(688 self,689 condition: torch.Tensor,690 text_finished: bool = False,691 max_new_token: int = 500,692 ):693 """input a condition embedding chunk, generate audio token each time,694 and accumulate to buffer, only yield when buffer satisfies chunk_size.695 696 Yields:697 torch.Tensor of shape [chunk_size] (2D: [1, chunk_size])698 """699 self.idx += 1700 self.device = self.tts.device701 702 # if text finished, first concatenate Text EOS703 if text_finished:704 condition = torch.cat([condition, self.text_eos_embed], dim=1)705 706 # always concatenate Audio BOS707 condition = torch.cat([condition, self.audio_bos_embeds], dim=1).to(self.device)708 709 self.all_conditions.append(condition)710 711 # Initialize current chunk info712 current_chunk_info = {713 "condition_len": condition.shape[1],714 "audio_token_count": 0,715 "condition": condition.clone(),716 "audio_tokens": [],717 }718 719 # Handle different attention types720 if self.attention_type == "sliding_recompute" and self.idx >= 1:721 # sliding_recompute: discard KV cache, recompute with previous + current chunk722 self.past_key_values = None723 current_condition = self._build_recompute_inputs(condition)724 self.text_start_pos = 0725 elif self.attention_type == "reindex" and self.idx >= 1:726 # reindex: truncate KV cache keeping sink + last chunk, reindex positions via RoPE727 self._reindex_kv_cache()728 current_condition = condition729 # Always update text_start_pos based on actual KV cache length (like reference code)730 if self.past_key_values is not None:731 if hasattr(self.past_key_values, "get_seq_length"):732 kv_len = self.past_key_values.get_seq_length()733 else:734 kv_len = self.past_key_values[0][0].shape[2]735 self.text_start_pos = kv_len736 else:737 current_condition = condition738 739 condition_length = current_condition.shape[1]740 prefill_len = condition_length741 finished = torch.zeros(1, dtype=torch.bool, device=self.device)742 chunk_generated_tokens = []743 744 for t in range(max_new_token):745 if t == 0:746 inputs_embeds = current_condition747 pos_ids = torch.arange(748 self.text_start_pos,749 self.text_start_pos + condition_length,750 dtype=torch.long,751 device=self.device,752 ).unsqueeze(0)753 else:754 last = self.all_generated_tokens[-1]755 # last: [1,1], directly as code id756 inputs_embeds = self.emb_code[0](last)757 pos_ids = torch.tensor(758 [self.text_start_pos + prefill_len + t - 1],759 dtype=torch.long,760 device=self.device,761 ).unsqueeze(0)762 763 outputs = self.tts.model(764 position_ids=pos_ids,765 past_key_values=self.past_key_values,766 inputs_embeds=inputs_embeds,767 use_cache=True,768 )769 hidden_states = outputs.last_hidden_state770 771 # Handle KV cache based on attention type772 if self.attention_type == "sliding_window":773 self.past_key_values = outputs.past_key_values774 self._truncate_kv_cache_sliding_window()775 else:776 self.past_key_values = outputs.past_key_values777 778 with P.cached():779 logits = torch.empty(780 hidden_states.size(0),781 hidden_states.size(1),782 self.num_audio_tokens,783 self.num_vq,784 dtype=torch.float,785 device=self.device,786 )787 for num_vq_iter in range(self.num_vq):788 x: torch.Tensor = self.head_code[num_vq_iter](hidden_states)789 logits[..., num_vq_iter] = x790 del x791 792 del hidden_states793 794 logits = logits[:, -1].float()795 796 logits = logits.permute(0, 2, 1)797 logits = logits.reshape(-1, logits.size(2))798 799 logits /= self.temperature800 801 audio_bos = len(self.all_generated_tokens) == 0 and t == 0802 803 if not audio_bos:804 # use generated tokens (current chunk) as input for processor/warper (align with modeling_minicpmo)805 all_generated_tokens = torch.cat(self.all_generated_tokens, dim=1).to(self.device) # [1, T]806 for processor in self.logits_processors:807 logits = processor(all_generated_tokens, logits)808 809 for warper in self.logits_warpers:810 logits = warper(all_generated_tokens, logits)811 del all_generated_tokens812 813 # sample next token (only use first codebook, same as generate)814 scores = F.softmax(logits, dim=-1)815 idx_next = torch.multinomial(scores, num_samples=1) # [(B*num_vq), 1]816 next_id = idx_next.view(-1, self.num_vq)[:, 0:1] # only take first codebook → [B, 1]817 del scores818 819 if next_id.eq(820 self.eos_token821 ).any(): # generated audio eos token, means this chunk is finished, no longer generate new tokens822 finished[:] = True823 else: # eos token cannot be added to buffer, he does not speak.824 # convert next_id to correct shape [1, 1], no num_vq dimension825 if next_id.dim() == 0: # if scalar826 next_tok = next_id.unsqueeze(0).unsqueeze(0) # [1, 1]827 elif next_id.dim() == 1: # if 1D [1]828 next_tok = next_id.unsqueeze(0) # [1, 1]829 else:830 next_tok = next_id831 832 self.all_generated_tokens.append(next_tok)833 chunk_generated_tokens.append(next_tok)834 835 # Update chunk info for sliding_recompute836 current_chunk_info["audio_tokens"].append(next_tok.clone())837 current_chunk_info["audio_token_count"] += 1838 839 self._token_buffer.append(next_tok)840 841 if len(self._token_buffer) == 0:842 # case 1: if last text chunk, yield None843 if text_finished:844 yield torch.empty(1, 0, dtype=torch.long, device=self.device), True845 break846 # case 2: if not last text chunk, break directly847 else:848 break849 else: # buffer has something850 # case 1: if buffer is larger/equal to chunk_size, yield out851 if len(self._token_buffer) >= self.chunk_size:852 batch = torch.cat(self._token_buffer[: self.chunk_size], dim=1) # [1, chunk_size]853 yield batch, False # → [1, chunk_size]854 # discard yielded part855 self._token_buffer = self._token_buffer[self.chunk_size :]856 857 # case 2: if buffer is smaller than chunk_size858 else:859 # if generation finished, and is the last text chunk, yield all remaining tokens, then break860 if finished.all():861 if text_finished:862 batch = torch.cat(self._token_buffer, dim=1) # [1, chunk_size]863 yield batch, True # → [1, chunk_size]864 self._token_buffer = []865 break866 else:867 # not the last text chunk, need to wait for next text chunk to fill up buffer, then this call ends868 break869 else: # generation of this audio chunk is not finished, continue generating870 continue871 872 # Save current chunk info for sliding_recompute and reindex873 self._chunk_info.append(current_chunk_info)874 self._total_seq_len += condition.shape[1] + len(chunk_generated_tokens)875 876 # Update text_start_pos based on attention type877 if self.attention_type == "sliding_recompute":878 # sliding_recompute: will be reset at next chunk start, update normally here879 self.text_start_pos += prefill_len + len(chunk_generated_tokens)880 elif self.attention_type == "reindex":881 # reindex: position based on actual KV cache length (positions have been reindexed to be continuous)882 if self.past_key_values is not None:883 if hasattr(self.past_key_values, "get_seq_length"):884 self.text_start_pos = self.past_key_values.get_seq_length()885 else:886 self.text_start_pos = self.past_key_values[0][0].shape[2]887 else:888 self.text_start_pos += condition.shape[1] + len(chunk_generated_tokens)889 else:890 self.text_start_pos += condition.shape[1] + len(chunk_generated_tokens)891 # note: remaining tokens in buffer will be kept, and accumulated next time892 893 894# sliding window895@dataclass896class StreamingWindowConfig:897 text_window_high_tokens: int = 8000898 text_window_low_tokens: int = 6000899 900 901@dataclass902class DuplexWindowConfig:903 """duplex sliding window configuration904 905 sliding window mode:906 - "off": disable sliding window907 - "basic": basic sliding window (trigger by cache length)908 - "context": sliding window with context (trigger by unit number, preserve generated text to previous)909 """910 911 # sliding window mode912 sliding_window_mode: str = "off" # "off" / "basic" / "context"913 914 # basic sliding window parameters915 basic_window_high_tokens: int = 8000 # high watermark: trigger sliding window when exceeded916 basic_window_low_tokens: int = 6000 # low watermark: keep to this value after sliding window917 918 # context sliding window parameters919 context_previous_max_tokens: int = 500 # previous maximum token number920 context_max_units: int = 24 # maximum unit number (trigger sliding window when exceeded)921 922 # verification mode (for comparison test)923 verify_mode: bool = False # whether to enable verification log924 925 926def as_dynamic_cache(past_key_values):927 """Convert legacy tuple cache to DynamicCache if needed."""928 if isinstance(past_key_values, DynamicCache):929 return past_key_values930 931 if isinstance(past_key_values, tuple):932 return DynamicCache.from_legacy_cache(past_key_values)933 934 return past_key_values935 936 937def get_kv_cache_length(cache) -> int:938 """Get the sequence length of a KV cache.939 940 Args:941 cache: DynamicCache or tuple-based cache942 943 Returns:944 The number of tokens in the cache945 """946 if cache is None:947 return 0948 949 if isinstance(cache, DynamicCache):950 if not cache.key_cache or not cache.key_cache[0].numel():951 return 0952 return cache.key_cache[0].shape[-2]953 954 if isinstance(cache, tuple):955 return cache[0][0].shape[2]956 957 return 0958 959 960def get_rotary_cos_sin(961 head_dim: int,962 positions: torch.Tensor,963 device: torch.device,964 dtype: torch.dtype,965 rope_theta: float = 10000.0,966 inv_freq_cache: Optional[Dict[Tuple, torch.Tensor]] = None,967) -> Tuple[torch.Tensor, torch.Tensor]:968 """Compute RoPE cos and sin components for given positions.969 970 Args:971 head_dim: Dimension of each attention head972 positions: Position indices tensor973 device: Target device974 dtype: Target dtype975 rope_theta: RoPE base frequency (default 10000.0)976 inv_freq_cache: Optional cache dict for inverse frequencies977 978 Returns:979 Tuple of (cos, sin) tensors with shape [1, 1, seq_len, head_dim]980 """981 cache_key = (head_dim, device)982 983 inv_freq = inv_freq_cache.get(cache_key) if inv_freq_cache is not None else None984 if inv_freq is None or inv_freq.device != device or inv_freq.shape[0] != head_dim // 2:985 exponent = torch.arange(0, head_dim, 2, device=device, dtype=torch.float32) / head_dim986 inv_freq = 1.0 / (rope_theta**exponent)987 if inv_freq_cache is not None:988 inv_freq_cache[cache_key] = inv_freq989 990 positions = positions.to(device=device, dtype=torch.float32)991 angles = torch.einsum("i,j->ij", positions, inv_freq)992 cos = torch.cos(angles)993 sin = torch.sin(angles)994 995 # Use cat instead of repeat_interleave, consistent with model's original RotaryEmbedding996 # Original: emb = torch.cat((freqs, freqs), dim=-1) -> [f0, f1, ..., f_{d/2}, f0, f1, ..., f_{d/2}]997 cos_full = torch.cat([cos, cos], dim=-1).to(dtype=dtype)998 sin_full = torch.cat([sin, sin], dim=-1).to(dtype=dtype)999 cos_full = cos_full.unsqueeze(0).unsqueeze(0)1000 sin_full = sin_full.unsqueeze(0).unsqueeze(0)1001 return cos_full, sin_full1002 1003 1004def realign_rotary_suffix(1005 suffix_keys: torch.Tensor,1006 old_positions: torch.Tensor,1007 new_positions: torch.Tensor,1008 rope_theta: float = 10000.0,1009 inv_freq_cache: Optional[Dict[Tuple, torch.Tensor]] = None,1010) -> torch.Tensor:1011 """Realign RoPE position encoding after cache eviction.1012 1013 When tokens are dropped from the middle of a cache, the suffix tokens1014 need their RoPE embeddings recalculated with new position indices.1015 1016 Args:1017 suffix_keys: Key tensor to realign, shape [batch, heads, seq_len, head_dim]1018 old_positions: Original position indices1019 new_positions: New position indices after eviction1020 rope_theta: RoPE base frequency1021 inv_freq_cache: Optional cache dict for inverse frequencies1022 1023 Returns:1024 Realigned key tensor with same shape as input1025 """1026 if suffix_keys.numel() == 0:1027 return suffix_keys1028 1029 head_dim = suffix_keys.shape[-1]1030 device = suffix_keys.device1031 dtype = suffix_keys.dtype1032 1033 # Compute old position cos/sin1034 cos_old, sin_old = get_rotary_cos_sin(head_dim, old_positions, device, dtype, rope_theta, inv_freq_cache)1035 1036 # Inverse transform: recover original key1037 base = cos_old * suffix_keys - sin_old * rotate_half(suffix_keys)1038 1039 # Compute new position cos/sin1040 cos_new, sin_new = get_rotary_cos_sin(head_dim, new_positions, device, dtype, rope_theta, inv_freq_cache)1041 1042 # Forward transform: re-encode with new positions1043 return cos_new * base + sin_new * rotate_half(base)1044 1045 1046def drop_tokens_from_cache(1047 cache: Optional[DynamicCache | Tuple],1048 length: int,1049 preserve: int,1050 position_offset: int,1051 rope_theta: float = 10000.0,1052 inv_freq_cache: Optional[Dict[Tuple, torch.Tensor]] = None,1053) -> Tuple[Optional[DynamicCache], int, bool]:1054 """Drop tokens from a KV cache while preserving system prompt.1055 1056 Removes tokens in the range [preserve, preserve + length) from the cache,1057 realigning RoPE embeddings for the suffix.1058 1059 Args:1060 cache: DynamicCache or tuple-based cache (will be converted to DynamicCache)1061 length: Number of tokens to drop1062 preserve: Number of tokens to preserve at the start (system prompt)1063 position_offset: Current position offset for RoPE calculation1064 rope_theta: RoPE base frequency1065 inv_freq_cache: Optional cache dict for inverse frequencies1066 1067 Returns:1068 Tuple of (cache, new_position_offset, success)1069 Note: Tuple cache will be converted to DynamicCache. Modification is in-place.1070 """1071 if cache is None or length <= 0:1072 return cache, position_offset, False1073 1074 cache = as_dynamic_cache(cache)1075 1076 total_len = get_kv_cache_length(cache)1077 if total_len <= 0:1078 return cache, position_offset, False1079 1080 preserve = min(preserve, total_len)1081 available = total_len - preserve1082 1083 if available < length:1084 logger.warning(1085 "Cannot drop %d tokens: only %d available (total=%d, preserve=%d)",1086 length,1087 available,1088 total_len,1089 preserve,1090 )1091 return cache, position_offset, False1092 1093 suffix_len = total_len - preserve - length1094 # note: after RoPE reindex, the position of cache has been compressed (from preserve start)1095 # so here should not add position_offset, but use the actual layout of current cache1096 suffix_offset = preserve + length # suffix current position in cache1097 prefix_offset = preserve # suffix new position (follow preserve)1098 1099 # Prepare position tensors for RoPE realignment1100 old_positions = None1101 new_positions = None1102 if suffix_len > 0:1103 device = cache.key_cache[0].device1104 old_positions = torch.arange(1105 suffix_offset,1106 suffix_offset + suffix_len,1107 device=device,1108 dtype=torch.long,1109 )1110 new_positions = torch.arange(1111 prefix_offset,1112 prefix_offset + suffix_len,1113 device=device,1114 dtype=torch.long,1115 )1116 1117 keep_len = total_len - length1118 1119 # Process each layer (in-place modification)1120 for layer_idx in range(len(cache.key_cache)):1121 key_tensor = cache.key_cache[layer_idx]1122 value_tensor = cache.value_cache[layer_idx]1123 1124 if not key_tensor.numel():1125 continue1126 1127 # Preserve prefix (system prompt)1128 prefix_keys = key_tensor[:, :, :preserve, :]1129 prefix_values = value_tensor[:, :, :preserve, :]1130 1131 if suffix_len > 0:1132 # Keep and realign suffix1133 suffix_keys = key_tensor[:, :, preserve + length :, :]1134 suffix_values = value_tensor[:, :, preserve + length :, :]1135 1136 if old_positions is not None and new_positions is not None and suffix_keys.numel():1137 suffix_keys = realign_rotary_suffix(1138 suffix_keys,1139 old_positions,1140 new_positions,1141 rope_theta,1142 inv_freq_cache,1143 )1144 1145 cache.key_cache[layer_idx] = torch.cat([prefix_keys, suffix_keys], dim=-2).contiguous()1146 cache.value_cache[layer_idx] = torch.cat([prefix_values, suffix_values], dim=-2).contiguous()1147 else:1148 cache.key_cache[layer_idx] = prefix_keys.contiguous()1149 cache.value_cache[layer_idx] = prefix_values.contiguous()1150 1151 cache.crop(keep_len)1152 cache._seen_tokens = max(keep_len, 0)1153 1154 new_offset = position_offset + length1155 logger.debug("Dropped %d tokens from cache, new length=%d", length, keep_len)1156 1157 return cache, new_offset, True1158 1159 1160# stream decoder1161def top_k_top_p_filtering(logits, top_k=0, top_p=0.0, filter_value=-float("inf")):1162 logits = logits.clone()1163 1164 # Top-k filtering1165 if top_k > 0:1166 top_k = min(top_k, logits.size(-1))1167 indices_to_remove = logits < torch.topk(logits, top_k)[0][..., -1, None]1168 logits[indices_to_remove] = filter_value1169 1170 # Top-p (nucleus) filtering1171 if top_p > 0.0:1172 sorted_logits, sorted_indices = torch.sort(logits, descending=True)1173 probs = F.softmax(sorted_logits, dim=-1)1174 cumulative_probs = torch.cumsum(probs, dim=-1)1175 1176 sorted_indices_to_remove = cumulative_probs > top_p1177 # keep the first token that exceeds top_p1178 sorted_indices_to_remove[..., 1:] = sorted_indices_to_remove[..., :-1].clone()1179 sorted_indices_to_remove[..., 0] = 01180 1181 indices_to_remove = sorted_indices[sorted_indices_to_remove]1182 logits[0, indices_to_remove] = filter_value1183 1184 return logits1185 1186 1187class StreamDecoder:1188 def __init__(self, llm, tokenizer, special_token_ids=None, forbidden_token_ids=None):1189 self.m = llm1190 self.tokenizer = tokenizer1191 self.listen_id = self.tokenizer.eos_token_id1192 1193 self.chunk_eos_id = self.tokenizer.convert_tokens_to_ids("<|chunk_eos|>")1194 self.chunk_tts_eos_id = self.tokenizer.convert_tokens_to_ids("<|chunk_tts_eos|>")1195 self.turn_eos_id = self.tokenizer.convert_tokens_to_ids("<|turn_eos|>")1196 self.speak_id = self.tokenizer.convert_tokens_to_ids("<|speak|>")1197 1198 self.special_token_ids = special_token_ids if special_token_ids is not None else []1199 1200 # cache special tokens (used for context sliding window filtering)