feat: GPU capability display in UI + GTX 1060 6GB SDXL fix
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
This commit is contained in:
@@ -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."
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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, '');
|
||||
}
|
||||
|
||||
@@ -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,26 +30,46 @@ 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
|
||||
? `<span style="color:#44cc44">● ${remote.provider} — connected</span>`
|
||||
: `<span style="color:#ffaa00">● ${remote.provider} — unreachable</span>`)
|
||||
? '<span style="color:#44cc44">● ' + remote.provider + ' — connected</span>'
|
||||
: '<span style="color:#ffaa00">● ' + remote.provider + ' — unreachable</span>')
|
||||
: '<span style="color:#888">No remote provider configured</span>';
|
||||
|
||||
this.POP.show({
|
||||
title: 'AI Provider Settings',
|
||||
params: [
|
||||
var gpuInfoHtml = gpuStatus ? _renderGpuInfo(gpuStatus) : '';
|
||||
|
||||
var providerValues = ['', 'openai', 'invokeai', 'comfyui', 'replicate', 'local_gpu'];
|
||||
|
||||
var params = [
|
||||
{
|
||||
title: 'Status:',
|
||||
html: `<div style="margin:4px 0 8px;font-size:12px;">${statusHtml}</div>`,
|
||||
html: '<div style="margin:4px 0 8px;font-size:12px;">' + statusHtml + '</div>',
|
||||
},
|
||||
];
|
||||
|
||||
if (gpuInfoHtml) {
|
||||
params.push({
|
||||
title: '',
|
||||
html: '<div style="margin:4px 0 8px"><div style="font-size:11px;color:#aaa;margin-bottom:3px">Detected GPU:</div>' + gpuInfoHtml + '</div>',
|
||||
});
|
||||
}
|
||||
|
||||
params.push(
|
||||
{
|
||||
name: 'provider',
|
||||
title: 'Default provider (used unless overridden below):',
|
||||
value: ls_get('provider', remote.provider || ''),
|
||||
values: ['', 'openai', 'invokeai', 'comfyui', 'replicate'],
|
||||
values: providerValues,
|
||||
type: 'select',
|
||||
},
|
||||
// ── Per-operation overrides ───────────────────────────────
|
||||
@@ -61,28 +81,28 @@ class Tools_ai_provider_settings_class {
|
||||
name: 'provider_inpaint',
|
||||
title: 'Inpaint / Replace Selection:',
|
||||
value: ls_get('provider_inpaint', remote.overrides?.inpaint || ''),
|
||||
values: ['', 'openai', 'invokeai', 'comfyui', 'replicate'],
|
||||
values: providerValues,
|
||||
type: 'select',
|
||||
},
|
||||
{
|
||||
name: 'provider_txt2img',
|
||||
title: 'Text → Image:',
|
||||
value: ls_get('provider_txt2img', remote.overrides?.txt2img || ''),
|
||||
values: ['', 'openai', 'invokeai', 'comfyui', 'replicate'],
|
||||
values: providerValues,
|
||||
type: 'select',
|
||||
},
|
||||
{
|
||||
name: 'provider_img2img',
|
||||
title: 'Image → Image:',
|
||||
value: ls_get('provider_img2img', remote.overrides?.img2img || ''),
|
||||
values: ['', 'openai', 'invokeai', 'comfyui', 'replicate'],
|
||||
values: providerValues,
|
||||
type: 'select',
|
||||
},
|
||||
{
|
||||
name: 'provider_outpaint',
|
||||
title: 'Expand Canvas (Outpaint):',
|
||||
value: ls_get('provider_outpaint', remote.overrides?.outpaint || ''),
|
||||
values: ['', 'openai', 'invokeai', 'comfyui', 'replicate'],
|
||||
values: providerValues,
|
||||
type: 'select',
|
||||
},
|
||||
// ── OpenAI ────────────────────────────────────────────────
|
||||
@@ -131,8 +151,12 @@ class Tools_ai_provider_settings_class {
|
||||
title: 'Replicate API key:',
|
||||
value: ls_get('replicate_key'),
|
||||
placeholder: 'r8_...',
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
this.POP.show({
|
||||
title: 'AI Provider Settings',
|
||||
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) {
|
||||
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 '<tr>' +
|
||||
'<td style="color:#aaa;padding:2px 8px 2px 0;white-space:nowrap">' + op + '</td>' +
|
||||
'<td style="color:#ddd">' + modelName + '</td>' +
|
||||
'<td style="color:#888;padding-left:8px;font-size:10px">' + s.memory_opt + '</td>' +
|
||||
'</tr>';
|
||||
})
|
||||
.join('');
|
||||
|
||||
var warnHtml = (g.warnings || []).length
|
||||
? '<div style="color:#ffaa44;margin-top:6px;font-size:10px">' +
|
||||
g.warnings.map(function(w) { return '⚠ ' + w; }).join('<br>') + '</div>'
|
||||
: '';
|
||||
|
||||
return '<div style="background:#1a2a1a;border:1px solid #2a4a2a;border-radius:6px;padding:10px;font-size:11px;font-family:monospace">' +
|
||||
'<div style="color:#44cc44;font-size:12px;margin-bottom:6px">⬛ ' + (g.device_name || 'GPU') + '</div>' +
|
||||
'<div style="color:#aaa">VRAM: <span style="color:#ddd">' + g.vram_total_gb + ' GB total · ' + g.vram_free_gb + ' GB free</span></div>' +
|
||||
'<div style="color:#aaa">Compute: <span style="color:#ddd">CC ' + g.compute_capability + '</span>' +
|
||||
(flags ? ' <span style="color:#888">' + flags + '</span>' : '') + '</div>' +
|
||||
'<div style="color:#aaa">Effective: <span style="color:#ddd">' + g.effective_vram_gb + ' GB</span>' +
|
||||
' Tier: <span style="color:#44cc44">' + g.tier + '</span></div>' +
|
||||
(rows ? '<div style="color:#aaa;margin-top:8px">Models selected:</div>' +
|
||||
'<table style="width:100%;margin-top:3px">' + rows + '</table>' : '') +
|
||||
warnHtml +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
export default Tools_ai_provider_settings_class;
|
||||
|
||||
@@ -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<Object|null>}
|
||||
*/
|
||||
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<boolean>}
|
||||
|
||||
Reference in New Issue
Block a user