From 9059cca2c5b8d0d1a005db323f15198c6511bb3c Mon Sep 17 00:00:00 2001 From: iDAPPA Date: Tue, 11 Mar 2025 08:01:46 -0400 Subject: [PATCH] Add files via upload --- __init__.py | 254 +++++++++++++++++++++++++++++++ screenshot.png | Bin 0 -> 15459 bytes web/index.js | 1 + web/monitor.js | 403 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 658 insertions(+) create mode 100644 __init__.py create mode 100644 screenshot.png create mode 100644 web/index.js create mode 100644 web/monitor.js diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..9ffd20c --- /dev/null +++ b/__init__.py @@ -0,0 +1,254 @@ +import os +import sys +import json +import time +import subprocess +import threading +import asyncio +from server import PromptServer + +# Global variable to store GPU stats +gpu_stats = { + "gpu_utilization": 0, + "vram_used": 0, + "vram_total": 0, + "vram_used_percent": 0, + "gpu_temperature": 0, + "last_update": 0 +} + +# Monitor thread control +monitor_thread = None +thread_control = threading.Event() +monitor_update_interval = 1 # seconds + +def find_rocm_smi(): + """Find the rocm-smi or amd-smi executable""" + rocm_paths = [ + "/opt/rocm/bin/rocm-smi", + "/usr/bin/rocm-smi", + "/usr/local/bin/rocm-smi", + "/opt/amdgpu-pro/bin/amd-smi", + "/usr/bin/amd-smi", + ] + + for path in rocm_paths: + if os.path.isfile(path) and os.access(path, os.X_OK): + return path + + # Try to find it in PATH + try: + result = subprocess.run(["which", "rocm-smi"], capture_output=True, text=True) + if result.returncode == 0: + return result.stdout.strip() + except: + pass + + try: + result = subprocess.run(["which", "amd-smi"], capture_output=True, text=True) + if result.returncode == 0: + return result.stdout.strip() + except: + pass + + return None + +def run_rocm_smi_command(rocm_smi_path, *args): + """Run a rocm-smi command and return the parsed JSON output""" + if not rocm_smi_path: + return {} + + cmd = [rocm_smi_path] + list(args) + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=5) + if result.returncode != 0: + return {} + + # Check if output is JSON + if '--json' in args: + try: + return json.loads(result.stdout) + except json.JSONDecodeError: + return {} + else: + return result.stdout + except subprocess.TimeoutExpired: + return {} + except Exception as e: + return {} + +def get_gpu_info(rocm_smi_path): + """Get current GPU information""" + global gpu_stats + + # Get GPU utilization + try: + info = run_rocm_smi_command(rocm_smi_path, '--showuse', '--json') + if isinstance(info, dict) and 'card0' in info: + card_info = info['card0'] # Use first GPU + if 'GPU use (%)' in card_info: + gpu_use = card_info['GPU use (%)'] + if isinstance(gpu_use, str): + gpu_use = gpu_use.replace('%', '') + gpu_stats["gpu_utilization"] = int(float(gpu_use)) + except: + pass + + # Get VRAM information + try: + info = run_rocm_smi_command(rocm_smi_path, '--showmeminfo', 'vram', '--json') + if isinstance(info, dict) and 'card0' in info: + card_info = info['card0'] # Use first GPU + + # Parse the B (bytes) format ROCm 5.x/6.x uses + if 'VRAM Total Memory (B)' in card_info and 'VRAM Total Used Memory (B)' in card_info: + vram_total_bytes = int(card_info['VRAM Total Memory (B)']) + vram_used_bytes = int(card_info['VRAM Total Used Memory (B)']) + + # Convert to MB for display + vram_total = vram_total_bytes / (1024 * 1024) + vram_used = vram_used_bytes / (1024 * 1024) + + gpu_stats["vram_total"] = int(vram_total) + gpu_stats["vram_used"] = int(vram_used) + gpu_stats["vram_used_percent"] = int((vram_used / vram_total) * 100) + except: + pass + + # Get temperature + try: + info = run_rocm_smi_command(rocm_smi_path, '--showtemp', '--json') + if isinstance(info, dict) and 'card0' in info: + card_info = info['card0'] # Use first GPU + + # Try different temperature sensors, starting with edge + if 'Temperature (Sensor edge) (C)' in card_info: + temp_str = card_info['Temperature (Sensor edge) (C)'] + if isinstance(temp_str, str): + temp_str = temp_str.replace('°C', '').strip() + gpu_stats["gpu_temperature"] = int(float(temp_str)) + elif 'Temperature (Sensor junction) (C)' in card_info: + temp_str = card_info['Temperature (Sensor junction) (C)'] + if isinstance(temp_str, str): + temp_str = temp_str.replace('°C', '').strip() + gpu_stats["gpu_temperature"] = int(float(temp_str)) + except: + pass + + gpu_stats["last_update"] = time.time() + return gpu_stats + +def send_monitor_update(): + """Format and send monitor update data""" + data = { + 'device_type': 'rocm', + 'gpus': [{ + 'gpu_utilization': gpu_stats['gpu_utilization'], + 'gpu_temperature': gpu_stats['gpu_temperature'], + 'vram_total': gpu_stats['vram_total'], + 'vram_used': gpu_stats['vram_used'], + 'vram_used_percent': gpu_stats['vram_used_percent'] + }] + } + + # Send the data + try: + PromptServer.instance.send_sync('amd_gpu_monitor', data) + except: + pass + +def monitor_thread_function(): + """Thread function to continuously monitor GPU stats""" + global monitor_update_interval + + rocm_smi_path = find_rocm_smi() + if not rocm_smi_path: + print("ERROR: Could not find rocm-smi or amd-smi executable") + return + + print(f"Using AMD SMI tool: {rocm_smi_path}") + + while not thread_control.is_set(): + try: + get_gpu_info(rocm_smi_path) + # Send update to UI + send_monitor_update() + except: + pass + + # Sleep for the update interval + time.sleep(monitor_update_interval) + +def start_monitor_thread(): + """Start the GPU monitoring thread""" + global monitor_thread, thread_control + + if monitor_thread is not None and monitor_thread.is_alive(): + # Already running + return + + # Clear the control event and start new thread + thread_control.clear() + monitor_thread = threading.Thread(target=monitor_thread_function) + monitor_thread.daemon = True + monitor_thread.start() + print("AMD GPU Monitor thread started") + +def stop_monitor_thread(): + """Stop the GPU monitoring thread""" + global monitor_thread, thread_control + + if monitor_thread is None or not monitor_thread.is_alive(): + # Not running + return + + # Set control event to stop thread + thread_control.set() + monitor_thread.join(timeout=5) + print("AMD GPU Monitor thread stopped") + +# Start the monitor thread when this module is loaded +start_monitor_thread() + +# Define our nodes (not really used, but required for ComfyUI to load the extension) +class AMDGPUMonitor: + """ + A placeholder node for ComfyUI. The actual monitoring is done via a separate thread. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "update_interval": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 10.0, "step": 0.1}), + }, + } + + RETURN_TYPES = ("STRING",) + FUNCTION = "monitor_gpu" + CATEGORY = "AMD GPU" + + def monitor_gpu(self, update_interval): + """Update interval can be changed via input""" + global monitor_update_interval + monitor_update_interval = update_interval + + # Return current stats as a string for debugging + stats = f"GPU: {gpu_stats['gpu_utilization']}% | VRAM: {gpu_stats['vram_used']}MB/{gpu_stats['vram_total']}MB ({gpu_stats['vram_used_percent']}%) | Temp: {gpu_stats['gpu_temperature']}°C" + return (stats,) + +# Register our node when this script is imported +NODE_CLASS_MAPPINGS = { + "AMDGPUMonitor": AMDGPUMonitor, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "AMDGPUMonitor": "AMD GPU Monitor", +} + +# This cleanup will be called when ComfyUI is shutting down +def cleanup(): + stop_monitor_thread() + +# Web directory setup for ComfyUI to find our JS files +WEB_DIRECTORY = os.path.join(os.path.dirname(os.path.realpath(__file__)), "web") +print(f"AMD GPU Monitor: Web directory set to {WEB_DIRECTORY}") diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..65e999201ba0fb6622ea8490f1d03273e89ee7ea GIT binary patch literal 15459 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 literal 0 HcmV?d00001 diff --git a/web/index.js b/web/index.js new file mode 100644 index 0000000..cb13995 --- /dev/null +++ b/web/index.js @@ -0,0 +1 @@ +import "./monitor.js"; diff --git a/web/monitor.js b/web/monitor.js new file mode 100644 index 0000000..afd0c5c --- /dev/null +++ b/web/monitor.js @@ -0,0 +1,403 @@ +import { app } from "../../scripts/app.js"; +import { api } from "../../scripts/api.js"; + +// Create the monitor UI element +const createMonitorElement = () => { + // Create main container + const container = document.createElement("div"); + container.className = "amd-gpu-monitor"; + container.style.position = "absolute"; + container.style.top = "40px"; // Moved down to avoid toolbar + container.style.right = "5px"; + container.style.zIndex = "1000"; + container.style.backgroundColor = "#1a1a1a"; + container.style.color = "#fff"; + container.style.padding = "10px"; + container.style.borderRadius = "5px"; + container.style.fontFamily = "monospace"; + container.style.fontSize = "12px"; + container.style.boxShadow = "0 0 10px rgba(0, 0, 0, 0.5)"; + container.style.width = "220px"; + container.style.userSelect = "none"; + + // Add title + const title = document.createElement("div"); + title.style.fontWeight = "bold"; + title.style.marginBottom = "5px"; + title.style.display = "flex"; + title.style.alignItems = "center"; + title.style.justifyContent = "space-between"; + title.innerHTML = 'AMD GPU Monitor'; + + // Add collapse button + const collapseButton = document.createElement("button"); + collapseButton.innerHTML = "−"; // Unicode minus sign + collapseButton.style.background = "none"; + collapseButton.style.border = "none"; + collapseButton.style.color = "#888"; + collapseButton.style.cursor = "pointer"; + collapseButton.style.fontSize = "14px"; + collapseButton.style.padding = "0 5px"; + collapseButton.title = "Collapse/Expand"; + + // Add close button + const closeButton = document.createElement("button"); + closeButton.innerHTML = "×"; // Unicode times sign + closeButton.style.background = "none"; + closeButton.style.border = "none"; + closeButton.style.color = "#888"; + closeButton.style.cursor = "pointer"; + closeButton.style.fontSize = "14px"; + closeButton.style.padding = "0 5px"; + closeButton.title = "Close"; + + const buttonContainer = document.createElement("div"); + buttonContainer.appendChild(collapseButton); + buttonContainer.appendChild(closeButton); + + title.appendChild(buttonContainer); + container.appendChild(title); + + // Content container that can be collapsed + const content = document.createElement("div"); + content.className = "amd-gpu-monitor-content"; + container.appendChild(content); + + // GPU Utilization section + const gpuSection = document.createElement("div"); + gpuSection.style.marginBottom = "8px"; + + const gpuLabel = document.createElement("div"); + gpuLabel.textContent = "GPU Utilization:"; + gpuLabel.style.marginBottom = "2px"; + + const gpuBarContainer = document.createElement("div"); + gpuBarContainer.style.height = "15px"; + gpuBarContainer.style.backgroundColor = "#333"; + gpuBarContainer.style.borderRadius = "3px"; + gpuBarContainer.style.position = "relative"; + + const gpuBar = document.createElement("div"); + gpuBar.className = "amd-gpu-utilization-bar"; + gpuBar.style.height = "100%"; + gpuBar.style.width = "0%"; + gpuBar.style.backgroundColor = "#47a0ff"; + gpuBar.style.borderRadius = "3px"; + gpuBar.style.transition = "width 0.5s ease-out, background-color 0.3s"; + + const gpuText = document.createElement("div"); + gpuText.className = "amd-gpu-utilization-text"; + gpuText.textContent = "0%"; + gpuText.style.position = "absolute"; + gpuText.style.top = "0"; + gpuText.style.left = "5px"; + gpuText.style.lineHeight = "15px"; + gpuText.style.textShadow = "1px 1px 1px #000"; + + gpuBarContainer.appendChild(gpuBar); + gpuBarContainer.appendChild(gpuText); + gpuSection.appendChild(gpuLabel); + gpuSection.appendChild(gpuBarContainer); + content.appendChild(gpuSection); + + // VRAM Usage section + const vramSection = document.createElement("div"); + vramSection.style.marginBottom = "8px"; + + const vramLabel = document.createElement("div"); + vramLabel.textContent = "VRAM Usage:"; + vramLabel.style.marginBottom = "2px"; + + const vramBarContainer = document.createElement("div"); + vramBarContainer.style.height = "15px"; + vramBarContainer.style.backgroundColor = "#333"; + vramBarContainer.style.borderRadius = "3px"; + vramBarContainer.style.position = "relative"; + + const vramBar = document.createElement("div"); + vramBar.className = "amd-vram-bar"; + vramBar.style.height = "100%"; + vramBar.style.width = "0%"; + vramBar.style.backgroundColor = "#47a0ff"; + vramBar.style.borderRadius = "3px"; + vramBar.style.transition = "width 0.5s ease-out, background-color 0.3s"; + + const vramText = document.createElement("div"); + vramText.className = "amd-vram-text"; + vramText.textContent = "0MB / 0MB (0%)"; + vramText.style.position = "absolute"; + vramText.style.top = "0"; + vramText.style.left = "5px"; + vramText.style.lineHeight = "15px"; + vramText.style.textShadow = "1px 1px 1px #000"; + + vramBarContainer.appendChild(vramBar); + vramBarContainer.appendChild(vramText); + vramSection.appendChild(vramLabel); + vramSection.appendChild(vramBarContainer); + content.appendChild(vramSection); + + // Temperature section + const tempSection = document.createElement("div"); + + const tempLabel = document.createElement("div"); + tempLabel.textContent = "GPU Temperature:"; + tempLabel.style.marginBottom = "2px"; + + const tempBarContainer = document.createElement("div"); + tempBarContainer.style.height = "15px"; + tempBarContainer.style.backgroundColor = "#333"; + tempBarContainer.style.borderRadius = "3px"; + tempBarContainer.style.position = "relative"; + + const tempBar = document.createElement("div"); + tempBar.className = "amd-temp-bar"; + tempBar.style.height = "100%"; + tempBar.style.width = "0%"; + tempBar.style.backgroundColor = "#47a0ff"; + tempBar.style.borderRadius = "3px"; + tempBar.style.transition = "width 0.5s ease-out, background-color 0.3s"; + + const tempText = document.createElement("div"); + tempText.className = "amd-temp-text"; + tempText.textContent = "0°C"; + tempText.style.position = "absolute"; + tempText.style.top = "0"; + tempText.style.left = "5px"; + tempText.style.lineHeight = "15px"; + tempText.style.textShadow = "1px 1px 1px #000"; + + tempBarContainer.appendChild(tempBar); + tempBarContainer.appendChild(tempText); + tempSection.appendChild(tempLabel); + tempSection.appendChild(tempBarContainer); + content.appendChild(tempSection); + + // Add event listener for collapsing + let isCollapsed = false; + collapseButton.addEventListener("click", () => { + if (isCollapsed) { + content.style.display = "block"; + collapseButton.innerHTML = "−"; + isCollapsed = false; + } else { + content.style.display = "none"; + collapseButton.innerHTML = "+"; + isCollapsed = true; + } + }); + + // Add event listener for closing + closeButton.addEventListener("click", () => { + container.style.display = "none"; + // Store the closed state in localStorage + localStorage.setItem("amd-gpu-monitor-closed", "true"); + }); + + // Make the monitor draggable + let isDragging = false; + let dragOffsetX, dragOffsetY; + + title.addEventListener("mousedown", (e) => { + // Only handle main button (left button) + if (e.button !== 0) return; + + isDragging = true; + dragOffsetX = e.clientX - container.offsetLeft; + dragOffsetY = e.clientY - container.offsetTop; + + // Prevent text selection during drag + e.preventDefault(); + }); + + document.addEventListener("mousemove", (e) => { + if (!isDragging) return; + + const x = e.clientX - dragOffsetX; + const y = e.clientY - dragOffsetY; + + // Keep monitor within window bounds + const maxX = window.innerWidth - container.offsetWidth; + const maxY = window.innerHeight - container.offsetHeight; + + container.style.left = Math.max(0, Math.min(x, maxX)) + "px"; + container.style.top = Math.max(0, Math.min(y, maxY)) + "px"; + + // We're now positioning with left instead of right + container.style.right = "auto"; + }); + + document.addEventListener("mouseup", () => { + isDragging = false; + + // Save position to localStorage + if (container.style.left && container.style.top) { + localStorage.setItem("amd-gpu-monitor-position", JSON.stringify({ + left: container.style.left, + top: container.style.top + })); + } + }); + + // Load saved position if available + const savedPosition = localStorage.getItem("amd-gpu-monitor-position"); + if (savedPosition) { + try { + const { left, top } = JSON.parse(savedPosition); + container.style.left = left; + container.style.top = top; + container.style.right = "auto"; + } catch (e) { + // Silently fail and use default position + } + } + + // Check if monitor was closed previously + if (localStorage.getItem("amd-gpu-monitor-closed") === "true") { + container.style.display = "none"; + } + + // Add a button to show the monitor again + const showButton = document.createElement("button"); + showButton.textContent = "Show AMD GPU Monitor"; + showButton.style.position = "fixed"; + showButton.style.top = "5px"; + showButton.style.right = "5px"; + showButton.style.zIndex = "999"; + showButton.style.padding = "5px"; + showButton.style.borderRadius = "3px"; + showButton.style.backgroundColor = "#333"; + showButton.style.color = "#fff"; + showButton.style.border = "none"; + showButton.style.fontSize = "12px"; + showButton.style.cursor = "pointer"; + showButton.style.display = "none"; + + showButton.addEventListener("click", () => { + container.style.display = "block"; + showButton.style.display = "none"; + localStorage.removeItem("amd-gpu-monitor-closed"); + }); + + document.body.appendChild(showButton); + + // Toggle showButton visibility based on monitor visibility + const updateShowButtonVisibility = () => { + if (container.style.display === "none") { + showButton.style.display = "block"; + } else { + showButton.style.display = "none"; + } + }; + + // Create a MutationObserver to watch for changes to container's display style + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.attributeName === "style") { + updateShowButtonVisibility(); + } + }); + }); + + observer.observe(container, { attributes: true }); + + // Initial visibility check + updateShowButtonVisibility(); + + return { container, gpuBar, gpuText, vramBar, vramText, tempBar, tempText }; +}; + +// Update the monitor UI with new data +const updateMonitorUI = (monitor, data) => { + // Check if we have GPU data + if (!data || !data.gpus || data.gpus.length === 0) return; + + const gpu = data.gpus[0]; // Use the first GPU + + // Update GPU utilization + if (monitor.gpuBar && monitor.gpuText) { + const utilization = gpu.gpu_utilization || 0; + monitor.gpuBar.style.width = `${utilization}%`; + monitor.gpuText.textContent = `${utilization}%`; + + // Change color based on utilization + if (utilization > 80) { + monitor.gpuBar.style.backgroundColor = '#ff4d4d'; // Red for high + } else if (utilization > 50) { + monitor.gpuBar.style.backgroundColor = '#ffad33'; // Orange for medium + } else { + monitor.gpuBar.style.backgroundColor = '#47a0ff'; // Blue for low + } + } + + // Update VRAM usage + if (monitor.vramBar && monitor.vramText) { + const vramPercent = gpu.vram_used_percent || 0; + const vramUsed = gpu.vram_used || 0; + const vramTotal = gpu.vram_total || 1; + + monitor.vramBar.style.width = `${vramPercent}%`; + + // Format the text to show MB or GB + let vramUsedText = vramUsed; + let vramTotalText = vramTotal; + let unit = 'MB'; + + if (vramTotal >= 1024) { + vramUsedText = (vramUsed / 1024).toFixed(1); + vramTotalText = (vramTotal / 1024).toFixed(1); + unit = 'GB'; + } + + monitor.vramText.textContent = `${vramUsedText}${unit} / ${vramTotalText}${unit} (${vramPercent}%)`; + + // Change color based on VRAM usage + if (vramPercent > 85) { + monitor.vramBar.style.backgroundColor = '#ff4d4d'; // Red for high + } else if (vramPercent > 70) { + monitor.vramBar.style.backgroundColor = '#ffad33'; // Orange for medium + } else { + monitor.vramBar.style.backgroundColor = '#47a0ff'; // Blue for low + } + } + + // Update temperature + if (monitor.tempBar && monitor.tempText) { + const temp = gpu.gpu_temperature || 0; + + // Assume max reasonable temp is 100°C for the progress bar + const tempPercent = Math.min(temp, 100); + monitor.tempBar.style.width = `${tempPercent}%`; + monitor.tempText.textContent = `${temp}°C`; + + // Change color based on temperature + if (temp > 80) { + monitor.tempBar.style.backgroundColor = '#ff4d4d'; // Red for high + } else if (temp > 60) { + monitor.tempBar.style.backgroundColor = '#ffad33'; // Orange for medium + } else { + monitor.tempBar.style.backgroundColor = '#47a0ff'; // Blue for low + } + } +}; + +// Main app function +const main = () => { + // Create the monitor UI + const monitor = createMonitorElement(); + document.body.appendChild(monitor.container); + + // Set up WebSocket listener for GPU updates + api.addEventListener("amd_gpu_monitor", (event) => { + updateMonitorUI(monitor, event.detail); + }); +}; + +// Wait for DOM to be loaded +app.registerExtension({ + name: "amd.gpu.monitor", + async setup() { + // Wait a bit for the UI to be fully loaded + setTimeout(main, 1000); + }, +});