From 9b895673eb7d91ab5b71cac2ec2e5774864d2193 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 13 Jun 2026 15:58:13 +0000 Subject: [PATCH] feat: GPU capability display in UI + GTX 1060 6GB SDXL fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Model selection: - Add sdxl_offload tier (eff_vram ≥ 4.0 GB) for GTX 1060 6GB and Quadro 6GB cards that were falling through to SD 2.1 despite SDXL fitting with model_cpu_offload. Cards with 5.3 GB effective VRAM now get SDXL quality. - Update _tier_label(), _caps(), _build_warnings() for new tier. Frontend GPU display: - api.js: add getGpuStatus() fetching /api/gpu/status - capabilities.js: add getGpuStatus() export with own LRU cache; refreshCapabilities() now also resets GPU status cache - provider-badge.js: when AI_PROVIDER=local_gpu show green badge with GPU name, tier, VRAM, CC, feature flags, and capabilities in tooltip. Strip "NVIDIA GeForce" prefix so "GTX 1060 6GB" fits in badge. - ai_provider_settings.js: add local_gpu to all provider dropdowns; show GPU info panel (device, VRAM, CC, features, tier, model table per operation) in the settings dialog when a GPU is detected. https://claude.ai/code/session_01WVDg7amsy1TTtxvpku7bcM --- backend/app/services/gpu_detect.py | 18 +- frontend/src/js/api/capabilities.js | 21 +- .../src/js/core/components/provider-badge.js | 51 +++- .../js/modules/tools/ai_provider_settings.js | 265 +++++++++++------- frontend/src/js/services/api.js | 15 + 5 files changed, 262 insertions(+), 108 deletions(-) diff --git a/backend/app/services/gpu_detect.py b/backend/app/services/gpu_detect.py index d9829be..e4d495f 100644 --- a/backend/app/services/gpu_detect.py +++ b/backend/app/services/gpu_detect.py @@ -10,6 +10,7 @@ Model selection ladder (txt2img): eff_vram ≥ 10 GB → FLUX.1-schnell (model_cpu_offload, 2–3× slower but fits) eff_vram ≥ 7.5 GB → SDXL base eff_vram ≥ 5.5 GB → SDXL base + attention slicing + eff_vram ≥ 4.0 GB → SDXL + model_cpu_offload (GTX 1060 6 GB, Quadro 6 GB) eff_vram ≥ 3.5 GB → Stable Diffusion 2.1 eff_vram ≥ 2.5 GB → SD 2.1-base + attention slicing eff_vram ≥ 1.7 GB → Stable Diffusion 1.5 @@ -61,7 +62,7 @@ class GpuCapabilities: effective_vram_gb: float # free VRAM after overhead, halved if fp32-only # Human-readable tier label - tier: str # flux_full | flux_offload | sdxl | sdxl_low | sd2x | sd15 | minimal + tier: str # flux_full | flux_offload | sdxl | sdxl_low | sdxl_offload | sd2x | sd2x_low | sd15 | minimal # Best model per operation recommended: dict[str, Optional[ModelSpec]] @@ -200,6 +201,8 @@ def _select_txt2img(eff: float) -> ModelSpec: return ModelSpec("stabilityai/stable-diffusion-xl-base-1.0", "sdxl", "none", 1024, 6.5) if eff >= 5.5: return ModelSpec("stabilityai/stable-diffusion-xl-base-1.0", "sdxl", "attention_slicing", 1024, 6.5) + if eff >= 4.0: + return ModelSpec("stabilityai/stable-diffusion-xl-base-1.0", "sdxl", "model_cpu_offload", 1024, 6.5) # SD 2.x if eff >= 3.5: return ModelSpec("stabilityai/stable-diffusion-2-1", "sd2x", "none", 768, 3.5) @@ -224,6 +227,8 @@ def _select_inpaint(eff: float) -> ModelSpec: return ModelSpec("diffusers/stable-diffusion-xl-1.0-inpainting-0.1", "sdxl", "none", 1024, 6.5) if eff >= 5.5: return ModelSpec("diffusers/stable-diffusion-xl-1.0-inpainting-0.1", "sdxl", "attention_slicing", 1024, 6.5) + if eff >= 4.0: + return ModelSpec("diffusers/stable-diffusion-xl-1.0-inpainting-0.1", "sdxl", "model_cpu_offload", 1024, 6.5) if eff >= 3.5: return ModelSpec("stabilityai/stable-diffusion-2-inpainting", "sd2x", "none", 512, 3.5) if eff >= 2.5: @@ -248,6 +253,7 @@ def _tier_label(eff_vram: float) -> str: if eff_vram >= 10: return "flux_offload" if eff_vram >= 7.5: return "sdxl" if eff_vram >= 5.5: return "sdxl_low" + if eff_vram >= 4.0: return "sdxl_offload" if eff_vram >= 3.5: return "sd2x" if eff_vram >= 2.5: return "sd2x_low" if eff_vram >= 1.7: return "sd15" @@ -256,7 +262,7 @@ def _tier_label(eff_vram: float) -> str: def _caps(tier: str) -> list[str]: base = ["txt2img", "inpaint", "img2img", "outpaint"] - if tier in ("flux_full", "flux_offload", "sdxl", "sdxl_low"): + if tier in ("flux_full", "flux_offload", "sdxl", "sdxl_low", "sdxl_offload"): return base + ["upscale_diffusion"] return base @@ -297,6 +303,12 @@ def _build_warnings( f"Very low effective VRAM ({vram_free:.1f} GB free). " "Sequential CPU offload will be used — expect 10–30 min per image." ) + elif tier == "sdxl_offload": + w.append( + f"Limited VRAM ({vram_free:.1f} GB free). " + "Using SDXL with model_cpu_offload — better quality than SD 2.x, ~30% slower. " + "Install xformers or upgrade to ≥5.5 GB effective VRAM for full-speed SDXL." + ) elif tier in ("sd15", "sd2x_low"): w.append( f"Limited VRAM ({vram_free:.1f} GB free). " @@ -309,7 +321,7 @@ def _build_warnings( "You may be able to run a higher-tier model than listed." ) else: - if tier in ("sdxl_low", "sd2x"): + if tier in ("sdxl_low", "sdxl_offload", "sd2x"): w.append( "xformers not installed. Install it (pip install xformers) to reduce " "VRAM usage ~20-30% and potentially unlock the next model tier." diff --git a/frontend/src/js/api/capabilities.js b/frontend/src/js/api/capabilities.js index b49b423..1944e3e 100644 --- a/frontend/src/js/api/capabilities.js +++ b/frontend/src/js/api/capabilities.js @@ -19,6 +19,8 @@ const DEFAULT_CAPS = { let _caps = null; let _fetchPromise = null; +let _gpuStatus = null; +let _gpuFetchPromise = null; /** * Return capabilities (fetched lazily, cached thereafter). @@ -48,12 +50,29 @@ export function hasRemote() { return !!(_caps?.remote?.healthy); } +/** + * Fetch and cache detailed GPU status (hardware, feature flags, model selection per op). + * Calls /api/gpu/status — only meaningful when AI_PROVIDER=local_gpu. + * Returns null on error. + */ +export async function getGpuStatus() { + if (_gpuStatus !== null) return _gpuStatus; + if (!_gpuFetchPromise) { + _gpuFetchPromise = apiService.getGpuStatus() + .then(data => { _gpuStatus = data; return _gpuStatus; }) + .catch(() => { _gpuStatus = null; return null; }); + } + return _gpuFetchPromise; +} + /** * Invalidate cache and re-fetch (call after saving provider settings). */ export async function refreshCapabilities() { _caps = null; _fetchPromise = null; + _gpuStatus = null; + _gpuFetchPromise = null; return getCapabilities(); } @@ -62,4 +81,4 @@ export async function refreshCapabilities() { */ getCapabilities(); -export default { getCapabilities, getCachedCapabilities, hasRemote, refreshCapabilities }; +export default { getCapabilities, getCachedCapabilities, hasRemote, refreshCapabilities, getGpuStatus }; diff --git a/frontend/src/js/core/components/provider-badge.js b/frontend/src/js/core/components/provider-badge.js index 8724733..fc20790 100644 --- a/frontend/src/js/core/components/provider-badge.js +++ b/frontend/src/js/core/components/provider-badge.js @@ -2,7 +2,7 @@ * ProviderBadge — small DOM element showing the active AI provider. * Inserted into the toolbar footer on app load. * - * Green = remote provider healthy + * Green = remote provider healthy (or local_gpu active) * Yellow = provider configured but unhealthy/unreachable * Grey = local only (LaMa + OpenCV) */ @@ -30,21 +30,55 @@ export async function mountProviderBadge(container) { var remote = caps.remote || {}; var local = caps.local || {}; - if (remote.provider && remote.healthy) { + if (remote.provider === 'local_gpu') { + // Local GPU provider — show GPU name and tier from /api/config local fields + var gpuName = _shortGpuName(local.gpu_device); + var tier = local.gpu_tier || ''; + + if (remote.healthy) { + dot.style.background = '#44cc44'; + badge.style.background = '#1a2a1a'; + badge.style.color = '#aaffaa'; + label.textContent = 'GPU · ' + tier + ' · ' + gpuName; + + var flagList = [ + local.gpu_fp16 && 'fp16', + local.gpu_bf16 && 'bf16', + local.gpu_fp8 && 'fp8', + local.gpu_tensor_cores && 'tensor-cores', + ].filter(Boolean).join(' '); + + badge.title = [ + local.gpu_device || gpuName, + 'VRAM: ' + local.gpu_vram_total + ' GB total ' + local.gpu_vram_free + ' GB free', + 'Compute: CC ' + local.gpu_cc + ' Eff: ' + local.gpu_eff_vram + ' GB', + flagList ? 'Features: ' + flagList : '', + 'Capabilities: ' + (local.local_gpu_capabilities || []).join(', '), + (local.local_gpu_warnings || []).length + ? '\nWarnings:\n' + local.local_gpu_warnings.join('\n') + : '', + ].filter(Boolean).join('\n'); + } else { + dot.style.background = '#ffaa00'; + badge.style.background = '#2a2000'; + badge.style.color = '#ffdd88'; + label.textContent = 'Local GPU (not ready)'; + badge.title = 'local_gpu is configured but the diffusers library may not be installed.\nCheck container logs for details.'; + } + } else if (remote.provider && remote.healthy) { dot.style.background = '#44cc44'; badge.style.background = '#1a2a1a'; badge.style.color = '#aaffaa'; - // Show override summary if any operations use different providers var overrides = remote.overrides || {}; var overrideEntries = Object.entries(overrides).filter(([, v]) => v); var overrideStr = overrideEntries.length - ? ' · ' + overrideEntries.map(([k, v]) => `${k}→${v}`).join(', ') + ? ' · ' + overrideEntries.map(([k, v]) => k + '→' + v).join(', ') : ''; label.textContent = remote.provider + overrideStr + (local.gpu_detected ? ' · GPU' : ''); var opLines = Object.entries(remote.operations || {}) - .map(([op, s]) => `${op}: ${s.provider || remote.provider} ${s.healthy ? '✓' : '✗'}`) + .map(([op, s]) => op + ': ' + (s.provider || remote.provider) + ' ' + (s.healthy ? '✓' : '✗')) .join('\n'); badge.title = opLines || ('Provider: ' + remote.provider); } else if (remote.provider && !remote.healthy) { @@ -70,3 +104,10 @@ export async function mountProviderBadge(container) { return badge; } + +function _shortGpuName(name) { + return (name || 'GPU') + .replace(/^NVIDIA GeForce\s+/i, '') + .replace(/^NVIDIA\s+/i, '') + .replace(/^AMD Radeon\s+/i, ''); +} diff --git a/frontend/src/js/modules/tools/ai_provider_settings.js b/frontend/src/js/modules/tools/ai_provider_settings.js index 4268346..b4694e6 100644 --- a/frontend/src/js/modules/tools/ai_provider_settings.js +++ b/frontend/src/js/modules/tools/ai_provider_settings.js @@ -6,7 +6,7 @@ import Dialog_class from './../../libs/popup.js'; import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js'; -import { getCapabilities } from './../../api/capabilities.js'; +import { getCapabilities, getGpuStatus } from './../../api/capabilities.js'; // localStorage key prefix const LS = 'paintplus_ai_'; @@ -30,109 +30,133 @@ class Tools_ai_provider_settings_class { async ai_provider_settings() { var _this = this; + + // Fetch caps and GPU status in parallel var caps = await getCapabilities(); + var gpuStatus = null; + var local = caps.local || {}; + if (local.local_gpu_available) { + gpuStatus = await getGpuStatus().catch(() => null); + } + var remote = caps.remote || {}; var statusHtml = remote.provider ? (remote.healthy - ? `● ${remote.provider} — connected` - : `● ${remote.provider} — unreachable`) + ? '● ' + remote.provider + ' — connected' + : '● ' + remote.provider + ' — unreachable') : 'No remote provider configured'; + var gpuInfoHtml = gpuStatus ? _renderGpuInfo(gpuStatus) : ''; + + var providerValues = ['', 'openai', 'invokeai', 'comfyui', 'replicate', 'local_gpu']; + + var params = [ + { + title: 'Status:', + html: '
' + statusHtml + '
', + }, + ]; + + if (gpuInfoHtml) { + params.push({ + title: '', + html: '
Detected GPU:
' + gpuInfoHtml + '
', + }); + } + + params.push( + { + name: 'provider', + title: 'Default provider (used unless overridden below):', + value: ls_get('provider', remote.provider || ''), + values: providerValues, + type: 'select', + }, + // ── Per-operation overrides ─────────────────────────────── + { + title: '', + html: '
Per-operation overrides — blank = use default above
', + }, + { + name: 'provider_inpaint', + title: 'Inpaint / Replace Selection:', + value: ls_get('provider_inpaint', remote.overrides?.inpaint || ''), + values: providerValues, + type: 'select', + }, + { + name: 'provider_txt2img', + title: 'Text → Image:', + value: ls_get('provider_txt2img', remote.overrides?.txt2img || ''), + values: providerValues, + type: 'select', + }, + { + name: 'provider_img2img', + title: 'Image → Image:', + value: ls_get('provider_img2img', remote.overrides?.img2img || ''), + values: providerValues, + type: 'select', + }, + { + name: 'provider_outpaint', + title: 'Expand Canvas (Outpaint):', + value: ls_get('provider_outpaint', remote.overrides?.outpaint || ''), + values: providerValues, + type: 'select', + }, + // ── OpenAI ──────────────────────────────────────────────── + { + name: 'openai_key', + title: 'OpenAI API key:', + value: ls_get('openai_key'), + placeholder: 'sk-...', + }, + { + name: 'openai_model', + title: 'OpenAI model:', + value: ls_get('openai_model', 'dall-e-3'), + values: ['dall-e-3', 'dall-e-2'], + type: 'select', + }, + // ── InvokeAI ────────────────────────────────────────────── + { + name: 'invokeai_url', + title: 'InvokeAI URL:', + value: ls_get('invokeai_url'), + placeholder: 'http://192.168.1.x:9090', + }, + { + name: 'invokeai_model', + title: 'InvokeAI default model:', + value: ls_get('invokeai_model', 'flux-dev'), + placeholder: 'flux-dev', + }, + // ── ComfyUI ─────────────────────────────────────────────── + { + name: 'comfyui_url', + title: 'ComfyUI URL:', + value: ls_get('comfyui_url'), + placeholder: 'http://192.168.1.x:8188', + }, + { + name: 'comfyui_model', + title: 'ComfyUI default checkpoint:', + value: ls_get('comfyui_model', 'v1-5-pruned-emaonly.ckpt'), + placeholder: 'v1-5-pruned-emaonly.ckpt', + }, + // ── Replicate ───────────────────────────────────────────── + { + name: 'replicate_key', + title: 'Replicate API key:', + value: ls_get('replicate_key'), + placeholder: 'r8_...', + } + ); + this.POP.show({ title: 'AI Provider Settings', - params: [ - { - title: 'Status:', - html: `
${statusHtml}
`, - }, - { - name: 'provider', - title: 'Default provider (used unless overridden below):', - value: ls_get('provider', remote.provider || ''), - values: ['', 'openai', 'invokeai', 'comfyui', 'replicate'], - type: 'select', - }, - // ── Per-operation overrides ─────────────────────────────── - { - title: '', - html: '
Per-operation overrides — blank = use default above
', - }, - { - name: 'provider_inpaint', - title: 'Inpaint / Replace Selection:', - value: ls_get('provider_inpaint', remote.overrides?.inpaint || ''), - values: ['', 'openai', 'invokeai', 'comfyui', 'replicate'], - type: 'select', - }, - { - name: 'provider_txt2img', - title: 'Text → Image:', - value: ls_get('provider_txt2img', remote.overrides?.txt2img || ''), - values: ['', 'openai', 'invokeai', 'comfyui', 'replicate'], - type: 'select', - }, - { - name: 'provider_img2img', - title: 'Image → Image:', - value: ls_get('provider_img2img', remote.overrides?.img2img || ''), - values: ['', 'openai', 'invokeai', 'comfyui', 'replicate'], - type: 'select', - }, - { - name: 'provider_outpaint', - title: 'Expand Canvas (Outpaint):', - value: ls_get('provider_outpaint', remote.overrides?.outpaint || ''), - values: ['', 'openai', 'invokeai', 'comfyui', 'replicate'], - type: 'select', - }, - // ── OpenAI ──────────────────────────────────────────────── - { - name: 'openai_key', - title: 'OpenAI API key:', - value: ls_get('openai_key'), - placeholder: 'sk-...', - }, - { - name: 'openai_model', - title: 'OpenAI model:', - value: ls_get('openai_model', 'dall-e-3'), - values: ['dall-e-3', 'dall-e-2'], - type: 'select', - }, - // ── InvokeAI ────────────────────────────────────────────── - { - name: 'invokeai_url', - title: 'InvokeAI URL:', - value: ls_get('invokeai_url'), - placeholder: 'http://192.168.1.x:9090', - }, - { - name: 'invokeai_model', - title: 'InvokeAI default model:', - value: ls_get('invokeai_model', 'flux-dev'), - placeholder: 'flux-dev', - }, - // ── ComfyUI ─────────────────────────────────────────────── - { - name: 'comfyui_url', - title: 'ComfyUI URL:', - value: ls_get('comfyui_url'), - placeholder: 'http://192.168.1.x:8188', - }, - { - name: 'comfyui_model', - title: 'ComfyUI default checkpoint:', - value: ls_get('comfyui_model', 'v1-5-pruned-emaonly.ckpt'), - placeholder: 'v1-5-pruned-emaonly.ckpt', - }, - // ── Replicate ───────────────────────────────────────────── - { - name: 'replicate_key', - title: 'Replicate API key:', - value: ls_get('replicate_key'), - placeholder: 'r8_...', - }, - ], + params: params, on_finish: async function (params) { await _this._save(params); }, @@ -182,12 +206,15 @@ class Tools_ai_provider_settings_class { var { refreshCapabilities } = await import('./../../api/capabilities.js'); var caps = await refreshCapabilities(); if (caps?.remote?.healthy) { - alertify.success(`Connected to ${caps.remote.provider}!`); + alertify.success('Connected to ' + caps.remote.provider + '!'); } else if (params.provider) { - alertify.warning('Settings saved but provider is not reachable. Check URL/key.'); + if (params.provider === 'local_gpu') { + alertify.success('local_gpu set — restart the container with docker-compose.gpu.yml to activate.'); + } else { + alertify.warning('Settings saved but provider is not reachable. Check URL/key.'); + } } } else { - // Server-side config update not supported — inform user to set .env alertify.warning( 'Settings saved locally. To make them permanent, ' + 'set these values in your .env file and restart the server.' @@ -201,4 +228,44 @@ class Tools_ai_provider_settings_class { } } +function _renderGpuInfo(g) { + var flags = [ + g.fp16 && 'fp16', + g.bf16 && 'bf16', + g.fp8 && 'fp8', + g.int8 && 'int8', + g.tensor_cores && 'tensor-cores', + g.xformers && 'xformers', + ].filter(Boolean).join(' · '); + + var rows = Object.entries(g.recommended || {}) + .filter(([, s]) => s) + .map(function([op, s]) { + var modelName = s.model_id.split('/').pop(); + return '' + + '' + op + '' + + '' + modelName + '' + + '' + s.memory_opt + '' + + ''; + }) + .join(''); + + var warnHtml = (g.warnings || []).length + ? '
' + + g.warnings.map(function(w) { return '⚠ ' + w; }).join('
') + '
' + : ''; + + return '
' + + '
⬛ ' + (g.device_name || 'GPU') + '
' + + '
VRAM: ' + g.vram_total_gb + ' GB total · ' + g.vram_free_gb + ' GB free
' + + '
Compute: CC ' + g.compute_capability + '' + + (flags ? ' ' + flags + '' : '') + '
' + + '
Effective: ' + g.effective_vram_gb + ' GB' + + ' Tier: ' + g.tier + '
' + + (rows ? '
Models selected:
' + + '' + rows + '
' : '') + + warnHtml + + '
'; +} + export default Tools_ai_provider_settings_class; diff --git a/frontend/src/js/services/api.js b/frontend/src/js/services/api.js index f853673..b78d6e9 100644 --- a/frontend/src/js/services/api.js +++ b/frontend/src/js/services/api.js @@ -213,6 +213,21 @@ class ApiService { } } + /** + * Fetch GPU status: hardware, feature flags, and selected models per operation. + * Only meaningful when AI_PROVIDER=local_gpu. + * @returns {Promise} + */ + async getGpuStatus() { + try { + const response = await fetch(`${this.baseUrl}/api/gpu/status`); + if (!response.ok) return null; + return response.json(); + } catch { + return null; + } + } + /** * Health check for the backend * @returns {Promise}