diff --git a/src/lib/onboard/docker-gpu-patch-wsl.test.ts b/src/lib/onboard/docker-gpu-patch-wsl.test.ts index a172f421285..bbce0ae150b 100644 --- a/src/lib/onboard/docker-gpu-patch-wsl.test.ts +++ b/src/lib/onboard/docker-gpu-patch-wsl.test.ts @@ -3,7 +3,10 @@ import { describe, expect, it } from "vitest"; -import { shouldApplyDockerGpuPatch } from "../../../dist/lib/onboard/docker-gpu-patch"; +import { + buildDockerGpuModeCandidates, + shouldApplyDockerGpuPatch, +} from "../../../dist/lib/onboard/docker-gpu-patch"; describe("shouldApplyDockerGpuPatch on Docker Desktop WSL", () => { it("ignores NEMOCLAW_DOCKER_GPU_PATCH=0 on Docker Desktop WSL where the patch is required", () => { @@ -53,3 +56,26 @@ describe("shouldApplyDockerGpuPatch on Docker Desktop WSL", () => { ).toBe(true); }); }); + +describe("buildDockerGpuModeCandidates on Docker Desktop WSL (#5512)", () => { + it("skips the CDI mode on Docker Desktop WSL even when CDI is advertised", () => { + // Docker Desktop WSL advertises CDI dirs but has no usable nvidia.com/gpu + // spec, so CDI fails the real recreate; the patch must use --gpus instead. + const modes = buildDockerGpuModeCandidates("all", { + cdiAvailable: true, + dockerDesktopWsl: true, + }); + expect(modes.map((mode) => mode.kind)).not.toContain("cdi"); + expect(modes[0]?.kind).toBe("gpus"); + expect(modes[0]?.args).toEqual(["--gpus", "all"]); + }); + + it("keeps CDI first on a non-Docker-Desktop-WSL host that advertises CDI", () => { + const modes = buildDockerGpuModeCandidates("all", { + cdiAvailable: true, + dockerDesktopWsl: false, + }); + expect(modes[0]?.kind).toBe("cdi"); + expect(modes[0]?.args).toEqual(["--device", "nvidia.com/gpu=all"]); + }); +}); diff --git a/src/lib/onboard/docker-gpu-patch.ts b/src/lib/onboard/docker-gpu-patch.ts index e48bff8c98c..2151a6784ce 100644 --- a/src/lib/onboard/docker-gpu-patch.ts +++ b/src/lib/onboard/docker-gpu-patch.ts @@ -496,7 +496,11 @@ export function buildDockerGpuMode( export function buildDockerGpuModeCandidates( device?: string | null, - options: { cdiAvailable?: boolean; backend?: DockerGpuPatchBackend } = {}, + options: { + cdiAvailable?: boolean; + backend?: DockerGpuPatchBackend; + dockerDesktopWsl?: boolean; + } = {}, ): DockerGpuPatchMode[] { if (options.backend === "jetson") { return [buildDockerGpuMode("nvidia-runtime", device, { backend: "jetson" })]; @@ -510,8 +514,16 @@ export function buildDockerGpuModeCandidates( // gateway wiring and the supervisor never reconnects (#4948). Keep --gpus // and the NVIDIA runtime as fallbacks until OpenShell exposes an // authoritative GPU mode contract that can replace CDI-spec probing. + // + // #5512: Docker Desktop WSL advertises CDI spec directories (so cdiAvailable + // is true) while the WSL distro has no usable nvidia.com/gpu spec. There the + // CDI mode passes the create-only probe but fails the real recreate with + // "unresolvable CDI devices nvidia.com/gpu=all", so skip CDI and use the + // --gpus compatibility path that preflight already commits to on this runtime. const candidates: DockerGpuPatchMode[] = []; - if (options.cdiAvailable) candidates.push(buildDockerGpuMode("cdi", device)); + if (options.cdiAvailable && !options.dockerDesktopWsl) { + candidates.push(buildDockerGpuMode("cdi", device)); + } candidates.push(buildDockerGpuMode("gpus", device), buildDockerGpuMode("nvidia-runtime", device)); return candidates; } @@ -949,7 +961,12 @@ function probeDockerGpuMode( } export function selectDockerGpuPatchMode( - options: { image: string; device?: string | null; backend?: DockerGpuPatchBackend }, + options: { + image: string; + device?: string | null; + backend?: DockerGpuPatchBackend; + dockerDesktopWsl?: boolean; + }, deps: DockerGpuPatchDeps = {}, ): { mode: DockerGpuPatchMode | null; attempts: DockerGpuPatchModeAttempt[] } { const cdiAvailable = options.backend === "jetson" ? false : dockerReportsNvidiaCdiDevices(deps); @@ -957,6 +974,7 @@ export function selectDockerGpuPatchMode( for (const mode of buildDockerGpuModeCandidates(options.device, { cdiAvailable, backend: options.backend, + dockerDesktopWsl: options.dockerDesktopWsl, })) { const result = probeDockerGpuMode(mode, options.image, deps); const attempt = { mode, ok: result.ok, error: result.error }; @@ -1014,6 +1032,7 @@ export function recreateOpenShellDockerSandboxWithGpu( waitForSupervisor?: boolean; openshellSandboxCommand?: readonly string[] | null; backend?: DockerGpuPatchBackend; + dockerDesktopWsl?: boolean; }, deps: DockerGpuPatchDeps = {}, ): DockerGpuPatchResult { @@ -1037,7 +1056,12 @@ export function recreateOpenShellDockerSandboxWithGpu( if (!image) throw new Error("OpenShell sandbox container inspect did not include an image."); const selection = selectDockerGpuPatchMode( - { image, device: options.gpuDevice, backend: options.backend }, + { + image, + device: options.gpuDevice, + backend: options.backend, + dockerDesktopWsl: options.dockerDesktopWsl, + }, deps, ); context.modeAttempts = selection.attempts; @@ -1184,6 +1208,7 @@ export function applyDockerGpuPatchOrExit( // /dev/nvmap group access. backend?: DockerGpuPatchBackend; openshellSandboxCommand?: readonly string[] | null; + dockerDesktopWsl?: boolean; }, deps: Pick, ): DockerGpuPatchResult { diff --git a/src/lib/onboard/docker-gpu-sandbox-create.ts b/src/lib/onboard/docker-gpu-sandbox-create.ts index 51eb4ed0426..df6d11565c0 100644 --- a/src/lib/onboard/docker-gpu-sandbox-create.ts +++ b/src/lib/onboard/docker-gpu-sandbox-create.ts @@ -62,6 +62,12 @@ type DockerGpuSandboxCreatePatchOptions = { openshellSandboxCommand?: readonly string[] | null; timeoutSecs: number; backend?: DockerGpuPatchBackend; + /** + * Whether the host is Docker Desktop WSL. Defaults to the cached + * `isDockerDesktopWslRuntime()` probe. When true, the GPU patch skips the CDI + * mode (unusable on this runtime) and uses `--gpus` instead (#5512). + */ + dockerDesktopWsl?: boolean; deps: DockerGpuSandboxCreateDeps; /** * Test seams. The production composition uses the canonical @@ -136,6 +142,7 @@ export function createDockerGpuSandboxCreatePatch( openshellSandboxCommand: options.openshellSandboxCommand ?? null, timeoutSecs: options.timeoutSecs, backend: options.backend, + dockerDesktopWsl: options.dockerDesktopWsl ?? isDockerDesktopWslRuntime(), }; return {