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
94 changes: 94 additions & 0 deletions src/lib/onboard/nim-image-compat-warning.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { describe, expect, it, vi } from "vitest";

import {
formatArm64NimImageCompatibilityWarning,
shouldWarnAboutArm64NimImageCompatibility,
warnAboutArm64NimImageCompatibility,
} from "./nim-image-compat-warning";

describe("arm64 NIM image compatibility warning", () => {
it("warns only when Local NIM is available on Linux arm64 DGX platforms", () => {
expect(
shouldWarnAboutArm64NimImageCompatibility({
arch: "arm64",
platform: "linux",
gpu: { platform: "spark" },
nimLocalAvailable: true,
}),
).toBe(true);
expect(
shouldWarnAboutArm64NimImageCompatibility({
arch: "arm64",
platform: "linux",
gpu: { spark: true },
nimLocalAvailable: true,
}),
).toBe(true);
expect(
shouldWarnAboutArm64NimImageCompatibility({
arch: "arm64",
platform: "linux",
gpu: { platform: "station" },
nimLocalAvailable: true,
}),
).toBe(true);
expect(
shouldWarnAboutArm64NimImageCompatibility({
arch: "x64",
platform: "linux",
gpu: { platform: "spark" },
nimLocalAvailable: true,
}),
).toBe(false);
expect(
shouldWarnAboutArm64NimImageCompatibility({
arch: "arm64",
platform: "linux",
gpu: { platform: "linux" },
nimLocalAvailable: true,
}),
).toBe(false);
expect(
shouldWarnAboutArm64NimImageCompatibility({
arch: "arm64",
platform: "linux",
gpu: { platform: "spark" },
nimLocalAvailable: false,
}),
).toBe(false);
});

it("describes image/platform compatibility without claiming Local NIM will fail", () => {
const lines = formatArm64NimImageCompatibilityWarning({ gpu: { platform: "station" } });

expect(lines.join("\n")).toContain("Linux arm64 DGX Station");
expect(lines.join("\n")).toContain("linux/arm64 manifests");
expect(lines.join("\n")).toContain("will try the selected image/platform digest");
expect(lines.join("\n")).not.toMatch(/will fail|does not work/i);
});

it("prints the warning once through the logger", () => {
const log = vi.fn();

expect(
warnAboutArm64NimImageCompatibility({
arch: "arm64",
platform: "linux",
gpu: { platform: "spark" },
nimLocalAvailable: true,
log,
}),
).toBe(true);

expect(log.mock.calls.map((call) => call[0])).toEqual([
"",
" Warning: Local NVIDIA NIM is experimental on Linux arm64 DGX Spark hosts.",
" Some NIM images may not publish linux/arm64 manifests.",
" NemoClaw will try the selected image/platform digest when possible; if Docker reports no matching platform, choose NVIDIA Endpoints, vLLM, or another provider.",
"",
]);
});
});
52 changes: 52 additions & 0 deletions src/lib/onboard/nim-image-compat-warning.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import type { GpuDetection } from "../inference/nim";

type Logger = (message?: string) => void;

export interface NimImageCompatibilityWarningInput {
arch?: NodeJS.Architecture;
gpu: Pick<GpuDetection, "platform" | "spark"> | null | undefined;
nimLocalAvailable: boolean;
platform?: NodeJS.Platform;
}

const ARM64_DGX_NIM_PLATFORMS = new Set(["spark", "station"]);

export function shouldWarnAboutArm64NimImageCompatibility({
arch = process.arch,
gpu,
nimLocalAvailable,
platform = process.platform,
}: NimImageCompatibilityWarningInput): boolean {
if (!nimLocalAvailable || platform !== "linux" || arch !== "arm64") return false;
return gpu?.spark === true || (gpu?.platform ? ARM64_DGX_NIM_PLATFORMS.has(gpu.platform) : false);
}

function dgxPlatformLabel(gpu: NimImageCompatibilityWarningInput["gpu"]): string {
if (gpu?.platform === "station") return "DGX Station";
return "DGX Spark";
}

export function formatArm64NimImageCompatibilityWarning(
input: Pick<NimImageCompatibilityWarningInput, "gpu">,
): string[] {
const hostLabel = dgxPlatformLabel(input.gpu);
return [
` Warning: Local NVIDIA NIM is experimental on Linux arm64 ${hostLabel} hosts.`,
" Some NIM images may not publish linux/arm64 manifests.",
" NemoClaw will try the selected image/platform digest when possible; if Docker reports no matching platform, choose NVIDIA Endpoints, vLLM, or another provider.",
];
}

export function warnAboutArm64NimImageCompatibility(
input: NimImageCompatibilityWarningInput & { log?: Logger },
): boolean {
if (!shouldWarnAboutArm64NimImageCompatibility(input)) return false;
const log = input.log ?? console.log;
log("");
for (const line of formatArm64NimImageCompatibilityWarning(input)) log(line);
log("");
return true;
}
10 changes: 9 additions & 1 deletion src/lib/onboard/provider-host-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
getWindowsHostOllamaDockerRequirement,
type WindowsHostOllamaDockerRequirement,
} from "./local-inference-topology";
import { warnAboutArm64NimImageCompatibility } from "./nim-image-compat-warning";
import { resolveOllamaInstallMenuEntry, type OllamaInstallMenuResult } from "./ollama-install-menu";
import { buildVllmMenuEntries, type VllmMenuEntry } from "./vllm-menu";
import { detectWindowsHostOllama, type WindowsHostOllamaState } from "./windows-host-ollama";
Expand Down Expand Up @@ -183,6 +184,13 @@ export function detectInferenceProviderHostState(
runCapture: deps.runCapture,
log,
});
const gpuNimCapable = Boolean(input.gpu?.nimCapable);
warnAboutArm64NimImageCompatibility({
gpu: input.gpu,
nimLocalAvailable: input.experimental && gpuNimCapable,
platform,
log,
});

const ollamaInstallMenu = resolveOllamaInstallMenuEntry({
hasOllama,
Expand Down Expand Up @@ -219,6 +227,6 @@ export function detectInferenceProviderHostState(
log: (message) => log(message),
}),
ollamaInstallMenu,
gpuNimCapable: Boolean(input.gpu?.nimCapable),
gpuNimCapable,
};
}
109 changes: 109 additions & 0 deletions test/onboard-nim-image-compat-warning.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { it, vi } from "vitest";

type SetupNim = (gpu: {
type: string;
name: string;
count: number;
totalMemoryMB: number;
perGpuMB: number;
nimCapable: boolean;
unifiedMemory: boolean;
spark: boolean;
platform: string;
}) => Promise<{ provider: string; model: string }>;

function writeAlwaysOkCurl(fakeBin: string): void {
fs.writeFileSync(
path.join(fakeBin, "curl"),
`#!/usr/bin/env bash
outfile=""
while [ "$#" -gt 0 ]; do
case "$1" in
-o) outfile="$2"; shift 2 ;;
*) shift ;;
esac
done
printf '%s' '{"id":"ok"}' > "$outfile"
printf '%s' "200"
`,
{ mode: 0o755 },
);
}

it("warns about arm64 NIM image compatibility when Local NIM is offered on DGX Spark", async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-onboard-arm64-nim-warning-"));
const fakeBin = path.join(tmpDir, "bin");

fs.mkdirSync(fakeBin, { recursive: true });
writeAlwaysOkCurl(fakeBin);

const originalArch = process.arch;
const originalPlatform = process.platform;
const originalEnv = { ...process.env };
const lines: string[] = [];
const originalLog = console.log;

vi.resetModules();
Object.defineProperty(process, "arch", { value: "arm64", configurable: true });
Object.defineProperty(process, "platform", { value: "linux", configurable: true });
process.env = {
...originalEnv,
HOME: tmpDir,
PATH: `${fakeBin}:${originalEnv.PATH || ""}`,
NEMOCLAW_EXPERIMENTAL: "1",
NEMOCLAW_NON_INTERACTIVE: "1",
NEMOCLAW_PROVIDER: "build",
NVIDIA_INFERENCE_API_KEY: "nvapi-test",
};
console.log = (...args: unknown[]) => lines.push(args.join(" "));

vi.doMock("../src/lib/credentials/store.js", async (importOriginal) => ({
...(await importOriginal<typeof import("../src/lib/credentials/store.js")>()),
prompt: async () => "",
ensureApiKey: async () => {},
}));
vi.doMock("../src/lib/runner.js", async (importOriginal) => ({
...(await importOriginal<typeof import("../src/lib/runner.js")>()),
runCapture: (_command: readonly string[]) => "",
}));

try {
const { setupNim } = (await import("../src/lib/onboard.js")) as unknown as {
setupNim: SetupNim;
};
const result = await setupNim({
type: "nvidia",
name: "NVIDIA GB10",
count: 1,
totalMemoryMB: 124607,
perGpuMB: 124607,
nimCapable: true,
unifiedMemory: true,
spark: true,
platform: "spark",
});

assert.equal(result.provider, "nvidia-prod");
assert.equal(result.model, "nvidia/nemotron-3-super-120b-a12b");
assert.ok(
lines.some((line) =>
line.includes("Local NVIDIA NIM is experimental on Linux arm64 DGX Spark hosts"),
),
);
assert.ok(lines.some((line) => line.includes("linux/arm64 manifests")));
} finally {
console.log = originalLog;
process.env = originalEnv;
Object.defineProperty(process, "arch", { value: originalArch, configurable: true });
Object.defineProperty(process, "platform", { value: originalPlatform, configurable: true });
vi.doUnmock("../src/lib/credentials/store.js");
vi.doUnmock("../src/lib/runner.js");
}
});