From d2e12366eb2e3b036208cb5309a08a79bf63cf7e Mon Sep 17 00:00:00 2001 From: Lightning Pixel Date: Sun, 22 Mar 2026 16:42:19 +0100 Subject: [PATCH] fix(generate): improve memory release on unload and disable free-memory during generation --- api/routers/model.py | 10 +++ api/services/generators/base.py | 19 +++++ api/services/generators/hunyuan3d.py | 9 --- electron/main/python-bridge.ts | 11 ++- src/areas/generate/GeneratePage.tsx | 3 +- src/areas/models/ModelsPage.tsx | 112 +++++++++++++++++++++++---- 6 files changed, 140 insertions(+), 24 deletions(-) diff --git a/api/routers/model.py b/api/routers/model.py index 2e3bee0..6ababd5 100644 --- a/api/routers/model.py +++ b/api/routers/model.py @@ -43,6 +43,16 @@ async def switch_model(model_id: str): async def unload_all_models(): """Unloads all models from memory to free VRAM/RAM.""" generator_registry.unload_all() + # Force Python to release memory back to the OS + import gc + gc.collect() + try: + import ctypes, sys + if sys.platform == "win32": + k32 = ctypes.windll.kernel32 + k32.SetProcessWorkingSetSizeEx(k32.GetCurrentProcess(), -1, -1, 0) + except Exception: + pass return {"unloaded": True} diff --git a/api/services/generators/base.py b/api/services/generators/base.py index 0180f93..a23b01d 100644 --- a/api/services/generators/base.py +++ b/api/services/generators/base.py @@ -77,6 +77,25 @@ def load(self) -> None: def unload(self) -> None: """Release memory. Can be overridden if needed.""" self._model = None + import gc + gc.collect() + try: + import torch + if torch.cuda.is_available(): + torch.cuda.empty_cache() + except ImportError: + pass + # Force the OS to reclaim unused memory from this process + try: + import ctypes + import sys + if sys.platform == "win32": + kernel32 = ctypes.windll.kernel32 + kernel32.SetProcessWorkingSetSizeEx( + kernel32.GetCurrentProcess(), -1, -1, 0 + ) + except Exception: + pass def is_loaded(self) -> bool: return self._model is not None diff --git a/api/services/generators/hunyuan3d.py b/api/services/generators/hunyuan3d.py index 2a2b247..5f63570 100644 --- a/api/services/generators/hunyuan3d.py +++ b/api/services/generators/hunyuan3d.py @@ -69,15 +69,6 @@ def load(self) -> None: self._model = pipeline print(f"[Hunyuan3DGenerator] Loaded on {device}.") - def unload(self) -> None: - super().unload() - try: - import torch - if torch.cuda.is_available(): - torch.cuda.empty_cache() - except ImportError: - pass - # ------------------------------------------------------------------ # # Inference # ------------------------------------------------------------------ # diff --git a/electron/main/python-bridge.ts b/electron/main/python-bridge.ts index dde2d66..d04d028 100644 --- a/electron/main/python-bridge.ts +++ b/electron/main/python-bridge.ts @@ -16,6 +16,7 @@ export class PythonBridge { private ready = false private startPromise: Promise | null = null private getWindow: (() => BrowserWindow | null) | null = null + private intentionalStop = false setWindowGetter(fn: () => BrowserWindow | null): void { this.getWindow = fn @@ -78,7 +79,7 @@ export class PythonBridge { console.log('[PythonBridge] Process exited with code', code) this.ready = false this.process = null - if (wasReady) { + if (wasReady && !this.intentionalStop) { this.getWindow()?.webContents.send('python:crashed', { code }) } }) @@ -100,6 +101,14 @@ export class PythonBridge { console.log('[PythonBridge] Stopped') } + async restart(): Promise { + console.log('[PythonBridge] Restarting to free memory…') + this.intentionalStop = true + await this.stop() + this.intentionalStop = false + await this.start() + } + private emitTqdmLog(raw: string): void { if (/INFO/.test(raw)) return if (!raw.trim()) return diff --git a/src/areas/generate/GeneratePage.tsx b/src/areas/generate/GeneratePage.tsx index 9bb1747..4aee54c 100644 --- a/src/areas/generate/GeneratePage.tsx +++ b/src/areas/generate/GeneratePage.tsx @@ -63,8 +63,9 @@ export default function GeneratePage(): JSX.Element { {/* Free memory button — top-left overlay */} + + + + + , + document.body + ) + })()} ) }