ABLingss commited on
Commit
6f19275
·
1 Parent(s): 57c72d2
Files changed (2) hide show
  1. app.py +264 -223
  2. src/heartlib/pipelines/music_generation.py +191 -2
app.py CHANGED
@@ -17,7 +17,6 @@ import uuid
17
  import subprocess
18
  import tempfile
19
  import wave
20
- import time
21
  from pathlib import Path
22
  from typing import Dict, Any, Iterator, Optional, Tuple, TYPE_CHECKING
23
 
@@ -29,7 +28,15 @@ try:
29
  except Exception:
30
  spaces = None
31
 
32
- GPU_MAX_DURATION = int(os.environ.get("GPU_MAX_DURATION", "100"))
 
 
 
 
 
 
 
 
33
 
34
 
35
  def _env_bool(name: str) -> Optional[bool]:
@@ -41,7 +48,7 @@ def _env_bool(name: str) -> Optional[bool]:
41
 
42
  _default_keep_model_loaded_env = _env_bool("KEEP_MODEL_LOADED_DEFAULT")
43
  if _default_keep_model_loaded_env is None:
44
- DEFAULT_KEEP_MODEL_LOADED = not (spaces is not None and os.environ.get("SPACE_ID"))
45
  else:
46
  DEFAULT_KEEP_MODEL_LOADED = _default_keep_model_loaded_env
47
 
@@ -59,9 +66,11 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
59
 
60
  # HF Spaces: disable SSR to avoid Node proxy/port issues.
61
  os.environ.setdefault("GRADIO_SSR_MODE", "0")
 
 
62
 
63
  if TYPE_CHECKING:
64
- from heartlib import HeartMuLaGenPipeline, HeartTranscriptorPipeline
65
  import gradio as gr
66
  import re
67
  from google import genai
@@ -209,26 +218,100 @@ except Exception as e:
209
  GRADIO_QUEUE_MAX_SIZE = int(os.environ.get("GRADIO_QUEUE_MAX_SIZE", "24"))
210
  GRADIO_DEFAULT_CONCURRENCY = int(os.environ.get("GRADIO_DEFAULT_CONCURRENCY", "1"))
211
  GPU_CONCURRENCY_LIMIT = int(os.environ.get("GRADIO_GPU_CONCURRENCY", "1"))
212
- STREAM_MIN_CHUNK_SEC = float(os.environ.get("STREAM_MIN_CHUNK_SEC", "0"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
 
214
 
215
  class ModelManager:
216
  def __init__(self, model_path: str, use_deepspeed_override: Optional[bool] = None):
217
  import torch
218
- from heartlib import HeartMuLaGenPipeline, HeartTranscriptorPipeline
219
 
220
  self.model_path = model_path
221
  self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
222
  self.dtype = torch.bfloat16 if torch.cuda.is_available() else torch.float32
223
  self._gen_pipes: Dict[Tuple[str, str, str], "HeartMuLaGenPipeline"] = {}
224
- self._transcribe_pipe: Optional["HeartTranscriptorPipeline"] = None
225
  if use_deepspeed_override is None:
226
  self.use_deepspeed = os.getenv("USE_DEEPSPEED_INFERENCE", "0").lower() in ("1", "true", "yes")
227
  else:
228
  self.use_deepspeed = use_deepspeed_override
229
  self.ds_inference_config = self._make_ds_inference_config()
230
  self._HeartMuLaGenPipeline = HeartMuLaGenPipeline
231
- self._HeartTranscriptorPipeline = HeartTranscriptorPipeline
232
 
233
  def _make_ds_inference_config(self) -> Dict[str, Any]:
234
  if not self.use_deepspeed:
@@ -249,6 +332,8 @@ class ModelManager:
249
  return None
250
  if self.device.type != "cuda":
251
  raise gr.Error("Quantization requires CUDA.")
 
 
252
  if quant_mode == "4bit":
253
  quant_type = "nf4"
254
  try:
@@ -278,22 +363,13 @@ class ModelManager:
278
  version=version,
279
  codec_version=codec_version,
280
  bnb_config=bnb_config,
 
281
  lazy_load=True,
282
  use_deepspeed=self.use_deepspeed,
283
  ds_inference_config=self.ds_inference_config,
284
  )
285
  return self._gen_pipes[key]
286
 
287
- def get_transcriptor(self) -> "HeartTranscriptorPipeline":
288
- import torch
289
- if self._transcribe_pipe is None:
290
- self._transcribe_pipe = self._HeartTranscriptorPipeline.from_pretrained(
291
- self.model_path,
292
- device=self.device,
293
- dtype=torch.float16 if self.device.type == "cuda" else torch.float32,
294
- )
295
- return self._transcribe_pipe
296
-
297
 
298
  model_managers: Dict[str, ModelManager] = {}
299
 
@@ -459,50 +535,18 @@ def download_models_if_needed(ckpt_dir):
459
  print("")
460
 
461
 
462
- def check_transcriptor_exists(ckpt_dir):
463
- """Check if HeartTranscriptor checkpoint exists"""
464
- return os.path.exists(os.path.join(ckpt_dir, "HeartTranscriptor-oss"))
465
-
466
-
467
- def download_transcriptor_if_needed(ckpt_dir):
468
- """Download HeartTranscriptor from ModelScope if not present"""
469
- if check_transcriptor_exists(ckpt_dir):
470
- print("=" * 50)
471
- print(f"✓ HeartTranscriptor found in {ckpt_dir}")
472
- print("✓ Skipping HeartTranscriptor download")
473
- print("=" * 50)
474
- return
475
-
476
- print("=" * 50)
477
- print("⬇ Downloading HeartTranscriptor from ModelScope")
478
- print("=" * 50)
479
- print("")
480
-
481
- from modelscope import snapshot_download
482
-
483
- snapshot_download(
484
- 'HeartMuLa/HeartTranscriptor-oss',
485
- local_dir=os.path.join(ckpt_dir, 'HeartTranscriptor-oss')
486
- )
487
-
488
- print("✓ HeartTranscriptor download completed")
489
- print("")
490
-
491
-
492
  def load_pipeline(model_path, version, codec_version, quant_mode, use_acceleration: bool):
493
  """Load HeartMuLa pipeline (lazy)"""
494
- if not use_acceleration:
495
- quant_mode = "none"
496
  manager = get_model_manager(use_acceleration)
497
  print(f"Using model from {model_path} on {manager.device}...")
498
  return manager.get_gen_pipeline(version, codec_version, quant_mode)
499
 
500
 
501
- def load_transcriptor(model_path):
502
- """Load HeartTranscriptor pipeline"""
503
- download_transcriptor_if_needed(model_path)
504
- manager = get_model_manager(use_acceleration=True)
505
- return manager.get_transcriptor()
506
 
507
 
508
  def generate(
@@ -523,11 +567,7 @@ def generate(
523
  ):
524
  """Generate music"""
525
  import torch
526
- if not lyrics.strip():
527
- raise gr.Error("Please enter lyrics")
528
-
529
- if not tags.strip():
530
- raise gr.Error("Please enter tags")
531
 
532
  max_audio_length_ms = int(duration_sec * 1000)
533
 
@@ -538,7 +578,7 @@ def generate(
538
  pipe = load_pipeline(MODEL_PATH, version, codec_version, quant_mode, use_acceleration)
539
  output_path = os.path.join(DATA_DIR, f"gen_{uuid.uuid4().hex}.wav")
540
 
541
- with torch.no_grad():
542
  pipe(
543
  {
544
  "lyrics": lyrics,
@@ -566,6 +606,9 @@ def generate(
566
 
567
  except Exception as e:
568
  raise gr.Error(f"Generation error: {str(e)}")
 
 
 
569
 
570
 
571
  @_gpu_guard
@@ -636,37 +679,6 @@ def generate_accelerated(
636
  )
637
 
638
 
639
- @_gpu_guard
640
- def transcribe_audio(audio_path, task, max_new_tokens, num_beams, temperature):
641
- """Transcribe or translate lyrics from audio"""
642
- import torch
643
- if not audio_path:
644
- raise gr.Error("Please upload an audio file")
645
-
646
- pipe = load_transcriptor(MODEL_PATH)
647
-
648
- try:
649
- with torch.no_grad():
650
- result = pipe(
651
- audio_path,
652
- **{
653
- "max_new_tokens": int(max_new_tokens),
654
- "num_beams": int(num_beams),
655
- "task": task,
656
- "condition_on_prev_tokens": False,
657
- "compression_ratio_threshold": 1.8,
658
- "temperature": float(temperature),
659
- "logprob_threshold": -1.0,
660
- "no_speech_threshold": 0.4,
661
- },
662
- )
663
- if isinstance(result, dict):
664
- return result.get("text", "")
665
- return str(result)
666
- except Exception as e:
667
- raise gr.Error(f"Transcription error: {str(e)}")
668
-
669
-
670
  def _normalize_stream_chunk(chunk: np.ndarray) -> np.ndarray:
671
  chunk = np.nan_to_num(chunk, nan=0.0, posinf=0.0, neginf=0.0)
672
  return np.clip(chunk, -1.0, 1.0)
@@ -685,9 +697,9 @@ def generate_music_streaming(
685
  keep_model_loaded,
686
  offload_mode,
687
  backend,
688
- chunk_frames,
689
  use_acceleration,
690
  ) -> Iterator[Tuple[int, np.ndarray]]:
 
691
  if backend == "exllama_v2":
692
  raise gr.Error("ExLlamaV2 backend is not implemented yet.")
693
  pipe = load_pipeline(MODEL_PATH, version, codec_version, quant_mode, use_acceleration)
@@ -698,7 +710,7 @@ def generate_music_streaming(
698
  temperature=temperature,
699
  topk=topk,
700
  cfg_scale=cfg_scale,
701
- chunk_frames=int(chunk_frames),
702
  keep_model_loaded=keep_model_loaded,
703
  offload_mode=offload_mode,
704
  ):
@@ -706,8 +718,8 @@ def generate_music_streaming(
706
  chunk = chunk.squeeze(0)
707
  chunk_np = chunk.cpu().numpy()
708
  chunk_np = _normalize_stream_chunk(chunk_np)
709
- print(f"stream chunk: samples={chunk_np.shape[0]} sr=48000")
710
- yield 48000, chunk_np
711
 
712
 
713
  def stream_generate(
@@ -723,22 +735,14 @@ def stream_generate(
723
  keep_model_loaded,
724
  offload_mode,
725
  backend,
726
- output_format,
727
- chunk_frames,
728
  use_acceleration,
729
  ):
730
  try:
731
- min_samples = max(0, int(STREAM_MIN_CHUNK_SEC * 48000))
732
- buffer = []
733
- buffered_samples = 0
734
- last_yield_samples = 0
735
- print(
736
- "stream start:",
737
- f"min_chunk_sec={STREAM_MIN_CHUNK_SEC}",
738
- f"duration_sec={duration_sec}",
739
- f"chunk_frames={chunk_frames}",
740
- )
741
-
742
  for sr, chunk_np in generate_music_streaming(
743
  lyrics=lyrics,
744
  tags=tags,
@@ -752,26 +756,19 @@ def stream_generate(
752
  keep_model_loaded=keep_model_loaded,
753
  offload_mode=offload_mode,
754
  backend=backend,
755
- chunk_frames=chunk_frames,
756
  use_acceleration=use_acceleration,
757
  ):
758
  chunk_np = chunk_np.astype("float32", copy=False)
759
- if min_samples <= 0:
760
- print(f"stream yield: samples={chunk_np.shape[0]}")
761
- yield sr, chunk_np
762
- continue
763
- buffer.append(chunk_np)
764
- buffered_samples += chunk_np.shape[0]
765
- if buffered_samples - last_yield_samples < min_samples:
766
  continue
767
- full_audio = np.concatenate(buffer)
768
- last_yield_samples = buffered_samples
769
- print(f"stream yield: samples={full_audio.shape[0]}")
770
- yield sr, full_audio
771
- if min_samples > 0 and buffer:
772
- full_audio = np.concatenate(buffer)
773
- print(f"stream final yield: samples={full_audio.shape[0]}")
774
- yield 48000, full_audio
775
  except Exception as e:
776
  raise gr.Error(f"Streaming error: {str(e)}")
777
 
@@ -790,8 +787,6 @@ def stream_generate_accelerated(
790
  keep_model_loaded,
791
  offload_mode,
792
  backend,
793
- output_format,
794
- chunk_frames,
795
  ):
796
  return stream_generate(
797
  lyrics,
@@ -806,8 +801,6 @@ def stream_generate_accelerated(
806
  keep_model_loaded,
807
  offload_mode,
808
  backend,
809
- output_format,
810
- chunk_frames,
811
  True,
812
  )
813
 
@@ -977,10 +970,15 @@ Output ONLY the lyrics with structure tags, no explanations.
977
 
978
  def create_ui():
979
  """Create Gradio UI"""
 
 
 
 
980
 
981
  with gr.Blocks(title="HeartMuLa Music Generation") as demo:
982
  gr.Markdown("# HeartMuLa Music Generation")
983
  gr.Markdown("Generate music from lyrics and style tags")
 
984
 
985
  with gr.Tabs():
986
  # Tab 1: Music Generation
@@ -1030,10 +1028,19 @@ def create_ui():
1030
  t7 = gr.CheckboxGroup(choices=TAG_DATA["Topic"], label="Topic")
1031
  tag_checkboxes.append(t7)
1032
 
 
 
 
 
 
 
 
 
 
1033
  # Generation parameters
1034
  with gr.Row():
1035
  cfg_scale = gr.Slider(0.0, 3.0, value=1.5, step=0.1, label="CFG Scale")
1036
- duration = gr.Slider(10, 300, value=180, step=10, label="Duration (sec)")
1037
 
1038
  with gr.Row():
1039
  temperature = gr.Slider(0.1, 2.0, value=1.0, step=0.1, label="Temperature")
@@ -1056,9 +1063,14 @@ def create_ui():
1056
  label="Codec Version"
1057
  )
1058
  quant_mode = gr.Dropdown(
1059
- choices=[("None", "none"), ("4-bit (NF4/FP4)", "4bit"), ("8-bit", "8bit")],
1060
- value="none",
1061
- label="Quantization"
 
 
 
 
 
1062
  )
1063
  keep_model_loaded = gr.Checkbox(
1064
  value=DEFAULT_KEEP_MODEL_LOADED,
@@ -1066,43 +1078,76 @@ def create_ui():
1066
  )
1067
  offload_mode = gr.Dropdown(
1068
  choices=["auto", "aggressive"],
1069
- value="auto",
1070
  label="Offload Mode"
1071
  )
1072
  output_format = gr.Radio(
1073
  choices=[("WAV", "wav"), ("MP3", "mp3")],
1074
  value="wav",
1075
- label="Output Format"
 
1076
  )
1077
- chunk_frames = gr.Slider(
1078
- 5, 100, value=20, step=1, label="Streaming Chunk Frames"
 
1079
  )
1080
 
1081
  gr.Markdown("### 🚀 Generation")
1082
 
1083
  generation_mode = gr.Radio(
1084
  choices=["Original (No Acceleration)", "Accelerated"],
1085
- value="Original (No Acceleration)",
1086
  label="Generation Mode",
1087
  )
1088
 
1089
  speed_submode = gr.Radio(
1090
- choices=["Standard", "Streaming"],
1091
- value="Standard",
 
 
 
1092
  label="Accelerated Options",
1093
- visible=False,
1094
  )
1095
 
1096
- btn_original = gr.Button("🎼 Generate Music (Original)", variant="primary", size="lg", visible=True)
1097
- btn_accel = gr.Button("🎼 Generate Music (Accelerated)", variant="primary", size="lg", visible=False)
1098
- btn_stream = gr.Button("🎼 Generate Music (Streaming)", variant="primary", size="lg", visible=False)
1099
- cancel_stream_btn = gr.Button("Cancel Streaming", variant="secondary", size="lg", visible=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1100
  cancel_state = gr.State()
1101
 
1102
  with gr.Column():
1103
  # Notice section
1104
  with gr.Accordion("Usage Notice", open=True):
1105
  gr.Markdown("""
 
 
 
 
 
 
 
1106
  ### Lyrics Format Requirements
1107
 
1108
  **Automatic Processing:**
@@ -1142,14 +1187,16 @@ Every day the fire burns
1142
  label="Generated Music (Full)",
1143
  type="filepath",
1144
  interactive=False,
 
1145
  )
1146
  stream_audio = gr.Audio(
1147
- label="Streaming Audio (Preview)",
1148
  streaming=True,
1149
  autoplay=True,
1150
  type="numpy",
1151
  format="wav",
1152
  interactive=False,
 
1153
  )
1154
 
1155
  # Event handlers for tag selection
@@ -1171,6 +1218,9 @@ Every day the fire burns
1171
  gr.update(visible=False), # btn_accel
1172
  gr.update(visible=False), # btn_stream
1173
  gr.update(visible=False), # cancel_stream_btn
 
 
 
1174
  )
1175
  show_stream = spd_mode == "Streaming"
1176
  return (
@@ -1179,17 +1229,83 @@ Every day the fire burns
1179
  gr.update(visible=not show_stream), # btn_accel
1180
  gr.update(visible=show_stream), # btn_stream
1181
  gr.update(visible=show_stream), # cancel_stream_btn
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1182
  )
1183
 
1184
  generation_mode.change(
1185
  fn=update_visibility,
1186
  inputs=[generation_mode, speed_submode],
1187
- outputs=[speed_submode, btn_original, btn_accel, btn_stream, cancel_stream_btn],
 
 
 
 
 
 
 
 
 
1188
  )
1189
  speed_submode.change(
1190
  fn=update_visibility,
1191
  inputs=[generation_mode, speed_submode],
1192
- outputs=[speed_submode, btn_original, btn_accel, btn_stream, cancel_stream_btn],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1193
  )
1194
 
1195
  btn_original.click(
@@ -1251,8 +1367,6 @@ Every day the fire burns
1251
  keep_model_loaded,
1252
  offload_mode,
1253
  backend,
1254
- output_format,
1255
- chunk_frames,
1256
  ],
1257
  outputs=[stream_audio],
1258
  concurrency_id="gpu_queue",
@@ -1397,81 +1511,8 @@ Every day the fire burns
1397
  outputs=[lyrics]
1398
  )
1399
 
1400
- # Tab 3: Lyrics Transcription
1401
- with gr.Tab("Lyrics Transcription"):
1402
- with gr.Row():
1403
- with gr.Column():
1404
- gr.Markdown("### Transcribe or Translate Lyrics from Audio")
1405
-
1406
- audio_input = gr.Audio(
1407
- label="Audio Input",
1408
- type="filepath"
1409
- )
1410
-
1411
- task_select = gr.Radio(
1412
- choices=[
1413
- ("Transcribe (Original Language)", "transcribe"),
1414
- ("Translate to English", "translate")
1415
- ],
1416
- value="transcribe",
1417
- label="Task"
1418
- )
1419
-
1420
- max_new_tokens = gr.Slider(
1421
- 64, 512, value=256, step=16, label="Max New Tokens"
1422
- )
1423
- num_beams = gr.Slider(
1424
- 1, 5, value=2, step=1, label="Beam Search"
1425
- )
1426
- transcribe_temperature = gr.Slider(
1427
- 0.0, 0.8, value=0.2, step=0.1, label="Temperature"
1428
- )
1429
-
1430
- transcribe_btn = gr.Button(
1431
- "Run Transcription",
1432
- variant="primary",
1433
- size="lg"
1434
- )
1435
-
1436
- use_generated_audio = gr.Button(
1437
- "Use Generated Music",
1438
- size="sm"
1439
- )
1440
-
1441
- with gr.Column():
1442
- with gr.Accordion("Notes", open=True):
1443
- gr.Markdown("""
1444
- ### Notes
1445
- - Best results come from **vocals-only** stems.
1446
- - If you pass full mixes, consider source separation first.
1447
- - The HeartTranscriptor model will auto-download on first use.
1448
- """)
1449
-
1450
- transcription_output = gr.Textbox(
1451
- label="Transcription Result",
1452
- lines=18,
1453
- placeholder="Transcribed lyrics will appear here...",
1454
- interactive=False
1455
- )
1456
-
1457
- transcribe_btn.click(
1458
- fn=transcribe_audio,
1459
- inputs=[audio_input, task_select, max_new_tokens, num_beams, transcribe_temperature],
1460
- outputs=[transcription_output],
1461
- concurrency_id="gpu_queue",
1462
- concurrency_limit=GPU_CONCURRENCY_LIMIT,
1463
- )
1464
-
1465
- use_generated_audio.click(
1466
- fn=lambda x: x,
1467
- inputs=[output_audio_file],
1468
- outputs=[audio_input]
1469
- )
1470
-
1471
  return demo
1472
 
1473
-
1474
- demo = create_ui()
1475
  demo.queue(max_size=GRADIO_QUEUE_MAX_SIZE, default_concurrency_limit=GRADIO_DEFAULT_CONCURRENCY)
1476
 
1477
  if __name__ == "__main__":
 
17
  import subprocess
18
  import tempfile
19
  import wave
 
20
  from pathlib import Path
21
  from typing import Dict, Any, Iterator, Optional, Tuple, TYPE_CHECKING
22
 
 
28
  except Exception:
29
  spaces = None
30
 
31
+ IS_SPACE = spaces is not None and os.environ.get("SPACE_ID")
32
+
33
+ _gpu_duration_env = os.environ.get("GPU_MAX_DURATION")
34
+ if _gpu_duration_env is None:
35
+ aoti_env = os.environ.get("ENABLE_AOTI")
36
+ enable_aoti_default = aoti_env is None or aoti_env.strip().lower() in ("1", "true", "yes", "y", "on")
37
+ GPU_MAX_DURATION = 600 if IS_SPACE and enable_aoti_default else 100
38
+ else:
39
+ GPU_MAX_DURATION = int(_gpu_duration_env)
40
 
41
 
42
  def _env_bool(name: str) -> Optional[bool]:
 
48
 
49
  _default_keep_model_loaded_env = _env_bool("KEEP_MODEL_LOADED_DEFAULT")
50
  if _default_keep_model_loaded_env is None:
51
+ DEFAULT_KEEP_MODEL_LOADED = not IS_SPACE
52
  else:
53
  DEFAULT_KEEP_MODEL_LOADED = _default_keep_model_loaded_env
54
 
 
66
 
67
  # HF Spaces: disable SSR to avoid Node proxy/port issues.
68
  os.environ.setdefault("GRADIO_SSR_MODE", "0")
69
+ # Mitigate CUDA memory fragmentation on small GPUs.
70
+ os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
71
 
72
  if TYPE_CHECKING:
73
+ from heartlib import HeartMuLaGenPipeline
74
  import gradio as gr
75
  import re
76
  from google import genai
 
218
  GRADIO_QUEUE_MAX_SIZE = int(os.environ.get("GRADIO_QUEUE_MAX_SIZE", "24"))
219
  GRADIO_DEFAULT_CONCURRENCY = int(os.environ.get("GRADIO_DEFAULT_CONCURRENCY", "1"))
220
  GPU_CONCURRENCY_LIMIT = int(os.environ.get("GRADIO_GPU_CONCURRENCY", "1"))
221
+ DEFAULT_DURATION_SEC = int(os.environ.get("DEFAULT_DURATION_SEC", "60" if IS_SPACE else "180"))
222
+ DEFAULT_QUANT_MODE = os.environ.get("DEFAULT_QUANT_MODE", "4bit" if IS_SPACE else "none")
223
+ DEFAULT_OFFLOAD_MODE = os.environ.get("DEFAULT_OFFLOAD_MODE", "aggressive" if IS_SPACE else "auto")
224
+ DEFAULT_GENERATION_MODE = os.environ.get(
225
+ "DEFAULT_GENERATION_MODE",
226
+ "Accelerated" if IS_SPACE else "Original (No Acceleration)",
227
+ )
228
+ DEFAULT_SPEED_SUBMODE = os.environ.get("DEFAULT_SPEED_SUBMODE", "Standard")
229
+ DEFAULT_PRESET = os.environ.get("DEFAULT_PRESET", "ZeroGPU Safe" if IS_SPACE else "Balanced")
230
+ MODEL_DETOKENIZE_INTERVAL_SEC = float(os.environ.get("MODEL_DETOKENIZE_INTERVAL_SEC", "29.76"))
231
+ AUDIO_SAMPLE_RATE = int(os.environ.get("AUDIO_SAMPLE_RATE", "48000"))
232
+ FRAME_MS = 80.0
233
+ BLOCK_FRAMES = max(1, int(round((MODEL_DETOKENIZE_INTERVAL_SEC * 1000.0) / FRAME_MS)))
234
+ BLOCK_SAMPLES = max(1, int(round(MODEL_DETOKENIZE_INTERVAL_SEC * AUDIO_SAMPLE_RATE)))
235
+ MIN_BUFFER_BLOCKS = int(os.environ.get("MIN_BUFFER_BLOCKS", "1"))
236
+ PREFETCH_BLOCKS = int(os.environ.get("PREFETCH_BLOCKS", "2"))
237
+ MAX_QUEUE_BLOCKS = int(os.environ.get("MAX_QUEUE_BLOCKS", "4"))
238
+
239
+ PRESET_CONFIGS = {
240
+ "ZeroGPU Safe": {
241
+ "duration": 60,
242
+ "quant_mode": "4bit",
243
+ "offload_mode": "aggressive",
244
+ "keep_model_loaded": False,
245
+ "temperature": 1.0,
246
+ "topk": 50,
247
+ "cfg_scale": 1.5,
248
+ "generation_mode": "Accelerated",
249
+ "speed_submode": "Standard",
250
+ },
251
+ "ZeroGPU AOTI FP8": {
252
+ "duration": 60,
253
+ "quant_mode": "fp8",
254
+ "offload_mode": "aggressive",
255
+ "keep_model_loaded": False,
256
+ "temperature": 1.0,
257
+ "topk": 50,
258
+ "cfg_scale": 1.5,
259
+ "generation_mode": "Accelerated",
260
+ "speed_submode": "Standard",
261
+ },
262
+ "Balanced": {
263
+ "duration": 120,
264
+ "quant_mode": "4bit" if IS_SPACE else "none",
265
+ "offload_mode": "auto",
266
+ "keep_model_loaded": DEFAULT_KEEP_MODEL_LOADED,
267
+ "temperature": 1.0,
268
+ "topk": 50,
269
+ "cfg_scale": 1.5,
270
+ "generation_mode": "Accelerated",
271
+ "speed_submode": "Standard",
272
+ },
273
+ "Quality": {
274
+ "duration": 180,
275
+ "quant_mode": "none",
276
+ "offload_mode": "auto",
277
+ "keep_model_loaded": DEFAULT_KEEP_MODEL_LOADED,
278
+ "temperature": 0.9,
279
+ "topk": 50,
280
+ "cfg_scale": 2.0,
281
+ "generation_mode": "Original (No Acceleration)",
282
+ "speed_submode": "Standard",
283
+ },
284
+ }
285
+ if DEFAULT_PRESET not in PRESET_CONFIGS:
286
+ DEFAULT_PRESET = "Balanced"
287
+ _default_preset_config = PRESET_CONFIGS[DEFAULT_PRESET]
288
+ if "DEFAULT_DURATION_SEC" not in os.environ:
289
+ DEFAULT_DURATION_SEC = _default_preset_config["duration"]
290
+ if "DEFAULT_QUANT_MODE" not in os.environ:
291
+ DEFAULT_QUANT_MODE = _default_preset_config["quant_mode"]
292
+ if "DEFAULT_OFFLOAD_MODE" not in os.environ:
293
+ DEFAULT_OFFLOAD_MODE = _default_preset_config["offload_mode"]
294
+ if "DEFAULT_GENERATION_MODE" not in os.environ:
295
+ DEFAULT_GENERATION_MODE = _default_preset_config["generation_mode"]
296
+ if "DEFAULT_SPEED_SUBMODE" not in os.environ:
297
+ DEFAULT_SPEED_SUBMODE = _default_preset_config["speed_submode"]
298
 
299
 
300
  class ModelManager:
301
  def __init__(self, model_path: str, use_deepspeed_override: Optional[bool] = None):
302
  import torch
303
+ from heartlib import HeartMuLaGenPipeline
304
 
305
  self.model_path = model_path
306
  self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
307
  self.dtype = torch.bfloat16 if torch.cuda.is_available() else torch.float32
308
  self._gen_pipes: Dict[Tuple[str, str, str], "HeartMuLaGenPipeline"] = {}
 
309
  if use_deepspeed_override is None:
310
  self.use_deepspeed = os.getenv("USE_DEEPSPEED_INFERENCE", "0").lower() in ("1", "true", "yes")
311
  else:
312
  self.use_deepspeed = use_deepspeed_override
313
  self.ds_inference_config = self._make_ds_inference_config()
314
  self._HeartMuLaGenPipeline = HeartMuLaGenPipeline
 
315
 
316
  def _make_ds_inference_config(self) -> Dict[str, Any]:
317
  if not self.use_deepspeed:
 
332
  return None
333
  if self.device.type != "cuda":
334
  raise gr.Error("Quantization requires CUDA.")
335
+ if quant_mode == "fp8":
336
+ return None
337
  if quant_mode == "4bit":
338
  quant_type = "nf4"
339
  try:
 
363
  version=version,
364
  codec_version=codec_version,
365
  bnb_config=bnb_config,
366
+ torchao_quantize=quant_mode == "fp8",
367
  lazy_load=True,
368
  use_deepspeed=self.use_deepspeed,
369
  ds_inference_config=self.ds_inference_config,
370
  )
371
  return self._gen_pipes[key]
372
 
 
 
 
 
 
 
 
 
 
 
373
 
374
  model_managers: Dict[str, ModelManager] = {}
375
 
 
535
  print("")
536
 
537
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
538
  def load_pipeline(model_path, version, codec_version, quant_mode, use_acceleration: bool):
539
  """Load HeartMuLa pipeline (lazy)"""
 
 
540
  manager = get_model_manager(use_acceleration)
541
  print(f"Using model from {model_path} on {manager.device}...")
542
  return manager.get_gen_pipeline(version, codec_version, quant_mode)
543
 
544
 
545
+ def _validate_generation_inputs(lyrics: str, tags: str) -> None:
546
+ if not lyrics.strip():
547
+ raise gr.Error("Please enter lyrics")
548
+ if not tags.strip():
549
+ raise gr.Error("Please enter tags")
550
 
551
 
552
  def generate(
 
567
  ):
568
  """Generate music"""
569
  import torch
570
+ _validate_generation_inputs(lyrics, tags)
 
 
 
 
571
 
572
  max_audio_length_ms = int(duration_sec * 1000)
573
 
 
578
  pipe = load_pipeline(MODEL_PATH, version, codec_version, quant_mode, use_acceleration)
579
  output_path = os.path.join(DATA_DIR, f"gen_{uuid.uuid4().hex}.wav")
580
 
581
+ with torch.inference_mode():
582
  pipe(
583
  {
584
  "lyrics": lyrics,
 
606
 
607
  except Exception as e:
608
  raise gr.Error(f"Generation error: {str(e)}")
609
+ finally:
610
+ if not keep_model_loaded and torch.cuda.is_available():
611
+ torch.cuda.empty_cache()
612
 
613
 
614
  @_gpu_guard
 
679
  )
680
 
681
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
682
  def _normalize_stream_chunk(chunk: np.ndarray) -> np.ndarray:
683
  chunk = np.nan_to_num(chunk, nan=0.0, posinf=0.0, neginf=0.0)
684
  return np.clip(chunk, -1.0, 1.0)
 
697
  keep_model_loaded,
698
  offload_mode,
699
  backend,
 
700
  use_acceleration,
701
  ) -> Iterator[Tuple[int, np.ndarray]]:
702
+ _validate_generation_inputs(lyrics, tags)
703
  if backend == "exllama_v2":
704
  raise gr.Error("ExLlamaV2 backend is not implemented yet.")
705
  pipe = load_pipeline(MODEL_PATH, version, codec_version, quant_mode, use_acceleration)
 
710
  temperature=temperature,
711
  topk=topk,
712
  cfg_scale=cfg_scale,
713
+ chunk_frames=BLOCK_FRAMES,
714
  keep_model_loaded=keep_model_loaded,
715
  offload_mode=offload_mode,
716
  ):
 
718
  chunk = chunk.squeeze(0)
719
  chunk_np = chunk.cpu().numpy()
720
  chunk_np = _normalize_stream_chunk(chunk_np)
721
+ print(f"stream chunk: samples={chunk_np.shape[0]} sr={AUDIO_SAMPLE_RATE}")
722
+ yield AUDIO_SAMPLE_RATE, chunk_np
723
 
724
 
725
  def stream_generate(
 
735
  keep_model_loaded,
736
  offload_mode,
737
  backend,
 
 
738
  use_acceleration,
739
  ):
740
  try:
741
+ _validate_generation_inputs(lyrics, tags)
742
+ start_threshold = max(MIN_BUFFER_BLOCKS, PREFETCH_BLOCKS)
743
+ queue = []
744
+ started = False
745
+ print("block stream start:", f"block_sec={MODEL_DETOKENIZE_INTERVAL_SEC}", f"duration_sec={duration_sec}")
 
 
 
 
 
 
746
  for sr, chunk_np in generate_music_streaming(
747
  lyrics=lyrics,
748
  tags=tags,
 
756
  keep_model_loaded=keep_model_loaded,
757
  offload_mode=offload_mode,
758
  backend=backend,
 
759
  use_acceleration=use_acceleration,
760
  ):
761
  chunk_np = chunk_np.astype("float32", copy=False)
762
+ queue.append(chunk_np)
763
+ if not started and len(queue) < start_threshold and len(queue) < MAX_QUEUE_BLOCKS:
 
 
 
 
 
764
  continue
765
+ if not started:
766
+ started = True
767
+ print(f"block stream start playback: buffered_blocks={len(queue)}")
768
+ while queue:
769
+ block = queue.pop(0)
770
+ print(f"block stream yield: samples={block.shape[0]}")
771
+ yield sr, block
 
772
  except Exception as e:
773
  raise gr.Error(f"Streaming error: {str(e)}")
774
 
 
787
  keep_model_loaded,
788
  offload_mode,
789
  backend,
 
 
790
  ):
791
  return stream_generate(
792
  lyrics,
 
801
  keep_model_loaded,
802
  offload_mode,
803
  backend,
 
 
804
  True,
805
  )
806
 
 
970
 
971
  def create_ui():
972
  """Create Gradio UI"""
973
+ show_stream_default = (
974
+ DEFAULT_GENERATION_MODE != "Original (No Acceleration)"
975
+ and DEFAULT_SPEED_SUBMODE == "Streaming"
976
+ )
977
 
978
  with gr.Blocks(title="HeartMuLa Music Generation") as demo:
979
  gr.Markdown("# HeartMuLa Music Generation")
980
  gr.Markdown("Generate music from lyrics and style tags")
981
+ gr.Markdown("Tip: start with the **ZeroGPU Safe** preset for reliable generation on small GPUs.")
982
 
983
  with gr.Tabs():
984
  # Tab 1: Music Generation
 
1028
  t7 = gr.CheckboxGroup(choices=TAG_DATA["Topic"], label="Topic")
1029
  tag_checkboxes.append(t7)
1030
 
1031
+ gr.Markdown("### Quick Presets")
1032
+ with gr.Row():
1033
+ preset_selector = gr.Dropdown(
1034
+ choices=list(PRESET_CONFIGS.keys()),
1035
+ value=DEFAULT_PRESET,
1036
+ label="Preset"
1037
+ )
1038
+ apply_preset_btn = gr.Button("Apply Preset", size="sm")
1039
+
1040
  # Generation parameters
1041
  with gr.Row():
1042
  cfg_scale = gr.Slider(0.0, 3.0, value=1.5, step=0.1, label="CFG Scale")
1043
+ duration = gr.Slider(10, 300, value=DEFAULT_DURATION_SEC, step=10, label="Duration (sec)")
1044
 
1045
  with gr.Row():
1046
  temperature = gr.Slider(0.1, 2.0, value=1.0, step=0.1, label="Temperature")
 
1063
  label="Codec Version"
1064
  )
1065
  quant_mode = gr.Dropdown(
1066
+ choices=[
1067
+ ("None", "none"),
1068
+ ("4-bit (NF4/FP4)", "4bit"),
1069
+ ("8-bit", "8bit"),
1070
+ ("FP8 (TorchAO)", "fp8"),
1071
+ ],
1072
+ value=DEFAULT_QUANT_MODE,
1073
+ label="Quantization (ZeroGPU recommended: 4-bit)"
1074
  )
1075
  keep_model_loaded = gr.Checkbox(
1076
  value=DEFAULT_KEEP_MODEL_LOADED,
 
1078
  )
1079
  offload_mode = gr.Dropdown(
1080
  choices=["auto", "aggressive"],
1081
+ value=DEFAULT_OFFLOAD_MODE,
1082
  label="Offload Mode"
1083
  )
1084
  output_format = gr.Radio(
1085
  choices=[("WAV", "wav"), ("MP3", "mp3")],
1086
  value="wav",
1087
+ label="Output Format",
1088
+ visible=not show_stream_default,
1089
  )
1090
+ gr.Markdown(
1091
+ f"Streaming is block-based: {MODEL_DETOKENIZE_INTERVAL_SEC:.2f}s per block "
1092
+ f"({BLOCK_FRAMES} frames, {BLOCK_SAMPLES} samples)."
1093
  )
1094
 
1095
  gr.Markdown("### 🚀 Generation")
1096
 
1097
  generation_mode = gr.Radio(
1098
  choices=["Original (No Acceleration)", "Accelerated"],
1099
+ value=DEFAULT_GENERATION_MODE,
1100
  label="Generation Mode",
1101
  )
1102
 
1103
  speed_submode = gr.Radio(
1104
+ choices=[
1105
+ ("Standard", "Standard"),
1106
+ ("Block Streaming (Preview)", "Streaming"),
1107
+ ],
1108
+ value=DEFAULT_SPEED_SUBMODE,
1109
  label="Accelerated Options",
1110
+ visible=DEFAULT_GENERATION_MODE != "Original (No Acceleration)",
1111
  )
1112
 
1113
+ btn_original = gr.Button(
1114
+ "🎼 Generate Music (Original)",
1115
+ variant="primary",
1116
+ size="lg",
1117
+ visible=DEFAULT_GENERATION_MODE == "Original (No Acceleration)",
1118
+ )
1119
+ btn_accel = gr.Button(
1120
+ "🎼 Generate Music (Accelerated)",
1121
+ variant="primary",
1122
+ size="lg",
1123
+ visible=DEFAULT_GENERATION_MODE != "Original (No Acceleration)"
1124
+ and not show_stream_default,
1125
+ )
1126
+ btn_stream = gr.Button(
1127
+ "🎼 Generate Music (Block Streaming)",
1128
+ variant="primary",
1129
+ size="lg",
1130
+ visible=show_stream_default,
1131
+ )
1132
+ cancel_stream_btn = gr.Button(
1133
+ "Cancel Streaming",
1134
+ variant="secondary",
1135
+ size="lg",
1136
+ visible=show_stream_default,
1137
+ )
1138
  cancel_state = gr.State()
1139
 
1140
  with gr.Column():
1141
  # Notice section
1142
  with gr.Accordion("Usage Notice", open=True):
1143
  gr.Markdown("""
1144
+ ### ZeroGPU Tips
1145
+ - Prefer **4-bit quantization** and **aggressive offload**
1146
+ - Keep durations short (<= 60s) for faster first results
1147
+ - **Streaming is block-based**, not token streaming: each block is ~29.76s
1148
+ - Use Streaming for preview; Standard mode outputs a full downloadable file
1149
+ - Try **FP8 + AOTI** (preset: ZeroGPU AOTI FP8) for the fastest H200 path
1150
+
1151
  ### Lyrics Format Requirements
1152
 
1153
  **Automatic Processing:**
 
1187
  label="Generated Music (Full)",
1188
  type="filepath",
1189
  interactive=False,
1190
+ visible=not show_stream_default,
1191
  )
1192
  stream_audio = gr.Audio(
1193
+ label="Block Streaming Audio (Preview)",
1194
  streaming=True,
1195
  autoplay=True,
1196
  type="numpy",
1197
  format="wav",
1198
  interactive=False,
1199
+ visible=show_stream_default,
1200
  )
1201
 
1202
  # Event handlers for tag selection
 
1218
  gr.update(visible=False), # btn_accel
1219
  gr.update(visible=False), # btn_stream
1220
  gr.update(visible=False), # cancel_stream_btn
1221
+ gr.update(visible=True), # output_format
1222
+ gr.update(visible=True), # output_audio_file
1223
+ gr.update(visible=False), # stream_audio
1224
  )
1225
  show_stream = spd_mode == "Streaming"
1226
  return (
 
1229
  gr.update(visible=not show_stream), # btn_accel
1230
  gr.update(visible=show_stream), # btn_stream
1231
  gr.update(visible=show_stream), # cancel_stream_btn
1232
+ gr.update(visible=not show_stream), # output_format
1233
+ gr.update(visible=not show_stream), # output_audio_file
1234
+ gr.update(visible=show_stream), # stream_audio
1235
+ )
1236
+
1237
+ def apply_preset(preset_name):
1238
+ preset = PRESET_CONFIGS.get(preset_name, PRESET_CONFIGS[DEFAULT_PRESET])
1239
+ return (
1240
+ gr.update(value=preset["cfg_scale"]),
1241
+ gr.update(value=preset["duration"]),
1242
+ gr.update(value=preset["temperature"]),
1243
+ gr.update(value=preset["topk"]),
1244
+ gr.update(value=preset["quant_mode"]),
1245
+ gr.update(value=preset["keep_model_loaded"]),
1246
+ gr.update(value=preset["offload_mode"]),
1247
+ gr.update(value=preset["generation_mode"]),
1248
+ gr.update(value=preset["speed_submode"]),
1249
  )
1250
 
1251
  generation_mode.change(
1252
  fn=update_visibility,
1253
  inputs=[generation_mode, speed_submode],
1254
+ outputs=[
1255
+ speed_submode,
1256
+ btn_original,
1257
+ btn_accel,
1258
+ btn_stream,
1259
+ cancel_stream_btn,
1260
+ output_format,
1261
+ output_audio_file,
1262
+ stream_audio,
1263
+ ],
1264
  )
1265
  speed_submode.change(
1266
  fn=update_visibility,
1267
  inputs=[generation_mode, speed_submode],
1268
+ outputs=[
1269
+ speed_submode,
1270
+ btn_original,
1271
+ btn_accel,
1272
+ btn_stream,
1273
+ cancel_stream_btn,
1274
+ output_format,
1275
+ output_audio_file,
1276
+ stream_audio,
1277
+ ],
1278
+ )
1279
+
1280
+ preset_event = apply_preset_btn.click(
1281
+ fn=apply_preset,
1282
+ inputs=[preset_selector],
1283
+ outputs=[
1284
+ cfg_scale,
1285
+ duration,
1286
+ temperature,
1287
+ topk,
1288
+ quant_mode,
1289
+ keep_model_loaded,
1290
+ offload_mode,
1291
+ generation_mode,
1292
+ speed_submode,
1293
+ ],
1294
+ )
1295
+
1296
+ preset_event.then(
1297
+ fn=update_visibility,
1298
+ inputs=[generation_mode, speed_submode],
1299
+ outputs=[
1300
+ speed_submode,
1301
+ btn_original,
1302
+ btn_accel,
1303
+ btn_stream,
1304
+ cancel_stream_btn,
1305
+ output_format,
1306
+ output_audio_file,
1307
+ stream_audio,
1308
+ ],
1309
  )
1310
 
1311
  btn_original.click(
 
1367
  keep_model_loaded,
1368
  offload_mode,
1369
  backend,
 
 
1370
  ],
1371
  outputs=[stream_audio],
1372
  concurrency_id="gpu_queue",
 
1511
  outputs=[lyrics]
1512
  )
1513
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1514
  return demo
1515
 
 
 
1516
  demo.queue(max_size=GRADIO_QUEUE_MAX_SIZE, default_concurrency_limit=GRADIO_DEFAULT_CONCURRENCY)
1517
 
1518
  if __name__ == "__main__":
src/heartlib/pipelines/music_generation.py CHANGED
@@ -1,17 +1,30 @@
1
  import gc
 
2
  import json
3
  import os
4
  from dataclasses import dataclass
5
- from typing import Any, Dict, Optional, Iterator
6
 
7
  import torch
8
  import torchaudio
9
  from tokenizers import Tokenizer
10
  from transformers import BitsAndBytesConfig
11
 
12
- from ..heartmula.modeling_heartmula import HeartMuLa
13
  from ..heartcodec.modeling_heartcodec import HeartCodec
14
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
  @dataclass
17
  class HeartMuLaGenConfig:
@@ -44,6 +57,9 @@ class HeartMuLaGenPipeline:
44
  lazy_load: bool = False,
45
  use_deepspeed: bool = False,
46
  ds_inference_config: Optional[Dict[str, Any]] = None,
 
 
 
47
  ):
48
  self.model = model
49
  self.audio_codec = audio_codec
@@ -62,6 +78,17 @@ class HeartMuLaGenPipeline:
62
  self._ds_inference_applied = False
63
  self._parallel_number = num_quantizers + 1 if num_quantizers else 9
64
  self._muq_dim = model.config.muq_dim if model else None
 
 
 
 
 
 
 
 
 
 
 
65
 
66
  def _init_deepspeed_inference(self):
67
  if not self.use_deepspeed or self._ds_inference_applied:
@@ -92,6 +119,9 @@ class HeartMuLaGenPipeline:
92
  )
93
  if str(next(self.model.parameters()).device) != str(self.device):
94
  self.model.to(self.device)
 
 
 
95
  self.model.eval()
96
  self._init_deepspeed_inference()
97
  self._muq_dim = self.model.config.muq_dim
@@ -120,6 +150,157 @@ class HeartMuLaGenPipeline:
120
  return torch.autocast(device_type="cuda", dtype=self.dtype)
121
  return torch.inference_mode()
122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  def preprocess(self, inputs: Dict[str, Any], cfg_scale: float):
124
  self.load_heartmula()
125
  tags = inputs["tags"]
@@ -258,6 +439,7 @@ class HeartMuLaGenPipeline:
258
  offload_mode: str = "auto",
259
  ) -> Iterator[torch.Tensor]:
260
  model_inputs = self.preprocess(inputs, cfg_scale=cfg_scale)
 
261
  self.load_heartcodec()
262
 
263
  prompt_tokens = model_inputs["tokens"]
@@ -352,6 +534,7 @@ class HeartMuLaGenPipeline:
352
  offload_mode: str = "auto",
353
  ):
354
  model_inputs = self.preprocess(inputs, cfg_scale=cfg_scale)
 
355
  frames = self._generate_frames(model_inputs, max_audio_length_ms, temperature, topk, cfg_scale)
356
  wav = self._decode_frames(frames)
357
  torchaudio.save(save_path, wav, 48000)
@@ -369,6 +552,9 @@ class HeartMuLaGenPipeline:
369
  lazy_load: bool = False,
370
  use_deepspeed: bool = False,
371
  ds_inference_config: Optional[Dict[str, Any]] = None,
 
 
 
372
  ):
373
  if os.path.isfile(vocab_path := os.path.join(pretrained_path, "tokenizer.json")):
374
  tokenizer = Tokenizer.from_file(vocab_path)
@@ -415,4 +601,7 @@ class HeartMuLaGenPipeline:
415
  lazy_load=lazy_load,
416
  use_deepspeed=use_deepspeed,
417
  ds_inference_config=ds_inference_config,
 
 
 
418
  )
 
1
  import gc
2
+ import inspect
3
  import json
4
  import os
5
  from dataclasses import dataclass
6
+ from typing import Any, Dict, Optional, Iterator, Tuple
7
 
8
  import torch
9
  import torchaudio
10
  from tokenizers import Tokenizer
11
  from transformers import BitsAndBytesConfig
12
 
13
+ from ..heartmula.modeling_heartmula import HeartMuLa, _index_causal_mask
14
  from ..heartcodec.modeling_heartcodec import HeartCodec
15
 
16
+ try:
17
+ import spaces # type: ignore
18
+ except Exception: # pragma: no cover - optional dependency
19
+ spaces = None
20
+
21
+
22
+ def _env_bool(name: str) -> Optional[bool]:
23
+ value = os.environ.get(name)
24
+ if value is None:
25
+ return None
26
+ return value.strip().lower() in ("1", "true", "yes", "y", "on")
27
+
28
 
29
  @dataclass
30
  class HeartMuLaGenConfig:
 
57
  lazy_load: bool = False,
58
  use_deepspeed: bool = False,
59
  ds_inference_config: Optional[Dict[str, Any]] = None,
60
+ torchao_quantize: bool = False,
61
+ aoti_enabled: Optional[bool] = None,
62
+ aoti_dynamic_shapes: Optional[bool] = None,
63
  ):
64
  self.model = model
65
  self.audio_codec = audio_codec
 
78
  self._ds_inference_applied = False
79
  self._parallel_number = num_quantizers + 1 if num_quantizers else 9
80
  self._muq_dim = model.config.muq_dim if model else None
81
+ self._torchao_quantize = torchao_quantize
82
+ space_default = spaces is not None and os.environ.get("SPACE_ID") is not None
83
+ env_aoti = _env_bool("ENABLE_AOTI")
84
+ env_dynamic = _env_bool("AOTI_DYNAMIC_SHAPES")
85
+ self._aoti_enabled = env_aoti if aoti_enabled is None else aoti_enabled
86
+ if self._aoti_enabled is None:
87
+ self._aoti_enabled = space_default
88
+ self._aoti_dynamic_shapes = env_dynamic if aoti_dynamic_shapes is None else aoti_dynamic_shapes
89
+ if self._aoti_dynamic_shapes is None:
90
+ self._aoti_dynamic_shapes = space_default
91
+ self._aoti_compiled = False
92
 
93
  def _init_deepspeed_inference(self):
94
  if not self.use_deepspeed or self._ds_inference_applied:
 
119
  )
120
  if str(next(self.model.parameters()).device) != str(self.device):
121
  self.model.to(self.device)
122
+ if self._torchao_quantize and not self._aoti_enabled:
123
+ self._maybe_fp8_quantize(self.model.backbone)
124
+ self._maybe_fp8_quantize(self.model.decoder)
125
  self.model.eval()
126
  self._init_deepspeed_inference()
127
  self._muq_dim = self.model.config.muq_dim
 
150
  return torch.autocast(device_type="cuda", dtype=self.dtype)
151
  return torch.inference_mode()
152
 
153
+ def _supports_aoti(self) -> Tuple[bool, str]:
154
+ if not self._aoti_enabled:
155
+ return False, "AOTI disabled"
156
+ if spaces is None:
157
+ return False, "spaces package not available"
158
+ if not torch.cuda.is_available():
159
+ return False, "CUDA unavailable"
160
+ if not hasattr(torch, "export"):
161
+ return False, "torch.export unavailable"
162
+ if self.use_deepspeed:
163
+ return False, "DeepSpeed enabled"
164
+ if self.bnb_config is not None:
165
+ return False, "bitsandbytes quantization active"
166
+ return True, ""
167
+
168
+ def _maybe_fp8_quantize(self, module: torch.nn.Module) -> bool:
169
+ if not self._torchao_quantize:
170
+ return False
171
+ if not torch.cuda.is_available():
172
+ print("AOTI FP8: CUDA unavailable, skipping torchao quantization.")
173
+ return False
174
+ try:
175
+ major, _ = torch.cuda.get_device_capability()
176
+ except Exception:
177
+ major = 0
178
+ if major < 9:
179
+ print("AOTI FP8: requires CUDA compute capability >= 9.0, skipping.")
180
+ return False
181
+ try:
182
+ from torchao.quantization import quantize_, Float8DynamicActivationFloat8WeightConfig
183
+ except Exception:
184
+ print("AOTI FP8: torchao not available, skipping.")
185
+ return False
186
+ quantize_(module, Float8DynamicActivationFloat8WeightConfig())
187
+ return True
188
+
189
+ def _aoti_compile_module(
190
+ self,
191
+ module: torch.nn.Module,
192
+ example_kwargs: Dict[str, Any],
193
+ dynamic_shapes: Optional[Dict[str, Any]] = None,
194
+ ) -> None:
195
+ if spaces is None:
196
+ return
197
+ with spaces.aoti_capture(module) as call:
198
+ module(**example_kwargs)
199
+ exported = torch.export.export(
200
+ module,
201
+ args=call.args,
202
+ kwargs=call.kwargs,
203
+ dynamic_shapes=dynamic_shapes,
204
+ )
205
+ compiled = spaces.aoti_compile(exported)
206
+ spaces.aoti_apply(compiled, module)
207
+
208
+ def _maybe_apply_aoti(self, model_inputs: Dict[str, Any], cfg_scale: float) -> None:
209
+ if self._aoti_compiled:
210
+ return
211
+ ok, reason = self._supports_aoti()
212
+ if not ok:
213
+ print(f"AOTI: skip ({reason}).")
214
+ self._aoti_compiled = True
215
+ return
216
+
217
+ try:
218
+ self.load_heartmula()
219
+
220
+ prompt_tokens = model_inputs["tokens"]
221
+ prompt_tokens_mask = model_inputs["tokens_mask"]
222
+ continuous_segment = model_inputs["muq_embed"]
223
+ starts = model_inputs["muq_idx"]
224
+ prompt_pos = model_inputs["pos"]
225
+
226
+ bs_size = 2 if cfg_scale != 1.0 else 1
227
+ self.model.setup_caches(bs_size)
228
+
229
+ uncond_mask = None
230
+ if cfg_scale > 1.0 and prompt_tokens.shape[0] > 1:
231
+ actual_B = prompt_tokens.shape[0] // 2
232
+ uncond_mask = torch.cat(
233
+ [
234
+ torch.zeros(actual_B, dtype=torch.bool, device=prompt_tokens.device),
235
+ torch.ones(actual_B, dtype=torch.bool, device=prompt_tokens.device),
236
+ ]
237
+ )
238
+
239
+ embeds = self.model._embed_tokens(prompt_tokens, uncond_mask=uncond_mask)
240
+ masked_embeds = embeds * prompt_tokens_mask.unsqueeze(-1)
241
+ h = masked_embeds.sum(dim=2, dtype=embeds.dtype)
242
+ if continuous_segment is not None:
243
+ continuous_segment = self.model.muq_linear(continuous_segment)
244
+ if uncond_mask is not None:
245
+ uncond_embed = self.model.unconditional_text_embedding(
246
+ torch.zeros(1, device=prompt_tokens.device, dtype=torch.long)
247
+ )
248
+ mask_expanded = uncond_mask.view(h.shape[0], 1).expand_as(continuous_segment)
249
+ continuous_segment = torch.where(mask_expanded, uncond_embed, continuous_segment)
250
+ batch_indices = torch.arange(h.shape[0], device=h.device)
251
+ h[batch_indices, starts] = continuous_segment
252
+
253
+ curr_backbone_mask = _index_causal_mask(self.model.backbone_causal_mask, prompt_pos)
254
+
255
+ backbone_params = list(inspect.signature(self.model.backbone.forward).parameters.keys())
256
+ backbone_name = backbone_params[1] if len(backbone_params) > 1 else "x"
257
+ backbone_kwargs = {
258
+ backbone_name: h,
259
+ "input_pos": prompt_pos,
260
+ "mask": curr_backbone_mask,
261
+ }
262
+
263
+ if self._aoti_dynamic_shapes and hasattr(torch, "export"):
264
+ seq_dim = torch.export.Dim("seq", min=4, max=self.model.backbone.max_seq_len)
265
+ backbone_dynamic = {
266
+ backbone_name: {1: seq_dim},
267
+ "input_pos": {1: seq_dim},
268
+ "mask": {1: seq_dim},
269
+ }
270
+ else:
271
+ backbone_dynamic = None
272
+
273
+ self._maybe_fp8_quantize(self.model.backbone)
274
+ self._aoti_compile_module(self.model.backbone, backbone_kwargs, backbone_dynamic)
275
+
276
+ last_h = h[:, -1, :]
277
+ c0_sample = torch.zeros((h.shape[0], 1), device=h.device, dtype=torch.long)
278
+ c0_embed = self.model._embed_audio(0, c0_sample)
279
+ curr_h = torch.cat([last_h.unsqueeze(1), c0_embed], dim=1)
280
+ curr_pos = (
281
+ torch.arange(0, curr_h.size(1), device=curr_h.device)
282
+ .unsqueeze(0)
283
+ .repeat(curr_h.size(0), 1)
284
+ )
285
+ curr_decoder_mask = _index_causal_mask(self.model.decoder_causal_mask, curr_pos)
286
+
287
+ decoder_params = list(inspect.signature(self.model.decoder.forward).parameters.keys())
288
+ decoder_name = decoder_params[1] if len(decoder_params) > 1 else "x"
289
+ decoder_kwargs = {
290
+ decoder_name: self.model.projection(curr_h),
291
+ "input_pos": curr_pos,
292
+ "mask": curr_decoder_mask,
293
+ }
294
+ decoder_dynamic = None
295
+
296
+ self._maybe_fp8_quantize(self.model.decoder)
297
+ self._aoti_compile_module(self.model.decoder, decoder_kwargs, decoder_dynamic)
298
+ self._aoti_compiled = True
299
+ print("AOTI: compiled backbone and decoder successfully.")
300
+ except Exception as exc:
301
+ self._aoti_compiled = True
302
+ print(f"AOTI: compilation failed, falling back to eager. Error: {exc}")
303
+
304
  def preprocess(self, inputs: Dict[str, Any], cfg_scale: float):
305
  self.load_heartmula()
306
  tags = inputs["tags"]
 
439
  offload_mode: str = "auto",
440
  ) -> Iterator[torch.Tensor]:
441
  model_inputs = self.preprocess(inputs, cfg_scale=cfg_scale)
442
+ self._maybe_apply_aoti(model_inputs, cfg_scale)
443
  self.load_heartcodec()
444
 
445
  prompt_tokens = model_inputs["tokens"]
 
534
  offload_mode: str = "auto",
535
  ):
536
  model_inputs = self.preprocess(inputs, cfg_scale=cfg_scale)
537
+ self._maybe_apply_aoti(model_inputs, cfg_scale)
538
  frames = self._generate_frames(model_inputs, max_audio_length_ms, temperature, topk, cfg_scale)
539
  wav = self._decode_frames(frames)
540
  torchaudio.save(save_path, wav, 48000)
 
552
  lazy_load: bool = False,
553
  use_deepspeed: bool = False,
554
  ds_inference_config: Optional[Dict[str, Any]] = None,
555
+ torchao_quantize: bool = False,
556
+ aoti_enabled: Optional[bool] = None,
557
+ aoti_dynamic_shapes: Optional[bool] = None,
558
  ):
559
  if os.path.isfile(vocab_path := os.path.join(pretrained_path, "tokenizer.json")):
560
  tokenizer = Tokenizer.from_file(vocab_path)
 
601
  lazy_load=lazy_load,
602
  use_deepspeed=use_deepspeed,
603
  ds_inference_config=ds_inference_config,
604
+ torchao_quantize=torchao_quantize,
605
+ aoti_enabled=aoti_enabled,
606
+ aoti_dynamic_shapes=aoti_dynamic_shapes,
607
  )