semantic.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454
  1. import random
  2. from dataclasses import dataclass
  3. from itertools import chain
  4. from pathlib import Path
  5. from random import Random
  6. from typing import Optional, Union
  7. import numpy as np
  8. import pyarrow.parquet as pq
  9. import torch
  10. import torch.nn.functional as F
  11. from datasets.download.streaming_download_manager import xopen
  12. from huggingface_hub import HfApi
  13. from lightning import LightningDataModule
  14. from torch.distributed import get_rank, get_world_size, is_initialized
  15. from torch.utils.data import DataLoader, IterableDataset, get_worker_info
  16. from fish_speech.conversation import (
  17. CODEBOOK_PAD_TOKEN_ID,
  18. Conversation,
  19. Message,
  20. TextPart,
  21. VQPart,
  22. )
  23. from fish_speech.datasets.protos.text_data_pb2 import SampledData
  24. from fish_speech.datasets.protos.text_data_stream import read_pb_stream
  25. from fish_speech.text.clean import clean_text
  26. from fish_speech.tokenizer import FishTokenizer
  27. from fish_speech.utils import RankedLogger
  28. from fish_speech.utils.braceexpand import braceexpand
  29. log = RankedLogger(__name__, rank_zero_only=True)
  30. def split_by_rank_worker(files):
  31. # We need to know the total number of devices
  32. # to split the data properly
  33. total_devices = 1
  34. if is_initialized():
  35. total_devices = get_world_size()
  36. worker_info = get_worker_info()
  37. if worker_info is not None:
  38. total_devices *= worker_info.num_workers
  39. if len(files) < total_devices:
  40. # Repeat the files N times to match the number of devices
  41. files = files * (total_devices // len(files) + 1)
  42. # DDP
  43. if is_initialized():
  44. files = files[get_rank() :: get_world_size()]
  45. # Split by worker
  46. if worker_info is not None:
  47. files = files[worker_info.id :: worker_info.num_workers]
  48. return files
  49. class AutoTextSemanticInstructionDataset(IterableDataset):
  50. """
  51. Auto Augment Dataset by Speaker
  52. 1. Random concatenate multiple sentences from the same speaker to form a longer sentence
  53. 2. Automatically normalize the text
  54. For interactive mode, we use the following format (multiple sequences):
  55. <s> [INST] [SPK: speaker] text [/INST] ... [INST] text [/INST] </s>
  56. For non-interactive mode, we use the following format (one long sequence):
  57. <s> [INST] text [/INST] ... </s>
  58. """
  59. def __init__(
  60. self,
  61. proto_files: list[str],
  62. seed: int = 42,
  63. interactive_prob: float = 0.5,
  64. max_length: int = 1024,
  65. tokenizer: FishTokenizer = None,
  66. use_speaker: bool | float = True,
  67. causal: bool = True,
  68. num_codebooks: Optional[int] = None,
  69. skip_text_prob: float = 0.0,
  70. ):
  71. """
  72. Args:
  73. proto_files: proto buf files if using local data
  74. seed: random seed
  75. interactive_prob: probability to use interactive mode
  76. max_length: max length of the text
  77. tokenizer: tokenizer
  78. use_speaker: include speaker information in the prompt
  79. causal: use causal sampling when using local data, disable will lead to random sampling
  80. num_codebooks: number of codebooks, if None, it will be automatically detected
  81. skip_text_prob: probability to skip the text (audio only), this only applies to interactive mode
  82. """
  83. super().__init__()
  84. assert 0 <= interactive_prob <= 1, "interactive_prob must be in [0, 1]"
  85. self.seed = seed
  86. self.max_length = max_length
  87. self.tokenizer = tokenizer
  88. self.interactive_prob = interactive_prob
  89. self.use_speaker = use_speaker
  90. self.proto_files = proto_files
  91. self.causal = causal
  92. self.num_codebooks = num_codebooks
  93. self.skip_text_prob = skip_text_prob
  94. self.groups = None
  95. def __iter__(self):
  96. while True:
  97. yield self.augment()
  98. def init_mock_data_server(self):
  99. if self.groups is not None:
  100. return
  101. # Expand the proto files
  102. expanded_proto_files = []
  103. for filename in self.proto_files:
  104. for i in braceexpand(filename):
  105. i = Path(i)
  106. if i.is_file():
  107. expanded_proto_files.append(i)
  108. elif i.is_dir():
  109. expanded_proto_files.extend(i.rglob("*.proto"))
  110. expanded_proto_files.extend(i.rglob("*.protos"))
  111. else:
  112. raise ValueError(f"{i} is not a file or directory")
  113. expanded_proto_files = sorted(expanded_proto_files)
  114. Random(self.seed).shuffle(expanded_proto_files)
  115. self.groups = []
  116. shard_proto_files = split_by_rank_worker(expanded_proto_files)
  117. log.info(
  118. f"Reading {len(shard_proto_files)} / {len(expanded_proto_files)} files"
  119. )
  120. count = 0
  121. for filename in shard_proto_files:
  122. with open(filename, "rb") as f:
  123. for text_data in read_pb_stream(f):
  124. self.groups.append(text_data)
  125. count += 1
  126. log.info(f"Read total {count} groups of data")
  127. # Shuffle the lines
  128. Random(self.seed).shuffle(self.groups)
  129. self.group_weights = [len(i.sentences) for i in self.groups]
  130. def sample_data(self):
  131. if self.groups is None:
  132. self.init_mock_data_server()
  133. # Shuffle unique lines, estimate that each sample is at least 20 tokens
  134. num_samples = self.max_length // 20
  135. # choice group based on their number of samples
  136. group = random.choices(self.groups, weights=self.group_weights, k=1)[0]
  137. if self.causal:
  138. # Sample in order
  139. if num_samples >= len(group.sentences):
  140. samples = group.sentences
  141. else:
  142. begin = random.randint(0, len(group.sentences) - num_samples)
  143. samples = group.sentences[begin : begin + num_samples]
  144. else:
  145. samples = random.choices(
  146. group.sentences, k=min(num_samples, len(group.sentences))
  147. )
  148. return SampledData(
  149. source=group.source,
  150. name=group.name,
  151. samples=samples,
  152. )
  153. def pack_sentences(
  154. self,
  155. sentences: list[str],
  156. semantics: list,
  157. # speaker: Optional[str] = None,
  158. skip_text: bool = False,
  159. ):
  160. # if speaker is None:
  161. # speaker = "assistant"
  162. messages = [
  163. Message(
  164. role="system",
  165. parts=[TextPart(text="Speak out the provided text.")],
  166. # add_im_end=False,
  167. # cal_loss=True,
  168. )
  169. ]
  170. cated_sentences = " ".join(sentences)
  171. if skip_text:
  172. cated_sentences = "<|skip_text|>"
  173. messages.append(
  174. Message(
  175. role="user",
  176. parts=[TextPart(text=cated_sentences)],
  177. # cal_loss=True,
  178. )
  179. )
  180. vq_codes = [x.values for x in semantics[0]]
  181. vq_codes_tensor = torch.tensor(vq_codes).to(torch.int32)
  182. vqpart = VQPart(codes=vq_codes_tensor)
  183. messages.append(
  184. Message(
  185. role="assistant",
  186. parts=[TextPart(text="<|voice|>"), vqpart],
  187. cal_loss=True,
  188. )
  189. )
  190. num_codebooks = (
  191. len(semantics[0]) if self.num_codebooks is None else self.num_codebooks
  192. )
  193. conversation = Conversation(messages=messages)
  194. # conversation.visualize(tokenizer=self.tokenizer)
  195. encoded = conversation.encode(
  196. tokenizer=self.tokenizer,
  197. )
  198. tokens_raw = encoded.tokens
  199. tokens = torch.zeros((num_codebooks + 1, len(tokens_raw)), dtype=torch.int)
  200. tokens[0] = tokens_raw
  201. vq_parts = encoded.vq_parts
  202. vq_parts = [part.to(tokens.device) for part in vq_parts]
  203. vq_parts = torch.cat(vq_parts, dim=1)
  204. tokens[1:, encoded.vq_mask_tokens] = vq_parts
  205. labels_raw = encoded.labels
  206. labels = torch.full((num_codebooks + 1, len(labels_raw)), -100, dtype=torch.int)
  207. labels[0, :] = labels_raw
  208. labels[1:, encoded.vq_mask_labels] = vq_parts
  209. labels[1:, -1:] = CODEBOOK_PAD_TOKEN_ID
  210. tokens = tokens.long()
  211. labels = labels.long()
  212. # Verify the padding is correct, and the last token is eos
  213. assert (tokens[1:, ~(encoded.vq_mask_tokens)] == CODEBOOK_PAD_TOKEN_ID).all()
  214. assert (labels[1:, -1:] == CODEBOOK_PAD_TOKEN_ID).all()
  215. return tokens, labels
  216. def augment(self):
  217. response = self.sample_data()
  218. if len(response.samples) == 0:
  219. # Invalid group
  220. return None
  221. samples = list(response.samples)
  222. all_tokens, all_labels = [], []
  223. while len(samples) > 0:
  224. sentence = samples.pop(0)
  225. text = clean_text(random.choice(sentence.texts))
  226. tokens, labels = self.pack_sentences(
  227. sentences=[text],
  228. semantics=[sentence.semantics],
  229. # speaker=response.name if use_speaker else None,
  230. skip_text=random.random() < self.skip_text_prob,
  231. )
  232. all_tokens.append(tokens)
  233. all_labels.append(labels)
  234. tokens = torch.cat(all_tokens, dim=1)
  235. labels = torch.cat(all_labels, dim=1)
  236. # Verify that the length is correct
  237. assert tokens.size(1) == labels.size(1), f"{tokens.size(1)} != {labels.size(1)}"
  238. data = {"tokens": tokens, "labels": labels}
  239. return data
  240. @dataclass
  241. class TextDataCollator:
  242. tokenizer: FishTokenizer
  243. max_length: int = 1024
  244. def __call__(self, examples):
  245. if "negative_tokens" in examples:
  246. positive_examples = []
  247. negative_examples = []
  248. for i in examples:
  249. positive_examples.append(
  250. {
  251. "tokens": i["tokens"],
  252. "labels": i["labels"],
  253. }
  254. )
  255. negative_examples.append(
  256. {
  257. "tokens": i["negative_tokens"],
  258. "labels": i["negative_labels"],
  259. }
  260. )
  261. examples = positive_examples + negative_examples
  262. return self.batchify(examples)
  263. def batchify(self, examples, tokens_key="tokens", labels_key="labels"):
  264. tokens, attention_masks, labels = [], [], []
  265. # Calculate the max length
  266. max_tokens_length = 0
  267. for example in examples:
  268. max_tokens_length = max(max_tokens_length, example[tokens_key].size(1))
  269. max_tokens_length = min(max_tokens_length, self.max_length)
  270. for example in examples:
  271. _tokens = example[tokens_key][:, :max_tokens_length]
  272. _labels = example[labels_key][:, :max_tokens_length]
  273. _attention_mask = torch.ones((max_tokens_length,), dtype=torch.bool)
  274. tokens_length = _tokens.size(1)
  275. _attention_mask[:tokens_length] = False
  276. assert tokens_length == _labels.size(
  277. 1
  278. ), f"{tokens_length} != {_labels.size(1)}"
  279. if tokens_length < max_tokens_length:
  280. _tokens = F.pad(
  281. _tokens,
  282. (0, max_tokens_length - tokens_length),
  283. value=self.tokenizer.get_token_id("<|end_of_text|>"),
  284. )
  285. _tokens[1:, tokens_length:] = CODEBOOK_PAD_TOKEN_ID
  286. _labels = F.pad(
  287. _labels, (0, max_tokens_length - _labels.size(1)), value=-100
  288. )
  289. tokens.append(_tokens)
  290. attention_masks.append(_attention_mask)
  291. labels.append(_labels)
  292. tokens = torch.stack(tokens, dim=0)
  293. attention_masks = torch.stack(attention_masks, dim=0)
  294. labels = torch.stack(labels, dim=0)
  295. return {
  296. "inputs": tokens,
  297. "attention_masks": attention_masks,
  298. "labels": labels,
  299. }
  300. class InterleaveDataset(IterableDataset):
  301. def __init__(
  302. self,
  303. datasets: list[IterableDataset],
  304. probabilities: list[float],
  305. seed: int = 42,
  306. ):
  307. super().__init__()
  308. self.datasets = datasets
  309. self.probabilities = probabilities
  310. self.seed = seed
  311. def __iter__(self):
  312. rng = np.random.default_rng(self.seed)
  313. dataset_iterators = [iter(dataset) for dataset in self.datasets]
  314. while True:
  315. # Random choice one
  316. dataset_idx = rng.choice(len(self.datasets), p=self.probabilities)
  317. dataset_iterator = dataset_iterators[dataset_idx]
  318. try:
  319. yield next(dataset_iterator)
  320. except StopIteration:
  321. # Exhausted, create a new iterator
  322. dataset_iterators[dataset_idx] = iter(self.datasets[dataset_idx])
  323. yield next(dataset_iterators[dataset_idx])
  324. class SemanticDataModule(LightningDataModule):
  325. def __init__(
  326. self,
  327. train_dataset: Union[AutoTextSemanticInstructionDataset, InterleaveDataset],
  328. val_dataset: Union[AutoTextSemanticInstructionDataset, InterleaveDataset],
  329. batch_size: int = 32,
  330. tokenizer: FishTokenizer = None,
  331. max_length: int = 1024,
  332. num_workers: int = 4,
  333. ):
  334. super().__init__()
  335. self.train_dataset = train_dataset
  336. self.val_dataset = val_dataset
  337. self.batch_size = batch_size
  338. self.tokenizer = tokenizer
  339. self.max_length = max_length
  340. self.num_workers = num_workers
  341. def train_dataloader(self):
  342. return DataLoader(
  343. self.train_dataset,
  344. batch_size=self.batch_size,
  345. collate_fn=TextDataCollator(self.tokenizer, self.max_length),
  346. num_workers=self.num_workers,
  347. persistent_workers=True,
  348. )
  349. def val_dataloader(self):
  350. return DataLoader(
  351. self.val_dataset,
  352. batch_size=self.batch_size,
  353. collate_fn=TextDataCollator(self.tokenizer, self.max_length),
  354. num_workers=self.num_workers,
  355. persistent_workers=True,
  356. )
  357. if __name__ == "__main__":
  358. from tqdm import tqdm
  359. ds = AutoTextSemanticInstructionDataset(
  360. ["data/protos"],
  361. tokenizer=FishTokenizer("checkpoints/fish-speech-1.5/tokenizer.tiktoken"),
  362. use_speaker=False,
  363. interactive_prob=1.0,
  364. skip_text_prob=0.5,
  365. )
  366. for i in ds:
  367. # Please uncomment line 235 to visualize the tokenized message
  368. print(i)
  369. break