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
18 changes: 18 additions & 0 deletions docs/inference/set-up-vllm.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,24 @@ When you start managed vLLM outside the installer express flow, NemoClaw uses th
For the managed single-node DGX Spark `nvidia/Qwen3.6-35B-A3B-NVFP4` profile, NemoClaw enables async scheduling and does not enable multi-token prediction (MTP) speculative decoding.
The profile retains `--gpu-memory-utilization 0.4`, a `262144`-token context window, `4` concurrent sequences, an `8192`-token batch limit, chunked prefill, prefix caching, and its registered parsers and acceleration backends.

NVIDIA Nemotron 3.5 Lightning is an additional explicit-only single-DGX Spark profile and does not change the Qwen default.
Select its stable catalog ID after confirming that `$$nemoclaw profiles list` reports it as compatible.

```bash
$$nemoclaw onboard --profile vllm.dgx-spark-gb10.single.nemotron-3.5-lightning-30b-a3b-nvfp4
```

The profile serves the public [`nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-NVFP4`](https://huggingface.co/nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-NVFP4) checkpoint through a pinned ARM64 vLLM image.
Its profile limits for one concurrent sequence are a 65,536-token context window, a 4,096-token batch limit, 0.7 GPU-memory utilization, FP8 KV cache, and one-token MTP speculative decoding.
It uses vLLM's Python frontend because the pinned runtime's Rust frontend does not register the required `step3p5` reasoning and tool-call parsers.
Physical single-DGX Spark validation covered plain chat, automatic and named structured tool calls, the 10-turn 32K-input/2K-output release workload, OpenClaw chat and file tools, cached restart, cleanup, and restoration of the original Qwen route.

<Warning title="Experimental Nemotron 3.5 Lightning vLLM Profile">
This profile is an explicit opt-in for one DGX Spark and remains Experimental.
Promotion requires broader validation of the pinned Python frontend and the `step3p5` reasoning and tool-call parsers.
The fixed recipe intentionally rejects model and serve-argument overrides; use another catalog profile instead of modifying its validated resource boundary.
</Warning>

Muse Glimmer is an additional single-DGX Spark choice and does not change the Qwen default.
Select `muse-glimmer-30b` to serve `Inferact/Muse-Glimmer-30B-NVFP4-W4A4` with the `muse-glimmer` alias.
The registered recipe configures a 32,768-token context window, 1 concurrent sequence, a 4,096-token batch limit, and 0.75 GPU-memory utilization.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

apiVersion: nemoclaw.nvidia.com/managed-inference/v1
kind: ServingPreset

metadata:
id: vllm.dgx-spark-gb10.single.nemotron-3.5-lightning-30b-a3b-nvfp4
displayName: NVIDIA Nemotron 3.5 Lightning 30B-A3B NVFP4 on one DGX Spark
supportState: experimental

spec:
selection: explicit-only
priority: 450

requirements:
all:
- readiness:
scope: everyNode
kind: qualification
id: host.platform.dgx_spark
status: qualified
- readiness:
scope: everyNode
kind: capability
id: host.platform.supported
state: present
- readiness:
scope: everyNode
kind: capability
id: host.platform.dgx_spark
state: present
- readiness:
scope: everyNode
kind: capability
id: host.docker.available
state: present
- readiness:
scope: everyNode
kind: capability
id: host.docker.daemon_reachable
state: present
- readiness:
scope: everyNode
kind: capability
id: host.docker.runtime_supported
state: present
- readiness:
scope: everyNode
kind: capability
id: host.docker.storage_compatible
state: present
- readiness:
scope: everyNode
kind: capability
id: host.gpu.nvidia_available
state: present
- readiness:
scope: everyNode
kind: capability
id: host.gpu.container_toolkit_available
state: present
- readiness:
scope: everyNode
kind: capability
id: host.gpu.cdi_healthy
state: present
- readiness:
scope: everyNode
kind: observation
id: host.os.platform
comparison:
operator: equals
value: linux
- readiness:
scope: everyNode
kind: observation
id: host.os.architecture
comparison:
operator: equals
value: arm64
- readiness:
scope: everyNode
kind: observation
id: host.docker.runtime
comparison:
operator: equals
value: docker
- readiness:
scope: everyNode
kind: observation
id: host.gpu.count
comparison:
operator: at-least
value: 1
- readiness:
scope: everyNode
kind: observation
id: host.gpu.driver_version
comparison:
operator: version-at-least
value: 580.65.06

plan:
backend: vllm
recipeRef: vllm.nemotron-3.5-lightning-30b-a3b-nvfp4.spark-single.v1
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

apiVersion: nemoclaw.nvidia.com/managed-inference/v1
kind: ServingRecipe

metadata:
id: vllm.nemotron-3.5-lightning-30b-a3b-nvfp4.spark-single.v1
displayName: NVIDIA Nemotron 3.5 Lightning 30B-A3B NVFP4 with vLLM

spec:
backend: vllm

model:
id: nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-NVFP4
revision: 0dcd680e5585c791728c83342b311d0a0026dbeb
servedName: nvidia-nemotron-3.5-lightning-30b-a3b-nvfp4
downloadSizeBytes: 21561882284
gated: false
installFastSafetensors: false
preparation:
ref: none/v1

runtime:
image: vllm/vllm-openai@sha256:3af90144a0926e5c5fe46ee16e5201e763dd854538b9d7ce433755f11dadaf78
imageDownloadSizeBytes: 12691539532
pullTimeoutSeconds: 3600
architecture: arm64
networkMode: bridge
ipcMode: host
sharedMemoryBytes: 34359738368
gpuRequest: all
devices: []
ulimits:
memlock: -1
stackBytes: 67108864
modelCache:
source: huggingface-cache
target: /root/.cache/huggingface
temporaryFilesystems: []
environment:
# The Lightning 3.5 Spark validation used vLLM's Python frontend;
# `step3p5` parsers are not available in the Rust frontend.
VLLM_USE_RUST_FRONTEND: "0"

execution:
materializerRef: vllm.host-local/v1
lifecycleRef: vllm.host-local.lifecycle/v1

serve:
authentication: bearer
executable: /usr/local/bin/vllm
arguments:
- name: --max-model-len
value: 65536
- name: --max-num-seqs
value: 1
- name: --gpu-memory-utilization
value: 0.7
- name: --kv-cache-dtype
value: fp8
- name: --moe-backend
value: marlin
- name: --max-num-batched-tokens
value: 4096
- name: --speculative-config
value: '{"method":"mtp","num_speculative_tokens":1,"moe_backend":"flashinfer_cutlass"}'
- name: --mamba-backend
value: flashinfer
- name: --mamba-ssm-cache-dtype
value: float16
- name: --enable-mamba-cache-stochastic-rounding
- name: --mamba-cache-philox-rounds
value: 5
- name: --enable-prefix-caching
- name: --mamba-cache-mode
value: align
- name: --compilation-config
value: '{"cudagraph_capture_sizes":[1,2,4,8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128,136,144,152,160,168,176,184,192,200,208,216,224,232,240,248,256,1024,2048,4096]}'
- name: --enable-auto-tool-choice
- name: --tool-call-parser
value: step3p5
- name: --reasoning-parser
value: step3p5

readiness:
timeoutSeconds: 1800
expectedModel: nvidia-nemotron-3.5-lightning-30b-a3b-nvfp4
47 changes: 47 additions & 0 deletions src/lib/inference/serving/adapter-registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ function shippedRecipe(): ManagedInferenceServingRecipe {
return structuredClone(recipe as ManagedInferenceServingRecipe);
}

function shippedHostLocalRecipe(): ManagedInferenceServingRecipe {
const recipe = loadManagedInferenceCatalog().recipes.find(
({ spec }) => spec.execution.materializerRef === HOST_LOCAL_VLLM_MATERIALIZER_REF,
);
expect(recipe).toBeDefined();
return structuredClone(recipe as ManagedInferenceServingRecipe);
}

describe("managed inference adapter registries", () => {
it("registers one versioned descriptor for each shipped managed cluster mechanic", () => {
expect(listManagedInferenceTopologyQualificationDescriptors()).toMatchObject([
Expand Down Expand Up @@ -143,6 +151,45 @@ describe("managed inference adapter registries", () => {
expect(getManagedInferenceRecipeRegistrationError(wrongShape)).toMatch(/TP times PP/u);
});

it("accepts shell-quoted JSON values for host-local vLLM arguments", () => {
const recipe = shippedHostLocalRecipe();
const structuredArgument = {
...recipe,
spec: {
...recipe.spec,
serve: {
...recipe.spec.serve,
arguments: [
...recipe.spec.serve.arguments,
{
name: "--speculative-config",
value: '{"method":"mtp","num_speculative_tokens":1}',
},
],
},
},
} as ManagedInferenceServingRecipe;
const unsafeArgument = {
...structuredArgument,
spec: {
...structuredArgument.spec,
serve: {
...structuredArgument.spec.serve,
arguments: structuredArgument.spec.serve.arguments.map((argument) =>
argument.name === "--speculative-config"
? { ...argument, value: '{"method":"mtp"};touch/tmp/unsafe' }
: argument,
),
},
},
} as ManagedInferenceServingRecipe;

expect(getManagedInferenceRecipeRegistrationError(structuredArgument)).toBeUndefined();
expect(getManagedInferenceRecipeRegistrationError(unsafeArgument)).toMatch(
/bounded safe text/u,
);
});

it("rejects schema-valid values that the registered adapter cannot execute", () => {
const recipe = shippedRecipe();
const unsupportedCache = {
Expand Down
7 changes: 5 additions & 2 deletions src/lib/inference/serving/adapter-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ const STABLE_ID = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,159}$/u;
const PINNED_IMAGE =
/^(?:[a-z0-9]+(?:[._-][a-z0-9]+)*(?::[0-9]+)?\/)?(?:[a-z0-9]+(?:[._-][a-z0-9]+)*\/)*[a-z0-9]+(?:[._-][a-z0-9]+)*@sha256:[0-9a-f]{64}$/u;
const SAFE_ENVIRONMENT_NAME = /^[A-Z][A-Z0-9_]{0,127}$/u;
// Structured vLLM flags are passed as individual argv values after shell
// quoting. Keep the allowlist narrow while admitting JSON objects and arrays.
const HOST_LOCAL_SAFE_VALUE = /^[A-Za-z0-9_@%+=:,./{}[\]"-]+$/u;
const SHA256_DIGEST = /^sha256:[0-9a-f]{64}$/u;
const MANAGED_CLUSTER_MATERIALIZER_OWNED_ENVIRONMENT = new Set([
"GLOO_SOCKET_IFNAME",
Expand Down Expand Up @@ -410,13 +413,13 @@ function validateHostLocalVllmMaterializerRecipe(
typeof value === "string" &&
(Buffer.byteLength(value, "utf8") > 16_384 ||
value.includes("\0") ||
!/^[A-Za-z0-9_@%+=:,./-]+$/u.test(value)),
!HOST_LOCAL_SAFE_VALUE.test(value)),
) ||
Object.values(recipe.spec.runtime.environment).some(
(value) =>
Buffer.byteLength(value, "utf8") > 4_096 ||
value.includes("\0") ||
!/^[A-Za-z0-9_@%+=:,./-]+$/u.test(value),
!HOST_LOCAL_SAFE_VALUE.test(value),
)
) {
return "host-local vLLM serving values must be bounded safe text";
Expand Down
4 changes: 4 additions & 0 deletions src/lib/inference/serving/catalog-loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,25 @@ const EMPTY_CATALOG: CompiledServingCatalog = {
const EXPECTED_MANAGED_RECIPE_IDS = [
"llama-cpp.nemotron-3-nano-30b-a3b.spark-single.v1",
"vllm.deepseek-v4-flash-0731.spark-dual.v1",
"vllm.nemotron-3.5-lightning-30b-a3b-nvfp4.spark-single.v1",
"vllm.qwen3-6-35b-a3b-nvfp4.spark-single.v1",
];
const EXPECTED_MANAGED_PRESET_IDS = [
"llama-cpp.dgx-spark-gb10.single.nemotron-3-nano-30b-a3b",
"llama-cpp.linux-amd64-nvidia.single.nemotron-3-nano-30b-a3b",
"local-model-profile.vllm.spark.v1",
"vllm.dgx-spark-gb10.dual.deepseek-v4-flash-0731",
"vllm.dgx-spark-gb10.single.nemotron-3.5-lightning-30b-a3b-nvfp4",
];
const EXPECTED_MANAGED_SOURCE_IDS = [
"llama-cpp.dgx-spark-gb10.single.nemotron-3-nano-30b-a3b",
"llama-cpp.linux-amd64-nvidia.single.nemotron-3-nano-30b-a3b",
"local-model-profile.vllm.spark.v1",
"vllm.dgx-spark-gb10.dual.deepseek-v4-flash-0731",
"vllm.dgx-spark-gb10.single.nemotron-3.5-lightning-30b-a3b-nvfp4",
"llama-cpp.nemotron-3-nano-30b-a3b.spark-single.v1",
"vllm.deepseek-v4-flash-0731.spark-dual.v1",
"vllm.nemotron-3.5-lightning-30b-a3b-nvfp4.spark-single.v1",
"vllm.qwen3-6-35b-a3b-nvfp4.spark-single.v1",
];

Expand Down
Loading
Loading