Skip to content

Commit 8d0eb04

Browse files
committed
fix: detect gpu for toolkit cdi preflight
1 parent 31dfdb2 commit 8d0eb04

5 files changed

Lines changed: 151 additions & 36 deletions

File tree

src/lib/onboard.ts

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1581,8 +1581,9 @@ function waitForSandboxReady(sandboxName: string, attempts = 10, delaySeconds =
15811581

15821582
// ── Step 1: Preflight ────────────────────────────────────────────
15831583

1584-
// Keep the Docker CDI guard near preflight so resume hits the same early failure path.
1585-
// Jetson/Tegra uses Docker's NVIDIA runtime backend and is exempt from CDI.
1584+
/**
1585+
* Fails onboarding when Docker CDI injection is configured but the NVIDIA GPU spec is invalid.
1586+
*/
15861587
function assertCdiNvidiaGpuSpecPresent(
15871588
host: ReturnType<typeof assessHost>,
15881589
optedOutGpuPassthrough: boolean,
@@ -1605,11 +1606,9 @@ type PreflightOptions = Pick<
16051606
optedOutGpuPassthrough?: boolean;
16061607
};
16071608

1608-
// Reject unsupported container runtimes (currently only Podman with the
1609-
// Linux Docker-driver gateway) before any Docker-specific probes. Both
1610-
// the fresh preflight and `--resume` backstop call this — if `docker`
1611-
// resolves to Podman, surface the unsupported-runtime message instead of
1612-
// running bridge/DNS diagnostics that would be misleading.
1609+
/**
1610+
* Rejects unsupported runtimes before Docker-specific bridge, DNS, and CDI probes.
1611+
*/
16131612
function rejectUnsupportedContainerRuntime(host: ReturnType<typeof assessHost>): void {
16141613
if (isLinuxDockerDriverGatewayEnabled() && host.runtime === "podman") {
16151614
console.error(` ✗ ${cliDisplayName()} onboarding now uses OpenShell's Docker driver.`);
@@ -1619,13 +1618,14 @@ function rejectUnsupportedContainerRuntime(host: ReturnType<typeof assessHost>):
16191618
}
16201619
}
16211620

1621+
/**
1622+
* Runs host preflight and blocks early on Docker, GPU, CDI, and runtime problems.
1623+
*/
16221624
async function preflight(
16231625
preflightOpts: PreflightOptions = {},
16241626
): Promise<ReturnType<typeof nim.detectGpu>> {
16251627
step(1, 8, "Preflight checks");
1626-
16271628
const host = assessHost();
1628-
16291629
// Docker / runtime
16301630
if (!host.dockerReachable) {
16311631
console.error(" Docker is not reachable. Please fix Docker and try again.");
@@ -1644,12 +1644,11 @@ async function preflight(
16441644
device: preflightOpts.sandboxGpuDevice ?? null,
16451645
});
16461646
exitOnSandboxGpuConfigErrors(sandboxGpuConfig);
1647-
const optedOutGpuPassthrough =
1647+
const explicitlyOptedOutGpuPassthrough =
16481648
preflightOpts.optedOutGpuPassthrough === true ||
16491649
preflightOpts.noGpu === true ||
1650-
!sandboxGpuConfig.sandboxGpuEnabled;
1651-
assertCdiNvidiaGpuSpecPresent(host, optedOutGpuPassthrough, sandboxGpuConfig.hostGpuPlatform);
1652-
1650+
sandboxGpuConfig.mode === "0";
1651+
assertCdiNvidiaGpuSpecPresent(host, explicitlyOptedOutGpuPassthrough, sandboxGpuConfig.hostGpuPlatform);
16531652
assertDockerBridgeAndContainerDnsHealthy(host, isNonInteractive());
16541653

16551654
if (host.runtime !== "unknown") {

src/lib/onboard/machine/handlers/preflight.test.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,13 +186,35 @@ describe("handlePreflightState", () => {
186186
expect(harness.deps.startRecordedStep).not.toHaveBeenCalled();
187187
expect(harness.deps.assertCdiNvidiaGpuSpecPresent).toHaveBeenCalledWith(
188188
{ cdiNvidiaGpuSpecMissing: false },
189-
true,
189+
false,
190190
undefined,
191191
);
192192
expect(harness.deps.validateSandboxGpuPreflight).toHaveBeenCalledOnce();
193193
expect(result.resumePreflight).toBe(true);
194194
});
195195

196+
it("keeps CDI guard active on resume when auto mode disables GPU after failed detection", async () => {
197+
const session = createSession();
198+
session.steps.preflight.status = "complete";
199+
session.gpuPassthrough = false;
200+
const assertCdiNvidiaGpuSpecPresent = vi.fn();
201+
const host = { cdiNvidiaGpuSpecMissing: true };
202+
const harness = createDeps({
203+
detectGpu: vi.fn(() => null),
204+
getResumeSandboxGpuOverrides: vi.fn(() => ({ flag: null, device: null })),
205+
resolveSandboxGpuConfig,
206+
assessHost: () => host,
207+
assertCdiNvidiaGpuSpecPresent,
208+
});
209+
210+
await handlePreflightState({
211+
...baseOptions(harness.deps, session),
212+
resume: true,
213+
});
214+
215+
expect(assertCdiNvidiaGpuSpecPresent).toHaveBeenCalledWith(host, false, null);
216+
});
217+
196218
it("passes host GPU platform into the resumed CDI guard", async () => {
197219
const session = createSession();
198220
session.steps.preflight.status = "complete";
@@ -220,7 +242,7 @@ describe("handlePreflightState", () => {
220242

221243
expect(assertCdiNvidiaGpuSpecPresent).toHaveBeenCalledWith(
222244
{ cdiNvidiaGpuSpecMissing: false },
223-
true,
245+
false,
224246
"jetson",
225247
);
226248
});

src/lib/onboard/machine/handlers/preflight.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,17 @@ export interface PreflightStateResult<Gpu, Config extends PreflightSandboxGpuCon
9393
stateResult: OnboardStateTransitionResult;
9494
}
9595

96+
/**
97+
* Checks whether the environment pins sandbox GPU mode or device selection.
98+
*/
9699
function envHasSandboxGpuOverride(env: NodeJS.ProcessEnv): boolean {
97100
return env.NEMOCLAW_SANDBOX_GPU !== undefined || env.NEMOCLAW_SANDBOX_GPU_DEVICE !== undefined;
98101
}
99102

103+
/**
104+
* Executes or revalidates the preflight state while preserving the user's
105+
* effective sandbox GPU intent across fresh onboarding and resume.
106+
*/
100107
export async function handlePreflightState<
101108
Gpu,
102109
SandboxEntry,
@@ -143,9 +150,7 @@ export async function handlePreflightState<
143150
});
144151
deps.validateSandboxGpuPreflight(resumeSandboxGpuConfig);
145152
const resumeOptedOutGpuPassthrough =
146-
noGpu ||
147-
(!gpuRequested && session?.gpuPassthrough === false) ||
148-
!resumeSandboxGpuConfig.sandboxGpuEnabled;
153+
noGpu || effectiveSandboxGpuFlag === "disable" || resumeSandboxGpuConfig.mode === "0";
149154
const resumeHost = deps.assessHost();
150155
// Reject unsupported runtimes (Podman) BEFORE the CDI GPU-spec
151156
// backstop and the Docker-specific bridge/DNS probes so Podman

src/lib/onboard/preflight-cdi.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import { assessHost, planHostRemediation } from "../../../dist/lib/onboard/prefl
88

99
type HostAssessment = Parameters<typeof planHostRemediation>[0];
1010

11+
/**
12+
* Creates a Linux Docker host assessment with NVIDIA CDI defaults for focused overrides.
13+
*/
1114
function baseAssessment(overrides: Partial<HostAssessment> = {}): HostAssessment {
1215
return {
1316
platform: "linux",
@@ -38,6 +41,9 @@ function baseAssessment(overrides: Partial<HostAssessment> = {}): HostAssessment
3841
};
3942
}
4043

44+
/**
45+
* Emulates the systemctl/stat probes needed by CDI staleness remediation tests.
46+
*/
4147
function healthySystemctlAndStat(command: readonly string[]) {
4248
if (command[0] === "systemctl" && command[1] === "is-enabled") return "enabled";
4349
if (command[0] === "systemctl" && command[1] === "is-active") return "active";
@@ -67,6 +73,50 @@ describe("assessHost — CDI", () => {
6773
expect(result.cdiNvidiaGpuSpecMissing).toBe(true);
6874
});
6975

76+
it("plans toolkit bootstrap when PCI detects NVIDIA hardware but nvidia-smi and nvidia-ctk are absent", () => {
77+
const result = assessHost({
78+
platform: "linux",
79+
env: {},
80+
release: "6.8.0-58-generic",
81+
readFileImpl: (filePath: string) =>
82+
filePath.endsWith("other.yaml")
83+
? "cdiVersion: 0.5.0\nkind: vendor.example/device\ndevices: []\n"
84+
: "Linux version 6.8.0-58-generic",
85+
readdirImpl: (dir: string) => (dir === "/etc/cdi" ? ["other.yaml"] : []),
86+
runCaptureImpl: (command: readonly string[]) => {
87+
if (command.join(" ").includes("apt-get")) return "/usr/bin/apt-get";
88+
if (command[0] === "lspci") {
89+
return "01:00.0 VGA compatible controller: NVIDIA Corporation GA102 [GeForce RTX 3090]\n";
90+
}
91+
if (command[0] === "systemctl" && command[1] === "is-active") return "active";
92+
if (command[0] === "systemctl" && command[1] === "is-enabled") return "enabled";
93+
return "";
94+
},
95+
dockerInfoOutput: JSON.stringify({
96+
ServerVersion: "27.0",
97+
OperatingSystem: "Ubuntu 24.04",
98+
CDISpecDirs: ["/etc/cdi", "/var/run/cdi"],
99+
}),
100+
commandExistsImpl: (name: string) =>
101+
name === "docker" || name === "lspci" || name === "systemctl",
102+
});
103+
104+
expect(result.hasNvidiaGpu).toBe(true);
105+
expect(result.nvidiaContainerToolkitInstalled).toBe(false);
106+
expect(result.cdiNvidiaGpuSpecMissing).toBe(true);
107+
108+
const action = planHostRemediation(result).find(
109+
(entry: { id: string }) => entry.id === "install_nvidia_container_toolkit",
110+
);
111+
expect(action).toBeTruthy();
112+
expect(action?.blocking).toBe(true);
113+
expect(action?.commands).toContain("sudo apt-get install -y nvidia-container-toolkit");
114+
expect(action?.commands.some((command) => command.includes("nvidia-ctk cdi generate"))).toBe(
115+
true,
116+
);
117+
expect(action?.commands.some((command) => command.includes("nvidia-ctk cdi list"))).toBe(true);
118+
});
119+
70120
it("does not flag the host when an nvidia.com/gpu YAML spec is present", () => {
71121
const result = assessHost({
72122
platform: "linux",

src/lib/onboard/preflight.ts

Lines changed: 57 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -385,13 +385,37 @@ function isHeadlessLikely(env: NodeJS.ProcessEnv): boolean {
385385
return !env.DISPLAY && !env.WAYLAND_DISPLAY && !env.TERM_PROGRAM;
386386
}
387387

388-
function detectNvidiaGpu(runCaptureImpl: RunCaptureFn): boolean {
389-
if (!commandExists("nvidia-smi", runCaptureImpl)) {
390-
return false;
391-
}
392-
return Boolean(String(runCaptureImpl(["nvidia-smi", "-L"], { ignoreError: true }) || "").trim());
388+
/**
389+
* Detects NVIDIA hardware via nvidia-smi first, then Linux PCI data as a toolkit-free fallback.
390+
*/
391+
function detectNvidiaGpu(opts: {
392+
platform: NodeJS.Platform | string;
393+
isWsl: boolean;
394+
runCaptureImpl: RunCaptureFn;
395+
commandExistsImpl?: (commandName: string) => boolean;
396+
}): boolean {
397+
const commandExistsImpl =
398+
opts.commandExistsImpl ??
399+
((commandName: string) => commandExists(commandName, opts.runCaptureImpl));
400+
if (commandExistsImpl("nvidia-smi")) {
401+
const smiOutput = opts.runCaptureImpl(["nvidia-smi", "-L"], { ignoreError: true });
402+
if (String(smiOutput || "").trim()) return true;
403+
}
404+
405+
if (opts.platform !== "linux" || opts.isWsl || !commandExistsImpl("lspci")) return false;
406+
const pciOutput = opts.runCaptureImpl(["lspci", "-nn"], { ignoreError: true });
407+
return String(pciOutput || "")
408+
.split(/\r?\n/)
409+
.some(
410+
(line) =>
411+
/nvidia/i.test(line) &&
412+
/(vga compatible controller|3d controller|display controller)/i.test(line),
413+
);
393414
}
394415

416+
/**
417+
* Detects the host package manager used for NVIDIA toolkit remediation commands.
418+
*/
395419
function detectPackageManager(runCaptureImpl: RunCaptureFn): PackageManager {
396420
if (commandExists("apt-get", runCaptureImpl)) return "apt";
397421
if (commandExists("dnf", runCaptureImpl)) return "dnf";
@@ -401,6 +425,9 @@ function detectPackageManager(runCaptureImpl: RunCaptureFn): PackageManager {
401425
return "unknown";
402426
}
403427

428+
/**
429+
* Normalizes systemctl active/enabled output into a tri-state service status.
430+
*/
404431
function parseSystemctlState(value = ""): boolean | null {
405432
const normalized = String(value || "")
406433
.trim()
@@ -447,6 +474,9 @@ export function buildContainerToolkitBootstrapCommands(
447474
];
448475
}
449476

477+
/**
478+
* Builds the host capability snapshot used to plan preflight remediation.
479+
*/
450480
export function assessHost(opts: AssessHostOpts = {}): HostAssessment {
451481
const platform = opts.platform ?? process.platform;
452482
const env = opts.env ?? process.env;
@@ -456,12 +486,33 @@ export function assessHost(opts: AssessHostOpts = {}): HostAssessment {
456486
runCapture(command, { ignoreError: options?.ignoreError ?? false }));
457487
const readFileImpl = opts.readFileImpl ?? fs.readFileSync;
458488
const readdirImpl = opts.readdirImpl ?? ((dir: string) => fs.readdirSync(dir));
489+
const shouldReadLinuxHostDetails = platform === "linux";
490+
const release = opts.release ?? (shouldReadLinuxHostDetails ? os.release() : "");
491+
const procVersion =
492+
opts.procVersion ??
493+
(shouldReadLinuxHostDetails
494+
? (() => {
495+
try {
496+
return readFileImpl("/proc/version", "utf-8");
497+
} catch {
498+
return "";
499+
}
500+
})()
501+
: "");
502+
const isWslHost = detectWsl({ platform, env, release, procVersion });
459503
const dockerInstalled =
460504
opts.commandExistsImpl?.("docker") ?? commandExists("docker", runCaptureImpl);
461505
const nodeInstalled = opts.commandExistsImpl?.("node") ?? commandExists("node", runCaptureImpl);
462506
const openshellInstalled =
463507
opts.commandExistsImpl?.("openshell") ?? commandExists("openshell", runCaptureImpl);
464-
const hasNvidiaGpu = opts.gpuProbeImpl?.() ?? detectNvidiaGpu(runCaptureImpl);
508+
const hasNvidiaGpu =
509+
opts.gpuProbeImpl?.() ??
510+
detectNvidiaGpu({
511+
platform,
512+
isWsl: isWslHost,
513+
runCaptureImpl,
514+
commandExistsImpl: opts.commandExistsImpl,
515+
});
465516
const nvidiaContainerToolkitInstalled =
466517
opts.commandExistsImpl?.("nvidia-ctk") ?? commandExists("nvidia-ctk", runCaptureImpl);
467518
const packageManager = detectPackageManager(runCaptureImpl);
@@ -480,22 +531,10 @@ export function assessHost(opts: AssessHostOpts = {}): HostAssessment {
480531
dockerReachable = true;
481532
dockerRunning = true;
482533
}
483-
484-
const release = opts.release ?? os.release();
485-
const procVersion =
486-
opts.procVersion ??
487-
(() => {
488-
try {
489-
return readFileImpl("/proc/version", "utf-8");
490-
} catch {
491-
return "";
492-
}
493-
})();
494534
let runtime = inferContainerRuntime(dockerInfoOutput);
495535
if (dockerReachable && runtime === "unknown" && platform === "linux") {
496536
runtime = "docker";
497537
}
498-
const isWslHost = detectWsl({ platform, env, release, procVersion });
499538
const dockerCgroupVersion = dockerReachable
500539
? parseDockerCgroupVersion(dockerInfoOutput)
501540
: "unknown";

0 commit comments

Comments
 (0)