Swagcrew commited on
Commit
ba6dc8e
·
verified ·
1 Parent(s): dbde30e

Upload scripts/quantize.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. scripts/quantize.py +562 -0
scripts/quantize.py ADDED
@@ -0,0 +1,562 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Fish Speech S2 Pro Quantization Toolkit
4
+ ========================================
5
+ Quantizes the S2 Pro model at multiple precision levels and generates
6
+ voice-cloned TTS samples for quality comparison.
7
+
8
+ Usage:
9
+ python quantize.py --phase all # Run all phases
10
+ python quantize.py --phase 1a # FP8 only
11
+ python quantize.py --phase 1b # INT4 only
12
+ python quantize.py --phase 2a # Hybrid INT4+FP8
13
+ python quantize.py --phase 2b # INT8
14
+ python quantize.py --phase 2c # INT3
15
+ python quantize.py --phase 3a # INT2
16
+ python quantize.py --phase 3b # INT2 all layers
17
+
18
+ Requirements:
19
+ - CUDA GPU with >= 24GB VRAM (A100 40/80GB recommended)
20
+ - pip install torch einops loguru ormsgpack hydra-core omegaconf safetensors torchaudio soundfile
21
+
22
+ Author: Fish Speech Quantization Experiment
23
+ """
24
+ import os, sys, json, time, gc, traceback, argparse
25
+ import torch
26
+ import torch.nn as nn
27
+ import numpy as np
28
+ import soundfile as sf
29
+ from pathlib import Path
30
+ from collections import OrderedDict
31
+ from safetensors.torch import save_file
32
+
33
+ os.environ["TOKENIZERS_PARALLELISM"] = "false"
34
+ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
35
+ DTYPE = torch.bfloat16
36
+ BASE_MODEL = "fishaudio/s2-pro"
37
+
38
+ # ============================================================
39
+ # QUANTIZATION MODULES
40
+ # ============================================================
41
+
42
+ class FP8Linear(nn.Module):
43
+ """Per-row symmetric FP8 (float8_e4m3fn) weight-only quantization.
44
+ Proven zero-quality-loss approach from drbaph/s2-pro-fp8."""
45
+ def __init__(self, in_f, out_f, bias=True):
46
+ super().__init__()
47
+ self.in_features = in_f
48
+ self.out_features = out_f
49
+ self.register_buffer("weight", torch.empty(out_f, in_f, dtype=torch.float8_e4m3fn))
50
+ self.register_buffer("weight_scale", torch.empty(out_f, 1, dtype=torch.float32))
51
+ self.has_bias = bias
52
+ if bias:
53
+ self.register_buffer("bias", torch.zeros(out_f, dtype=torch.bfloat16))
54
+ else:
55
+ self.bias = None
56
+
57
+ @staticmethod
58
+ def from_linear(linear):
59
+ fp8 = FP8Linear(linear.in_features, linear.out_features, linear.bias is not None)
60
+ FP8_MAX = 448.0
61
+ w = linear.weight.data.detach().to(torch.bfloat16)
62
+ scale = w.abs().amax(dim=1, keepdim=True) / FP8_MAX
63
+ scale = scale.clamp(min=1e-12)
64
+ w_q = (w / scale).round().clamp(-FP8_MAX, FP8_MAX).to(torch.float8_e4m3fn)
65
+ fp8.weight.data.copy_(w_q)
66
+ fp8.weight_scale.data.copy_(scale)
67
+ if linear.bias is not None:
68
+ fp8.bias.data.copy_(linear.bias.data.detach().to(torch.bfloat16))
69
+ return fp8
70
+
71
+ def forward(self, x):
72
+ w = self.weight.to(torch.bfloat16) * self.weight_scale
73
+ return nn.functional.linear(x, w, self.bias)
74
+
75
+
76
+ class INT8Linear(nn.Module):
77
+ """Per-row symmetric INT8 weight-only quantization."""
78
+ def __init__(self, in_f, out_f, bias=True):
79
+ super().__init__()
80
+ self.in_features = in_f
81
+ self.out_features = out_f
82
+ self.register_buffer("weight", torch.empty(out_f, in_f, dtype=torch.int8))
83
+ self.register_buffer("weight_scale", torch.empty(out_f, 1, dtype=torch.float32))
84
+ self.has_bias = bias
85
+ if bias:
86
+ self.register_buffer("bias", torch.zeros(out_f, dtype=torch.bfloat16))
87
+ else:
88
+ self.bias = None
89
+
90
+ @staticmethod
91
+ def from_linear(linear):
92
+ q = INT8Linear(linear.in_features, linear.out_features, linear.bias is not None)
93
+ w = linear.weight.data.detach().to(torch.bfloat16)
94
+ scale = w.abs().amax(dim=1, keepdim=True) / 127.0
95
+ scale = scale.clamp(min=1e-12)
96
+ w_q = (w / scale).round().clamp(-128, 127).to(torch.int8)
97
+ q.weight.data.copy_(w_q)
98
+ q.weight_scale.data.copy_(scale)
99
+ if linear.bias is not None:
100
+ q.bias.data.copy_(linear.bias.data.detach().to(torch.bfloat16))
101
+ return q
102
+
103
+ def forward(self, x):
104
+ w = self.weight.to(torch.bfloat16) * self.weight_scale
105
+ return nn.functional.linear(x, w, self.bias)
106
+
107
+
108
+ class INT4Linear(nn.Module):
109
+ """Group-wise symmetric INT4 weight-only quantization (group_size=128).
110
+ Approximates GPTQ-style quantization without calibration data."""
111
+ def __init__(self, in_f, out_f, group_size=128, bias=True):
112
+ super().__init__()
113
+ self.in_features = in_f
114
+ self.out_features = out_f
115
+ self.group_size = group_size
116
+ # Store as int8 for simplicity (each value uses [-7,7] range of int8)
117
+ self.register_buffer("weight_q", torch.empty(out_f, in_f, dtype=torch.int8))
118
+ self.register_buffer("weight_scale", torch.empty(
119
+ out_f, (in_f + group_size - 1) // group_size, dtype=torch.float32))
120
+ self.has_bias = bias
121
+ if bias:
122
+ self.register_buffer("bias", torch.zeros(out_f, dtype=torch.bfloat16))
123
+ else:
124
+ self.bias = None
125
+
126
+ @staticmethod
127
+ def from_linear(linear, group_size=128):
128
+ in_f = linear.in_features
129
+ out_f = linear.out_features
130
+ q = INT4Linear(in_f, out_f, group_size, linear.bias is not None)
131
+ w = linear.weight.data.detach().to(torch.bfloat16)
132
+ n_groups = (in_f + group_size - 1) // group_size
133
+ pad = n_groups * group_size - in_f
134
+ if pad > 0:
135
+ w = nn.functional.pad(w, (0, pad))
136
+ w_g = w.reshape(out_f, n_groups, group_size)
137
+ scale = w_g.abs().amax(dim=-1, keepdim=True).clamp(min=1e-10) / 7.0
138
+ w_q = (w_g / scale).round().clamp(-7, 7).to(torch.int8)
139
+ q.weight_q.data.copy_(w_q.reshape(out_f, -1)[:, :in_f])
140
+ q.weight_scale.data.copy_(scale.squeeze(-1)[:, :n_groups])
141
+ if linear.bias is not None:
142
+ q.bias.data.copy_(linear.bias.data.detach().to(torch.bfloat16))
143
+ return q
144
+
145
+ def forward(self, x):
146
+ s = self.weight_scale.repeat_interleave(self.group_size, dim=1)[:, :self.in_features]
147
+ w = self.weight_q[:, :self.in_features].to(torch.bfloat16) * s
148
+ return nn.functional.linear(x, w, self.bias)
149
+
150
+
151
+ class INT3Linear(nn.Module):
152
+ """Group-wise symmetric INT3 weight-only quantization (group_size=128).
153
+ Values in range [-3, 3]."""
154
+ def __init__(self, in_f, out_f, group_size=128, bias=True):
155
+ super().__init__()
156
+ self.in_features = in_f
157
+ self.out_features = out_f
158
+ self.group_size = group_size
159
+ self.register_buffer("weight_q", torch.empty(out_f, in_f, dtype=torch.int8))
160
+ self.register_buffer("weight_scale", torch.empty(
161
+ out_f, (in_f + group_size - 1) // group_size, dtype=torch.float32))
162
+ self.has_bias = bias
163
+ if bias:
164
+ self.register_buffer("bias", torch.zeros(out_f, dtype=torch.bfloat16))
165
+ else:
166
+ self.bias = None
167
+
168
+ @staticmethod
169
+ def from_linear(linear, group_size=128):
170
+ in_f = linear.in_features
171
+ out_f = linear.out_features
172
+ q = INT3Linear(in_f, out_f, group_size, linear.bias is not None)
173
+ w = linear.weight.data.detach().to(torch.bfloat16)
174
+ n_groups = (in_f + group_size - 1) // group_size
175
+ pad = n_groups * group_size - in_f
176
+ if pad > 0:
177
+ w = nn.functional.pad(w, (0, pad))
178
+ w_g = w.reshape(out_f, n_groups, group_size)
179
+ scale = w_g.abs().amax(dim=-1, keepdim=True).clamp(min=1e-10) / 3.0
180
+ w_q = (w_g / scale).round().clamp(-3, 3).to(torch.int8)
181
+ q.weight_q.data.copy_(w_q.reshape(out_f, -1)[:, :in_f])
182
+ q.weight_scale.data.copy_(scale.squeeze(-1)[:, :n_groups])
183
+ if linear.bias is not None:
184
+ q.bias.data.copy_(linear.bias.data.detach().to(torch.bfloat16))
185
+ return q
186
+
187
+ def forward(self, x):
188
+ s = self.weight_scale.repeat_interleave(self.group_size, dim=1)[:, :self.in_features]
189
+ w = self.weight_q[:, :self.in_features].to(torch.bfloat16) * s
190
+ return nn.functional.linear(x, w, self.bias)
191
+
192
+
193
+ class INT2Linear(nn.Module):
194
+ """Group-wise symmetric INT2 weight-only quantization (group_size=64).
195
+ Values in range [-1, 0, 1]."""
196
+ def __init__(self, in_f, out_f, group_size=64, bias=True):
197
+ super().__init__()
198
+ self.in_features = in_f
199
+ self.out_features = out_f
200
+ self.group_size = group_size
201
+ self.register_buffer("weight_q", torch.empty(out_f, in_f, dtype=torch.int8))
202
+ self.register_buffer("weight_scale", torch.empty(
203
+ out_f, (in_f + group_size - 1) // group_size, dtype=torch.float32))
204
+ self.has_bias = bias
205
+ if bias:
206
+ self.register_buffer("bias", torch.zeros(out_f, dtype=torch.bfloat16))
207
+ else:
208
+ self.bias = None
209
+
210
+ @staticmethod
211
+ def from_linear(linear, group_size=64):
212
+ in_f = linear.in_features
213
+ out_f = linear.out_features
214
+ q = INT2Linear(in_f, out_f, group_size, linear.bias is not None)
215
+ w = linear.weight.data.detach().to(torch.bfloat16)
216
+ n_groups = (in_f + group_size - 1) // group_size
217
+ pad = n_groups * group_size - in_f
218
+ if pad > 0:
219
+ w = nn.functional.pad(w, (0, pad))
220
+ w_g = w.reshape(out_f, n_groups, group_size)
221
+ scale = w_g.abs().amax(dim=-1, keepdim=True).clamp(min=1e-10) / 1.0
222
+ w_q = (w_g / scale).round().clamp(-1, 1).to(torch.int8)
223
+ q.weight_q.data.copy_(w_q.reshape(out_f, -1)[:, :in_f])
224
+ q.weight_scale.data.copy_(scale.squeeze(-1)[:, :n_groups])
225
+ if linear.bias is not None:
226
+ q.bias.data.copy_(linear.bias.data.detach().to(torch.bfloat16))
227
+ return q
228
+
229
+ def forward(self, x):
230
+ s = self.weight_scale.repeat_interleave(self.group_size, dim=1)[:, :self.in_features]
231
+ w = self.weight_q[:, :self.in_features].to(torch.bfloat16) * s
232
+ return nn.functional.linear(x, w, self.bias)
233
+
234
+
235
+
236
+ # ============================================================
237
+ # QUANTIZATION APPLIER
238
+ # ============================================================
239
+
240
+ def apply_quantization(model, quant_class, target="slow_ar", skip_names=None, **kwargs):
241
+ """Replace nn.Linear layers with quantized versions.
242
+
243
+ Args:
244
+ target: 'slow_ar' = only Slow AR (36 layers), 'all' = both Slow + Fast AR
245
+ skip_names: list of name substrings to skip (e.g., ['embed', 'norm'])
246
+ """
247
+ if skip_names is None:
248
+ skip_names = ['embed', 'norm']
249
+ count = 0
250
+ for name, module in list(model.named_modules()):
251
+ if not isinstance(module, nn.Linear):
252
+ continue
253
+ if any(s in name for s in skip_names):
254
+ continue
255
+ is_fast = "fast_" in name
256
+ if target == "slow_ar" and is_fast:
257
+ continue
258
+ parts = name.split(".")
259
+ parent = model
260
+ for p in parts[:-1]:
261
+ parent = getattr(parent, p)
262
+ try:
263
+ quantized = quant_class.from_linear(module, **kwargs)
264
+ setattr(parent, parts[-1], quantized)
265
+ count += 1
266
+ except Exception as e:
267
+ print(f" Skip {name}: {e}")
268
+ return model, count
269
+
270
+
271
+ def get_model_size_mb(model):
272
+ """Get total model size in MB"""
273
+ total = 0
274
+ for p in model.parameters():
275
+ total += p.numel() * p.element_size()
276
+ for b in model.buffers():
277
+ total += b.numel() * b.element_size()
278
+ return total / (1024 * 1024)
279
+
280
+
281
+ # ============================================================
282
+ # SAMPLE GENERATION
283
+ # ============================================================
284
+
285
+ def generate_tts_simple(model, codec, text, output_path, device="cuda"):
286
+ """Generate TTS sample without reference audio (text-only)."""
287
+ import torchaudio
288
+ from fish_speech.tokenizer import IM_END_TOKEN
289
+ from fish_speech.models.text2semantic.inference import generate, decode_one_token_ar
290
+ from fish_speech.content_sequence import TextPart
291
+ from fish_speech.conversation import Conversation, Message
292
+
293
+ conv = Conversation()
294
+ conv.add_message(Message(role="user", parts=[TextPart(text="")]))
295
+ conv.add_message(Message(role="assistant", parts=[TextPart(text=text)]))
296
+ prompt = conv.encode_for_inference(model.config)
297
+ codebook_dim = 1 + model.config.num_codebooks
298
+ audio_masks = torch.zeros(1, codebook_dim, prompt.shape[-1], dtype=torch.bool, device=device)
299
+ audio_parts = torch.zeros(1, codebook_dim, prompt.shape[-1], dtype=torch.long, device=device)
300
+
301
+ if not getattr(model, '_cache_setup_done', False):
302
+ model.setup_caches(max_batch_size=1, max_seq_len=model.config.max_seq_len, dtype=DTYPE)
303
+ model._cache_setup_done = True
304
+
305
+ with torch.autocast(device_type="cuda", dtype=DTYPE):
306
+ result = generate(
307
+ model=model, prompt=prompt, max_new_tokens=512,
308
+ audio_masks=audio_masks, audio_parts=audio_parts,
309
+ temperature=0.7, top_p=0.7, top_k=30,
310
+ decode_one_token=decode_one_token_ar,
311
+ )
312
+ codes = result[0:1, :, :].unsqueeze(0)
313
+ with torch.autocast(device_type="cuda", dtype=DTYPE):
314
+ audio = codec.decode(codes.to(device))
315
+ audio_np = audio.squeeze().cpu().float().numpy()
316
+ sr = getattr(codec, 'sample_rate', 44100)
317
+ sf.write(output_path, audio_np, sr)
318
+ dur = len(audio_np) / sr
319
+ print(f" Saved: {output_path} ({dur:.1f}s)")
320
+ return True, dur
321
+
322
+
323
+ def generate_voice_clone(model, codec, text, ref_path, ref_text, output_path, device="cuda"):
324
+ """Generate voice-cloned TTS sample from reference audio."""
325
+ import torchaudio
326
+ from fish_speech.models.text2semantic.inference import generate, decode_one_token_ar
327
+ from fish_speech.content_sequence import TextPart, VQPart
328
+ from fish_speech.conversation import Conversation, Message
329
+
330
+ wav, sr = torchaudio.load(ref_path)
331
+ if wav.shape[0] > 1:
332
+ wav = wav.mean(dim=0, keepdim=True)
333
+ if sr != 44100:
334
+ wav = torchaudio.functional.resample(wav, sr, 44100)
335
+ wav = wav.to(device)
336
+
337
+ with torch.autocast(device_type="cuda", dtype=DTYPE):
338
+ encoded = codec.encode(wav.unsqueeze(0))
339
+ prompt_tokens = (encoded[0] if isinstance(encoded, tuple) else encoded).cpu().numpy()
340
+
341
+ conv = Conversation()
342
+ conv.add_message(Message(role="user", parts=[
343
+ VQPart(codes=prompt_tokens), TextPart(text=ref_text)]))
344
+ conv.add_message(Message(role="assistant", parts=[TextPart(text=text)]))
345
+
346
+ prompt = conv.encode_for_inference(model.config)
347
+ codebook_dim = 1 + model.config.num_codebooks
348
+ audio_masks = torch.zeros(1, codebook_dim, prompt.shape[-1], dtype=torch.bool, device=device)
349
+ audio_parts = torch.zeros(1, codebook_dim, prompt.shape[-1], dtype=torch.long, device=device)
350
+
351
+ if not getattr(model, '_cache_setup_done', False):
352
+ model.setup_caches(max_batch_size=1, max_seq_len=model.config.max_seq_len, dtype=DTYPE)
353
+ model._cache_setup_done = True
354
+
355
+ with torch.autocast(device_type="cuda", dtype=DTYPE):
356
+ result = generate(
357
+ model=model, prompt=prompt, max_new_tokens=512,
358
+ audio_masks=audio_masks, audio_parts=audio_parts,
359
+ temperature=0.7, top_p=0.7, top_k=30,
360
+ decode_one_token=decode_one_token_ar,
361
+ )
362
+ codes = result[0:1, :, :].unsqueeze(0)
363
+ with torch.autocast(device_type="cuda", dtype=DTYPE):
364
+ audio = codec.decode(codes.to(device))
365
+ audio_np = audio.squeeze().cpu().float().numpy()
366
+ sr = getattr(codec, 'sample_rate', 44100)
367
+ sf.write(output_path, audio_np, sr)
368
+ dur = len(audio_np) / sr
369
+ print(f" Voice clone saved: {output_path} ({dur:.1f}s)")
370
+ return True, dur
371
+
372
+
373
+
374
+ # ============================================================
375
+ # PHASE RUNNER
376
+ # ============================================================
377
+
378
+ def run_phase(phase_id, quant_class, target, codec, ref_audio_path, ref_text,
379
+ test_text, clone_text, output_dir, **qkwargs):
380
+ """Run one quantization phase end-to-end."""
381
+ from fish_speech.models.text2semantic.inference import init_model
382
+
383
+ phase_dir = f"{output_dir}/{phase_id}"
384
+ samples_dir = f"{output_dir}/samples"
385
+ os.makedirs(phase_dir, exist_ok=True)
386
+ os.makedirs(samples_dir, exist_ok=True)
387
+
388
+ print(f"\n{'='*60}")
389
+ print(f" {phase_id.upper()}: {quant_class.__name__} ({target})")
390
+ print(f"{'='*60}")
391
+
392
+ # Load fresh model
393
+ model, _ = init_model(BASE_MODEL, DEVICE, DTYPE, compile=False)
394
+ orig_size = get_model_size_mb(model)
395
+
396
+ # Quantize
397
+ t0 = time.time()
398
+ model, n_layers = apply_quantization(model, quant_class, target=target, **qkwargs)
399
+ model = model.to(DEVICE)
400
+ t_quant = time.time() - t0
401
+
402
+ quant_size = get_model_size_mb(model)
403
+ ratio = orig_size / quant_size if quant_size > 0 else 0
404
+ print(f" {orig_size:.0f} MB -> {quant_size:.0f} MB ({ratio:.2f}x, {n_layers} layers, {t_quant:.1f}s)")
405
+
406
+ # Save
407
+ save_path = f"{phase_dir}/model.safetensors"
408
+ save_file(model.state_dict(), save_path)
409
+ disk_mb = os.path.getsize(save_path) / (1024*1024)
410
+ print(f" Disk: {disk_mb:.0f} MB")
411
+
412
+ # Generate TTS sample
413
+ tts_ok, tts_dur = False, 0
414
+ try:
415
+ tts_ok, tts_dur = generate_tts_simple(
416
+ model, codec, test_text, f"{samples_dir}/{phase_id}_tts.wav")
417
+ except Exception as e:
418
+ print(f" TTS failed: {e}")
419
+
420
+ # Generate voice clone sample
421
+ clone_ok, clone_dur = False, 0
422
+ if ref_audio_path and os.path.exists(ref_audio_path):
423
+ try:
424
+ clone_ok, clone_dur = generate_voice_clone(
425
+ model, codec, clone_text, ref_audio_path, ref_text,
426
+ f"{samples_dir}/{phase_id}_clone.wav")
427
+ except Exception as e:
428
+ print(f" Clone failed: {e}")
429
+
430
+ del model
431
+ gc.collect()
432
+ torch.cuda.empty_cache()
433
+
434
+ result = {
435
+ "phase": phase_id, "method": quant_class.__name__, "target": target,
436
+ "original_mb": round(orig_size), "quantized_mb": round(quant_size),
437
+ "disk_mb": round(disk_mb), "compression": round(ratio, 3),
438
+ "n_layers": n_layers, "time_s": round(t_quant, 1),
439
+ "tts_ok": tts_ok, "tts_dur_s": round(tts_dur, 1),
440
+ "clone_ok": clone_ok, "clone_dur_s": round(clone_dur, 1),
441
+ }
442
+ with open(f"{phase_dir}/results.json", "w") as f:
443
+ json.dump(result, f, indent=2)
444
+ return result
445
+
446
+
447
+
448
+ # ============================================================
449
+ # MAIN
450
+ # ============================================================
451
+
452
+ TEST_TEXT = (
453
+ "The quick brown fox jumps over the lazy dog. "
454
+ "Artificial intelligence is transforming the way we communicate with machines."
455
+ )
456
+ CLONE_TEXT = (
457
+ "Hello everyone, welcome to this special presentation. "
458
+ "Today we explore the fascinating world of neural text to speech synthesis."
459
+ )
460
+ REF_TEXT = "This is a reference voice recording used for demonstration purposes."
461
+ # Use the "Morgan Freeman" style reference text
462
+ CELEBRITY_REF_TEXT = (
463
+ "Good morning. I want to tell you something about the universe. "
464
+ "Every atom in your body came from a star that exploded. "
465
+ "We are all made of star stuff."
466
+ )
467
+
468
+ PHASES = {
469
+ "1a": {"cls": FP8Linear, "target": "slow_ar", "kwargs": {}},
470
+ "1b": {"cls": INT4Linear, "target": "slow_ar", "kwargs": {"group_size": 128}},
471
+ "2a": {"cls": INT4Linear, "target": "all", "kwargs": {"group_size": 128}},
472
+ "2b": {"cls": INT8Linear, "target": "slow_ar", "kwargs": {}},
473
+ "2c": {"cls": INT3Linear, "target": "slow_ar", "kwargs": {"group_size": 128}},
474
+ "3a": {"cls": INT2Linear, "target": "slow_ar", "kwargs": {"group_size": 64}},
475
+ "3b": {"cls": INT2Linear, "target": "all", "kwargs": {"group_size": 64}},
476
+ }
477
+
478
+ def main():
479
+ parser = argparse.ArgumentParser(description="Fish Speech S2 Pro Quantization")
480
+ parser.add_argument("--phase", default="all", help="Phase to run (1a,1b,2a,2b,2c,3a,3b,all)")
481
+ parser.add_argument("--output", default="./output", help="Output directory")
482
+ parser.add_argument("--model", default=BASE_MODEL, help="Model ID or path")
483
+ parser.add_argument("--ref-audio", default=None, help="Reference audio for voice cloning")
484
+ args = parser.parse_args()
485
+
486
+ global BASE_MODEL
487
+ BASE_MODEL = args.model
488
+ output_dir = args.output
489
+ os.makedirs(f"{output_dir}/samples", exist_ok=True)
490
+
491
+ # Setup
492
+ if not os.path.exists("fish-speech"):
493
+ os.system("git clone --depth 1 https://github.com/fishaudio/fish-speech.git")
494
+ sys.path.insert(0, "fish-speech")
495
+
496
+ from fish_speech.models.text2semantic.inference import init_model
497
+ from fish_speech.models.dac.inference import load_codec_model
498
+
499
+ # Load codec (shared)
500
+ print("Loading codec...")
501
+ codec = load_codec_model(f"{BASE_MODEL}/codec.pth", DEVICE, DTYPE)
502
+
503
+ # Generate reference audio from base model
504
+ ref_path = args.ref_audio or f"{output_dir}/reference_celebrity.wav"
505
+ if not os.path.exists(ref_path):
506
+ print("Generating celebrity-style reference audio from base model...")
507
+ model_base, _ = init_model(BASE_MODEL, DEVICE, DTYPE, compile=False)
508
+ try:
509
+ generate_tts_simple(model_base, codec, CELEBRITY_REF_TEXT, ref_path)
510
+ print(f"Reference audio saved: {ref_path}")
511
+ except Exception as e:
512
+ print(f"Warning: Could not generate reference audio: {e}")
513
+ ref_path = None
514
+
515
+ # Generate baseline sample
516
+ try:
517
+ generate_tts_simple(model_base, codec, TEST_TEXT, f"{output_dir}/samples/baseline_bf16_tts.wav")
518
+ if ref_path:
519
+ generate_voice_clone(model_base, codec, CLONE_TEXT, ref_path, REF_TEXT,
520
+ f"{output_dir}/samples/baseline_bf16_clone.wav")
521
+ except Exception as e:
522
+ print(f"Warning: Baseline generation issue: {e}")
523
+ del model_base
524
+ gc.collect()
525
+ torch.cuda.empty_cache()
526
+
527
+ # Select phases to run
528
+ if args.phase == "all":
529
+ phases_to_run = list(PHASES.keys())
530
+ else:
531
+ phases_to_run = [p.strip() for p in args.phase.split(",")]
532
+
533
+ all_results = []
534
+ for pid in phases_to_run:
535
+ if pid not in PHASES:
536
+ print(f"Unknown phase: {pid}")
537
+ continue
538
+ cfg = PHASES[pid]
539
+ r = run_phase(
540
+ f"phase{pid}", cfg["cls"], cfg["target"], codec,
541
+ ref_path, REF_TEXT, TEST_TEXT, CLONE_TEXT, output_dir,
542
+ **cfg["kwargs"]
543
+ )
544
+ all_results.append(r)
545
+
546
+ # Summary
547
+ print(f"\n{'='*70}")
548
+ print(" QUANTIZATION EXPERIMENT SUMMARY")
549
+ print(f"{'='*70}")
550
+ print(f"{'Phase':<12} {'Method':<12} {'Target':<10} {'Disk MB':<10} {'Ratio':<8} {'TTS':<5} {'Clone':<5}")
551
+ print("-" * 65)
552
+ for r in all_results:
553
+ print(f"{r['phase']:<12} {r['method']:<12} {r['target']:<10} {r['disk_mb']:<10} {r['compression']:<8.2f} "
554
+ f"{'OK' if r['tts_ok'] else 'FAIL':<5} {'OK' if r['clone_ok'] else 'FAIL':<5}")
555
+
556
+ with open(f"{output_dir}/all_results.json", "w") as f:
557
+ json.dump(all_results, f, indent=2)
558
+ print(f"\nAll results saved to {output_dir}/all_results.json")
559
+
560
+ if __name__ == "__main__":
561
+ main()
562
+