Repository navigation
feat(recipes): add Solar Open2 250B NVFP4 aggregated and disaggregated recipes for B200 #14376
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
ynpandey-nv
merged 11 commits into
ai-dynamo:main
from
snarravula-dl:solar-open2-250b-agg-recipe
Sep 16, 2026
Merged
Changes from 6 commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
77c85ef
feat(recipes): add Solar Open2 250B NVFP4 aggregated and disaggregate…
snarravula-dl 099afba
docs(recipes): address the Solar Open2 250B documentation audit
snarravula-dl d68066d
refactor(recipes): move cluster-specific settings out of the Solar Op…
snarravula-dl e2005a3
docs(recipes): correct Solar Open2 250B trace staging and RDMA instru…
snarravula-dl e66c04e
fix(recipes): request rdma/ib and set the UCX transport in the disagg…
snarravula-dl f280bbd
docs(recipes): select the trace staging pod by worker component, not …
snarravula-dl 96603cf
docs(recipes): correct the Solar Open2 250B container note in the rec…
snarravula-dl 3f2eac8
chore(recipes): allow one benchmark retry and ignore the unpublished …
snarravula-dl b600df7
Merge remote-tracking branch 'origin/main' into solar-open2-250b-agg-…
snarravula-dl 8b56d9e
fix(recipes): download the Solar Open2 250B snapshot the deployments …
snarravula-dl 7ec6d26
feat(recipes): point Solar Open2 250B at the published runtime image
snarravula-dl File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
269 changes: 269 additions & 0 deletions
269
docs/fern/pages/recipes/model-recipes/solar-open2-250b.mdx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,269 @@ | ||
| --- | ||
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| title: "Solar Open2 250B" | ||
| subtitle: "Serve Solar-Open2-250B-NVFP4 with Dynamo on B200, aggregated or disaggregated." | ||
| --- | ||
|
|
||
| ## Profiles | ||
|
|
||
| | Profile | Serving mode | GPUs | Context | | ||
| |---|---|---:|---| | ||
| | `agg-b200-chat` | Aggregated, two replicas, KV-aware routing | 4 | 1M | | ||
| | `disagg-b200-chat` | Disaggregated 1P:2D, round-robin routing | 8 | 1M | | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| - Kubernetes cluster with B200 nodes and the Dynamo operator installed | ||
| - A `ReadWriteMany` storage class. `model-cache.yaml` requests 1200Gi, which holds | ||
| the checkpoint alongside benchmark traces and artifacts; the checkpoint itself is | ||
| about 153 GB. | ||
| - Registry credentials for the runtime image, as a pull secret of your choosing. | ||
| The recipe does not name one; add it to the deployment as described under | ||
| Runtime image below. | ||
| - A Hugging Face token, as a secret named `hf-token-secret`, used by the download | ||
| Job to fetch the checkpoint: | ||
|
|
||
| ```bash | ||
| kubectl create secret generic hf-token-secret --from-literal=HF_TOKEN=<token> | ||
| ``` | ||
| - For the disaggregated profile, an RDMA/InfiniBand device plugin exposing `rdma/ib` | ||
| on the GPU nodes. If your plugin advertises a different resource name, substitute it | ||
| in the worker resource requests. | ||
|
|
||
| ## Prepare the model cache | ||
|
|
||
| Edit `storageClassName` in `model-cache.yaml` first. | ||
|
|
||
| ```bash | ||
| kubectl apply -f recipes/solar-open2-250b/model-cache/model-cache.yaml | ||
| kubectl apply -f recipes/solar-open2-250b/model-cache/model-download.yaml | ||
| kubectl wait --for=condition=complete job/model-download --timeout=2h | ||
| ``` | ||
|
|
||
| The checkpoint is approximately 153 GB. Both profiles share this cache. | ||
|
|
||
| ## Deploy | ||
|
|
||
| Aggregated: | ||
|
|
||
| ```bash | ||
| kubectl apply -f recipes/solar-open2-250b/vllm/agg-b200-chat/deploy.yaml | ||
| kubectl wait --for=condition=Ready \ | ||
| dynamographdeployment/solar-open2-250b-vllm-b200-agg-chat --timeout=30m | ||
| ``` | ||
|
|
||
| Disaggregated: | ||
|
|
||
| ```bash | ||
| kubectl apply -f recipes/solar-open2-250b/vllm/disagg-b200-chat/deploy.yaml | ||
| kubectl wait --for=condition=Ready \ | ||
| dynamographdeployment/solar-open2-250b-vllm-b200-disagg-chat --timeout=30m | ||
| ``` | ||
|
|
||
| ## Smoke test | ||
|
|
||
| Port-forward the frontend of the profile you deployed. For the aggregated profile: | ||
|
|
||
| ```bash | ||
| kubectl port-forward svc/solar-open2-250b-vllm-b200-agg-chat-frontend 8000:8000 & | ||
| ``` | ||
|
|
||
| For the disaggregated profile: | ||
|
|
||
| ```bash | ||
| kubectl port-forward svc/solar-open2-250b-vllm-b200-disagg-chat-frontend 8000:8000 & | ||
| ``` | ||
|
|
||
| Then, against either: | ||
|
|
||
| ```bash | ||
| curl -s http://localhost:8000/v1/chat/completions \ | ||
| -H 'Content-Type: application/json' \ | ||
| -d '{"model":"solar-open2-250b", | ||
| "messages":[{"role":"user","content":"What is 12 times 7?"}], | ||
| "max_tokens":128}' | ||
| ``` | ||
|
|
||
| Solar Open2 is a reasoning model. Short generations may return the answer in | ||
| `reasoning_content` with `content` empty; allow a larger `max_tokens` to see a | ||
| final answer in `content`. | ||
|
|
||
| ## Benchmark | ||
|
|
||
| Both performance Jobs read a mooncake trace from the `shared-model-cache` PVC. The | ||
| trace is not part of the model download, so stage it first. The published figures use | ||
| `nim_turbo_8k_1k_70kv_chat_new_noschedule_short_15perc.jsonl` (1805 requests, mean | ||
| ISL 37.4k, mean OSL 1008), which ships in this repository under | ||
| `recipes/nemotron-3-ultra/perf/traces/`. | ||
|
|
||
| Copy it in through any running pod that mounts the PVC. Note that the worker pods | ||
| mount that volume at `/shared-model-cache`, while the benchmark Job mounts the same | ||
| volume at `/model-cache`, so the file staged below is what `TRACE_FILE` refers to: | ||
|
|
||
| ```bash | ||
| TRACE=recipes/nemotron-3-ultra/perf/traces/nim_turbo_8k_1k_70kv_chat_new_noschedule_short_15perc.jsonl | ||
|
|
||
| # The traces are stored in Git LFS. A plain clone leaves a small pointer file in | ||
| # place of the data, so fetch the real contents first and check the size: the file | ||
| # is roughly 900 KB, not a few hundred bytes. | ||
| git lfs install | ||
| git lfs pull --include="$TRACE" | ||
| ls -l "$TRACE" | ||
|
|
||
| # Whichever profile you deployed: | ||
| DGD=solar-open2-250b-vllm-b200-agg-chat # or solar-open2-250b-vllm-b200-disagg-chat | ||
|
|
||
| # The workers mount the cache PVC; the frontend does not. Match the worker | ||
| # components by name rather than excluding the frontend, so that pods carrying no | ||
| # component-type label are not selected either. | ||
| POD=$(kubectl get pods \ | ||
| -l "nvidia.com/dynamo-graph-deployment-name=$DGD,nvidia.com/dynamo-component-type in (worker,prefill,decode)" \ | ||
| --field-selector status.phase=Running \ | ||
| -o jsonpath='{.items[0].metadata.name}') | ||
|
|
||
| kubectl exec "$POD" -- mkdir -p /shared-model-cache/traces | ||
| kubectl cp "$TRACE" "$POD:/shared-model-cache/traces/" | ||
| ``` | ||
|
|
||
| Set `TRACE_FILE` in the Job to match whichever trace you stage. | ||
|
|
||
| Then run the Job for the profile under test: | ||
|
|
||
| ```bash | ||
| kubectl apply -f recipes/solar-open2-250b/perf/perf.yaml # aggregated | ||
| kubectl apply -f recipes/solar-open2-250b/perf/perf-disagg.yaml # disaggregated | ||
| ``` | ||
|
|
||
| Each Job runs the trace once and writes its summary to `ARTIFACT_DIR`. To run it | ||
| again, delete the Job first and re-apply; Jobs are immutable, so re-applying over an | ||
| existing one fails. | ||
|
|
||
| ## Runtime image | ||
|
|
||
| Both manifests pin a purpose-built runtime image by digest. It extends the | ||
| Dynamo 1.4.1 vLLM runtime with the Solar Open2 architecture and parser support | ||
| and a Dynamo frontend compatibility patch. The changes are Python-only. | ||
|
|
||
| The stock Dynamo 1.4.1 vLLM runtime does not serve this model — it has no Solar | ||
| Open2 model definition — so the pinned image is required rather than optional. | ||
| Pin by digest, not by tag. | ||
|
|
||
| Until the supporting container changes land upstream this image is served from a | ||
| staging registry and needs registry credentials to pull. The recipe deliberately | ||
| carries no pull secret or other cluster-specific settings, so supply an | ||
| `imagePullSecrets` entry for your environment. | ||
|
|
||
| ## Configuration notes | ||
|
|
||
| ### Both profiles | ||
|
|
||
| - **NVFP4 weights, FP8 KV cache.** Both manifests set `--kv-cache-dtype fp8`; | ||
| the disaggregated profile additionally sets `--calculate-kv-scales`. FP8 halves | ||
| KV bytes relative to BF16, which roughly doubles the KV pool and raises the | ||
| realised prefix-cache hit rate. | ||
| - **MoE backend.** `--moe-backend flashinfer_trtllm` with | ||
| `--enable-flashinfer-autotune`. | ||
| - **Attention.** FlashInfer is selected automatically. Note that FlashAttention | ||
| is not compatible with an FP8 KV cache on this model and will fail at startup | ||
| if forced. | ||
| - **Hybrid KV pages.** Solar Open2 has 12 grouped-query attention layers and 36 | ||
| KDA linear-attention layers. The effective KV page is a hybrid page of roughly | ||
| 1072 tokens at `--block-size 16`, not the block size itself. Tools that assume | ||
| block size equals page size need adjusting. | ||
| - Speculative decoding is not supported on this checkpoint and is not enabled. | ||
|
|
||
| ### Aggregated profile | ||
|
|
||
| - Two worker replicas at tensor parallel size 2, fronted by a KV-aware router | ||
| (`--router-mode kv`, `--router-kv-events`) with a ZMQ KV-event publisher on | ||
| each worker. KV-aware routing outperforms round-robin on prefix-reuse | ||
| workloads. | ||
| - **Expert parallelism is left off.** It measured as neutral for aggregated | ||
| serving on this checkpoint, so the simpler configuration is used. | ||
|
|
||
| ### Disaggregated profile | ||
|
|
||
| - Separate prefill and decode workers connected by `NixlConnector`, with | ||
| `kv_role: kv_producer` on prefill and `kv_role: kv_consumer` on decode. | ||
| - **The topology is one prefill worker and two decode workers** — TP4 with | ||
| expert parallelism for prefill (4 GPUs), TP2 for each decode worker (4 GPUs), | ||
| 8 GPUs in total. | ||
| - **`VLLM_SSM_CONV_STATE_LAYOUT=DS` is required.** Solar Open2's KDA layers | ||
| register as SSM layers, and their convolution state must cross the KV-transfer | ||
| boundary. Without this variable the engine asserts during initialization and | ||
| the workers will not start. | ||
| - **Each worker requests `rdma/ib` equal to its GPU count**, four on prefill and two | ||
| on each decode worker. This is required, not optional: without it the device plugin | ||
| does not inject `/dev/infiniband`, UCX falls back to TCP, and nothing logs an error. | ||
| Verify inside a running worker with `kubectl exec <pod> -c main -- ls /dev/infiniband` | ||
| and expect `uverbsN`, `umadN` and `rdma_cm`. One HCA per GPU is correct; requesting | ||
| more measured worse. If your RDMA device plugin advertises a different resource name | ||
| than `rdma/ib`, substitute it in both the requests and the limits. | ||
| - Transport is configured with `NCCL_IB_DISABLE=0`, `UCX_TLS=^cuda_ipc`, | ||
| `UCX_RNDV_SCHEME=get_zcopy` and `UCX_MAX_RNDV_RAILS=4`. `UCX_MAX_RNDV_RAILS` tracks | ||
| the number of HCAs per worker; adjust it if your nodes differ. `UCX_NET_DEVICES` is | ||
| deliberately left unset; pinning it produced `NIXL_ERR_BACKEND`. | ||
| - **Per-user output throughput runs close to the floor.** The disaggregated profile | ||
| clears the 50 tok/s target with little margin, and individual runs on contended | ||
| hardware may land at or below it. | ||
| - **The frontend uses round-robin routing, not KV-aware routing.** This is | ||
| deliberate. KV-aware routing improves the aggregated profile, but with a single | ||
| prefill worker there is nothing for a prefix-aware router to choose between. | ||
| KV-aware and load-aware routing were both measured on this topology and neither | ||
| improved on round-robin. | ||
|
|
||
| ## Known limitations | ||
|
|
||
| - The disaggregated profile delivers slightly lower per-GPU throughput than the | ||
| 4-GPU aggregated profile, at roughly an order of magnitude higher | ||
| time-to-first-token. Prefer the aggregated profile unless independent | ||
| prefill/decode scaling is the priority. | ||
| - The 1M context length requires sufficient KV capacity; reduce | ||
| `--max-model-len` if deploying on a smaller GPU allocation. | ||
| - Speculative decoding is not available on this checkpoint. | ||
|
|
||
| ### API compatibility | ||
|
|
||
| Some OpenAI-compatible surfaces do not behave as advertised. These originate in the | ||
| Dynamo frontend, are not specific to this model, and no recipe setting affects them. | ||
|
|
||
| - **Logprobs.** Requesting `logprobs` on `/v1/chat/completions` returns HTTP 500. | ||
| - **Responses API.** `store=true` succeeds but the response is not retrievable | ||
| afterwards (`GET /v1/responses/{id}` returns 404). Unsupported parameters such as | ||
| `frequency_penalty` are accepted and silently ignored. | ||
| - **Completions.** `stop_token_ids` with more than three IDs returns HTTP 500 rather | ||
| than a 400. | ||
|
|
||
| ### Reasoning budget and structured output | ||
|
|
||
| The recipe sets `reasoning_effort: high`. Generations are long: on GPQA Diamond the | ||
| median output is roughly 6,400 tokens, the 99th percentile 131,000, and the longest | ||
| observed 209,000. A small `max_tokens` therefore truncates the model mid-reasoning | ||
| and returns `finish_reason=length` with no final content, which shows up most often | ||
| on structured-output requests where the JSON never arrives in `message.content`. | ||
|
|
||
| Omit `max_tokens` and let the 1M context window bound the request, or set it well | ||
| above the reasoning length the workload needs. Values in the low thousands are not | ||
| sufficient for this model. | ||
|
|
||
| ## Measured performance | ||
|
|
||
| Chat workload, 15% subset of the internal `nim_turbo` 8k/1k 70kv trace | ||
| (1805 requests, mean ISL 37.4k tokens, mean OSL 1008 tokens). SLA: p50 TTFT | ||
| below 5 s and p50 user output throughput at or above 50 tok/s. | ||
|
|
||
| | Workload | Recipe | Framework | SKU | Concurrency | System output tok/s/GPU | User output tok/s (P50) | TTFT P50 (ms) | | ||
| |---|---|---|---|---:|---:|---:|---:| | ||
| | Chat (15% subset) | Aggregated, 2 replicas, KV-aware routing | vLLM | B200 | 20 | 216.88 | 52.02 | 308 | | ||
| | Chat (15% subset) | Disaggregated 1P:2D, round-robin | vLLM | B200 | 44 | 206.20 | 51.69 | 2277 | | ||
|
|
||
| Figures are measured with the profile's benchmark manifest (`perf.yaml` for | ||
| aggregated, `perf-disagg.yaml` for disaggregated). That configuration isolates the | ||
| warmup phase from profiling; it does not clear server-side KV cache. For runs that | ||
| must start from a cold cache, restart the worker pods between them. | ||
|
|
||
| ## Source | ||
|
|
||
| - Manifests: `recipes/solar-open2-250b/` | ||
| - Model: `nota-ai/Solar-Open2-250B-Nota-NVFP4` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| <!-- | ||
| SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| SPDX-License-Identifier: Apache-2.0 | ||
| --> | ||
|
|
||
| # Solar Open2 250B NVFP4 Recipes | ||
|
|
||
| Documentation for these recipes is published at | ||
| <https://docs.nvidia.com/dynamo/latest/recipes/model-recipes/solar-open2-250b>. | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| apiVersion: v1 | ||
| kind: PersistentVolumeClaim | ||
| metadata: | ||
| name: shared-model-cache | ||
| spec: | ||
| accessModes: | ||
| - ReadWriteMany | ||
| resources: | ||
| requests: | ||
| storage: 1200Gi | ||
| # Edit this | ||
| storageClassName: "your-storage-class-name" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| apiVersion: batch/v1 | ||
| kind: Job | ||
| metadata: | ||
| name: model-download | ||
| spec: | ||
| backoffLimit: 3 | ||
| completions: 1 | ||
| parallelism: 1 | ||
| template: | ||
| metadata: | ||
| labels: | ||
| app: model-download | ||
| spec: | ||
| restartPolicy: Never | ||
| containers: | ||
| - name: model-download | ||
| image: python:3.10-slim | ||
| securityContext: | ||
| allowPrivilegeEscalation: false | ||
| capabilities: | ||
| drop: ["ALL"] | ||
| seccompProfile: | ||
| type: RuntimeDefault | ||
| command: ["sh", "-c"] | ||
| envFrom: | ||
| - secretRef: | ||
| name: hf-token-secret | ||
| env: | ||
| - name: MODEL_NAME | ||
| value: nota-ai/Solar-Open2-250B-Nota-NVFP4 | ||
| - name: HF_HOME | ||
| value: /shared-model-cache | ||
| - name: HF_XET_HIGH_PERFORMANCE | ||
| value: "1" | ||
| args: | ||
| - | | ||
| set -eux | ||
| pip install --no-cache-dir huggingface_hub==1.16.4 | ||
| hf download $MODEL_NAME | ||
| resources: | ||
| requests: | ||
| cpu: "8" | ||
| memory: "128Gi" | ||
| volumeMounts: | ||
| - name: shared-model-cache | ||
| mountPath: /shared-model-cache | ||
| volumes: | ||
| - name: shared-model-cache | ||
| persistentVolumeClaim: | ||
| claimName: shared-model-cache |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.