Vulkan GPU Memory Management in Upscayl#
Upscayl uses a two-layer architecture for GPU operations. TL;DR: Vulkan resource cleanup is handled entirely by killing the backend process β not by explicit Vulkan API calls from the JS layer. The known failure mode (vkQueueSubmit failed -4, VK_ERROR_DEVICE_LOST) occurs on the second and subsequent images, most severely on Intel integrated graphics.
Upscayl uses a two-layer architecture for GPU operations. The Electron/TypeScript frontend (electron/commands/) spawns a native C++ backend binary (upscayl-bin) that performs the actual Vulkan inference via ncnn. The frontend has no direct access to Vulkan objects β it manages GPU resource lifecycle entirely through child process lifecycle: starting, monitoring, and killing the backend process .
The backend binary is invoked per-image with parameters like GPU ID (-g), tile size (-t), model, and I/O paths. Each new image processing operation spawns a fresh call, but the Vulkan context inside the binary may persist state between invocations if the process is not fully torn down.
Where to Look#
| Concern | File |
|---|---|
| Process spawning | electron/utils/spawn-upscayl.ts |
| Single image lifecycle | electron/commands/image-upscayl.ts |
| Batch lifecycle | electron/commands/batch-upscayl.ts |
| Double-pass lifecycle | electron/commands/double-upscayl.ts |
| Process tracking / stop | electron/utils/config-variables.ts Β· electron/commands/stop.ts |
Resource Cleanup and Memory Lifecycle#
All Vulkan memory cleanup in Upscayl is coarse-grained: it happens by killing the backend process, not by calling Vulkan destroy functions directly from the JS layer.
Normal completion path (image-upscayl.ts): on the close event, if no error occurred, the handler calls upscayl.kill() with an explicit // Free up memory comment. The same pattern appears in batch-upscayl.ts and double-upscayl.ts.
Error path: if stderr data contains the strings "Error" or "failed", the process is killed immediately and an error is forwarded to the renderer . This generic string match catches vkQueueSubmit failed messages emitted by the ncnn backend.
User stop: stop.ts calls .kill() on every entry in the childProcesses array.
Gap: childProcesses is an append-only array . Entries are not removed after a process exits. While this does not cause GPU memory leaks (the OS reclaims memory when the process dies), stale entries accumulate across sequential runs within a single session.
Key point for integrated graphics: On Intel/AMD integrated GPUs, VRAM is a window into system RAM. If the backend process is not fully torn down between runs β or if it exits but leaves Vulkan objects unreleased β the next invocation re-initializes Vulkan against already-stressed device memory, which can cause driver-level failures.
vkQueueSubmit failed -4: The Known Failure Mode#
Error code: VK_ERROR_DEVICE_LOST (-4 in the Vulkan spec). This means the device became unresponsive mid-queue-submission β typically due to a GPU timeout, memory exhaustion, or driver crash.
Observed pattern: The error surfaces reliably starting on the second image when processing sequentially. The first image completes; all subsequent images fail at low progress percentages (4β74%) with the message vkQueueSubmit failed -4 written to stderr by the ncnn backend .
Affected hardware: Predominantly reported on Intel integrated graphics (Intel Iris Xe Graphics, 11th Gen) , but also reproducible on AMD Radeon discrete GPUs (RX 7700 XT) during folder batch operations . In the Intel case, symptoms escalate to screen flickering and blackouts that persist after Upscayl is closed β requiring a full system restart.
Why workarounds fail: Lowering tile size and reducing thread count do not resolve the issue . These parameters only affect memory pressure per-tile, not the root cause: stale Vulkan device state or residual allocations left from a prior invocation.
SwiftShader fallback: Affected systems may show the Electron GPU info reporting SwiftShader Device (Subzero) as the active GPU β this is Chromium's software Vulkan renderer and does not reflect the actual device used by upscayl-bin, which enumerates and selects physical GPUs independently .
Debugging and Mitigation#
Reading the logs: Look for vkQueueSubmit failed -4 in the BACKEND REPORTED / UPSCAYL_PROGRESS log stream. If this appears mid-progress, the backend process is killed by the error handler at , and no output file is written.
GPU device enumeration: The backend logs each detected Vulkan physical device on startup, e.g. [0 Intel(R) Iris(R) Xe Graphics] queueC=0[1] queueG=0[1] queueT=0[1]. Each device appears twice in the log because the renderer IPC layer duplicates stdout/stderr . The -g argument selects which device to use; gpuId: null means the backend picks automatically.
Where actual Vulkan memory management code lives: The ncnn C++ backend at upscayl-ncnn handles vkAllocateMemory, vkCreateBuffer, vkQueueSubmit, and associated destroy calls. If the failure root cause is in Vulkan object lifetime (e.g., pipelines or command pools not being destroyed before the second invocation), the fix must go there, not in the Electron layer.
Potential fix directions:
- Ensure the ncnn backend fully calls
vkDeviceWaitIdleand destroys all Vulkan objects before process exit, so the next process starts with a clean device state. - Investigate whether
upscayl-bincan be modified to process multiple images in a single process invocation (avoiding repeated Vulkan context init/teardown), which would also remove the kill-and-restart pattern. - On Intel integrated graphics specifically, check for known driver bugs around
VK_ERROR_DEVICE_LOSTin compute workloads and whether updating the Intel graphics driver mitigates the issue.