docs(cookbook): add the Kimi-K3 serving cookbook (#32542)

Co-authored-by: kpham-sgl <khoa.pham@radixark.ai>
Co-authored-by: Zijie Xia <zijie.xia@radixark.ai>
Co-authored-by: ispobock <ispobaoke@gmail.com>
Co-authored-by: Mick <mickjagger19@icloud.com>
Co-authored-by: Baizhou Zhang <sobereddiezhang@gmail.com>
Co-authored-by: thomawan <thomawan@amd.com>
Co-authored-by: BBuf <1182563586@qq.com>
This commit is contained in:
Liangsheng Yin
2026-07-27 08:37:30 -07:00
committed by GitHub
co-authored by kpham-sgl Zijie Xia ispobock Mick Baizhou Zhang thomawan BBuf
parent 8d6549bc40
commit 7dafacca49
14 changed files with 3649 additions and 274 deletions
@@ -4,7 +4,7 @@ Loaded on demand by the `cookbook-add-model` skill. This is the field-by-field
contract for when the clone needs more than a rename. The two engine files are
the canonical specs — read their headers first:
- [`_deployment.jsx`](../../../../docs_new/src/snippets/_deployment.jsx) — the 5-dim matrix widget; lists every config field.
- [`_deployment.jsx`](../../../../docs_new/src/snippets/_deployment.jsx) — the matrix widget; its header lists every config field. Dimensions are the legacy fixed five by default, or config-declared via `matchDims` / `overlayDims` (§2.1b).
- [`_playground.jsx`](../../../../docs_new/src/snippets/_playground.jsx) — the diff-based override widget; lists the `playgroundFeatures` axes + the `AXIS_HANDLERS` interface.
Engine extension (adding a new playground axis) lives in [engine-axis.md](engine-axis.md).
@@ -34,7 +34,7 @@ the full contract):
| `quantizations` | `{id, label}[]` | 3rd-dim option list. |
| `strategies` | `{id, label}[]` | 4th-dim option list. Canonical ids: `low-latency` / `balanced` / `high-throughput` (never model-specific ids like `mtp`). **The count follows the page's operating points**: one recipe → a single `balanced`; two → `low-latency` + `high-throughput`; three → the full trio (the ideal). Tiers apply per (hw × variant × quant) combination — a single-recipe combination parks under its semantically honest tier (clear slant → that tier, e.g. DSv4's RTX 6000 → `low-latency`; no slant → `balanced`, e.g. Qwen3.5's Xeon); the page's list is the union and the engine greys unused chips per selection. Never invent a recipe just to fill chips. When two recipes differ by MTP / speculative decoding, the assignment is deterministic: spec ON → `low-latency`, spec OFF → `high-throughput` (at saturation the draft+verify overhead outweighs the speedup — same reason DSv4's high-throughput recipes disable MTP). The recurring markers in the other direction: dp-attention ON (MLA-attention models) and EP / DP+EP ON (MoE models) → `high-throughput`. |
| `nodesOptions` | `{id, label}[]` | 5th-dim option list. The `id` MUST be `single` or `multi-N` — the engine parses N from the id for `--nnodes`. |
| `cells` | `{match, verified?, env, flags}[]` | One per supported (hw × variant × quant × strategy × nodes) combination. See §2.2. |
| `cells` | `{match, verified?, nnodes?, env, flags}[]` | One per supported (hw × match-dim) combination. See §2.2. `nnodes` supplies the node count when the config declares no `nodes` dim (default 1). |
| `modelNames` | `{[key]: string}` | HF slug lookup. Keys are either `hw\|variant\|quant` (most specific) or `variant\|quant` (fallback). |
| `placeholders` | `{[key]: {target, label, default?}}` | `{{KEY}}` interpolation map for command + curl. `target` is `'command'` or `'curl'`. Editable through the Env modal. |
| `curl` | string | cURL template. Uses `{{MODEL_NAME}}` + placeholder keys. |
@@ -52,7 +52,42 @@ the full contract):
| `latencyPercentile` | `"Mean" \| "P50"` | Optional, **temporary**; the percentile the benchmark TTFT/TPOT values are. **Default `"P50"`** — the card renders `TTFT (<pct>)` / `TPOT (<pct>)`. Set `"Mean"` only for legacy data recorded as Mean (being re-measured to P50). A benchmarks entry may carry its own `latencyPercentile` to override the page value per cell (entry → config → `"P50"`). `tokens_per_sec_per_gpu` is stored as **total (in+out)/GPU** = `output tok/s/GPU × (isl+osl)/osl`, shown by the card as-is. |
| `github` | `{owner?, repo?, issueTemplate?, cookbookModel?}` | Overrides for the "Submit verified cell" CTA in the playground. Defaults: `sgl-project/sglang` + `3-playground-verified-cell.yml` + `"deepseek-ai/deepseek-v4"`. Set `cookbookModel` to the model's HF id (`<hf-org>/<model-slug>`); it prefills the issue template's free-form `model` input when the issue opens. **Don't prune this block** — without it the engine falls back to `deepseek-ai/deepseek-v4` and submissions from your page get mislabeled. |
## 2.2 Author the 5-dim matrix (`cells[]`)
## 2.1b Custom dimensions (`matchDims` / `overlayDims`)
The legacy shape above is a fixed five dimensions. A model whose axes don't fit
(no variant axis, a deployment-shape axis, an orthogonal feature toggle) declares
its own instead. Declaring `matchDims` replaces `variants`/`quantizations`/
`strategies`/`nodesOptions` wholesale; omitting it keeps the legacy behaviour, so
existing pages need no change.
| Field | Shape | Notes |
|---|---|---|
| `matchDims` | `{id, title, options}[]` | Rows that key the cell lookup. `hw` is always the implicit first dim, so cells match on (hw × these ids). Order is priority: lower rows adapt to higher ones. |
| `overlayDims` | `{id, title, default?, showWhen?, options}[]` | Rows that do NOT key the lookup — the picked option layers onto whichever cell matched. Use this for a knob that is orthogonal to the grid (speculative decoding, hierarchical cache): as a match dim it would multiply the cell count, as an overlay it costs nothing. |
Option shape, both kinds:
| Key | Meaning |
|---|---|
| `id`, `label` | as usual |
| `showWhen(sel)` | option is hidden unless the predicate accepts the current selection — this is how one row shows a different option set per mode |
| `disabled`, `disableReason` | `true` or a predicate over the selection; greys the option out and supplies the tooltip. Use it for a combination the server rejects, so the reader learns why instead of hitting a startup error |
Overlay options additionally take `flags`, `env` and `hints` — each a literal array
or a function of the whole selection (so an "auto" value can resolve against another
row). `hints` render as `# ...` comment lines above the command, for setup the
launch line cannot express on its own.
`_playground.jsx` sees the overlay dims in its `base`, so a playground axis can gate
on them with its own `showWhen(base)` — an axis whose feature the Deploy panel never
switched on is not rendered at all. Changing the Deploy selection resets every
playground axis back to inherit-from-base.
> The overlay resolution rule is written in BOTH engines (snippets can't import each
> other); each copy is marked `MIRROR`. Change both or neither, or the Deploy command
> and the playground base silently disagree.
## 2.2 Author the matrix (`cells[]`)
Each cell describes one verified (or auto-estimated) launch recipe.
@@ -3,7 +3,6 @@ title: Kimi-K2.7-Code
description: "Deploy Kimi-K2.7-Code with SGLang for coding-focused agentic workflows, thinking output, tool calling, and multimodal input."
metatags:
description: "Deploy Kimi-K2.7-Code native multimodal agentic model with SGLang - reasoning, tool calling, and multimodal capabilities."
tag: NEW
---
## 1. Model Introduction
@@ -0,0 +1,403 @@
---
title: Kimi-K3
description: "Deploy Moonshot AI's Kimi-K3 with SGLang — a 2.8T-parameter hybrid Mixture-of-Experts vision-language model (Kimi Delta Attention + MLA, 16/896 active experts) with NVIDIA and AMD recipes."
tag: NEW
---
## Deployment
<a id="install" />
<Accordion title="Install SGLang">
For all methods and hardware platforms, see the [official SGLang installation guide](../../../docs/get-started/install).
<Tabs>
<Tab title="Docker">
```bash Command
docker pull lmsysorg/sglang:kimi-k3 # CUDA13
docker pull lmsysorg/sglang:kimi-k3-cu12 # CUDA12
docker pull lmsysorg/sglang-rocm:rocm720-mi35x-k3-20260727 # ROCM
```
These tags publish with the public K3 launch; until then, build from the Dockerfiles linked below.
For how to launch the image, see [Install → Method 3: Using Docker](../../../docs/get-started/install#method-3-using-docker). Substitute the inner `sglang serve ...` with what the command generator below produces.
</Tab>
</Tabs>
If you do not want to use a Docker image, reproduce the dependency installation steps from the [CUDA 13 Dockerfile](https://github.com/DarkSharpness/sglang-kimi/blob/kimi-k3/docker/kimi_k3/kimi_k3_cu13.Dockerfile) or [CUDA 12 Dockerfile](https://github.com/DarkSharpness/sglang-kimi/blob/kimi-k3/docker/kimi_k3/kimi_k3_cu12.Dockerfile).
</Accordion>
Pick your hardware, then the deployment shape and operating point. Node count follows the hardware recipe (B200 2×8, GB200 4×4, H100 4×8, B300 1×8, H200 2×8, GB300 2×4, MI350X/MI355X 1×8), so it is not a separate choice.
**PD Mode** — `Unified` serves prefill and decode together. `Prefill` / `Decode` split them into dedicated pools (see [PD disaggregation](#pd-disaggregation)); `Prefill` ships two strategies on the TP8 platforms, both chunked at 16k: `Default` (TP8) and `Long-Context` (`--pp-size 8 --tp-size 1`, see [Deep PP](#deep-pp-for-long-context-prefill)).
**Strategy** — the operating point within that shape:
- **Low-Latency** — plain TP, no DCP. For chat.
- **Balanced** — the accuracy-preserving default: TP16/DCP16 on B200/GB200, TP8/DCP8 on B300/GB300, TP8 ROCm/AITER on MI35x.
- **High-Throughput** — the large-scale lane: pick a **Cluster Size** and **Large-Scale Preset** in the Playground ([details](#large-scale-presets)). On H100/H200: Balanced plus `extra_buffer_lazy`.
- **Long-Context** — B200 only: TP8/PP2 splits KV and KDA state across two pipeline stages.
**Spec Decode** — independent of the strategy: DSPARK layers onto any `pp_size == 1` recipe, proposing 7 draft tokens per step (tune in the Playground); DFLASH has no published draft checkpoint. Its win is largest on short interactive traffic and fades as the prompt grows.
<Note>
`--mamba-full-memory-ratio` is the one sizing flag, computed live: set your average request length in the [Mamba ratio calculator](#mamba-ratio-calculator); everything else follows the panels, and the result is pinned into the command.
</Note>
import { Deployment } from "/src/snippets/_deployment.jsx";
import { config } from "/src/snippets/configs/moonshotai/kimi-k3.jsx";
import { benchmarks } from "/src/snippets/configs/moonshotai/kimi-k3-benchmarks.jsx";
import { KimiK3MambaRatioCalculator } from "/src/snippets/_kimi_k3_mamba_ratio_calculator.jsx";
<Deployment config={config} benchmarks={benchmarks} />
### Mamba ratio calculator
<KimiK3MambaRatioCalculator />
<Accordion title="How --mamba-full-memory-ratio is calculated">
`--mamba-full-memory-ratio` is the ratio between the KDA state pool and the MLA KV pool. Every parameter below except `L` is read live from the Deploy panel and Playground selection; the balanced value is the per-request cost ratio:
```text
ratio = (S + D) x state_bytes / (L x (mla_kv_bytes / DCP + draft_kv_bytes))
```
- `S` — KDA state slots per request: `extra_buffer=5`, `extra_buffer_lazy=4`, `no_buffer=3`, disabled radix cache `=1`. `SGLANG_OPT_MAMBA_SKIP_DECODE_LOCK` frees one slot on the extra-buffer strategies; with the overlap scheduler off (or `pp > 1`, which disables it) the track buffer costs one slot instead of two.
- `D` — verify intermediate states under speculative decoding: `0` when disabled, otherwise DSPARK block size + 1 (`8` at the default 7). ReplaySSM (`--enable-linear-replayssm-spec`) folds them into a per-slot ring, returning `D` to `0`.
- `state_bytes` — one state slot's bytes, from K3's fixed geometry, the attention-TP width, and the SSM dtype.
- `mla_kv_bytes` — one token's MLA latent KV bytes (KV-dtype dependent); DCP shards it across its ranks. The DSPARK draft model's KV (~1.4 KB per token) is replicated on every rank, so it enters flat — negligible without DCP, the same order as the sharded MLA share under DCP8.
- `L` — average total request length in tokens: input + output.
</Accordion>
<a id="playground" style={{ scrollMarginTop: "96px" }} />
## Advanced Features Playground
The Playground is where you experiment with **SGLang features beyond the deployment matrix**. The Deploy panel above emits the recipes the SGLang team is converging on; the Playground lets you turn on additional knobs on top of whichever cell the Deploy panel is currently showing.
import { Playground } from "/src/snippets/_playground.jsx";
<Playground config={config} />
## 1. Model Introduction
**Kimi-K3** is Moonshot AI's flagship hybrid MoE vision-language model: **2.8 trillion parameters**, **16 of 896 experts** active per token, roughly **2.5× the scaling efficiency of Kimi-K2**. The backbone interleaves **Kimi Delta Attention (KDA)** with MLA across 93 layers (plus Attention Residuals and Stable LatentMoE); serving supports image input and a **1M-token** window with prefix caching. Weights ship in **MXFP4**: the FlashInfer MXFP4 (trtllm-gen SiTU) runner serves them on Blackwell, Marlin (W4A16) elsewhere, MegaMoE for short-context batch throughput.
K3 **always runs with thinking enabled**, with reasoning depth controlled by `reasoning_effort` (`low` / `high` / `max`; default `max`).
<Note>
Kimi-K3 is Moonshot AI's first open-source model in the trillion-plus class; **full model weights
are scheduled to release by July 27, 2026**. The recipes on this page were validated on the
[`DarkSharpness/sglang-kimi`](https://github.com/DarkSharpness/sglang-kimi) fork — the HuggingFace
repository (`moonshotai/Kimi-K3`) and a public `lmsysorg/sglang` image with K3 support will be
available at launch.
Every cell on this page is currently marked **Not Verified**: the recipes run, but none has a
serving round on the final weights and current code behind it. Re-measure throughput and accuracy
before you rely on any of them.
</Note>
**Recommended generation:** `temperature=1.0`, `top_p=0.95`, `presence_penalty=0`, `frequency_penalty=0` (fixed by the model; informational — do not hardcode in sample code).
**Resources:** [HuggingFace](https://huggingface.co/moonshotai/Kimi-K3) · [Kimi-K3 Quickstart](https://platform.kimi.ai/docs/guide/kimi-k3-quickstart).
## 2. Configuration Tips
**Memory: two pools, one flag.** K3 splits static memory into a worst-case-reserved **KDA state pool** (it sets the concurrency ceiling) and a paged **MLA KV pool**, divided by `--mamba-full-memory-ratio`. The command panel pins that flag to the [calculator](#mamba-ratio-calculator)'s output — set your average request length there; every other calculator input follows the panels. After boot, read back `max_total_num_tokens` (the KV side) and the admitted-request cap (the state side).
Capacity levers, all in the Playground. Each trades precision or cache behavior for capacity — re-verify accuracy on your workload:
| Lever | Effect |
|---|---|
| `--mamba-radix-cache-strategy extra_buffer_lazy` | 4 state slots per request instead of 5 |
| `--mamba-ssm-dtype bfloat16` | ~halves state bytes; with spec on, KDA verification falls back from the fused kernel to Triton |
| `--kv-cache-dtype fp8_e4m3` | halves KV bytes per token; under PD both roles must match at connect |
| `--mem-fraction-static` 0.900.92 | cheapest first win when the boot log shows a large idle `avail mem` |
| `SGLANG_OPT_MAMBA_SKIP_DECODE_LOCK=1` | frees one more slot per request (experimental, under validation) |
Speculation: DSPARK holds block size + 1 (= 8) intermediate states per request — the calculator folds this in — and an unset `--max-running-requests` resets to 48 under spec (the command panel reminds you; set it explicitly to raise).
**MoE runner.** Leave `--moe-runner-backend` unset on Blackwell: FlashInfer MXFP4 (W4A8, prebuilt trtllm-gen SiTU kernels) when the cubin pool is installed, Marlin (W4A16) otherwise; H100/H200 pin Marlin. The published Docker images already provision the **SiTU cubin pool**; to install it independently, run the same flow as the Dockerfile:
```bash
wget https://github.com/sgl-project/whl/releases/download/trtllm_gen_moe_cubin_20260617/trtllm_gen_moe_cubin_pool_20260617_v0613rc1.zip
sudo mkdir -p /opt/trtllm_gen_moe_cubin_pool
sudo unzip -q trtllm_gen_moe_cubin_pool_20260617_v0613rc1.zip -d /opt/trtllm_gen_moe_cubin_pool
export SGLANG_TRTLLM_GEN_MOE_CUBIN_POOL=/opt/trtllm_gen_moe_cubin_pool/trtllm_gen_moe_cubin_pool_20260617_v0613rc1
```
Remaining kernel sources JIT once from the public `flashinfer` wheel (a few minutes, cached).
**Attention backend.** Leave all three attention knobs unset on Blackwell: K3 resolves prefill, decode, and — under DSPARK — verification as a set (`trtllm_mla` across the board; `cutedsl_mla` takes decode and verification under DCP). On the non-DCP recipes, setting any one of the three cancels the auto-resolution for the others. H100/H200 pin `flashmla` for decode.
**Context length.** `--context-length` bounds the longest accepted request plus some context-scaled buffers; it does not size the KV pool. For long context the lever that adds capacity is `fp8_e4m3` KV.
**DSPARK.** Adds `--speculative-algorithm DSPARK` plus the draft checkpoint on top of the showing strategy. Leave `--speculative-draft-attention-backend` unset. No serving round on the final draft checkpoint has landed — measure against the same recipe running NOSPEC before adopting.
**Per-platform notes:**
| Platform | Topology | Notes |
|---|---|---|
| B300 1×8 | TP8 (+DCP8) | accuracy-first defaults on Low-Latency and Balanced |
| GB300 2×4 | TP8/DCP8 | MNNVL transport and cuMem auto-detected |
| B200 2×8 | TP16 (+DCP16); Long-Context TP8/PP2, 128K ctx, 8192 chunked prefill | DSPARK off on Long-Context (`pp_size == 1` required) |
| GB200 4×4 | TP16/DCP16 | MNNVL auto-detected |
| H200 2×8 | TP16/EP16 + symm-mem | same block on both ranks; export the cross-node NIC (`GLOO_SOCKET_IFNAME` / `NCCL_SOCKET_IFNAME`, `SGLANG_HOST_IP`); keep `NCCL_MNNVL_ENABLE=1 NCCL_CUMEM_ENABLE=1` |
| H100 4×8 | TP32/EP32, Marlin + FlashMLA | SM90a build of the K3 image; pin NCCL/Gloo to the same NIC on all nodes; least post-weight headroom (80 GB) |
| MI350X/MI355X 1×8 | TP8 ROCm/AITER | AITER A8W4 FlyDSL MoE, Triton attention, graph bs up to 256; DSPARK supported |
**DCP notes** (Blackwell Balanced / High-Throughput):
- DCP is the only axis that shards the TP-replicated MLA KV; Low-Latency skips it.
- Leave `--dcp-comm-backend` unset (fabric-resolved: `fi_a2a` on GB200/GB300, `a2a` on B200/B300).
- No `--enable-symm-mem` under DCP (force-disabled for decode-graph correctness).
- Explicit `tokenspeed_mla` force-rewrites `--kv-cache-dtype` to fp8; the default `cutedsl_mla` serves either dtype.
- Calculator ratios run well above 1 here (`r > 1` is legal): `bfloat16` state buys admission, `fp8` KV buys context.
- Don't add EP — its a2a buffers reclaim the KV that DCP buys. Compose only to measure.
No cell has a serving round in this exact shape — treat them as starting points to verify.
## 3. Advanced Usage
### 3.1 Reasoning
K3 always thinks; the `kimi_k3` reasoning parser (toggle **Reasoning Parser** in the **Parsers** card of the [Playground above](#playground)) separates that thinking from the final answer — thinking lands in `message.reasoning_content`, the answer in `message.content`. Control the reasoning depth with `reasoning_effort` (`low` / `high` / `max`; default `max`).
<Accordion title="Reasoning Example (Python)">
```python Example
from openai import OpenAI
client = OpenAI(base_url="http://localhost:30000/v1", api_key="EMPTY")
resp = client.chat.completions.create(
model="moonshotai/Kimi-K3",
messages=[{"role": "user", "content": "What is 15% of 240?"}],
reasoning_effort="high", # "low" | "high" | "max" (default max)
)
msg = resp.choices[0].message
print("Reasoning:", getattr(msg, "reasoning_content", None))
print("Answer:", msg.content)
```
</Accordion>
<Accordion title="Example Output">
```text Output
Pending update...
```
</Accordion>
### 3.2 Tool Calling
Enable the `kimi_k3` tool-call parser (toggle **Tool Call Parser** in the **Parsers** card of the [Playground above](#playground)) to surface structured tool calls via `message.tool_calls`. Because K3 is a thinking model, the follow-up turn may put text in `reasoning_content` as well as `content` — print both.
<Accordion title="Tool Calling Example (Python)">
```python Example
from openai import OpenAI
client = OpenAI(base_url="http://localhost:30000/v1", api_key="EMPTY")
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather for a city",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
},
}]
resp = client.chat.completions.create(
model="moonshotai/Kimi-K3",
messages=[{"role": "user", "content": "What's the weather in Beijing?"}],
tools=tools,
)
msg = resp.choices[0].message
print("Reasoning:", getattr(msg, "reasoning_content", None))
print("Tool calls:", msg.tool_calls)
```
</Accordion>
<Accordion title="Example Output">
```text Output
Pending update...
```
</Accordion>
### 3.3 HiCache (Hierarchical KV Caching)
K3's hybrid HiCache tiers the paged MLA KV **and** the KDA/mamba state across L1 (GPU) / L2 (host) / L3 (Mooncake) — enable it from the **HiCache** card in the [Playground above](#playground) for long multi-turn workloads.
- On the DCP recipes (Blackwell Balanced / High-Throughput), the host tiers are not fully DCP-aware yet: **L3 always, and L1+L2 with Spec Decode on, drop the DCP flags and run plain TP** (the command hints call it out — per-request KV capacity shrinks accordingly). L1+L2 NOSPEC keeps DCP.
- Low-Latency and the Hopper recipes take all tiers unchanged.
### 3.4 PD Disaggregation
PD splits prefill and decode into separate server groups; because K3 is hybrid, the transfer moves **both** the paged MLA KV and the KDA recurrent state.
- **Transfer**: the cells emit **NiXL** (RDMA); Mooncake stays selectable in the Playground.
- **Ports**: prefill `30000`, decode `30100` (derived ZMQ/dist ranges must not collide on a shared host). The positional `8998` after `--prefill` must match `--disaggregation-bootstrap-port`, or only the decode worker registers.
- **Decode state pool**: chunk cache — one slot per request; `--mamba-radix-cache-strategy` is inert. Keep `--disaggregation-decode-extra-slots` pinned: unpinned it defaults to twice the batch below 32 requests and **zero** above.
#### Deep PP for long-context prefill
The `Long-Context` prefill strategy is `--pp-size 8 --tp-size 1`: pipeline P2P overlaps the next microbatch's compute, unlike TP/EP collectives, and each stage owns whole layers (a clean slice of KV and state).
- Use all eight stages; a shallow split still pays the in-stage all-reduce and can lose to TP8.
- Pays only with several requests in flight (hence `Default` = TP8).
- DSPARK off (`pp_size == 1` required).
- Fan one prefill role out to several decode roles; budget for in-transfer KV on the decode side.
<Accordion title="Router">
```bash Command
python3 -m sglang_router.launch_router \
--pd-disaggregation \
--prefill http://<prefill-host>:30000 8998 \
--decode http://<decode-host>:30100 \
--host 0.0.0.0 --port 8000 \
--disable-circuit-breaker \
--health-check-interval-secs 999999
```
</Accordion>
Clients then send requests to the router (`:8000`) instead of an individual role server.
### 3.5 VLM Serving Profiles
The open-source K3 serving contract currently supports **image input only** — its
processor rejects video and audio input.
#### Recommended high-speed VLM
The command panel now opens on the **B300 · Unified · Balanced**
recipe below. It makes the VLM-specific performance choices explicit:
```bash Command
sglang serve \
--trust-remote-code \
--model-path moonshotai/Kimi-K3 \
--tp-size 8 \
--dcp-size 8 \
--mem-fraction-static 0.85 \
--mm-feature-transport cuda_ipc \
--mm-processor-worker-num 2 \
--mm-io-worker-num 16 \
--reasoning-parser kimi_k3 \
--tool-call-parser kimi_k3 \
--host 0.0.0.0 \
--port 30000
```
- `--mm-feature-transport cuda_ipc` — single-node only: skips the CPU round trip, bounded pool (per-tensor CPU fallback when full), reserves up to `SGLANG_MM_FEATURE_CACHE_MB` on the base GPU. Multi-node recipes use CPU transport.
- 2 processor / 16 I/O workers are the measured defaults; more adds contention.
- Leave `--mm-attention-backend` unset — auto-selected, with a correctness fallback.
- Don't add `--mm-enable-dp-encoder`; K3 already shards images across TP ranks.
#### Should ViT BCG be enabled?
Keep ViT BCG **off** for general serving; enable `SGLANG_VIT_ENABLE_CUDA_GRAPH=1` only for ViT-only / EPD encoder workloads with recurring image shapes and spare HBM.
- The win is confined to the encoder — no reliable end-to-end TTFT/TPOT gain in full-model serving.
- Each captured graph retains HBM (graph + per-entry metadata); measure on your own shapes.
- The default cache captures after two hits and falls back to eager above 6,144 tokens; do not enlarge it without measuring.
#### Low-HBM VLM
Use this profile when keeping HBM headroom matters more than peak concurrency.
It removes the 1 GiB CUDA IPC pool, keeps ViT BCG disabled, halves the context
window, caps concurrency, and lowers the static-memory target:
```bash Command
SGLANG_VIT_ENABLE_CUDA_GRAPH=0 \
sglang serve \
--trust-remote-code \
--model-path moonshotai/Kimi-K3 \
--tp-size 8 \
--context-length 65536 \
--enable-symm-mem \
--mem-fraction-static 0.82 \
--mm-feature-transport cpu \
--mm-processor-worker-num 2 \
--mm-io-worker-num 16 \
--reasoning-parser kimi_k3 \
--tool-call-parser kimi_k3 \
--host 0.0.0.0 \
--port 30000
```
`--mem-fraction-static 0.82` is a conservative B300 starting point, not a portable minimum: raise it toward `0.85` if startup reports insufficient memory; if HBM must go back to other workloads, reduce context/concurrency first. The precision levers (`fp8_e4m3` KV, `bfloat16` SSM state) save far more but stay accuracy-gated.
<a id="large-scale-presets" />
### 3.6 Large-Scale Serving Presets (1664 GPUs, Blackwell)
**The KDA state pool is the concurrency ceiling** — DP, EP, and DCP do not shard it; only attention-TP width, SSM dtype, and cache strategy change the per-GPU bill. The MLA KV is cheap to shrink (fp8) or deduplicate (DCP).
Two presets come out of this, at `N = 8k` GPUs:
| Preset | What it trades | Pick it for |
|---|---|---|
| **Peak Throughput** — `dp = k`, attention-TP 8 | State shards 8-way. The per-step KDA all-reduce stays within one 8-GPU B200/B300 node, or spans two 4-GPU GB200/GB300 nodes over MNNVL. `--kv-cache-dtype fp8_e4m3` is load-bearing — bf16 KV does not fit 128 requests per replica. | Maximum sustained TPS — the default large-scale shape. |
| **Peak Capacity (+DCP8)** — `dp = k` + `--dcp-size 8` | Deduplicates the attention-TP group's MLA KV: concurrency ceiling +72% at the same engine throughput, ~1.8× ITL. | Context ≥ ~16K, or per-replica concurrency past 128. |
- **Radix cache** is independent of the preset: for prefix-free traffic (offline batch, evals) switch it off (Playground's **Prefix Cache** card) — one state slot per request instead of 45.
- The fully data-parallel extreme (`--dp-size` = GPU count, attention-TP 1) — the shape behind the 64-GPU sweep's ~3K tok/s per GPU — is not a preset: 288 GB GPUs only, radix forced off, no head-to-head against the preset shape.
The Peak Throughput preset at 32 GPUs on B200/B300 (4 nodes × 8; every node runs the same command with its own `--node-rank`). On GB200/GB300 the same 32-GPU shape uses 8 nodes × 4, and the Playground emits `--nnodes 8`:
```bash Command
SGLANG_OPT_DEEPGEMM_MEGA_MOE_NUM_MAX_TOKENS_PER_RANK=20480 \
sglang serve \
--trust-remote-code \
--model-path moonshotai/Kimi-K3 \
--tp-size 32 --ep-size 32 \
--enable-dp-attention --dp-size 4 --enable-dp-lm-head \
--nnodes 4 --node-rank <rank> --dist-init-addr <node0-ip>:20000 \
--moe-a2a-backend megamoe --moe-runner-backend deep_gemm \
--kv-cache-dtype fp8_e4m3 \
--mamba-ssm-dtype bfloat16 \
--mamba-radix-cache-strategy extra_buffer_lazy \
--mem-fraction-static 0.92 \
--reasoning-parser kimi_k3 --tool-call-parser kimi_k3 \
--host 0.0.0.0 --port 30000
```
Scale by holding the per-replica shape fixed and moving only the replica count; pool sizing rides the calculator-driven `--mamba-full-memory-ratio`, which folds in DP, DCP, precision, and speculation:
| GPUs | B200/B300 nodes | GB200/GB300 nodes | `--tp-size` / `--ep-size` | `--dp-size` |
|---|---|---|---|---|
| 16 | 2×8 | 4×4 | 16 | 2 |
| 32 | 4×8 | 8×4 | 32 | 4 |
| 64 | 8×8 | 16×4 | 64 | 8 |
For Peak Capacity, add `--dcp-size 8` and re-derive the pool split with the [Mamba ratio calculator](#mamba-ratio-calculator).
Both presets are one click away in the [Playground above](#playground): pick a **Cluster Size** and a **Large-Scale Preset** and the full command composes onto whichever cell is showing.
Decisions the preset already makes:
- **MegaMoE on `deep_gemm`** — the fastest a2a backend; needs the SiTU cubin pool ([§2](#2-configuration-tips)).
- **SP-MoE and shared-expert overlap** engage automatically under EP a2a; the K3 all-reduce fusion does not.
- **Spec Decode follows the Deploy knob.** Acceptance thins at large batch; spec × EP × DP-attention is validated only at 8-GPU EP8 × DP2 (full GSM8K) — experimental at these scales.
<Note>
No preset has a full serving round on final weights; the constants derive from measured single- and dual-node rounds plus a 64-GPU sweep. Validate throughput and accuracy on your workload before committing a fleet.
</Note>
+1 -1
View File
@@ -70,7 +70,7 @@ metatags:
<Card
title="Moonshotai"
mode="card"
href="/cookbook/autoregressive/Moonshotai/Kimi-K2.7-Code"
href="/cookbook/autoregressive/Moonshotai/Kimi-K3"
img="/cards/logos/moonshotai.png"
/>
<Card
+1
View File
@@ -1060,6 +1060,7 @@
{
"group": "Moonshotai",
"pages": [
"cookbook/autoregressive/Moonshotai/Kimi-K3",
"cookbook/autoregressive/Moonshotai/Kimi-K2.7-Code",
"cookbook/autoregressive/Moonshotai/Kimi-K2.6",
"cookbook/autoregressive/Moonshotai/Kimi-K2.5",
+179
View File
@@ -9,6 +9,185 @@ keywords:
mode: wide
---
<div className="not-prose">
<div
style={{
position: "relative",
overflow: "hidden",
marginBottom: "1.5rem",
padding: "clamp(1.5rem, 4vw, 2.5rem)",
border: "1px solid rgba(251, 146, 60, 0.35)",
borderRadius: "1rem",
background:
"linear-gradient(135deg, #111827 0%, #31202f 58%, #9a3412 100%)",
boxShadow: "0 20px 45px rgba(17, 24, 39, 0.18)",
color: "#ffffff",
}}
>
<div
aria-hidden="true"
style={{
position: "absolute",
top: "-7rem",
right: "-5rem",
width: "18rem",
height: "18rem",
borderRadius: "999px",
background: "rgba(251, 146, 60, 0.18)",
filter: "blur(2px)",
}}
/>
<div
style={{
position: "relative",
zIndex: 1,
display: "flex",
flexWrap: "wrap",
alignItems: "center",
gap: "clamp(1.25rem, 3vw, 2rem)",
}}
>
<div style={{ flex: "1 1 24rem", minWidth: 0 }}>
<div
style={{
display: "inline-flex",
alignItems: "center",
gap: "0.45rem",
marginBottom: "0.9rem",
padding: "0.35rem 0.7rem",
border: "1px solid rgba(255, 255, 255, 0.28)",
borderRadius: "999px",
background: "rgba(255, 255, 255, 0.1)",
fontSize: "0.72rem",
fontWeight: 750,
letterSpacing: "0.08em",
textTransform: "uppercase",
}}
>
<span aria-hidden="true">✦</span>
Featured model · New
</div>
<a
href="/cookbook/autoregressive/Moonshotai/Kimi-K3"
style={{
display: "block",
margin: 0,
color: "#ffffff",
fontSize: "clamp(1.75rem, 4vw, 2.65rem)",
fontWeight: 750,
lineHeight: 1.08,
letterSpacing: "-0.035em",
textDecoration: "none",
}}
>
Meet Kimi-K3 on SGLang
</a>
<p
style={{
maxWidth: "48rem",
margin: "1rem 0 0",
color: "rgba(255, 255, 255, 0.82)",
fontSize: "1rem",
lineHeight: 1.65,
}}
>
SGLang natively implements and deeply optimizes K3's new architecture
with fused KDA decode kernels, DP attention, MTP, PD disaggregation,
and KDA-aware prefix caching. Kimi-K3 is supported on both NVIDIA and
AMD GPUs.
</p>
<div
style={{
display: "flex",
flexWrap: "wrap",
gap: "0.5rem",
marginTop: "1.15rem",
}}
>
{[
"2.8T parameters",
"Fused KDA decode",
"NVIDIA + AMD",
].map((item) => (
<span
key={item}
style={{
padding: "0.35rem 0.65rem",
borderRadius: "999px",
background: "rgba(255, 255, 255, 0.1)",
color: "rgba(255, 255, 255, 0.9)",
fontSize: "0.78rem",
fontWeight: 650,
}}
>
{item}
</span>
))}
</div>
<a
href="/cookbook/autoregressive/Moonshotai/Kimi-K3"
style={{
display: "inline-flex",
alignItems: "center",
marginTop: "1.35rem",
padding: "0.7rem 1rem",
borderRadius: "0.55rem",
background: "#ffffff",
color: "#7c2d12",
fontSize: "0.88rem",
fontWeight: 750,
textDecoration: "none",
}}
>
Open the Kimi-K3 cookbook&nbsp;→
</a>
</div>
<a
href="/cookbook/autoregressive/Moonshotai/Kimi-K3"
aria-label="Open the Kimi-K3 cookbook"
style={{
flex: "0 1 12rem",
minWidth: "10rem",
padding: "0.8rem",
border: "1px solid rgba(255, 255, 255, 0.22)",
borderRadius: "0.9rem",
background: "rgba(255, 255, 255, 0.96)",
boxShadow: "0 16px 35px rgba(0, 0, 0, 0.22)",
textDecoration: "none",
}}
>
<div
role="img"
aria-label="Moonshot AI"
style={{
width: "100%",
aspectRatio: "16 / 9",
borderRadius: "0.45rem",
backgroundColor: "#ffffff",
backgroundImage: "url('/cards/logos/moonshotai.png')",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
backgroundSize: "cover",
}}
/>
<div
style={{
padding: "0.65rem 0.35rem 0.2rem",
color: "#111827",
textAlign: "center",
fontSize: "0.78rem",
fontWeight: 750,
letterSpacing: "0.06em",
textTransform: "uppercase",
}}
>
Kimi-K3 deployment guide
</div>
</a>
</div>
</div>
</div>
<a
class="github-button"
href="https://github.com/sgl-project/sglang"
+192
View File
@@ -0,0 +1,192 @@
#!/usr/bin/env node
// Static guard for the cookbook deployment/playground engines and their configs.
// Zero dependencies, no browser, no Mintlify — plain `node`.
//
// node docs_new/scripts/check_cookbook_configs.mjs
//
// What it protects, in order of how expensive the bug is to find by hand:
//
// 1. MIRROR drift. The overlay-resolution rule is written in both engines
// because Mintlify snippets cannot import each other. If the copies drift,
// the Deploy command and the playground's base disagree and the reader sees
// phantom +/- lines in the diff — with no error anywhere.
// 2. Sibling identity. Overlay resolution clones the base cell, so sibling
// detection must compare match dimensions rather than object references.
// 3. Config/engine contract. A cell keyed on a dimension the config no longer
// declares silently stops matching; the panel just shows a different cell.
// 4. Predicate safety. showWhen / disabled / flags run against selections the
// author never clicked through; a throw there blanks the whole widget.
import { readFileSync, readdirSync } from "node:fs";
import { dirname, join, relative } from "node:path";
import { fileURLToPath } from "node:url";
const SNIPPETS = join(dirname(fileURLToPath(import.meta.url)), "..", "src", "snippets");
const CONFIGS = join(SNIPPETS, "configs");
const LEGACY_DIMS = ["variants", "quantizations", "strategies", "nodesOptions"];
const failures = [];
const fail = (where, msg) => failures.push(`${where}: ${msg}`);
// ---------------------------------------------------------------- 1. MIRROR
// Compare the marked blocks with comments and whitespace normalized away, so
// wording may differ per file but the rule may not.
const mirrorBody = (file) => {
const src = readFileSync(join(SNIPPETS, file), "utf8");
const start = src.indexOf("==== MIRROR");
const end = src.indexOf("==== end MIRROR");
if (start === -1 || end === -1) return null;
return src
.slice(src.indexOf("\n", start), end)
.split("\n")
.map((l) => l.trim())
.filter((l) => l && !l.startsWith("//"))
.join(" ")
.replace(/\s+/g, " ");
};
const a = mirrorBody("_deployment.jsx");
const b = mirrorBody("_playground.jsx");
if (a === null) fail("_deployment.jsx", "MIRROR markers missing");
if (b === null) fail("_playground.jsx", "MIRROR markers missing");
if (a && b && a !== b) {
fail("MIRROR", "overlay resolution has drifted between the two engines");
const [la, lb] = [a.split(" "), b.split(" ")];
const i = la.findIndex((t, k) => t !== lb[k]);
fail("MIRROR", `first divergence near token ${i}: `
+ `_deployment "${la.slice(i, i + 8).join(" ")}" vs `
+ `_playground "${lb.slice(i, i + 8).join(" ")}"`);
}
// `withOverlay` returns a clone, so object identity can never distinguish the
// current base cell from a true sibling. This previously made every cookbook
// show a spurious "matches … / switch base" hint before the reader changed
// anything.
const playgroundSource = readFileSync(join(SNIPPETS, "_playground.jsx"), "utf8");
if (/\bmatchedCell\s*!==\s*baseCell\b/.test(playgroundSource)) {
fail("_playground.jsx", "sibling detection compares cloned cells by object identity");
}
// --------------------------------------------------------------- 3/4. Configs
// Configs are .jsx with a single `export const config` literal; import them
// through a data: URL so no temp file is needed.
const loadConfig = async (path) => {
const src = readFileSync(path, "utf8");
const mod = await import(
"data:text/javascript," + encodeURIComponent(src)
);
return mod.config;
};
// Every combination of match dims + overlay dims the reader can produce.
const selectionSpace = (config) => {
const dims = [
{ id: "hw", options: (config.supportedHardware || []).map((id) => ({ id })) },
...(config.matchDims || []),
...(config.overlayDims || []),
];
let space = [{}];
for (const d of dims) {
const next = [];
for (const partial of space) {
for (const opt of (d.options || [])) next.push({ ...partial, [d.id]: opt.id });
}
space = next.length ? next : space;
if (space.length > 20000) return space.slice(0, 20000); // cheap blow-up guard
}
return space;
};
const walk = (dir) => readdirSync(dir, { withFileTypes: true }).flatMap((e) =>
e.isDirectory() ? walk(join(dir, e.name))
: (e.name.endsWith(".jsx") && !e.name.includes("benchmark") ? [join(dir, e.name)] : []));
for (const path of walk(CONFIGS)) {
const where = relative(join(SNIPPETS, ".."), path);
let config;
try {
config = await loadConfig(path);
} catch (e) {
fail(where, `does not parse as a module: ${e.message}`);
continue;
}
if (!config) { fail(where, "no `export const config`"); continue; }
const custom = Array.isArray(config.matchDims);
// A config either declares its own dims or carries the full legacy set —
// half of each means the engine silently renders a dimension nobody authored.
if (!custom) {
for (const k of LEGACY_DIMS) {
if (!Array.isArray(config[k])) fail(where, `legacy config is missing \`${k}\``);
}
}
const matchIds = ["hw", ...(custom
? config.matchDims.map((d) => d.id)
: LEGACY_DIMS.map((k) => ({ variants: "variant", quantizations: "quant",
strategies: "strategy", nodesOptions: "nodes" })[k]))];
for (const [i, cell] of (config.cells || []).entries()) {
const keys = Object.keys(cell.match || {}).sort();
const want = [...matchIds].sort();
if (keys.join(",") !== want.join(",")) {
fail(where, `cells[${i}].match keys [${keys}] != declared dims [${want}]`);
}
for (const dim of (config.matchDims || [])) {
const v = cell.match[dim.id];
if (!(dim.options || []).some((o) => o.id === v)) {
fail(where, `cells[${i}].match.${dim.id}="${v}" is not an option of that dim`);
}
}
// Without a `nodes` dim the node count rides on the cell; a missing one
// silently degrades a multi-node recipe to single-node.
if (custom && !matchIds.includes("nodes") && cell.nnodes === undefined) {
fail(where, `cells[${i}] has no \`nnodes\` and the config declares no nodes dim`);
}
}
for (const dim of (config.overlayDims || [])) {
const ids = (dim.options || []).map((o) => o.id);
if (dim.default !== undefined && !ids.includes(dim.default)) {
fail(where, `overlayDims.${dim.id}.default="${dim.default}" is not one of [${ids}]`);
}
}
// Predicates and flag builders must survive every reachable selection.
const space = selectionSpace(config);
const probe = (fn, label) => {
for (const sel of space) {
try { fn(sel); } catch (e) {
fail(where, `${label} throws on ${JSON.stringify(sel)}: ${e.message}`);
return;
}
}
};
for (const dim of [...(config.matchDims || []), ...(config.overlayDims || [])]) {
if (typeof dim.showWhen === "function") probe(dim.showWhen, `${dim.id}.showWhen`);
for (const opt of (dim.options || [])) {
const tag = `${dim.id}.${opt.id}`;
if (typeof opt.showWhen === "function") probe(opt.showWhen, `${tag}.showWhen`);
if (typeof opt.disabled === "function") probe(opt.disabled, `${tag}.disabled`);
for (const key of ["flags", "env", "hints"]) {
if (typeof opt[key] !== "function") continue;
probe((sel) => {
const out = opt[key](sel);
if (out !== undefined && !Array.isArray(out)) throw new Error(`${key} returned ${typeof out}, expected an array`);
for (const f of (out || [])) {
if (typeof f !== "string") throw new Error(`${key} yielded a non-string entry`);
if (/undefined|NaN/.test(f)) throw new Error(`${key} produced "${f}"`);
}
}, `${tag}.${key}`);
}
}
}
}
if (failures.length) {
console.error(`FAIL (${failures.length})`);
for (const f of failures) console.error(" - " + f);
process.exit(1);
}
console.log("cookbook config check: OK");
+362 -86
View File
@@ -8,12 +8,29 @@
// supportedHardware hw ids shown in the catalog (subset of HARDWARE_CATALOG config.hardware)
// hardware optional — per-model GPUs the shared HARDWARE_CATALOG lacks:
// {id, label, vram, vendor}[] merged into the catalog at render
// (so a model-specific GPU never needs an engine-catalog edit)
// variants/quantizations/strategies/nodesOptions the 5-dim option lists
// (nodesOptions id is `single` or `multi-N` → --nnodes N)
// cells {match, verified?, env, flags}[] — one per
// (hw × variant × quant × strategy × nodes); env/flags are
// flat literals, only {{PLACEHOLDER}} subst applied
// (so a model-specific GPU never needs an engine-catalog edit);
// vendor picks the selector group: blackwell | hopper | amd
// variants/quantizations/strategies/nodesOptions LEGACY 4-dim option lists,
// used when `matchDims` is absent (nodesOptions id is
// `single` or `multi-N` → --nnodes N)
// matchDims optional — replaces the legacy four. {id, title, options}[]
// where each option is {id, label, showWhen?(sel), disabled?,
// disableReason?}. `hw` is always the implicit first dim.
// Cells are then keyed on (hw × <these ids>).
// overlayDims optional — rows that do NOT participate in cell lookup; the
// picked option layers onto the matched cell, so an orthogonal
// knob does not multiply the cell count. Same option shape plus
// `flags` / `env` / `hints` (each a literal array or a function
// of the whole selection), and a row-level `default` / `showWhen`.
// `hints` render as `# ...` lines above the command.
// cells {match, verified?, nnodes?, warn?, redirect?, env, flags}[] — one per
// (hw × match dims); env/flags are flat literals, only
// {{PLACEHOLDER}} subst applied. `nnodes` supplies the node
// count for configs with no `nodes` dim (default 1). `warn`
// renders as a ⚠️ banner under the cell's command; it may
// embed [label](#anchor) links. `redirect: true` renders the
// banner ALONE — no command, header, or copy buttons — for
// cells that only point somewhere else.
// modelNames HF slug lookup, `hw|variant|quant` then `variant|quant`
// placeholders {{KEY}} → {target: 'command'|'curl', label, default?}
// curl cURL template (uses {{MODEL_NAME}} + placeholders)
@@ -31,6 +48,8 @@
// multiNodeHints optional — {[hwId]: string[]} prepended as `# ...` lines
// dockerImages optional — `docker run` image, keyed by `hw|quant`
// then `hw`; falls back to `lmsysorg/sglang:dev`
// runModes optional — command output tabs to show (`python` and/or
// `docker`); defaults to both, in that order
// github optional — "Submit verified cell" issue-template overrides
// playgroundFeatures optional — consumed by _playground.jsx (see its header)
//
@@ -49,13 +68,15 @@ export const Deployment = ({ config, benchmarks }) => {
// ==== 1. Hardware catalog (shared across cookbooks) ====
// VRAM is per-GPU on-chip memory, not per-module.
const HARDWARE_CATALOG = {
nvidia: [
{ id: "h100", label: "H100", vram: "80GB" },
{ id: "h200", label: "H200", vram: "141GB" },
{ id: "b200", label: "B200", vram: "192GB" },
blackwell: [
{ id: "b300", label: "B300", vram: "288GB" },
{ id: "gb200", label: "GB200", vram: "192GB" },
{ id: "gb300", label: "GB300", vram: "288GB" },
{ id: "b200", label: "B200", vram: "192GB" },
{ id: "gb200", label: "GB200", vram: "192GB" },
],
hopper: [
{ id: "h200", label: "H200", vram: "141GB" },
{ id: "h100", label: "H100", vram: "80GB" },
],
amd: [
{ id: "mi300x", label: "MI300X", vram: "192GB" },
@@ -86,10 +107,12 @@ export const Deployment = ({ config, benchmarks }) => {
},
title: { fontSize: "12px", fontWeight: "600", minWidth: "108px", flexShrink: 0, color: isDark ? "#e5e7eb" : "inherit" },
vendorRow: { display: "flex", alignItems: "center", gap: "6px" },
// Fixed width so every row's chips start at the same x regardless of the
// group name ("BLACKWELL" is the widest).
vendorLabel: {
fontSize: "10px", fontWeight: "600",
color: isDark ? "#9ca3af" : "#6b7280",
minWidth: "38px", textTransform: "uppercase", letterSpacing: "0.04em",
width: "68px", flexShrink: 0, textTransform: "uppercase", letterSpacing: "0.04em",
},
// auto-fit + a real min width: columns wrap on narrow screens instead of
// shrinking below their label (the old minmax(0,1fr) let buttons overlap on
@@ -365,9 +388,72 @@ export const Deployment = ({ config, benchmarks }) => {
});
// ==== 3. Pure helpers (no React state) ====
// Two kinds of selector row:
// match dims participate in cell lookup (cell.match[dim] === sel[dim])
// overlay dims never touch cell lookup; the picked option contributes flags
// on top of the matched cell (so an orthogonal knob like
// speculative decoding does not multiply the cell count)
// A config that declares neither keeps the legacy fixed 5-dim shape, so model
// pages written before this existed render unchanged.
const LEGACY_MATCH_DIMS = [
{ id: "variant", title: "Model Variant", optionsKey: "variants" },
{ id: "quant", title: "Quantization", optionsKey: "quantizations" },
{ id: "strategy", title: "Strategy", optionsKey: "strategies" },
{ id: "nodes", title: "Nodes", optionsKey: "nodesOptions" },
];
// `hw` is always the first match dim; it has its own vendor-grouped renderer.
const matchDimSpecs = (config.matchDims || LEGACY_MATCH_DIMS).map((d) => ({
...d,
options: d.options || config[d.optionsKey] || [],
}));
const overlayDimSpecs = config.overlayDims || [];
// DIMENSIONS is ordered by priority — higher-index dims adapt to lower-index
// picks, never the reverse. Drives the grey-out/snap logic below.
const DIMENSIONS = ["hw", "variant", "quant", "strategy", "nodes"];
const DIMENSIONS = ["hw", ...matchDimSpecs.map((d) => d.id)];
// An option is visible when it declares no `showWhen`, or its predicate accepts
// the current selection. Hidden options are excluded from snapping and from the
// grey-out scan, so a stale pick can never survive a dependent-row switch.
// ==== MIRROR in _playground.jsx — keep the two copies identical ====
// Snippets cannot import each other, so the overlay-resolution rule is written
// twice. A divergence makes the Deploy command and the playground base disagree,
// which shows up as phantom +/- lines in the diff and no error anywhere.
// Guarded by docs_new/scripts/check_cookbook_configs.mjs.
const optionVisible = (opt, sel) =>
typeof opt.showWhen !== "function" || opt.showWhen(sel);
const optionDisabled = (opt, sel) =>
typeof opt.disabled === "function" ? opt.disabled(sel) : !!opt.disabled;
const visibleOptions = (spec, sel) =>
(spec.options || []).filter((o) => optionVisible(o, sel));
const rowVisible = (spec, sel) =>
(typeof spec.showWhen !== "function" || spec.showWhen(sel)) &&
visibleOptions(spec, sel).length > 0;
const overlayPick = (sel) => {
const picked = [];
for (const spec of (config.overlayDims || [])) {
if (!rowVisible(spec, sel)) continue;
const opt = (spec.options || []).find((o) => o.id === sel[spec.id]);
if (opt && !optionDisabled(opt, sel)) picked.push(opt);
}
return picked;
};
const overlayPart = (sel, key) => {
const out = [];
for (const opt of overlayPick(sel)) {
const add = typeof opt[key] === "function" ? opt[key](sel) : opt[key];
if (add) out.push(...add);
}
return out;
};
// An overlay option may also REMOVE cell flags, declared as `stripPrefixes`
// (a static list, or a function of the selection). L3 uses it to drop the
// whole DCP operating point, which the server rejects with an L3 backend.
const overlayStrip = (cellFlags, sel) => {
const strip = overlayPart(sel, "stripPrefixes");
if (!strip.length) return [...(cellFlags || [])];
return (cellFlags || []).filter((f) => !strip.includes(f.split(/[\s=]/)[0]));
};
// ==== end MIRROR ====
const findCell = (cells, sel) =>
cells.find((c) => DIMENSIONS.every((d) => c.match[d] === sel[d]));
@@ -461,37 +547,74 @@ export const Deployment = ({ config, benchmarks }) => {
valid[dim] = fallback ? fallback.match[dim] : want;
}
}
// Overlay dims ride along: they never key cells, so snapping must not drop
// them (it did — a strict-mode hash round-trip lost the spec default).
// Keep the parsed value when it names a real option, else the row default.
for (const spec of overlayDimSpecs) {
const want = parsed[spec.id];
const opts = spec.options || [];
valid[spec.id] = opts.some((o) => o.id === want)
? want
: spec.default ?? (opts[0] && opts[0].id) ?? "";
}
return valid;
};
// Lookup walks most-specific to least so a config that drops the variant/quant
// dims can key its HF slug on `hw` alone, or on the single "default" entry.
const resolveModelName = (sel) => {
const triple = `${sel.hw}|${sel.variant}|${sel.quant}`;
const pair = `${sel.variant}|${sel.quant}`;
return config.modelNames[triple] ?? config.modelNames[pair] ?? "";
const keys = [
`${sel.hw}|${sel.variant}|${sel.quant}`,
`${sel.variant}|${sel.quant}`,
sel.hw,
"default",
];
for (const k of keys) {
const hit = config.modelNames[k];
if (hit) return hit;
}
return "";
};
const interpolate = (text, env, modelName) =>
text.replace(/{{(\w+)}}/g, (_, key) =>
key === "MODEL_NAME" ? modelName : (env[key] ?? `{{${key}}}`));
// Node count comes from the `nodes` dim when the config has one; without that
// dim it is a property of the cell itself (`nnodes`), since the deployment
// shape is then fixed by the hardware rather than picked by the reader.
const parseNnodes = (id) => {
if (id === "single") return 1;
const m = /^multi-(\d+)$/.exec(id);
const m = /^multi-(\d+)$/.exec(id || "");
return m ? parseInt(m[1], 10) : 1;
};
const cellNnodes = (cell, sel) =>
sel.nodes !== undefined ? parseNnodes(sel.nodes) : (cell.nnodes || 1);
// Role-specific serving ports for PD deployments — keep in sync with PD_PORTS
// in _playground.jsx, which the generated router command targets. Each role
// derives 5 ZMQ/dist ports from its --port, so the serve ports are spaced 100
// apart to keep those ranges from overlapping on a same-host deployment.
const PD_SERVE_PORTS = { prefill: 30000, decode: 30100 };
// `flags` / `env` / `hints` may each be a function of the whole selection, so an
// "Auto" option can resolve against another row (draft tokens per strategy).
const overlayFlags = (sel) => overlayPart(sel, "flags");
const overlayEnv = (sel) => overlayPart(sel, "env");
const overlayHints = (sel) => overlayPart(sel, "hints");
// python mode → bare `sglang serve`; docker mode → wrapped in `docker run`.
const renderCommand = (cell, sel, envValues, mode = "python") => {
if (!cell) return "# No command available for the current selection.";
const modelName = resolveModelName(sel);
const nnodes = parseNnodes(sel.nodes);
const nnodes = cellNnodes(cell, sel);
const multinode = nnodes > 1;
const cellEnv = cell.env || [];
const flags = [...(cell.flags || [])];
const cellEnv = [...(cell.env || []), ...overlayEnv(sel)];
const flags = [...overlayStrip(cell.flags, sel), ...overlayFlags(sel)];
if (multinode) {
// Insert the multi-node trio after the last parallelism flag,
// falling back to right after --model-path.
const PARALLELISM_ANCHORS = ["--enable-dp-attention", "--dp", "--tp"];
const PARALLELISM_ANCHORS = ["--enable-dp-attention", "--dp", "--tp-size", "--tp"];
let i = -1;
for (const anchor of PARALLELISM_ANCHORS) {
i = flags.findIndex((f) => f.split(/[\s=]/)[0] === anchor);
@@ -504,6 +627,15 @@ export const Deployment = ({ config, benchmarks }) => {
`--dist-init-addr {{NODE0_IP}}:20000`);
}
const pdServePort = PD_SERVE_PORTS[sel.pdMode];
if (pdServePort !== undefined) {
for (let j = 0; j < flags.length; j++) {
if (flags[j].split(/[\s=]/)[0] === "--port") {
flags[j] = `--port ${pdServePort}`;
}
}
}
let cmd;
if (mode === "docker") {
// Image keyed by `hw|quant` (most specific) then `hw`; `:dev` if unmapped.
@@ -553,8 +685,14 @@ export const Deployment = ({ config, benchmarks }) => {
cmd = `${envBlock}sglang serve \\\n${flagBlock}`;
}
if (multinode && config.multiNodeHints && config.multiNodeHints[sel.hw]) {
const hint = config.multiNodeHints[sel.hw]
const hintLines = [
...overlayHints(sel),
...(multinode && config.multiNodeHints && config.multiNodeHints[sel.hw]
? config.multiNodeHints[sel.hw]
: []),
];
if (hintLines.length) {
const hint = hintLines
.map((line) => (line.length ? "# " + line : "#")).join("\n");
cmd = `${hint}\n${cmd}`;
}
@@ -836,13 +974,19 @@ export const Deployment = ({ config, benchmarks }) => {
return groups;
};
// Match dims seed from the first cell (authoring convention: put the flagship
// verified cell first). Overlay dims seed from their own `default`, or the
// first option, since no cell carries them.
const initialSelectionFromCells = () => {
const first = config.cells[0];
if (!first) return Object.fromEntries(DIMENSIONS.map((d) => [d, ""]));
return {
hw: first.match.hw, variant: first.match.variant, quant: first.match.quant,
strategy: first.match.strategy, nodes: first.match.nodes,
};
const sel = Object.fromEntries(
DIMENSIONS.map((d) => [d, first ? first.match[d] : ""]),
);
for (const spec of overlayDimSpecs) {
const opts = spec.options || [];
sel[spec.id] = spec.default ?? (opts[0] && opts[0].id) ?? "";
}
return sel;
};
const placeholderDefaults = (schema) => {
@@ -888,6 +1032,8 @@ export const Deployment = ({ config, benchmarks }) => {
};
const [sel, setSel] = useState(() => initialSelectionFromCells());
const INTERNAL_HASH_STATE_KEY = "__sglangDeployInternalHash";
const DEPLOYMENT_COMPONENT_ID = "deployment-configurator";
useEffect(() => {
const hydrate = () => {
const raw = window.location.hash.replace(/^#/, "");
@@ -902,10 +1048,15 @@ export const Deployment = ({ config, benchmarks }) => {
if (!touched) return;
// Snap to a real cell if the hash named an impossible combo (stale link).
setSel(validateSelection(config.cells, parsed));
// Scroll the Deploy section into view. Heading slugs to "deployment" or
// "deploy"; only fires on hash navigation (not replaceState chip clicks).
const el = document.getElementById("deployment")
|| document.getElementById("deploy");
const historyState = window.history.state;
const isInternalHash =
historyState &&
typeof historyState === "object" &&
historyState[INTERNAL_HASH_STATE_KEY] === `#${raw}`;
if (isInternalHash) return;
// External selection hashes land on the interactive configurator. Hashes
// written internally while initializing or changing chips do not scroll.
const el = document.getElementById(DEPLOYMENT_COMPONENT_ID);
if (el) el.scrollIntoView({ behavior: "smooth", block: "start" });
};
hydrate();
@@ -917,7 +1068,15 @@ export const Deployment = ({ config, benchmarks }) => {
useEffect(() => {
const target = "#" + new URLSearchParams(sel).toString();
if (window.location.hash !== target) {
window.history.replaceState(null, "", target);
const historyState =
window.history.state && typeof window.history.state === "object"
? window.history.state
: {};
window.history.replaceState(
{ ...historyState, [INTERNAL_HASH_STATE_KEY]: target },
"",
target
);
}
window.dispatchEvent(new CustomEvent("sglang-deploy-sel", { detail: sel }));
}, [sel]);
@@ -943,29 +1102,123 @@ export const Deployment = ({ config, benchmarks }) => {
const [benchConc, setBenchConc] = useState(null);
const [benchAcc, setBenchAcc] = useState(null);
const [benchCopied, setBenchCopied] = useState(null);
const [runMode, setRunMode] = useState("python"); // "python" | "docker"
const runModes = config.runModes || ["python", "docker"];
const [runMode, setRunMode] = useState(runModes[0]); // "python" | "docker"
useEffect(() => { if (modal === "env") setEnvDraft(env); }, [modal, env]);
// Live --mamba-full-memory-ratio from the ratio calculator (K3 pages):
// pool sizing is consolidated into this one flag, computed from the
// calculator's request length plus the current panel selection.
const [mambaRatio, setMambaRatio] = useState(null);
useEffect(() => {
// Deploy shows base flags only, so it takes the base-config ratio (the
// effective one belongs to the playground's composed command).
const onRatio = (e) =>
setMambaRatio((e.detail && (e.detail.baseRatio || e.detail.ratio)) || null);
window.addEventListener("sglang-k3-mamba-ratio", onRatio);
return () => window.removeEventListener("sglang-k3-mamba-ratio", onRatio);
}, []);
// ==== 5. Derived values ====
const s = makeStyles(isDark);
const cell = findCell(config.cells, sel);
const command = renderCommand(cell, sel, env, runMode);
// MTP hint: fire on the actual command (speculative decoding ON) — NOT on
// strategy=low-latency, since a low-latency cell may not enable MTP. SGLang
// resets --max-running-requests to 48 when spec is on and it's unset.
// Pin the calculator-computed ratio into the rendered command (before the
// host/port tail); cells themselves stay ratio-free.
const cellWithRatio = (() => {
if (!cell || !mambaRatio) return cell;
if (cell.flags.some((f) => f.startsWith("--mamba-full-memory-ratio"))) return cell;
const flags = [...cell.flags];
const line = `--mamba-full-memory-ratio ${mambaRatio}`;
const i = flags.findIndex((f) => f.startsWith("--host"));
if (i >= 0) flags.splice(i, 0, line);
else flags.push(line);
return { ...cell, flags };
})();
const command = renderCommand(cellWithRatio, sel, env, runMode);
// MTP hint on the EFFECTIVE flags — speculation arrives via the Spec Decode
// overlay, never the cell. SGLang resets --max-running-requests to 48 when
// spec is on and it's unset.
const effFlags = cell ? [...overlayStrip(cell.flags, sel), ...overlayFlags(sel)] : [];
const mtpHint =
!!cell &&
(cell.flags || []).some((f) => f.split(/[\s=]/)[0] === "--speculative-algorithm") &&
!(cell.flags || []).some((f) => f.split(/[\s=]/)[0] === "--max-running-requests");
effFlags.some((f) => f.split(/[\s=]/)[0] === "--speculative-algorithm") &&
!effFlags.some((f) => f.split(/[\s=]/)[0] === "--max-running-requests");
// cell.warn may embed [label](#anchor) links — rendered as scrollIntoView
// buttons, not hrefs, so the hash (which carries the selection) isn't overwritten.
const renderWarn = (text) => {
const out = [];
const re = /\[([^\]]+)\]\(#([^)]+)\)/g;
let last = 0;
for (let m; (m = re.exec(text)); last = m.index + m[0].length) {
if (m.index > last) out.push(text.slice(last, m.index));
const anchor = m[2];
out.push(
<button
key={m.index}
type="button"
onClick={() => {
const el = document.getElementById(anchor);
if (el) el.scrollIntoView({ behavior: "smooth", block: "start" });
}}
style={{
background: "transparent",
border: "none",
padding: 0,
color: isDark ? "#FDBA74" : "#C2410C",
cursor: "pointer",
font: "inherit",
fontWeight: 600,
textDecoration: "underline",
textUnderlineOffset: "2px",
}}
>
{m[1]}
</button>
);
}
if (last < text.length) out.push(text.slice(last));
return out;
};
const modelName = resolveModelName(sel);
const curlText = interpolate(config.curl || "", env, modelName);
const hwGroups = buildHardwareGroups();
const benchEntry = benchmarks ? findBenchmark(benchmarks, sel) : null;
const isEnabled = (dim, value) => isOptionAvailable(config.cells, sel, dim, value);
// Overlay dims have no cells to constrain them, so an option is selectable
// unless it says otherwise; only match dims get the grey-out scan.
const isOverlayDim = (dim) => overlayDimSpecs.some((d) => d.id === dim);
const findOption = (dim, value) => {
const spec = [...matchDimSpecs, ...overlayDimSpecs].find((d) => d.id === dim);
return spec && (spec.options || []).find((o) => o.id === value);
};
const isEnabled = (dim, value) => {
const opt = findOption(dim, value);
if (opt && optionDisabled(opt, sel)) return false;
return isOverlayDim(dim) || isOptionAvailable(config.cells, sel, dim, value);
};
// Switching a match dim can hide the option a dependent row currently holds
// (Strategy's option set differs per PD mode). Re-seat any overlay/match pick
// that just became invisible onto the first visible option of its row.
const reseatHiddenPicks = (next) => {
let out = next;
for (const spec of [...matchDimSpecs, ...overlayDimSpecs]) {
const opts = visibleOptions(spec, out).filter((o) => !optionDisabled(o, out));
if (!opts.length) continue;
if (!opts.some((o) => o.id === out[spec.id])) {
out = { ...out, [spec.id]: opts[0].id };
}
}
return out;
};
const handleSelect = (dim, value) => {
setSel((prev) => snapToValidCell(config.cells, prev, dim, value));
setSel((prev) =>
reseatHiddenPicks(
isOverlayDim(dim)
? { ...prev, [dim]: value }
: snapToValidCell(config.cells, prev, dim, value),
),
);
};
const handleCopy = () => {
@@ -1006,7 +1259,11 @@ export const Deployment = ({ config, benchmarks }) => {
...(checked ? s.checked : {}),
...(disabled ? s.disabled : {}),
}}
title={disabled ? "Not supported for current selection" : ""}
title={
disabled
? item.disableReason || "Not supported for current selection"
: ""
}
onClick={(e) => {
if (disabled) { e.preventDefault(); return; }
handleSelect(dim, item.id);
@@ -1035,7 +1292,11 @@ export const Deployment = ({ config, benchmarks }) => {
const maxHwCols = Math.max(...hwGroups.map((x) => x.items.length));
return (
<div style={s.container} className="not-prose">
<div
id={DEPLOYMENT_COMPONENT_ID}
style={{ ...s.container, scrollMarginTop: "104px" }}
className="not-prose"
>
{/* Hardware section (2 vendor rows in one card, equal-width grid) */}
<div style={s.cardColumn}>
<div style={{ ...s.title, marginBottom: "2px" }}>Hardware Platform</div>
@@ -1052,54 +1313,69 @@ export const Deployment = ({ config, benchmarks }) => {
))}
</div>
{renderFlatSection("Model Variant", config.variants, "variant", sel.variant)}
{renderFlatSection("Quantization", config.quantizations, "quant", sel.quant)}
{renderFlatSection("Strategy", config.strategies, "strategy", sel.strategy)}
{renderFlatSection("Nodes", config.nodesOptions, "nodes", sel.nodes)}
{matchDimSpecs
.filter((d) => rowVisible(d, sel))
.map((d) => (
<div key={d.id}>
{renderFlatSection(d.title, visibleOptions(d, sel), d.id, sel[d.id])}
</div>
))}
{overlayDimSpecs
.filter((d) => rowVisible(d, sel))
.map((d) => (
<div key={d.id}>
{renderFlatSection(d.title, visibleOptions(d, sel), d.id, sel[d.id])}
</div>
))}
{/* Command box */}
<div style={s.card}>
<div style={s.title}>Run this Command:</div>
<div style={s.title}>Command:</div>
<div style={s.commandWrap}>
<div style={s.commandHeader}>
<div style={s.headerLeft}>
<div style={s.badge(Boolean(cell && cell.verified))}>
<span style={s.badgeDot(Boolean(cell && cell.verified))} />
{cell && cell.verified ? "Verified" : "Not Verified"}
{cell && cell.redirect ? (
cell.warn && <div style={s.mtpWarn}> {renderWarn(cell.warn)}</div>
) : (<>
<div style={s.commandHeader}>
<div style={s.headerLeft}>
<div style={s.badge(Boolean(cell && cell.verified))}>
<span style={s.badgeDot(Boolean(cell && cell.verified))} />
{cell && cell.verified ? "Verified" : "Not Verified"}
</div>
<div style={s.runModeWrap} role="tablist" aria-label="Output format">
{runModes.map((mode, index) => (
<span
key={mode}
style={{
...(index === runModes.length - 1
? s.runModeChipLast(runMode === mode)
: s.runModeChip(runMode === mode)),
...(runModes.length === 1 ? { borderRadius: 7 } : {}),
}}
onClick={() => setRunMode(mode)}
role="tab"
aria-selected={runMode === mode}
>
{mode === "docker" ? "Docker" : "Python"}
</span>
))}
</div>
</div>
<div style={s.runModeWrap} role="tablist" aria-label="Output format">
<span
style={s.runModeChip(runMode === "python")}
onClick={() => setRunMode("python")}
role="tab"
aria-selected={runMode === "python"}
>
Python
</span>
<span
style={s.runModeChipLast(runMode === "docker")}
onClick={() => setRunMode("docker")}
role="tab"
aria-selected={runMode === "docker"}
>
Docker
</span>
<div style={s.iconRow}>
<button style={s.iconButton} onClick={handleCopy}>
{copied ? "✓ Copied" : "⧉ Copy"}
</button>
<button style={s.iconButton} onClick={() => setModal("curl")}>$ cURL</button>
<button style={s.iconButton} onClick={() => setModal("env")}> Env</button>
</div>
</div>
<div style={s.iconRow}>
<button style={s.iconButton} onClick={handleCopy}>
{copied ? "✓ Copied" : "⧉ Copy"}
</button>
<button style={s.iconButton} onClick={() => setModal("curl")}>$ cURL</button>
<button style={s.iconButton} onClick={() => setModal("env")}> Env</button>
</div>
</div>
<pre style={s.commandPre}>{command}</pre>
{mtpHint && (
<div style={s.mtpWarn}>
Speculative decoding (MTP) is on SGLang resets <code>--max-running-requests</code> to <strong>48</strong> when it isn't set. Add <code>--max-running-requests &lt;N&gt;</code> sized for your target concurrency.
</div>
)}
<pre style={s.commandPre}>{command}</pre>
{cell && cell.warn && <div style={s.mtpWarn}> {renderWarn(cell.warn)}</div>}
{mtpHint && (
<div style={s.mtpWarn}>
Speculative decoding (MTP) is on SGLang resets <code>--max-running-requests</code> to <strong>48</strong> when it isn't set. Add <code>--max-running-requests &lt;N&gt;</code> sized for your target concurrency.
</div>
)}
</>)}
</div>
</div>
@@ -0,0 +1,331 @@
// Kimi-K3-only calculator, live-coupled to the Deploy panel: every parameter
// except the average request length is derived from the effective config the
// Playground broadcasts (base cell + Deploy overlays + Playground overrides),
// and the computed --mamba-full-memory-ratio is broadcast back for the Deploy
// panel to pin into its command.
export const KimiK3MambaRatioCalculator = () => {
const [isDark, setIsDark] = useState(false);
const [requestLength, setRequestLength] = useState("11264");
const [copied, setCopied] = useState(false);
// Effective serving config; empty until the Playground's first broadcast
// (the parse below then falls back to the stock defaults: tp8, bf16 KV,
// fp32 state, extra_buffer, NOSPEC, no DCP).
const [cfg, setCfg] = useState({ flags: [], env: [], baseFlags: [], baseEnv: [] });
useEffect(() => {
const checkTheme = () => {
const html = document.documentElement;
setIsDark(
html.classList.contains("dark") ||
html.getAttribute("data-theme") === "dark" ||
html.style.colorScheme === "dark"
);
};
checkTheme();
const observer = new MutationObserver(checkTheme);
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class", "data-theme", "style"],
});
return () => observer.disconnect();
}, []);
useEffect(() => {
const onCfg = (e) =>
setCfg({
flags: (e.detail && e.detail.flags) || [],
env: (e.detail && e.detail.env) || [],
baseFlags: (e.detail && e.detail.baseFlags) || [],
baseEnv: (e.detail && e.detail.baseEnv) || [],
});
window.addEventListener("sglang-k3-effective-config", onCfg);
return () => window.removeEventListener("sglang-k3-effective-config", onCfg);
}, []);
const length = Number.parseFloat(requestLength);
// Derive the serving parameters from a flag/env list and evaluate the balance
// formula (measured dual-pool balance), written as a per-request cost ratio:
//
// r = (S + D) x state_bytes / (L x per_token_kv_bytes)
//
// The state side (S main slots plus D verify intermediates) is per-GPU and
// never DCP-sharded. The KV side is per-GPU per logical token: DCP shards the
// MLA latent KV across its ranks, while the DSPARK draft model's own KV is
// replicated on every rank, so it stays a flat term. Without DCP the draft
// term is ~10% noise; once DCP shards MLA it is the same order as the MLA
// share, which is why it cannot be folded into a plain x dcp factor.
const derive = (flags, env) => {
const flagArg = (name) => {
for (const f of flags) {
const parts = f.split(/\s+/);
if (parts[0] === name) return parts[1];
}
return null;
};
const hasFlag = (name) => flags.some((f) => f.split(/[\s=]/)[0] === name);
const tp = Number(flagArg("--tp-size")) || 8;
const dp = hasFlag("--enable-dp-attention") ? Number(flagArg("--dp-size")) || 1 : 1;
// KDA state and the replicated MLA KV both live per attention-TP group.
// PP needs no term: it splits the layers of both pools equally.
const attnTp = Math.max(1, Math.round(tp / dp));
const dcp = Number(flagArg("--dcp-size")) || 1;
const kvDtype = flagArg("--kv-cache-dtype") === "fp8_e4m3" ? "fp8_e4m3" : "bfloat16";
const specOn = hasFlag("--speculative-algorithm");
const replaySpec = hasFlag("--enable-linear-replayssm-spec");
// ReplaySSM removes the D intermediate states but does NOT pin the state
// dtype: an unset --mamba-ssm-dtype defaults to fp32 either way, and an
// explicit 16-bit state is accepted (warned for drift), so read the flag.
const ssmFlag = flagArg("--mamba-ssm-dtype");
const ssmDtype =
ssmFlag === "bfloat16" || ssmFlag === "float16" ? ssmFlag : "float32";
const radixOff = hasFlag("--disable-radix-cache");
// "auto" (and anything unrecognized) resolves to extra_buffer.
const strategyFlag = flagArg("--mamba-radix-cache-strategy");
const strategy =
strategyFlag === "no_buffer" || strategyFlag === "extra_buffer_lazy"
? strategyFlag
: "extra_buffer";
const skipLock = env.some((e) => e.startsWith("SGLANG_OPT_MAMBA_SKIP_DECODE_LOCK=1"));
const pdRole = flagArg("--disaggregation-mode");
// Pipeline parallelism is incompatible with the overlap scheduler, so pp > 1
// turns it off for you — and the track buffer then costs one slot, not two.
const overlapOff =
hasFlag("--disable-overlap-schedule") || (Number(flagArg("--pp-size")) || 1) > 1;
// Mirrors kv_cache_configurator._calculate_mamba_ratio: base 3, minus 1 under
// the decode-lock skip, plus the ping-pong track buffer (2 under the overlap
// scheduler, 1 for lazy or without overlap). no_buffer has no track buffer and
// adds the skip's drop back, so it stays 3; a disabled radix cache is 1.
// A PD decode server runs a chunk cache: one live slot per request, and the
// radix-strategy knobs are inert there.
const slots = pdRole === "decode"
? 1
: radixOff
? 1
: strategy === "no_buffer"
? 3
: 3 -
(skipLock ? 1 : 0) +
(overlapOff || strategy === "extra_buffer_lazy" ? 1 : 2);
const block = Number(flagArg("--speculative-dspark-block-size")) || 7;
const drafts = specOn && !replaySpec && pdRole !== "prefill" ? block + 1 : 0;
const ssmBytes = ssmDtype === "float32" ? 4 : 2;
const kvBytes = kvDtype === "fp8_e4m3" ? 1 : 2;
// Fixed K3 geometry:
// KDA: 69 layers, 96 heads, head_dim 128, conv kernel 4 (conv state always bf16).
// MLA: 24 layers, kv_lora_rank 512, qk_rope_head_dim 64.
const stateBytesPerSlot =
69 * ((96 / attnTp) * 128 * 128 * ssmBytes + 3 * 3 * (96 / attnTp) * 128 * 2);
const kvBytesPerToken = 24 * (512 + 64) * kvBytes;
// DCP shards the MLA latent KV across its ranks; the DSPARK draft model's KV
// is replicated on every rank (~1.4 KB/token on trtllm_mha), so it does not
// shard and is added flat.
const draftKvBytesPerToken = specOn ? 1400 : 0;
const kvBytesPerTokenPerRank = kvBytesPerToken / dcp + draftKvBytesPerToken;
const ratio =
((slots + drafts) * stateBytesPerSlot) / (kvBytesPerTokenPerRank * length);
return { ratio, tp, dp, attnTp, dcp, kvDtype, ssmDtype, radixOff, strategy, skipLock, slots, specOn, replaySpec, block, pdRole };
};
// Two evaluations: `eff` matches the Playground's composed command, `bs`
// matches the Deploy command (cell + overlays only).
const eff = derive(cfg.flags, cfg.env);
const bs = derive(cfg.baseFlags.length ? cfg.baseFlags : cfg.flags,
cfg.baseFlags.length ? cfg.baseEnv : cfg.env);
const { ratio, tp, dp, attnTp, dcp, kvDtype, ssmDtype, radixOff, strategy, skipLock, slots, specOn, replaySpec, block, pdRole } = eff;
const valid = Number.isFinite(ratio) && ratio > 0 && length > 0 && 96 % attnTp === 0;
const baseValid = Number.isFinite(bs.ratio) && bs.ratio > 0 && length > 0;
const formatRatio = (value) => {
if (!Number.isFinite(value)) return "—";
if (value >= 10) return value.toFixed(1).replace(/\.0$/, "");
if (value >= 1) return value.toFixed(2).replace(/\.?0+$/, "");
return Number(value.toPrecision(2)).toString();
};
const result = valid ? formatRatio(ratio) : "—";
const baseResult = baseValid ? formatRatio(bs.ratio) : "—";
const cliFlag = valid ? `--mamba-full-memory-ratio ${result}` : "";
// Broadcast both results: the Deploy command takes the base-config value,
// the Playground's composed command takes the effective one.
useEffect(() => {
window.dispatchEvent(
new CustomEvent("sglang-k3-mamba-ratio", {
detail: {
ratio: valid ? result : null,
baseRatio: baseValid ? baseResult : null,
},
})
);
}, [result, valid, baseResult, baseValid]);
const copyFlag = () => {
if (!cliFlag || typeof navigator === "undefined" || !navigator.clipboard) return;
navigator.clipboard.writeText(cliFlag);
setCopied(true);
window.setTimeout(() => setCopied(false), 1600);
};
const colors = {
border: isDark ? "#374151" : "#e5e7eb",
panel: isDark ? "#1f2937" : "#ffffff",
input: isDark ? "#111827" : "#f8fafc",
text: isDark ? "#e5e7eb" : "#1f2937",
muted: isDark ? "#9ca3af" : "#64748b",
accent: isDark ? "#E85D4D" : "#D45D44",
error: isDark ? "#fca5a5" : "#b91c1c",
};
const inputStyle = {
width: "100%",
boxSizing: "border-box",
padding: "8px 10px",
border: `1px solid ${colors.border}`,
borderRadius: "5px",
background: colors.input,
color: colors.text,
fontSize: "13px",
};
const labelStyle = {
display: "flex",
flexDirection: "column",
gap: "5px",
fontSize: "12px",
fontWeight: 600,
};
const chipStyle = {
padding: "3px 9px",
border: `1px solid ${colors.border}`,
borderRadius: "999px",
background: colors.input,
color: colors.text,
fontSize: "12px",
whiteSpace: "nowrap",
};
const specLabel = !specOn
? "NOSPEC"
: replaySpec
? "DSPARK + ReplaySSM (D folded)"
: `DSPARK (D = ${block + 1})`;
const derivedChips = [
// With DP attention on, show the whole topology so a large-scale preset is
// visibly understood: total GPUs = DP replicas x attention-TP group width.
dp > 1
? `${tp} GPUs = DP ${dp} × Attention TP ${attnTp}`
: `Attention TP ${attnTp}`,
`DCP ${dcp}`,
`KV ${kvDtype === "fp8_e4m3" ? "FP8" : "BF16"}`,
`State ${ssmDtype === "float32" ? "FP32" : ssmDtype === "bfloat16" ? "BF16" : "FP16"}`,
pdRole === "decode"
? "PD decode: chunk cache (S = 1)"
: radixOff
? "Radix off (S = 1)"
: `${strategy}${skipLock ? " + slot saving" : ""} (S = ${slots})`,
pdRole === "prefill" ? "PD prefill (no verify states)" : null,
specLabel,
].filter(Boolean);
return (
<div
className="not-prose"
style={{
display: "grid",
gap: "12px",
padding: "14px",
border: `1px solid ${colors.border}`,
borderRadius: "8px",
background: colors.panel,
color: colors.text,
}}
>
<div
style={{
display: "grid",
gridTemplateColumns: "minmax(180px, 260px) 1fr",
gap: "14px",
alignItems: "start",
}}
>
<label htmlFor="k3-ratio-length" style={labelStyle}>
Average request length
<input
id="k3-ratio-length"
type="number"
min="1"
step="1"
value={requestLength}
onChange={(event) => setRequestLength(event.target.value)}
style={inputStyle}
/>
<span style={{ color: colors.muted, fontSize: "11px", fontWeight: 400 }}>
Input + output tokens the only free parameter
</span>
</label>
<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
<span style={{ fontSize: "12px", fontWeight: 600 }}>
Serving configuration (follows the Deploy panel and Playground)
</span>
<div style={{ display: "flex", flexWrap: "wrap", gap: "6px" }}>
{derivedChips.map((c) => (
<span key={c} style={chipStyle}>{c}</span>
))}
</div>
</div>
</div>
{!valid ? (
<div style={{ color: colors.error, fontSize: "12px" }}>
Enter a valid request length.
</div>
) : (
<div
style={{
display: "flex",
alignItems: "center",
gap: "12px",
paddingTop: "12px",
borderTop: `1px solid ${colors.border}`,
flexWrap: "wrap",
}}
>
<div>
<div style={{ color: colors.muted, fontSize: "11px" }}>
Balanced ratio pinned into the commands above
</div>
<div style={{ fontSize: "26px", fontWeight: 700 }}>{result}</div>
{baseResult !== result ? (
<div style={{ color: colors.muted, fontSize: "11px" }}>
Deploy command (without Playground overrides): {baseResult}
</div>
) : null}
</div>
<code style={{ flex: 1, minWidth: "240px", color: colors.text }}>
{cliFlag}
</code>
<button
type="button"
onClick={copyFlag}
style={{
padding: "7px 11px",
border: 0,
borderRadius: "5px",
background: colors.accent,
color: "#ffffff",
fontSize: "12px",
fontWeight: 600,
cursor: "pointer",
}}
>
{copied ? "Copied" : "Copy flag"}
</button>
</div>
)}
</div>
);
};
+350 -181
View File
@@ -10,6 +10,17 @@
// moe — backend (+ MegaMoE quantization sub-select) + EP
// parsers — per-item toggle flags
// speculative — single-select preset
//
// Axis-level `showWhen(base)` (any axis): the card is not rendered when the Deploy
// panel has not switched that feature on. `base` carries the cell match dims plus
// the Deploy panel's overlay dims, so an axis can gate on either.
// flagSelects extras: `control: "slider"` renders the option list as a range input
// (option order is the scale), and a per-select `showWhen(base)` gates one row.
// A row may set `default` (initial pick instead of inherit) and options may carry
// `env` (stripped/re-added like the MoE backend card) or `flags` as a FUNCTION
// `(rowValues, base) => [...]` for cross-row composition (e.g. preset x cluster
// size); a function returning null means "leave the base untouched".
// Changing the Deploy selection resets every axis back to inherit-from-base.
// pdDisagg — role + transfer backend + IB device + optional router
// hicache — enable + backend + write policy
// hisparse — enable + host ratio (decode-only)
@@ -17,7 +28,7 @@
// its own title + strip-prefixes + options (no per-feature code)
//
// Adding an axis = one entry in AXIS_HANDLERS below; nothing else switches on
// an axis id. Each handler implements initState / revertHidden / apply /
// an axis id. Each handler implements initState / apply /
// render, plus optional deriveFromBase (recover state from base cell flags)
// and getRenderHints.
//
@@ -34,7 +45,60 @@ export const Playground = ({ config }) => {
// ==========================================================================
// 1. Constants
// ==========================================================================
const DIMENSIONS = ["hw", "variant", "quant", "strategy", "nodes"];
const DIMENSIONS = ["hw", ...((config.matchDims
|| [{ id: "variant" }, { id: "quant" }, { id: "strategy" }, { id: "nodes" }])
.map((d) => d.id))];
// The Deploy panel layers its overlay dims (speculation, hicache, ...) on top of
// the matched cell, so the playground's "base" is cell + overlay — otherwise the
// diff shows overlay flags as playground additions and deriveFromBase can't see
// what Deploy already resolved.
//
// ==== MIRROR in _deployment.jsx — keep the two copies identical ====
// Snippets cannot import each other, so the overlay-resolution rule is written
// twice. A divergence makes the Deploy command and the playground base disagree,
// which shows up as phantom +/- lines in the diff and no error anywhere.
// Guarded by docs_new/scripts/check_cookbook_configs.mjs.
const optionVisible = (opt, sel) =>
typeof opt.showWhen !== "function" || opt.showWhen(sel);
const optionDisabled = (opt, sel) =>
typeof opt.disabled === "function" ? opt.disabled(sel) : !!opt.disabled;
const visibleOptions = (spec, sel) =>
(spec.options || []).filter((o) => optionVisible(o, sel));
const rowVisible = (spec, sel) =>
(typeof spec.showWhen !== "function" || spec.showWhen(sel)) &&
visibleOptions(spec, sel).length > 0;
const overlayPick = (sel) => {
const picked = [];
for (const spec of (config.overlayDims || [])) {
if (!rowVisible(spec, sel)) continue;
const opt = (spec.options || []).find((o) => o.id === sel[spec.id]);
if (opt && !optionDisabled(opt, sel)) picked.push(opt);
}
return picked;
};
const overlayPart = (sel, key) => {
const out = [];
for (const opt of overlayPick(sel)) {
const add = typeof opt[key] === "function" ? opt[key](sel) : opt[key];
if (add) out.push(...add);
}
return out;
};
// An overlay option may also REMOVE cell flags, declared as `stripPrefixes`
// (a static list, or a function of the selection). L3 uses it to drop the
// whole DCP operating point, which the server rejects with an L3 backend.
const overlayStrip = (cellFlags, sel) => {
const strip = overlayPart(sel, "stripPrefixes");
if (!strip.length) return [...(cellFlags || [])];
return (cellFlags || []).filter((f) => !strip.includes(f.split(/[\s=]/)[0]));
};
// ==== end MIRROR ====
const withOverlay = (cell, sel) => (cell && {
...cell,
flags: [...overlayStrip(cell.flags, sel), ...overlayPart(sel, "flags")],
env: [...(cell.env || []), ...overlayPart(sel, "env")],
}) || cell;
// Shared with `_deployment.jsx` (HOST/PORT/etc. unified across the page).
const STORAGE_KEY = "sglang-deploy-env";
@@ -56,9 +120,11 @@ export const Playground = ({ config }) => {
cells.find((c) => DIMENSIONS.every((d) => c.match[d] === sel[d]));
// After applying overrides, the resulting (env, flags) may equal another
// cell sharing the same (hw, variant, quant, nodes) but a different
// strategy. flags compared ordered; env compared as a set.
// cell sharing every match dim except strategy. Keep this generic so custom
// grids (for example K3's PD mode) cannot match across unrelated base cells.
// flags compared ordered; env compared as a set.
const findMatchingCell = (cells, sel, pgEnv, pgFlags) => {
const fixedDims = DIMENSIONS.filter((d) => d !== "strategy");
const flagsEq = (a, b) =>
a.length === b.length && a.every((x, i) => x === b[i]);
const envEq = (a, b) => {
@@ -68,10 +134,7 @@ export const Playground = ({ config }) => {
return true;
};
for (const c of cells) {
if (c.match.hw !== sel.hw) continue;
if (c.match.variant !== sel.variant) continue;
if (c.match.quant !== sel.quant) continue;
if (c.match.nodes !== sel.nodes) continue;
if (fixedDims.some((d) => c.match[d] !== sel[d])) continue;
if (flagsEq(c.flags || [], pgFlags || []) && envEq(c.env || [], pgEnv || [])) {
return c;
}
@@ -236,20 +299,37 @@ export const Playground = ({ config }) => {
return null;
};
// --tp / --ep spelling families: configs write either the canonical
// --tp-size / --ep-size or the short --tp / --ep. Parse and strip every
// spelling; when re-emitting, keep the spelling the base already uses.
const TP_HEADS = ["--tp-size", "--tp", "--tensor-parallel-size"];
const EP_HEADS = ["--ep-size", "--ep", "--expert-parallel-size"];
const parseIntFlagAny = (flags, heads) => {
for (const head of heads) {
const n = parseIntFlag(flags, head);
if (n !== null) return n;
}
return null;
};
const flagSpelling = (flags, heads, fallback) =>
heads.find((head) =>
(flags || []).some((f) => f.split(/[\s=]/)[0] === head)) || fallback;
// Insertion-anchor sets (priority-ordered; each includes siblings so
// insertion still works in partial cells).
const ANCHOR_NEAR_MODEL_PATH = ["--model-path"];
const ANCHOR_NEAR_TP = ["--tp", "--model-path"];
const ANCHOR_NEAR_DP = ["--dp", "--tp", "--model-path"];
const ANCHOR_NEAR_DPATTN = ["--enable-dp-attention", "--dp", "--tp", "--model-path"];
const ANCHOR_NEAR_TP = ["--tp-size", "--tp", "--model-path"];
const ANCHOR_NEAR_DP = ["--dp", "--tp-size", "--tp", "--model-path"];
const ANCHOR_NEAR_DPATTN = ["--enable-dp-attention", "--dp", "--tp-size", "--tp", "--model-path"];
const ANCHOR_NEAR_MOE = ["--moe-a2a-backend", "--moe-runner-backend",
"--enable-dp-attention", "--dp", "--tp", "--model-path"];
"--enable-dp-attention", "--dp", "--tp-size", "--tp", "--model-path"];
// Helper bundle passed to every axis handler.
const helpers = {
matchConstraint, evaluateChip, findEntry, isHidden,
stripFlagsByFirstToken, stripEnvByPrefix, insertBeforeTail, insertAfter,
parseIntFlag, hasFlag, findFlagArg,
TP_HEADS, EP_HEADS, parseIntFlagAny, flagSpelling,
ANCHOR_NEAR_MODEL_PATH, ANCHOR_NEAR_TP, ANCHOR_NEAR_DP,
ANCHOR_NEAR_DPATTN, ANCHOR_NEAR_MOE,
};
@@ -288,7 +368,7 @@ export const Playground = ({ config }) => {
// ==========================================================================
// 5. AXIS_HANDLERS — the built-in playground axis registry
// ==========================================================================
// Each entry implements initState / revertHidden / apply / render (plus
// Each entry implements initState / apply / render (plus
// optional deriveFromBase / getRenderHints). Iterated in insertion order;
// axes absent from config.playgroundFeatures are skipped.
// - inherit-from-base sentinels: null / "current" / "auto" / "off" /
@@ -322,26 +402,13 @@ export const Playground = ({ config }) => {
else dpAttn = false;
const cpSize = h.parseIntFlag(flags, "--attn-cp-size");
return {
tp: h.parseIntFlag(flags, "--tp"),
tp: h.parseIntFlagAny(flags, h.TP_HEADS),
cp: cpEnabledIn(flags) ? (cpSize !== null ? cpSize : 2) : null,
cpStrategy: bakedCpStrategy(flags),
dpAttn,
};
},
revertHidden: (value, fc, base, h) => {
let changed = false;
const next = { ...value };
for (const knob of (fc.knobs || [])) {
const cur = next[knob.id];
if (cur !== null && cur !== undefined
&& h.isHidden(knob.values, cur, base)) {
next[knob.id] = null; changed = true;
}
}
return changed ? next : value;
},
apply: ({ flags, env, value, fc, sel, h }) => {
// Live facts for constraint checks (same keys the render-side
// constraintBase exposes), recomputed after each mutation.
@@ -351,7 +418,7 @@ export const Playground = ({ config }) => {
dpAttnOn: h.hasFlag(flags, "--enable-dp-attention"),
cpOn: cpEnabledIn(flags),
cpStrategy: bakedCpStrategy(flags) || "interleave",
effTp: h.parseIntFlag(flags, "--tp"),
effTp: h.parseIntFlagAny(flags, h.TP_HEADS),
});
// The runtime derives the prefill-CP size as attn_cp_size = tp/dp
// (a mismatched --attn-cp-size is overridden), so with DP-Attention
@@ -366,7 +433,7 @@ export const Playground = ({ config }) => {
: (h.hasFlag(flags, "--enable-dp-attention")
? (h.parseIntFlag(flags, "--dp") ?? 1) : false);
if (typeof dpIntent === "number" && dpIntent > 1) return null;
return h.parseIntFlag(flags, "--tp");
return h.parseIntFlagAny(flags, h.TP_HEADS);
};
// Skip a knob whose entry or picked value is hidden/disabled under
// the live facts — mirrors the grayed controls, so stale state never
@@ -389,8 +456,9 @@ export const Playground = ({ config }) => {
// below the command box) rather than banned.
if (value.tp !== null && !blocked("tp", value.tp)) {
flags = h.stripFlagsByFirstToken(flags, ["--tp"]);
flags = h.insertAfter(flags, h.ANCHOR_NEAR_MODEL_PATH, [`--tp ${value.tp}`]);
const tpHead = h.flagSpelling(flags, h.TP_HEADS, "--tp");
flags = h.stripFlagsByFirstToken(flags, h.TP_HEADS);
flags = h.insertAfter(flags, h.ANCHOR_NEAR_MODEL_PATH, [`${tpHead} ${value.tp}`]);
}
// CP override: an explicit size pick, or a strategy-only pick on a
// base that already carries CP. Strategy precedence: explicit knob >
@@ -521,43 +589,29 @@ export const Playground = ({ config }) => {
(e) => e.startsWith("SGLANG_OPT_DEEPGEMM_MEGA_MOE_USE_FP4_ACTS"));
return {
backend: a2a || runner || null,
ep: h.parseIntFlag(flags, "--ep"),
ep: h.parseIntFlagAny(flags, h.EP_HEADS),
mmQuant: fp4Acts ? "w4a4" : "w4a8",
};
},
revertHidden: (value, fc, base, h) => {
let changed = false;
const next = { ...value };
if (next.backend !== null && fc.backend?.options
&& h.isHidden(fc.backend.options, next.backend, base)) {
next.backend = null; changed = true;
}
// MegaMoE backend availability — gated by its option's requiresHw /
// excludesStrategy (this model gates by hw only; the check is generic).
const mmOpt = (fc.backend?.options || []).find((o) => o.id === "megamoe");
const mmAvail = !!mmOpt
&& (!mmOpt.requiresHw || mmOpt.requiresHw.includes(base.hw))
&& (!mmOpt.excludesStrategy || !mmOpt.excludesStrategy.includes(base.strategy));
if (next.backend === "megamoe" && !mmAvail) {
next.backend = null; changed = true;
}
if (next.ep !== null && fc.ep?.values
&& h.isHidden(fc.ep.values, next.ep, base)) {
next.ep = null; changed = true;
}
return changed ? next : value;
},
apply: ({ flags, env, value, fc, h, derived }) => {
if (value.backend !== null) {
flags = h.stripFlagsByFirstToken(flags, [
"--moe-a2a-backend", "--moe-runner-backend",
]);
// Backend options may carry their own env (e.g. the FlashInfer MXFP4
// cubin-pool path): strip every backend option's env keys, then
// re-add the selected option's.
const backendEnvKeys = [];
for (const o of (fc.backend?.options || [])) {
for (const e of (o.env || [])) backendEnvKeys.push(e.split("=")[0]);
}
if (backendEnvKeys.length) env = h.stripEnvByPrefix(env, backendEnvKeys);
const opt = (fc.backend?.options || []).find((o) => o.id === value.backend);
if (opt?.flags?.length) {
flags = h.insertAfter(flags, h.ANCHOR_NEAR_DPATTN, opt.flags);
}
if (opt?.env?.length) env = [...env, ...opt.env];
}
// MegaMoE owns the MoE path: when the effective backend is megamoe, strip the
// DeepEP dispatch + any prior megamoe env, then re-add the selected quant's
@@ -582,9 +636,10 @@ export const Playground = ({ config }) => {
}
}
if (value.ep !== null) {
flags = h.stripFlagsByFirstToken(flags, ["--ep"]);
const epHead = h.flagSpelling(flags, h.EP_HEADS, "--ep");
flags = h.stripFlagsByFirstToken(flags, h.EP_HEADS);
if (value.ep > 1) {
flags = h.insertAfter(flags, h.ANCHOR_NEAR_MOE, [`--ep ${value.ep}`]);
flags = h.insertAfter(flags, h.ANCHOR_NEAR_MOE, [`${epHead} ${value.ep}`]);
}
}
return { flags, env };
@@ -665,18 +720,6 @@ export const Playground = ({ config }) => {
return out;
},
revertHidden: (value, fc, base, h) => {
let changed = false;
const next = { ...value };
for (const item of (fc.items || [])) {
if (next[item.id] !== null && next[item.id] !== undefined
&& h.evaluateChip(item, base).hidden) {
next[item.id] = null; changed = true;
}
}
return changed ? next : value;
},
apply: ({ flags, env, value, fc, h, derived }) => {
const items = fc.items || [];
// Effective state per item: explicit > derived > false. Skip
@@ -755,13 +798,6 @@ export const Playground = ({ config }) => {
return "current";
},
revertHidden: (value, fc, base, h) => {
if (value !== "current" && h.isHidden(fc.options || [], value, base)) {
return "current";
}
return value;
},
apply: ({ flags, env, value, fc, h, derived }) => {
if (value === "current") return { flags, env };
// No-op when the pick already matches base (preserves flag position).
@@ -815,39 +851,41 @@ export const Playground = ({ config }) => {
// Owns the `--disaggregation-*` flags (unconditional strip). A backend may
// carry hw-gated env (transferBackends[].env + .envWhen).
pdDisagg: {
initState: () => ({ mode: "off", transferBackend: "mooncake", ibDevice: "auto" }),
revertHidden: (value, fc, base, h) => {
let changed = false;
const next = { ...value };
if (next.mode !== "off" && fc.modes
&& h.isHidden(fc.modes, next.mode, base)) {
next.mode = "off"; changed = true;
}
if (next.ibDevice !== "auto" && fc.ibDevices
&& h.isHidden(fc.ibDevices, next.ibDevice, base)) {
next.ibDevice = "auto"; changed = true;
}
if (next.transferBackend !== "mooncake" && fc.transferBackends
&& h.isHidden(fc.transferBackends, next.transferBackend, base)) {
next.transferBackend = "mooncake"; changed = true;
}
return changed ? next : value;
},
// The transport default is the config's first entry, so a model whose
// recipes standardize on one backend does not silently start on another.
initState: (fc) => ({
mode: "off",
transferBackend: (fc && (fc.transferBackends || [])[0] || {}).id || "mooncake",
ibDevice: "auto",
}),
apply: ({ flags, env, value, sel, fc, h }) => {
// The bootstrap port is the base cell's to choose — the router's
// --prefill positional has to match it — so carry it across the strip
// rather than dropping it and silently falling back to the default.
const bootstrapPort = h.findFlagArg(flags, "--disaggregation-bootstrap-port");
flags = h.stripFlagsByFirstToken(flags, [
"--disaggregation-mode", "--disaggregation-transfer-backend",
"--disaggregation-ib-device", "--disaggregation-bootstrap-port",
]);
const backends = fc.transferBackends || [];
// A config that omits `modes` has the role on the Deploy panel instead;
// this card then only tunes the transport for whatever role is selected.
const mode = (fc.modes || []).length
? value.mode
: ((sel && sel.pdMode) || "off");
if (value.mode === "prefill" || value.mode === "decode") {
const backend = value.transferBackend || "mooncake";
if (mode === "prefill" || mode === "decode") {
const backend = value.transferBackend || (backends[0] || {}).id || "mooncake";
const adds = [
`--disaggregation-mode ${value.mode}`,
`--disaggregation-mode ${mode}`,
`--disaggregation-transfer-backend ${backend}`,
];
// Re-emitted in the base cell's own order, so an untouched Prefill
// recipe renders byte-identical to the Deploy panel.
if (bootstrapPort) {
adds.push(`--disaggregation-bootstrap-port ${bootstrapPort}`);
}
if (value.ibDevice && value.ibDevice !== "auto") {
adds.push(`--disaggregation-ib-device ${value.ibDevice}`);
}
@@ -859,7 +897,7 @@ export const Playground = ({ config }) => {
// Role-specific serving port so the router's prefill / decode targets
// line up (and prefill+decode don't collide on a single host).
const servePort = PD_PORTS[value.mode].serve;
const servePort = PD_PORTS[mode].serve;
flags = flags.map((f) =>
f.split(/[\s=]/)[0] === "--port" ? `--port ${servePort}` : f);
@@ -930,14 +968,6 @@ export const Playground = ({ config }) => {
hisparse: {
initState: (fc) => ({ enable: false, hostRatio: (fc && fc.defaultHostRatio) || null }),
revertHidden: (value, fc, base, h) => {
if (value.hostRatio !== null && fc.hostRatios
&& h.isHidden(fc.hostRatios, value.hostRatio, base)) {
return { ...value, hostRatio: (fc && fc.defaultHostRatio) || null };
}
return value;
},
apply: ({ flags, env, value, fc, h }) => {
const ownedHeads = [
"--enable-hisparse", "--hisparse-config",
@@ -969,10 +999,12 @@ export const Playground = ({ config }) => {
<div key={axisId} style={s.card}>
<div style={s.compactRow}>
<span style={s.axisTitle}>HiSparse</span>
<span style={s.field}>
{renderChip("Enable", value.enable, true,
() => setSlot("enable", !value.enable))}
</span>
{typeof fc.showWhen !== "function" && (
<span style={s.field}>
{renderChip("Enable", value.enable, true,
() => setSlot("enable", !value.enable))}
</span>
)}
{hasRatios && (
<span style={s.field}>
<span style={s.fieldLabel}>Host ratio</span>
@@ -992,22 +1024,23 @@ export const Playground = ({ config }) => {
hicache: {
initState: () => ({ enable: false, backend: null, writePolicy: "auto" }),
revertHidden: (value, fc, base, h) => {
let changed = false;
const next = { ...value };
if (next.backend !== null && fc.backends
&& h.isHidden(fc.backends, next.backend, base)) {
next.backend = null; changed = true;
}
if (next.writePolicy !== "auto" && fc.writePolicies
&& h.isHidden(fc.writePolicies, next.writePolicy, base)) {
next.writePolicy = "auto"; changed = true;
}
return changed ? next : value;
},
apply: ({ flags, env, value, fc, sel, h }) => {
if (fc.excludesHw && sel && fc.excludesHw.includes(sel.hw)) return { flags, env };
// When the Deploy panel owns enablement (`showWhen`), the base already
// carries a complete, verified hicache recipe. Rebuilding it from this
// axis's own defaults would silently swap ratio/layout/io-backend, so
// only the two knobs this card actually exposes are touched.
if (typeof fc.showWhen === "function") {
const set = (name, val) => {
flags = h.stripFlagsByFirstToken(flags, [name]);
if (val) flags = h.insertBeforeTail(flags, [`${name} ${val}`]);
};
if (value.backend) set("--hicache-storage-backend", value.backend);
if (value.writePolicy && value.writePolicy !== "auto") {
set("--hicache-write-policy", value.writePolicy);
}
return { flags, env };
}
flags = h.stripFlagsByFirstToken(flags, [
"--enable-hierarchical-cache", "--hicache-ratio", "--hicache-size",
"--hicache-write-policy", "--hicache-mem-layout", "--hicache-io-backend",
@@ -1066,10 +1099,12 @@ export const Playground = ({ config }) => {
<div key={axisId} style={s.card}>
<div style={s.compactRow}>
<span style={s.axisTitle}>HiCache</span>
<span style={s.field}>
{renderChip("Enable", value.enable, true,
() => setSlot("enable", !value.enable))}
</span>
{typeof fc.showWhen !== "function" && (
<span style={s.field}>
{renderChip("Enable", value.enable, true,
() => setSlot("enable", !value.enable))}
</span>
)}
{hasBackends && (
<span style={s.field}>
<span style={s.fieldLabel}>Storage</span>
@@ -1092,23 +1127,29 @@ export const Playground = ({ config }) => {
// ---- Axis: Flag Selects (generic, config-declared) ----------------------
// A LIST of single-selects, each declared entirely in config:
// { id, title, stripPrefixes: [...], options: [{ id, label, flags? }] }
// { id, title, stripPrefixes: [...], stripEnv?: [...],
// options: [{ id, label, flags?, env? }] }
// Same shape as `speculative` minus its hardcoded title + strip list: pick
// an option → strip the family, splice the option's flags. A flagless
// option is the "none" / accuracy-safe choice (matches a base carrying none
// of the family). Model-specific controls (KV-cache dtype, mamba scheduler
// strategy, …) live here as DATA — no per-feature engine code. Supports
// multiple selects per page. State: { [selectId]: optionId | null }
// (null = inherit base).
// (null = inherit base). `default` may be an option id or a function of the
// base selection (re-evaluated on every base change).
flagSelects: {
initState: (fc) => {
initState: (fc, base) => {
const out = {};
for (const spec of (fc || [])) out[spec.id] = null;
for (const spec of (fc || [])) {
const d = typeof spec.default === "function" ? spec.default(base) : spec.default;
out[spec.id] = d ?? null;
}
return out;
},
// Per select: match base's family flags (first token ∈ stripPrefixes)
// against each option's flags. A flagless option matches an empty family.
// Function-flag options (computed from the row values) never match.
deriveFromBase: (cell, fc) => {
const flags = (cell && cell.flags) || [];
const out = {};
@@ -1117,6 +1158,7 @@ export const Playground = ({ config }) => {
const fam = flags.filter((f) => prefixes.includes(f.split(/[\s=]/)[0]));
let hit = null;
for (const opt of (spec.options || [])) {
if (typeof opt.flags === "function") continue;
const of = opt.flags || [];
if (of.length === fam.length && of.every((x) => fam.includes(x))) {
hit = opt.id; break;
@@ -1127,19 +1169,6 @@ export const Playground = ({ config }) => {
return out;
},
revertHidden: (value, fc, base, h) => {
let changed = false;
const next = { ...value };
for (const spec of (fc || [])) {
const cur = next[spec.id];
if (cur !== null && cur !== undefined
&& h.isHidden(spec.options, cur, base)) {
next[spec.id] = null; changed = true;
}
}
return changed ? next : value;
},
apply: ({ flags, env, value, fc, sel, h, derived }) => {
const evalBase = {
...(sel || {}),
@@ -1147,6 +1176,9 @@ export const Playground = ({ config }) => {
pdMode: h.findFlagArg(flags, "--disaggregation-mode") || "off",
};
for (const spec of (fc || [])) {
// Hidden rows must not emit: showWhen also receives the sibling row
// values (explicit picks + derived), so a row can gate on another's pick.
if (typeof spec.showWhen === "function" && !spec.showWhen(sel, value, derived)) continue;
const v = value ? value[spec.id] : null;
if (v === null || v === undefined) continue; // inherit base
const d = derived ? derived[spec.id] : null;
@@ -1154,10 +1186,51 @@ export const Playground = ({ config }) => {
const opt = (spec.options || []).find((o) => o.id === v);
if (!opt) continue;
if (h.evaluateChip(opt, evalBase).disabled) continue;
flags = h.stripFlagsByFirstToken(flags, spec.stripPrefixes || []);
if (opt.flags && opt.flags.length) {
flags = h.insertBeforeTail(flags, opt.flags);
// `flags` may be a function of the whole row-value object (cross-row
// presets, e.g. preset x cluster-size). A function returning null
// means "leave the base untouched" (a true no-op Off).
const optFlags = typeof opt.flags === "function"
? opt.flags(value, evalBase)
: (opt.flags || []);
if (optFlags === null) continue;
// In-place substitution keeps the rendered diff minimal: a family the
// base already carries changes value at its original position; families
// stripped and not re-emitted vanish in place; only net-new flags
// append as a block before the tail.
const strip = new Set(spec.stripPrefixes || []);
const byTok = new Map();
for (const f of optFlags) {
const t = f.split(/[\s=]/)[0];
if (!byTok.has(t)) byTok.set(t, []);
byTok.get(t).push(f);
}
const consumed = new Set();
const next = [];
for (const f of flags) {
const t = f.split(/[\s=]/)[0];
if (byTok.has(t)) {
if (!consumed.has(t)) {
next.push(...byTok.get(t));
consumed.add(t);
}
} else if (!strip.has(t)) {
next.push(f);
}
}
const fresh = [];
for (const [t, fs] of byTok) {
if (!consumed.has(t)) fresh.push(...fs);
}
flags = fresh.length ? h.insertBeforeTail(next, fresh) : next;
// Option env (env-var toggles, preset env): strip spec.stripEnv plus
// every option's env keys, then add the picked option's — mirrors the
// MoE backend card's strip-then-emit shape.
const envKeys = [...(spec.stripEnv || [])];
for (const o of (spec.options || [])) {
for (const e of (o.env || [])) envKeys.push(e.split("=")[0]);
}
if (envKeys.length) env = h.stripEnvByPrefix(env, envKeys);
if (opt.env && opt.env.length) env = [...env, ...opt.env];
}
return { flags, env };
},
@@ -1165,6 +1238,10 @@ export const Playground = ({ config }) => {
render: ({ axisId, value, setValue, fc, base, s, h, renderChip, derived }) => {
const cards = [];
for (const spec of (fc || [])) {
// Row-level gate: a select whose whole family is meaningless under the
// current base (e.g. draft tokens with speculation off) is not rendered.
// Also receives (rowValues, derived) for sibling-dependent rows.
if (typeof spec.showWhen === "function" && !spec.showWhen(base, value, derived)) continue;
const opts = (spec.options || [])
.map((o) => h.evaluateChip(o, base))
.filter((c) => !c.hidden);
@@ -1172,6 +1249,34 @@ export const Playground = ({ config }) => {
const explicit = value ? value[spec.id] : null;
const display = (explicit !== null && explicit !== undefined)
? explicit : (derived ? derived[spec.id] : null);
// `control: "slider"` renders the same option list as a range input —
// for dense ordered scales (1..7) where chips are just noise. Option
// ORDER is the scale; the option id is still what apply() consumes.
if (spec.control === "slider") {
const idx = Math.max(0, opts.findIndex((c) => c.value === display));
const cur = opts[idx];
cards.push(
<div key={`${axisId}-${spec.id}`} style={s.card}>
<div style={s.compactRow}>
<span style={s.axisTitle}>{spec.title}</span>
<input
type="range"
min={0} max={opts.length - 1} step={1}
value={idx}
onChange={(e) => setValue({
...value,
[spec.id]: opts[Number(e.target.value)].value,
})}
style={{ flex: 1, minWidth: "120px", accentColor: "#D45D44" }}
/>
<span style={{ ...s.axisTitle, minWidth: "24px", textAlign: "right" }}>
{cur ? cur.label : "-"}
</span>
</div>
</div>
);
continue;
}
cards.push(
<div key={`${axisId}-${spec.id}`} style={s.card}>
<div style={s.compactRow}>
@@ -1224,13 +1329,21 @@ export const Playground = ({ config }) => {
// callers can pass a modified env (e.g. MegaMoE's stripEnv + append).
const renderCommandLines = (cell, flags, cellEnv, sel, envValues, pdMode = null, mode = "python") => {
const modelName = resolveModelName(sel);
const nnodes = parseNnodes(sel.nodes);
const multinode = nnodes > 1;
let f = [...flags];
// Presets may replace the base cell's topology by emitting --nnodes
// directly (for example, B300 1-node -> large-scale 4/8-node). Use that
// effective value for Docker networking, hints, and the command banner.
const nnodesFlag = f.find((x) => x.split(/[\s=]/)[0] === "--nnodes");
const nnodesMatch = nnodesFlag && /^--nnodes(?:\s+|=)(\d+)$/.exec(nnodesFlag.trim());
const baseNnodes = sel.nodes !== undefined
? parseNnodes(sel.nodes)
: ((cell && cell.nnodes) || 1);
const nnodes = nnodesMatch ? parseInt(nnodesMatch[1], 10) : baseNnodes;
const multinode = nnodes > 1;
if (multinode && !f.some((x) => x.startsWith("--nnodes"))) {
// Insert the multi-node trio after the last parallelism flag (matches
// _deployment.jsx so untouched-base output is byte-identical).
const PARALLELISM_ANCHORS = ["--enable-dp-attention", "--dp", "--tp"];
const PARALLELISM_ANCHORS = ["--enable-dp-attention", "--dp", "--tp-size", "--tp"];
let at = -1;
for (const anchor of PARALLELISM_ANCHORS) {
at = f.findIndex((x) => x.split(/[\s=]/)[0] === anchor);
@@ -1290,7 +1403,7 @@ export const Playground = ({ config }) => {
&& config.playgroundFeatures.pdDisagg.router;
const routerPort = (routerCfg && routerCfg.port) || 8000;
const routerLine = routerCfg
? `# then front BOTH with the Router (SGLang Model Gateway) shown below.\n`
? `# then front BOTH with the Router shown below.\n`
+ `# Client traffic (cURL) targets the router (:${routerPort}), not this role server.`
: `# then front BOTH with a router; client traffic targets the router, not this role server.`;
const hicacheCfg = config.playgroundFeatures
@@ -1685,8 +1798,18 @@ export const Playground = ({ config }) => {
// Base selection — live-linked to the Deployment panel via URL hash + custom event
// (history.replaceState doesn't fire hashchange, hence the event too).
const overlayDefaults = () => {
const out = {};
for (const d of (config.overlayDims || [])) {
const opts = d.options || [];
out[d.id] = d.default !== undefined ? d.default : ((opts[0] && opts[0].id) || "");
}
return out;
};
const baseFallback = () => ({ ...config.cells[0].match, ...overlayDefaults() });
const initialBaseFromHash = () => {
const fallback = config.cells[0].match;
const fallback = baseFallback();
if (typeof window === "undefined") return { ...fallback };
const raw = window.location.hash.replace(/^#/, "");
if (!raw) return { ...fallback };
@@ -1699,7 +1822,7 @@ export const Playground = ({ config }) => {
useEffect(() => {
const onHash = () => setBase(initialBaseFromHash());
const onSelEvent = (e) => {
const fallback = config.cells[0].match;
const fallback = baseFallback();
const incoming = (e && e.detail) || {};
const next = { ...fallback };
for (const k of Object.keys(next)) {
@@ -1715,35 +1838,36 @@ export const Playground = ({ config }) => {
};
}, []);
// Calculator-computed --mamba-full-memory-ratio pair (K3): `eff` matches the
// composed command (with playground overrides), `base` matches the base view.
// Rendered into the command views below; never part of the broadcast flags.
const [pgRatios, setPgRatios] = useState({ eff: null, base: null });
useEffect(() => {
const onRatio = (e) => setPgRatios({
eff: (e.detail && e.detail.ratio) || null,
base: (e.detail && (e.detail.baseRatio || e.detail.ratio)) || null,
});
window.addEventListener("sglang-k3-mamba-ratio", onRatio);
return () => window.removeEventListener("sglang-k3-mamba-ratio", onRatio);
}, []);
// Deltas: one slot per declared axis.
const initialDeltas = () => {
const out = {};
for (const [axisId, handler] of Object.entries(AXIS_HANDLERS)) {
const fc = pgFeatures[axisId];
if (fc) out[axisId] = handler.initState(fc);
if (fc) out[axisId] = handler.initState(fc, base);
}
return out;
};
const [deltas, setDeltas] = useState(initialDeltas);
// On base change, revert any now-hidden picks to their inherit default.
// Disabled picks are NOT reverted (soft warning).
// Picking a different base is picking a different starting point, so the
// playground goes back to inheriting everything. Carrying overrides across a
// base change silently mixes knobs from a config the reader already left.
useEffect(() => {
setDeltas((d) => {
let next = d;
let mutated = false;
for (const [axisId, handler] of Object.entries(AXIS_HANDLERS)) {
const fc = pgFeatures[axisId];
if (!fc || d[axisId] === undefined) continue;
const nv = handler.revertHidden(d[axisId], fc, base, helpers);
if (nv !== d[axisId]) {
if (!mutated) { next = { ...d }; mutated = true; }
next[axisId] = nv;
}
}
return mutated ? next : d;
});
}, [base.hw, base.variant, base.quant, base.strategy, base.nodes]);
setDeltas(initialDeltas());
}, [Object.keys(base).sort().map((k) => `${k}=${base[k]}`).join("&")]);
const [modal, setModal] = useState(null); // 'curl' | 'env' | 'submit' | null
@@ -1800,7 +1924,7 @@ export const Playground = ({ config }) => {
// 10. Derived values
// ==========================================================================
const s = makeStyles(isDark);
const baseCell = findCell(config.cells, base);
const baseCell = withOverlay(findCell(config.cells, base), base);
const modelName = resolveModelName(base);
// Per-axis state recovered from the base cell's flags (deriveFromBase).
@@ -1815,7 +1939,7 @@ export const Playground = ({ config }) => {
// Cross-axis facts folded into the `base` handed to chip-constraint
// matching, so `hide`/`disable` can react to another axis's live state
// (render path only; revertHidden keeps the clean 5-dim base).
// (render path only; the raw base stays untouched).
// dpAttnOn — effective DP-Attention resolves to "on" (positive degree
// or true), explicit override else derived-from-base.
// cpOn — effective prefill-CP resolves to "on" (degree > 1),
@@ -1871,7 +1995,10 @@ export const Playground = ({ config }) => {
? attnDelta.cpStrategy
: (attnDerived.cpStrategy !== undefined ? attnDerived.cpStrategy : null))
|| "interleave";
const pdMode = (deltas.pdDisagg && deltas.pdDisagg.mode) || "off";
const pdCardOwnsMode = ((pgFeatures.pdDisagg && pgFeatures.pdDisagg.modes) || []).length > 0;
const pdMode = pdCardOwnsMode
? ((deltas.pdDisagg && deltas.pdDisagg.mode) || "off")
: (base.pdMode || "off");
const constraintBase = {
...base, dpAttnOn, cpOn, cpStrategy, cpSizeTarget, effTp, pdMode,
};
@@ -1881,14 +2008,47 @@ export const Playground = ({ config }) => {
let diffLines = [];
let pgFlagsLatest = [];
let pgEnvLatest = [];
// Render-only ratio injection (before the host/port tail); skipped if the
// flags somehow already carry the family.
const withRatio = (fl, value) => {
if (!value) return fl;
if (fl.some((f) => f.startsWith("--mamba-full-memory-ratio"))) return fl;
const out = [...fl];
const line = `--mamba-full-memory-ratio ${value}`;
const i = out.findIndex((f) => f.startsWith("--host"));
if (i >= 0) out.splice(i, 0, line);
else out.push(line);
return out;
};
if (baseCell) {
baseCommand = renderCommandLines(baseCell, baseCell.flags, baseCell.env, base, env, null, runMode);
baseCommand = renderCommandLines(baseCell, withRatio(baseCell.flags, pgRatios.base), baseCell.env, base, env, null, runMode);
const { flags: pgFlags, env: pgEnv, pdMode } = applyAllDeltas(baseCell.flags, baseCell.env, deltas, base, derivedMap);
pgFlagsLatest = pgFlags;
pgEnvLatest = pgEnv;
playgroundCommand = renderCommandLines(baseCell, pgFlags, pgEnv, base, env, pdMode, runMode);
playgroundCommand = renderCommandLines(baseCell, withRatio(pgFlags, pgRatios.eff), pgEnv, base, env, pdMode, runMode);
diffLines = computeDiff(baseCommand, playgroundCommand);
}
// Broadcast both configs for outside consumers (the mamba ratio calculator):
// `baseFlags`/`baseEnv` = cell + Deploy overlays (what the Deploy command
// shows); `flags`/`env` = that plus playground overrides. Keyed on content
// so re-renders don't spam events.
const effectiveKey =
pgFlagsLatest.join("\n") + " " + pgEnvLatest.join("\n") + " " +
(baseCell ? baseCell.flags.join("\n") + " " + baseCell.env.join("\n") : "");
useEffect(() => {
if (typeof window === "undefined" || !baseCell) return;
window.dispatchEvent(
new CustomEvent("sglang-k3-effective-config", {
detail: {
flags: pgFlagsLatest,
env: pgEnvLatest,
baseFlags: baseCell.flags,
baseEnv: baseCell.env,
},
})
);
}, [effectiveKey]);
// Cross-cell verified detection: the emitted (env, flags) may match the
// base cell itself or a sibling (different strategy). A sibling match
// still shows Verified, plus a "switch base" link.
@@ -1896,7 +2056,8 @@ export const Playground = ({ config }) => {
? findMatchingCell(config.cells, base, pgEnvLatest, pgFlagsLatest)
: null;
const playgroundVerified = !!(matchedCell && matchedCell.verified);
const matchedSiblingCell = (matchedCell && matchedCell !== baseCell)
const matchedSiblingCell = (matchedCell
&& DIMENSIONS.some((d) => matchedCell.match[d] !== base[d]))
? matchedCell : null;
// MTP hint on the EFFECTIVE (post-override) command — fires when the user
// toggles speculative decoding on without setting --max-running-requests
@@ -1969,8 +2130,13 @@ export const Playground = ({ config }) => {
setTimeout(() => setCurlCopied(false), 1200);
};
// Summarize whichever dims the Deploy panel actually has — a config may drop
// variant/quant/nodes or add its own (PD mode, ...), so nothing is hardcoded.
const baseSummary = baseCell
? `${base.hw.toUpperCase()} · ${base.variant} · ${base.quant.toUpperCase()} · ${base.strategy} · ${base.nodes}`
? Object.entries(base)
.filter(([, v]) => v !== undefined && v !== "")
.map(([k, v]) => (k === "hw" ? String(v).toUpperCase() : String(v)))
.join(" · ")
: "(no verified cell at the current Deploy selection — showing playground only)";
// ==========================================================================
@@ -2071,6 +2237,9 @@ export const Playground = ({ config }) => {
{Object.entries(AXIS_HANDLERS).map(([axisId, handler]) => {
const fc = pgFeatures[axisId];
if (!fc) return null;
// An axis whose feature is not switched on in the Deploy panel has
// nothing to tune — declared per config as `showWhen(base)`.
if (typeof fc.showWhen === "function" && !fc.showWhen(constraintBase)) return null;
const setValue = (next) => setDeltas((d) => ({ ...d, [axisId]: next }));
return handler.render({
axisId, value: deltas[axisId], setValue,
@@ -2181,7 +2350,7 @@ export const Playground = ({ config }) => {
{/* PD-Disagg router companion (separate block so the role diff stays pure). */}
{pdRouter && routerText && (
<div style={s.card}>
<div style={s.title}>Router (SGLang Model Gateway)</div>
<div style={s.title}>Router</div>
<div style={{ fontSize: 11, opacity: 0.7, margin: "0 0 6px" }}>
Run after both roles are up. Substitute <code>{"<prefill-host>"}</code> /{" "}
<code>{"<decode-host>"}</code> with reachable hosts (both <code>127.0.0.1</code>{" "}
@@ -17,7 +17,7 @@ export const config = {
// merges these in, so a model-specific GPU is config data, not an engine edit.
// RTX PRO 6000 (SM120 / Blackwell Desktop) is a workstation card, not datacenter.
hardware: [
{ id: "rtx6000", label: "RTX PRO 6000", vram: "96GB", vendor: "nvidia" },
{ id: "rtx6000", label: "RTX PRO 6000", vram: "96GB", vendor: "blackwell" },
],
variants: [
@@ -8,7 +8,7 @@ export const config = {
// Model-specific GPUs the shared HARDWARE_CATALOG does not carry.
hardware: [
{ id: "h20", label: "H20", vram: "96GB", vendor: "nvidia" },
{ id: "h20", label: "H20", vram: "96GB", vendor: "hopper" },
],
variants: [
@@ -0,0 +1,23 @@
export const benchmarks = [
{ match: { hw: "b300", pdMode: "unified", strategy: "balanced" } },
{ match: { hw: "b300", pdMode: "unified", strategy: "low-latency" } },
{ match: { hw: "b300", pdMode: "unified", strategy: "high-throughput" } },
{ match: { hw: "b200", pdMode: "unified", strategy: "low-latency" } },
{ match: { hw: "b200", pdMode: "unified", strategy: "balanced" } },
{ match: { hw: "b200", pdMode: "unified", strategy: "high-throughput" } },
{ match: { hw: "b200", pdMode: "unified", strategy: "long-context" } },
{ match: { hw: "mi350x", pdMode: "unified", strategy: "balanced" } },
{ match: { hw: "mi355x", pdMode: "unified", strategy: "balanced" } },
{ match: { hw: "h100", pdMode: "unified", strategy: "low-latency" } },
{ match: { hw: "h100", pdMode: "unified", strategy: "balanced" } },
{ match: { hw: "h100", pdMode: "unified", strategy: "high-throughput" } },
{ match: { hw: "h200", pdMode: "unified", strategy: "low-latency" } },
{ match: { hw: "h200", pdMode: "unified", strategy: "balanced" } },
{ match: { hw: "h200", pdMode: "unified", strategy: "high-throughput" } },
{ match: { hw: "gb300", pdMode: "unified", strategy: "low-latency" } },
{ match: { hw: "gb300", pdMode: "unified", strategy: "balanced" } },
{ match: { hw: "gb300", pdMode: "unified", strategy: "high-throughput" } },
{ match: { hw: "gb200", pdMode: "unified", strategy: "low-latency" } },
{ match: { hw: "gb200", pdMode: "unified", strategy: "balanced" } },
{ match: { hw: "gb200", pdMode: "unified", strategy: "high-throughput" } },
];
File diff suppressed because it is too large Load Diff