inference.py 38 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226
  1. import os
  2. import queue
  3. import re
  4. import threading
  5. import time
  6. import traceback
  7. from dataclasses import dataclass
  8. from pathlib import Path
  9. from typing import Callable, Literal, Optional, Tuple, Union
  10. import click
  11. import numpy as np
  12. import torch._inductor.config
  13. from loguru import logger
  14. from tqdm import tqdm
  15. from fish_speech.content_sequence import (
  16. TextPart,
  17. VQPart,
  18. )
  19. from fish_speech.conversation import Conversation, Message
  20. from fish_speech.tokenizer import IM_END_TOKEN
  21. os.environ["TOKENIZERS_PARALLELISM"] = "false"
  22. torch._inductor.config.coordinate_descent_tuning = True
  23. torch._inductor.config.triton.unique_kernel_names = True
  24. if hasattr(torch._inductor.config, "fx_graph_cache"):
  25. torch._inductor.config.fx_graph_cache = True
  26. from torch.nn.attention import SDPBackend, sdpa_kernel
  27. from fish_speech.models.text2semantic.llama import (
  28. DualARTransformer,
  29. )
  30. def multinomial_sample_one_no_sync(probs_sort):
  31. q = torch.rand_like(probs_sort)
  32. q = -torch.log(q)
  33. return torch.argmax(probs_sort / q, dim=-1, keepdim=True).to(dtype=torch.int)
  34. RAS_WIN_SIZE = 10 # window for Repetition Aware Sampling
  35. RAS_HIGH_TEMP = 1.0
  36. RAS_HIGH_TOP_P = 0.9
  37. def logits_to_probs(
  38. logits,
  39. temperature: torch.Tensor,
  40. top_p: torch.Tensor,
  41. top_k: int, # 注意: 我看到你传进来的是 int,这很关键
  42. ) -> torch.Tensor:
  43. sorted_logits, sorted_indices = torch.sort(logits, descending=True)
  44. cum_probs = torch.cumsum(torch.nn.functional.softmax(sorted_logits, dim=-1), dim=-1)
  45. indices = torch.arange(sorted_logits.shape[-1], device=sorted_logits.device)
  46. top_k_mask = indices >= top_k
  47. sorted_indices_to_remove = (cum_probs > top_p) | top_k_mask
  48. sorted_indices_to_remove[0] = False # 单元素修改问题不大,或者写成 | (indices != 0)
  49. indices_to_remove = sorted_indices_to_remove.scatter(
  50. dim=-1, index=sorted_indices, src=sorted_indices_to_remove
  51. )
  52. logits = torch.where(
  53. indices_to_remove, float("-Inf"), logits
  54. ) # 同样替换 masked_fill_ 为 torch.where
  55. logits = logits / torch.clip(temperature, min=1e-5)
  56. probs = torch.nn.functional.softmax(logits, dim=-1)
  57. return probs
  58. def sample(
  59. logits,
  60. temperature: torch.Tensor,
  61. top_p: torch.Tensor,
  62. top_k: int,
  63. ) -> Tuple[torch.Tensor, torch.Tensor]:
  64. probs = logits_to_probs(
  65. logits=logits[0, -1],
  66. temperature=temperature,
  67. top_p=top_p,
  68. top_k=top_k,
  69. )
  70. idx_next = multinomial_sample_one_no_sync(probs)
  71. return idx_next, probs
  72. def decode_one_token_ar(
  73. model: DualARTransformer,
  74. x: torch.Tensor,
  75. input_pos: torch.Tensor,
  76. temperature: torch.Tensor,
  77. top_p: torch.Tensor,
  78. top_k: int,
  79. semantic_logit_bias: torch.Tensor,
  80. audio_masks: torch.Tensor,
  81. audio_parts: torch.Tensor,
  82. previous_tokens: Optional[torch.Tensor] = None,
  83. ) -> torch.Tensor:
  84. forward_result = model.forward_generate(
  85. x,
  86. input_pos,
  87. audio_masks=audio_masks,
  88. audio_parts=audio_parts,
  89. )
  90. logits = forward_result.logits # (1, 1, vocab_size)
  91. hidden_states = forward_result.hidden_states
  92. # Apply constrained decoding: only allow semantic tokens + im_end
  93. biased_logits = logits + semantic_logit_bias
  94. # Normal sample
  95. main_token_normal = sample(
  96. biased_logits, temperature=temperature, top_p=top_p, top_k=top_k
  97. )[0]
  98. # RAS: also sample with high temp to use as fallback if token repeats
  99. high_temp = torch.tensor(
  100. RAS_HIGH_TEMP, device=temperature.device, dtype=temperature.dtype
  101. )
  102. high_top_p = torch.tensor(RAS_HIGH_TOP_P, device=top_p.device, dtype=top_p.dtype)
  103. main_token_high = sample(
  104. biased_logits, temperature=high_temp, top_p=high_top_p, top_k=top_k
  105. )[0]
  106. # Use high-temp sample if: token is semantic AND token is in previous window
  107. if previous_tokens is not None:
  108. in_window = (previous_tokens[0] == main_token_normal).any()
  109. # Use tensor ops (&, torch.where) instead of Python (and, if) — torch.compile requires no data-dependent branching
  110. is_semantic = (main_token_normal >= model.config.semantic_begin_id) & (
  111. main_token_normal <= model.config.semantic_end_id
  112. )
  113. should_use_high = in_window & is_semantic
  114. main_token_normal = torch.where(
  115. should_use_high, main_token_high, main_token_normal
  116. )
  117. codebooks = [main_token_normal]
  118. input_pos = torch.tensor([0], device=hidden_states.device, dtype=torch.long)
  119. model.forward_generate_fast(hidden_states, input_pos)
  120. a = codebooks[0] - model.config.semantic_begin_id
  121. a = torch.clamp(a, min=0, max=model.config.codebook_size - 1)
  122. hidden_states = model.fast_embeddings(a)
  123. codebooks.append(a)
  124. for codebook_idx in range(1, model.config.num_codebooks):
  125. input_pos = torch.tensor(
  126. [codebook_idx], device=hidden_states.device, dtype=torch.long
  127. )
  128. logits = model.forward_generate_fast(hidden_states, input_pos)
  129. short_logits = logits # DualAR predicts config.codebook_size number of tokens
  130. # Convert logits to probs (no constrain for fast codebooks)
  131. a = sample(
  132. short_logits,
  133. temperature=temperature,
  134. top_p=top_p,
  135. top_k=top_k,
  136. )[0]
  137. hidden_states = model.fast_embeddings(a)
  138. codebooks.append(a)
  139. codebooks = torch.stack(codebooks, dim=1)
  140. # Only delete references, let Python GC handle cleanup
  141. del logits, hidden_states, forward_result
  142. return codebooks.T
  143. def decode_n_tokens(
  144. model: DualARTransformer,
  145. cur_token: torch.Tensor,
  146. input_pos: torch.Tensor,
  147. num_new_tokens: int,
  148. temperature: torch.Tensor,
  149. top_p: torch.Tensor,
  150. top_k: int,
  151. semantic_logit_bias: torch.Tensor,
  152. audio_masks: torch.Tensor,
  153. audio_parts: torch.Tensor,
  154. decode_one_token=decode_one_token_ar,
  155. ):
  156. # Rolling window for RAS (Repetition Aware Sampling)
  157. previous_tokens = torch.zeros(
  158. (model.config.num_codebooks + 1, RAS_WIN_SIZE),
  159. dtype=torch.int,
  160. device=cur_token.device,
  161. )
  162. # Accumulate all generated tokens (the actual output)
  163. new_tokens = []
  164. # [MODIFIED] Pre-fetch ID for efficiency loop
  165. im_end_id = model.tokenizer.get_token_id(IM_END_TOKEN)
  166. for i in tqdm(range(num_new_tokens)):
  167. with sdpa_kernel(SDPBackend.MATH):
  168. next_token = decode_one_token(
  169. model=model,
  170. x=cur_token,
  171. input_pos=input_pos,
  172. previous_tokens=previous_tokens,
  173. temperature=temperature,
  174. top_p=top_p,
  175. top_k=top_k,
  176. semantic_logit_bias=semantic_logit_bias,
  177. audio_masks=audio_masks,
  178. audio_parts=audio_parts,
  179. ).clone()
  180. input_pos += 1
  181. cur_token = next_token.view(1, model.config.num_codebooks + 1, -1)
  182. # Roll RAS window left and insert new token at end
  183. previous_tokens = previous_tokens.roll(-1, dims=1)
  184. previous_tokens[:, -1] = next_token.view(model.config.num_codebooks + 1, -1)[
  185. :, 0
  186. ]
  187. new_tokens.append(next_token)
  188. if cur_token[0, 0, -1] == im_end_id:
  189. break
  190. del cur_token
  191. return torch.cat(new_tokens, dim=1)
  192. @torch.no_grad()
  193. @torch.inference_mode()
  194. def generate(
  195. *,
  196. model: DualARTransformer,
  197. prompt: torch.Tensor,
  198. max_new_tokens: int,
  199. audio_masks: torch.Tensor,
  200. audio_parts: torch.Tensor,
  201. decode_one_token=decode_one_token_ar,
  202. num_samples: int = 1,
  203. **sampling_kwargs,
  204. ):
  205. """
  206. Takes a conditioning sequence (prompt) as input and continues to generate as many tokens as requested.
  207. """
  208. # create an empty tensor of the expected final shape and fill in the current tokens
  209. T = prompt.size(1)
  210. prompt = prompt[None].repeat(num_samples, 1, 1)
  211. if T >= model.config.max_seq_len:
  212. raise ValueError(
  213. f"Input sequence length {T} exceeds max_seq_len {model.config.max_seq_len}"
  214. )
  215. if max_new_tokens:
  216. if T + max_new_tokens > model.config.max_seq_len:
  217. max_new_tokens = model.config.max_seq_len - T
  218. T_new = T + max_new_tokens
  219. else:
  220. T_new = model.config.max_seq_len
  221. max_new_tokens = T_new - T
  222. device = prompt.device
  223. dtype = next(
  224. model.parameters()
  225. ).dtype # model weight dtype (bfloat16), NOT prompt dtype (int32)
  226. # Critical fix: Only set up cache on first run or when necessary
  227. if not hasattr(model, "_cache_setup_done") or not model._cache_setup_done:
  228. with torch.device(device):
  229. model.setup_caches(
  230. max_batch_size=1, # Fixed to 1, avoid dynamic changes
  231. max_seq_len=model.config.max_seq_len,
  232. dtype=next(model.parameters()).dtype,
  233. )
  234. model._cache_setup_done = True
  235. codebook_dim = 1 + model.config.num_codebooks
  236. # Create new tensor each time, but try to reuse memory
  237. input_pos = torch.arange(0, T, device=device, dtype=torch.long)
  238. empty = torch.empty(
  239. (codebook_dim, model.config.max_seq_len), dtype=prompt.dtype, device=device
  240. )
  241. empty[:, :T] = prompt
  242. seq = empty
  243. temp_val = sampling_kwargs.get("temperature", 1.0)
  244. top_p_val = sampling_kwargs.get("top_p", 0.9)
  245. top_k_val = sampling_kwargs.get("top_k", 30)
  246. temperature = torch.tensor(temp_val, device=device, dtype=dtype)
  247. top_p = torch.tensor(top_p_val, device=device, dtype=dtype)
  248. # Build semantic logit bias: 0 for semantic tokens + im_end, -inf for all others
  249. vocab_size = model.config.vocab_size
  250. semantic_logit_bias = torch.full(
  251. (1, 1, vocab_size), float("-inf"), device=device, dtype=dtype
  252. )
  253. # [MODIFIED] Use config for semantic range
  254. semantic_logit_bias[
  255. 0, 0, model.config.semantic_begin_id: model.config.semantic_end_id + 1
  256. ] = 0.0
  257. # [MODIFIED] Use tokenizer.get_token_id (Wrapper method)
  258. semantic_logit_bias[0, 0, model.tokenizer.get_token_id(IM_END_TOKEN)] = 0.0
  259. prefill_decode = decode_one_token_ar
  260. first_token = prefill_decode(
  261. model,
  262. prompt.view(1, codebook_dim, -1),
  263. input_pos,
  264. temperature,
  265. top_p,
  266. top_k_val,
  267. semantic_logit_bias,
  268. audio_masks,
  269. audio_parts,
  270. )
  271. seq[:, T: T + 1] = first_token
  272. # Recreate input_pos
  273. input_pos = torch.tensor([T], device=device, dtype=torch.int)
  274. x = decode_n_tokens(
  275. model,
  276. first_token.view(1, codebook_dim, -1),
  277. input_pos,
  278. max_new_tokens - 1,
  279. temperature=temperature,
  280. top_p=top_p,
  281. top_k=top_k_val,
  282. semantic_logit_bias=semantic_logit_bias,
  283. audio_masks=audio_masks,
  284. audio_parts=audio_parts,
  285. decode_one_token=decode_one_token,
  286. )
  287. seq = seq[:, : T + 1 + x.size(1)]
  288. seq[:, T + 1:] = x
  289. # Clean up temporary variables
  290. del first_token, x, prompt, empty, input_pos
  291. return seq
  292. def init_model(checkpoint_path, device, precision, compile=False):
  293. model = DualARTransformer.from_pretrained(checkpoint_path, load_weights=True)
  294. logger.info(f"precision: {precision.__class__.__name__}")
  295. model = model.to(device=device, dtype=precision)
  296. logger.info(f"Restored model from checkpoint")
  297. if isinstance(model, DualARTransformer):
  298. decode_one_token = decode_one_token_ar
  299. # prefill_n_tokens = decode_one_token_ar
  300. logger.info("Using DualARTransformer")
  301. else:
  302. raise ValueError("Unsupported model type")
  303. # Pre-create fixed parameter tensors to avoid runtime creation
  304. model.fixed_temperature = torch.tensor(0.7, device=device, dtype=torch.float)
  305. model.fixed_top_p = torch.tensor(0.7, device=device, dtype=torch.float)
  306. model.fixed_repetition_penalty = torch.tensor(1.5, device=device, dtype=torch.float)
  307. # Mark whether cache has been initialized
  308. model._cache_setup_done = False
  309. if compile:
  310. logger.info("Compiling function...")
  311. decode_one_token = torch.compile(
  312. decode_one_token,
  313. backend="inductor" if torch.cuda.is_available() else "aot_eager",
  314. mode="default" if torch.cuda.is_available() else None,
  315. fullgraph=True,
  316. )
  317. return model.eval(), decode_one_token
  318. @torch.inference_mode()
  319. def load_codec_model(codec_checkpoint_path, device, precision=torch.bfloat16):
  320. """Load the DAC codec model for audio encoding/decoding."""
  321. from hydra.utils import instantiate
  322. from omegaconf import OmegaConf
  323. config_path = Path(__file__).parent.parent.parent / "configs" / "modded_dac_vq.yaml"
  324. cfg = OmegaConf.load(str(config_path))
  325. codec = instantiate(cfg)
  326. state_dict = torch.load(codec_checkpoint_path, map_location="cpu")
  327. if "state_dict" in state_dict:
  328. state_dict = state_dict["state_dict"]
  329. if any("generator" in k for k in state_dict):
  330. state_dict = {
  331. k.replace("generator.", ""): v
  332. for k, v in state_dict.items()
  333. if "generator." in k
  334. }
  335. codec.load_state_dict(state_dict, strict=False)
  336. codec.eval()
  337. codec.to(device=device, dtype=precision)
  338. return codec
  339. @torch.inference_mode()
  340. def encode_audio(audio_path, codec, device):
  341. """Encode an audio file to VQ codes."""
  342. import torchaudio
  343. wav, sr = torchaudio.load(str(audio_path))
  344. if wav.shape[0] > 1:
  345. wav = wav.mean(dim=0, keepdim=True)
  346. wav = torchaudio.functional.resample(wav.to(device), sr, codec.sample_rate)[0]
  347. # Match codec model dtype (e.g. bfloat16)
  348. model_dtype = next(codec.parameters()).dtype
  349. audios = wav[None, None].to(dtype=model_dtype) # (1, 1, T)
  350. audio_lengths = torch.tensor([len(wav)], device=device, dtype=torch.long)
  351. indices, feature_lengths = codec.encode(audios, audio_lengths)
  352. return indices[0, :, : feature_lengths[0]] # (num_codebooks, T)
  353. @torch.inference_mode()
  354. def decode_to_audio(codes, codec):
  355. """Decode VQ codes to audio waveform."""
  356. # codes: (num_codebooks, T) -> (1, num_codebooks, T)
  357. audio = codec.from_indices(codes[None])
  358. return audio[0, 0] # (T,) mono waveform
  359. @dataclass
  360. class GenerateResponse:
  361. action: Literal["sample", "next"]
  362. codes: Optional[torch.Tensor] = None
  363. text: Optional[str] = None
  364. def split_text_by_speaker(text: str) -> list[str]:
  365. """
  366. Split text into turns based on <|speaker:X|> tags.
  367. Args:
  368. text: The full text with speaker tags
  369. Returns:
  370. List of speaker turns, each starting with <|speaker:X|>
  371. """
  372. pattern = r"(<\|speaker:\d+\|>)"
  373. parts = re.split(pattern, text)
  374. turns = []
  375. i = 0
  376. while i < len(parts):
  377. part = parts[i].strip()
  378. if re.match(pattern, part):
  379. if i + 1 < len(parts):
  380. turn = part + parts[i + 1]
  381. turns.append(turn.strip())
  382. i += 2
  383. else:
  384. turns.append(part)
  385. i += 1
  386. else:
  387. i += 1
  388. return turns
  389. def group_turns_into_batches(
  390. turns: list[str], max_speakers: int = 3, max_bytes: int = 300
  391. ) -> list[str]:
  392. """
  393. Group turns into batches based on speaker count or byte limit.
  394. Args:
  395. turns: List of speaker turns
  396. max_speakers: Maximum number of speakers per batch (default 3)
  397. max_bytes: Maximum UTF-8 bytes per batch (default 300)
  398. Returns:
  399. List of batched text strings
  400. """
  401. batches = []
  402. current_batch = []
  403. current_bytes = 0
  404. for turn in turns:
  405. turn_bytes = len(turn.encode("utf-8"))
  406. would_exceed_speakers = len(current_batch) >= max_speakers
  407. would_exceed_bytes = current_bytes + turn_bytes > max_bytes and current_batch
  408. if would_exceed_speakers or would_exceed_bytes:
  409. batches.append("\n".join(current_batch))
  410. current_batch = [turn]
  411. current_bytes = turn_bytes
  412. else:
  413. current_batch.append(turn)
  414. current_bytes += turn_bytes
  415. if current_batch:
  416. batches.append("\n".join(current_batch))
  417. return batches
  418. def generate_long(
  419. *,
  420. model,
  421. device: Union[str, torch.device],
  422. decode_one_token: Callable,
  423. text: str,
  424. num_samples: int = 1,
  425. max_new_tokens: int = 0,
  426. top_p: float = 0.9,
  427. top_k: int = 30,
  428. repetition_penalty: float = 1.1,
  429. temperature: float = 1.0,
  430. compile: bool = False,
  431. iterative_prompt: bool = True,
  432. chunk_length: int = 512,
  433. prompt_text: Optional[Union[str, list[str]]] = None,
  434. prompt_tokens: Optional[Union[torch.Tensor, list[torch.Tensor]]] = None,
  435. ):
  436. assert 0 < top_p <= 1, "top_p must be in (0, 1]"
  437. assert 0 < temperature < 2, "temperature must be in (0, 2)"
  438. logger.info(f"generate_long.param.device: {device}")
  439. logger.info(f"generate_long.param.text: {text}")
  440. logger.info(f"generate_long.param.max_new_tokens: {max_new_tokens}")
  441. logger.info(f"generate_long.param.top_p: {top_p}")
  442. logger.info(f"generate_long.param.top_k: {top_k}")
  443. logger.info(f"generate_long.param.temperature: {temperature}")
  444. logger.info(f"generate_long.param.compile: {compile}")
  445. logger.info(f"generate_long.param.chunk_length: {chunk_length}")
  446. logger.info(f"generate_long.param.prompt_text: {prompt_text}")
  447. logger.info(f"generate_long.param.prompt_tokens: {prompt_tokens}")
  448. use_prompt = bool(prompt_text) and bool(prompt_tokens)
  449. if use_prompt and isinstance(prompt_text, str):
  450. prompt_text = [prompt_text]
  451. prompt_tokens = [prompt_tokens]
  452. if use_prompt:
  453. assert len(prompt_text) == len(
  454. prompt_tokens
  455. ), "Prompt text and tokens must have the same length"
  456. if prompt_tokens:
  457. prompt_tokens = [i.cpu() for i in prompt_tokens]
  458. model_size = sum(p.numel() for p in model.parameters() if p.requires_grad)
  459. tokenizer = model.tokenizer
  460. max_length = model.config.max_seq_len
  461. # Build base conversation with system message
  462. base_conversation = Conversation()
  463. if use_prompt:
  464. # Auto-add speaker tags to prompt texts that don't have them
  465. tagged_prompt_text = []
  466. for i, t in enumerate(prompt_text):
  467. if not re.search(r"<\|speaker:\d+\|>", t):
  468. tagged_prompt_text.append(f"<|speaker:{i}|>{t}")
  469. else:
  470. tagged_prompt_text.append(t)
  471. system_parts = [
  472. TextPart(
  473. text="convert the provided text to speech reference to the following:\n\nText:\n",
  474. cal_loss=False,
  475. ),
  476. ]
  477. reference_text = "\n".join(tagged_prompt_text)
  478. system_parts.append(TextPart(text=reference_text, cal_loss=False))
  479. system_parts.append(TextPart(text="\n\nSpeech:\n", cal_loss=False))
  480. all_codes = torch.cat([c for c in prompt_tokens], dim=1)
  481. system_parts.append(VQPart(codes=all_codes, cal_loss=False))
  482. # torch.save(all_codes, "debug_vq_codes.pt")
  483. else:
  484. system_parts = [
  485. TextPart(text="convert the provided text to speech", cal_loss=False)
  486. ]
  487. base_conversation.append(
  488. Message(
  489. role="system",
  490. parts=system_parts,
  491. cal_loss=False,
  492. add_im_start=True,
  493. add_im_end=True,
  494. )
  495. )
  496. # Split text by speaker and group into batches
  497. turns = split_text_by_speaker(text)
  498. if turns:
  499. batches = group_turns_into_batches(
  500. turns, max_speakers=5, max_bytes=chunk_length
  501. )
  502. else:
  503. batches = [text]
  504. logger.info(f"Split into {len(turns)} turns, grouped into {len(batches)} batches")
  505. for sample_idx in range(num_samples):
  506. if torch.cuda.is_available():
  507. torch.cuda.synchronize()
  508. t0 = time.perf_counter()
  509. # Deep copy base conversation for this sample
  510. conversation = deepcopy(base_conversation)
  511. for batch_idx, batch_text in enumerate(batches):
  512. logger.info(
  513. f"--- Sample {sample_idx}, Batch {batch_idx} "
  514. f"({len(batch_text.encode('utf-8'))} bytes) ---"
  515. )
  516. logger.info(f"Batch text: {batch_text}")
  517. # Add user message
  518. conversation.append(
  519. Message(
  520. role="user",
  521. parts=[TextPart(text=batch_text, cal_loss=False)],
  522. cal_loss=False,
  523. add_im_start=True,
  524. add_im_end=True,
  525. )
  526. )
  527. # Deep copy for generation (don't pollute original conversation)
  528. conversation_gen = deepcopy(conversation)
  529. conversation_gen.append(
  530. Message(
  531. role="assistant",
  532. parts=[],
  533. cal_loss=False,
  534. modality="voice",
  535. add_im_start=True,
  536. add_im_end=False,
  537. )
  538. )
  539. logger.info("Visualizing prompt structure:")
  540. conversation_gen.visualize(
  541. tokenizer,
  542. merge_audio_tokens=True,
  543. merge_semantic_tokens=True,
  544. )
  545. encoded, audio_masks, audio_parts = conversation_gen.encode_for_inference(
  546. tokenizer, num_codebooks=model.config.num_codebooks
  547. )
  548. logger.info(f"Encoded prompt shape: {encoded.shape}")
  549. if audio_parts is not None:
  550. logger.info(f"Audio parts shape: {audio_parts.shape}")
  551. if audio_masks is not None:
  552. logger.info(
  553. f"Audio masks non-zero count: {torch.count_nonzero(audio_masks)}"
  554. )
  555. if encoded.size(1) > max_length - 2048:
  556. raise ValueError(
  557. f"Prompt is too long: {encoded.size(1)} > {max_length - 2048}"
  558. )
  559. encoded = encoded.to(device=device)
  560. prompt_length = encoded.size(1)
  561. y = generate(
  562. model=model,
  563. prompt=encoded,
  564. max_new_tokens=max_new_tokens,
  565. audio_masks=audio_masks,
  566. audio_parts=audio_parts,
  567. decode_one_token=decode_one_token,
  568. temperature=temperature,
  569. top_p=top_p,
  570. top_k=top_k,
  571. )
  572. if sample_idx == 0 and batch_idx == 0 and compile:
  573. logger.info(f"Compilation time: {time.perf_counter() - t0:.2f} seconds")
  574. if torch.cuda.is_available():
  575. torch.cuda.synchronize()
  576. t_batch = time.perf_counter() - t0
  577. tokens_generated = y.size(1) - prompt_length
  578. tokens_sec = tokens_generated / t_batch if t_batch > 0 else 0
  579. logger.info(
  580. f"Batch {batch_idx}: Generated {tokens_generated} tokens in "
  581. f"{t_batch:.02f} seconds, {tokens_sec:.02f} tokens/sec"
  582. )
  583. logger.info(
  584. f"Bandwidth achieved: {model_size * tokens_sec / 1e9:.02f} GB/s"
  585. )
  586. # Extract generated codes
  587. codes = y[1:, prompt_length:-1].clone()
  588. assert (codes >= 0).all(), f"Negative code found: {codes}"
  589. # Add assistant message with generated codes back to conversation
  590. conversation.append(
  591. Message(
  592. role="assistant",
  593. parts=[VQPart(codes=codes.cpu(), cal_loss=False)],
  594. cal_loss=False,
  595. modality="voice",
  596. add_im_start=True,
  597. add_im_end=True,
  598. )
  599. )
  600. yield GenerateResponse(action="sample", codes=codes, text=batch_text)
  601. MAX_HISTORY_TURNS = 2 # 只保留最近 2 轮 user/assistant
  602. assistant_indices = [i for i, m in enumerate(conversation.messages) if m.role == "assistant"]
  603. if len(assistant_indices) > MAX_HISTORY_TURNS:
  604. drop = assistant_indices[0]
  605. # 移除最早的 user+assistant 对,保留 system 消息
  606. conversation = Conversation([m for i, m in enumerate(conversation.messages)
  607. if i not in (drop - 1, drop)])
  608. # Cleanup
  609. del y, encoded
  610. if torch.cuda.is_available():
  611. torch.cuda.empty_cache()
  612. import gc
  613. gc.collect()
  614. if torch.cuda.is_available():
  615. logger.info(
  616. f"GPU Memory used: {torch.cuda.max_memory_reserved() / 1e9:.02f} GB"
  617. )
  618. yield GenerateResponse(action="next")
  619. @dataclass
  620. class WrappedGenerateResponse:
  621. status: Literal["success", "error"]
  622. response: Optional[Union[GenerateResponse, Exception]] = None
  623. @dataclass
  624. class GenerateRequest:
  625. request: dict
  626. response_queue: queue.Queue
  627. def launch_thread_safe_queue(
  628. checkpoint_path,
  629. device,
  630. precision,
  631. compile: bool = False,
  632. ):
  633. input_queue = queue.Queue()
  634. init_event = threading.Event()
  635. def worker():
  636. model, decode_one_token = init_model(
  637. checkpoint_path, device, precision, compile=compile
  638. )
  639. with torch.device(device):
  640. model.setup_caches(
  641. max_batch_size=1,
  642. max_seq_len=model.config.max_seq_len,
  643. dtype=next(model.parameters()).dtype,
  644. )
  645. init_event.set()
  646. while True:
  647. item: GenerateRequest | None = input_queue.get()
  648. if item is None:
  649. break
  650. kwargs = item.request
  651. response_queue = item.response_queue
  652. try:
  653. for chunk in generate_long_parallel_batched(
  654. model=model, decode_one_token=decode_one_token, **kwargs
  655. ):
  656. response_queue.put(
  657. WrappedGenerateResponse(status="success", response=chunk)
  658. )
  659. # Only clear cache after complete request batch
  660. if torch.cuda.is_available():
  661. torch.cuda.empty_cache()
  662. except Exception as e:
  663. logger.error(traceback.format_exc())
  664. response_queue.put(WrappedGenerateResponse(status="error", response=e))
  665. # Clear cache on error
  666. if torch.cuda.is_available():
  667. torch.cuda.empty_cache()
  668. threading.Thread(target=worker, daemon=True).start()
  669. init_event.wait()
  670. return input_queue
  671. import torch
  672. from copy import deepcopy
  673. from typing import List, Optional
  674. # =========================
  675. # 1. Prompt 构建(从 generate_long 抽出)
  676. # =========================
  677. def build_prompt(
  678. *,
  679. model,
  680. device,
  681. text,
  682. chunk_length,
  683. prompt_text=None,
  684. prompt_tokens=None,
  685. ):
  686. tokenizer = model.tokenizer
  687. max_length = model.config.max_seq_len
  688. use_prompt = bool(prompt_text) and bool(prompt_tokens)
  689. if use_prompt and isinstance(prompt_text, str):
  690. prompt_text = [prompt_text]
  691. prompt_tokens = [prompt_tokens]
  692. if prompt_tokens:
  693. prompt_tokens = [p.cpu() for p in prompt_tokens]
  694. # -------- system prompt --------
  695. base_conversation = Conversation()
  696. if use_prompt:
  697. tagged = []
  698. for i, t in enumerate(prompt_text):
  699. if not re.search(r"<\|speaker:\d+\|>", t):
  700. tagged.append(f"<|speaker:{i}|>{t}")
  701. else:
  702. tagged.append(t)
  703. system_parts = [
  704. TextPart(text="convert the provided text to speech reference to the following:\n\nText:\n", cal_loss=False, ),
  705. TextPart(text="\n".join(tagged), cal_loss=False),
  706. TextPart(text="\n\nSpeech:\n", cal_loss=False)
  707. ]
  708. all_codes = torch.cat(prompt_tokens, dim=1)
  709. system_parts.append(VQPart(codes=all_codes, cal_loss=False))
  710. else:
  711. system_parts = [
  712. TextPart(text="convert the provided text to speech", cal_loss=False)
  713. ]
  714. base_conversation.append(
  715. Message(
  716. role="system",
  717. parts=system_parts,
  718. cal_loss=False,
  719. add_im_start=True,
  720. add_im_end=True,
  721. )
  722. )
  723. # -------- chunk text --------
  724. turns = split_text_by_speaker(text)
  725. batches = (
  726. group_turns_into_batches(turns, max_speakers=5, max_bytes=chunk_length)
  727. if turns else [text]
  728. )
  729. # 只做 chunk
  730. encoded_prompts = []
  731. metas = []
  732. for batch_text in batches:
  733. conv = deepcopy(base_conversation)
  734. conv.append(
  735. Message(
  736. role="user",
  737. parts=[TextPart(text=batch_text, cal_loss=False)],
  738. cal_loss=False,
  739. add_im_start=True,
  740. add_im_end=True,
  741. )
  742. )
  743. conv_gen = deepcopy(conv)
  744. conv_gen.append(
  745. Message(
  746. role="assistant",
  747. parts=[],
  748. cal_loss=False,
  749. modality="voice",
  750. add_im_start=True,
  751. add_im_end=False,
  752. )
  753. )
  754. encoded, audio_masks, audio_parts = conv_gen.encode_for_inference(
  755. tokenizer,
  756. num_codebooks=model.config.num_codebooks,
  757. )
  758. if encoded.size(1) > max_length - 2048:
  759. raise ValueError("Prompt too long")
  760. encoded = encoded.to(device)
  761. encoded_prompts.append(encoded)
  762. metas.append(
  763. {
  764. "audio_masks": audio_masks,
  765. "audio_parts": audio_parts,
  766. "prompt_len": encoded.size(1),
  767. "text": batch_text,
  768. }
  769. )
  770. return encoded_prompts, metas
  771. # =========================
  772. # 2. Batched decode(核心)
  773. # =========================
  774. def generate_batched(
  775. *,
  776. model,
  777. prompts: List[torch.Tensor],
  778. decode_one_token,
  779. max_new_tokens,
  780. temperature,
  781. top_p,
  782. top_k,
  783. ):
  784. """
  785. prompts: List[Tensor] [1, T]
  786. return: List[Tensor] decoded sequences
  787. """
  788. device = prompts[0].device
  789. max_len = max(p.size(1) for p in prompts)
  790. padded = []
  791. for p in prompts:
  792. if p.size(1) < max_len:
  793. pad = torch.zeros((1, max_len - p.size(1)), dtype=p.dtype, device=device)
  794. p = torch.cat([p, pad], dim=1)
  795. padded.append(p)
  796. y = torch.cat(padded, dim=0) # [B, T]
  797. for _ in range(max_new_tokens):
  798. next_token = decode_one_token(
  799. model,
  800. y,
  801. temperature=temperature,
  802. top_p=top_p,
  803. top_k=top_k,
  804. audio_masks=None,
  805. audio_parts=None,
  806. ) # [B, 1]
  807. y = torch.cat([y, next_token], dim=1)
  808. outputs = [y[i:i + 1] for i in range(y.size(0))]
  809. return outputs
  810. # =========================
  811. # 3. 并行 TTS 主入口
  812. # =========================
  813. @torch.inference_mode()
  814. def generate_long_parallel_batched(
  815. *,
  816. model,
  817. device: Union[str, torch.device],
  818. decode_one_token: Callable,
  819. text: str,
  820. num_samples: int = 1,
  821. max_new_tokens: int = 0,
  822. top_p: float = 0.9,
  823. top_k: int = 30,
  824. repetition_penalty: float = 1.1,
  825. temperature: float = 1.0,
  826. compile: bool = False,
  827. iterative_prompt: bool = True,
  828. chunk_length: int = 512,
  829. prompt_text: Optional[Union[str, list[str]]] = None,
  830. prompt_tokens: Optional[Union[torch.Tensor, list[torch.Tensor]]] = None,
  831. ):
  832. """
  833. 最小侵入版本:
  834. chunk + batch decode + concat
  835. """
  836. # ===== 1. build prompts =====
  837. encoded_prompts, metas = build_prompt(
  838. model=model,
  839. device=device,
  840. text=text,
  841. chunk_length=chunk_length,
  842. prompt_text=prompt_text,
  843. prompt_tokens=prompt_tokens,
  844. )
  845. # ===== 2. batched decode =====
  846. outputs = generate_batched(
  847. model=model,
  848. prompts=encoded_prompts,
  849. decode_one_token=decode_one_token,
  850. max_new_tokens=max_new_tokens,
  851. temperature=temperature,
  852. top_p=top_p,
  853. top_k=top_k,
  854. )
  855. # ===== 3. merge outputs =====
  856. all_codes = []
  857. for y, meta in zip(outputs, metas):
  858. prompt_len = meta["prompt_len"]
  859. codes = y[1:, prompt_len:-1].clone()
  860. all_codes.append(codes)
  861. final_codes = torch.cat(all_codes, dim=1)
  862. return final_codes
  863. # ============================================
  864. # =============== 原始代码 =================
  865. # ============================================
  866. @click.command()
  867. @click.option(
  868. "--text",
  869. type=str,
  870. default="<|speaker:0|>你说的对, 但是原神是一款由米哈游自主研发的开放世界手游.",
  871. )
  872. @click.option("--prompt-text", type=str, default=None, multiple=True)
  873. @click.option(
  874. "--prompt-tokens",
  875. type=click.Path(path_type=Path, exists=True),
  876. default=None,
  877. multiple=True,
  878. )
  879. @click.option(
  880. "--prompt-audio",
  881. type=click.Path(path_type=Path, exists=True),
  882. default=None,
  883. multiple=True,
  884. )
  885. @click.option("--output", type=click.Path(path_type=Path), default=None)
  886. @click.option("--num-samples", type=int, default=1)
  887. @click.option("--max-new-tokens", type=int, default=0)
  888. @click.option("--top-p", type=float, default=0.9)
  889. @click.option("--top-k", type=int, default=30)
  890. @click.option("--temperature", type=float, default=1.0)
  891. @click.option(
  892. "--checkpoint-path",
  893. type=click.Path(path_type=Path, exists=True),
  894. default="checkpoints/s2-pro",
  895. )
  896. @click.option("--device", type=str, default="cuda")
  897. @click.option("--compile/--no-compile", default=False)
  898. @click.option("--seed", type=int, default=42)
  899. @click.option("--half/--no-half", default=False)
  900. @click.option("--iterative-prompt/--no-iterative-prompt", default=True)
  901. @click.option("--chunk-length", type=int, default=300)
  902. @click.option("--output-dir", type=Path, default="output")
  903. def main(
  904. text: str,
  905. prompt_text: Optional[tuple[str, ...]],
  906. prompt_tokens: Optional[tuple[Path, ...]],
  907. prompt_audio: Optional[tuple[Path, ...]],
  908. output: Optional[Path],
  909. num_samples: int,
  910. max_new_tokens: int,
  911. top_p: float,
  912. top_k: int,
  913. temperature: float,
  914. checkpoint_path: Path,
  915. device: str,
  916. compile: bool,
  917. seed: int,
  918. half: bool,
  919. iterative_prompt: bool,
  920. chunk_length: int,
  921. output_dir: Path,
  922. ) -> None:
  923. os.makedirs(output_dir, exist_ok=True)
  924. precision = torch.half if half else torch.bfloat16
  925. if prompt_text and not prompt_audio and not prompt_tokens:
  926. raise ValueError(
  927. "--prompt-text requires either --prompt-audio or --prompt-tokens"
  928. )
  929. if prompt_text and prompt_tokens and len(prompt_text) != len(prompt_tokens):
  930. raise ValueError(
  931. f"Number of prompt text ({len(prompt_text)}) and prompt tokens ({len(prompt_tokens)}) should be the same"
  932. )
  933. if prompt_text and prompt_audio and len(prompt_text) != len(prompt_audio):
  934. raise ValueError(
  935. f"Number of prompt text ({len(prompt_text)}) and prompt audio ({len(prompt_audio)}) should be the same"
  936. )
  937. logger.info("Loading model ...")
  938. t0 = time.time()
  939. model, decode_one_token = init_model(
  940. checkpoint_path, device, precision, compile=compile
  941. )
  942. with torch.device(device):
  943. model.setup_caches(
  944. max_batch_size=1,
  945. max_seq_len=model.config.max_seq_len,
  946. dtype=next(model.parameters()).dtype,
  947. )
  948. if torch.cuda.is_available():
  949. torch.cuda.synchronize()
  950. logger.info(f"Time to load model: {time.time() - t0:.02f} seconds")
  951. codec = None
  952. codec_checkpoint = checkpoint_path / "codec.pth"
  953. # Handle prompt: --prompt-audio takes priority over --prompt-tokens
  954. prompt_tokens_list = None
  955. if prompt_audio:
  956. logger.info("Loading codec model for audio encoding...")
  957. codec = load_codec_model(codec_checkpoint, device, precision)
  958. prompt_tokens_list = [
  959. encode_audio(p, codec, device).cpu() for p in prompt_audio
  960. ]
  961. logger.info(f"Encoded {len(prompt_audio)} audio file(s) to VQ codes")
  962. elif prompt_tokens is not None:
  963. prompt_tokens_list = [torch.from_numpy(np.load(p)) for p in prompt_tokens]
  964. torch.manual_seed(seed)
  965. if torch.cuda.is_available():
  966. torch.cuda.manual_seed(seed)
  967. generator = generate_long(
  968. model=model,
  969. device=device,
  970. decode_one_token=decode_one_token,
  971. text=text,
  972. num_samples=num_samples,
  973. max_new_tokens=max_new_tokens,
  974. top_p=top_p,
  975. top_k=top_k,
  976. temperature=temperature,
  977. compile=compile,
  978. iterative_prompt=iterative_prompt,
  979. chunk_length=chunk_length,
  980. prompt_text=list(prompt_text) if prompt_text else None,
  981. prompt_tokens=prompt_tokens_list,
  982. )
  983. idx = 0
  984. codes = []
  985. for response in generator:
  986. if response.action == "sample":
  987. codes.append(response.codes)
  988. logger.info(f"Sampled text: {response.text}")
  989. elif response.action == "next":
  990. if codes:
  991. merged_codes = torch.cat(codes, dim=1)
  992. codes_npy_path = os.path.join(output_dir, f"codes_{idx}.npy")
  993. np.save(codes_npy_path, merged_codes.cpu().numpy())
  994. logger.info(f"Saved codes to {codes_npy_path}")
  995. # Decode to wav if --output is specified
  996. if output:
  997. if codec is None:
  998. logger.info("Loading codec model for audio decoding...")
  999. codec = load_codec_model(codec_checkpoint, device, precision)
  1000. audio = decode_to_audio(merged_codes.to(device), codec)
  1001. import soundfile as sf
  1002. out_path = (
  1003. str(output)
  1004. if num_samples == 1
  1005. else str(output.with_stem(f"{output.stem}_{idx}"))
  1006. )
  1007. sf.write(out_path, audio.cpu().float().numpy(), codec.sample_rate)
  1008. logger.info(f"Saved audio to {out_path}")
  1009. logger.info(f"Next sample")
  1010. codes = []
  1011. idx += 1
  1012. else:
  1013. logger.error(f"Error: {response}")
  1014. if __name__ == "__main__":
  1015. main()