Skip to content

Commit 2a93abe

Browse files
committed
feat(inspect gpu command): detect and report missing prebuilt binary modules and custom npm registry
1 parent d9bc17b commit 2a93abe

4 files changed

Lines changed: 248 additions & 5 deletions

File tree

src/bindings/utils/compileLLamaCpp.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -531,7 +531,21 @@ async function resolvePrebuiltBinaryPath(prebuiltBinaryDirectoryPath: string) {
531531
return null;
532532
}
533533

534-
function getPrebuiltBinariesPackageDirectoryForBuildOptions(buildOptions: BuildOptions) {
534+
export async function checkWhetherPrebuiltBinariesModuleIsInstalled(gpu: BuildGpu) {
535+
const prebuiltBinariesPaths = await getPrebuiltBinariesPackageDirectoryForBuildOptions({
536+
platform: getPlatform(),
537+
arch: process.arch,
538+
gpu
539+
});
540+
541+
return prebuiltBinariesPaths != null;
542+
}
543+
544+
function getPrebuiltBinariesPackageDirectoryForBuildOptions(buildOptions: {
545+
platform: BinaryPlatform,
546+
arch: typeof process.arch,
547+
gpu: BuildGpu
548+
}) {
535549
async function getBinariesPathFromModules(moduleImport: () => Promise<{getBinsDir(): {binsDir: string, packageVersion: string}}>) {
536550
try {
537551
const [

src/cli/commands/inspect/commands/InspectGpuCommand.ts

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import {isRunningUnderRosetta} from "../../../utils/isRunningUnderRosetta.js";
1616
import {toBytes} from "../../../utils/toBytes.js";
1717
import {getBinariesGithubRelease} from "../../../../bindings/utils/binariesGithubRelease.js";
1818
import {getClonedLlamaCppRepoReleaseInfo} from "../../../../bindings/utils/cloneLlamaCppRepo.js";
19+
import {checkWhetherPrebuiltBinariesModuleIsInstalled} from "../../../../bindings/utils/compileLLamaCpp.js";
20+
import {getCurrentNpmrcConfig, getNpmrcRegistry} from "../../../utils/resolveNpmrcConfig.js";
1921

2022
type InspectGpuCommand = {
2123
// no options for now
@@ -34,6 +36,7 @@ export const InspectGpuCommand: CommandModule<object, InspectGpuCommand> = {
3436
const gpusToLogVramUsageOf: BuildGpu[] = [];
3537
const gpuToLlama = new Map<BuildGpu, Llama | undefined>();
3638
let lastLlama: Llama | undefined;
39+
let missingPrebuiltBinaryModules: boolean = false;
3740

3841
async function loadLlamaForGpu(gpu: BuildGpu) {
3942
if (!gpuToLlama.has(gpu)) {
@@ -113,7 +116,11 @@ export const InspectGpuCommand: CommandModule<object, InspectGpuCommand> = {
113116
const llama = await loadLlamaForGpu("metal");
114117

115118
if (llama == null) {
116-
console.info(`${chalk.yellow("Metal:")} ${chalk.red("Metal is detected, but using it failed")}`);
119+
if (!(await checkWhetherPrebuiltBinariesModuleIsInstalled("metal"))) {
120+
console.info(`${chalk.yellow("Metal:")} ${chalk.red("The Metal prebuilt binaries module is missing")}`);
121+
missingPrebuiltBinaryModules = true;
122+
} else
123+
console.info(`${chalk.yellow("Metal:")} ${chalk.red("Metal is detected, but using it failed")}`);
117124
} else {
118125
console.info(`${chalk.yellow("Metal:")} ${chalk.green("available")}`);
119126
gpusToLogVramUsageOf.push("metal");
@@ -134,7 +141,11 @@ export const InspectGpuCommand: CommandModule<object, InspectGpuCommand> = {
134141

135142
const llama = await loadLlamaForGpu(false);
136143
if (llama == null) {
137-
console.info(`${chalk.yellow("CPU:")} ${chalk.red("Loading a binding with only CPU support failed")}`);
144+
if (!(await checkWhetherPrebuiltBinariesModuleIsInstalled(false))) {
145+
console.info(`${chalk.yellow("CPU:")} ${chalk.red("The CPU-only prebuilt binaries module is missing")}`);
146+
missingPrebuiltBinaryModules = true;
147+
} else
148+
console.info(`${chalk.yellow("CPU:")} ${chalk.red("Loading a binding with only CPU support failed")}`);
138149
}
139150
}
140151

@@ -148,7 +159,12 @@ export const InspectGpuCommand: CommandModule<object, InspectGpuCommand> = {
148159
const llama = await loadLlamaForGpu("cuda");
149160

150161
if (llama == null) {
151-
console.info(`${chalk.yellow("CUDA:")} ${chalk.red("CUDA is detected, but using it failed")}`);
162+
if (!(await checkWhetherPrebuiltBinariesModuleIsInstalled("cuda"))) {
163+
console.info(`${chalk.yellow("CUDA:")} ${chalk.red("The CUDA prebuilt binaries modules are missing")}`);
164+
missingPrebuiltBinaryModules = true;
165+
} else
166+
console.info(`${chalk.yellow("CUDA:")} ${chalk.red("CUDA is detected, but using it failed")}`);
167+
152168
console.info(chalk.yellow("To resolve errors related to CUDA, see the CUDA guide: ") + documentationPageUrls.CUDA);
153169
} else {
154170
console.info(`${chalk.yellow("CUDA:")} ${chalk.green("available")}`);
@@ -163,7 +179,12 @@ export const InspectGpuCommand: CommandModule<object, InspectGpuCommand> = {
163179
const llama = await loadLlamaForGpu("vulkan");
164180

165181
if (llama == null) {
166-
console.info(`${chalk.yellow("Vulkan:")} ${chalk.red("Vulkan is detected, but using it failed")}`);
182+
if (!(await checkWhetherPrebuiltBinariesModuleIsInstalled("vulkan"))) {
183+
console.info(`${chalk.yellow("Vulkan:")} ${chalk.red("The Vulkan prebuilt binaries module is missing")}`);
184+
missingPrebuiltBinaryModules = true;
185+
} else
186+
console.info(`${chalk.yellow("Vulkan:")} ${chalk.red("Vulkan is detected, but using it failed")}`);
187+
167188
console.info(chalk.yellow("To resolve errors related to Vulkan, see the Vulkan guide: ") + documentationPageUrls.Vulkan);
168189
} else {
169190
console.info(`${chalk.yellow("Vulkan:")} ${chalk.green("available")}`);
@@ -194,6 +215,39 @@ export const InspectGpuCommand: CommandModule<object, InspectGpuCommand> = {
194215
if (lastLlama != null) {
195216
await logSwapUsage(lastLlama);
196217
console.info(`${chalk.yellow("mmap:")} ${lastLlama.supportsMmap ? "supported" : "unsupported"}`);
218+
} else {
219+
if (!(await checkWhetherPrebuiltBinariesModuleIsInstalled(false))) {
220+
console.info();
221+
console.info(chalk.yellow("The CPU-only prebuilt binaries module is missing"));
222+
missingPrebuiltBinaryModules = true;
223+
}
224+
}
225+
226+
if (missingPrebuiltBinaryModules) {
227+
const npmrcConfig = await getCurrentNpmrcConfig();
228+
const npmRegistry = getNpmrcRegistry(npmrcConfig);
229+
if (!npmRegistry.isDefault) {
230+
console.info();
231+
console.info(chalk.yellow("npm registry: ") + npmRegistry.registryUrl);
232+
console.info(
233+
chalk.yellow(
234+
"It seems that you have a custom npm registry configured. " +
235+
"The prebuilt binary modules may be missing in that registry. " + (
236+
"Consider switching to the default npm registry" +
237+
(
238+
lastLlama != null
239+
? ""
240+
: " or building from source"
241+
) +
242+
" if you're having issues."
243+
)
244+
)
245+
);
246+
247+
// only show a link to the building from source guide when a URL for the CUDA or Vulkan guides was not already provided
248+
if (lastLlama == null)
249+
console.info(chalk.yellow("To build from source, see the Building From Source guide: ") + documentationPageUrls.BuildingFromSource);
250+
}
197251
}
198252
}
199253
};
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import os from "node:os";
2+
import path from "node:path";
3+
import fs from "fs-extra";
4+
import {getPlatform} from "../../bindings/utils/getPlatform.js";
5+
6+
const defaultNpmRegistry = "https://registry.npmjs.org/";
7+
8+
export async function getCurrentNpmrcConfig() {
9+
const layers = await getNpmConfigLayers(process.cwd());
10+
11+
const mergedConfig: Record<string, string> = {
12+
...layers.builtin,
13+
...layers.global,
14+
...layers.user,
15+
...layers.project,
16+
...layers.env
17+
};
18+
19+
return mergedConfig;
20+
}
21+
22+
export function getNpmrcRegistry(npmrcConfig: Record<string, string>) {
23+
const registryUrl = npmrcConfig.registry ?? defaultNpmRegistry;
24+
let cleanRegistryUrl: string = registryUrl;
25+
26+
try {
27+
const url = new URL(registryUrl);
28+
url.search = "";
29+
url.hash = "";
30+
cleanRegistryUrl = url.href;
31+
} catch (err) {
32+
// do nothing
33+
}
34+
35+
if (!cleanRegistryUrl.endsWith("/"))
36+
cleanRegistryUrl += "/";
37+
38+
return {
39+
isDefault: cleanRegistryUrl === defaultNpmRegistry,
40+
registryUrl,
41+
cleanRegistryUrl: cleanRegistryUrl
42+
};
43+
}
44+
45+
async function findNearestProjectNpmrc(startDir: string): Promise<string | undefined> {
46+
let currentDirPath = path.resolve(startDir);
47+
48+
while (true) {
49+
const npmrcPath = path.join(currentDirPath, ".npmrc");
50+
if (await fs.pathExists(npmrcPath))
51+
return npmrcPath;
52+
53+
const parentDirPath = path.dirname(currentDirPath);
54+
if (parentDirPath === currentDirPath)
55+
return undefined;
56+
57+
currentDirPath = parentDirPath;
58+
}
59+
}
60+
61+
function parseNpmrc(content: string, env: NodeJS.ProcessEnv = process.env): Record<string, string> {
62+
const result: Record<string, string> = {};
63+
64+
for (const rawLine of content.split(/\r?\n/u)) {
65+
const line = rawLine.trim();
66+
67+
if (line === "" || line.startsWith(";") || line.startsWith("#"))
68+
continue;
69+
70+
const eqIndex = line.indexOf("=");
71+
if (eqIndex <= 0)
72+
continue;
73+
74+
const key = line.slice(0, eqIndex).trim();
75+
const value = line.slice(eqIndex + 1)
76+
.trim()
77+
.replace(/\$\{([^}]+)\}/gu, (match: string, envVarName: string) => (env[envVarName] ?? ""));
78+
79+
result[key] = value;
80+
}
81+
82+
return result;
83+
}
84+
85+
async function readNpmrc(filePath: string | undefined): Promise<Record<string, string>> {
86+
if (filePath == null || !(await fs.pathExists(filePath)))
87+
return {};
88+
89+
return parseNpmrc(fs.readFileSync(filePath, "utf8"));
90+
}
91+
92+
async function getDefaultGlobalConfigPath(npmPrefixConfig?: string): Promise<string | undefined> {
93+
const prefix = npmPrefixConfig ?? process.env.PREFIX;
94+
95+
if (prefix != null && prefix !== "")
96+
return path.join(prefix, "etc", "npmrc");
97+
98+
const platform = getPlatform();
99+
if (platform === "win") {
100+
const appData = process.env.APPDATA;
101+
if (appData != null && appData !== "")
102+
return path.join(appData, "npm", "etc", "npmrc");
103+
} else if (platform === "mac") {
104+
const npmrcLocations = [
105+
"/opt/homebrew/etc/npmrc",
106+
"/usr/local/etc/npmrc"
107+
];
108+
109+
for (const candidate of npmrcLocations) {
110+
if (await fs.pathExists(candidate))
111+
return candidate;
112+
}
113+
} else if (platform === "linux")
114+
return "/etc/npmrc";
115+
116+
return undefined;
117+
}
118+
119+
async function getNpmConfigLayers(startDir: string): Promise<NpmConfigLayers> {
120+
const envConfig: Record<string, string> = {};
121+
122+
for (const [key, value] of Object.entries(process.env)) {
123+
if (value == null)
124+
continue;
125+
126+
const lowerKey = key.toLowerCase();
127+
if (lowerKey.startsWith("npm_config_")) {
128+
const configKey = lowerKey.slice("npm_config_".length).replaceAll("_", "-");
129+
envConfig[configKey] = value;
130+
}
131+
}
132+
133+
const globalConfigPath = envConfig["globalconfig"] ?? await getDefaultGlobalConfigPath(envConfig["prefix"]);
134+
const userConfigPath = envConfig["userconfig"] ?? path.join(os.homedir(), ".npmrc");
135+
const projectConfigPath = await findNearestProjectNpmrc(startDir);
136+
137+
const [
138+
globalConfig,
139+
userConfig,
140+
projectConfig
141+
] = await Promise.all([
142+
await readNpmrc(globalConfigPath),
143+
await readNpmrc(userConfigPath),
144+
await readNpmrc(projectConfigPath)
145+
]);
146+
147+
return {
148+
builtin: {
149+
registry: defaultNpmRegistry
150+
},
151+
global: globalConfig,
152+
user: userConfig,
153+
project: projectConfig,
154+
env: envConfig,
155+
paths: {
156+
project: projectConfigPath,
157+
user: userConfigPath,
158+
global: globalConfigPath
159+
}
160+
};
161+
}
162+
163+
export type NpmConfigLayers = {
164+
builtin: Record<string, string>,
165+
global: Record<string, string>,
166+
user: Record<string, string>,
167+
project: Record<string, string>,
168+
env: Record<string, string>,
169+
paths: {
170+
project: string | undefined,
171+
user: string | undefined,
172+
global: string | undefined
173+
}
174+
};

src/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ const documentationCliUrl = documentationUrl + "/cli";
102102
export const documentationPageUrls = {
103103
CUDA: documentationUrl + "/guide/CUDA",
104104
Vulkan: documentationUrl + "/guide/Vulkan",
105+
BuildingFromSource: documentationUrl + "/guide/building-from-source",
105106
CLI: {
106107
index: documentationCliUrl,
107108
Pull: documentationCliUrl + "/pull",

0 commit comments

Comments
 (0)