Преглед изворни кода

feat:添加generate_long_parallel_batched

zhaohaipeng пре 2 месеци
родитељ
комит
144f72d069
1 измењених фајлова са 318 додато и 86 уклоњено
  1. 318 86
      fish_speech/models/text2semantic/inference.py

+ 318 - 86
fish_speech/models/text2semantic/inference.py

@@ -4,14 +4,12 @@ import re
 import threading
 import time
 import traceback
-from copy import deepcopy
 from dataclasses import dataclass
 from pathlib import Path
 from typing import Callable, Literal, Optional, Tuple, Union
 
 import click
 import numpy as np
-import torch
 import torch._inductor.config
 from loguru import logger
 from tqdm import tqdm
@@ -30,7 +28,6 @@ torch._inductor.config.triton.unique_kernel_names = True
 if hasattr(torch._inductor.config, "fx_graph_cache"):
     torch._inductor.config.fx_graph_cache = True
 
-
 from torch.nn.attention import SDPBackend, sdpa_kernel
 
 from fish_speech.models.text2semantic.llama import (
@@ -50,10 +47,10 @@ RAS_HIGH_TOP_P = 0.9
 
 
 def logits_to_probs(
-    logits,
-    temperature: torch.Tensor,
-    top_p: torch.Tensor,
-    top_k: int,  # 注意: 我看到你传进来的是 int,这很关键
+        logits,
+        temperature: torch.Tensor,
+        top_p: torch.Tensor,
+        top_k: int,  # 注意: 我看到你传进来的是 int,这很关键
 ) -> torch.Tensor:
     sorted_logits, sorted_indices = torch.sort(logits, descending=True)
     cum_probs = torch.cumsum(torch.nn.functional.softmax(sorted_logits, dim=-1), dim=-1)
@@ -76,10 +73,10 @@ def logits_to_probs(
 
 
 def sample(
-    logits,
-    temperature: torch.Tensor,
-    top_p: torch.Tensor,
-    top_k: int,
+        logits,
+        temperature: torch.Tensor,
+        top_p: torch.Tensor,
+        top_k: int,
 ) -> Tuple[torch.Tensor, torch.Tensor]:
     probs = logits_to_probs(
         logits=logits[0, -1],
@@ -92,16 +89,16 @@ def sample(
 
 
 def decode_one_token_ar(
-    model: DualARTransformer,
-    x: torch.Tensor,
-    input_pos: torch.Tensor,
-    temperature: torch.Tensor,
-    top_p: torch.Tensor,
-    top_k: int,
-    semantic_logit_bias: torch.Tensor,
-    audio_masks: torch.Tensor,
-    audio_parts: torch.Tensor,
-    previous_tokens: Optional[torch.Tensor] = None,
+        model: DualARTransformer,
+        x: torch.Tensor,
+        input_pos: torch.Tensor,
+        temperature: torch.Tensor,
+        top_p: torch.Tensor,
+        top_k: int,
+        semantic_logit_bias: torch.Tensor,
+        audio_masks: torch.Tensor,
+        audio_parts: torch.Tensor,
+        previous_tokens: Optional[torch.Tensor] = None,
 ) -> torch.Tensor:
     forward_result = model.forward_generate(
         x,
@@ -134,7 +131,7 @@ def decode_one_token_ar(
         in_window = (previous_tokens[0] == main_token_normal).any()
         # Use tensor ops (&, torch.where) instead of Python (and, if) — torch.compile requires no data-dependent branching
         is_semantic = (main_token_normal >= model.config.semantic_begin_id) & (
-            main_token_normal <= model.config.semantic_end_id
+                main_token_normal <= model.config.semantic_end_id
         )
         should_use_high = in_window & is_semantic
         main_token_normal = torch.where(
@@ -180,17 +177,17 @@ def decode_one_token_ar(
 
 
 def decode_n_tokens(
-    model: DualARTransformer,
-    cur_token: torch.Tensor,
-    input_pos: torch.Tensor,
-    num_new_tokens: int,
-    temperature: torch.Tensor,
-    top_p: torch.Tensor,
-    top_k: int,
-    semantic_logit_bias: torch.Tensor,
-    audio_masks: torch.Tensor,
-    audio_parts: torch.Tensor,
-    decode_one_token=decode_one_token_ar,
+        model: DualARTransformer,
+        cur_token: torch.Tensor,
+        input_pos: torch.Tensor,
+        num_new_tokens: int,
+        temperature: torch.Tensor,
+        top_p: torch.Tensor,
+        top_k: int,
+        semantic_logit_bias: torch.Tensor,
+        audio_masks: torch.Tensor,
+        audio_parts: torch.Tensor,
+        decode_one_token=decode_one_token_ar,
 ):
     # Rolling window for RAS (Repetition Aware Sampling)
     previous_tokens = torch.zeros(
@@ -239,15 +236,15 @@ def decode_n_tokens(
 @torch.no_grad()
 @torch.inference_mode()
 def generate(
-    *,
-    model: DualARTransformer,
-    prompt: torch.Tensor,
-    max_new_tokens: int,
-    audio_masks: torch.Tensor,
-    audio_parts: torch.Tensor,
-    decode_one_token=decode_one_token_ar,
-    num_samples: int = 1,
-    **sampling_kwargs,
+        *,
+        model: DualARTransformer,
+        prompt: torch.Tensor,
+        max_new_tokens: int,
+        audio_masks: torch.Tensor,
+        audio_parts: torch.Tensor,
+        decode_one_token=decode_one_token_ar,
+        num_samples: int = 1,
+        **sampling_kwargs,
 ):
     """
     Takes a conditioning sequence (prompt) as input and continues to generate as many tokens as requested.
@@ -311,7 +308,7 @@ def generate(
 
     # [MODIFIED] Use config for semantic range
     semantic_logit_bias[
-        0, 0, model.config.semantic_begin_id : model.config.semantic_end_id + 1
+        0, 0, model.config.semantic_begin_id: model.config.semantic_end_id + 1
     ] = 0.0
 
     # [MODIFIED] Use tokenizer.get_token_id (Wrapper method)
@@ -330,7 +327,7 @@ def generate(
         audio_masks,
         audio_parts,
     )
-    seq[:, T : T + 1] = first_token
+    seq[:, T: T + 1] = first_token
 
     # Recreate input_pos
     input_pos = torch.tensor([T], device=device, dtype=torch.int)
@@ -349,7 +346,7 @@ def generate(
         decode_one_token=decode_one_token,
     )
     seq = seq[:, : T + 1 + x.size(1)]
-    seq[:, T + 1 :] = x
+    seq[:, T + 1:] = x
 
     # Clean up temporary variables
     del first_token, x, prompt, empty, input_pos
@@ -483,7 +480,7 @@ def split_text_by_speaker(text: str) -> list[str]:
 
 
 def group_turns_into_batches(
-    turns: list[str], max_speakers: int = 3, max_bytes: int = 300
+        turns: list[str], max_speakers: int = 3, max_bytes: int = 300
 ) -> list[str]:
     """
     Group turns into batches based on speaker count or byte limit.
@@ -521,22 +518,22 @@ def group_turns_into_batches(
 
 
 def generate_long(
-    *,
-    model,
-    device: Union[str, torch.device],
-    decode_one_token: Callable,
-    text: str,
-    num_samples: int = 1,
-    max_new_tokens: int = 0,
-    top_p: float = 0.9,
-    top_k: int = 30,
-    repetition_penalty: float = 1.1,
-    temperature: float = 1.0,
-    compile: bool = False,
-    iterative_prompt: bool = True,
-    chunk_length: int = 512,
-    prompt_text: Optional[Union[str, list[str]]] = None,
-    prompt_tokens: Optional[Union[torch.Tensor, list[torch.Tensor]]] = None,
+        *,
+        model,
+        device: Union[str, torch.device],
+        decode_one_token: Callable,
+        text: str,
+        num_samples: int = 1,
+        max_new_tokens: int = 0,
+        top_p: float = 0.9,
+        top_k: int = 30,
+        repetition_penalty: float = 1.1,
+        temperature: float = 1.0,
+        compile: bool = False,
+        iterative_prompt: bool = True,
+        chunk_length: int = 512,
+        prompt_text: Optional[Union[str, list[str]]] = None,
+        prompt_tokens: Optional[Union[torch.Tensor, list[torch.Tensor]]] = None,
 ):
     assert 0 < top_p <= 1, "top_p must be in (0, 1]"
     assert 0 < temperature < 2, "temperature must be in (0, 2)"
@@ -770,10 +767,10 @@ class GenerateRequest:
 
 
 def launch_thread_safe_queue(
-    checkpoint_path,
-    device,
-    precision,
-    compile: bool = False,
+        checkpoint_path,
+        device,
+        precision,
+        compile: bool = False,
 ):
     input_queue = queue.Queue()
     init_event = threading.Event()
@@ -799,8 +796,8 @@ def launch_thread_safe_queue(
             response_queue = item.response_queue
 
             try:
-                for chunk in generate_long(
-                    model=model, decode_one_token=decode_one_token, **kwargs
+                for chunk in generate_long_parallel_batched(
+                        model=model, decode_one_token=decode_one_token, **kwargs
                 ):
                     response_queue.put(
                         WrappedGenerateResponse(status="success", response=chunk)
@@ -823,6 +820,241 @@ def launch_thread_safe_queue(
     return input_queue
 
 
+import torch
+from copy import deepcopy
+from typing import List, Optional
+
+
+# =========================
+# 1. Prompt 构建(从 generate_long 抽出)
+# =========================
+def build_prompt(
+        *,
+        model,
+        device,
+        text,
+        chunk_length,
+        prompt_text=None,
+        prompt_tokens=None,
+):
+    tokenizer = model.tokenizer
+    max_length = model.config.max_seq_len
+
+    use_prompt = bool(prompt_text) and bool(prompt_tokens)
+
+    if use_prompt and isinstance(prompt_text, str):
+        prompt_text = [prompt_text]
+        prompt_tokens = [prompt_tokens]
+
+    if prompt_tokens:
+        prompt_tokens = [p.cpu() for p in prompt_tokens]
+
+    # -------- system prompt --------
+    base_conversation = Conversation()
+
+    if use_prompt:
+        tagged = []
+        for i, t in enumerate(prompt_text):
+            if not re.search(r"<\|speaker:\d+\|>", t):
+                tagged.append(f"<|speaker:{i}|>{t}")
+            else:
+                tagged.append(t)
+
+        system_parts = [
+            TextPart(text="convert the provided text to speech reference to the following:\n\nText:\n", cal_loss=False, ),
+            TextPart(text="\n".join(tagged), cal_loss=False),
+            TextPart(text="\n\nSpeech:\n", cal_loss=False)
+        ]
+
+        all_codes = torch.cat(prompt_tokens, dim=1)
+        system_parts.append(VQPart(codes=all_codes, cal_loss=False))
+
+    else:
+        system_parts = [
+            TextPart(text="convert the provided text to speech", cal_loss=False)
+        ]
+
+    base_conversation.append(
+        Message(
+            role="system",
+            parts=system_parts,
+            cal_loss=False,
+            add_im_start=True,
+            add_im_end=True,
+        )
+    )
+
+    # -------- chunk text --------
+    turns = split_text_by_speaker(text)
+    batches = (
+        group_turns_into_batches(turns, max_speakers=5, max_bytes=chunk_length)
+        if turns else [text]
+    )
+
+    # 只做 chunk
+    encoded_prompts = []
+    metas = []
+
+    for batch_text in batches:
+        conv = deepcopy(base_conversation)
+
+        conv.append(
+            Message(
+                role="user",
+                parts=[TextPart(text=batch_text, cal_loss=False)],
+                cal_loss=False,
+                add_im_start=True,
+                add_im_end=True,
+            )
+        )
+
+        conv_gen = deepcopy(conv)
+        conv_gen.append(
+            Message(
+                role="assistant",
+                parts=[],
+                cal_loss=False,
+                modality="voice",
+                add_im_start=True,
+                add_im_end=False,
+            )
+        )
+
+        encoded, audio_masks, audio_parts = conv_gen.encode_for_inference(
+            tokenizer,
+            num_codebooks=model.config.num_codebooks,
+        )
+
+        if encoded.size(1) > max_length - 2048:
+            raise ValueError("Prompt too long")
+
+        encoded = encoded.to(device)
+
+        encoded_prompts.append(encoded)
+        metas.append(
+            {
+                "audio_masks": audio_masks,
+                "audio_parts": audio_parts,
+                "prompt_len": encoded.size(1),
+                "text": batch_text,
+            }
+        )
+
+    return encoded_prompts, metas
+
+
+# =========================
+# 2. Batched decode(核心)
+# =========================
+def generate_batched(
+        *,
+        model,
+        prompts: List[torch.Tensor],
+        decode_one_token,
+        max_new_tokens,
+        temperature,
+        top_p,
+        top_k,
+):
+    """
+    prompts: List[Tensor] [1, T]
+    return: List[Tensor] decoded sequences
+    """
+
+    device = prompts[0].device
+
+    max_len = max(p.size(1) for p in prompts)
+
+    padded = []
+    for p in prompts:
+        if p.size(1) < max_len:
+            pad = torch.zeros((1, max_len - p.size(1)), dtype=p.dtype, device=device)
+            p = torch.cat([p, pad], dim=1)
+        padded.append(p)
+
+    y = torch.cat(padded, dim=0)  # [B, T]
+
+    for _ in range(max_new_tokens):
+        next_token = decode_one_token(
+            model,
+            y,
+            temperature=temperature,
+            top_p=top_p,
+            top_k=top_k,
+            audio_masks=None,
+            audio_parts=None,
+        )  # [B, 1]
+
+        y = torch.cat([y, next_token], dim=1)
+
+    outputs = [y[i:i + 1] for i in range(y.size(0))]
+    return outputs
+
+
+# =========================
+# 3. 并行 TTS 主入口
+# =========================
+@torch.inference_mode()
+def generate_long_parallel_batched(
+        *,
+        model,
+        device,
+        decode_one_token,
+        text,
+        prompt_text=None,
+        prompt_tokens=None,
+        max_new_tokens=512,
+        chunk_length=512,
+        temperature=1.0,
+        top_p=0.9,
+        top_k=30,
+):
+    """
+    最小侵入版本:
+    chunk + batch decode + concat
+    """
+
+    # ===== 1. build prompts =====
+    encoded_prompts, metas = build_prompt(
+        model=model,
+        device=device,
+        text=text,
+        chunk_length=chunk_length,
+        prompt_text=prompt_text,
+        prompt_tokens=prompt_tokens,
+    )
+
+    # ===== 2. batched decode =====
+    outputs = generate_batched(
+        model=model,
+        prompts=encoded_prompts,
+        decode_one_token=decode_one_token,
+        max_new_tokens=max_new_tokens,
+        temperature=temperature,
+        top_p=top_p,
+        top_k=top_k,
+    )
+
+    # ===== 3. merge outputs =====
+    all_codes = []
+
+    for y, meta in zip(outputs, metas):
+        prompt_len = meta["prompt_len"]
+
+        codes = y[1:, prompt_len:-1].clone()
+
+        all_codes.append(codes)
+
+    final_codes = torch.cat(all_codes, dim=1)
+
+    return final_codes
+
+
+# ============================================
+# ===============   原始代码  =================
+# ============================================
+
+
 @click.command()
 @click.option(
     "--text",
@@ -861,24 +1093,24 @@ def launch_thread_safe_queue(
 @click.option("--chunk-length", type=int, default=300)
 @click.option("--output-dir", type=Path, default="output")
 def main(
-    text: str,
-    prompt_text: Optional[tuple[str, ...]],
-    prompt_tokens: Optional[tuple[Path, ...]],
-    prompt_audio: Optional[tuple[Path, ...]],
-    output: Optional[Path],
-    num_samples: int,
-    max_new_tokens: int,
-    top_p: float,
-    top_k: int,
-    temperature: float,
-    checkpoint_path: Path,
-    device: str,
-    compile: bool,
-    seed: int,
-    half: bool,
-    iterative_prompt: bool,
-    chunk_length: int,
-    output_dir: Path,
+        text: str,
+        prompt_text: Optional[tuple[str, ...]],
+        prompt_tokens: Optional[tuple[Path, ...]],
+        prompt_audio: Optional[tuple[Path, ...]],
+        output: Optional[Path],
+        num_samples: int,
+        max_new_tokens: int,
+        top_p: float,
+        top_k: int,
+        temperature: float,
+        checkpoint_path: Path,
+        device: str,
+        compile: bool,
+        seed: int,
+        half: bool,
+        iterative_prompt: bool,
+        chunk_length: int,
+        output_dir: Path,
 ) -> None:
     os.makedirs(output_dir, exist_ok=True)
     precision = torch.half if half else torch.bfloat16