Add a Sleep GPU button that stops Comfy to free VRAM.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -10,6 +10,36 @@
|
|||||||
<button type="button" class="rounded-full border border-white/10 px-3 py-1 text-sm text-zinc-400 hover:text-white" @click="emit('close')">Close</button>
|
<button type="button" class="rounded-full border border-white/10 px-3 py-1 text-sm text-zinc-400 hover:text-white" @click="emit('close')">Close</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<section class="mt-6">
|
||||||
|
<h3 class="text-sm font-semibold uppercase tracking-wider text-zinc-500">GPU</h3>
|
||||||
|
<div class="mt-2 space-y-3 rounded-2xl border border-white/10 bg-zinc-950/50 px-4 py-3">
|
||||||
|
<p class="text-sm text-zinc-400">
|
||||||
|
Stops Comfy on the desktop GPU so it drops out of VRAM. The PC stays on. Generate or Wake GPU will start it again.
|
||||||
|
</p>
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-xl border px-3 py-2 text-sm disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
|
:class="gpuSleeping ? 'border-amber-300/40 text-amber-100' : 'border-white/15 text-zinc-200 hover:border-amber-300/50 hover:text-white'"
|
||||||
|
:disabled="gpuSleeping || gpuWaking"
|
||||||
|
@click="sleepGpu"
|
||||||
|
>
|
||||||
|
{{ gpuSleeping ? 'Stopping GPU…' : 'Sleep GPU' }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-xl border px-3 py-2 text-sm disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
|
:class="gpuWaking ? 'border-amber-300/40 text-amber-100' : 'border-amber-400/40 text-amber-200 hover:border-amber-300 hover:text-amber-50'"
|
||||||
|
:disabled="gpuWaking || gpuSleeping"
|
||||||
|
@click="wakeGpu"
|
||||||
|
>
|
||||||
|
{{ gpuWaking ? 'Starting GPU…' : 'Wake GPU' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p v-if="gpuMessage" class="text-xs" :class="gpuError ? 'text-red-300' : 'text-zinc-500'">{{ gpuMessage }}</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="mt-6">
|
<section class="mt-6">
|
||||||
<h3 class="text-sm font-semibold uppercase tracking-wider text-zinc-500">Storage</h3>
|
<h3 class="text-sm font-semibold uppercase tracking-wider text-zinc-500">Storage</h3>
|
||||||
<div class="mt-2 rounded-2xl border border-white/10 bg-zinc-950/50 px-4 py-3">
|
<div class="mt-2 rounded-2xl border border-white/10 bg-zinc-950/50 px-4 py-3">
|
||||||
@@ -256,6 +286,10 @@ const loginNext = ref('')
|
|||||||
const drafts = reactive<Record<string, { name: string; protect: boolean; password: string; currentPassword: string }>>({})
|
const drafts = reactive<Record<string, { name: string; protect: boolean; password: string; currentPassword: string }>>({})
|
||||||
const storageLoading = ref(true)
|
const storageLoading = ref(true)
|
||||||
const storageError = ref('')
|
const storageError = ref('')
|
||||||
|
const gpuSleeping = ref(false)
|
||||||
|
const gpuWaking = ref(false)
|
||||||
|
const gpuError = ref(false)
|
||||||
|
const gpuMessage = ref('')
|
||||||
const storage = ref<{
|
const storage = ref<{
|
||||||
libraryBytes: number
|
libraryBytes: number
|
||||||
breakdown: { clips: number; stills: number; tmp: number; other: number }
|
breakdown: { clips: number; stills: number; tmp: number; other: number }
|
||||||
@@ -392,6 +426,38 @@ async function loadStorage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function sleepGpu() {
|
||||||
|
if (gpuSleeping.value || gpuWaking.value) return
|
||||||
|
gpuSleeping.value = true
|
||||||
|
gpuError.value = false
|
||||||
|
gpuMessage.value = 'Stopping Comfy on the GPU…'
|
||||||
|
try {
|
||||||
|
await $fetch('/api/comfy/sleep', { method: 'POST' })
|
||||||
|
gpuMessage.value = 'Comfy is stopped. VRAM is free until you generate or wake the GPU.'
|
||||||
|
} catch (error: any) {
|
||||||
|
gpuError.value = true
|
||||||
|
gpuMessage.value = error?.data?.statusMessage || error?.statusMessage || 'Could not stop Comfy.'
|
||||||
|
} finally {
|
||||||
|
gpuSleeping.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function wakeGpu() {
|
||||||
|
if (gpuWaking.value || gpuSleeping.value) return
|
||||||
|
gpuWaking.value = true
|
||||||
|
gpuError.value = false
|
||||||
|
gpuMessage.value = 'Starting Comfy on the GPU…'
|
||||||
|
try {
|
||||||
|
await $fetch('/api/comfy/wake', { method: 'POST' })
|
||||||
|
gpuMessage.value = 'Wake sent. The first job can still take a couple of minutes if Comfy is cold.'
|
||||||
|
} catch (error: any) {
|
||||||
|
gpuError.value = true
|
||||||
|
gpuMessage.value = error?.data?.statusMessage || error?.statusMessage || 'Could not wake the GPU.'
|
||||||
|
} finally {
|
||||||
|
gpuWaking.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadLoraKeys() {
|
async function loadLoraKeys() {
|
||||||
loraRowsLoading.value = true
|
loraRowsLoading.value = true
|
||||||
try {
|
try {
|
||||||
|
|||||||
+42
-2
@@ -30,6 +30,17 @@
|
|||||||
>
|
>
|
||||||
{{ gpuWaking ? 'Starting GPU…' : 'Wake GPU' }}
|
{{ gpuWaking ? 'Starting GPU…' : 'Wake GPU' }}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="showSleepGpu"
|
||||||
|
type="button"
|
||||||
|
class="rounded-full border px-3 py-1 hover:border-amber-300/50 hover:text-white disabled:cursor-not-allowed disabled:opacity-80"
|
||||||
|
:class="gpuSleeping ? 'border-amber-300/40 text-amber-100' : 'border-white/15 text-zinc-300'"
|
||||||
|
:disabled="gpuSleeping || rigBusy"
|
||||||
|
:title="gpuWakeError || (rigBusy ? 'Cancel the running job first' : (gpuSleeping ? 'Stopping Comfy on the GPU' : 'Stop Comfy on the GPU to free VRAM. Does not shut the PC down.'))"
|
||||||
|
@click="sleepGpu"
|
||||||
|
>
|
||||||
|
{{ gpuSleeping ? 'Stopping GPU…' : 'Sleep GPU' }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<span class="hidden md:block text-zinc-400">{{ userName }}</span>
|
<span class="hidden md:block text-zinc-400">{{ userName }}</span>
|
||||||
<button type="button" class="rounded-full border border-white/10 px-3 py-1 text-zinc-300 hover:border-amber-300/50 hover:text-white" @click="settingsOpen = true">
|
<button type="button" class="rounded-full border border-white/10 px-3 py-1 text-zinc-300 hover:border-amber-300/50 hover:text-white" @click="settingsOpen = true">
|
||||||
@@ -2453,6 +2464,7 @@ const comfyOk = ref(false)
|
|||||||
const comfyBusy = ref(false)
|
const comfyBusy = ref(false)
|
||||||
const comfyAsleep = ref(false)
|
const comfyAsleep = ref(false)
|
||||||
const gpuWaking = ref(false)
|
const gpuWaking = ref(false)
|
||||||
|
const gpuSleeping = ref(false)
|
||||||
const gpuWakeError = ref('')
|
const gpuWakeError = ref('')
|
||||||
const userName = ref('')
|
const userName = ref('')
|
||||||
const authEnabled = ref(false)
|
const authEnabled = ref(false)
|
||||||
@@ -2726,8 +2738,17 @@ const liveMaxStep = computed(() => (
|
|||||||
const rigBusy = computed(() => (
|
const rigBusy = computed(() => (
|
||||||
comfyBusy.value || imageComfyBusy.value || videoBusy.value || editBusy.value
|
comfyBusy.value || imageComfyBusy.value || videoBusy.value || editBusy.value
|
||||||
))
|
))
|
||||||
const showWakeGpu = computed(() => gpuWaking.value || (!comfyOk.value && !imageComfyOk.value))
|
const showWakeGpu = computed(() => gpuWaking.value || (!gpuSleeping.value && !comfyOk.value && !imageComfyOk.value))
|
||||||
|
const showSleepGpu = computed(() => gpuSleeping.value || ((comfyOk.value || imageComfyOk.value) && !gpuWaking.value))
|
||||||
const rigBadge = computed(() => {
|
const rigBadge = computed(() => {
|
||||||
|
if (gpuSleeping.value) {
|
||||||
|
return {
|
||||||
|
label: 'Stopping…',
|
||||||
|
className: 'border-amber-400/40 text-amber-200',
|
||||||
|
dot: 'bg-amber-400 animate-pulse',
|
||||||
|
title: 'Stopping Comfy on the GPU'
|
||||||
|
}
|
||||||
|
}
|
||||||
if (gpuWaking.value) {
|
if (gpuWaking.value) {
|
||||||
return {
|
return {
|
||||||
label: 'Starting…',
|
label: 'Starting…',
|
||||||
@@ -2857,6 +2878,7 @@ const recommendHint = computed(() => {
|
|||||||
return 'Send this still and draft to Beast Qwen VL.'
|
return 'Send this still and draft to Beast Qwen VL.'
|
||||||
})
|
})
|
||||||
const gpuSleepHint = computed(() => {
|
const gpuSleepHint = computed(() => {
|
||||||
|
if (gpuSleeping.value) return 'Stopping Comfy on the GPU to free VRAM. The PC stays on.'
|
||||||
if (comfyOk.value || imageComfyOk.value) return ''
|
if (comfyOk.value || imageComfyOk.value) return ''
|
||||||
if (gpuWaking.value) return 'Starting Comfy on the GPU. Generate will wait in the job queue until it answers.'
|
if (gpuWaking.value) return 'Starting Comfy on the GPU. Generate will wait in the job queue until it answers.'
|
||||||
if (comfyAsleep.value) return 'GPU is asleep. Generate will wake it — the first job can take a few minutes. Or click Wake GPU to pre-warm.'
|
if (comfyAsleep.value) return 'GPU is asleep. Generate will wake it — the first job can take a few minutes. Or click Wake GPU to pre-warm.'
|
||||||
@@ -3528,7 +3550,7 @@ async function pollComfyHealth() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function pokeGpu() {
|
async function pokeGpu() {
|
||||||
if (gpuWaking.value) return
|
if (gpuWaking.value || gpuSleeping.value) return
|
||||||
gpuWaking.value = true
|
gpuWaking.value = true
|
||||||
gpuWakeError.value = ''
|
gpuWakeError.value = ''
|
||||||
try {
|
try {
|
||||||
@@ -3550,6 +3572,24 @@ async function pokeGpu() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function sleepGpu() {
|
||||||
|
if (gpuSleeping.value || gpuWaking.value || rigBusy.value) return
|
||||||
|
gpuSleeping.value = true
|
||||||
|
gpuWakeError.value = ''
|
||||||
|
try {
|
||||||
|
await $fetch('/api/comfy/sleep', { method: 'POST' })
|
||||||
|
comfyOk.value = false
|
||||||
|
imageComfyOk.value = false
|
||||||
|
imageBeastOk.value = false
|
||||||
|
comfyAsleep.value = true
|
||||||
|
await pollComfyHealth()
|
||||||
|
} catch (error: any) {
|
||||||
|
gpuWakeError.value = error?.data?.statusMessage || error?.statusMessage || error?.message || 'Could not stop Comfy.'
|
||||||
|
} finally {
|
||||||
|
gpuSleeping.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
withSound.value = localStorage.getItem('aigen-generate-sound') !== 'false'
|
withSound.value = localStorage.getItem('aigen-generate-sound') !== 'false'
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { listJobs } from '~/server/utils/jobs'
|
||||||
|
import { listOwnersWithStudioQueues, listStudioJobs } from '~/server/utils/studioQueue'
|
||||||
|
import { requestComfySleep } from '~/server/utils/comfyLifecycle'
|
||||||
|
|
||||||
|
export default defineEventHandler(async () => {
|
||||||
|
const live = listJobs().filter(job => job.status === 'queued' || job.status === 'uploading' || job.status === 'running')
|
||||||
|
if (live.length) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 409,
|
||||||
|
statusMessage: 'A job is still running. Cancel it first, then sleep the GPU.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for (const owner of listOwnersWithStudioQueues()) {
|
||||||
|
if (listStudioJobs(owner).some(job => job.status === 'running')) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 409,
|
||||||
|
statusMessage: 'A job is still running. Cancel it first, then sleep the GPU.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return await requestComfySleep()
|
||||||
|
})
|
||||||
@@ -81,7 +81,7 @@ export async function fetchQueue() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function controlRequest(path: string, method = 'GET') {
|
async function controlRequest(path: string, method = 'GET', timeoutMs = 5000) {
|
||||||
const { controlUrl, controlToken } = settings()
|
const { controlUrl, controlToken } = settings()
|
||||||
if (!controlUrl) return null
|
if (!controlUrl) return null
|
||||||
try {
|
try {
|
||||||
@@ -91,10 +91,9 @@ async function controlRequest(path: string, method = 'GET') {
|
|||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
...(controlToken ? { Authorization: `Bearer ${controlToken}` } : {})
|
...(controlToken ? { Authorization: `Bearer ${controlToken}` } : {})
|
||||||
},
|
},
|
||||||
signal: AbortSignal.timeout(5000)
|
signal: AbortSignal.timeout(timeoutMs)
|
||||||
})
|
})
|
||||||
if (!res.ok) return { ok: false, status: res.status }
|
const body = await res.json().catch(() => null) as {
|
||||||
return await res.json() as {
|
|
||||||
ok?: boolean
|
ok?: boolean
|
||||||
http?: boolean
|
http?: boolean
|
||||||
process?: boolean
|
process?: boolean
|
||||||
@@ -108,9 +107,23 @@ async function controlRequest(path: string, method = 'GET') {
|
|||||||
port?: number | null
|
port?: number | null
|
||||||
proxyPort?: number | null
|
proxyPort?: number | null
|
||||||
stopped?: boolean
|
stopped?: boolean
|
||||||
|
killed?: unknown
|
||||||
error?: string
|
error?: string
|
||||||
|
running?: number
|
||||||
|
pending?: number
|
||||||
status?: number
|
status?: number
|
||||||
|
} | null
|
||||||
|
if (!res.ok) {
|
||||||
|
return {
|
||||||
|
ok: false as const,
|
||||||
|
status: res.status,
|
||||||
|
error: body?.error,
|
||||||
|
running: body?.running,
|
||||||
|
pending: body?.pending,
|
||||||
|
killed: body?.killed
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return body || { ok: true }
|
||||||
} catch {
|
} catch {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -151,6 +164,43 @@ export async function requestComfyWake() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function requestComfySleep() {
|
||||||
|
const { controlUrl } = settings()
|
||||||
|
if (!controlUrl) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 503,
|
||||||
|
statusMessage: 'COMFY_CONTROL_URL is not set. The studio cannot poke the Beast host agent.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const remote = await controlRequest('/stop', 'POST', 25_000)
|
||||||
|
if (!remote) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 503,
|
||||||
|
statusMessage: 'Beast host agent is unreachable. Confirm it is running on the desktop.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (remote.status === 409 || remote.error === 'queue-busy') {
|
||||||
|
const running = Number(remote.running || 0)
|
||||||
|
const pending = Number(remote.pending || 0)
|
||||||
|
throw createError({
|
||||||
|
statusCode: 409,
|
||||||
|
statusMessage: `Comfy is still working (${running} running, ${pending} queued). Cancel the job first, then sleep the GPU.`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (remote.ok === false) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: Number(remote.status) || 502,
|
||||||
|
statusMessage: 'Beast host agent refused to stop Comfy.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
stopped: true,
|
||||||
|
asleep: true,
|
||||||
|
killed: remote.killed ?? null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function findLocalComfyProcess() {
|
async function findLocalComfyProcess() {
|
||||||
const platform = process.platform
|
const platform = process.platform
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user