8383SETTINGS_FILE = os .path .join (os .environ .get ("HEARTMULA_DB_PATH" , _default_db_dir ).replace ("jobs.db" , "" ), "settings.json" )
8484
8585
86+ def is_mps_available () -> bool :
87+ """
88+ Check if Apple Metal Performance Shaders (MPS) is available.
89+
90+ Returns:
91+ bool: True if MPS is available, False otherwise
92+ """
93+ return hasattr (torch .backends , 'mps' ) and torch .backends .mps .is_available ()
94+
95+
8696def detect_optimal_gpu_config () -> dict :
8797 """
8898 Auto-detect the optimal GPU configuration based on available VRAM.
@@ -94,6 +104,7 @@ def detect_optimal_gpu_config() -> dict:
94104 - gpu_info: dict - info about each GPU (name, vram, compute capability)
95105 - config_name: str - human-readable name of the selected configuration
96106 - warning: str or None - any warnings about the configuration
107+ - device_type: str - type of device to use ("cuda", "mps", or "cpu")
97108 """
98109 result = {
99110 "use_quantization" : True , # Default to quantization for safety
@@ -102,10 +113,38 @@ def detect_optimal_gpu_config() -> dict:
102113 "gpu_info" : {},
103114 "config_name" : "CPU Only" ,
104115 "warning" : None ,
116+ "device_type" : "cpu" ,
105117 }
106118
107- if not torch .cuda .is_available ():
108- result ["warning" ] = "No CUDA GPU detected. Running on CPU will be very slow."
119+ # Check for CUDA GPUs first
120+ if torch .cuda .is_available ():
121+ result ["device_type" ] = "cuda"
122+ # Continue with existing CUDA logic below
123+ # Check for Apple Metal (MPS) on macOS
124+ elif is_mps_available ():
125+ result ["device_type" ] = "mps"
126+ result ["num_gpus" ] = 1
127+ result ["use_quantization" ] = False # MPS works better with full precision
128+ result ["use_sequential_offload" ] = False # Unified memory architecture
129+ result ["config_name" ] = "Apple Metal (MPS)"
130+ result ["gpu_info" ] = {
131+ 0 : {
132+ "name" : "Apple Metal Performance Shaders" ,
133+ "vram_gb" : "Unified Memory" ,
134+ "compute_capability" : "MPS" ,
135+ "supports_flash_attention" : False ,
136+ }
137+ }
138+ print (f"\n [Auto-Config] Using Apple Metal (MPS) device" , flush = True )
139+ print (f"[Auto-Config] MPS uses unified memory - no VRAM limits" , flush = True )
140+ return result
141+ # No GPU available - fall back to CPU
142+ else :
143+ result ["warning" ] = "No CUDA GPU or Metal GPU detected. Running on CPU will be very slow."
144+ return result
145+
146+ # Continue with CUDA-specific logic if CUDA is available
147+ if result ["device_type" ] != "cuda" :
109148 return result
110149
111150 num_gpus = torch .cuda .device_count ()
@@ -433,6 +472,11 @@ def configure_flash_attention_for_gpu(device_id: int):
433472 - NVIDIA SM 6.x and older: Disables Flash Attention, uses math backend
434473 - AMD ROCm: Conservatively disables Flash Attention (compatibility varies)
435474 """
475+ # Skip if MPS is being used
476+ if is_mps_available () and not torch .cuda .is_available ():
477+ logger .info ("[GPU Config] Apple Metal (MPS) device - skipping Flash Attention configuration" )
478+ return
479+
436480 if not torch .cuda .is_available ():
437481 logger .info ("[GPU Config] CUDA not available - skipping Flash Attention configuration" )
438482 return
@@ -662,9 +706,12 @@ def _pad_audio_token(token):
662706 pipeline .mula .reset_caches ()
663707 pipeline ._mula .to ("cpu" )
664708 gc .collect ()
665- torch .cuda .empty_cache ()
666- torch .cuda .synchronize ()
667- print (f"[Sequential Offload] VRAM after offload: { torch .cuda .memory_allocated ()/ 1024 ** 3 :.2f} GB" , flush = True )
709+ if torch .cuda .is_available ():
710+ torch .cuda .empty_cache ()
711+ torch .cuda .synchronize ()
712+ print (f"[Sequential Offload] VRAM after offload: { torch .cuda .memory_allocated ()/ 1024 ** 3 :.2f} GB" , flush = True )
713+ elif is_mps_available ():
714+ torch .mps .empty_cache ()
668715 else :
669716 pipeline ._unload ()
670717
@@ -683,7 +730,8 @@ def _pad_audio_token(token):
683730 device_map = pipeline .codec_device ,
684731 dtype = torch .float32 ,
685732 )
686- print (f"[Lazy Loading] HeartCodec loaded. VRAM: { torch .cuda .memory_allocated ()/ 1024 ** 3 :.2f} GB" , flush = True )
733+ if torch .cuda .is_available ():
734+ print (f"[Lazy Loading] HeartCodec loaded. VRAM: { torch .cuda .memory_allocated ()/ 1024 ** 3 :.2f} GB" , flush = True )
687735 else :
688736 raise RuntimeError ("Cannot load HeartCodec: codec_path not available" )
689737
@@ -696,8 +744,11 @@ def _pad_audio_token(token):
696744 del pipeline ._codec
697745 pipeline ._codec = None
698746 gc .collect ()
699- torch .cuda .empty_cache ()
700- torch .cuda .synchronize ()
747+ if torch .cuda .is_available ():
748+ torch .cuda .empty_cache ()
749+ torch .cuda .synchronize ()
750+ elif is_mps_available ():
751+ torch .mps .empty_cache ()
701752
702753 if pipeline ._sequential_offload :
703754 # Move HeartMuLa back to GPU for next generation
@@ -726,6 +777,9 @@ def cleanup_gpu_memory():
726777 torch .cuda .empty_cache ()
727778 torch .cuda .synchronize ()
728779 logger .info ("GPU memory cleaned up" )
780+ elif is_mps_available ():
781+ torch .mps .empty_cache ()
782+ logger .info ("MPS memory cleaned up" )
729783
730784
731785def get_gpu_memory (device_id ):
@@ -1036,18 +1090,34 @@ def _unload_all_models(self):
10361090 with torch .cuda .device (i ):
10371091 torch .cuda .empty_cache ()
10381092 torch .cuda .synchronize ()
1093+ elif is_mps_available ():
1094+ # MPS memory cleanup
1095+ torch .mps .empty_cache ()
10391096
10401097 logger .info ("All models unloaded" )
10411098
10421099 def get_gpu_info (self ) -> dict :
10431100 """Get GPU hardware information."""
10441101 result = {
10451102 "cuda_available" : torch .cuda .is_available (),
1103+ "mps_available" : is_mps_available (),
10461104 "num_gpus" : 0 ,
10471105 "gpus" : [],
10481106 "total_vram_gb" : 0
10491107 }
10501108
1109+ # Check for MPS (Apple Metal) first
1110+ if result ["mps_available" ] and not result ["cuda_available" ]:
1111+ result ["num_gpus" ] = 1
1112+ result ["gpus" ].append ({
1113+ "index" : 0 ,
1114+ "name" : "Apple Metal Performance Shaders" ,
1115+ "vram_gb" : "Unified Memory" ,
1116+ "compute_capability" : "MPS" ,
1117+ "supports_flash_attention" : False
1118+ })
1119+ return result
1120+
10511121 if not torch .cuda .is_available ():
10521122 return result
10531123
@@ -1157,8 +1227,58 @@ def _load_pipeline_multi_gpu(self, model_path: str, version: str):
11571227 # Store the detected config for reference
11581228 self .gpu_config = auto_config
11591229
1230+ device_type = auto_config .get ("device_type" , "cpu" )
11601231 num_gpus = auto_config ["num_gpus" ]
11611232
1233+ # Handle Apple Metal (MPS) devices
1234+ if device_type == "mps" :
1235+ logger .info ("Using Apple Metal (MPS) for GPU acceleration" )
1236+ self .gpu_mode = "single"
1237+ print ("[Apple Metal] Using MPS device for inference" , flush = True )
1238+ print ("[Apple Metal] Note: MPS uses unified memory architecture" , flush = True )
1239+
1240+ # Check if quantization is manually enabled
1241+ if use_quantization :
1242+ logger .warning ("4-bit quantization is not supported on MPS. Using full precision instead." )
1243+ print ("[Apple Metal] WARNING: 4-bit quantization not supported on MPS, using full precision" , flush = True )
1244+
1245+ # MPS doesn't support bfloat16, use float32 instead
1246+ pipeline = HeartMuLaGenPipeline .from_pretrained (
1247+ model_path ,
1248+ device = {
1249+ "mula" : torch .device ("mps" ),
1250+ "codec" : torch .device ("mps" ),
1251+ },
1252+ dtype = {
1253+ "mula" : torch .float32 ,
1254+ "codec" : torch .float32 ,
1255+ },
1256+ version = version ,
1257+ )
1258+ return patch_pipeline_with_callback (pipeline , sequential_offload = False )
1259+
1260+ # Handle CPU-only mode (no CUDA or MPS available)
1261+ if device_type == "cpu" :
1262+ logger .warning ("No GPU detected - running on CPU will be very slow" )
1263+ self .gpu_mode = "cpu"
1264+ print ("[CPU Mode] No GPU detected, using CPU for inference" , flush = True )
1265+ print ("[CPU Mode] WARNING: This will be extremely slow. Consider using a system with GPU support." , flush = True )
1266+
1267+ pipeline = HeartMuLaGenPipeline .from_pretrained (
1268+ model_path ,
1269+ device = {
1270+ "mula" : torch .device ("cpu" ),
1271+ "codec" : torch .device ("cpu" ),
1272+ },
1273+ dtype = {
1274+ "mula" : torch .float32 ,
1275+ "codec" : torch .float32 ,
1276+ },
1277+ version = version ,
1278+ )
1279+ return patch_pipeline_with_callback (pipeline , sequential_offload = False )
1280+
1281+ # At this point, device_type must be "cuda"
11621282 if use_quantization :
11631283 print (f"[Quantization] 4-bit quantization ENABLED - model will use ~3GB instead of ~11GB" , flush = True )
11641284 else :
@@ -1640,6 +1760,8 @@ async def update_compile_progress():
16401760 if torch .cuda .is_available ():
16411761 torch .cuda .empty_cache ()
16421762 torch .cuda .synchronize ()
1763+ elif is_mps_available ():
1764+ torch .mps .empty_cache ()
16431765 logger .info ("GPU memory cleaned up after generation" )
16441766 except Exception as cleanup_err :
16451767 logger .warning (f"Memory cleanup warning: { cleanup_err } " )
0 commit comments