fix(tiny): repaired cuda side-runtime install

Downloaded missing ONNX Runtime CUDA provider sidecars when the compiled tiny-model side runtime is used with PI_TINY_DEVICE=cuda.

Preserved actionable CUDA worker diagnostics in tiny-models text output and added focused regression coverage.

Fixes #4475
This commit is contained in:
roboomp
2026-07-03 18:14:34 +00:00
parent d0c1890a6c
commit f257fd533f
6 changed files with 325 additions and 11 deletions
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Fixed
- Fixed Linux x64 `PI_TINY_DEVICE=cuda` tiny-model side runtimes missing ONNX Runtime CUDA provider binaries by repairing the `onnxruntime-node` CUDA sidecar install and preserving actionable CUDA diagnostics in `omp tiny-models download` output ([#4475](https://github.com/can1357/oh-my-pi/issues/4475)).
## [16.3.4] - 2026-07-03
### Fixed
@@ -35,12 +35,19 @@ function writeLine(text = ""): void {
process.stdout.write(`${text}\n`);
}
const ACTIONABLE_DOWNLOAD_ERROR_LINE = /PI_TINY_|CUDA|cuDNN|cudnn|libcudnn|tiny-title-runtime|onnxruntime-node/i;
function downloadErrorSummary(error: string | undefined): string | undefined {
return error
?.split(/\r?\n/)
.map(line => line.trim())
.find(line => line.length > 0)
?.replace(/^Error:\s*/, "");
const lines =
error
?.split(/\r?\n/)
.map(line => line.trim().replace(/^Error:\s*/, ""))
.filter(line => line.length > 0) ?? [];
const first = lines[0];
if (!first) return undefined;
const details = lines.slice(1).filter(line => ACTIONABLE_DOWNLOAD_ERROR_LINE.test(line));
if (details.length === 0) return first;
return [first, ...details].join("\n");
}
export function resolveModels(model: string | undefined): TinyLocalModelKey[] {
@@ -1,3 +1,4 @@
import * as fsp from "node:fs/promises";
import { createRequire } from "node:module";
import * as path from "node:path";
import type { ProgressInfo } from "@huggingface/transformers";
@@ -27,6 +28,15 @@ import packageJson from "../../package.json" with { type: "json" };
export const TRANSFORMERS_PACKAGE = "@huggingface/transformers";
const COMPILED_TRANSFORMERS_VERSION = process.env.PI_TINY_TRANSFORMERS_VERSION;
const ONNX_RUNTIME_NODE_PACKAGE = "onnxruntime-node";
const ONNX_RUNTIME_CUDA_INSTALL = "cuda12";
const ONNX_RUNTIME_CUDA_PROVIDER_FILES = [
"libonnxruntime_providers_cuda.so",
"libonnxruntime_providers_shared.so",
"libonnxruntime_providers_tensorrt.so",
] as const;
const LINUX_X64_ONNX_RUNTIME_CUDA_PROVIDER_DIR = path.join("bin", "napi-v6", "linux", "x64");
const sourceRequire = createRequire(import.meta.url);
// ── Error serialization ─────────────────────────────────────────────
@@ -162,6 +172,82 @@ export async function installSharpStubResolver(runtimeDir: string): Promise<stri
return nodeModules;
}
function shouldInstallOnnxRuntimeCudaProviders(device: string | undefined): boolean {
return process.platform === "linux" && process.arch === "x64" && device?.trim().toLowerCase() === "cuda";
}
async function missingOnnxRuntimeCudaProviderFiles(binDir: string): Promise<string[]> {
const missing: string[] = [];
for (const file of ONNX_RUNTIME_CUDA_PROVIDER_FILES) {
try {
await fsp.access(path.join(binDir, file));
} catch {
missing.push(file);
}
}
return missing;
}
async function readPipe(stream: ReadableStream<Uint8Array> | null): Promise<string> {
if (!stream) return "";
return new Response(stream).text();
}
async function installOnnxRuntimeCudaProviders(packageDir: string, runtimeDir: string, binDir: string): Promise<void> {
const script = path.join(packageDir, "script", "install.js");
try {
await fsp.access(script);
} catch {
throw new Error(
`ONNX Runtime CUDA provider binaries are missing from ${binDir}, and ${script} is unavailable. Remove the tiny-model side runtime cache at ${runtimeDir} and retry.`,
);
}
const proc = Bun.spawn([process.execPath, script], {
cwd: runtimeDir,
env: { ...Bun.env, BUN_BE_BUN: "1", ONNXRUNTIME_NODE_INSTALL: ONNX_RUNTIME_CUDA_INSTALL },
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([
readPipe(proc.stdout as ReadableStream<Uint8Array> | null),
readPipe(proc.stderr as ReadableStream<Uint8Array> | null),
proc.exited,
]);
if (exitCode !== 0) {
const output = `${stdout}\n${stderr}`.trim();
throw new Error(
`Failed to install ONNX Runtime CUDA provider binaries into ${binDir} with ${process.execPath} ${script} (exit ${exitCode}). Remove the tiny-model side runtime cache at ${runtimeDir} and retry with network access. ${output}`,
);
}
}
/**
* Repairs the compiled Transformers side runtime when CUDA was requested and
* Bun skipped `onnxruntime-node`'s NuGet sidecar install.
*/
export async function ensureOnnxRuntimeCudaProviders(
runtimeDir: string,
device = process.env.PI_TINY_DEVICE,
): Promise<void> {
if (!shouldInstallOnnxRuntimeCudaProviders(device)) return;
const nodeModules = path.join(runtimeDir, "node_modules");
const manifest = resolveRuntimeModule(nodeModules, `${ONNX_RUNTIME_NODE_PACKAGE}/package.json`);
if (!manifest)
throw new Error(`Unable to resolve ${ONNX_RUNTIME_NODE_PACKAGE} in compiled runtime at ${nodeModules}`);
const packageDir = path.dirname(manifest);
const binDir = path.join(packageDir, LINUX_X64_ONNX_RUNTIME_CUDA_PROVIDER_DIR);
const missing = await missingOnnxRuntimeCudaProviderFiles(binDir);
if (missing.length === 0) return;
await installOnnxRuntimeCudaProviders(packageDir, runtimeDir, binDir);
const stillMissing = await missingOnnxRuntimeCudaProviderFiles(binDir);
if (stillMissing.length === 0) return;
throw new Error(
`ONNX Runtime CUDA provider install completed but ${stillMissing.join(", ")} are still missing from ${binDir}. Remove the tiny-model side runtime cache at ${runtimeDir} and retry.`,
);
}
/**
* Prepare a freshly-installed compiled runtime for loading and return the
* absolute entrypoint of `packageName` to `require`.
@@ -211,6 +297,98 @@ interface ConfigurableTransformers {
LogLevel: { ERROR: unknown };
}
export interface TransformersRuntimeMetadata {
__ompRuntimeNodeModules?: string;
__ompTransformersEntry?: string;
}
function attachTransformersRuntimeMetadata<T extends ConfigurableTransformers>(
transformers: T,
metadata: TransformersRuntimeMetadata,
): T {
const runtime = transformers as T & TransformersRuntimeMetadata;
runtime.__ompRuntimeNodeModules = metadata.__ompRuntimeNodeModules;
runtime.__ompTransformersEntry = metadata.__ompTransformersEntry;
return runtime;
}
const TRANSITIVE_CUDA_LIBRARY_RE =
/\b(lib(?:cu|nv)[A-Za-z0-9_.+-]*\.so(?:\.[0-9]+)*)\b[^:\n]*:\s*cannot open shared object file/iu;
const CUDA_DEVICE_UNAVAILABLE_RE = /\bCUDA failure 100\b|no CUDA-capable device is detected|cudaSetDevice|GPU=-1/iu;
function cudaDeviceUnavailable(error: unknown): boolean {
return CUDA_DEVICE_UNAVAILABLE_RE.test(errorText(error));
}
function missingCudaLibrary(error: unknown): string | undefined {
return TRANSITIVE_CUDA_LIBRARY_RE.exec(errorText(error))?.[1];
}
function cudaFailureCause(error: unknown, missingFiles: readonly string[]): string {
if (missingFiles.length > 0) return `missing ONNX Runtime CUDA provider file(s): ${missingFiles.join(", ")}`;
const missingLibrary = missingCudaLibrary(error);
if (missingLibrary) return `${missingLibrary}: cannot open shared object file`;
if (cudaDeviceUnavailable(error)) {
return "CUDA provider files are present; CUDA runtime reports no CUDA-capable device";
}
return "CUDA provider files are present; inspect the original ONNX Runtime CUDA error";
}
function cudaFailureHint(error: unknown, missingFiles: readonly string[]): string {
if (missingFiles.length > 0) return "reinstall the tiny side runtime with ONNX Runtime postinstall enabled";
if (missingCudaLibrary(error)) {
return "install the matching CUDA/cuDNN shared libraries and expose them on the dynamic loader path";
}
if (cudaDeviceUnavailable(error)) {
return "make the NVIDIA GPU visible to this process/session, or use providers.tinyModelDevice=default/cpu";
}
return "check the host CUDA driver, device visibility, and ONNX Runtime CUDA compatibility";
}
function resolveOnnxRuntimePackageDir(metadata: TransformersRuntimeMetadata): string | null {
const entry = metadata.__ompTransformersEntry;
if (entry) {
try {
return path.dirname(createRequire(entry).resolve(`${ONNX_RUNTIME_NODE_PACKAGE}/package.json`));
} catch {
// Fall through to the side-runtime resolver below.
}
}
const nodeModules = metadata.__ompRuntimeNodeModules;
if (!nodeModules) return null;
const manifest = resolveRuntimeModule(nodeModules, `${ONNX_RUNTIME_NODE_PACKAGE}/package.json`);
return manifest ? path.dirname(manifest) : null;
}
export async function formatOnnxRuntimeCudaDiagnostics(
metadata: TransformersRuntimeMetadata,
requestedDevice: string,
error: unknown,
): Promise<string | null> {
const device = requestedDevice.trim().toLowerCase();
if (device !== "cuda" && device !== "gpu" && device !== "auto") return null;
if (process.platform !== "linux" || process.arch !== "x64") return null;
const packageDir = resolveOnnxRuntimePackageDir(metadata);
if (!packageDir) {
return [
"ONNX Runtime CUDA diagnostics:",
` PI_TINY_DEVICE=${requestedDevice} requested CUDAExecutionProvider`,
" cause: unable to resolve onnxruntime-node in the tiny-model runtime",
].join("\n");
}
const binDir = path.join(packageDir, LINUX_X64_ONNX_RUNTIME_CUDA_PROVIDER_DIR);
const missingFiles = await missingOnnxRuntimeCudaProviderFiles(binDir);
const sideRuntime = metadata.__ompRuntimeNodeModules;
const lines = [
"ONNX Runtime CUDA diagnostics:",
` PI_TINY_DEVICE=${requestedDevice} requested CUDAExecutionProvider`,
sideRuntime ? ` side runtime: ${sideRuntime}` : ` onnxruntime-node: ${packageDir}`,
` cause: ${cudaFailureCause(error, missingFiles)}`,
];
lines.push(` hint: ${cudaFailureHint(error, missingFiles)}`);
return lines.join("\n");
}
function configureTransformers<T extends ConfigurableTransformers>(transformers: T): T {
transformers.env.cacheDir = getTinyModelsCacheDir();
transformers.env.allowLocalModels = false;
@@ -251,7 +429,12 @@ export function loadTransformersRuntime<T extends ConfigurableTransformers, K>(
runtimeDir: () => string,
): Promise<T> {
return holder.load(async () => {
if (!isCompiledBinary()) return configureTransformers(sourceRequire(TRANSFORMERS_PACKAGE) as T);
if (!isCompiledBinary()) {
const entry = sourceRequire.resolve(TRANSFORMERS_PACKAGE);
return attachTransformersRuntimeMetadata(configureTransformers(sourceRequire(entry) as T), {
__ompTransformersEntry: entry,
});
}
const installedDir = await ensureRuntimeInstalled({
runtimeDir: runtimeDir(),
install: {
@@ -270,8 +453,12 @@ export function loadTransformersRuntime<T extends ConfigurableTransformers, K>(
},
}),
});
await ensureOnnxRuntimeCudaProviders(installedDir);
const entry = await prepareCompiledRuntime(installedDir, TRANSFORMERS_PACKAGE);
const require_ = createRequire(entry);
return configureTransformers(require_(entry) as T);
return attachTransformersRuntimeMetadata(configureTransformers(require_(entry) as T), {
__ompRuntimeNodeModules: path.join(installedDir, "node_modules"),
__ompTransformersEntry: entry,
});
});
}
+14 -4
View File
@@ -10,12 +10,14 @@ import tinyTitleSystemPrompt from "../prompts/system/tiny-title-system.md" with
import {
errorMessage,
errorText,
formatOnnxRuntimeCudaDiagnostics,
getTransformersVersionSpec,
loadTransformersRuntime,
MemoizedRuntime,
replayCachedReady,
sendLog,
sendProgress,
type TransformersRuntimeMetadata,
} from "../subprocess/worker-runtime";
import { resolveTinyModelDevicePreference, type TinyModelDevice, tinyModelDeviceLoadOrder } from "./device";
import { resolveTinyModelDtypeOverride, type TinyModelDtype } from "./dtype";
@@ -39,7 +41,7 @@ const TINY_TITLE_SYSTEM_PROMPT = prompt.render(tinyTitleSystemPrompt);
const tinyModelDevicePreference = resolveTinyModelDevicePreference();
const tinyModelDtypeOverride = resolveTinyModelDtypeOverride();
interface TransformersRuntime {
interface TransformersRuntime extends TransformersRuntimeMetadata {
env: {
cacheDir?: string;
allowLocalModels?: boolean;
@@ -136,6 +138,7 @@ async function loadPipelineWithDeviceFallback(
device: devices[0],
});
}
let cudaDiagnostics: string | null = null;
for (let i = 0; i < devices.length; i += 1) {
const device = devices[i]!;
try {
@@ -144,15 +147,22 @@ async function loadPipelineWithDeviceFallback(
device,
};
} catch (error) {
if (i === devices.length - 1) throw error;
const deviceDiagnostics = await formatOnnxRuntimeCudaDiagnostics(transformers, device, error);
if (deviceDiagnostics) cudaDiagnostics = deviceDiagnostics;
if (i === devices.length - 1) {
if (cudaDiagnostics) throw new Error(`${errorText(error)}\n${cudaDiagnostics}`);
throw error;
}
const fallbackDevice = devices[i + 1]!;
sendLog(transport, "warn", "tiny-model: accelerated device failed; falling back", {
const meta: Record<string, unknown> = {
modelKey,
repo: spec.repo,
device,
fallbackDevice,
error: errorMessage(error),
});
};
if (deviceDiagnostics) meta.cudaDiagnostics = deviceDiagnostics;
sendLog(transport, "warn", "tiny-model: accelerated device failed; falling back", meta);
}
}
throw new Error("No tiny model devices configured");
@@ -75,4 +75,40 @@ describe("tiny-models download model resolution", () => {
expect(output.join("")).toContain("Failed to download LFM2 700M: runtime install failed.");
});
it("prints actionable CUDA provider diagnostics from worker errors", async () => {
const output: string[] = [];
const isTtyDescriptor = Object.getOwnPropertyDescriptor(process.stdout, "isTTY");
const diagnostic = [
"Error: Failed to load ONNX Runtime CUDA execution provider",
"ONNX Runtime CUDA diagnostics:",
" PI_TINY_DEVICE=cuda requested CUDAExecutionProvider",
" side runtime: /home/user/.omp/cache/tiny-title-runtime/transformers-test/node_modules",
" cause: libcudnn.so.9: cannot open shared object file",
].join("\n");
Object.defineProperty(process.stdout, "isTTY", { configurable: true, value: false });
spyOn(process.stdout, "write").mockImplementation((chunk: string | Uint8Array) => {
output.push(typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk));
return true;
});
spyOn(tinyTitleClient, "downloadModel").mockResolvedValue({
ok: false,
error: diagnostic,
});
try {
await expect(runTinyModelsCommand({ action: "download", model: "lfm2-700m", flags: {} })).rejects.toThrow(
"One or more tiny title models failed to download",
);
} finally {
if (isTtyDescriptor) Object.defineProperty(process.stdout, "isTTY", isTtyDescriptor);
else Reflect.deleteProperty(process.stdout, "isTTY");
}
const text = output.join("");
expect(text).toContain("Failed to download LFM2 700M:");
expect(text).toContain("PI_TINY_DEVICE=cuda");
expect(text).toContain("libcudnn.so.9");
expect(text).toContain("tiny-title-runtime");
});
});
@@ -0,0 +1,70 @@
import { afterEach, describe, expect, it } from "bun:test";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { ensureOnnxRuntimeCudaProviders, formatOnnxRuntimeCudaDiagnostics } from "../src/subprocess/worker-runtime";
const tempDirs: string[] = [];
const CUDA_PROVIDER_FILES = [
"libonnxruntime_providers_cuda.so",
"libonnxruntime_providers_shared.so",
"libonnxruntime_providers_tensorrt.so",
];
afterEach(async () => {
await Promise.all(tempDirs.splice(0).map(dir => fs.rm(dir, { recursive: true, force: true })));
});
async function makeRuntimeWithOnnxInstallScript(): Promise<string> {
const runtimeDir = await fs.mkdtemp(path.join(os.tmpdir(), "omp-tiny-runtime-install-"));
tempDirs.push(runtimeDir);
const packageDir = path.join(runtimeDir, "node_modules", "onnxruntime-node");
await Bun.write(
path.join(packageDir, "package.json"),
JSON.stringify({ name: "onnxruntime-node", version: "1.24.3", main: "dist/index.js" }),
);
await Bun.write(path.join(packageDir, "dist", "index.js"), "module.exports = {};\n");
await Bun.write(
path.join(packageDir, "script", "install.js"),
[
"const fs = require('node:fs');",
"const path = require('node:path');",
"if (process.env.ONNXRUNTIME_NODE_INSTALL !== 'cuda12') process.exit(2);",
"const dir = path.join(__dirname, '..', 'bin', 'napi-v6', 'linux', 'x64');",
"fs.mkdirSync(dir, { recursive: true });",
`for (const file of ${JSON.stringify(CUDA_PROVIDER_FILES)}) fs.writeFileSync(path.join(dir, file), '');`,
].join("\n"),
);
return runtimeDir;
}
describe("tiny runtime CUDA provider repair", () => {
it("runs onnxruntime-node's cuda12 installer when compiled runtime sidecars are missing", async () => {
if (process.platform !== "linux" || process.arch !== "x64") return;
const runtimeDir = await makeRuntimeWithOnnxInstallScript();
await ensureOnnxRuntimeCudaProviders(runtimeDir, "cuda");
const binDir = path.join(runtimeDir, "node_modules", "onnxruntime-node", "bin", "napi-v6", "linux", "x64");
for (const file of CUDA_PROVIDER_FILES) {
expect(await Bun.file(path.join(binDir, file)).exists()).toBe(true);
}
});
it("reports CUDA device visibility failures after provider sidecars exist", async () => {
if (process.platform !== "linux" || process.arch !== "x64") return;
const runtimeDir = await makeRuntimeWithOnnxInstallScript();
await ensureOnnxRuntimeCudaProviders(runtimeDir, "cuda");
const diagnostic = await formatOnnxRuntimeCudaDiagnostics(
{ __ompRuntimeNodeModules: path.join(runtimeDir, "node_modules") },
"cuda",
new Error(
"CUDA failure 100: no CUDA-capable device is detected ; GPU=-1 ; expr=cudaSetDevice(info_.device_id);",
),
);
expect(diagnostic).toContain("CUDA runtime reports no CUDA-capable device");
expect(diagnostic).toContain("make the NVIDIA GPU visible to this process/session");
});
});