Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/beta-gate.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ the same change.
|---|---|---|---|
| G1 | **Runs on a current ComfyUI.** Every runnable preset's nodes and inputs match a recorded `/object_info` from the newest ComfyUI release, and a test fails when they drift. | Roadmap U2: a snapshot in the repo, a test of every preset's `requiredNodes` against it, and a script that diffs a live ComfyUI against the snapshot. Its output against a current ComfyUI is pasted in the PR. | **Open.** Every preset has been checked only against ComfyUI 0.37.0, the version on the test machine. ComfyUI 0.40 was out by 2026-10-13. |
| G2 | **Finds Comfy Desktop.** The port scan includes Comfy Desktop's default port. | A test on `COMFY_PORT_CANDIDATES`, plus Settings › Find ComfyUI Active Port against a ComfyUI started on that port. | **Done in v0.41.** 8000 is scanned second, right after 8188, with a test. Not yet run against a real Comfy Desktop: on the test machine a ComfyUI on 8188 always answers first. A clean-install tester (G6) using Comfy Desktop checks it for real. |
| G3 | **Says so when there is no GPU.** When ComfyUI reports only a CPU device, the panel says which tools will be too slow to use, rather than failing or hanging. | A unit test with a CPU-only `/system_stats` fixture, plus one run against ComfyUI started with `--cpu`. | **Open.** Nothing in the code handles a CPU-only ComfyUI. |
| G3 | **Says so when there is no GPU.** When ComfyUI reports only a CPU device, the panel says which tools will be too slow to use, rather than failing or hanging. | A unit test with a CPU-only `/system_stats` fixture, plus one run against ComfyUI started with `--cpu`. | **Done in v0.41.** Check ComfyUI says "ComfyUI is online, but on CPU only." with the reason underneath, and Detect Hardware reports "Processor only, no graphics card", VRAM none, tier No GPU. A CPU device no longer counts its system RAM as VRAM. Checked in Photoshop 2025 on 2026-11-04 against a real Comfy Desktop instance started with `--cpu` (ComfyUI 0.38.1, port 8189), and against a GPU ComfyUI 0.44.0 on 8188, which still reports normally. |
| G4 | **Passes the full smoke checklist on the release build.** All blocks of [the UXP smoke checklist](uxp-smoke-checklist.md) on the build that will be tagged, with a row in its Results table. | The Results table. | **Open.** The only recorded full run is from v0.37.1. v0.38 to v0.40 were checked per feature, in their own PRs. |
| G5 | **Works on Photoshop 2024.** Someone other than the author runs blocks 0 to 2 of the smoke checklist on Photoshop 2024 with the beta candidate. | Their report, linked here. | **Open.** One tester confirmed the panel works on Photoshop 2024 with v0.37.1 ([issue 127](https://github.com/MehranMarxian/OpenLayer/issues/127)). Nothing newer has been run on 2024. |
| G6 | **A first image on a clean install.** From an empty ComfyUI to a first layer by following [Getting started](getting-started.md), on a machine that is not the author's. | The tester's step count and any place they got stuck, linked here. | **Open.** v0.39 timed it at seven clicks on the author's machine only. |
Expand Down
44 changes: 38 additions & 6 deletions src/comfy/hardwareAdvisor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,26 +45,35 @@ const FAMILY_LABELS: Record<ModelFamily, string> = {
unknown: "Unknown"
};

/**
* Plain text: shown by Check ComfyUI and Detect Hardware through textContent.
* It names the cause and what to check rather than a list of tools, because
* no tool has been timed on a CPU-only ComfyUI.
*/
export const CPU_ONLY_NOTE =
"ComfyUI is running on the processor only, with no graphics card. Every tool that generates an image will take minutes per picture instead of seconds, and the large models may not run at all. If this computer has an NVIDIA, AMD or Apple graphics card, ComfyUI was probably started with its CPU-only option or without GPU support.";

export function createHardwareRecommendationReport(
systemStats: ComfySystemStats,
inventory: ComfyModelInventory,
presets: WorkflowPresetDefinition[]
): HardwareRecommendationReport {
const primaryDevice = choosePrimaryDevice(systemStats.devices);
const cpuOnly = isCpuOnlyComfy(systemStats);
const vramTotalBytes = getPrimaryDeviceVramTotalBytes(systemStats) ?? undefined;
const vramFreeBytes = primaryDevice?.vramFreeBytes ?? primaryDevice?.torchVramFreeBytes;
const vramFreeBytes = cpuOnly ? undefined : primaryDevice?.vramFreeBytes ?? primaryDevice?.torchVramFreeBytes;
const tier = getHardwareTier(vramTotalBytes);
const detectedFamilies = summarizeDetectedFamilies(inventory);
const zImageTurboMessage = getZImageTurboMessage(inventory);

return {
tier,
tierLabel: getHardwareTierLabel(tier),
deviceName: primaryDevice?.name ?? "No GPU reported by ComfyUI",
tierLabel: cpuOnly ? "No GPU" : getHardwareTierLabel(tier),
deviceName: cpuOnly ? "Processor only, no graphics card" : primaryDevice?.name ?? "No GPU reported by ComfyUI",
deviceType: primaryDevice?.type ?? "unknown",
vramTotalLabel: formatBytes(vramTotalBytes),
vramFreeLabel: formatBytes(vramFreeBytes),
summary: getTierSummary(tier),
vramTotalLabel: cpuOnly ? "none" : formatBytes(vramTotalBytes),
vramFreeLabel: cpuOnly ? "none" : formatBytes(vramFreeBytes),
summary: cpuOnly ? CPU_ONLY_NOTE : getTierSummary(tier),
detectedFamilies,
zImageTurboMessage,
recommendations: createRecommendations(tier, inventory, presets)
Expand Down Expand Up @@ -299,6 +308,21 @@ function getZImageTurboMessage(inventory: ComfyModelInventory) {
return "Z_image_Turbo was not detected in the diffusion model loader. It will not appear in the checkpoint selector because it is not a checkpoint model.";
}

/**
* Whether ComfyUI is generating on the processor alone: it reported at least
* one device and every one is type "cpu". That is what ComfyUI started with
* `--cpu`, or a PyTorch build without GPU support, reports. Apple's "mps" and
* every GPU backend are not CPU. Beta gate G3.
*/
export function isCpuOnlyComfy(systemStats: ComfySystemStats): boolean {
return systemStats.devices.length > 0
&& systemStats.devices.every((device) => device.type.toLowerCase() === "cpu");
}

function isCpuDevice(device: ComfyHardwareDevice | undefined) {
return device?.type.toLowerCase() === "cpu";
}

function choosePrimaryDevice(devices: ComfyHardwareDevice[]) {
return devices.find((device) => device.type.toLowerCase().includes("cuda"))
?? devices.find((device) => Boolean(device.vramTotalBytes ?? device.torchVramTotalBytes))
Expand All @@ -307,6 +331,14 @@ function choosePrimaryDevice(devices: ComfyHardwareDevice[]) {

export function getPrimaryDeviceVramTotalBytes(systemStats: ComfySystemStats): number | null {
const primaryDevice = choosePrimaryDevice(systemStats.devices);

// ComfyUI reports a CPU device's "vram_total" as the machine's system RAM.
// Read as VRAM it ranked a 32 GB CPU-only machine as a workstation card, and
// sized VRAM limits and Setup's outlook from it. There is no VRAM here.
if (isCpuDevice(primaryDevice)) {
return null;
}

return primaryDevice?.vramTotalBytes ?? primaryDevice?.torchVramTotalBytes ?? null;
}

Expand Down
37 changes: 26 additions & 11 deletions src/ui/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { findActiveComfyUrl } from "../comfy/comfyPortDiscovery";
import {
createHardwareRecommendationReport,
getPrimaryDeviceVramTotalBytes,
isCpuOnlyComfy,
formatHardwareReport,
HardwareRecommendationReport
} from "../comfy/hardwareAdvisor";
Expand Down Expand Up @@ -226,7 +227,7 @@ import { setSeedDiceEnabled } from "./seedDice";
import { enhanceSelectAsPicker, PICKER_SYNC_EVENT } from "./wheelProofPicker";
import { pickStarterDefault } from "./starterDefaults";
import { shouldRefreshToolModels } from "./setupInventoryChange";
import { getComfyFoundStatus, getComfyOnlineStatus, getNoCheckpointsDiagnostics } from "./comfyCheckStatus";
import { getComfyCheckDiagnostics, getComfyFoundStatus, getComfyOnlineStatus } from "./comfyCheckStatus";
import { bindPromptMemory } from "./promptMemory";
import { bindPromptWallet, PROMPT_WALLET_TOOLS, PromptWalletTool } from "./promptWallet";
import {
Expand Down Expand Up @@ -2467,8 +2468,9 @@ export function renderApp(rootElement: HTMLElement) {
updateOutpaintCheckpointCompatibility(elements, outpaintSource);
updateUpscaleCompatibility(elements, upscaleSource);
updateStyleReferenceCheckpointCompatibility(elements, styleReferenceSource);
setGlobalStatus(elements, getComfyOnlineStatus(checkpointCount), "ready");
setNoCheckpointsDiagnostics(checkpointCount);
const cpuOnly = await readCpuOnly(client);
setGlobalStatus(elements, getComfyOnlineStatus(checkpointCount, cpuOnly), "ready");
setComfyCheckDiagnostics(checkpointCount, cpuOnly);
savePreferencesFromElements(elements);
updateSettingsReport(elements);
} catch (caughtError) {
Expand Down Expand Up @@ -2577,16 +2579,27 @@ export function renderApp(rootElement: HTMLElement) {
}
}

// Only when there is something to say: a check that found checkpoints keeps
// whatever the diagnostics line already reads ("Check ComfyUI pressed.").
function setNoCheckpointsDiagnostics(checkpointCount: number) {
const note = getNoCheckpointsDiagnostics(checkpointCount);
// Only when there is something to say: a check that found checkpoints on a
// GPU keeps whatever the diagnostics line already reads ("Check ComfyUI
// pressed.").
function setComfyCheckDiagnostics(checkpointCount: number, cpuOnly: boolean) {
const note = getComfyCheckDiagnostics(checkpointCount, cpuOnly);

if (note) {
setGlobalDiagnostics(elements, note);
}
}

// A failed read must not fail a check that has already succeeded: this only
// adds a warning, and without the stats there is nothing to warn about.
async function readCpuOnly(client: ComfyClient) {
try {
return isCpuOnlyComfy(await client.getSystemStats());
} catch {
return false;
}
}

async function handleCheckComfy() {
setGlobalDiagnostics(elements, "Check ComfyUI pressed.");
setGlobalError(elements, "");
Expand All @@ -2612,8 +2625,9 @@ export function renderApp(rootElement: HTMLElement) {
updateOutpaintCheckpointCompatibility(elements, outpaintSource);
updateUpscaleCompatibility(elements, upscaleSource);
updateStyleReferenceCheckpointCompatibility(elements, styleReferenceSource);
setGlobalStatus(elements, getComfyOnlineStatus(checkpointCount), "ready");
setNoCheckpointsDiagnostics(checkpointCount);
const cpuOnly = await readCpuOnly(client);
setGlobalStatus(elements, getComfyOnlineStatus(checkpointCount, cpuOnly), "ready");
setComfyCheckDiagnostics(checkpointCount, cpuOnly);
savePreferencesFromElements(elements);
updateSettingsReport(elements);
} catch (caughtError) {
Expand Down Expand Up @@ -2661,8 +2675,9 @@ export function renderApp(rootElement: HTMLElement) {
updateUpscaleCompatibility(elements, upscaleSource);
updateStyleReferenceCheckpointCompatibility(elements, styleReferenceSource);
savePreferencesFromElements(elements);
setGlobalStatus(elements, getComfyFoundStatus(foundUrl, checkpointCount), "ready");
setGlobalDiagnostics(elements, [`Active ComfyUI server selected: ${foundUrl}.`, getNoCheckpointsDiagnostics(checkpointCount)].filter(Boolean).join(" "));
const cpuOnly = await readCpuOnly(client);
setGlobalStatus(elements, cpuOnly ? getComfyOnlineStatus(checkpointCount, true) : getComfyFoundStatus(foundUrl, checkpointCount), "ready");
setGlobalDiagnostics(elements, [`Active ComfyUI server selected: ${foundUrl}.`, getComfyCheckDiagnostics(checkpointCount, cpuOnly)].filter(Boolean).join(" "));
} catch (caughtError) {
setGlobalStatus(elements, `Found ${foundUrl}, but model loading failed.`, "error");
setGlobalError(elements, getErrorMessage(caughtError));
Expand Down
23 changes: 21 additions & 2 deletions src/ui/comfyCheckStatus.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { CPU_ONLY_NOTE } from "../comfy/hardwareAdvisor";

/**
* What the panel says once ComfyUI has answered and the model lists are read.
*
Expand Down Expand Up @@ -27,8 +29,19 @@ export const COMFY_ONLINE_NO_CHECKPOINTS_STATUS = "ComfyUI is online. No SD chec

export const NO_CHECKPOINTS_NOTE = "FLUX.2 Klein needs no checkpoint; only SD 1.5 and SDXL workflows do.";

/** The status line after a successful check, given how many checkpoints ComfyUI listed. */
export function getComfyOnlineStatus(checkpointCount: number): string {
/** 35 characters, inside the status bar's ~48; the reason goes on the line below. */
export const COMFY_ONLINE_CPU_ONLY_STATUS = "ComfyUI is online, but on CPU only.";

/**
* The status line after a successful check, given how many checkpoints ComfyUI
* listed and whether it runs on the processor alone. CPU-only wins: it decides
* whether any tool is usable at all, which an empty checkpoints folder does not.
*/
export function getComfyOnlineStatus(checkpointCount: number, cpuOnly = false): string {
if (cpuOnly) {
return COMFY_ONLINE_CPU_ONLY_STATUS;
}

return checkpointCount > 0 ? COMFY_ONLINE_STATUS : COMFY_ONLINE_NO_CHECKPOINTS_STATUS;
}

Expand All @@ -41,3 +54,9 @@ export function getComfyFoundStatus(url: string, _checkpointCount: number): stri
export function getNoCheckpointsDiagnostics(checkpointCount: number): string | null {
return checkpointCount > 0 ? null : NO_CHECKPOINTS_NOTE;
}

/** Every note a finished check has to add, CPU-only first, or null for none. */
export function getComfyCheckDiagnostics(checkpointCount: number, cpuOnly: boolean): string | null {
const notes = [cpuOnly ? CPU_ONLY_NOTE : null, getNoCheckpointsDiagnostics(checkpointCount)].filter(Boolean);
return notes.length > 0 ? notes.join(" ") : null;
}
110 changes: 110 additions & 0 deletions tests/comfy/cpuOnlyComfy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { describe, expect, it } from "vitest";
import {
CPU_ONLY_NOTE,
createHardwareRecommendationReport,
formatHardwareReport,
getPrimaryDeviceVramTotalBytes,
isCpuOnlyComfy
} from "../../src/comfy/hardwareAdvisor";
import { listWorkflowPresets } from "../../src/comfy/presetRegistry";
import type { ComfyModelInventory, ComfySystemStats } from "../../src/comfy/types";
import {
COMFY_ONLINE_CPU_ONLY_STATUS,
COMFY_ONLINE_STATUS,
getComfyCheckDiagnostics,
getComfyOnlineStatus,
NO_CHECKPOINTS_NOTE
} from "../../src/ui/comfyCheckStatus";

const GB = 1024 ** 3;

/**
* What ComfyUI started with --cpu reports, as readComfySystemStats parses it:
* one device named and typed "cpu", whose "vram_total" is the machine's system
* RAM (ComfyUI's get_total_memory returns psutil's total for a CPU device).
*/
function cpuOnlyStats(ramGb = 32): ComfySystemStats {
return {
system: { os: "nt", ramTotalBytes: ramGb * GB, ramFreeBytes: 20 * GB },
devices: [{ name: "cpu", type: "cpu", vramTotalBytes: ramGb * GB, vramFreeBytes: 20 * GB }]
} as ComfySystemStats;
}

function gpuStats(type = "cuda", vramGb = 12): ComfySystemStats {
return {
system: { os: "nt", ramTotalBytes: 32 * GB, ramFreeBytes: 20 * GB },
devices: [{ name: "NVIDIA GeForce RTX 4070 Ti", type, vramTotalBytes: vramGb * GB, vramFreeBytes: 10 * GB }]
} as ComfySystemStats;
}

const emptyInventory = {
checkpoints: [],
diffusionModels: [],
clipModels: [],
vaeModels: [],
controlNetModels: [],
modelPatches: [],
upscaleModels: [],
loras: [],
visionLanguageModels: [],
backgroundRemovalModels: [],
inpaintModels: [],
loraModels: [],
missingSources: []
} as unknown as ComfyModelInventory;

describe("isCpuOnlyComfy", () => {
it("is true when every device ComfyUI reports is the processor", () => {
expect(isCpuOnlyComfy(cpuOnlyStats())).toBe(true);
});

it("is false for CUDA, Apple MPS and other GPU backends", () => {
for (const type of ["cuda", "mps", "xpu", "privateuseone"]) {
expect(isCpuOnlyComfy(gpuStats(type)), type).toBe(false);
}
});

it("is false when ComfyUI reports no devices at all, which says nothing either way", () => {
expect(isCpuOnlyComfy({ ...cpuOnlyStats(), devices: [] })).toBe(false);
});
});

describe("CPU-only VRAM", () => {
it("does not read system RAM as VRAM", () => {
// Before beta gate G3, a 32 GB CPU-only machine came back as 32 GB of VRAM:
// a workstation card, and the size every VRAM limit was scaled from.
expect(getPrimaryDeviceVramTotalBytes(cpuOnlyStats(32))).toBeNull();
});

it("still reads a real card's VRAM", () => {
expect(getPrimaryDeviceVramTotalBytes(gpuStats("cuda", 12))).toBe(12 * GB);
});

it("reports no GPU instead of a workstation tier", () => {
const report = createHardwareRecommendationReport(cpuOnlyStats(64), emptyInventory, listWorkflowPresets());

expect(report.tier).toBe("unknown");
expect(report.tierLabel).toBe("No GPU");
expect(report.vramTotalLabel).toBe("none");
expect(formatHardwareReport(report)).toContain(CPU_ONLY_NOTE);
expect(formatHardwareReport(report)).not.toMatch(/Workstation/);
});
});

describe("Check ComfyUI on a CPU-only ComfyUI", () => {
it("says CPU only on the status line, inside the bar's 48 characters", () => {
expect(getComfyOnlineStatus(5, true)).toBe(COMFY_ONLINE_CPU_ONLY_STATUS);
expect(COMFY_ONLINE_CPU_ONLY_STATUS.length).toBeLessThanOrEqual(48);
expect(getComfyOnlineStatus(5, false)).toBe(COMFY_ONLINE_STATUS);
});

it("puts the reason on the diagnostics line, before the no-checkpoints note", () => {
expect(getComfyCheckDiagnostics(0, true)).toBe(`${CPU_ONLY_NOTE} ${NO_CHECKPOINTS_NOTE}`);
expect(getComfyCheckDiagnostics(5, true)).toBe(CPU_ONLY_NOTE);
expect(getComfyCheckDiagnostics(5, false)).toBeNull();
});

it("keeps the note plain text, as the panel renders it with textContent", () => {
expect(CPU_ONLY_NOTE).not.toMatch(/`|--|\*\*|Photoshop plugin/);
});
});
Loading