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
49 changes: 45 additions & 4 deletions docs/inference/set-up-vllm.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,15 @@ $$nemoclaw onboard

Select **Local vLLM**.
On generic hosts, the entry includes an experimental label.
N1x does not offer the existing-server entry; it offers only the managed **Install vLLM** or **Start vLLM** entry with the **Deferred preview** label.
On N1x, the readiness gate admits only explicit managed-preview intent, so the user-facing onboarding flow does not offer the existing-server entry.
NemoClaw validates the detected endpoint and records the model reported by `/v1/models`.

When you request a vLLM serving profile, the detected server must report the profile's served model alias or a safe `root` that matches the profile's declared model.
NemoClaw stops onboarding when neither identifier matches, rather than recording a route that the profile does not declare.
Stop the server and rerun the original command, or unset `NEMOCLAW_SERVING_PRESET` and `NEMOCLAW_PROVIDER` before you start fresh onboarding to keep the detected model.

On hosts other than N1x, when an install request encounters an already-running server, NemoClaw selects that server instead of starting another container.
On N1x, explicit managed-preview intent never reuses an already-running server.
On hosts other than N1x, when an install request without a managed GPU selection encounters an already-running server, NemoClaw selects that server instead of starting another container.
On N1x, explicit managed-preview intent keeps the managed entry and stops onboarding when another vLLM server occupies the configured port.
Stop the server, then rerun managed onboarding.

```bash
Expand Down Expand Up @@ -146,6 +146,47 @@ Only managed single-host vLLM installation accepts this selector.
$$nemoclaw onboard --profile <profile-id> --vllm-gpu-device <index-or-uuid>
```

#### Handle a Running vLLM Server

The single-host managed vLLM runtime is host-global.
Destroying a sandbox does not stop it, and the container remains available for other sandboxes.
On hosts other than N1x, later onboarding normally reuses a server that is already listening on the configured vLLM port instead of replacing it automatically.
When a managed GPU selection accompanies explicit managed-install intent, NemoClaw preserves that intent and reports the running-server conflict.

Choose one action:

- On hosts other than N1x, to reuse the running server, omit `--vllm-gpu-device` and select **Local vLLM**.
- To change the GPU or port, inspect the sandbox registry for every gateway-port environment on the host.
Run `NEMOCLAW_GATEWAY_PORT=<port> $$nemoclaw list` for port `8080` and for every non-default port represented by a numeric directory under `~/.nemoclaw/gateways/`.
Treat every sandbox that reports provider `vllm-local` as a possible consumer because the list output does not distinguish local vLLM from Local NIM.
Continue only when no environment reports that provider; otherwise, keep the server running or move each possible consumer to another inference provider.
Stop an operator-managed server through the system service, container, or process workflow that started it.
For a NemoClaw-managed container, inspect its ID and labels before stopping it:

```bash
docker container inspect \
--format '{{.Id}} {{json .Config.Labels}}' \
nemoclaw-vllm
```

Continue only when the output includes `"com.nvidia.nemoclaw.managed-vllm":"true"` and does not include `com.nvidia.nemoclaw.vllm-role`, `com.nvidia.nemoclaw.vllm-endpoint`, or `com.nvidia.nemoclaw.vllm-cluster`.
Do not stop the container when any distributed label is present; follow the matching multi-node vLLM guide instead.
Stop that exact container ID:

```bash
docker container stop <container-id>
```

Set `NEMOCLAW_VLLM_PORT` before onboarding if you are changing the port.
Rerun managed onboarding:

```bash
$$nemoclaw onboard --profile <profile-id> --vllm-gpu-device <index-or-uuid>
```

Accept the result when onboarding completes and `$$nemoclaw list` reports the new sandbox with provider `vllm-local` and the expected model.
NemoClaw does not stop or recreate a running vLLM server automatically because another sandbox can use it.

NemoClaw requires a resumed session to reuse the recorded GPU selector.
A legacy in-progress session cannot add one, and a resumed session cannot change one.
Run `$$nemoclaw onboard --fresh` when you need a different GPU selection.
Expand All @@ -166,7 +207,7 @@ The container carries NemoClaw ownership and immutable catalog labels so later o
NemoClaw also records an owner-only, credential-free runtime receipt that binds those labels to the container ID and API-key fingerprint.

If an existing `nemoclaw-vllm` container does not carry that label, NemoClaw preserves it and asks you to remove or rename it before installing managed vLLM.
On hosts other than N1x, if another vLLM server already occupies the configured vLLM port, NemoClaw selects the existing-server path and applies the model-identity checks described above.
On hosts other than N1x, if another vLLM server already occupies the configured vLLM port and no managed GPU selection accompanies explicit managed-install intent, NemoClaw selects the existing-server path and applies the model-identity checks described above.
On N1x, NemoClaw stops managed onboarding and requires you to stop that server before trying the Deferred preview again.
If another process holds the configured single-host port, NemoClaw stops the managed install before it persists a bearer key or records the selected model.
NemoClaw also stops before storage prompts, cache creation, image pull, or container start.
Expand Down
1 change: 1 addition & 0 deletions src/lib/onboard/sandbox-readiness-tracing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ describe("createSandboxReadyWaiter", () => {
target: TARGET,
isLinuxDockerDriverGatewayEnabled: () => true,
sleep,
now: () => 0,
});

await expect(waitForSandboxReady(NAME, 2, 3)).resolves.toEqual({
Expand Down
52 changes: 49 additions & 3 deletions src/lib/onboard/setup-nim-flow-vllm-gpu-device.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

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

import type { VllmProfile } from "../inference/vllm";
import { NEMOCLAW_VLLM_GPU_DEVICE_ENV } from "../inference/vllm-models";
import { makeDeps, makeHostState } from "./__test-helpers__/setup-nim-flow";
import type { SetupNimFlowDeps } from "./setup-nim-flow";
Expand Down Expand Up @@ -44,6 +45,7 @@ describe("managed vLLM GPU provider selection", () => {
makeDeps({
isNonInteractive: () => true,
getNonInteractiveProvider: () => "vllm",
vllmPort: 18000,
detectInferenceProviderHostState: () =>
makeHostState({
vllmRunning: true,
Expand All @@ -54,15 +56,59 @@ describe("managed vLLM GPU provider selection", () => {
}),
);

await expect(setupNim(null)).rejects.toThrow(
"applies only when NemoClaw installs managed vLLM",
await expect(setupNim(null)).rejects.toThrow("vLLM is already running on localhost:18000");
expect(abortNonInteractive).toHaveBeenCalledWith(
expect.stringContaining(
"Omit --vllm-gpu-device and rerun with NEMOCLAW_PROVIDER=vllm",
),
);
expect(abortNonInteractive).toHaveBeenCalledWith(
expect.stringContaining("only if no other gateway or distributed deployment uses it"),
);
expect(abortNonInteractive).toHaveBeenCalledWith(
expect.stringContaining("selected provider is 'vllm'"),
expect.stringContaining("NEMOCLAW_VLLM_PORT"),
);
expect(handleVllmSelection).not.toHaveBeenCalled();
});

it("preserves managed intent and explains how to resolve a running-server conflict", async () => {
vi.stubEnv(NEMOCLAW_VLLM_GPU_DEVICE_ENV, "2");
const abortNonInteractive = vi.fn((message: string): never => {
throw new Error(message);
});
const installVllm = vi.fn<SetupNimFlowDeps["installVllm"]>();
const profile = { name: "DGX Station" } as VllmProfile;
const setupNim = createSetupNim(
makeDeps({
isNonInteractive: () => true,
getNonInteractiveProvider: () => "install-vllm",
vllmPort: 18000,
detectInferenceProviderHostState: () =>
makeHostState({
vllmRunning: true,
vllmProfile: profile,
vllmEntries: [{ key: "install-vllm", label: "Start vLLM (DGX Station)" }],
}),
abortNonInteractive,
installVllm,
}),
);

await expect(setupNim(null)).rejects.toThrow("vLLM is already running on localhost:18000");
expect(abortNonInteractive).toHaveBeenCalledWith(
expect.stringContaining(
"Omit --vllm-gpu-device and rerun with NEMOCLAW_PROVIDER=vllm",
),
);
expect(abortNonInteractive).toHaveBeenCalledWith(
expect.stringContaining("only if no other gateway or distributed deployment uses it"),
);
expect(abortNonInteractive).toHaveBeenCalledWith(
expect.stringContaining("NEMOCLAW_VLLM_PORT"),
);
expect(installVllm).not.toHaveBeenCalled();
});

it("allows the persisted device when resuming its managed vLLM provider", async () => {
vi.stubEnv(NEMOCLAW_VLLM_GPU_DEVICE_ENV, "2");
const handleVllmSelection = vi.fn<SetupNimFlowDeps["handleVllmSelection"]>(async (state) => {
Expand Down
10 changes: 8 additions & 2 deletions src/lib/onboard/setup-nim-flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1111,7 +1111,10 @@ describe("createSetupNim", () => {

expect(error).toHaveBeenCalledWith(expect.stringContaining("requires managed vLLM"));
expect(error).toHaveBeenCalledWith(expect.stringContaining("localhost:8000"));
expect(error).toHaveBeenCalledWith(expect.stringContaining("Stop the existing server"));
expect(error).toHaveBeenCalledWith(
expect.stringContaining("only if no other gateway or distributed deployment uses it"),
);
expect(error).toHaveBeenCalledWith(expect.stringContaining("NEMOCLAW_VLLM_PORT"));
expect(error).toHaveBeenCalledWith(
expect.stringContaining("NEMOCLAW_PROVIDER=install-vllm"),
);
Expand Down Expand Up @@ -1435,7 +1438,10 @@ describe("createSetupNim", () => {

expect(prompt).toHaveBeenCalledTimes(2);
expect(selectFromNumberedMenu).toHaveBeenCalledTimes(2);
expect(error).toHaveBeenCalledWith(expect.stringContaining("stop the existing server"));
expect(error).toHaveBeenCalledWith(
expect.stringContaining("only if no other gateway or distributed deployment uses it"),
);
expect(error).toHaveBeenCalledWith(expect.stringContaining("NEMOCLAW_VLLM_PORT"));
expect(installVllm).not.toHaveBeenCalled();
expect(handleVllmSelection).not.toHaveBeenCalled();
expect(handleRemoteProviderSelection).toHaveBeenCalledOnce();
Expand Down
27 changes: 21 additions & 6 deletions src/lib/onboard/setup-nim-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,16 +299,21 @@ function assertVllmGpuProviderSelection(
recoveredFromSandbox: boolean,
deps: Pick<
SetupNimFlowDeps,
"abortNonInteractive" | "error" | "exitProcess" | "isNonInteractive"
"abortNonInteractive" | "error" | "exitProcess" | "isNonInteractive" | "vllmPort"
>,
): void {
const requestedDevice = String(process.env.NEMOCLAW_VLLM_GPU_DEVICE ?? "").trim();
const resumedManagedVllm = recoveredFromSandbox && selected.key === "vllm";
if (!requestedDevice || selected.key === "install-vllm" || resumedManagedVllm) return;

const message =
`--vllm-gpu-device applies only when NemoClaw installs managed vLLM; ` +
`the selected provider is '${selected.key}'.`;
selected.key === "vllm"
? `vLLM is already running on localhost:${deps.vllmPort}, so --vllm-gpu-device cannot change its GPU. ` +
`Omit --vllm-gpu-device and rerun with NEMOCLAW_PROVIDER=vllm to reuse that server. ` +
`To select a different GPU, stop the server only if no other gateway or distributed deployment uses it. ` +
`Otherwise, keep it running and set NEMOCLAW_VLLM_PORT to an unused port before rerunning managed onboarding.`
: `--vllm-gpu-device applies only when NemoClaw installs managed vLLM; ` +
`the selected provider is '${selected.key}'.`;
deps.error(` ${message}`);
if (deps.isNonInteractive()) deps.abortNonInteractive(message);
deps.exitProcess(1);
Expand Down Expand Up @@ -613,11 +618,15 @@ async function handleEndpointProviderSelection(input: {
function vllmPortConflictMessage(
platform: InferenceProviderHostGpu["platform"],
port: number,
hasGpuSelection: boolean,
): string {
if (platform === "n1x") {
return `The N1x Deferred preview requires managed vLLM, but vLLM is already running on localhost:${port}. Stop the existing server, then rerun with NEMOCLAW_PROVIDER=install-vllm.`;
return `The N1x Deferred preview requires managed vLLM, but vLLM is already running on localhost:${port}. Stop the server only if no other gateway or distributed deployment uses it. Otherwise, keep it running and set NEMOCLAW_VLLM_PORT to an unused port. Then rerun with NEMOCLAW_PROVIDER=install-vllm.`;
}
return "vLLM is already running on this host. Select Local vLLM, or stop the existing server before selecting the managed install path.";
const reuseAction = hasGpuSelection
? "Omit --vllm-gpu-device and rerun with NEMOCLAW_PROVIDER=vllm to reuse it."
: "Rerun with NEMOCLAW_PROVIDER=vllm to reuse it.";
return `vLLM is already running on localhost:${port}. ${reuseAction} To change its GPU or port, stop the server only if no other gateway or distributed deployment uses it. Otherwise, keep it running and set NEMOCLAW_VLLM_PORT to an unused port before rerunning managed onboarding.`;
}

/**
Expand Down Expand Up @@ -1221,7 +1230,13 @@ export function createSetupNim(
continue selectionLoop;
}
if (vllmRunning) {
const message = vllmPortConflictMessage(gpu?.platform, deps.vllmPort);
const hasGpuSelection =
String(process.env.NEMOCLAW_VLLM_GPU_DEVICE ?? "").trim() !== "";
const message = vllmPortConflictMessage(
gpu?.platform,
deps.vllmPort,
hasGpuSelection,
);
deps.error(` ${message}`);
if (deps.isNonInteractive()) {
deps.abortNonInteractive(message);
Expand Down
24 changes: 23 additions & 1 deletion src/lib/onboard/vllm-menu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ describe("buildVllmMenuEntries", () => {
assert.equal(entries[0].label, "Start vLLM (DGX Station)");
});

it("labels only the N1x managed install entry as a Deferred preview (#8574)", () => {
it("keeps the Deferred label scoped to the pre-admission N1x managed entry (#8574)", () => {
const install = buildVllmMenuEntries({
vllmRunning: false,
vllmProfile: { name: "N1x" },
Expand All @@ -119,6 +119,9 @@ describe("buildVllmMenuEntries", () => {
log: () => {},
});

// N1x readiness rejects the running-server state without explicit managed
// intent. If this lower-level helper sees that state in isolation, it must
// not mislabel an operator-managed server as the Deferred managed preview.
assert.equal(install[0].label, "Install vLLM (N1x) [Deferred preview]");
assert.equal(running[0].label, "Local vLLM (localhost:8000) — running");
assert.doesNotMatch(running[0].label, /Deferred preview/);
Expand Down Expand Up @@ -209,6 +212,25 @@ describe("buildVllmMenuEntries", () => {
assert.match(logs[0], /selecting the running instance/);
});

it("preserves managed install intent when a running server conflicts with GPU selection", () => {
const logs: string[] = [];
const entries = buildVllmMenuEntries({
vllmRunning: true,
vllmProfile: { name: "DGX Station" },
experimental: false,
platform: "station",
hasVllmImage: true,
env: {
NEMOCLAW_PROVIDER: "install-vllm",
NEMOCLAW_VLLM_GPU_DEVICE: "2",
},
log: (message) => logs.push(message),
});

assert.equal(entries[0].key, "install-vllm");
assert.deepEqual(logs, []);
});

it("does not log the override note when the user did not request install-vllm", () => {
const logs: string[] = [];
buildVllmMenuEntries({
Expand Down
12 changes: 8 additions & 4 deletions src/lib/onboard/vllm-menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@
* available for this host." message. It also lets the caller surface managed
* vLLM by default for known DGX platforms while generic Linux stays gated, and
* logs a note when running-vLLM takes precedence over the env-var opt-in. N1x
* keeps the managed selection because its readiness exception is limited to
* the managed-vLLM preview.
* and explicit managed GPU selection keep the managed selection so the
* provider flow can report the running-server conflict without changing the
* user's intent.
*/

import { VLLM_PORT } from "../core/ports";
Expand Down Expand Up @@ -66,8 +67,11 @@ export function buildVllmMenuEntries(opts: BuildVllmMenuOptions): VllmMenuEntry[
const env = opts.env ?? process.env;
const userChoseManagedVllm =
(env.NEMOCLAW_PROVIDER || "").trim().toLowerCase() === MANAGED_VLLM_PROVIDER_KEY;
const keepN1xManagedPreview = userChoseManagedVllm && opts.platform === "n1x";
if (opts.vllmRunning && !keepN1xManagedPreview) {
const hasManagedVllmGpuSelection =
String(env.NEMOCLAW_VLLM_GPU_DEVICE ?? "").trim() !== "";
const preserveManagedVllmIntent =
userChoseManagedVllm && (opts.platform === "n1x" || hasManagedVllmGpuSelection);
if (opts.vllmRunning && !preserveManagedVllmIntent) {
if (userChoseManagedVllm) {
log(
` Note: NEMOCLAW_PROVIDER=install-vllm requested, but vLLM is already running on localhost:${VLLM_PORT} — selecting the running instance.`,
Expand Down
Loading