fix(api): stop claiming no GPU exists before the first AI bundle install (#682)

A fresh GPU deployment has working passthrough but no torch/ONNX runtime until a bundle installs, so the dispatcher reports gpu=false and the boot banner said no GPU was detected. Add the hardware-present state, driven by the same /dev/nvidia0 check the instance census uses. Fixes #673.
This commit is contained in:
SnapOtter
2026-07-30 09:49:45 +08:00
committed by GitHub
parent b6c69b7aeb
commit 890b38a71a
3 changed files with 59 additions and 5 deletions
+2 -5
View File
@@ -33,6 +33,7 @@ import {
startInterruptedInstallRecovery,
stopInterruptedInstallRecovery,
} from "./lib/feature-status.js";
import { gpuBootLine } from "./lib/gpu-boot-line.js";
import { logger } from "./lib/logger.js";
import { requestDuration } from "./lib/metrics.js";
import { purgeOcrRuntimeDownloads, runOcrRuntimeMaintenance } from "./lib/ocr-runtime-install.js";
@@ -771,11 +772,7 @@ try {
await app.listen({ port: env.PORT, host: "0.0.0.0" });
const dispatcherResult = await initDispatcher();
const gpuLine = dispatcherResult.ready
? dispatcherResult.gpu
? "[INFO] GPU detected -- AI tools will use CUDA acceleration"
: "[WARN] No GPU detected -- AI tools will use CPU (slower)"
: "[WARN] AI sidecar did not start -- AI tools will use per-request Python (slower)";
const gpuLine = gpuBootLine(dispatcherResult, gatherSystemProperties().gpu_present);
console.log(
[
`SnapOtter v${APP_VERSION} running on port ${env.PORT}`,
+26
View File
@@ -0,0 +1,26 @@
interface DispatcherStartState {
ready: boolean;
gpu: boolean;
}
/**
* One boot-banner line describing the AI acceleration state.
*
* The hardware-present-but-unusable branch exists because a fresh GPU
* deployment has working passthrough but no torch/ONNX runtime until the
* first AI bundle installs, so the dispatcher reports gpu=false. Logging
* "No GPU detected" there sends GPU users off to debug their container
* toolkit (#673).
*/
export function gpuBootLine(dispatcher: DispatcherStartState, gpuHardwarePresent: boolean): string {
if (!dispatcher.ready) {
return "[WARN] AI sidecar did not start -- AI tools will use per-request Python (slower)";
}
if (dispatcher.gpu) {
return "[INFO] GPU detected -- AI tools will use CUDA acceleration";
}
if (gpuHardwarePresent) {
return "[INFO] GPU hardware detected -- AI frameworks install with the first AI bundle; AI tools use CPU until then";
}
return "[WARN] No GPU detected -- AI tools will use CPU (slower)";
}
+31
View File
@@ -0,0 +1,31 @@
import { describe, expect, it } from "vitest";
import { gpuBootLine } from "../../../apps/api/src/lib/gpu-boot-line.js";
describe("gpuBootLine", () => {
it("announces CUDA when the dispatcher can use the GPU", () => {
expect(gpuBootLine({ ready: true, gpu: true }, true)).toBe(
"[INFO] GPU detected -- AI tools will use CUDA acceleration",
);
});
it("explains the pending-bundle state instead of claiming no GPU exists", () => {
// Fresh GPU deployment: passthrough works, but no torch/ONNX runtime is
// installed until the first AI bundle, so the dispatcher reports gpu=false.
const line = gpuBootLine({ ready: true, gpu: false }, true);
expect(line).toContain("GPU hardware detected");
expect(line).toContain("AI bundle");
expect(line).not.toContain("No GPU detected");
});
it("keeps the plain no-GPU warning for hosts without a GPU", () => {
expect(gpuBootLine({ ready: true, gpu: false }, false)).toBe(
"[WARN] No GPU detected -- AI tools will use CPU (slower)",
);
});
it("keeps the sidecar warning when the dispatcher did not start", () => {
expect(gpuBootLine({ ready: false, gpu: false }, true)).toBe(
"[WARN] AI sidecar did not start -- AI tools will use per-request Python (slower)",
);
});
});