From 3bdb376861a2831c4b79773c9f8d7157ea9b8a41 Mon Sep 17 00:00:00 2001 From: Prekshi Vyas Date: Tue, 11 Aug 2026 07:08:06 -0700 Subject: [PATCH 1/2] feat(inference): add Nemotron 3.5 Lightning Spark profile Signed-off-by: Prekshi Vyas --- docs/inference/set-up-vllm.mdx | 17 +++ ....nemotron-3.5-lightning-30b-a3b-nvfp4.yaml | 106 ++++++++++++++++++ ...ghtning-30b-a3b-nvfp4.spark-single.v1.yaml | 88 +++++++++++++++ .../serving/adapter-registry.test.ts | 47 ++++++++ src/lib/inference/serving/adapter-registry.ts | 7 +- .../inference/serving/catalog-loader.test.ts | 4 + ...managed-inference-catalog-compiler.test.ts | 44 ++++++++ 7 files changed, 311 insertions(+), 2 deletions(-) create mode 100644 managed-inference/presets/vllm.dgx-spark-gb10.single.nemotron-3.5-lightning-30b-a3b-nvfp4.yaml create mode 100644 managed-inference/recipes/vllm.nemotron-3.5-lightning-30b-a3b-nvfp4.spark-single.v1.yaml diff --git a/docs/inference/set-up-vllm.mdx b/docs/inference/set-up-vllm.mdx index ac1f484ca76..fb11aa56ac1 100644 --- a/docs/inference/set-up-vllm.mdx +++ b/docs/inference/set-up-vllm.mdx @@ -267,6 +267,23 @@ 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 qualified single-user boundary is a 65,536-token context window, 1 concurrent sequence, 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. + + +This profile is an explicit opt-in for one DGX Spark and remains Experimental while its pinned frontend and parser combination is qualified for broader support. +The fixed recipe intentionally rejects model and serve-argument overrides; use another catalog profile instead of modifying its validated resource boundary. + + 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. diff --git a/managed-inference/presets/vllm.dgx-spark-gb10.single.nemotron-3.5-lightning-30b-a3b-nvfp4.yaml b/managed-inference/presets/vllm.dgx-spark-gb10.single.nemotron-3.5-lightning-30b-a3b-nvfp4.yaml new file mode 100644 index 00000000000..778238f29d0 --- /dev/null +++ b/managed-inference/presets/vllm.dgx-spark-gb10.single.nemotron-3.5-lightning-30b-a3b-nvfp4.yaml @@ -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 diff --git a/managed-inference/recipes/vllm.nemotron-3.5-lightning-30b-a3b-nvfp4.spark-single.v1.yaml b/managed-inference/recipes/vllm.nemotron-3.5-lightning-30b-a3b-nvfp4.spark-single.v1.yaml new file mode 100644 index 00000000000..bb4d06e630f --- /dev/null +++ b/managed-inference/recipes/vllm.nemotron-3.5-lightning-30b-a3b-nvfp4.spark-single.v1.yaml @@ -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 diff --git a/src/lib/inference/serving/adapter-registry.test.ts b/src/lib/inference/serving/adapter-registry.test.ts index c520136d049..0580bf9d477 100644 --- a/src/lib/inference/serving/adapter-registry.test.ts +++ b/src/lib/inference/serving/adapter-registry.test.ts @@ -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([ @@ -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 = { diff --git a/src/lib/inference/serving/adapter-registry.ts b/src/lib/inference/serving/adapter-registry.ts index a10be5c0159..00ae863ca73 100644 --- a/src/lib/inference/serving/adapter-registry.ts +++ b/src/lib/inference/serving/adapter-registry.ts @@ -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", @@ -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"; diff --git a/src/lib/inference/serving/catalog-loader.test.ts b/src/lib/inference/serving/catalog-loader.test.ts index 88e75d13e2f..4b34cbe7d49 100644 --- a/src/lib/inference/serving/catalog-loader.test.ts +++ b/src/lib/inference/serving/catalog-loader.test.ts @@ -32,6 +32,7 @@ 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 = [ @@ -39,14 +40,17 @@ const EXPECTED_MANAGED_PRESET_IDS = [ "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", ]; diff --git a/test/managed-inference-catalog-compiler.test.ts b/test/managed-inference-catalog-compiler.test.ts index 49c8fbd2a35..0585231a0f1 100644 --- a/test/managed-inference-catalog-compiler.test.ts +++ b/test/managed-inference-catalog-compiler.test.ts @@ -21,6 +21,8 @@ const LLAMA_CPP_IMAGE = "ghcr.io/ggml-org/llama.cpp@sha256:866ad568474de9e835e487ae841ad6ace1a494b5eab4f292cbd45adb6180f711"; const LLAMA_CPP_MODEL_DIGEST = "sha256:627f5b04aedc97f967332f331bd75b7a4ed2f33ca83e6ee74b44235cc1887890"; +const LIGHTNING_PROFILE_ID = "vllm.dgx-spark-gb10.single.nemotron-3.5-lightning-30b-a3b-nvfp4"; +const LIGHTNING_RECIPE_ID = "vllm.nemotron-3.5-lightning-30b-a3b-nvfp4.spark-single.v1"; function catalogSources(): ServingCatalogSource[] { return (["presets", "recipes"] as const).flatMap((kind) => { @@ -140,6 +142,46 @@ describe("managed inference YAML profile contract", () => { }); }); + it("compiles the explicit DGX Spark Lightning 3.5 vLLM profile from YAML (#8385)", () => { + const catalog = compile(catalogSources()); + const preset = catalog.presets.find(({ metadata }) => metadata.id === LIGHTNING_PROFILE_ID); + const recipe = catalog.recipes.find(({ metadata }) => metadata.id === LIGHTNING_RECIPE_ID); + + expect(preset?.metadata.supportState).toBe("experimental"); + expect(preset?.spec).toMatchObject({ + selection: "explicit-only", + plan: { backend: "vllm", recipeRef: LIGHTNING_RECIPE_ID }, + }); + expect(recipe?.spec).toMatchObject({ + backend: "vllm", + model: { + id: "nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-NVFP4", + revision: "0dcd680e5585c791728c83342b311d0a0026dbeb", + servedName: "nvidia-nemotron-3.5-lightning-30b-a3b-nvfp4", + }, + runtime: { + architecture: "arm64", + image: + "vllm/vllm-openai@sha256:3af90144a0926e5c5fe46ee16e5201e763dd854538b9d7ce433755f11dadaf78", + environment: { VLLM_USE_RUST_FRONTEND: "0" }, + }, + execution: { + materializerRef: "vllm.host-local/v1", + lifecycleRef: "vllm.host-local.lifecycle/v1", + }, + serve: { + arguments: expect.arrayContaining([ + { name: "--tool-call-parser", value: "step3p5" }, + { name: "--reasoning-parser", value: "step3p5" }, + { + name: "--speculative-config", + value: '{"method":"mtp","num_speculative_tokens":1,"moe_backend":"flashinfer_cutlass"}', + }, + ]), + }, + }); + }); + it("keeps vLLM automatic while llama.cpp remains explicit-only (#8173)", () => { const catalog = compile(catalogSources()); const vllmPreset = catalog.presets.find(({ metadata }) => metadata.id === PROFILE_ID); @@ -177,5 +219,7 @@ describe("managed inference YAML profile contract", () => { expect(productionSources).not.toContain(RECIPE_ID); expect(productionSources).not.toContain(LLAMA_CPP_PROFILE_ID); expect(productionSources).not.toContain(LLAMA_CPP_RECIPE_ID); + expect(productionSources).not.toContain(LIGHTNING_PROFILE_ID); + expect(productionSources).not.toContain(LIGHTNING_RECIPE_ID); }); }); From 9cea10b2b388230aba47f506ba7c7f0690b6a4e9 Mon Sep 17 00:00:00 2001 From: Prekshi Vyas Date: Tue, 11 Aug 2026 08:06:57 -0700 Subject: [PATCH 2/2] docs: clarify experimental Lightning support boundary --- docs/inference/set-up-vllm.mdx | 5 +++-- ...managed-inference-catalog-compiler.test.ts | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/docs/inference/set-up-vllm.mdx b/docs/inference/set-up-vllm.mdx index fb11aa56ac1..fcd0ded5c94 100644 --- a/docs/inference/set-up-vllm.mdx +++ b/docs/inference/set-up-vllm.mdx @@ -275,12 +275,13 @@ $$nemoclaw onboard --profile vllm.dgx-spark-gb10.single.nemotron-3.5-lightning-3 ``` 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 qualified single-user boundary is a 65,536-token context window, 1 concurrent sequence, a 4,096-token batch limit, 0.7 GPU-memory utilization, FP8 KV cache, and one-token MTP speculative decoding. +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. -This profile is an explicit opt-in for one DGX Spark and remains Experimental while its pinned frontend and parser combination is qualified for broader support. +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. diff --git a/test/managed-inference-catalog-compiler.test.ts b/test/managed-inference-catalog-compiler.test.ts index 0585231a0f1..11ea4ca683a 100644 --- a/test/managed-inference-catalog-compiler.test.ts +++ b/test/managed-inference-catalog-compiler.test.ts @@ -182,6 +182,25 @@ describe("managed inference YAML profile contract", () => { }); }); + it("documents the Experimental Lightning support boundary (#8385)", () => { + const setupGuide = readFileSync( + path.join(REPOSITORY_ROOT, "docs", "inference", "set-up-vllm.mdx"), + "utf8", + ); + const warning = setupGuide.match( + /([\s\S]*?)<\/Warning>/u, + )?.[1]; + + expect(warning).toBeDefined(); + expect(warning).toContain( + "This profile is an explicit opt-in for one DGX Spark and remains Experimental.", + ); + expect(warning).toContain( + "Promotion requires broader validation of the pinned Python frontend and the `step3p5` reasoning and tool-call parsers.", + ); + expect(warning).not.toContain("qualified for broader support"); + }); + it("keeps vLLM automatic while llama.cpp remains explicit-only (#8173)", () => { const catalog = compile(catalogSources()); const vllmPreset = catalog.presets.find(({ metadata }) => metadata.id === PROFILE_ID);