feat(cookbook): generic config-declared flagSelects playground axis (#28128)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
zijiexia
2026-06-13 11:46:34 -07:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 29128f31fd
commit d988d5d681
4 changed files with 166 additions and 30 deletions
@@ -4,13 +4,36 @@ Loaded on demand by the `cookbook-add-model` skill. **Rare** — adding a model
cookbook is data-only and never needs this. The current 7 built-in axes cookbook is data-only and never needs this. The current 7 built-in axes
(`attention`, `moe`, `parsers`, `speculative`, `pdDisagg`, `hicache`, (`attention`, `moe`, `parsers`, `speculative`, `pdDisagg`, `hicache`,
`hisparse`) already cover the SGLang feature surface most cookbooks need. `hisparse`) already cover the SGLang feature surface most cookbooks need.
(MegaMoE is not its own axis — it lives inside `moe` as a backend option +
a `megamoeQuant` sub-select.) Only add a new axis if a real cookbook needs it **Model-specific features are config DATA, not engine code.** The axis
and the feature does not fit any existing axis. Touches `_playground.jsx` only. handlers read options / flags / env / gating straight from
One routine trigger: the `cookbook-migrate-model` skill requires every legacy `config.playgroundFeatures`, so a model-specific feature is added as data on
control to survive as a dimension or a Playground axis — a legacy feature no an existing axis with NO engine edit — MegaMoE W4A4 is not its own axis, it's
built-in axis covers (e.g. Nemotron3's "KV Cache DType" radio) lands here config data on `moe` (a `megamoe` backend option + a `megamoeQuant`
first, as its own engine PR ahead of the migration PR. sub-select). Reach for this file ONLY when a feature's *shape* — its
title + the flag family it strips + its option/state model — is something no
existing axis can express.
**When you do extend, build a GENERIC primitive, never a model-named handler.**
The right unit is a reusable shape (e.g. "a titled single-select that strips a
configurable flag family and splices the picked option's flags" — exactly the
`speculative` handler's shape, minus its hardcoded title + `--speculative-*`
strip list). Parameterize title, strip-prefixes, and options from config so
the next model of that shape is pure config. Do NOT add a `kvcache` /
`<model-feature>` handler that hardcodes one model's flag — that's the
model-specific-code-in-the-engine anti-pattern this architecture exists to
avoid. (Precedent: Nemotron3's "KV Cache DType" and Qwen3's mamba-cache select
are the SAME single-select shape → one generic primitive serves both, then
both are config.)
**A new axis is backward-compatible — zero churn on existing configs.** The
runtime is opt-in per key: the apply/render loop does `const fc =
pgFeatures[axisId]; if (!fc) continue;`, so any config that doesn't declare
the key never sees the axis. And a model-specific axis does NOT join the
opt-out "general axes ship on every cookbook" set (that set is an authoring
convention for NEW configs, not a runtime default) — so review-pr won't flag
existing pages for lacking it, and you never touch a merged config. Only the
models that expose the control declare it. Touches `_playground.jsx` only.
For the per-model config/cells/MDX reference see [authoring-reference.md](authoring-reference.md). For the per-model config/cells/MDX reference see [authoring-reference.md](authoring-reference.md).
@@ -20,12 +43,15 @@ For the per-model config/cells/MDX reference see [authoring-reference.md](author
Before touching the engine, confirm: Before touching the engine, confirm:
- The feature is a STABLE part of the SGLang CLI surface (will appear in - The feature cannot be expressed as data on an existing axis (a new MoE
multiple cookbooks, not one-off). backend belongs in `moe.backend.options`; a new parser in `parsers.items`;
- The feature cannot be expressed as a new option inside an existing axis a new spec preset in `speculative.options`). This is the common case —
(e.g. a new MoE backend belongs in `moe.backend.options`, not a new most "new features" are new options, not new shapes.
axis). - The *shape* is genuinely new (state model + strip pattern), AND you are
- The feature has a clean strip-prefix → emit-flag pattern. adding it as a GENERIC config-parameterized axis (title / strip-prefixes /
options all from config), not a one-model handler. If you'd hardcode a
specific flag like `--kv-cache-dtype`, stop — generalize the shape instead.
- The shape has a clean strip-prefix → emit-flag pattern.
If unsure, add it as data first (in one cookbook's config under an If unsure, add it as data first (in one cookbook's config under an
existing axis) before promoting it to a built-in axis. existing axis) before promoting it to a built-in axis.
+14 -8
View File
@@ -63,10 +63,12 @@ your dispatch prompt, or ask for it.
including combos that look memory-infeasible; keep them verbatim and list including combos that look memory-infeasible; keep them verbatim and list
them in the PR body for the re-verification track. them in the PR body for the re-verification track.
4. **Engines are read-only.** `_deployment.jsx` / `_playground.jsx` must not 4. **Engines are read-only.** `_deployment.jsx` / `_playground.jsx` must not
change in a migration PR. If the model needs an engine capability (a new change in a migration PR. Model-specific features are config DATA consumed
axis, accuracy labels, …), that is a separate prior PR. The every-feature by generic axis handlers (MegaMoE precedent), so they need NO engine
rule (step 2) triggers this routinely: a legacy control no built-in axis change — only a genuinely new control *shape* does, and then as a one-time
covers means an engine-axis PR lands first. generic primitive (never a model-named handler) on a separate prior PR
(engine-axis.md). KV Cache DType / mamba-cache select trigger this once for
the whole round; afterwards both are config.
5. **`github.cookbookModel` must be set** (`<hf-org>/<page-slug>`, e.g. 5. **`github.cookbookModel` must be set** (`<hf-org>/<page-slug>`, e.g.
`qwen/qwen3.5`) and the block never pruned — without it Submit ↗ mislabels `qwen/qwen3.5`) and the block never pruned — without it Submit ↗ mislabels
as deepseek-v4. The issue template itself needs NO edits (free-form input). as deepseek-v4. The issue template itself needs NO edits (free-form input).
@@ -113,10 +115,14 @@ ON — EXCEPT parsers:
`--reasoning-parser`/`--tool-call-parser` are NEVER baked into cells, they are `--reasoning-parser`/`--tool-call-parser` are NEVER baked into cells, they are
Playground-only (DSv4 convention). **Every legacy control survives as an Playground-only (DSv4 convention). **Every legacy control survives as an
interactive control** — a dimension or a Playground axis, never a tips-only interactive control** — a dimension or a Playground axis, never a tips-only
mention. A feature none of the built-in axes covers (Nemotron3's "KV Cache mention — and a model-specific control is **config data, not engine code**
DType" radio is the precedent) still lands in the Playground: add the axis (MegaMoE W4A4 is all DSv4 config on the existing `moe` axis). It's pure
via a separate PRIOR engine PR (engine-axis.md), keeping the migration PR config whenever it fits an existing axis's data schema. Only a genuinely new
itself data-only (hard rule 4). The strategy count follows the page's *shape* (Nemotron3's "KV Cache DType" — a titled single-select stripping a
flag family no axis manages) needs the engine, and then as a ONE-TIME
generic config-parameterized primitive (never a model-named handler) on a
separate PRIOR engine PR, keeping the migration PR data-only (hard rule 4,
engine-axis.md). The strategy count follows the page's
operating points: **one recipe → a single `balanced`; two → `low-latency` + operating points: **one recipe → a single `balanced`; two → `low-latency` +
`high-throughput`; three → the full trio (the ideal)**. The tiers apply per `high-throughput`; three → the full trio (the ideal)**. The tiers apply per
(hw × variant × quant) combination — a single-recipe combination on a (hw × variant × quant) combination — a single-recipe combination on a
@@ -13,9 +13,9 @@ this file is about the *mapping decisions*.
| model-size / model-name radio | `variants` | One variant per deployable checkpoint family; single `{id:"default"}` when there's no variant axis (then `modelNames` keys drop the variant half). | | model-size / model-name radio | `variants` | One variant per deployable checkpoint family; single `{id:"default"}` when there's no variant axis (then `modelNames` keys drop the variant half). |
| quantization radio | `quantizations` | Real precision ids (`bf16`/`fp8`/`fp4`/`int4`/…). One `fp4` id even when checkpoints differ per vendor — route via `hw\|variant\|quant` triple keys in `modelNames` (NVFP4 on Blackwell vs AMD MXFP4 is the precedent); per-hw greying falls out of which cells exist. | | quantization radio | `quantizations` | Real precision ids (`bf16`/`fp8`/`fp4`/`int4`/…). One `fp4` id even when checkpoints differ per vendor — route via `hw\|variant\|quant` triple keys in `modelNames` (NVFP4 on Blackwell vs AMD MXFP4 is the precedent); per-hw greying falls out of which cells exist. |
| toggle that **couples** with other parts of the command (changes TP/mem/EP), OR one the legacy page labels with **operating-point words** | `strategies` | The Playground applies pure flag diffs — it cannot do coupled changes. Example: Qwen3.5's MTP toggle bumps TP on three H100 combos → strategies `low-latency` (MTP on) / `high-throughput` (MTP off). **Naming counts like coupling**: GLM-5.1's / Kimi-K2.6's `dpattention` adds only `--dp N --enable-dp-attention` (uncoupled), but its options are subtitled "Low Latency" / "High Throughput" — the page's own named operating-point split → strategies; a flag-only spec toggle riding alongside it stays a Playground axis and bakes per its legacy default. GPU-count radios (GLM-4.7, MiniMax-M2.5/2.7) → budget-tier strategies with the legacy SUPPORT matrix preserved by which cells exist. Strategy count follows the page's operating points: 1 → `balanced`, 2 → `low-latency`+`high-throughput`, 3 → the full trio (§4). | | toggle that **couples** with other parts of the command (changes TP/mem/EP), OR one the legacy page labels with **operating-point words** | `strategies` | The Playground applies pure flag diffs — it cannot do coupled changes. Example: Qwen3.5's MTP toggle bumps TP on three H100 combos → strategies `low-latency` (MTP on) / `high-throughput` (MTP off). **Naming counts like coupling**: GLM-5.1's / Kimi-K2.6's `dpattention` adds only `--dp N --enable-dp-attention` (uncoupled), but its options are subtitled "Low Latency" / "High Throughput" — the page's own named operating-point split → strategies; a flag-only spec toggle riding alongside it stays a Playground axis and bakes per its legacy default. GPU-count radios (GLM-4.7, MiniMax-M2.5/2.7) → budget-tier strategies with the legacy SUPPORT matrix preserved by which cells exist. Strategy count follows the page's operating points: 1 → `balanced`, 2 → `low-latency`+`high-throughput`, 3 → the full trio (§4). |
| toggle that only adds/removes its own flags | Playground axis (+ bake, EXCEPT parsers and accuracy-degrading flags) | **Parsers (`--reasoning-parser` / `--tool-call-parser`) are NEVER baked into cells** — Deployment commands ship without them regardless of the legacy default or the measured command; the `parsers` axis adds them on top (DSv4 convention; cells mirror the legacy generator's parsers-OFF output). Accuracy-degrading toggles are never baked either — §2 caveats (axis-only, accuracy-safe cells). Other flag-only toggles: legacy default ON → bake into cells AND declare the axis so users can strip (red strikethrough); default OFF → keep cells clean, axis preset only. MTP/EAGLE presets → `speculative` axis; dp-attention → a strategy when the legacy page labels it as the operating-point split or when coupled (see the row above), else `attention.dpAttn`. **No fitting built-in axis ≠ drop the feature** — EVERY legacy control survives as an interactive control (a dimension or a Playground axis), never a tips-only mention. When none of the built-in axes fits (Nemotron3's "KV Cache DType" radio is the precedent), add the axis via the engine-axis flow: a separate PRIOR engine PR, then the migration config declares it (hard rule 4). | | toggle that only adds/removes its own flags | Playground axis (+ bake, EXCEPT parsers and accuracy-degrading flags) | **Parsers (`--reasoning-parser` / `--tool-call-parser`) are NEVER baked into cells** — Deployment commands ship without them regardless of the legacy default or the measured command; the `parsers` axis adds them on top (DSv4 convention; cells mirror the legacy generator's parsers-OFF output). Accuracy-degrading toggles are never baked either — §2 caveats (axis-only, accuracy-safe cells). Other flag-only toggles: legacy default ON → bake into cells AND declare the axis so users can strip (red strikethrough); default OFF → keep cells clean, axis preset only. MTP/EAGLE presets → `speculative` axis; dp-attention → a strategy when the legacy page labels it as the operating-point split or when coupled (see the row above), else `attention.dpAttn`. **EVERY legacy control survives as an interactive control** (a dimension or a Playground axis), never a tips-only mention — but a model-specific control is **config DATA, not engine code**: the axis handler reads options/flags/env/gating straight from `config.playgroundFeatures` (MegaMoE W4A4 is entirely DSv4 config data on the existing `moe` axis — no per-model engine edit). A control that fits an existing axis's data schema is therefore pure config, full stop. Only when the *shape* is genuinely new — a titled single-select that strips a flag family no axis manages, e.g. Nemotron3's "KV Cache DType" (`--kv-cache-dtype`) — does the engine need that shape, and then you add it ONCE as a **generic config-parameterized primitive** (title + strip-prefixes + options all from config), **never** a model-named `kvcache` handler; afterwards this feature and every future one of its shape are pure config. Such a primitive is backward-compatible — runtime is opt-in per key (`if (!fc) continue`), and a model-specific axis is NOT added to the opt-out general set — so it churns zero existing configs (engine-axis.md). |
| per-combo hidden option (e.g. spec hidden on Xeon) | absent cells | Don't create cells for combos the legacy widget couldn't produce; the engine greys them automatically. `# Error:` pseudo-commands → no cell + explanation in §2 tips and/or a chip `disable`/`disableReason`. | | per-combo hidden option (e.g. spec hidden on Xeon) | absent cells | Don't create cells for combos the legacy widget couldn't produce; the engine greys them automatically. `# Error:` pseudo-commands → no cell + explanation in §2 tips and/or a chip `disable`/`disableReason`. |
| coupled secondary knob (e.g. mamba cache V1/V2) | cells + Playground axis | Bake the correct value per cell following the legacy coupling (Qwen3.5: MTP ⇒ `--mamba-scheduler-strategy extra_buffer` on NVIDIA; AMD/Xeon ⇒ V1/no flag) and document the coupling in §2 tips — AND surface the knob as a Playground axis like every other legacy feature (row above; add the axis when none fits). Baking alone is NOT enough — the every-feature rule supersedes the pilot's cells+prose-only treatment of Qwen3.5's mamba knob (retrofit pending); Qwen3.6 / Qwen3-Coder-Next carry the same knob, so their migrations need the axis in place first. | | coupled secondary knob (e.g. mamba cache V1/V2) | cells + Playground axis | Bake the correct value per cell following the legacy coupling (Qwen3.5: MTP ⇒ `--mamba-scheduler-strategy extra_buffer` on NVIDIA; AMD/Xeon ⇒ V1/no flag) and document the coupling in §2 tips — AND surface the knob as a Playground axis like every other legacy feature (row above; add the axis when none fits). Baking alone is NOT enough — the every-feature rule supersedes the pilot's cells+prose-only treatment of Qwen3.5's mamba knob (retrofit pending). The mamba knob is the same single-select shape as KV Cache DType, so it rides the SAME generic primitive (row above) — once that lands, Qwen3.6 / Qwen3-Coder-Next declare it purely in config. |
## 2. Command rewrite table (the ONLY allowed normalizations) ## 2. Command rewrite table (the ONLY allowed normalizations)
@@ -44,13 +44,14 @@ Caveats discovered in the pilot:
asking: asking:
- offered as a legacy **selectable option/toggle** → never select it; - offered as a legacy **selectable option/toggle** → never select it;
cells mirror the accuracy-safe side (even if the legacy default was the cells mirror the accuracy-safe side (even if the legacy default was the
lossy side). The option itself **must survive as a Playground axis** — the user's choice may not degrade to a lossy side). The option itself **must survive as a Playground control**
tips mention. Use the existing axis when one fits (DSv4 gates W4A4 — the user's choice may not degrade to a tips mention. Express it as
behind `megamoeQuant`); when the playground lacks one — e.g. config data on the fitting axis (DSv4 gates W4A4 behind `megamoeQuant`);
Nemotron3-Ultra's "KV Cache DType" radio (None default / fp8_e4m3 / when no axis models its shape — e.g. Nemotron3-Ultra's "KV Cache DType"
bf16) has no kv-cache axis today — **add the axis** via the engine-axis radio (None default / fp8_e4m3 / bf16) is a titled single-select the
flow: a separate PRIOR engine PR (hard rule 4, like accuracyLabels playground has no shape for today — that shape is added ONCE as a
#27842), then the migration config declares it; generic config-parameterized primitive (row above / engine-axis.md),
then this config and every future one declare it as data;
- baked into the recipe's **unconditional/default command** → keep it - baked into the recipe's **unconditional/default command** → keep it
verbatim. The legacy measurements ran with it, and fp8 KV halves KV verbatim. The legacy measurements ran with it, and fp8 KV halves KV
memory — stripping could OOM the recipe. Expect this pattern: legacy memory — stripping could OOM the recipe. Expect this pattern: legacy
+103
View File
@@ -13,6 +13,8 @@
// pdDisagg — role + transfer backend + IB device + optional router // pdDisagg — role + transfer backend + IB device + optional router
// hicache — enable + backend + write policy // hicache — enable + backend + write policy
// hisparse — enable + host ratio (decode-only) // hisparse — enable + host ratio (decode-only)
// flagSelects — generic: a config-declared LIST of single-selects, each with
// its own title + strip-prefixes + options (no per-feature code)
// //
// Adding an axis = one entry in AXIS_HANDLERS below; nothing else switches on // 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 / revertHidden / apply /
@@ -915,6 +917,107 @@ 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? }] }
// 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).
flagSelects: {
initState: (fc) => {
const out = {};
for (const spec of (fc || [])) out[spec.id] = 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.
deriveFromBase: (cell, fc) => {
const flags = (cell && cell.flags) || [];
const out = {};
for (const spec of (fc || [])) {
const prefixes = spec.stripPrefixes || [];
const fam = flags.filter((f) => prefixes.includes(f.split(/[\s=]/)[0]));
let hit = null;
for (const opt of (spec.options || [])) {
const of = opt.flags || [];
if (of.length === fam.length && of.every((x) => fam.includes(x))) {
hit = opt.id; break;
}
}
out[spec.id] = hit;
}
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 || {}),
dpAttnOn: h.hasFlag(flags, "--enable-dp-attention"),
pdMode: h.findFlagArg(flags, "--disaggregation-mode") || "off",
};
for (const spec of (fc || [])) {
const v = value ? value[spec.id] : null;
if (v === null || v === undefined) continue; // inherit base
const d = derived ? derived[spec.id] : null;
if (v === d) continue; // already == base
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);
}
}
return { flags, env };
},
render: ({ axisId, value, setValue, fc, base, s, h, renderChip, derived }) => {
const cards = [];
for (const spec of (fc || [])) {
const opts = (spec.options || [])
.map((o) => h.evaluateChip(o, base))
.filter((c) => !c.hidden);
if (!opts.length) continue;
const explicit = value ? value[spec.id] : null;
const display = (explicit !== null && explicit !== undefined)
? explicit : (derived ? derived[spec.id] : null);
cards.push(
<div key={`${axisId}-${spec.id}`} style={s.card}>
<div style={s.compactRow}>
<span style={s.axisTitle}>{spec.title}</span>
{opts.map((c) => (
<span key={c.value} style={s.field}>
{renderChip(c.label, display, c.value,
() => setValue({ ...value, [spec.id]: c.value }),
{ disabled: c.disabled, disabledReason: c.disableReason })}
</span>
))}
</div>
</div>
);
}
return cards.length ? cards : null;
},
},
}; };
// ========================================================================== // ==========================================================================