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
36 changes: 16 additions & 20 deletions src/lib/onboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,7 @@ import {
} from "./onboard/sandbox-gpu-mode";
import {
exitOnSandboxGpuConfigErrors,
formatSandboxGpuPassthroughNote,
sandboxGpuRemediationLines,
validateSandboxGpuPreflight,
} from "./onboard/sandbox-gpu-preflight";
Expand Down Expand Up @@ -3205,19 +3206,14 @@ function waitForSandboxReady(sandboxName: string, attempts = 10, delaySeconds =

// ── Step 1: Preflight ────────────────────────────────────────────

// CDI spec gap (#3152). When Docker is configured for CDI device injection
// (CDISpecDirs is set) but no nvidia.com/gpu spec is present, OpenShell's
// `gateway start --gpu` fails minutes later with `unresolvable CDI devices
// nvidia.com/gpu=all`. Block now and surface `nvidia-ctk cdi generate`. The
// check is a no-op when the user opts out of GPU passthrough (--no-gpu),
// since the legacy nvidia runtime does not need a CDI spec.
//
// Extracted so the same guard runs on the `--resume` branch, where preflight()
// itself is skipped via the cached session.
// Keep the Docker CDI guard near preflight so resume hits the same early failure path.
// Jetson/Tegra uses Docker's NVIDIA runtime backend and is exempt from CDI.
function assertCdiNvidiaGpuSpecPresent(
host: ReturnType<typeof assessHost>,
optedOutGpuPassthrough: boolean,
hostGpuPlatform: string | null | undefined = null,
): void {
if (hostGpuPlatform === "jetson") return;
if (!host.cdiNvidiaGpuSpecMissing || optedOutGpuPassthrough) return;
Comment on lines 3211 to 3217

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

CI blocker: src/lib/onboard.ts still exceeds the onboard entrypoint budget.

This file is currently over budget (+14 net), so the PR will not pass CI. Please move the new Jetson/GPU decision/messaging logic into a helper under src/lib/onboard/ and keep orchestration only in src/lib/onboard.ts.

As per coding guidelines: src/lib/onboard.ts: “This file contains core onboarding logic. Changes here affect the full sandbox creation and configuration flow.”

Also applies to: 3301-3301, 5502-5502, 9352-9385

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/onboard.ts` around lines 3249 - 3255, The onboard entrypoint is over
budget because the new Jetson/GPU decision and messaging logic (including the
assertCdiNvidiaGpuSpecPresent function) was added directly to
src/lib/onboard.ts; extract that logic into a new helper module under
src/lib/onboard/ (e.g., create a file exporting helpers like
assertCdiNvidiaGpuSpecPresent, any Jetson-specific checks and message builders),
update src/lib/onboard.ts to import and call those helpers so onboard.ts only
orchestrates decisions, and ensure you move the related code referenced around
the other affected symbols/blocks (the logic near the other ranges mentioned)
into the same helper to reduce onboard.ts size while preserving behavior and
tests.

console.error(
" Docker is configured for CDI device injection (CDISpecDirs is set), but no",
Expand Down Expand Up @@ -3264,7 +3260,7 @@ async function preflight(
preflightOpts.optedOutGpuPassthrough === true ||
preflightOpts.noGpu === true ||
!sandboxGpuConfig.sandboxGpuEnabled;
assertCdiNvidiaGpuSpecPresent(host, optedOutGpuPassthrough);
assertCdiNvidiaGpuSpecPresent(host, optedOutGpuPassthrough, sandboxGpuConfig.hostGpuPlatform);

// DNS resolution from inside containers (#2101). A corp firewall that
// blocks outbound UDP:53 to public resolvers leaves the sandbox build
Expand Down Expand Up @@ -5465,6 +5461,7 @@ async function createSandbox(
gpuDevice: effectiveSandboxGpuConfig.sandboxGpuDevice,
openshellSandboxCommand: sandboxStartupCommand,
timeoutSecs: sandboxReadyTimeoutSecs,
backend: effectiveSandboxGpuConfig.hostGpuPlatform === "jetson" ? "jetson" : "generic",
deps: { runOpenshell, runCaptureOpenshell, sleep },
});
const createResult = await streamSandboxCreate(createCommand, sandboxEnv, {
Expand Down Expand Up @@ -9291,10 +9288,7 @@ async function onboard(opts: OnboardOptions = {}): Promise<void> {
assessHost,
assertCdiNvidiaGpuSpecPresent,
resolveSandboxGpuConfig,
validateSandboxGpuPreflight: (config) => {
exitOnSandboxGpuConfigErrors(config);
validateSandboxGpuPreflight(config);
},
validateSandboxGpuPreflight,
skippedStepMessage,
startRecordedStep,
recordStepComplete,
Expand All @@ -9311,14 +9305,16 @@ async function onboard(opts: OnboardOptions = {}): Promise<void> {
const gpu = preflightResult.gpu ?? null;
if (gpuPassthrough) {
note(
resumeHasResolvedGpuIntent && recordedGpuPassthroughBeforePreflight
? " [resume] Continuing GPU passthrough from the saved onboarding session."
: requestedGpuPassthrough || sandboxGpuConfig.mode === "1"
? " GPU passthrough requested; passing --gpu to OpenShell gateway and sandbox creation."
: " NVIDIA GPU detected; enabling OpenShell GPU passthrough. Use --no-gpu to opt out.",
formatSandboxGpuPassthroughNote({
hostGpuPlatform: sandboxGpuConfig.hostGpuPlatform,
resumeHasResolvedGpuIntent,
recordedGpuPassthroughBeforePreflight,
requestedGpuPassthrough,
sandboxGpuMode: sandboxGpuConfig.mode,
}),
);
} else if (gpu?.platform === "jetson") {
note(" GPU sandbox passthrough disabled by default on Jetson.");
note(" Sandbox GPU disabled by configuration on Jetson/Tegra.");
} else if (process.platform === "linux" && !opts.noGpu) {
try {
const lspci = spawnSync("lspci", { encoding: "utf-8", timeout: 5000 });
Expand Down
46 changes: 45 additions & 1 deletion src/lib/onboard/docker-gpu-patch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
buildDockerGpuMode,
buildDockerGpuModeCandidates,
collectDockerGpuPatchDiagnostics,
type DockerContainerInspect,
detectSandboxFallbackDns,
dockerReportsNvidiaCdiDevices,
formatDockerInspectNetworkSummary,
Expand All @@ -21,7 +22,6 @@ import {
recreateOpenShellDockerSandboxWithGpu,
selectDockerGpuPatchMode,
shouldApplyDockerGpuPatch,
type DockerContainerInspect,
} from "../../../dist/lib/onboard/docker-gpu-patch";

function inspectFixture(): DockerContainerInspect {
Expand Down Expand Up @@ -300,6 +300,22 @@ describe("docker-gpu-patch", () => {
expect(buildDockerGpuMode("gpus", "1,2").args).toEqual(["--gpus", "device=1,2"]);
});

it("uses Jetson NVIDIA runtime args without selecting generic --gpus or CDI candidates", () => {
expect(buildDockerGpuMode("nvidia-runtime", null, { backend: "jetson" }).args).toEqual([
"--runtime",
"nvidia",
"--env",
"NVIDIA_VISIBLE_DEVICES=all",
"--env",
"NVIDIA_DRIVER_CAPABILITIES=compute,utility",
]);
expect(
buildDockerGpuModeCandidates("all", { backend: "jetson", cdiAvailable: true }).map(
(m) => m.kind,
),
).toEqual(["nvidia-runtime"]);
});

it("uses a Docker-GPU-specific supervisor reconnect wait with an override", () => {
expect(getDockerGpuSupervisorReconnectTimeoutSecs(180, {})).toBe(900);
expect(getDockerGpuSupervisorReconnectTimeoutSecs(600, {})).toBe(900);
Expand Down Expand Up @@ -376,6 +392,34 @@ describe("docker-gpu-patch", () => {
]);
});

it("probes only NVIDIA runtime for Jetson Docker GPU mode", () => {
const dockerCapture = vi.fn(() => "");
const dockerRun = vi.fn(() => ({ status: 0, stdout: "probe-id" }));

const selected = selectDockerGpuPatchMode(
{ image: "openshell/sandbox:abc", backend: "jetson" },
{
dockerCapture,
dockerRun,
dockerRm: vi.fn(() => ({ status: 0 })),
},
);

expect(selected.mode?.kind).toBe("nvidia-runtime");
expect(selected.attempts.map((attempt) => attempt.mode.kind)).toEqual(["nvidia-runtime"]);
expect(dockerRun).toHaveBeenCalledWith(
expect.arrayContaining([
"create",
"--runtime",
"nvidia",
"--env",
"NVIDIA_DRIVER_CAPABILITIES=compute,utility",
]),
expect.objectContaining({ ignoreError: true }),
);
expect(dockerCapture).not.toHaveBeenCalled();
});

it("tries CDI only when Docker reports readable NVIDIA CDI specs", () => {
expect(buildDockerGpuModeCandidates("all", { cdiAvailable: false }).map((m) => m.kind)).toEqual(
["gpus", "nvidia-runtime"],
Expand Down
41 changes: 29 additions & 12 deletions src/lib/onboard/docker-gpu-patch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export type DockerGpuPatchDeps = {
};

export type DockerGpuPatchModeKind = "gpus" | "nvidia-runtime" | "cdi";
export type DockerGpuPatchBackend = "generic" | "jetson";

export type DockerGpuPatchMode = {
kind: DockerGpuPatchModeKind;
Expand Down Expand Up @@ -294,7 +295,11 @@ function normalizeGpuDeviceForCdi(device: string | null | undefined): string {
return `nvidia.com/gpu=${dockerDevice || "all"}`;
}

export function buildDockerGpuMode(kind: DockerGpuPatchModeKind, device?: string | null): DockerGpuPatchMode {
export function buildDockerGpuMode(
kind: DockerGpuPatchModeKind,
device?: string | null,
options: { backend?: DockerGpuPatchBackend } = {},
): DockerGpuPatchMode {
const dockerDevice = normalizeGpuDeviceForDocker(device);
if (kind === "gpus") {
const gpuValue = dockerDevice === "all" ? "all" : `device=${dockerDevice}`;
Expand All @@ -306,11 +311,15 @@ export function buildDockerGpuMode(kind: DockerGpuPatchModeKind, device?: string
};
}
if (kind === "nvidia-runtime") {
const args = ["--runtime", "nvidia", "--env", `NVIDIA_VISIBLE_DEVICES=${dockerDevice}`];
if (options.backend === "jetson") {
args.push("--env", "NVIDIA_DRIVER_CAPABILITIES=compute,utility");
}
return {
kind,
label: `--runtime nvidia (NVIDIA_VISIBLE_DEVICES=${dockerDevice})`,
device: dockerDevice,
args: ["--runtime", "nvidia", "--env", `NVIDIA_VISIBLE_DEVICES=${dockerDevice}`],
args,
};
}
const cdiDevice = normalizeGpuDeviceForCdi(device);
Expand All @@ -324,12 +333,12 @@ export function buildDockerGpuMode(kind: DockerGpuPatchModeKind, device?: string

export function buildDockerGpuModeCandidates(
device?: string | null,
options: { cdiAvailable?: boolean } = {},
options: { cdiAvailable?: boolean; backend?: DockerGpuPatchBackend } = {},
): DockerGpuPatchMode[] {
const candidates = [
buildDockerGpuMode("gpus", device),
buildDockerGpuMode("nvidia-runtime", device),
];
if (options.backend === "jetson") {
return [buildDockerGpuMode("nvidia-runtime", device, { backend: "jetson" })];
}
const candidates = [buildDockerGpuMode("gpus", device), buildDockerGpuMode("nvidia-runtime", device)];
if (options.cdiAvailable) candidates.push(buildDockerGpuMode("cdi", device));
return candidates;
}
Expand Down Expand Up @@ -685,12 +694,15 @@ function probeDockerGpuMode(
}

export function selectDockerGpuPatchMode(
options: { image: string; device?: string | null },
options: { image: string; device?: string | null; backend?: DockerGpuPatchBackend },
deps: DockerGpuPatchDeps = {},
): { mode: DockerGpuPatchMode | null; attempts: DockerGpuPatchModeAttempt[] } {
const cdiAvailable = dockerReportsNvidiaCdiDevices(deps);
const cdiAvailable = options.backend === "jetson" ? false : dockerReportsNvidiaCdiDevices(deps);
const attempts: DockerGpuPatchModeAttempt[] = [];
for (const mode of buildDockerGpuModeCandidates(options.device, { cdiAvailable })) {
for (const mode of buildDockerGpuModeCandidates(options.device, {
cdiAvailable,
backend: options.backend,
})) {
const result = probeDockerGpuMode(mode, options.image, deps);
const attempt = { mode, ok: result.ok, error: result.error };
attempts.push(attempt);
Expand Down Expand Up @@ -784,6 +796,7 @@ export function recreateOpenShellDockerSandboxWithGpu(
timeoutSecs?: number;
waitForSupervisor?: boolean;
openshellSandboxCommand?: readonly string[] | null;
backend?: DockerGpuPatchBackend;
},
deps: DockerGpuPatchDeps = {},
): DockerGpuPatchResult {
Expand All @@ -807,13 +820,17 @@ export function recreateOpenShellDockerSandboxWithGpu(
if (!image) throw new Error("OpenShell sandbox container inspect did not include an image.");

const selection = selectDockerGpuPatchMode(
{ image, device: options.gpuDevice },
{ image, device: options.gpuDevice, backend: options.backend },
deps,
);
context.modeAttempts = selection.attempts;
context.selectedMode = selection.mode;
if (!selection.mode) {
throw new Error("Docker did not accept --gpus, NVIDIA runtime, or CDI GPU modes.");
const modeMessage =
options.backend === "jetson"
? "Docker did not accept the Jetson NVIDIA runtime GPU mode."
: "Docker did not accept --gpus, NVIDIA runtime, or CDI GPU modes.";
throw new Error(modeMessage);
}

const originalName = dockerContainerName(inspect);
Expand Down
12 changes: 10 additions & 2 deletions src/lib/onboard/docker-gpu-sandbox-create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0

import type {
DockerGpuPatchBackend,
DockerGpuPatchDeps,
DockerGpuPatchMode,
DockerGpuPatchResult,
Expand All @@ -27,12 +28,14 @@ type DockerGpuSandboxCreatePatchOptions = {
gpuDevice?: string | null;
openshellSandboxCommand?: readonly string[] | null;
timeoutSecs: number;
backend?: DockerGpuPatchBackend;
deps: DockerGpuSandboxCreateDeps;
};

type DockerGpuSandboxConfig = {
sandboxGpuEnabled: boolean;
sandboxGpuDevice?: string | null;
hostGpuPlatform?: string | null;
};

type DockerGpuSandboxCreatePlan = {
Expand Down Expand Up @@ -61,6 +64,7 @@ export function createDockerGpuSandboxCreatePatch(
gpuDevice: options.gpuDevice,
openshellSandboxCommand: options.openshellSandboxCommand ?? null,
timeoutSecs: options.timeoutSecs,
backend: options.backend,
};

return {
Expand Down Expand Up @@ -145,7 +149,9 @@ export function shouldUseDockerGpuPatchForCreate(
});
if (enabled) {
options.log?.(
" Docker-driver GPU patch active; creating sandbox first, then recreating the Docker container with GPU access.",
config.hostGpuPlatform === "jetson"
? " Jetson Docker GPU patch active; creating sandbox first, then recreating the Docker container with NVIDIA runtime GPU access."
: " Docker-driver GPU patch active; creating sandbox first, then recreating the Docker container with GPU access.",
);
}
return enabled;
Expand All @@ -158,7 +164,9 @@ export function resolveDockerGpuSandboxCreatePlan(
const useDockerGpuPatch = shouldUseDockerGpuPatchForCreate(config, options);
const logMessage = config.sandboxGpuEnabled
? useDockerGpuPatch
? " Docker-driver GPU patch active; allowing /proc writes required by Docker GPU initialization."
? config.hostGpuPlatform === "jetson"
? " Jetson sandbox GPU enabled; using NVIDIA Container Runtime instead of CDI/--gpus."
: " Docker-driver GPU patch active; allowing /proc writes required by Docker GPU initialization."
: " Direct sandbox GPU enabled; allowing OpenShell GPU policy enrichment."
: null;
return { useDockerGpuPatch, logMessage };
Expand Down
54 changes: 20 additions & 34 deletions src/lib/onboard/gateway-gpu-passthrough.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,43 +141,29 @@ describe("gateway GPU passthrough inspection", () => {
expect(canRestartCpuOnlyGatewayForGpuIntent(["alpha", "beta"], "alpha", true)).toBe(false);
});

it("aborts unsupported Jetson GPU passthrough before gateway inspection or cleanup", () => {
it("does not categorically abort Jetson GPU passthrough on Docker-driver gateways", () => {
vi.mocked(docker.dockerInspect).mockClear();
const stopDashboardForwards = vi.fn();
const retireLegacyGatewayForDockerDriverUpgrade = vi.fn();
const destroyGatewayRuntimeForGpuReuse = vi.fn();
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
const exitSpy = vi.spyOn(process, "exit").mockImplementation(((code?: number | string | null) => {
throw new Error(`exit:${code}`);
}) as never);

try {
expect(() =>
reconcileGatewayGpuReuseForGpuIntent({
gatewayReuseState: healthy,
gpuPassthrough: true,
gatewayName: "nemoclaw",
currentSandboxName: "jetson-box",
hostGpuPlatform: "jetson",
recreateSandbox: true,
confirmedDockerDriverGateway: false,
stopDashboardForwards,
retireLegacyGatewayForDockerDriverUpgrade,
destroyGatewayRuntimeForGpuReuse,
}),
).toThrow("exit:1");

const message = errorSpy.mock.calls.map((call) => call[0]).join("\n");
expect(message).toContain("Jetson/Tegra sandbox GPU passthrough is not supported");
expect(message).toContain("--no-gpu");
expect(message).not.toContain("destroy --yes");
expect(docker.dockerInspect).not.toHaveBeenCalled();
expect(stopDashboardForwards).not.toHaveBeenCalled();
expect(retireLegacyGatewayForDockerDriverUpgrade).not.toHaveBeenCalled();
expect(destroyGatewayRuntimeForGpuReuse).not.toHaveBeenCalled();
} finally {
errorSpy.mockRestore();
exitSpy.mockRestore();
}

const result = reconcileGatewayGpuReuseForGpuIntent({
gatewayReuseState: healthy,
gpuPassthrough: true,
gatewayName: "nemoclaw",
currentSandboxName: "jetson-box",
hostGpuPlatform: "jetson",
recreateSandbox: true,
confirmedDockerDriverGateway: true,
stopDashboardForwards,
retireLegacyGatewayForDockerDriverUpgrade,
destroyGatewayRuntimeForGpuReuse,
});

expect(result).toBe(healthy);
expect(docker.dockerInspect).not.toHaveBeenCalled();
expect(stopDashboardForwards).not.toHaveBeenCalled();
expect(retireLegacyGatewayForDockerDriverUpgrade).not.toHaveBeenCalled();
expect(destroyGatewayRuntimeForGpuReuse).not.toHaveBeenCalled();
});
});
6 changes: 0 additions & 6 deletions src/lib/onboard/gateway-gpu-passthrough.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,18 +134,12 @@ export function reconcileGatewayGpuReuseForGpuIntent({
gpuPassthrough,
gatewayName,
currentSandboxName,
hostGpuPlatform = null,
recreateSandbox,
confirmedDockerDriverGateway,
stopDashboardForwards,
retireLegacyGatewayForDockerDriverUpgrade,
destroyGatewayRuntimeForGpuReuse,
}: GatewayGpuReuseReconcileOptions): GatewayReuseState {
if (gpuPassthrough && hostGpuPlatform === "jetson") {
reportGpuPassthroughRecovery(console.error, () => [], { unsupportedPlatform: "jetson" });
process.exit(1);
}

if (!shouldInspectLegacyGatewayGpuPassthrough(
gatewayReuseState,
gpuPassthrough,
Expand Down
Loading