359 lines
12 KiB
Python
359 lines
12 KiB
Python
"""BC-250 Full GPU Fix: Verify ROCm, diagnose VRAM, start ComfyUI on GPU, generate image."""
|
|
import paramiko
|
|
import time
|
|
import json
|
|
import sys
|
|
|
|
SSH_HOST = '192.168.178.150'
|
|
SSH_USER = 'fabian'
|
|
SSH_KEY = r'C:\Users\fabia\.ssh\id_ed25519'
|
|
|
|
def ssh_connect():
|
|
k = paramiko.Ed25519Key.from_private_key_file(SSH_KEY)
|
|
c = paramiko.SSHClient()
|
|
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
c.connect(SSH_HOST, username=SSH_USER, pkey=k, timeout=15)
|
|
return c
|
|
|
|
def run(c, cmd, timeout=30):
|
|
"""Run command via fish shell, return stdout."""
|
|
wrapped = f'bash -c {repr(cmd)}'
|
|
_, o, e = c.exec_command(wrapped, timeout=timeout)
|
|
return o.read().decode(errors='replace').strip()
|
|
|
|
def run_full(c, cmd, timeout=30):
|
|
"""Run command, return (stdout, stderr)."""
|
|
wrapped = f'bash -c {repr(cmd)}'
|
|
_, o, e = c.exec_command(wrapped, timeout=timeout)
|
|
return o.read().decode(errors='replace').strip(), e.read().decode(errors='replace').strip()
|
|
|
|
# ============================================================
|
|
# PHASE 1: Kill any remnants
|
|
# ============================================================
|
|
print("="*60)
|
|
print("PHASE 1: Clean slate")
|
|
print("="*60)
|
|
c = ssh_connect()
|
|
run(c, 'pkill -f "python.*main.py" 2>/dev/null; pkill -f comfyui 2>/dev/null')
|
|
time.sleep(2)
|
|
leftover = run(c, 'pgrep -af "python.*main.py" 2>/dev/null')
|
|
if leftover:
|
|
print(f"WARNING: Still running: {leftover}")
|
|
run(c, 'pkill -9 -f "python.*main.py" 2>/dev/null')
|
|
time.sleep(1)
|
|
print("ComfyUI killed. Clean slate.")
|
|
|
|
# ============================================================
|
|
# PHASE 2: Verify ROCm + PyTorch GPU
|
|
# ============================================================
|
|
print("\n" + "="*60)
|
|
print("PHASE 2: Verify ROCm + PyTorch GPU access")
|
|
print("="*60)
|
|
|
|
# Set GPU env vars for ALL subsequent commands
|
|
GPU_ENV = (
|
|
'export HSA_OVERRIDE_GFX_VERSION=10.1.0; '
|
|
'export HIP_VISIBLE_DEVICES=0; '
|
|
'export HSA_ENABLE_SDMA=0; '
|
|
'export HSA_TOOLS_LIB=""; '
|
|
'export HSA_TOOLS_REPORT_LOAD_FAILURE=0; '
|
|
'export PYTORCH_HIP_ALLOC_CONF=expandable_segments:False; '
|
|
)
|
|
|
|
# Check rocminfo
|
|
out = run(c, f'{GPU_ENV} rocminfo 2>&1 | grep -E "Name:|Marketing Name:|gfx" | head -10')
|
|
print(f"ROCm devices:\n{out}")
|
|
|
|
# Check PyTorch GPU
|
|
gpu_test = f'''{GPU_ENV} cd ~/ComfyUI && source ~/comfyui-env/bin/activate.fish 2>/dev/null; . ~/comfyui-env/bin/activate 2>/dev/null; python3 -c "
|
|
import torch
|
|
print(f'PyTorch: {{torch.__version__}}')
|
|
print(f'CUDA available: {{torch.cuda.is_available()}}')
|
|
print(f'Device count: {{torch.cuda.device_count()}}')
|
|
if torch.cuda.is_available():
|
|
print(f'Device name: {{torch.cuda.get_device_name(0)}}')
|
|
free, total = torch.cuda.mem_get_info(0)
|
|
print(f'VRAM: {{free//1024//1024}}MB free / {{total//1024//1024}}MB total')
|
|
# Quick GPU compute test
|
|
x = torch.randn(1024, 1024, device='cuda', dtype=torch.float16)
|
|
y = torch.mm(x, x)
|
|
print(f'GPU compute test: OK (result sum={{y.sum().item():.1f}})')
|
|
del x, y
|
|
torch.cuda.empty_cache()
|
|
else:
|
|
print('ERROR: GPU NOT AVAILABLE')
|
|
import sys; sys.exit(1)
|
|
"'''
|
|
out, err = run_full(c, gpu_test, timeout=60)
|
|
print(out)
|
|
if err:
|
|
print(f"STDERR: {err}")
|
|
if 'ERROR: GPU NOT AVAILABLE' in out or 'CUDA available: False' in out:
|
|
print("\n*** FATAL: PyTorch cannot see the GPU! ***")
|
|
c.close()
|
|
sys.exit(1)
|
|
print("\nGPU verified OK!")
|
|
|
|
# ============================================================
|
|
# PHASE 3: Start ComfyUI with correct GPU flags
|
|
# ============================================================
|
|
print("\n" + "="*60)
|
|
print("PHASE 3: Start ComfyUI with GPU")
|
|
print("="*60)
|
|
|
|
# The key insight: --novram was offloading EVERYTHING to CPU (0 MB on GPU)
|
|
# For this APU with shared memory, --lowvram is better:
|
|
# it keeps compute on GPU but swaps model layers in/out
|
|
# We also use --force-fp16 to reduce memory pressure
|
|
# --cpu-vae to avoid the known VAE decode hang on this GPU
|
|
|
|
COMFYUI_CMD = (
|
|
f'{GPU_ENV} '
|
|
'export OMP_NUM_THREADS=12; '
|
|
'export MKL_NUM_THREADS=12; '
|
|
'export OPENBLAS_NUM_THREADS=12; '
|
|
'export MIOPEN_FIND_MODE=1; ' # Fast MIOpen kernel search
|
|
'cd ~/ComfyUI && '
|
|
'source ~/comfyui-env/bin/activate 2>/dev/null; . ~/comfyui-env/bin/activate 2>/dev/null; '
|
|
'nohup python3 main.py '
|
|
'--listen 0.0.0.0 --port 8188 '
|
|
'--lowvram '
|
|
'--force-fp16 '
|
|
'--cpu-vae '
|
|
'--disable-smart-memory '
|
|
'> /tmp/comfyui.log 2>&1 &'
|
|
)
|
|
|
|
# Truncate old log first
|
|
run(c, 'truncate -s 0 /tmp/comfyui.log 2>/dev/null; touch /tmp/comfyui.log')
|
|
print("Starting ComfyUI with: --lowvram --force-fp16 --cpu-vae --disable-smart-memory")
|
|
print("(--lowvram keeps compute on GPU, swaps layers; --novram was wrong - it put everything on CPU)")
|
|
run(c, COMFYUI_CMD)
|
|
time.sleep(3)
|
|
|
|
# Verify it started
|
|
pid = run(c, 'pgrep -f "python.*main.py" 2>/dev/null')
|
|
if not pid:
|
|
print("ERROR: ComfyUI failed to start!")
|
|
log = run(c, 'cat /tmp/comfyui.log')
|
|
print(f"Log:\n{log}")
|
|
c.close()
|
|
sys.exit(1)
|
|
print(f"ComfyUI started, PID: {pid}")
|
|
|
|
# Wait for server ready
|
|
print("Waiting for server ready...")
|
|
for i in range(60):
|
|
try:
|
|
resp = run(c, 'curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8188/ 2>/dev/null', timeout=5)
|
|
if resp == '200':
|
|
print(f"Server ready after {i*3}s!")
|
|
break
|
|
except:
|
|
pass
|
|
# Also check for crash
|
|
log_tail = run(c, 'tail -3 /tmp/comfyui.log 2>/dev/null')
|
|
if 'Traceback' in log_tail or 'Error' in log_tail:
|
|
print(f"Server log issue: {log_tail}")
|
|
if i % 5 == 0 and i > 0:
|
|
print(f" [{i*3}s] Still waiting... log: {log_tail[-80:]}")
|
|
time.sleep(3)
|
|
else:
|
|
print("TIMEOUT waiting for ComfyUI!")
|
|
log = run(c, 'tail -30 /tmp/comfyui.log')
|
|
print(f"Log:\n{log}")
|
|
c.close()
|
|
sys.exit(1)
|
|
|
|
# Print startup log to confirm flags
|
|
log = run(c, 'head -20 /tmp/comfyui.log')
|
|
print(f"\nStartup log:\n{log}")
|
|
|
|
# ============================================================
|
|
# PHASE 4: Submit workflow via SFTP
|
|
# ============================================================
|
|
print("\n" + "="*60)
|
|
print("PHASE 4: Submit workflow")
|
|
print("="*60)
|
|
|
|
workflow = {
|
|
"prompt": {
|
|
"1": {
|
|
"class_type": "UnetLoaderGGUF",
|
|
"inputs": {
|
|
"unet_name": "z_image_turbo-Q5_K_S.gguf"
|
|
}
|
|
},
|
|
"2": {
|
|
"class_type": "CLIPLoaderGGUF",
|
|
"inputs": {
|
|
"clip_name": "Qwen3-4B.i1-Q5_K_S.gguf",
|
|
"type": "qwen_image"
|
|
}
|
|
},
|
|
"3": {
|
|
"class_type": "VAELoader",
|
|
"inputs": {
|
|
"vae_name": "ae.safetensors"
|
|
}
|
|
},
|
|
"4": {
|
|
"class_type": "CLIPTextEncode",
|
|
"inputs": {
|
|
"text": "A red fox in a snowy forest, photorealistic, highly detailed",
|
|
"clip": ["2", 0]
|
|
}
|
|
},
|
|
"5": {
|
|
"class_type": "EmptyLatentImage",
|
|
"inputs": {
|
|
"width": 512,
|
|
"height": 512,
|
|
"batch_size": 1
|
|
}
|
|
},
|
|
"6": {
|
|
"class_type": "KSampler",
|
|
"inputs": {
|
|
"model": ["1", 0],
|
|
"positive": ["4", 0],
|
|
"negative": ["4", 0],
|
|
"latent_image": ["5", 0],
|
|
"seed": 42,
|
|
"steps": 8,
|
|
"cfg": 1.0,
|
|
"sampler_name": "euler",
|
|
"scheduler": "simple",
|
|
"denoise": 1.0
|
|
}
|
|
},
|
|
"7": {
|
|
"class_type": "VAEDecode",
|
|
"inputs": {
|
|
"samples": ["6", 0],
|
|
"vae": ["3", 0]
|
|
}
|
|
},
|
|
"8": {
|
|
"class_type": "SaveImage",
|
|
"inputs": {
|
|
"images": ["7", 0],
|
|
"filename_prefix": "ZImageTurbo_GPU"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# Write via SFTP
|
|
sftp = c.open_sftp()
|
|
wf_json = json.dumps(workflow)
|
|
with sftp.open('/tmp/wf.json', 'w') as f:
|
|
f.write(wf_json)
|
|
sftp.close()
|
|
print("Workflow written to /tmp/wf.json via SFTP")
|
|
|
|
# Verify JSON
|
|
verify = run(c, 'python3 -c "import json; d=json.load(open(\'/tmp/wf.json\')); print(f\'Nodes: {list(d[chr(34)+chr(34) if False else \"prompt\"].keys())}\')"')
|
|
print(f"Verify: {verify}")
|
|
|
|
# Submit
|
|
resp = run(c, 'curl -s -X POST http://127.0.0.1:8188/prompt -H "Content-Type: application/json" -d @/tmp/wf.json 2>/dev/null')
|
|
print(f"Submit response: {resp}")
|
|
|
|
if 'error' in resp.lower() and 'prompt_id' not in resp.lower():
|
|
print(f"\n*** SUBMISSION ERROR ***")
|
|
# Check what went wrong
|
|
log = run(c, 'tail -10 /tmp/comfyui.log')
|
|
print(f"Log: {log}")
|
|
c.close()
|
|
sys.exit(1)
|
|
|
|
try:
|
|
resp_data = json.loads(resp)
|
|
prompt_id = resp_data.get('prompt_id', 'unknown')
|
|
print(f"Prompt ID: {prompt_id}")
|
|
except:
|
|
print("Could not parse response, continuing anyway...")
|
|
|
|
# ============================================================
|
|
# PHASE 5: Monitor generation with GPU tracking
|
|
# ============================================================
|
|
print("\n" + "="*60)
|
|
print("PHASE 5: Monitor generation (GPU must be active!)")
|
|
print("="*60)
|
|
|
|
start_time = time.time()
|
|
last_log_len = 0
|
|
|
|
for i in range(120): # Up to 30 minutes
|
|
elapsed = int(time.time() - start_time)
|
|
|
|
# GPU metrics
|
|
gpu_pct = run(c, 'cat /sys/class/drm/card0/device/gpu_busy_percent 2>/dev/null')
|
|
gpu_temp = run(c, 'cat /sys/class/drm/card0/device/hwmon/hwmon*/temp1_input 2>/dev/null')
|
|
temp_c = int(gpu_temp) // 1000 if gpu_temp.isdigit() else '?'
|
|
|
|
# GPU power
|
|
gpu_power = run(c, f'{GPU_ENV} rocm-smi -P 2>&1 | grep "Graphics Package" | grep -oP "[\\d.]+" | head -1')
|
|
|
|
# Process info
|
|
proc = run(c, 'ps -p $(pgrep -f "python.*main.py" | head -1) -o %cpu,%mem,rss --no-headers 2>/dev/null')
|
|
|
|
# Log tail
|
|
log = run(c, 'tail -5 /tmp/comfyui.log 2>/dev/null')
|
|
last_line = log.split('\n')[-1] if log else ''
|
|
|
|
# Output files
|
|
files = run(c, 'ls ~/ComfyUI/output/*.png 2>/dev/null')
|
|
|
|
# Queue
|
|
queue = run(c, 'curl -s http://127.0.0.1:8188/queue 2>/dev/null')
|
|
|
|
status = f"[{elapsed:>4}s] GPU:{gpu_pct:>3}% {temp_c}C {gpu_power}W | proc:{proc} | {last_line[-100:]}"
|
|
print(status)
|
|
|
|
# SUCCESS: Image generated!
|
|
if files:
|
|
print(f"\n{'='*60}")
|
|
print(f"*** SUCCESS! IMAGE GENERATED! ***")
|
|
print(f"Files: {files}")
|
|
print(f"Total time: {elapsed}s")
|
|
print(f"{'='*60}")
|
|
|
|
# Print final log
|
|
final_log = run(c, 'tail -20 /tmp/comfyui.log 2>/dev/null')
|
|
print(f"\nFinal log:\n{final_log}")
|
|
break
|
|
|
|
# Check if queue is empty (job done or failed)
|
|
try:
|
|
qdata = json.loads(queue)
|
|
running = len(qdata.get('queue_running', []))
|
|
pending = len(qdata.get('queue_pending', []))
|
|
if running == 0 and pending == 0 and elapsed > 30:
|
|
print(f"\nQueue empty after {elapsed}s. Checking if image was saved...")
|
|
time.sleep(2)
|
|
files = run(c, 'ls ~/ComfyUI/output/*.png 2>/dev/null')
|
|
if files:
|
|
print(f"*** SUCCESS! {files}")
|
|
else:
|
|
print("No image. Checking log for errors:")
|
|
err_log = run(c, 'tail -30 /tmp/comfyui.log 2>/dev/null')
|
|
print(err_log)
|
|
break
|
|
except:
|
|
pass
|
|
|
|
# Check for process death
|
|
alive = run(c, 'pgrep -f "python.*main.py" 2>/dev/null')
|
|
if not alive:
|
|
print("\n*** ComfyUI process died! ***")
|
|
crash_log = run(c, 'tail -40 /tmp/comfyui.log 2>/dev/null')
|
|
print(f"Crash log:\n{crash_log}")
|
|
break
|
|
|
|
time.sleep(15)
|
|
|
|
c.close()
|
|
print("\nDone.")
|