diff --git a/web/gpupanel.js b/web/gpupanel.js index 44c674c..261d5fd 100644 --- a/web/gpupanel.js +++ b/web/gpupanel.js @@ -5,6 +5,7 @@ class MultiGPUExtension { // Button styling constants static BUTTON_STYLES = { base: "width: 100%; padding: 8px; font-size: 12px; color: white; border: none; border-radius: 4px; cursor: pointer; transition: background-color 0.2s;", + clearMemory: "background-color: #555;", success: "background-color: #3ca03c;", error: "background-color: #c04c4c;" }; @@ -97,6 +98,57 @@ class MultiGPUExtension { return button; } + async _handleClearMemory(button) { + const originalText = button.textContent; + button.textContent = "Clearing..."; + button.disabled = true; + + try { + const urlsToClear = this.enabledWorkers.map(w => ({ + name: w.name, + url: `http://${w.host || window.location.hostname}:${w.port}` + })); + + if (urlsToClear.length === 0) { + button.textContent = "No Workers"; + button.style.backgroundColor = MultiGPUExtension.BUTTON_STYLES.error.split(':')[1].trim().replace(';', ''); + setTimeout(() => { + button.textContent = originalText; + button.style.cssText = MultiGPUExtension.BUTTON_STYLES.base + MultiGPUExtension.BUTTON_STYLES.clearMemory; + }, 3000); + return; + } + + const promises = urlsToClear.map(target => + fetch(`${target.url}/multigpu/clear_memory`, { + method: 'POST', + mode: 'cors' + }) + .then(response => ({ ok: response.ok, name: target.name })) + .catch(() => ({ ok: false, name: target.name })) + ); + + const results = await Promise.all(promises); + const failures = results.filter(r => !r.ok); + + if (failures.length === 0) { + button.textContent = "Success!"; + button.style.backgroundColor = MultiGPUExtension.BUTTON_STYLES.success.split(':')[1].trim().replace(';', ''); + } else { + button.textContent = "Error! See Console"; + button.style.backgroundColor = MultiGPUExtension.BUTTON_STYLES.error.split(':')[1].trim().replace(';', ''); + console.error("[MultiGPU] Failed to clear memory on:", failures.map(f => f.name).join(", ")); + } + + setTimeout(() => { + button.textContent = originalText; + button.style.cssText = MultiGPUExtension.BUTTON_STYLES.base + MultiGPUExtension.BUTTON_STYLES.clearMemory; + }, 3000); + } finally { + button.disabled = false; + } + } + renderSidebarContent(el) { el.innerHTML = ''; @@ -131,7 +183,9 @@ class MultiGPUExtension { label.htmlFor = `gpu-${worker.id}`; label.style.cssText = "cursor: pointer; display: flex; align-items: center; gap: 8px;"; const gpuInfo = document.createElement("span"); - gpuInfo.innerHTML = `${worker.name}
CUDA ${worker.cuda_device} • Port ${worker.port}`; + const hostInfo = worker.host ? ` • ${worker.host}` : ''; + const cudaInfo = worker.cuda_device !== undefined ? `CUDA ${worker.cuda_device} • ` : ''; + gpuInfo.innerHTML = `${worker.name}
${cudaInfo}Port ${worker.port}${hostInfo}`; label.appendChild(checkbox); label.appendChild(gpuInfo); gpuDiv.appendChild(label); @@ -140,6 +194,14 @@ class MultiGPUExtension { gpuSection.appendChild(gpuList); container.appendChild(gpuSection); + const actionsSection = document.createElement("div"); + actionsSection.style.cssText = "padding-top: 10px; margin-bottom: 15px; border-top: 1px solid #444;"; + + const clearMemButton = this._createButton("Clear Worker VRAM", (e) => this._handleClearMemory(e.target), MultiGPUExtension.BUTTON_STYLES.clearMemory); + clearMemButton.title = "Clear VRAM on all enabled worker GPUs (not master)"; + actionsSection.appendChild(clearMemButton); + + container.appendChild(actionsSection); const summarySection = document.createElement("div"); summarySection.style.cssText = "border-top: 1px solid #444; padding-top: 10px;"; @@ -273,7 +335,7 @@ class MultiGPUExtension { } async _dispatchToWorker(worker, prompt, workflow) { - const workerUrl = `http://localhost:${worker.port}`; + const workerUrl = `http://${worker.host || window.location.hostname}:${worker.port}`; console.log(`[MultiGPU] Dispatching to worker: ${worker.name} (${workerUrl})`); const promptToSend = { prompt,