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
5 changes: 3 additions & 2 deletions docs/inference/use-local-inference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ Managed vLLM uses these profiles:

| Host profile | Default model |
|---|---|
| DGX Spark | `Qwen/Qwen3.6-27B-FP8` |
| DGX Spark | `nvidia/Qwen3.6-35B-A3B-NVFP4` |
| DGX Station | `Qwen/Qwen3.6-27B-FP8` |
| Linux with an NVIDIA GPU | `nvidia/NVIDIA-Nemotron-3-Nano-4B-FP8` |

Expand Down Expand Up @@ -338,7 +338,8 @@ Recognised slugs:

| Slug | Hugging Face model | Notes |
|---|---|---|
| `qwen3.6-27b` | `Qwen/Qwen3.6-27B-FP8` | Default on DGX Spark and DGX Station profiles |
| `qwen3.6-27b` | `Qwen/Qwen3.6-27B-FP8` | Default on the DGX Station profile |
| `qwen3.6-35b-a3b-nvfp4` | `nvidia/Qwen3.6-35B-A3B-NVFP4` | Default on the DGX Spark profile |
| `nemotron-3-nano-4b` | `nvidia/NVIDIA-Nemotron-3-Nano-4B-FP8` | Default on the generic Linux + NVIDIA GPU profile |
| `deepseek-r1-distill-70b` | `deepseek-ai/DeepSeek-R1-Distill-Llama-70B` | Gated. Requires Hugging Face license acceptance |

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1385,7 +1385,7 @@ Set them before running `nemoclaw onboard`.
| `NEMOCLAW_SANDBOX` | sandbox name | Alternate spelling of `NEMOCLAW_SANDBOX_NAME`; used by `services` and `debug` lookups when neither a flag nor `NEMOCLAW_SANDBOX_NAME` is set. |
| `NEMOCLAW_INSTALL_REF` | git ref | For internal installer commands: the git ref to install from. Overridden by the `--install-ref` flag. |
| `NEMOCLAW_INSTALL_TAG` | release tag | For internal installer commands: the release tag to install. Defaults to the admin-promoted `lkg` tag when unset. Overridden by the `--install-tag` flag. |
| `NEMOCLAW_VLLM_MODEL` | registry slug or Hugging Face model id | Selects the model the managed-vLLM install path serves. Recognised slugs: `qwen3.6-27b`, `nemotron-3-nano-4b`, `deepseek-r1-distill-70b`. Unset uses the per-platform profile default. Gated models (e.g. `deepseek-r1-distill-70b`) require `HF_TOKEN` or `HUGGING_FACE_HUB_TOKEN`. |
| `NEMOCLAW_VLLM_MODEL` | registry slug or Hugging Face model id | Selects the model the managed-vLLM install path serves. Recognised slugs: `qwen3.6-27b`, `qwen3.6-35b-a3b-nvfp4`, `nemotron-3-nano-4b`, `deepseek-r1-distill-70b`. Unset uses the per-platform profile default. Gated models (e.g. `deepseek-r1-distill-70b`) require `HF_TOKEN` or `HUGGING_FACE_HUB_TOKEN`. |
| `NEMOCLAW_MODEL_ROUTER_PYTHON` | absolute path | Pins the host Python interpreter used to create the Model Router virtual environment. Strict. NemoClaw probes only that interpreter and aborts with the failure reason if it does not qualify, rather than silently falling back to another python. Relative command names such as `python3.12` are rejected. When unset, NemoClaw probes `python3.13`, `python3.12`, `python3.11`, `python3.10`, and bare `python3`, retains every interpreter whose version is in `[3.10, 3.14)` and whose `ensurepip`, `pyexpat`, `ssl`, and `venv` stdlib modules import cleanly, and tries `python -m venv` on each in priority order until one succeeds. Set the pin when the auto-discovered interpreter is broken (for example, Homebrew `python@3.14` with a `pyexpat` dlopen mismatch on macOS). |

#### Linux Ollama install mode details
Expand Down
39 changes: 39 additions & 0 deletions src/lib/inference/vllm-models.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,45 @@ describe("vllm model registry", () => {
expect(cmd).toContain("--tool-call-parser hermes");
expect(cmd).not.toContain("--reasoning-parser qwen3");
});

it("registers the Qwen3.6-35B NVFP4 checkpoint for DGX Spark", () => {
const qwen35b = VLLM_MODELS.find((m) => m.envValue === "qwen3.6-35b-a3b-nvfp4");
expect(qwen35b).toBeDefined();
expect(qwen35b!.id).toBe("nvidia/Qwen3.6-35B-A3B-NVFP4");
expect(qwen35b!.gated).toBe(false);
});

it("builds the NVFP4 serve command with env exports, the fastsafetensors install, and additive model flags", () => {
const qwen35b = VLLM_MODELS.find((m) => m.envValue === "qwen3.6-35b-a3b-nvfp4");
const cmd = buildVllmServeCommand(qwen35b!);
// Env exports are prefixed before serve.
expect(cmd).toContain("export VLLM_USE_FLASHINFER_MOE_FP4=0");
expect(cmd).toContain("export VLLM_FP8_MOE_BACKEND=flashinfer_cutlass");
expect(cmd).toContain("export FLASHINFER_DISABLE_VERSION_CHECK=1");
expect(cmd).toContain("export CUTE_DSL_ARCH=sm_121a");
// fastsafetensors is always installed and used.
expect(cmd).toContain("pip install vllm[fastsafetensors]");
expect(cmd).toContain("--load-format fastsafetensors");
// Model-specific flags appended on top of the shared serving defaults.
expect(cmd).toContain("vllm serve nvidia/Qwen3.6-35B-A3B-NVFP4");
expect(cmd).toContain("--quantization modelopt");
expect(cmd).toContain("--kv-cache-dtype fp8");
expect(cmd).toContain("--attention-backend flashinfer");
expect(cmd).toContain("--moe-backend marlin");
expect(cmd).toContain("--enable-auto-tool-choice");
expect(cmd).toContain("--tool-call-parser qwen3_coder");
expect(cmd).toContain("--reasoning-parser qwen3");
expect(cmd).toContain("--max-model-len 65536");
expect(cmd).toContain(
`--speculative-config '{"method":"mtp","num_speculative_tokens":3,"moe_backend":"triton"}'`,
);
// Shared defaults are kept: 0.7 utilization and the single-node parallel
// flags (harmless on Spark), not a model-specific 0.85 override.
expect(cmd).toContain("--gpu-memory-utilization 0.7");
expect(cmd).toContain("--pipeline-parallel-size 1");
expect(cmd).toContain("--data-parallel-size 1");
expect(cmd).not.toContain("--gpu-memory-utilization 0.85");
});
});

describe("preflightVllmModelEnv", () => {
Expand Down
77 changes: 70 additions & 7 deletions src/lib/inference/vllm-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ export interface VllmModelDef {
modelArgs: string[];
/** True when the upstream HF repo requires accepting a licence. */
gated: boolean;
/**
* Environment variables exported immediately before `vllm serve` (e.g.
* FlashInfer / MoE-backend selection, target SM arch). Joined as
* `export K=V && …` so they apply to the serve process inside the
* container shell.
*/
serveEnv?: Record<string, string>;
}

export const VLLM_MODELS: readonly VllmModelDef[] = [
Expand Down Expand Up @@ -83,6 +90,54 @@ export const VLLM_MODELS: readonly VllmModelDef[] = [
modelArgs: ["--load-format", "fastsafetensors"],
gated: false,
},
{
id: "nvidia/Qwen3.6-35B-A3B-NVFP4",
label: "Qwen3.6 35B-A3B NVFP4",
envValue: "qwen3.6-35b-a3b-nvfp4",
maxModelLen: 65536,
// Additive flags on top of the shared serving defaults. The shared flags
// already cover --tensor-parallel-size/--pipeline-parallel-size/
// --data-parallel-size (all 1 — harmless on a single Spark node),
// --gpu-memory-utilization 0.7, --port 8000, and --trust-remote-code;
// --max-model-len comes from maxModelLen above.
modelArgs: [
"--dtype",
"auto",
"--quantization",
"modelopt",
"--kv-cache-dtype",
"fp8",
"--attention-backend",
"flashinfer",
"--moe-backend",
"marlin",
"--max-num-seqs",
"4",
"--max-num-batched-tokens",
"8192",
"--enable-chunked-prefill",
"--async-scheduling",
"--enable-prefix-caching",
"--enable-auto-tool-choice",
"--tool-call-parser",
"qwen3_coder",
"--reasoning-parser",
"qwen3",
"--speculative-config",
`'{"method":"mtp","num_speculative_tokens":3,"moe_backend":"triton"}'`,
"--load-format",
"fastsafetensors",
],
gated: false,
// Arch- and backend-specific knobs required for the NVFP4 MoE checkpoint
// on DGX Spark (GB10 / sm_121a) with the FlashInfer CUTLASS FP8 path.
serveEnv: {
VLLM_USE_FLASHINFER_MOE_FP4: "0",
VLLM_FP8_MOE_BACKEND: "flashinfer_cutlass",
FLASHINFER_DISABLE_VERSION_CHECK: "1",
CUTE_DSL_ARCH: "sm_121a",
},
},
] as const;

export const DEFAULT_VLLM_MODEL: VllmModelDef = VLLM_MODELS[0];
Expand All @@ -92,8 +147,9 @@ const HF_TOKEN_ENV_KEYS = ["HF_TOKEN", "HUGGING_FACE_HUB_TOKEN"] as const;
/**
* Look up the requested express-vLLM model from `NEMOCLAW_VLLM_MODEL`.
* Returns `null` when the env var is empty so the caller can fall back to
* the per-platform profile default (Spark/Station prefer Qwen3.6-27B, the
* generic Linux profile prefers Nemotron-Nano-4B for VRAM headroom).
* the per-platform profile default (Station prefers Qwen3.6-27B, Spark the
* Qwen3.6-35B-A3B NVFP4 checkpoint, and the generic Linux profile prefers
* Nemotron-Nano-4B for VRAM headroom).
*
* Match is case-insensitive against either the `envValue` slug or the full
* HF id. Throws when the env var names something not in the registry so the
Expand Down Expand Up @@ -184,17 +240,24 @@ const SHARED_VLLM_ARGS: readonly string[] = [
] as const;

/**
* Build the `vllm serve` command line for the supplied model, with the
* shared serving flags merged with the model-specific args from the
* registry. The command starts with the `pip install` that pulls the
* `fastsafetensors` extra so existing express scripts keep working.
* Build the `vllm serve` command line for the supplied model: the shared
* serving flags merged with the model-specific args from the registry.
*
* The command is prefixed with the `pip install` that pulls the
* `fastsafetensors` extra so existing express scripts keep working; a model
* may prepend env exports via `serveEnv`.
*/
export function buildVllmServeCommand(model: VllmModelDef): string {
const envPrefix = model.serveEnv
? `${Object.entries(model.serveEnv)
.map(([key, value]) => `export ${key}=${value}`)
.join(" && ")} && `
: "";
const args = [
...SHARED_VLLM_ARGS,
"--max-model-len",
String(model.maxModelLen),
...model.modelArgs,
];
return `pip install vllm[fastsafetensors] && vllm serve ${model.id} ${args.join(" ")}`;
return `${envPrefix}pip install vllm[fastsafetensors] && vllm serve ${model.id} ${args.join(" ")}`;
}
29 changes: 21 additions & 8 deletions src/lib/inference/vllm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,23 @@ interface VllmProfile {
loadTimeoutSec: number;
}

// vllm 0.21.1rc1.dev323+g1fc2cee50
const UPSTREAM_VLLM_IMAGE =
"vllm/vllm-openai:nightly-1fc2cee50a09a094b9f2bbdfcb0ab0cadb536712";
const NGC_VLLM_IMAGE = "nvcr.io/nvidia/vllm:26.03.post1-py3";

function nemotronNanoModel(): VllmModelDef {
const match = VLLM_MODELS.find((m) => m.envValue === "nemotron-3-nano-4b");
if (!match) throw new Error("vllm-models registry is missing the nemotron-3-nano-4b entry");
return match;
}

function qwen35bNvfp4Model(): VllmModelDef {
const match = VLLM_MODELS.find((m) => m.envValue === "qwen3.6-35b-a3b-nvfp4");
if (!match) throw new Error("vllm-models registry is missing the qwen3.6-35b-a3b-nvfp4 entry");
return match;
}

const HF_TOKEN_ENV_KEYS = ["HF_TOKEN", "HUGGING_FACE_HUB_TOKEN"] as const;

function pickHfTokenEntry(
Expand Down Expand Up @@ -105,8 +116,8 @@ export function buildHfTokenForwardEnv(

const SPARK_PROFILE: VllmProfile = {
name: "DGX Spark",
image: "nvcr.io/nvidia/vllm:26.03.post1-py3",
defaultModel: DEFAULT_VLLM_MODEL,
image: UPSTREAM_VLLM_IMAGE,
defaultModel: qwen35bNvfp4Model(),
containerName: "nemoclaw-vllm",
dockerRunFlags: [
"--gpus",
Expand Down Expand Up @@ -142,8 +153,8 @@ const SPARK_PROFILE: VllmProfile = {
// DGX Station.
const STATION_PROFILE: VllmProfile = {
name: "DGX Station",
image: SPARK_PROFILE.image,
defaultModel: SPARK_PROFILE.defaultModel,
image: NGC_VLLM_IMAGE,
defaultModel: DEFAULT_VLLM_MODEL,
containerName: "nemoclaw-vllm",
dockerRunFlags: SPARK_PROFILE.dockerRunFlags,
buildDockerRunFlags: () => {
Expand Down Expand Up @@ -176,7 +187,7 @@ const STATION_PROFILE: VllmProfile = {
// most GPUs.
const GENERIC_LINUX_PROFILE: VllmProfile = {
name: "Linux + NVIDIA GPU",
image: SPARK_PROFILE.image,
image: NGC_VLLM_IMAGE,
defaultModel: nemotronNanoModel(),
containerName: "nemoclaw-vllm",
dockerRunFlags: SPARK_PROFILE.dockerRunFlags,
Expand Down Expand Up @@ -248,13 +259,14 @@ function downloadModel(
[
"run",
"--rm",
"--entrypoint",
"hf",
"-v",
`${process.env.HOME}/.cache/huggingface:/root/.cache/huggingface`,
"-e",
"HF_HOME=/root/.cache/huggingface",
...buildHfTokenDockerArgs(),
profile.image,
"hf",
"download",
model.id,
],
Expand Down Expand Up @@ -338,7 +350,7 @@ function startContainer(
const flags = [resolvedFlags.join(" "), hfTokenFlags].filter(Boolean).join(" ");
const cmd =
`docker run -d ${flags} -p ${String(VLLM_PORT)}:8000 ` +
`--name ${profile.containerName} ${profile.image} bash -c ${JSON.stringify(buildVllmServeCommand(model))}`;
`--name ${profile.containerName} --entrypoint /bin/bash ${profile.image} -lc ${JSON.stringify(buildVllmServeCommand(model))}`;
const result = runShell(cmd, {
ignoreError: true,
suppressOutput: true,
Expand Down Expand Up @@ -461,7 +473,8 @@ export async function installVllm(
): Promise<{ ok: boolean }> {
// Resolve the model to serve: `NEMOCLAW_VLLM_MODEL` override if set, else
// the per-platform profile default. The generic-Linux profile defaults to
// Nemotron-Nano-4B for VRAM headroom; Spark/Station to Qwen3.6-27B.
// Nemotron-Nano-4B for VRAM headroom; Station to Qwen3.6-27B; Spark to the
// Qwen3.6-35B-A3B NVFP4 checkpoint.
// Validate gated-model access (HF_TOKEN required for models like
// DeepSeek-R1 Distill 70B) before touching docker so the user does not
// burn a multi-minute pull on a 401.
Expand Down
9 changes: 8 additions & 1 deletion test/detect-vllm-profile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ describe("detectVllmProfile", () => {
const profile = detectVllmProfile({ platform: "spark", type: "nvidia" });
expect(profile).not.toBeNull();
expect(profile!.name).toBe("DGX Spark");
expect(profile!.defaultModel.id).toBe("Qwen/Qwen3.6-27B-FP8");
expect(profile!.defaultModel.id).toBe("nvidia/Qwen3.6-35B-A3B-NVFP4");
expect(profile!.image).toBe(
"vllm/vllm-openai:nightly-1fc2cee50a09a094b9f2bbdfcb0ab0cadb536712",
);
});

it("returns the Spark profile when legacy gpu.spark is true", () => {
Expand All @@ -27,13 +30,17 @@ describe("detectVllmProfile", () => {
const profile = detectVllmProfile({ platform: "station", type: "nvidia" });
expect(profile).not.toBeNull();
expect(profile!.name).toBe("DGX Station");
// Station is unchanged by the Spark NVFP4 move: NGC image + Qwen3.6-27B.
expect(profile!.image).toBe("nvcr.io/nvidia/vllm:26.03.post1-py3");
expect(profile!.defaultModel.id).toBe("Qwen/Qwen3.6-27B-FP8");
});

it("returns the generic Linux profile for non-Spark/Station NVIDIA hosts", () => {
const profile = detectVllmProfile({ type: "nvidia" });
expect(profile).not.toBeNull();
expect(profile!.name).toBe("Linux + NVIDIA GPU");
expect(profile!.defaultModel.id).toContain("Nemotron-3-Nano-4B");
expect(profile!.image).toBe("nvcr.io/nvidia/vllm:26.03.post1-py3");
});

it("prefers Spark over generic when both flags qualify", () => {
Expand Down
Loading