From f82b8d3b0f3329f43bd03c1f82568dfb042da872 Mon Sep 17 00:00:00 2001 From: Ho Lim Date: Fri, 11 Sep 2026 00:49:34 -0700 Subject: [PATCH 1/4] fix(inference): adopt an interrupted managed vLLM container holding the serving port Host-local lifecycle recovery admits only a completed authenticated install: it requires the runtime receipt written after startup and the auth label a profile adds only for managed bearer auth. An install interrupted before either exists leaves its own managed container running on the serving port, so the guard reported that port as held by another process and neither onboard --resume nor a fresh install could make progress. Classify the port holder by its ownership labels when recovery cannot claim it, and reuse the existing replacement path for a running managed container. A foreign or unlabeled holder, an ambiguous inspection, a distributed head or worker, and a container that is not running all remain conflicts. Fixes #11426 Signed-off-by: Ho Lim --- src/lib/inference/vllm-serving-port.test.ts | 53 +++++++++++++++++++++ src/lib/inference/vllm.ts | 26 +++++++++- 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/src/lib/inference/vllm-serving-port.test.ts b/src/lib/inference/vllm-serving-port.test.ts index bcb5d71463f..8376a0ab392 100644 --- a/src/lib/inference/vllm-serving-port.test.ts +++ b/src/lib/inference/vllm-serving-port.test.ts @@ -415,4 +415,57 @@ describe("managed vLLM serving-port guard (#8685)", () => { expect(mocks.dockerRunDetached).toHaveBeenCalled(); expect(errSpy.mock.calls.flat().join("\n")).not.toContain("already in use"); }); + it("adopts its own interrupted managed container that holds the serving port", async () => { + const profile = detectVllmProfile({ platform: "spark", type: "nvidia" })!; + const managed = vllmContainerRow(profile.containerName, { state: "running" }); + // Three responses: the ownership check that classifies the port holder, then + // the ordinary replacement guard's own inspection. + mockSuccessfulVllmInstall(mocks, profile.containerName, [ + () => managed, + () => managed, + () => managed, + ]); + // An interrupted install persists no runtime receipt, and a profile without + // managed bearer auth carries no auth label, so lifecycle recovery cannot + // admit the container this very install left behind. + mocks.recoverHostLocalManagedVllmEndpoint.mockReturnValue(null); + const checkServingPort = vi.fn(async () => ({ + ok: false, + reason: "port 8000 is held by docker-proxy (PID 4242)", + })); + + const result = await installVllm(profile, { + hasImage: true, + nonInteractive: true, + promptFn: vi.fn<(q: string) => Promise>(), + checkServingPort, + }); + + expect(result).toEqual({ ok: true }); + expect(mocks.dockerForceRm).toHaveBeenCalledWith( + MANAGED_CONTAINER_ID, + expect.objectContaining({ ignoreError: true, suppressOutput: true }), + ); + expect(mocks.dockerRunDetached).toHaveBeenCalled(); + expect(errSpy.mock.calls.flat().join("\n")).not.toContain("another process"); + }); + + it("still refuses when an unlabeled container holds the serving port", async () => { + const profile = detectVllmProfile({ platform: "spark", type: "nvidia" })!; + const foreign = vllmContainerRow(profile.containerName, { label: "", state: "running" }); + mockSuccessfulVllmInstall(mocks, profile.containerName, [() => foreign, () => foreign]); + mocks.recoverHostLocalManagedVllmEndpoint.mockReturnValue(null); + + const result = await installVllm(profile, { + hasImage: true, + nonInteractive: true, + promptFn: vi.fn<(q: string) => Promise>(), + checkServingPort: async () => ({ ok: false, reason: "port 8000 is held" }), + }); + + expect(result).toEqual({ ok: false }); + expect(mocks.dockerForceRm).not.toHaveBeenCalled(); + expect(mocks.dockerRunDetached).not.toHaveBeenCalled(); + expect(errSpy.mock.calls.flat().join("\n")).toContain("already in use"); + }); }); diff --git a/src/lib/inference/vllm.ts b/src/lib/inference/vllm.ts index a455c88fc93..18233f03104 100644 --- a/src/lib/inference/vllm.ts +++ b/src/lib/inference/vllm.ts @@ -1953,6 +1953,24 @@ function applyRequestedVllmGpuDevice( * Name the process holding the serving port so the operator can act, matching * how the Ollama auth proxy reports its own port conflict. */ +/** + * Container id of this install's own managed container when that container is + * what holds the serving port. + * + * Lifecycle recovery admits only a completed authenticated install: it needs + * the runtime receipt written after startup and the auth label that a profile + * adds only for managed bearer auth. An install interrupted before either + * exists leaves a running managed container that recovery can never claim, so + * the ownership labels decide instead. Every other state — a foreign or + * unlabeled holder, an ambiguous inspection, a distributed head or worker, or a + * container that is not running — remains a conflict. + */ +function adoptableServingPortHolder(containerName: string): string | undefined { + const ownership = inspectVllmContainerOwnership(containerName); + if (ownership.kind !== "managed" || !ownership.running) return undefined; + return ownership.containerId; +} + function printServingPortConflict(probe: ServingPortProbe): void { console.error( ` vLLM install failed: port ${String(VLLM_PORT)} is already in use by another process.`, @@ -2328,8 +2346,12 @@ async function runVllmInstall( recoveredHostLocalContainerId = recovered.containerId; // Continue through the ordinary managed-container replacement path. } else { - printServingPortConflict(servingPort); - return { ok: false }; + const adopted = adoptableServingPortHolder(runtimeProfile.containerName); + if (adopted === undefined) { + printServingPortConflict(servingPort); + return { ok: false }; + } + recoveredHostLocalContainerId = adopted; } } catch (error) { console.error( From 01be96c44500c90121a3265c504318a37dc997f8 Mon Sep 17 00:00:00 2001 From: Ho Lim Date: Fri, 11 Sep 2026 05:30:52 -0700 Subject: [PATCH 2/4] fix(inference): require the adopted container to hold the failed port Review found that ownership alone did not prove the managed container was the process holding the serving port: a managed container published on another host port would have been adopted and removed while an unrelated process kept the port, freeing nothing and destroying an unrelated runtime. Compare the container's published binding for the fixed container port with the port that failed, and keep the conflict outcome when they differ or the binding cannot be read. Signed-off-by: Ho Lim --- src/lib/inference/vllm-serving-port.test.ts | 34 +++++++++++++++++++++ src/lib/inference/vllm.ts | 25 ++++++++++++--- 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/src/lib/inference/vllm-serving-port.test.ts b/src/lib/inference/vllm-serving-port.test.ts index 8376a0ab392..501f9f5bb86 100644 --- a/src/lib/inference/vllm-serving-port.test.ts +++ b/src/lib/inference/vllm-serving-port.test.ts @@ -429,6 +429,7 @@ describe("managed vLLM serving-port guard (#8685)", () => { // managed bearer auth carries no auth label, so lifecycle recovery cannot // admit the container this very install left behind. mocks.recoverHostLocalManagedVllmEndpoint.mockReturnValue(null); + publishContainerPort(8000); const checkServingPort = vi.fn(async () => ({ ok: false, reason: "port 8000 is held by docker-proxy (PID 4242)", @@ -463,6 +464,39 @@ describe("managed vLLM serving-port guard (#8685)", () => { checkServingPort: async () => ({ ok: false, reason: "port 8000 is held" }), }); + expect(result).toEqual({ ok: false }); + expect(mocks.dockerForceRm).not.toHaveBeenCalled(); + expect(mocks.dockerRunDetached).not.toHaveBeenCalled(); + expect(errSpy.mock.calls.flat().join("\n")).toContain("already in use"); + }); + /** Report the host port the managed container publishes for container 8000. */ + function publishContainerPort(hostPort: number): void { + const base = mocks.dockerCapture.getMockImplementation(); + mocks.dockerCapture.mockImplementation((args: readonly string[]) => + args[0] === "port" ? `0.0.0.0:${String(hostPort)}\n` : (base?.(args) ?? ""), + ); + } + + it("refuses a managed container published on a different host port", async () => { + const profile = detectVllmProfile({ platform: "spark", type: "nvidia" })!; + const managed = vllmContainerRow(profile.containerName, { state: "running" }); + mockSuccessfulVllmInstall(mocks, profile.containerName, [ + () => managed, + () => managed, + () => managed, + ]); + mocks.recoverHostLocalManagedVllmEndpoint.mockReturnValue(null); + // Our managed container serves another port, so an unrelated process owns + // the port that failed. Removing this container would free nothing. + publishContainerPort(19_000); + + const result = await installVllm(profile, { + hasImage: true, + nonInteractive: true, + promptFn: vi.fn<(q: string) => Promise>(), + checkServingPort: async () => ({ ok: false, reason: "port 8000 is held" }), + }); + expect(result).toEqual({ ok: false }); expect(mocks.dockerForceRm).not.toHaveBeenCalled(); expect(mocks.dockerRunDetached).not.toHaveBeenCalled(); diff --git a/src/lib/inference/vllm.ts b/src/lib/inference/vllm.ts index 18233f03104..1eeaf0d718e 100644 --- a/src/lib/inference/vllm.ts +++ b/src/lib/inference/vllm.ts @@ -1961,13 +1961,28 @@ function applyRequestedVllmGpuDevice( * the runtime receipt written after startup and the auth label that a profile * adds only for managed bearer auth. An install interrupted before either * exists leaves a running managed container that recovery can never claim, so - * the ownership labels decide instead. Every other state — a foreign or - * unlabeled holder, an ambiguous inspection, a distributed head or worker, or a - * container that is not running — remains a conflict. + * ownership labels decide instead. + * + * Ownership alone is not enough. A managed container published on a different + * host port is not the process holding this port, and removing it would free + * nothing while destroying an unrelated runtime, so its published binding must + * match the port that failed. Every other state — a foreign or unlabeled + * holder, an ambiguous inspection, a distributed head or worker, a container + * that is not running, and an unreadable binding — remains a conflict. */ -function adoptableServingPortHolder(containerName: string): string | undefined { +function adoptableServingPortHolder( + containerName: string, + servingPort: number, +): string | undefined { const ownership = inspectVllmContainerOwnership(containerName); if (ownership.kind !== "managed" || !ownership.running) return undefined; + // The managed container always publishes the fixed container port 8000. + const published = dockerCapture(["port", containerName, "8000"], { + env: buildVllmDockerEnv(), + ignoreError: true, + timeout: 10_000, + })?.match(/:(\d+)\s*$/); + if (!published || Number(published[1]) !== servingPort) return undefined; return ownership.containerId; } @@ -2346,7 +2361,7 @@ async function runVllmInstall( recoveredHostLocalContainerId = recovered.containerId; // Continue through the ordinary managed-container replacement path. } else { - const adopted = adoptableServingPortHolder(runtimeProfile.containerName); + const adopted = adoptableServingPortHolder(runtimeProfile.containerName, VLLM_PORT); if (adopted === undefined) { printServingPortConflict(servingPort); return { ok: false }; From 52c4b37d455f7be6b90fa959a182103bd2904873 Mon Sep 17 00:00:00 2001 From: Julie Yaunches Date: Fri, 11 Sep 2026 14:47:28 -0400 Subject: [PATCH 3/4] fix(inference): keep adopted vLLM on local Docker Signed-off-by: Julie Yaunches --- src/lib/inference/vllm-serving-port.test.ts | 44 +++++++++++++++++++-- src/lib/inference/vllm.ts | 33 ++++++++++------ 2 files changed, 62 insertions(+), 15 deletions(-) diff --git a/src/lib/inference/vllm-serving-port.test.ts b/src/lib/inference/vllm-serving-port.test.ts index 501f9f5bb86..a08feccab0c 100644 --- a/src/lib/inference/vllm-serving-port.test.ts +++ b/src/lib/inference/vllm-serving-port.test.ts @@ -231,6 +231,7 @@ describe("managed vLLM serving-port guard (#8685)", () => { const profile = detectVllmProfile({ platform: "spark", type: "nvidia" })!; const managed = vllmContainerRow(profile.containerName); mockSuccessfulVllmInstall(mocks, profile.containerName, [() => managed, () => managed]); + mockDefaultDockerOwnership(managed); mocks.recoverHostLocalManagedVllmEndpoint.mockReturnValue({ baseUrl: "http://127.0.0.1:8000", apiKey: "b".repeat(64), @@ -286,6 +287,10 @@ describe("managed vLLM serving-port guard (#8685)", () => { () => vllmContainerRow(profile.containerName), () => vllmContainerRow(profile.containerName, { id: "c".repeat(64) }), ]); + mockDefaultDockerOwnership( + vllmContainerRow(profile.containerName), + vllmContainerRow(profile.containerName, { id: "c".repeat(64) }), + ); mocks.recoverHostLocalManagedVllmEndpoint.mockReturnValue({ baseUrl: "http://127.0.0.1:8000", apiKey: "b".repeat(64), @@ -415,7 +420,9 @@ describe("managed vLLM serving-port guard (#8685)", () => { expect(mocks.dockerRunDetached).toHaveBeenCalled(); expect(errSpy.mock.calls.flat().join("\n")).not.toContain("already in use"); }); - it("adopts its own interrupted managed container that holds the serving port", async () => { + it("adopts its interrupted container on the default Docker context (#11426)", async () => { + process.env.DOCKER_HOST = "ssh://remote-builder.example.test"; + process.env.DOCKER_CONTEXT = "remote-builder"; const profile = detectVllmProfile({ platform: "spark", type: "nvidia" })!; const managed = vllmContainerRow(profile.containerName, { state: "running" }); // Three responses: the ownership check that classifies the port holder, then @@ -425,6 +432,7 @@ describe("managed vLLM serving-port guard (#8685)", () => { () => managed, () => managed, ]); + mockDefaultDockerOwnership(managed); // An interrupted install persists no runtime receipt, and a profile without // managed bearer auth carries no auth label, so lifecycle recovery cannot // admit the container this very install left behind. @@ -449,12 +457,28 @@ describe("managed vLLM serving-port guard (#8685)", () => { ); expect(mocks.dockerRunDetached).toHaveBeenCalled(); expect(errSpy.mock.calls.flat().join("\n")).not.toContain("another process"); + const dockerOptions = [ + ...mocks.dockerImageInspectFormat.mock.calls.map((call) => call[2]), + ...mocks.dockerPullWithProgressWatchdog.mock.calls.map((call) => call[1]), + ...mocks.dockerSpawn.mock.calls.map((call) => call[1]), + ...mocks.dockerForceRm.mock.calls.map((call) => call[1]), + ...mocks.dockerRunDetached.mock.calls.map((call) => call[1]), + ...mocks.dockerCapture.mock.calls.map((call) => call[1]), + ]; + expect(dockerOptions.length).toBeGreaterThan(0); + expect(new Set(dockerOptions.map((options) => options?.env?.DOCKER_CONTEXT))).toEqual( + new Set(["default"]), + ); + expect(new Set(dockerOptions.map((options) => options?.env?.DOCKER_HOST))).toEqual( + new Set([undefined]), + ); }); it("still refuses when an unlabeled container holds the serving port", async () => { const profile = detectVllmProfile({ platform: "spark", type: "nvidia" })!; const foreign = vllmContainerRow(profile.containerName, { label: "", state: "running" }); mockSuccessfulVllmInstall(mocks, profile.containerName, [() => foreign, () => foreign]); + mockDefaultDockerOwnership(foreign); mocks.recoverHostLocalManagedVllmEndpoint.mockReturnValue(null); const result = await installVllm(profile, { @@ -469,11 +493,24 @@ describe("managed vLLM serving-port guard (#8685)", () => { expect(mocks.dockerRunDetached).not.toHaveBeenCalled(); expect(errSpy.mock.calls.flat().join("\n")).toContain("already in use"); }); + /** Return each ownership row from the physical host's default Docker context. */ + function mockDefaultDockerOwnership(...rows: string[]): void { + const base = mocks.dockerCapture.getMockImplementation(); + let rowIndex = 0; + mocks.dockerCapture.mockImplementation( + (args: readonly string[], options?: { env?: NodeJS.ProcessEnv }) => + args[0] === "container" && options?.env?.DOCKER_CONTEXT === "default" + ? (rows[Math.min(rowIndex++, rows.length - 1)] ?? "") + : (base?.(args, options) ?? ""), + ); + } + /** Report the host port the managed container publishes for container 8000. */ function publishContainerPort(hostPort: number): void { const base = mocks.dockerCapture.getMockImplementation(); - mocks.dockerCapture.mockImplementation((args: readonly string[]) => - args[0] === "port" ? `0.0.0.0:${String(hostPort)}\n` : (base?.(args) ?? ""), + mocks.dockerCapture.mockImplementation( + (args: readonly string[], options?: { env?: NodeJS.ProcessEnv }) => + args[0] === "port" ? `0.0.0.0:${String(hostPort)}\n` : (base?.(args, options) ?? ""), ); } @@ -485,6 +522,7 @@ describe("managed vLLM serving-port guard (#8685)", () => { () => managed, () => managed, ]); + mockDefaultDockerOwnership(managed); mocks.recoverHostLocalManagedVllmEndpoint.mockReturnValue(null); // Our managed container serves another port, so an unrelated process owns // the port that failed. Removing this container would free nothing. diff --git a/src/lib/inference/vllm.ts b/src/lib/inference/vllm.ts index 1eeaf0d718e..1cb47eeadde 100644 --- a/src/lib/inference/vllm.ts +++ b/src/lib/inference/vllm.ts @@ -1242,7 +1242,7 @@ function startContainer( // avoids deleting an unrelated same-name container if the name changes hands. const replacement = vllmContainerReplacementTarget( profile.containerName, - model.managedBearerAuth ? dockerEnv : undefined, + model.managedBearerAuth || expectedReplacementContainerId !== undefined ? dockerEnv : undefined, expectedReplacementContainerId, ); if (!replacement.ok) return replacement; @@ -1949,10 +1949,6 @@ function applyRequestedVllmGpuDevice( return requestedGpuDevice ? selectVllmGpuDevice(profile, requestedGpuDevice) : profile; } -/** - * Name the process holding the serving port so the operator can act, matching - * how the Ollama auth proxy reports its own port conflict. - */ /** * Container id of this install's own managed container when that container is * what holds the serving port. @@ -1973,12 +1969,13 @@ function applyRequestedVllmGpuDevice( function adoptableServingPortHolder( containerName: string, servingPort: number, + dockerEnv: Record, ): string | undefined { - const ownership = inspectVllmContainerOwnership(containerName); + const ownership = inspectVllmContainerOwnershipInDockerEnv(containerName, dockerEnv); if (ownership.kind !== "managed" || !ownership.running) return undefined; // The managed container always publishes the fixed container port 8000. const published = dockerCapture(["port", containerName, "8000"], { - env: buildVllmDockerEnv(), + env: dockerEnv, ignoreError: true, timeout: 10_000, })?.match(/:(\d+)\s*$/); @@ -1986,6 +1983,10 @@ function adoptableServingPortHolder( return ownership.containerId; } +/** + * Name the process holding the serving port so the operator can act, matching + * how the Ollama auth proxy reports its own port conflict. + */ function printServingPortConflict(probe: ServingPortProbe): void { console.error( ` vLLM install failed: port ${String(VLLM_PORT)} is already in use by another process.`, @@ -2348,6 +2349,7 @@ async function runVllmInstall( // Port 25000 is not checked here: it belongs to the managed-cluster // rendezvous contract and this single-node path never binds it. let recoveredHostLocalContainerId: string | undefined; + let recoveredHostLocalDockerEnv: Record | undefined; const servingPort = await opts.checkServingPort?.(VLLM_PORT); if (servingPort && !servingPort.ok) { // An interrupted host-local install can leave its authenticated managed @@ -2356,18 +2358,24 @@ async function runVllmInstall( // credential fingerprint. The replacement guard below then removes the // inspected container ID immediately before the new launch. try { + const hostLocalDockerEnv = buildLocalManagedVllmDockerEnv(); const recovered = recoverHostLocalManagedVllmEndpoint(); if (recovered?.baseUrl === `http://127.0.0.1:${String(VLLM_PORT)}`) { recoveredHostLocalContainerId = recovered.containerId; // Continue through the ordinary managed-container replacement path. } else { - const adopted = adoptableServingPortHolder(runtimeProfile.containerName, VLLM_PORT); + const adopted = adoptableServingPortHolder( + runtimeProfile.containerName, + VLLM_PORT, + hostLocalDockerEnv, + ); if (adopted === undefined) { printServingPortConflict(servingPort); return { ok: false }; } recoveredHostLocalContainerId = adopted; } + recoveredHostLocalDockerEnv = hostLocalDockerEnv; } catch (error) { console.error( ` vLLM install failed: managed host-local vLLM recovery could not verify the container: ${(error as Error).message}`, @@ -2410,9 +2418,8 @@ async function runVllmInstall( let hostLocalApiKey: string | null = null; let localDockerEnv = dualStationPlan ? buildLocalDualStationDockerEnv() - : model.managedBearerAuth - ? buildLocalManagedVllmDockerEnv() - : buildVllmDockerEnv(); + : (recoveredHostLocalDockerEnv ?? + (model.managedBearerAuth ? buildLocalManagedVllmDockerEnv() : buildVllmDockerEnv())); let gpuMemoryWarningShown = false; const reportGpuMemoryWarning = (result: GpuMemoryPreflightResult): void => { if (!result.ok || !result.warning || gpuMemoryWarningShown) return; @@ -2463,7 +2470,9 @@ async function runVllmInstall( } else { const replacement = vllmContainerReplacementTarget( runtimeProfile.containerName, - model.managedBearerAuth ? localDockerEnv : undefined, + model.managedBearerAuth || recoveredHostLocalContainerId !== undefined + ? localDockerEnv + : undefined, recoveredHostLocalContainerId, ); if (!replacement.ok) { From baed467e001558efef8b6d8e54edadc222fe9bf1 Mon Sep 17 00:00:00 2001 From: Julie Yaunches Date: Fri, 11 Sep 2026 15:30:12 -0400 Subject: [PATCH 4/4] fix(inference): match adopted vLLM binding address Signed-off-by: Julie Yaunches --- src/lib/inference/vllm-serving-port.test.ts | 35 ++++++++++++++++++--- src/lib/inference/vllm.ts | 18 +++++++---- 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/src/lib/inference/vllm-serving-port.test.ts b/src/lib/inference/vllm-serving-port.test.ts index a08feccab0c..474cf867ad7 100644 --- a/src/lib/inference/vllm-serving-port.test.ts +++ b/src/lib/inference/vllm-serving-port.test.ts @@ -437,7 +437,7 @@ describe("managed vLLM serving-port guard (#8685)", () => { // managed bearer auth carries no auth label, so lifecycle recovery cannot // admit the container this very install left behind. mocks.recoverHostLocalManagedVllmEndpoint.mockReturnValue(null); - publishContainerPort(8000); + publishContainerBindings("0.0.0.0:8000", "[::]:8000"); const checkServingPort = vi.fn(async () => ({ ok: false, reason: "port 8000 is held by docker-proxy (PID 4242)", @@ -505,12 +505,12 @@ describe("managed vLLM serving-port guard (#8685)", () => { ); } - /** Report the host port the managed container publishes for container 8000. */ - function publishContainerPort(hostPort: number): void { + /** Report the host bindings the managed container publishes for container 8000. */ + function publishContainerBindings(...bindings: string[]): void { const base = mocks.dockerCapture.getMockImplementation(); mocks.dockerCapture.mockImplementation( (args: readonly string[], options?: { env?: NodeJS.ProcessEnv }) => - args[0] === "port" ? `0.0.0.0:${String(hostPort)}\n` : (base?.(args, options) ?? ""), + args[0] === "port" ? `${bindings.join("\n")}\n` : (base?.(args, options) ?? ""), ); } @@ -526,7 +526,32 @@ describe("managed vLLM serving-port guard (#8685)", () => { mocks.recoverHostLocalManagedVllmEndpoint.mockReturnValue(null); // Our managed container serves another port, so an unrelated process owns // the port that failed. Removing this container would free nothing. - publishContainerPort(19_000); + publishContainerBindings("0.0.0.0:19000"); + + const result = await installVllm(profile, { + hasImage: true, + nonInteractive: true, + promptFn: vi.fn<(q: string) => Promise>(), + checkServingPort: async () => ({ ok: false, reason: "port 8000 is held" }), + }); + + expect(result).toEqual({ ok: false }); + expect(mocks.dockerForceRm).not.toHaveBeenCalled(); + expect(mocks.dockerRunDetached).not.toHaveBeenCalled(); + expect(errSpy.mock.calls.flat().join("\n")).toContain("already in use"); + }); + + it("refuses a managed container bound away from the probed loopback address", async () => { + const profile = detectVllmProfile({ platform: "spark", type: "nvidia" })!; + const managed = vllmContainerRow(profile.containerName, { state: "running" }); + mockSuccessfulVllmInstall(mocks, profile.containerName, [ + () => managed, + () => managed, + () => managed, + ]); + mockDefaultDockerOwnership(managed); + mocks.recoverHostLocalManagedVllmEndpoint.mockReturnValue(null); + publishContainerBindings("127.0.0.2:8000", "192.168.1.10:8000"); const result = await installVllm(profile, { hasImage: true, diff --git a/src/lib/inference/vllm.ts b/src/lib/inference/vllm.ts index 1cb47eeadde..f92c101e60d 100644 --- a/src/lib/inference/vllm.ts +++ b/src/lib/inference/vllm.ts @@ -1961,10 +1961,11 @@ function applyRequestedVllmGpuDevice( * * Ownership alone is not enough. A managed container published on a different * host port is not the process holding this port, and removing it would free - * nothing while destroying an unrelated runtime, so its published binding must - * match the port that failed. Every other state — a foreign or unlabeled - * holder, an ambiguous inspection, a distributed head or worker, a container - * that is not running, and an unreadable binding — remains a conflict. + * nothing while destroying an unrelated runtime, so one published binding must + * cover the probed loopback address and match the port that failed. Every other + * state — a foreign or unlabeled holder, an ambiguous inspection, a distributed + * head or worker, a container that is not running, and an unreadable or + * address-mismatched binding — remains a conflict. */ function adoptableServingPortHolder( containerName: string, @@ -1978,8 +1979,13 @@ function adoptableServingPortHolder( env: dockerEnv, ignoreError: true, timeout: 10_000, - })?.match(/:(\d+)\s*$/); - if (!published || Number(published[1]) !== servingPort) return undefined; + }) + ?.split(/\r?\n/u) + .some((binding) => { + const endpoint = binding.trim().match(/^(127[.]0[.]0[.]1|0[.]0[.]0[.]0):(\d+)$/u); + return endpoint !== null && Number(endpoint[2]) === servingPort; + }); + if (!published) return undefined; return ownership.containerId; }