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
11 changes: 8 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,14 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
extracted folder; like the portable exe, it does not update itself. The Setup installer is still
the recommended download.
- The AI engine remembers the GPU backend that worked (`gpu-backend-cache.json` in the app's data
folder) and tries it first on the next launch. An app update, a different GPU, a change in the GPU
runtimes node-llama-cpp finds (such as installing a CUDA toolkit), or 14 days make it run the full
backend search again.
folder) and tries it first on the next launch. An app update, a different GPU or graphics driver
version, a change in the GPU runtimes node-llama-cpp finds (such as installing a CUDA toolkit), or
14 days make it run the full backend search again. The optional embedding worker
(`STRATOSORT_ENABLE_EMBEDDING_WORKER`) uses the same record and supported-backend filter.
- The Windows x64 build no longer ships node-llama-cpp's arm64 prebuilt (about 11 MB).
- On Windows and macOS the app no longer forces Chromium's `enable-gpu-rasterization` and
`enable-zero-copy` switches, so Chromium's per-driver GPU blocklist applies. Linux keeps forcing
them, as before.
- After three GPU process exits in one session, software rendering now stays on for up to 3 days
(`.gpu-fallback` in the app's data folder) instead of switching back on the next launch, which
made launches alternate between slow and fast on a failing driver. Delete the file while the app
Expand Down
23 changes: 12 additions & 11 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,9 @@ Orchestrates the boot sequence with progress reporting to the splash screen:
on read. `simple-main.js` calls `clearGpuFallbackAfterStableRun()`
`TIMEOUTS.GPU_FALLBACK_STABLE_RUN` (30 s) after creating the window, next to the
`.crash-timestamps` reset; it deletes the flag only for a hardware-accelerated session with no GPU
process exit and a visible window, so a software-rendered session cannot clear it. Chromium's GPU
rasterization and zero-copy switches are left at their per-driver defaults.
process exit and a visible window, so a software-rendered session cannot clear it. On Windows and
macOS Chromium's GPU rasterization and zero-copy switches are left at their per-driver defaults;
Linux keeps forcing both, as earlier releases did (`getForcedRasterSwitches()`).
- **First-paint gate** (`core/firstPaintGate.js`): coordinated startup and `runBackgroundSetup()`
wait until the window has been shown or its document has finished loading, capped at
`TIMEOUTS.FIRST_PAINT_GATE` (3 s), so the synchronous stretches of AI bring-up (node-llama-cpp
Expand All @@ -181,15 +182,15 @@ Orchestrates the boot sequence with progress reporting to the splash screen:
- **Last-known-good AI backend**: `LlamaService` hands `initLlamaWithBackend()`
(`utils/llamaBackendSelector.js`) a record from `utils/llamaBackendCache.js`, stored as
`userData/gpu-backend-cache.json`. When the record's fingerprint (platform, arch, app version,
node-llama-cpp version, GPU type and name; `driverVersion` stays null until `GPUMonitor` reports
one) and the GPU backends `getLlamaGpuTypes('supported')` reports both match what was saved, and
it is under 14 days old, the remembered GPU backend moves to the front of the candidate list
before the node-llama-cpp support filter runs. The supported set is probed before the record is
read, so a Vulkan record saved while the CUDA runtime was missing is not used once CUDA becomes
available. It only reorders: on Windows node-llama-cpp still test-loads the binary, a failed
attempt of the remembered backend deletes the record, and a GPU backend that differs from the
record is saved (CPU never is). `STRATOSORT_LLM_GPU_BACKEND` and software or safe mode bypass the
record entirely.
node-llama-cpp version, GPU type, name and driver version, which `GPUMonitor` reads from
`nvidia-smi` or the Windows video controller) and the GPU backends `getLlamaGpuTypes('supported')`
reports both match what was saved, and it is under 14 days old, the remembered GPU backend moves
to the front of the candidate list before the node-llama-cpp support filter runs. The supported
set is probed before the record is read, so a Vulkan record saved while the CUDA runtime was
missing is not used once CUDA becomes available. It only reorders: on Windows node-llama-cpp still
test-loads the binary, a failed attempt of the remembered backend deletes the record, and a GPU
backend that differs from the record is saved (CPU never is). `STRATOSORT_LLM_GPU_BACKEND` and
software or safe mode bypass the record entirely.
- **Phases** (progress reported to splash): Starting (0%) -> Preflight checks (10%) -> Migration
(12%) -> Services -> Models -> App Services -> Ready (100%). See `reportProgress(...)` calls in
`StartupManagerCore.js` for the authoritative sequence.
Expand Down
42 changes: 26 additions & 16 deletions docs/CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,10 @@ concurrency and VRAM is accounted for by the model memory manager.
Setting `STRATOSORT_ENABLE_EMBEDDING_WORKER=true` starts a separate `embeddingWorker` thread that
loads its own `node-llama-cpp` stack. That pool runs with `maxThreads: 1`, which caps parallel
embedding throughput, and it can hold a second copy of the embedding model in VRAM that the
main-process memory manager does not see. Enable it only for isolation experiments or debugging.
main-process memory manager does not see. Enable it only for isolation experiments or debugging. The
worker selects its GPU backend the same way as the main process (supported-backend filter and the
remembered backend from `gpu-backend-cache.json`, read-only), so it does not re-test GPU binaries
the main process has already ruled out.

The two queue waits are off by default for the same throughput reason.
`STRATOSORT_ANALYSIS_EMBEDDING_QUEUE_WAIT=true` restores the per-file `waitForAnalysisQueueCapacity`
Expand Down Expand Up @@ -160,8 +163,11 @@ These settings are configurable via **Settings > Performance > Processing limits
| `STRATOSORT_DISABLE_BACKGROUND_THROTTLING` | `0` | Set `1` to disable Chromium background-page throttling so analysis keeps running at full speed when the window is hidden. Increases idle CPU. |

`STRATOSORT_FORCE_SOFTWARE_GPU`, `ELECTRON_FORCE_SOFTWARE` and `--safe-mode` also run the AI engine
on the CPU. The app no longer appends Chromium's `enable-gpu-rasterization` or `enable-zero-copy`
switches, so GPU rasterization follows Chromium's own per-driver decision.
on the CPU. On Windows and macOS the app no longer appends Chromium's `enable-gpu-rasterization` or
`enable-zero-copy` switches, so GPU rasterization follows Chromium's own per-driver decision. Linux
still appends both, as every earlier release did on all platforms, because Chromium's Linux default
varies with version and driver and the AppImage has not been measured without them. Software mode
appends neither.

**Automatic software fallback (`.gpu-fallback`).** After three GPU process exits in one session,
each within an hour of the one before (the count restarts after an hour with no exit), the app
Expand Down Expand Up @@ -441,21 +447,25 @@ They are listed here for support engineers and contributors who need to debug ed
**Last-known-good GPU backend (`gpu-backend-cache.json`).** After the text and embedding engine
settles on a GPU backend (`cuda`, `vulkan` or `metal`), the app saves it to `gpu-backend-cache.json`
in its data directory together with a fingerprint: platform, architecture, app version,
node-llama-cpp version, and the detected GPU type and name. (It has a driver version field, but the
GPU probe does not report a driver version yet, so a driver update alone does not reset the record.)
The record also keeps the GPU backends node-llama-cpp reported as supported at the time, so
installing a CUDA toolkit (or anything else that changes that set) makes the next launch run the
full ladder again instead of keeping an earlier Vulkan result first. On later launches a record
whose fingerprint and supported set match and that is less than 14 days old puts that backend first
in the candidate list (`[LlamaService] Llama backend candidates ordered by cached selection`), so
candidates that failed before are not tested again. It only changes the order: on Windows
node-llama-cpp still test-loads the binary, and if the remembered backend fails, the record is
deleted (`[LlamaService] Cleared cached Llama backend after it failed`) and the normal ladder
continues. A CPU result is never saved, and an unchanged record is not rewritten; a new or changed
GPU result logs `[LlamaService] Saved Llama backend selection for the next launch`.
node-llama-cpp version, and the detected GPU type, name and driver version (from `nvidia-smi` on
NVIDIA GPUs, from Windows' video controller entry otherwise; Linux without `nvidia-smi` and macOS
report none). A driver update therefore makes the next launch run the full ladder. The record also
keeps the GPU backends node-llama-cpp reported as supported at the time, so installing a CUDA
toolkit (or anything else that changes that set) makes the next launch run the full ladder again
instead of keeping an earlier Vulkan result first. On later launches a record whose fingerprint and
supported set match and that is less than 14 days old puts that backend first in the candidate list
(`[LlamaService] Llama backend candidates ordered by cached selection`), so candidates that failed
before are not tested again. It only changes the order: on Windows node-llama-cpp still test-loads
the binary, and if the remembered backend fails, the record is deleted
(`[LlamaService] Cleared cached Llama backend after it failed`) and the normal ladder continues. A
CPU result is never saved, and an unchanged record is not rewritten; a new or changed GPU result
logs `[LlamaService] Saved Llama backend selection for the next launch`.
`STRATOSORT_LLM_GPU_BACKEND`, `STRATOSORT_FORCE_SOFTWARE_GPU`, `ELECTRON_FORCE_SOFTWARE` and
`--safe-mode` bypass the record: it is neither read nor changed while they are set. Deleting the
file while the app is closed makes the next launch run the full ladder.
file while the app is closed makes the next launch run the full ladder. With
`STRATOSORT_ENABLE_EMBEDDING_WORKER=true` the worker thread reads the same record and applies the
same supported-backend filter, but only the main process saves or deletes the record; a failed
attempt there logs `[EmbeddingWorker] Cached Llama backend failed; record left to its owner`.

### Storage and persistence

Expand Down
20 changes: 10 additions & 10 deletions docs/DEBUGGING_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,16 +261,16 @@ result and the GPU fallback decision at module load) are not in the file.
`[LlamaService] Llama backend init failed` lines once the record exists. On Windows
node-llama-cpp still test-loads the chosen binary in a child process, so the record saves the
cost of candidates that fail, not of the one that works. The record is not used on the very
first launch (there is none yet), after an app update, when the detected GPU name or type
changes, when the GPU backends node-llama-cpp finds runtimes for change (for example after a
CUDA toolkit is installed), or once it is 14 days old; the full ladder then runs and a GPU
result is saved again. While `STRATOSORT_LLM_GPU_BACKEND`, `STRATOSORT_FORCE_SOFTWARE_GPU`,
`ELECTRON_FORCE_SOFTWARE` or `--safe-mode` chooses the backend, the record is neither read nor
changed. A CPU result is never saved. Deleting the file while the app is closed makes the next
launch run the full ladder. Why a record was not used is logged as
`[LlamaBackendCache] Ignoring cached backend record` with `reason` (`missing`, `unreadable`,
`corrupt`, `invalid`, `fingerprint-mismatch`, `supported-backends-changed` or `expired`) at
debug level, which packaged builds do not write by default.
first launch (there is none yet), after an app update, when the detected GPU name, type or
driver version changes, when the GPU backends node-llama-cpp finds runtimes for change (for
example after a CUDA toolkit is installed), or once it is 14 days old; the full ladder then
runs and a GPU result is saved again. While `STRATOSORT_LLM_GPU_BACKEND`,
`STRATOSORT_FORCE_SOFTWARE_GPU`, `ELECTRON_FORCE_SOFTWARE` or `--safe-mode` chooses the
backend, the record is neither read nor changed. A CPU result is never saved. Deleting the file
while the app is closed makes the next launch run the full ladder. Why a record was not used is
logged as `[LlamaBackendCache] Ignoring cached backend record` with `reason` (`missing`,
`unreadable`, `corrupt`, `invalid`, `fingerprint-mismatch`, `supported-backends-changed` or
`expired`) at debug level, which packaged builds do not write by default.
4. **Several `StratoSort Stack.exe` processes are normal.** In Task Manager, Details tab, add the
Command line column (right-click a column header, Select columns). During a launch expect: one
`StratoSort Stack.exe` without `--type`, one with `--type=gpu-process`, one `--type=renderer`,
Expand Down
10 changes: 6 additions & 4 deletions docs/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -423,10 +423,12 @@ the on-disk sizes in `node_modules`; the download shrinks less, by an estimated
the installer compresses the source files well). Update the download size in
[INSTALL_GUIDE.md](INSTALL_GUIDE.md) after the next packaged build. `verify-packaged-artifacts.js`
checks that `llama/binariesGithubRelease.json` (read by node-llama-cpp on import) still ships and
that `llama/gitRelease.bundle` does not. `@node-llama-cpp/win-arm64` (11 MB) also lands in the x64
build, because its package declares `cpu: ["arm64", "x64"]` and npm installs it on x64 runners; it
cannot be excluded from the shared `files` list without also removing it from the arm64 build, which
inherits that list through `extends`.
that `llama/gitRelease.bundle` does not. `@node-llama-cpp/win-arm64` (11 MB) declares
`cpu: ["arm64", "x64"]`, so npm installs it on x64 runners too. `win.files` excludes the other
arch's llama prebuilts with `!**/node_modules/@node-llama-cpp/win-!(${arch}|${arch}-*)/**`:
electron-builder expands `${arch}` per packed arch, so the x64 build drops `win-arm64` and the arm64
build (which inherits `win.files` through `extends`) drops any `win-x64*` package.
`verify-packaged-artifacts.js` fails if a package for the other arch still holds files.

## Signing And Notarization Requirements

Expand Down
3 changes: 2 additions & 1 deletion electron-builder.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@
"icon": "assets/icons/win/icon.ico",
"target": ["nsis", "zip"],
"files": [
"!**/node_modules/better-sqlite3/prebuilds/{darwin,linux,linuxmusl}-*.node"
"!**/node_modules/better-sqlite3/prebuilds/{darwin,linux,linuxmusl}-*.node",
"!**/node_modules/@node-llama-cpp/win-!(${arch}|${arch}-*)/**"
]
},
"nsis": {
Expand Down
31 changes: 31 additions & 0 deletions scripts/verify-packaged-artifacts.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,27 @@ function getRequiredLlamaPrebuiltPackages(platform, arch) {
];
}

/**
* node-llama-cpp prebuilt packages for the OTHER Windows arch, which `win.files`
* excludes through `win-!(${arch}|${arch}-*)`. @node-llama-cpp/win-arm64
* declares `cpu: ["arm64", "x64"]`, so npm installs it on x64 runners too.
*/
function getForeignArchLlamaPrebuiltPackages(platform, arch) {
const platformKey = String(platform || '').toLowerCase();
if (platformKey !== 'win' && platformKey !== 'win32') return [];
return String(arch || '').toLowerCase() === 'arm64'
? getRequiredLlamaPrebuiltPackages(platform, 'x64')
: getRequiredLlamaPrebuiltPackages(platform, 'arm64');
}

/** True when `dir` holds at least one file at any depth. */
function containsAnyFile(dir) {
if (!fs.existsSync(dir)) return false;
return fs
.readdirSync(dir, { withFileTypes: true })
.some((entry) => (entry.isDirectory() ? containsAnyFile(path.join(dir, entry.name)) : true));
}

/**
* The Windows x64 no-install artifact (electron-builder `zip` target, named by
* the top-level artifactName). arm64 ships the installer only.
Expand Down Expand Up @@ -256,6 +277,15 @@ function verifyLlamaPrebuiltPackages(unpackedNodeModulesRoot, platform, arch) {
`node-llama-cpp prebuilt package ${packageName}`
);
});
getForeignArchLlamaPrebuiltPackages(platform, arch).forEach((packageName) => {
const packageDir = path.join(unpackedNodeModulesRoot, ...packageName.split('/'));
if (containsAnyFile(packageDir)) {
throw new Error(
`node-llama-cpp prebuilt package ${packageName} for another arch was packaged: ` +
`${packageDir}. Check the @node-llama-cpp exclusion in electron-builder.json "win.files".`
);
}
});
}

function verifyWhisperVariantFolders(unpackedNodeModulesRoot, platform, arch) {
Expand Down Expand Up @@ -397,6 +427,7 @@ if (require.main === module) {

module.exports = {
getRequiredLlamaPrebuiltPackages,
getForeignArchLlamaPrebuiltPackages,
getRequiredMacElectronRuntimeFiles,
getRequiredWhisperVariantPackages,
getWindowsZipArtifactName,
Expand Down
31 changes: 24 additions & 7 deletions src/main/core/gpuConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,16 @@ function shouldForceInferenceCpuMode() {
);
}

/**
* Raster switches forced on this platform: none on Windows and macOS, the
* historical pair on Linux (see initializeGpuConfig).
*
* @returns {string[]}
*/
function getForcedRasterSwitches() {
return process.platform === 'linux' ? ['enable-gpu-rasterization', 'enable-zero-copy'] : [];
}

/**
* Initialize GPU configuration
* Must be called before app.ready
Expand Down Expand Up @@ -163,13 +173,20 @@ function initializeGpuConfig({ getMainWindow, isFirstShowPending } = {}) {
logger.warn('[GPU] GPU blocklist ignored via STRATOSORT_IGNORE_GPU_BLOCKLIST');
}

// No enable-gpu-rasterization / enable-zero-copy: both override
// Chromium's per-driver decisions. The first bypasses the GPU
// rasterization blocklist (GPU raster is already on by default where
// the driver is not blocklisted); the second forces native-buffer tile
// uploads that Chromium leaves off outside macOS. Either only changes
// behaviour on the drivers Chromium already distrusts.
logger.info(`[GPU] Flags set: ANGLE=${angleBackend}`);
// Windows and macOS: no enable-gpu-rasterization / enable-zero-copy.
// Both override Chromium's per-driver decisions; the first bypasses the
// GPU rasterization blocklist (GPU raster is already on by default
// there where the driver is not blocklisted), the second forces
// native-buffer tile uploads. Either only changes behaviour on drivers
// Chromium already distrusts.
// Linux keeps both, as every earlier release set them on all platforms:
// Chromium's Linux raster default varies with version and driver, and
// the AppImage has not been measured without them.
const rasterSwitches = getForcedRasterSwitches();
for (const rasterSwitch of rasterSwitches) {
app.commandLine.appendSwitch(rasterSwitch);
}
logger.info(`[GPU] Flags set: ANGLE=${angleBackend}`, { rasterSwitches });
}
} catch (e) {
logger.warn('[GPU] Failed to apply GPU flags:', e?.message || 'Unknown error');
Expand Down
Loading
Loading