Skip to content

Commit 43d9b7d

Browse files
authored
Merge pull request #8 from audiohacking/copilot/fix-torch-cuda-issue
Add Apple Metal (MPS) GPU support for macOS
2 parents d34abe8 + 576a90b commit 43d9b7d

1 file changed

Lines changed: 130 additions & 8 deletions

File tree

backend/app/services/music_service.py

Lines changed: 130 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,16 @@
8383
SETTINGS_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+
8696
def 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

731785
def 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

Comments
 (0)