diff --git a/assets/requirements/windows_cpu.compiled b/assets/requirements/windows_cpu.compiled new file mode 100644 index 00000000..c08b8b04 --- /dev/null +++ b/assets/requirements/windows_cpu.compiled @@ -0,0 +1,340 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile C:\source\desktop\assets\ComfyUI\requirements.txt C:\source\desktop\assets\ComfyUI\custom_nodes\ComfyUI-Manager\requirements.txt --emit-index-annotation --emit-index-url --index-strategy unsafe-best-match --override C:\source\desktop\assets\override.txt -o C:\source\desktop\assets\requirements.cpu.compiled +--index-url https://pypi.org/simple + +aiohappyeyeballs==2.4.3 + # via + # --override assets/override.txt + # aiohttp + # from https://pypi.org/simple +aiohttp==3.11.7 + # via + # --override assets/override.txt + # -r assets/ComfyUI/requirements.txt + # from https://pypi.org/simple +aiosignal==1.3.1 + # via + # --override assets/override.txt + # aiohttp + # from https://pypi.org/simple +attrs==24.2.0 + # via + # --override assets/override.txt + # aiohttp + # from https://pypi.org/simple +certifi==2024.8.30 + # via + # --override assets/override.txt + # requests + # from https://pypi.org/simple +cffi==1.17.1 + # via + # --override assets/override.txt + # cryptography + # pynacl + # soundfile + # from https://pypi.org/simple +charset-normalizer==3.4.0 + # via + # --override assets/override.txt + # requests + # from https://pypi.org/simple +click==8.1.7 + # via typer + # from https://pypi.org/simple +colorama==0.4.6 + # via + # --override assets/override.txt + # click + # tqdm + # from https://pypi.org/simple +cryptography==44.0.0 + # via pyjwt + # from https://pypi.org/simple +deprecated==1.2.15 + # via pygithub + # from https://pypi.org/simple +einops==0.8.0 + # via + # --override assets/override.txt + # -r assets/ComfyUI/requirements.txt + # spandrel + # from https://pypi.org/simple +filelock==3.16.1 + # via + # --override assets/override.txt + # huggingface-hub + # torch + # transformers + # from https://pypi.org/simple +frozenlist==1.5.0 + # via + # --override assets/override.txt + # aiohttp + # aiosignal + # from https://pypi.org/simple +fsspec==2024.10.0 + # via + # --override assets/override.txt + # huggingface-hub + # torch + # from https://pypi.org/simple +gitdb==4.0.11 + # via gitpython + # from https://pypi.org/simple +gitpython==3.1.43 + # via -r assets/ComfyUI/custom_nodes/ComfyUI-Manager/requirements.txt + # from https://pypi.org/simple +huggingface-hub==0.26.2 + # via + # --override assets/override.txt + # -r assets/ComfyUI/custom_nodes/ComfyUI-Manager/requirements.txt + # tokenizers + # transformers + # from https://pypi.org/simple +idna==3.10 + # via + # --override assets/override.txt + # requests + # yarl + # from https://pypi.org/simple +jinja2==3.1.4 + # via + # --override assets/override.txt + # torch + # from https://pypi.org/simple +kornia==0.7.4 + # via + # --override assets/override.txt + # -r assets/ComfyUI/requirements.txt + # from https://pypi.org/simple +kornia-rs==0.1.7 + # via + # --override assets/override.txt + # kornia + # from https://pypi.org/simple +markdown-it-py==3.0.0 + # via rich + # from https://pypi.org/simple +markupsafe==3.0.2 + # via + # --override assets/override.txt + # jinja2 + # from https://pypi.org/simple +mdurl==0.1.2 + # via markdown-it-py + # from https://pypi.org/simple +mpmath==1.3.0 + # via + # --override assets/override.txt + # sympy + # from https://pypi.org/simple +multidict==6.1.0 + # via + # --override assets/override.txt + # aiohttp + # yarl + # from https://pypi.org/simple +networkx==3.4.2 + # via + # --override assets/override.txt + # torch + # from https://pypi.org/simple +numpy==2.1.3 + # via + # --override assets/override.txt + # scipy + # spandrel + # torchsde + # torchvision + # transformers + # from https://pypi.org/simple +packaging==24.2 + # via + # --override assets/override.txt + # huggingface-hub + # kornia + # transformers + # from https://pypi.org/simple +pillow==11.0.0 + # via + # --override assets/override.txt + # -r assets/ComfyUI/requirements.txt + # torchvision + # from https://pypi.org/simple +propcache==0.2.0 + # via + # --override assets/override.txt + # aiohttp + # yarl + # from https://pypi.org/simple +psutil==6.1.0 + # via + # --override assets/override.txt + # -r assets/ComfyUI/requirements.txt + # from https://pypi.org/simple +pycparser==2.22 + # via + # --override assets/override.txt + # cffi + # from https://pypi.org/simple +pygithub==2.5.0 + # via -r assets/ComfyUI/custom_nodes/ComfyUI-Manager/requirements.txt + # from https://pypi.org/simple +pygments==2.18.0 + # via rich + # from https://pypi.org/simple +pyjwt==2.10.1 + # via pygithub + # from https://pypi.org/simple +pynacl==1.5.0 + # via pygithub + # from https://pypi.org/simple +pyyaml==6.0.2 + # via + # --override assets/override.txt + # -r assets/ComfyUI/requirements.txt + # huggingface-hub + # transformers + # from https://pypi.org/simple +regex==2024.11.6 + # via + # --override assets/override.txt + # transformers + # from https://pypi.org/simple +requests==2.32.3 + # via + # --override assets/override.txt + # huggingface-hub + # pygithub + # transformers + # from https://pypi.org/simple +rich==13.9.4 + # via + # -r assets/ComfyUI/custom_nodes/ComfyUI-Manager/requirements.txt + # typer + # from https://pypi.org/simple +safetensors==0.4.5 + # via + # --override assets/override.txt + # -r assets/ComfyUI/requirements.txt + # spandrel + # transformers + # from https://pypi.org/simple +scipy==1.14.1 + # via + # --override assets/override.txt + # -r assets/ComfyUI/requirements.txt + # torchsde + # from https://pypi.org/simple +sentencepiece==0.2.0 + # via + # --override assets/override.txt + # -r assets/ComfyUI/requirements.txt + # from https://pypi.org/simple +setuptools==75.6.0 + # via + # --override assets/override.txt + # torch + # from https://pypi.org/simple +shellingham==1.5.4 + # via typer + # from https://pypi.org/simple +smmap==5.0.1 + # via gitdb + # from https://pypi.org/simple +soundfile==0.12.1 + # via + # --override assets/override.txt + # -r assets/ComfyUI/requirements.txt + # from https://pypi.org/simple +spandrel==0.4.0 + # via + # --override assets/override.txt + # -r assets/ComfyUI/requirements.txt + # from https://pypi.org/simple +sympy==1.13.1 + # via + # --override assets/override.txt + # torch + # from https://pypi.org/simple +tokenizers==0.20.3 + # via + # --override assets/override.txt + # -r assets/ComfyUI/requirements.txt + # transformers + # from https://pypi.org/simple +torch==2.5.1 + # via + # --override assets/override.txt + # -r assets/ComfyUI/requirements.txt + # kornia + # spandrel + # torchaudio + # torchsde + # torchvision + # from https://pypi.org/simple +torchaudio==2.5.1 + # via + # --override assets/override.txt + # -r assets/ComfyUI/requirements.txt + # from https://pypi.org/simple +torchsde==0.2.6 + # via + # --override assets/override.txt + # -r assets/ComfyUI/requirements.txt + # from https://pypi.org/simple +torchvision==0.20.1 + # via + # --override assets/override.txt + # -r assets/ComfyUI/requirements.txt + # spandrel + # from https://pypi.org/simple +tqdm==4.67.1 + # via + # --override assets/override.txt + # -r assets/ComfyUI/requirements.txt + # huggingface-hub + # transformers + # from https://pypi.org/simple +trampoline==0.1.2 + # via + # --override assets/override.txt + # torchsde + # from https://pypi.org/simple +transformers==4.46.3 + # via + # --override assets/override.txt + # -r assets/ComfyUI/custom_nodes/ComfyUI-Manager/requirements.txt + # -r assets/ComfyUI/requirements.txt + # from https://pypi.org/simple +typer==0.13.1 + # via -r assets/ComfyUI/custom_nodes/ComfyUI-Manager/requirements.txt + # from https://pypi.org/simple +typing-extensions==4.12.2 + # via + # --override assets/override.txt + # -r assets/ComfyUI/custom_nodes/ComfyUI-Manager/requirements.txt + # gitpython + # huggingface-hub + # multidict + # pygithub + # rich + # spandrel + # torch + # typer + # from https://pypi.org/simple +urllib3==2.2.3 + # via + # --override assets/override.txt + # pygithub + # requests + # from https://pypi.org/simple +wrapt==1.17.0 + # via deprecated + # from https://pypi.org/simple +yarl==1.18.0 + # via + # --override assets/override.txt + # aiohttp + # from https://pypi.org/simple diff --git a/src/constants.ts b/src/constants.ts index 7bcfac71..863064ff 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -31,6 +31,8 @@ export const IPC_CHANNELS = { SHOW_DIRECTORY_PICKER: 'show-directory-picker', INSTALL_COMFYUI: 'install-comfyui', SHOW_CONTEXT_MENU: 'show-context-menu', + RESTART_CORE: 'restart-core', + GET_GPU: 'get-gpu', } as const; export enum ProgressStatus { diff --git a/src/install/installWizard.ts b/src/install/installWizard.ts index 824dd8d7..352238a0 100644 --- a/src/install/installWizard.ts +++ b/src/install/installWizard.ts @@ -63,6 +63,12 @@ export class InstallWizard { 'Comfy-Desktop.AutoUpdate': this.installOptions.autoUpdate, 'Comfy-Desktop.SendStatistics': this.installOptions.allowMetrics, }; + + if (this.installOptions.device === 'cpu') { + settings['Comfy.Server.LaunchArgs'] ??= {}; + settings['Comfy.Server.LaunchArgs']['cpu'] = ''; + } + const settingsJson = JSON.stringify(settings, null, 2); fs.writeFileSync(settingsPath, settingsJson); log.info(`Wrote settings to ${settingsPath}: ${settingsJson}`); diff --git a/src/main-process/comfyDesktopApp.ts b/src/main-process/comfyDesktopApp.ts index cdc8097f..4135db32 100644 --- a/src/main-process/comfyDesktopApp.ts +++ b/src/main-process/comfyDesktopApp.ts @@ -9,7 +9,7 @@ import { AppWindow } from './appWindow'; import { ComfyServer } from './comfyServer'; import { ComfyServerConfig } from '../config/comfyServerConfig'; import fs from 'fs'; -import { InstallOptions, type ElectronContextMenuOptions } from '../preload'; +import { InstallOptions, type ElectronContextMenuOptions, type TorchDeviceType } from '../preload'; import path from 'path'; import { getModelsDirectory, validateHardware } from '../utils'; import { DownloadManager } from '../models/DownloadManager'; @@ -134,6 +134,17 @@ export class ComfyDesktopApp { return null; } }); + // Config + ipcMain.handle(IPC_CHANNELS.GET_GPU, async (_event): Promise => { + return await DesktopConfig.getAsync('detectedGpu'); + }); + // Restart core + ipcMain.handle(IPC_CHANNELS.RESTART_CORE, async (_event): Promise => { + if (!this.comfyServer) return false; + await this.comfyServer?.kill(); + await this.comfyServer.start(); + return true; + }); } /** @@ -141,6 +152,8 @@ export class ComfyDesktopApp { */ static async install(appWindow: AppWindow): Promise { const validation = await validateHardware(); + if (typeof validation?.gpu === 'string') DesktopConfig.store.set('detectedGpu', validation.gpu); + if (!validation.isValid) { await appWindow.loadRenderer('not-supported'); log.error(validation.error); @@ -154,6 +167,11 @@ export class ComfyDesktopApp { const { store } = DesktopConfig; store.set('basePath', installWizard.basePath); + const { device } = installOptions; + if (device !== undefined) { + store.set('selectedDevice', device); + } + await installWizard.install(); store.set('installState', 'installed'); appWindow.maximize(); @@ -182,7 +200,11 @@ export class ComfyDesktopApp { DownloadManager.getInstance(this.appWindow!, getModelsDirectory(this.basePath)); this.appWindow.sendServerStartProgress(ProgressStatus.PYTHON_SETUP); - const virtualEnvironment = new VirtualEnvironment(this.basePath); + + const { store } = DesktopConfig; + const selectedDevice = store.get('selectedDevice'); + const virtualEnvironment = new VirtualEnvironment(this.basePath, selectedDevice); + await virtualEnvironment.create({ onStdout: (data) => { log.info(data); @@ -193,7 +215,7 @@ export class ComfyDesktopApp { this.appWindow.send(IPC_CHANNELS.LOG_MESSAGE, data); }, }); - const { store } = DesktopConfig; + if (!store.get('Comfy-Desktop.RestoredCustomNodes', false)) { try { await restoreCustomNodes(virtualEnvironment, this.appWindow); diff --git a/src/preload.ts b/src/preload.ts index 0b181631..6f42f9dd 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -2,6 +2,7 @@ import { contextBridge, ipcRenderer } from 'electron'; import { IPC_CHANNELS, ELECTRON_BRIDGE_API, ProgressStatus, DownloadStatus } from './constants'; import type { DownloadState } from './models/DownloadManager'; import path from 'node:path'; +import { DesktopConfig } from './store/desktopConfig'; /** * Open a folder in the system's default file explorer. @@ -12,12 +13,18 @@ const openFolder = async (folderPath: string) => { ipcRenderer.send(IPC_CHANNELS.OPEN_PATH, path.join(basePath, folderPath)); }; +export type GpuType = 'nvidia' | 'mps' | 'unsupported'; +export type TorchDeviceType = GpuType | 'cpu'; + export interface InstallOptions { + /** Base installation path */ installPath: string; autoUpdate: boolean; allowMetrics: boolean; migrationSourcePath?: string; migrationItemIds?: string[]; + /** Torch compute device */ + device?: TorchDeviceType; } export interface SystemPaths { @@ -229,6 +236,23 @@ const electronAPI = { showContextMenu: (options?: ElectronContextMenuOptions): void => { return ipcRenderer.send(IPC_CHANNELS.SHOW_CONTEXT_MENU, options); }, + Config: { + /** + * Finds the name of the last detected GPU type. Detection only runs during installation. + * @returns The last GPU detected by `validateHardware` - runs during installation + */ + getDetectedGpu: async (): Promise => { + return await ipcRenderer.invoke(IPC_CHANNELS.GET_GPU); + }, + }, + /** Restart the python server without restarting desktop. */ + restartCore: async (): Promise => { + console.log('Restarting core process'); + await ipcRenderer.invoke(IPC_CHANNELS.RESTART_APP); + }, + getPlatform: (): NodeJS.Platform => { + return process.platform; + }, } as const; export type ElectronAPI = typeof electronAPI; diff --git a/src/store/desktopConfig.ts b/src/store/desktopConfig.ts index ee211334..841c6736 100644 --- a/src/store/desktopConfig.ts +++ b/src/store/desktopConfig.ts @@ -4,6 +4,7 @@ import { app, dialog, shell } from 'electron'; import path from 'node:path'; import fs from 'fs/promises'; import type { DesktopSettings } from '.'; +import type { TorchDeviceType } from '../preload'; /** Handles loading of electron-store config, pre-window errors, and provides a non-null interface for the store. */ export class DesktopConfig { @@ -14,6 +15,10 @@ export class DesktopConfig { return store; } + static get gpu(): TorchDeviceType | undefined { + return DesktopConfig.store.get('detectedGpu'); + } + static async load( options?: ConstructorParameters>[0] ): Promise | undefined> { @@ -56,6 +61,35 @@ export class DesktopConfig { } } } + + /** + * Saves each {@link config} setting individually, returning a promise for the task. + * @param key The key of {@link DesktopSettings} to save + * @param value The value to be saved. Must be valid. + * @returns A promise that resolves on successful save, or rejects with the first caught error. + */ + static async setAsync(key: Key, value: DesktopSettings[Key]): Promise { + return new Promise((resolve, reject) => { + log.info(`Saving setting: [${key}]`, value); + try { + DesktopConfig.store.set(key, value); + resolve(); + } catch (error) { + reject(error); + } + }); + } + + /** @inheritdoc {@link ElectronStore.get} */ + static async getAsync(key: Key): Promise { + return new Promise((resolve, reject) => { + try { + resolve(DesktopConfig.store.get(key)); + } catch (error) { + reject(error); + } + }); + } } function showResetPrompt(configFilePath: string): Promise { diff --git a/src/store/index.ts b/src/store/index.ts index f0cabae9..af9318e5 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,3 +1,5 @@ +import type { GpuType, TorchDeviceType } from '../preload'; + export type AppWindowSettings = { windowWidth: number; windowHeight: number; @@ -16,4 +18,11 @@ export type DesktopSettings = { * in the yaml config. */ installState?: 'started' | 'installed' | 'upgraded'; + /** + * The last GPU that was detected during hardware validation. + * Allows manual override of some install behaviour. + */ + detectedGpu?: GpuType; + /** The pytorch device that the user selected during installation. */ + selectedDevice?: TorchDeviceType; }; diff --git a/src/utils.ts b/src/utils.ts index 69925a6d..d9ca2d09 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -6,6 +6,7 @@ import si from 'systeminformation'; import { exec } from 'child_process'; import { promisify } from 'util'; import log from 'electron-log/main'; +import type { GpuType } from './preload'; export async function pathAccessible(path: string): Promise { try { @@ -88,6 +89,8 @@ const execAsync = promisify(exec); interface HardwareValidation { isValid: boolean; + /** The detected GPU (not guaranteed to be valid - check isValid) */ + gpu?: GpuType; error?: string; } @@ -108,7 +111,7 @@ export async function validateHardware(): Promise { }; } - return { isValid: true }; + return { isValid: true, gpu: 'mps' }; } // Windows NVIDIA GPU validation @@ -127,11 +130,9 @@ export async function validateHardware(): Promise { 'powershell.exe -c "$n = \'*NVIDIA*\'; Get-CimInstance win32_videocontroller | ? { $_.Name -like $n -or $_.VideoProcessor -like $n -or $_.AdapterCompatibility -like $n }"' ); if (!res?.stdout) throw new Error('No video card'); - return { isValid: true }; } catch { try { await execAsync('nvidia-smi'); - return { isValid: true }; } catch { return { isValid: false, @@ -141,7 +142,7 @@ export async function validateHardware(): Promise { } } - return { isValid: true }; + return { isValid: true, gpu: 'nvidia' }; } return { diff --git a/src/virtualEnvironment.ts b/src/virtualEnvironment.ts index 043c7af4..58269cde 100644 --- a/src/virtualEnvironment.ts +++ b/src/virtualEnvironment.ts @@ -6,6 +6,8 @@ import { app } from 'electron'; import * as pty from 'node-pty'; import * as os from 'os'; import { getDefaultShell } from './shell/util'; +import { DesktopConfig } from './store/desktopConfig'; +import type { TorchDeviceType } from './preload'; type ProcessCallbacks = { onStdout?: (data: string) => void; @@ -25,6 +27,7 @@ export class VirtualEnvironment { readonly pythonInterpreterPath: string; readonly comfyUIRequirementsPath: string; readonly comfyUIManagerRequirementsPath: string; + readonly selectedDevice?: string; uvPty: pty.IPty | undefined; get uvPtyInstance() { @@ -48,8 +51,11 @@ export class VirtualEnvironment { return this.uvPty; } - constructor(venvPath: string, pythonVersion: string = '3.12.4') { + constructor(venvPath: string, selectedDevice: TorchDeviceType | undefined, pythonVersion: string = '3.12.4') { this.venvRootPath = venvPath; + this.pythonVersion = pythonVersion; + this.selectedDevice = selectedDevice; + this.venvPath = path.join(venvPath, '.venv'); // uv defaults to .venv const resourcesPath = app.isPackaged ? path.join(process.resourcesPath) : path.join(app.getAppPath(), 'assets'); this.comfyUIRequirementsPath = path.join(resourcesPath, 'ComfyUI', 'requirements.txt'); @@ -61,12 +67,11 @@ export class VirtualEnvironment { 'requirements.txt' ); - this.pythonVersion = pythonVersion; this.cacheDir = path.join(venvPath, 'uv-cache'); - this.requirementsCompiledPath = - process.platform === 'win32' - ? path.join(resourcesPath, 'requirements', 'windows_nvidia.compiled') - : path.join(resourcesPath, 'requirements', 'macos.compiled'); + + const filename = `${compiledRequirements()}.compiled`; + this.requirementsCompiledPath = path.join(resourcesPath, 'requirements', filename); + this.pythonInterpreterPath = process.platform === 'win32' ? path.join(this.venvPath, 'Scripts', 'python.exe') @@ -86,6 +91,13 @@ export class VirtualEnvironment { throw new Error(`Unsupported platform: ${process.platform}`); } log.info(`Using uv at ${this.uvPath}`); + + function compiledRequirements() { + if (process.platform === 'darwin') return 'macos'; + if (process.platform === 'win32') { + return selectedDevice === 'cpu' ? 'windows_cpu' : 'windows_nvidia'; + } + } } public async create(callbacks?: ProcessCallbacks): Promise { @@ -120,6 +132,11 @@ export class VirtualEnvironment { } private async createEnvironment(callbacks?: ProcessCallbacks): Promise { + if (this.selectedDevice === 'unsupported') { + log.info('User elected to manually configure their environment. Skipping python configuration.'); + return; + } + try { if (await this.exists()) { log.info(`Virtual environment already exists at ${this.venvPath}`); @@ -151,6 +168,7 @@ export class VirtualEnvironment { } public async installRequirements(callbacks?: ProcessCallbacks): Promise { + // pytorch nightly is required for MPS if (process.platform === 'darwin') { return this.manualInstall(callbacks); } @@ -311,7 +329,14 @@ export class VirtualEnvironment { } private async installPytorch(callbacks?: ProcessCallbacks): Promise { - if (process.platform === 'win32') { + const { selectedDevice } = this; + + if (selectedDevice === 'cpu') { + // CPU mode + log.info('Installing PyTorch CPU'); + await this.runUvCommandAsync(['pip', 'install', 'torch', 'torchvision', 'torchaudio'], callbacks); + } else if (selectedDevice === 'nvidia' || process.platform === 'win32') { + // Win32 default log.info('Installing PyTorch CUDA 12.1'); await this.runUvCommandAsync( [ @@ -325,9 +350,8 @@ export class VirtualEnvironment { ], callbacks ); - } - - if (process.platform === 'darwin') { + } else if (selectedDevice === 'mps' || process.platform === 'darwin') { + // macOS default log.info('Installing PyTorch Nightly for macOS.'); await this.runUvCommandAsync( [ @@ -346,6 +370,7 @@ export class VirtualEnvironment { ); } } + private async installComfyUIRequirements(callbacks?: ProcessCallbacks): Promise { log.info(`Installing ComfyUI requirements from ${this.comfyUIRequirementsPath}`); const installCmd = ['pip', 'install', '-r', this.comfyUIRequirementsPath];