2804 lines
125 KiB
React
2804 lines
125 KiB
React
// Engine half of the SGLang cookbook playground widget. Pairs with a
|
||
// per-model config under `/src/snippets/configs/<vendor>/<model>.jsx`.
|
||
// See the cookbook-add-model skill (.claude/skills/cookbook-add-model/) for the
|
||
// authoring workflow; references/engine-axis.md covers adding a new axis.
|
||
//
|
||
// Consumes the same config shape as `_deployment.jsx`, plus
|
||
// `config.playgroundFeatures` — a keyed map where each present key opts that
|
||
// axis in. Recognised axes:
|
||
// attention — TP/CP/DP-Attention knobs
|
||
// moe — backend (+ MegaMoE quantization sub-select) + EP
|
||
// parsers — per-item toggle flags
|
||
// speculative — single-select preset; an option may carry `note` (a
|
||
// prerequisite line rendered under the chips while that option
|
||
// is the one in effect)
|
||
//
|
||
// 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)
|
||
// 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
|
||
// an axis id. Each handler implements initState / apply /
|
||
// render, plus optional deriveFromBase (recover state from base cell flags)
|
||
// and getRenderHints.
|
||
//
|
||
// Mintlify caveats (same as _deployment.jsx):
|
||
// - Module-level statements are stripped → everything lives in the wrapper.
|
||
// - Capitalized JSX tags get rebound → lowercase HTML tags only.
|
||
// - `!(x in y)` crashes the AST walker → use `obj.key === undefined`.
|
||
|
||
export const Playground = ({ config }) => {
|
||
if (!config) {
|
||
return <div style={{padding: 12, color: "#b91c1c"}}>Playground: missing <code>config</code> prop</div>;
|
||
}
|
||
|
||
// ==========================================================================
|
||
// 1. Constants
|
||
// ==========================================================================
|
||
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/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.
|
||
//
|
||
// Overlay flags append, except a flag whose family the same overlay stripped:
|
||
// that one is spliced back where the stripped flag was, so a rewritten
|
||
// parallelism block (DSPARK folding a pipeline flat) stays put instead of
|
||
// landing past the --host/--port tail with the multi-node trio behind it.
|
||
const overlayCompose = (cellFlags, sel) => {
|
||
const strip = overlayPart(sel, "stripPrefixes");
|
||
const add = overlayPart(sel, "flags");
|
||
if (!strip.length) return [...(cellFlags || []), ...add];
|
||
const used = new Set();
|
||
// Consumed once, so a family the cell carries twice is not emitted twice.
|
||
const replacementsFor = (tok) => {
|
||
const out = [];
|
||
add.forEach((f, i) => {
|
||
if (used.has(i) || f.split(/[\s=]/)[0] !== tok) return;
|
||
used.add(i);
|
||
out.push(f);
|
||
});
|
||
return out;
|
||
};
|
||
const out = [];
|
||
for (const f of (cellFlags || [])) {
|
||
const tok = f.split(/[\s=]/)[0];
|
||
if (!strip.includes(tok)) out.push(f);
|
||
else out.push(...replacementsFor(tok));
|
||
}
|
||
add.forEach((f, i) => { if (!used.has(i)) out.push(f); });
|
||
return out;
|
||
};
|
||
// ==== end MIRROR ====
|
||
const withOverlay = (cell, sel) => (cell && {
|
||
...cell,
|
||
flags: overlayCompose(cell.flags, sel),
|
||
env: [...(cell.env || []), ...overlayPart(sel, "env")],
|
||
}) || cell;
|
||
// Shared with `_deployment.jsx` (HOST/PORT/etc. unified across the page).
|
||
const STORAGE_KEY = "sglang-deploy-env";
|
||
// ==== MIRROR of DEPLOYMENT_COMPONENT_ID in _deployment.jsx — keep identical ====
|
||
// Snippets cannot import each other. The Deploy panel puts this id on its
|
||
// root (carrying its own scrollMarginTop), which is what "jump to the base"
|
||
// should land on.
|
||
const DEPLOYMENT_COMPONENT_ID = "deployment-configurator";
|
||
|
||
const pgFeatures = config.playgroundFeatures || {};
|
||
// Single-host PD runs prefill + decode as two engines on one box. Each derives
|
||
// 5 consecutive ZMQ/dist ports from its --port (port+233, see server_args.py
|
||
// ZMQ_TCP_PORT_DELTA), so the serve ports are spaced 100 apart to keep those
|
||
// derived ranges from overlapping — no --dist-init-addr needed single-host.
|
||
// `dist` is only used by the multi-node renderer (cross-node rendezvous).
|
||
const PD_PORTS = {
|
||
prefill: { serve: 30000, dist: 30335 },
|
||
decode: { serve: 30100, dist: 30435 },
|
||
};
|
||
|
||
// ==========================================================================
|
||
// 2. Pure data helpers
|
||
// ==========================================================================
|
||
const findCell = (cells, sel) =>
|
||
cells.find((c) => DIMENSIONS.every((d) => c.match[d] === sel[d]));
|
||
|
||
// After applying overrides, the resulting (env, flags) may equal another
|
||
// 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) => {
|
||
if (a.length !== b.length) return false;
|
||
const set = new Set(a);
|
||
for (const x of b) if (!set.has(x)) return false;
|
||
return true;
|
||
};
|
||
for (const c of cells) {
|
||
if (fixedDims.some((d) => c.match[d] !== sel[d])) continue;
|
||
if (flagsEq(c.flags || [], pgFlags || []) && envEq(c.env || [], pgEnv || [])) {
|
||
return c;
|
||
}
|
||
}
|
||
return null;
|
||
};
|
||
|
||
// hw|variant|quant → variant|quant → hw|quant → quant → hw → default.
|
||
const resolveModelName = (sel) => {
|
||
const keys = [
|
||
`${sel.hw}|${sel.variant}|${sel.quant}`,
|
||
`${sel.variant}|${sel.quant}`,
|
||
`${sel.hw}|${sel.quant}`,
|
||
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}}}`));
|
||
|
||
const parseNnodes = (id) => {
|
||
if (id === "single") return 1;
|
||
const m = /^multi-(\d+)$/.exec(id);
|
||
return m ? parseInt(m[1], 10) : 1;
|
||
};
|
||
|
||
const placeholderDefaults = (schema) => {
|
||
const out = {};
|
||
for (const [k, v] of Object.entries(schema || {})) out[k] = v.default ?? "";
|
||
return out;
|
||
};
|
||
|
||
// ==========================================================================
|
||
// 3. Per-chip constraint evaluation
|
||
// ==========================================================================
|
||
// A chip entry is a bare value, a `{value, hide?, disable?, ...}` wrapper,
|
||
// or a rich `{id, ...}` option. `hide`/`disable` are constraint objects
|
||
// mapping base-cell dims to allowed-value arrays; matches when every key
|
||
// matches (AND across keys, OR within a key). Empty/malformed never match.
|
||
// `disabled: true` / `disable: true` are static always-disabled forms.
|
||
// `disable` may also be an ARRAY of `{when: constraint, reason?}` items
|
||
// (OR across items, first match wins and supplies its own reason) for
|
||
// conditions that need OR across keys or per-condition tooltips.
|
||
const matchConstraint = (base, constraint) => {
|
||
if (!constraint || typeof constraint !== "object") return false;
|
||
const entries = Object.entries(constraint);
|
||
if (entries.length === 0) return false;
|
||
return entries.every(([k, vs]) =>
|
||
Array.isArray(vs) && vs.includes(base[k]));
|
||
};
|
||
|
||
// Normalize a chip entry into `{value, label?, hidden, disabled,
|
||
// disableReason, ...rest}`. `value` resolves to `entry.id` (rich form) or
|
||
// `entry.value` (wrapper form), or the entry itself for bare values.
|
||
const evaluateChip = (entry, base) => {
|
||
if (entry === null || typeof entry !== "object") {
|
||
return {
|
||
value: entry, label: undefined,
|
||
hidden: false, disabled: false, disableReason: "",
|
||
};
|
||
}
|
||
const hidden = entry.hide ? matchConstraint(base, entry.hide) : false;
|
||
let disabled = entry.disabled === true || entry.disable === true;
|
||
let disableReason = typeof entry.disableReason === "function"
|
||
? entry.disableReason(base)
|
||
: entry.disableReason || "";
|
||
if (!disabled && entry.disable && typeof entry.disable === "object") {
|
||
if (Array.isArray(entry.disable)) {
|
||
for (const item of entry.disable) {
|
||
const cond = (item && item.when) || item;
|
||
if (matchConstraint(base, cond)) {
|
||
disabled = true;
|
||
if (item && item.reason) disableReason = item.reason;
|
||
break;
|
||
}
|
||
}
|
||
} else {
|
||
disabled = matchConstraint(base, entry.disable);
|
||
}
|
||
}
|
||
return {
|
||
...entry,
|
||
value: entry.id !== undefined ? entry.id : entry.value,
|
||
label: entry.label,
|
||
hidden,
|
||
disabled,
|
||
disableReason,
|
||
};
|
||
};
|
||
|
||
const findEntry = (entries, picked) => {
|
||
for (const e of (entries || [])) {
|
||
const v = (e === null || typeof e !== "object")
|
||
? e : (e.id !== undefined ? e.id : e.value);
|
||
if (v === picked) return e;
|
||
}
|
||
return null;
|
||
};
|
||
const isHidden = (entries, picked, base) => {
|
||
const e = findEntry(entries, picked);
|
||
if (e === null || e === undefined) return false;
|
||
return evaluateChip(e, base).hidden;
|
||
};
|
||
|
||
// ==========================================================================
|
||
// 4. Flag/env mutation primitives
|
||
// ==========================================================================
|
||
// Strip flags whose first whitespace/equals-delimited token is in `prefixes`.
|
||
const stripFlagsByFirstToken = (flags, prefixes) => {
|
||
const set = new Set(prefixes);
|
||
return flags.filter((f) => !set.has(f.split(/[\s=]/)[0]));
|
||
};
|
||
|
||
// Strip env entries whose name (before `=`) is in `prefixes`.
|
||
const stripEnvByPrefix = (envList, prefixes) => {
|
||
if (!prefixes || !prefixes.length) return envList;
|
||
const set = new Set(prefixes);
|
||
return envList.filter((e) => !set.has(e.split("=")[0]));
|
||
};
|
||
|
||
// Insert new flags just before the trailing --host/--port pair.
|
||
const insertBeforeTail = (flags, additions) => {
|
||
const idx = flags.findIndex((f) => f.startsWith("--host"));
|
||
const at = idx === -1 ? flags.length : idx;
|
||
const out = flags.slice();
|
||
out.splice(at, 0, ...additions);
|
||
return out;
|
||
};
|
||
|
||
// Insert after the first present anchor in priority-ordered `afterAnyOf`,
|
||
// falling back to right-after --model-path. Priority order keeps insertion
|
||
// position-stable so the diff doesn't drop shared lines around the swap.
|
||
const insertAfter = (flags, afterAnyOf, additions) => {
|
||
let idx = -1;
|
||
for (const anchor of afterAnyOf) {
|
||
idx = flags.findIndex((f) => f.split(/[\s=]/)[0] === anchor);
|
||
if (idx !== -1) break;
|
||
}
|
||
if (idx === -1) idx = flags.findIndex((f) => f.startsWith("--model-path"));
|
||
const out = flags.slice();
|
||
out.splice(idx + 1, 0, ...additions);
|
||
return out;
|
||
};
|
||
|
||
// -------- Flag-reading helpers (used by `deriveFromBase` methods) ------
|
||
// First integer arg of `--prefix N` (space/`=`-delimited), or null.
|
||
const parseIntFlag = (flags, prefix) => {
|
||
for (const f of (flags || [])) {
|
||
if (f.split(/[\s=]/)[0] !== prefix) continue;
|
||
const rest = f.slice(prefix.length).replace(/^[\s=]+/, "");
|
||
const n = parseInt(rest, 10);
|
||
if (!isNaN(n)) return n;
|
||
}
|
||
return null;
|
||
};
|
||
// True if any flag's first token equals `name` (boolean flags).
|
||
const hasFlag = (flags, name) =>
|
||
(flags || []).some((f) => f.split(/[\s=]/)[0] === name);
|
||
// String arg of `--prefix arg` (space/`=`-delimited), or null.
|
||
const findFlagArg = (flags, prefix) => {
|
||
for (const f of (flags || [])) {
|
||
if (f.split(/[\s=]/)[0] !== prefix) continue;
|
||
const rest = f.slice(prefix.length).replace(/^[\s=]+/, "");
|
||
return rest.length ? rest : null;
|
||
}
|
||
return null;
|
||
};
|
||
|
||
// --tp / --ep / --dp spelling families: configs write either the canonical
|
||
// --tp-size / --ep-size / --dp-size, a short alias, or the long form. 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 DP_HEADS = ["--dp-size", "--dp", "--data-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-size", "--tp", "--model-path"];
|
||
const ANCHOR_NEAR_DP = ["--dp-size", "--dp", "--tp-size", "--tp", "--model-path"];
|
||
const ANCHOR_NEAR_DPATTN = ["--enable-dp-attention", "--dp-size", "--dp", "--tp-size", "--tp", "--model-path"];
|
||
const ANCHOR_NEAR_MOE = ["--moe-a2a-backend", "--moe-runner-backend",
|
||
"--enable-dp-attention", "--dp-size", "--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, DP_HEADS, parseIntFlagAny, flagSpelling,
|
||
ANCHOR_NEAR_MODEL_PATH, ANCHOR_NEAR_TP, ANCHOR_NEAR_DP,
|
||
ANCHOR_NEAR_DPATTN, ANCHOR_NEAR_MOE,
|
||
};
|
||
|
||
// -------- Prefill-CP flag family (shared by the attention axis) --------
|
||
// Every flag head that toggles/parameterizes prefill context parallelism:
|
||
// the canonical pair plus all per-family legacy spellings.
|
||
const CP_ENABLE_HEADS = [
|
||
"--enable-prefill-cp",
|
||
"--enable-nsa-prefill-context-parallel",
|
||
"--enable-dsa-prefill-context-parallel",
|
||
"--enable-prefill-context-parallel",
|
||
];
|
||
const CP_MODE_HEADS = [
|
||
"--nsa-prefill-cp-mode", "--dsa-prefill-cp-mode", "--prefill-cp-mode",
|
||
];
|
||
const CP_OWNED_HEADS = [
|
||
...CP_ENABLE_HEADS, ...CP_MODE_HEADS, "--cp-strategy", "--attn-cp-size",
|
||
];
|
||
// Legacy mode spellings → new-style --cp-strategy values.
|
||
const CP_MODE_TO_STRATEGY = {
|
||
"in-seq-split": "zigzag",
|
||
"round-robin-split": "interleave",
|
||
};
|
||
const cpEnabledIn = (flags) =>
|
||
CP_ENABLE_HEADS.some((head) => hasFlag(flags, head));
|
||
// Strategy a cell's flags carry: --cp-strategy first, else a mapped legacy
|
||
// mode flag, else null (no strategy baked).
|
||
const bakedCpStrategy = (flags) =>
|
||
findFlagArg(flags, "--cp-strategy")
|
||
|| CP_MODE_TO_STRATEGY[findFlagArg(flags, "--nsa-prefill-cp-mode")]
|
||
|| CP_MODE_TO_STRATEGY[findFlagArg(flags, "--dsa-prefill-cp-mode")]
|
||
|| CP_MODE_TO_STRATEGY[findFlagArg(flags, "--prefill-cp-mode")]
|
||
|| null;
|
||
|
||
// ==========================================================================
|
||
// 5. AXIS_HANDLERS — the built-in playground axis registry
|
||
// ==========================================================================
|
||
// 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" /
|
||
// "disabled" / false — apply no-ops on these (except always-strip axes).
|
||
// - apply must not mutate its inputs.
|
||
// - strip policy: most axes strip ONLY when overridden; pdDisagg and
|
||
// hicache strip unconditionally.
|
||
const AXIS_HANDLERS = {
|
||
|
||
// ---- Axis: Attention Parallelism ----------------------------------------
|
||
// TP / CP / DP-Attention sub-knobs; `null` = inherit. DP-Attention is
|
||
// combined: a numeric value emits `--dp N --enable-dp-attention`, `false`
|
||
// strips both. An optional `cpStrategy` knob (values from --cp-strategy:
|
||
// "zigzag" / "interleave") picks the CP layout; without it the strategy
|
||
// baked in the base is preserved, defaulting to "interleave" (the legacy
|
||
// knob's round-robin-split).
|
||
attention: {
|
||
initState: () => ({ tp: null, cp: null, cpStrategy: null, dpAttn: null }),
|
||
|
||
// DP-Attention: `--dp N --enable-dp-attention` → N; neither → false;
|
||
// bare `--enable-dp-attention` → 1. CP: any enable spelling →
|
||
// `--attn-cp-size N` (bare enable → 2, the legacy convention), plus the
|
||
// baked strategy (legacy mode flags mapped to zigzag/interleave).
|
||
deriveFromBase: (cell, fc, h) => {
|
||
const flags = (cell && cell.flags) || [];
|
||
const dpVal = h.parseIntFlagAny(flags, h.DP_HEADS);
|
||
const hasDpAttn = h.hasFlag(flags, "--enable-dp-attention");
|
||
let dpAttn;
|
||
if (dpVal !== null) dpAttn = dpVal;
|
||
else if (hasDpAttn) dpAttn = 1;
|
||
else dpAttn = false;
|
||
const cpSize = h.parseIntFlag(flags, "--attn-cp-size");
|
||
return {
|
||
tp: h.parseIntFlagAny(flags, h.TP_HEADS),
|
||
cp: cpEnabledIn(flags) ? (cpSize !== null ? cpSize : 2) : null,
|
||
cpStrategy: bakedCpStrategy(flags),
|
||
dpAttn,
|
||
};
|
||
},
|
||
|
||
apply: ({ flags, env, value, fc, sel, h }) => {
|
||
// Live facts for constraint checks (same keys the render-side
|
||
// constraintBase exposes), recomputed after each mutation.
|
||
const knobEntry = (id) => (fc.knobs || []).find((k) => k.id === id) || {};
|
||
const factsNow = () => ({
|
||
...(sel || {}),
|
||
dpAttnOn: h.hasFlag(flags, "--enable-dp-attention"),
|
||
cpOn: cpEnabledIn(flags),
|
||
cpStrategy: bakedCpStrategy(flags) || "interleave",
|
||
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
|
||
// off only CP == TP is real. With DP-Attention on the sizes are NOT
|
||
// gated: CP + DP-Attention is an allowed experiment (warning hint
|
||
// below the command box). Mirrors the render-side auto-gating; a
|
||
// config opts out entirely with `freeSize: true` on the cp knob.
|
||
const cpSizeTargetNow = () => {
|
||
if (knobEntry("cp").freeSize) return null;
|
||
const dpIntent = (value.dpAttn !== null && value.dpAttn !== undefined)
|
||
? value.dpAttn
|
||
: (h.hasFlag(flags, "--enable-dp-attention")
|
||
? (h.parseIntFlagAny(flags, h.DP_HEADS) ?? 1) : false);
|
||
if (typeof dpIntent === "number" && dpIntent > 1) return null;
|
||
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
|
||
// emits a blocked combination.
|
||
const blocked = (id, v) => {
|
||
const facts = factsNow();
|
||
const kc = h.evaluateChip(knobEntry(id), facts);
|
||
if (kc.hidden || kc.disabled) return true;
|
||
if (id === "cp" && typeof v === "number" && v > 1) {
|
||
const target = cpSizeTargetNow();
|
||
if (target !== null && v !== target) return true;
|
||
}
|
||
const e = h.findEntry(knobEntry(id).values || [], v);
|
||
return !!(e !== null && e !== undefined
|
||
&& h.evaluateChip(e, facts).disabled);
|
||
};
|
||
// NOTE: interleave prefill-CP + DP-Attention currently fails the
|
||
// runtime's dp_size == 1 assert, but combined support is planned
|
||
// upstream — the combination is allowed here (with a warning hint
|
||
// below the command box) rather than banned.
|
||
|
||
if (value.tp !== null && !blocked("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 >
|
||
// baked-in-base > "interleave" (the legacy knob's round-robin-split).
|
||
const cpStrategyOverride =
|
||
(value.cpStrategy && !blocked("cpStrategy", value.cpStrategy))
|
||
? value.cpStrategy : null;
|
||
const cpPick = (value.cp !== null)
|
||
? value.cp
|
||
: ((cpStrategyOverride && cpEnabledIn(flags))
|
||
? (h.parseIntFlag(flags, "--attn-cp-size") ?? 2)
|
||
: null);
|
||
const cpStrategyPick =
|
||
cpStrategyOverride || bakedCpStrategy(flags) || "interleave";
|
||
if (cpPick !== null && !blocked("cp", cpPick)) {
|
||
// Own the whole CP flag family (canonical + every legacy spelling)
|
||
// so an override fully replaces (or removes) whatever the base
|
||
// recipe baked in.
|
||
flags = h.stripFlagsByFirstToken(flags, CP_OWNED_HEADS);
|
||
if (cpPick > 1) {
|
||
flags = h.insertAfter(flags, h.ANCHOR_NEAR_DPATTN, [
|
||
`--attn-cp-size ${cpPick}`,
|
||
"--enable-prefill-cp",
|
||
`--cp-strategy ${cpStrategyPick}`,
|
||
]);
|
||
}
|
||
}
|
||
if (value.dpAttn !== null && value.dpAttn !== undefined
|
||
&& !blocked("dpAttn", value.dpAttn)) {
|
||
// Capture the spelling before stripping — the TP/EP handlers do the
|
||
// same, and a lookup on the stripped array always hits the fallback.
|
||
const dpHead = h.flagSpelling(flags, h.DP_HEADS, "--dp-size");
|
||
flags = h.stripFlagsByFirstToken(flags, [...h.DP_HEADS, "--enable-dp-attention"]);
|
||
if (typeof value.dpAttn === "number" && value.dpAttn > 0) {
|
||
flags = h.insertAfter(flags, h.ANCHOR_NEAR_TP, [
|
||
`${dpHead} ${value.dpAttn}`,
|
||
"--enable-dp-attention",
|
||
]);
|
||
}
|
||
}
|
||
return { flags, env };
|
||
},
|
||
|
||
render: ({ axisId, value, setValue, fc, base, s, h, renderSelect, derived }) => {
|
||
const knobs = fc.knobs || [];
|
||
if (!knobs.length) return null;
|
||
const setKnob = (k, v) => setValue({ ...value, [k]: v });
|
||
// Interleave prefill-CP + DP-Attention is deliberately NOT grayed:
|
||
// current releases assert dp_size == 1 for interleave, but combined
|
||
// support is planned upstream — a warning hint below the command box
|
||
// covers it instead.
|
||
const labelFor = (knob) => (c) => {
|
||
if (c.label !== undefined) return c.label;
|
||
if (knob.id === "dpAttn") {
|
||
const labelMap = knob.labels || { "auto": "Auto", "false": "Off" };
|
||
const k = c.value === null ? "auto" : String(c.value);
|
||
return labelMap[k] || k;
|
||
}
|
||
return c.value === null ? "Auto" : String(c.value);
|
||
};
|
||
// Display: explicit pick > derived > null sentinel.
|
||
const knobDisplay = (knob) => {
|
||
const v = value[knob.id];
|
||
if (v !== null && v !== undefined) return v;
|
||
if (derived && derived[knob.id] !== undefined) return derived[knob.id];
|
||
return null;
|
||
};
|
||
const hideNullFor = (knob) => {
|
||
const d = derived ? derived[knob.id] : null;
|
||
return (d !== null && d !== undefined) ? [null] : [];
|
||
};
|
||
// Auto-gate CP sizes to the runtime derivation attn_cp_size = tp/dp
|
||
// (mirrors apply's cpSizeTargetNow; `freeSize: true` opts out).
|
||
const entriesFor = (knob) => {
|
||
const vals = knob.values || [null];
|
||
if (knob.id !== "cp" || knob.freeSize) return vals;
|
||
const target = base.cpSizeTarget;
|
||
if (target === null || target === undefined) return vals;
|
||
return vals.map((entry) => {
|
||
const v = (entry === null || typeof entry !== "object")
|
||
? entry : (entry.id !== undefined ? entry.id : entry.value);
|
||
if (typeof v !== "number" || v <= 1 || v === target) return entry;
|
||
const wrapped = (entry === null || typeof entry !== "object")
|
||
? { value: entry } : { ...entry };
|
||
return {
|
||
...wrapped,
|
||
disabled: true,
|
||
disableReason: `SGLang derives the prefill-CP size as attn_cp_size = TP / DP-Attention (= ${target} here), so only that size can be enabled.`,
|
||
};
|
||
});
|
||
};
|
||
return (
|
||
<div key={axisId} style={s.card}>
|
||
<div style={s.compactRow}>
|
||
<span style={s.axisTitle}>Attention</span>
|
||
{knobs.map((knob) => {
|
||
const kc = h.evaluateChip(knob, base);
|
||
if (kc.hidden) return null;
|
||
return (
|
||
<span key={knob.id} style={s.field}>
|
||
<span style={s.fieldLabel}>{knob.label || knob.id.toUpperCase()}</span>
|
||
{renderSelect(knobDisplay(knob), entriesFor(knob),
|
||
(nv) => setKnob(knob.id, nv), base, labelFor(knob),
|
||
{ hideValues: hideNullFor(knob),
|
||
disabled: kc.disabled,
|
||
disabledReason: kc.disableReason })}
|
||
</span>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
);
|
||
},
|
||
},
|
||
|
||
// ---- Axis: MoE Parallelism ----------------------------------------------
|
||
// Backend single-select + EP numeric knob; either is optional. Picking the
|
||
// "megamoe" backend reveals a Quantization sub-select (W4A8 / W4A4) in the same
|
||
// row — W4A4 adds the FP4-activations server flag.
|
||
moe: {
|
||
initState: () => ({ backend: null, ep: null, mmQuant: null }),
|
||
|
||
// Prefer --moe-a2a-backend over --moe-runner-backend when both present.
|
||
// mmQuant is derived from the base flag (FP4 activations present → W4A4).
|
||
deriveFromBase: (cell, fc, h) => {
|
||
const flags = (cell && cell.flags) || [];
|
||
const a2a = h.findFlagArg(flags, "--moe-a2a-backend");
|
||
const runner = h.findFlagArg(flags, "--moe-runner-backend");
|
||
const w4a4 = h.hasFlag(flags, "--enable-w4a4-mxfp4-megamoe");
|
||
return {
|
||
backend: a2a || runner || null,
|
||
ep: h.parseIntFlagAny(flags, h.EP_HEADS),
|
||
mmQuant: w4a4 ? "w4a4" : "w4a8",
|
||
};
|
||
},
|
||
|
||
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
|
||
// backend-specific 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 quant settings, then re-add the
|
||
// selected quant's flags/env. When the backend is explicitly switched
|
||
// away from MegaMoE, only drop the MegaMoE quant settings (leave DeepEP
|
||
// dispatch intact).
|
||
const mq = fc.megamoeQuant;
|
||
if (mq) {
|
||
const quantKeys = [];
|
||
const quantFlagHeads = [];
|
||
for (const o of (mq.options || [])) {
|
||
for (const e of (o.env || [])) quantKeys.push(e.split("=")[0]);
|
||
for (const f of (o.flags || [])) quantFlagHeads.push(f.split(/[\s=]/)[0]);
|
||
}
|
||
flags = h.stripFlagsByFirstToken(flags, quantFlagHeads);
|
||
const effBackend = value.backend !== null
|
||
? value.backend : (derived && derived.backend);
|
||
if (effBackend === "megamoe") {
|
||
env = h.stripEnvByPrefix(env, [...(mq.stripEnv || []), ...quantKeys]);
|
||
const quant = value.mmQuant != null
|
||
? value.mmQuant : ((derived && derived.mmQuant) || "w4a8");
|
||
const opt = (mq.options || []).find((o) => o.id === quant);
|
||
if (opt?.flags?.length) {
|
||
flags = h.insertAfter(flags, h.ANCHOR_NEAR_MOE, opt.flags);
|
||
}
|
||
if (opt?.env?.length) env = [...env, ...opt.env];
|
||
} else if (value.backend !== null) {
|
||
env = h.stripEnvByPrefix(env, quantKeys);
|
||
}
|
||
}
|
||
if (value.ep !== null) {
|
||
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, [`${epHead} ${value.ep}`]);
|
||
}
|
||
}
|
||
return { flags, env };
|
||
},
|
||
|
||
render: ({ axisId, value, setValue, fc, base, s, renderSelect, derived }) => {
|
||
if (!fc.backend && !fc.ep) return null;
|
||
const setSlot = (k, v) => setValue({ ...value, [k]: v });
|
||
// Display: explicit > derived > null.
|
||
const slotDisplay = (k) => {
|
||
const v = value[k];
|
||
if (v !== null && v !== undefined) return v;
|
||
if (derived && derived[k] !== undefined) return derived[k];
|
||
return null;
|
||
};
|
||
const hideNull = (k) => {
|
||
const d = derived ? derived[k] : null;
|
||
return (d !== null && d !== undefined) ? [null] : [];
|
||
};
|
||
// Hide the MegaMoE backend option where its requiresHw / excludesStrategy exclude this base.
|
||
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));
|
||
const backendIsMega = slotDisplay("backend") === "megamoe";
|
||
// `ep.showWhen` (function of base) drops the whole EP select on bases
|
||
// where EP is not a supported lever (e.g. the single-shape A3 recipe).
|
||
const epShown = !!fc.ep
|
||
&& !(typeof fc.ep.showWhen === "function" && !fc.ep.showWhen(base));
|
||
return (
|
||
<div key={axisId} style={s.card}>
|
||
<div style={s.compactRow}>
|
||
<span style={s.axisTitle}>MoE</span>
|
||
{fc.backend && (
|
||
<span style={s.field}>
|
||
<span style={s.fieldLabel}>Backend</span>
|
||
{renderSelect(slotDisplay("backend"), fc.backend.options || [],
|
||
(v) => setSlot("backend", v), base, undefined,
|
||
{ hideValues: [...hideNull("backend"), ...(mmAvail ? [] : ["megamoe"])] })}
|
||
</span>
|
||
)}
|
||
{fc.megamoeQuant && backendIsMega && (
|
||
<span style={s.field}>
|
||
<span style={s.fieldLabel}>Quantization</span>
|
||
{renderSelect(
|
||
value.mmQuant != null ? value.mmQuant : ((derived && derived.mmQuant) || "w4a8"),
|
||
fc.megamoeQuant.options || [],
|
||
(v) => setSlot("mmQuant", v), base)}
|
||
</span>
|
||
)}
|
||
{epShown && (
|
||
<span style={s.field}>
|
||
<span style={s.fieldLabel}>{fc.ep.label || "EP"}</span>
|
||
{renderSelect(slotDisplay("ep"), fc.ep.values || [null],
|
||
(v) => setSlot("ep", v), base, undefined,
|
||
{ hideValues: hideNull("ep") })}
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
},
|
||
},
|
||
|
||
// ---- Axis: Parsers ------------------------------------------------------
|
||
// Per-item toggles (tri-state: null = inherit). Strips only when overridden,
|
||
// then re-emits one item.flag per toggled-on item.
|
||
parsers: {
|
||
initState: (fc) => {
|
||
const out = {};
|
||
for (const item of (fc.items || [])) out[item.id] = null;
|
||
return out;
|
||
},
|
||
|
||
deriveFromBase: (cell, fc, h) => {
|
||
const flags = (cell && cell.flags) || [];
|
||
const out = {};
|
||
for (const item of (fc.items || [])) {
|
||
const prefix = item.flag.split(/[\s=]/)[0];
|
||
out[item.id] = h.hasFlag(flags, prefix);
|
||
}
|
||
return out;
|
||
},
|
||
|
||
apply: ({ flags, env, value, fc, h, derived }) => {
|
||
const items = fc.items || [];
|
||
// Effective state per item: explicit > derived > false. Skip
|
||
// strip+emit when nothing differs from base.
|
||
const eff = {};
|
||
const baseOf = {};
|
||
for (const item of items) {
|
||
baseOf[item.id] = derived ? !!derived[item.id] : false;
|
||
const v = value[item.id];
|
||
eff[item.id] = (v === null || v === undefined) ? baseOf[item.id] : v;
|
||
}
|
||
const anyOverride = items.some((it) => eff[it.id] !== baseOf[it.id]);
|
||
if (!anyOverride) return { flags, env };
|
||
flags = h.stripFlagsByFirstToken(flags, ["--reasoning-parser", "--tool-call-parser"]);
|
||
const adds = [];
|
||
for (const item of items) {
|
||
if (eff[item.id]) adds.push(item.flag);
|
||
}
|
||
if (adds.length) flags = h.insertBeforeTail(flags, adds);
|
||
return { flags, env };
|
||
},
|
||
|
||
render: ({ axisId, value, setValue, fc, base, s, h, renderChip, derived }) => {
|
||
const visible = (fc.items || [])
|
||
.map((item) => ({ item, c: h.evaluateChip(item, base) }))
|
||
.filter(({ c }) => !c.hidden);
|
||
if (visible.length === 0) return null;
|
||
// Effective on/off: explicit pick > derived > off.
|
||
const effOn = (id) => {
|
||
const v = value[id];
|
||
if (v !== null && v !== undefined) return v;
|
||
if (derived && derived[id] !== undefined) return derived[id];
|
||
return false;
|
||
};
|
||
return (
|
||
<div key={axisId} style={s.card}>
|
||
<div style={s.compactRow}>
|
||
<span style={s.axisTitle}>Parsers</span>
|
||
{visible.map(({ item, c }) => (
|
||
<span key={item.id} style={s.field}>
|
||
{renderChip(item.label, effOn(item.id), true,
|
||
() => setValue({ ...value, [item.id]: !effOn(item.id) }),
|
||
{ disabled: c.disabled, disabledReason: c.disableReason })}
|
||
</span>
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
},
|
||
},
|
||
|
||
// ---- Axis: Speculative Decoding -----------------------------------------
|
||
// Single-select. "current" = leave base untouched, "off" = strip (greedy),
|
||
// <other> = strip + splice option.flags.
|
||
speculative: {
|
||
initState: () => "current",
|
||
|
||
// Match base's `--speculative-*` set against each option. No spec flags
|
||
// → "off"; some but no match → "current".
|
||
deriveFromBase: (cell, fc) => {
|
||
const flags = (cell && cell.flags) || [];
|
||
const baseSpec = flags.filter((f) => {
|
||
const head = f.split(/[\s=]/)[0];
|
||
return head === "--speculative-algorithm"
|
||
|| head === "--speculative-num-steps"
|
||
|| head === "--speculative-eagle-topk"
|
||
|| head === "--speculative-num-draft-tokens"
|
||
// Adaptive draft depth is part of an EAGLE preset, not a
|
||
// separate knob: a base that carries it must strip it when
|
||
// another algorithm is picked, or the flag survives and the
|
||
// server warns it away (only EAGLE/EAGLE3 honor it).
|
||
|| head === "--speculative-adaptive"
|
||
|| head === "--speculative-dspark-block-size"
|
||
|| head === "--enable-linear-replayssm-spec"
|
||
|| head === "--linear-replayssm-cache-len"
|
||
|| head === "--speculative-ngram-max-bfs-breadth";
|
||
});
|
||
if (baseSpec.length === 0) return "off";
|
||
for (const opt of (fc.options || [])) {
|
||
if (!opt.flags || opt.flags.length !== baseSpec.length) continue;
|
||
const ok = opt.flags.every((pf) => baseSpec.includes(pf));
|
||
if (ok) return opt.id;
|
||
}
|
||
return "current";
|
||
},
|
||
|
||
apply: ({ flags, env, value, fc, sel, h, derived }) => {
|
||
if (value === "current") return { flags, env };
|
||
// No-op when the pick already matches base (preserves flag position).
|
||
if (derived && value === derived) return { flags, env };
|
||
const picked = (fc.options || []).find((p) => p.id === value);
|
||
if (picked && h.evaluateChip(picked, {
|
||
...sel,
|
||
dpAttnOn: h.hasFlag(flags, "--enable-dp-attention"),
|
||
}).disabled) {
|
||
return { flags, env };
|
||
}
|
||
flags = h.stripFlagsByFirstToken(flags, [
|
||
"--speculative-algorithm", "--speculative-num-steps",
|
||
"--speculative-eagle-topk", "--speculative-num-draft-tokens",
|
||
"--speculative-adaptive",
|
||
"--speculative-dspark-block-size", "--enable-linear-replayssm-spec",
|
||
"--linear-replayssm-cache-len",
|
||
"--speculative-ngram-max-bfs-breadth",
|
||
]);
|
||
const preset = (fc.options || []).find((p) => p.id === value);
|
||
if (preset?.flags?.length) flags = h.insertBeforeTail(flags, preset.flags);
|
||
return { flags, env };
|
||
},
|
||
|
||
render: ({ axisId, value, setValue, fc, base, s, h, renderChip, derived }) => {
|
||
const opts = fc.options || [];
|
||
if (!opts.length) return null;
|
||
// Display: explicit pick (!= "current") > derived.
|
||
const display = (value !== "current") ? value
|
||
: (derived ? derived : "current");
|
||
// Hide "current" when derive resolved to a real preset.
|
||
const hideCurrent = !!(derived && derived !== "current");
|
||
const visible = opts
|
||
.map((opt) => h.evaluateChip(opt, base))
|
||
.filter((c) => !c.hidden && !(hideCurrent && c.value === "current"));
|
||
if (visible.length === 0) return null;
|
||
// An option may carry a `note`: a prerequisite the reader must act on
|
||
// before the composed command runs at all (an algorithm whose support
|
||
// is not in the page's pinned image yet, a draft checkpoint to fetch).
|
||
// Shown only for the option in effect, so the card stays a chip row
|
||
// until the pick actually needs something.
|
||
const note = (visible.find((c) => c.value === display) || {}).note;
|
||
return (
|
||
<div key={axisId} style={s.card}>
|
||
<div style={s.compactRow}>
|
||
<span style={s.axisTitle}>Speculative</span>
|
||
{visible.map((c) => (
|
||
<span key={c.value} style={s.field}>
|
||
{renderChip(c.label, display, c.value,
|
||
() => setValue(c.value),
|
||
{ disabled: c.disabled, disabledReason: c.disableReason })}
|
||
</span>
|
||
))}
|
||
</div>
|
||
{note && <div style={s.axisNote}>{note}</div>}
|
||
</div>
|
||
);
|
||
},
|
||
},
|
||
|
||
// ---- Axis: PD Disaggregation --------------------------------------------
|
||
// Role (off/prefill/decode) + transfer backend + optional IB device.
|
||
// Owns the `--disaggregation-*` flags (unconditional strip). A backend may
|
||
// carry hw-gated env (transferBackends[].env + .envWhen).
|
||
pdDisagg: {
|
||
// 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 (mode === "prefill" || mode === "decode") {
|
||
// PD and some speculative algorithms cannot run together. Keep the
|
||
// PD card reachable for a speculative base recipe, then make the
|
||
// user's explicit PD-role selection win by removing the whole
|
||
// speculative flag family before composing the role command.
|
||
const specAlgorithm = (h.findFlagArg(
|
||
flags, "--speculative-algorithm") || "").toUpperCase();
|
||
if ((fc.incompatibleSpeculativeAlgorithms || []).includes(specAlgorithm)) {
|
||
flags = flags.filter((flag) =>
|
||
!flag.split(/[\s=]/)[0].startsWith("--speculative-"));
|
||
}
|
||
const backend = value.transferBackend || (backends[0] || {}).id || "mooncake";
|
||
const adds = [
|
||
`--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}`);
|
||
}
|
||
// A `modes[]` entry may declare `flags` / `env` that only the PD role
|
||
// it names needs (the prefill worker's balance policy, the decode
|
||
// worker's polling interval, ...). Strip the same heads first so the
|
||
// role's value wins over a base cell that sets one for its own
|
||
// reasons, instead of emitting the flag twice. This runs only inside
|
||
// the role branch: applyAllDeltas re-seeds from the base cell on every
|
||
// render, so a base flag is never left over from an earlier selection
|
||
// and must not be stripped when the role is Off.
|
||
const modeMeta = (fc.modes || []).find((m) => m.id === mode);
|
||
if (modeMeta && modeMeta.flags && modeMeta.flags.length) {
|
||
flags = h.stripFlagsByFirstToken(
|
||
flags, modeMeta.flags.map((f) => f.split(/[\s=]/)[0]));
|
||
adds.push(...modeMeta.flags);
|
||
}
|
||
// Single-host needs no --dist-init-addr: prefill/decode derive their
|
||
// ZMQ/dist ports from the role-specific --port (spaced 100 apart, see
|
||
// PD_PORTS), so the ranges don't overlap. Multi-node still gets a
|
||
// cross-node --dist-init-addr from the renderer.
|
||
flags = h.insertBeforeTail(flags, adds);
|
||
|
||
// 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[mode].serve;
|
||
flags = flags.map((f) =>
|
||
f.split(/[\s=]/)[0] === "--port" ? `--port ${servePort}` : f);
|
||
|
||
// Add the selected backend's env (gated by hw via `envWhen`), keeping
|
||
// any the base cell already carries in place. We don't strip base env
|
||
// (e.g. gb200 NCCL_*): a blanket strip would drop it when PD is off and
|
||
// show a spurious remove+add in the diff. apply is pure from baseEnv, so
|
||
// no stale backend env accumulates across renders.
|
||
const meta = backends.find((b) => b.id === backend);
|
||
if (meta && meta.env && meta.env.length) {
|
||
const gate = meta.envWhen;
|
||
const ok = !gate || Object.keys(gate).every(
|
||
(k) => (gate[k] || []).includes(sel[k]));
|
||
if (ok) env = [...env, ...meta.env.filter((e) => !env.includes(e))];
|
||
}
|
||
// Same for env declared on the selected role.
|
||
if (modeMeta && modeMeta.env && modeMeta.env.length) {
|
||
env = [...env, ...modeMeta.env.filter((e) => !env.includes(e))];
|
||
}
|
||
}
|
||
return { flags, env };
|
||
},
|
||
|
||
getRenderHints: (value, fc, context) => {
|
||
const specAlgorithm = (context.h.findFlagArg(
|
||
context.flags, "--speculative-algorithm") || "").toUpperCase();
|
||
if ((fc.incompatibleSpeculativeAlgorithms || []).includes(specAlgorithm)) {
|
||
return null;
|
||
}
|
||
if (value.mode === "prefill" || value.mode === "decode") {
|
||
return { pdMode: value.mode };
|
||
}
|
||
return null;
|
||
},
|
||
|
||
render: ({ axisId, value, setValue, fc, base, s, renderSelect }) => {
|
||
const setSlot = (k, v) => setValue({ ...value, [k]: v });
|
||
const showModes = (fc.modes || []).length > 0;
|
||
const showBackends = (fc.transferBackends || []).length > 0;
|
||
const showIb = (fc.ibDevices || []).length > 0;
|
||
if (!showModes && !showBackends && !showIb) return null;
|
||
return (
|
||
<div key={axisId} style={s.card}>
|
||
<div style={s.compactRow}>
|
||
<span style={s.axisTitle}>PD Disagg</span>
|
||
{showModes && (
|
||
<span style={s.field}>
|
||
<span style={s.fieldLabel}>Mode</span>
|
||
{renderSelect(value.mode, fc.modes,
|
||
(v) => setSlot("mode", v), base)}
|
||
</span>
|
||
)}
|
||
{showBackends && (
|
||
<span style={s.field}>
|
||
<span style={s.fieldLabel}>Transfer Backend</span>
|
||
{renderSelect(value.transferBackend, fc.transferBackends,
|
||
(v) => setSlot("transferBackend", v), base)}
|
||
</span>
|
||
)}
|
||
{showIb && (
|
||
<span style={s.field}>
|
||
<span style={s.fieldLabel}>IB Device</span>
|
||
{renderSelect(value.ibDevice, fc.ibDevices,
|
||
(v) => setSlot("ibDevice", v), base)}
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
},
|
||
},
|
||
|
||
// ---- Axis: HiSparse (hierarchical sparse attention) ---------------------
|
||
// Enable + host_to_device_ratio. Owns `--enable-hisparse`/`--hisparse-config`
|
||
// plus fc.requiredFlags (unconditional strip, re-added when enabled).
|
||
// Decode-only: gated on PD-Disagg mode == "decode" in both render and apply.
|
||
hisparse: {
|
||
initState: (fc) => ({ enable: false, hostRatio: (fc && fc.defaultHostRatio) || null }),
|
||
|
||
apply: ({ flags, env, value, fc, h }) => {
|
||
const ownedHeads = [
|
||
"--enable-hisparse", "--hisparse-config",
|
||
...((fc.requiredFlags || []).map((f) => f.split(/\s/)[0])),
|
||
];
|
||
flags = h.stripFlagsByFirstToken(flags, ownedHeads);
|
||
// Decode gate: pdDisagg runs first and inserts this flag.
|
||
const isDecode = flags.includes("--disaggregation-mode decode");
|
||
if (value.enable && isDecode) {
|
||
const ratio = (value.hostRatio !== null && value.hostRatio !== undefined)
|
||
? value.hostRatio
|
||
: (fc.defaultHostRatio || 10);
|
||
const cfg = { ...(fc.config || {}), host_to_device_ratio: ratio };
|
||
const adds = [
|
||
...(fc.requiredFlags || []),
|
||
"--enable-hisparse",
|
||
`--hisparse-config '${JSON.stringify(cfg)}'`,
|
||
];
|
||
flags = h.insertBeforeTail(flags, adds);
|
||
}
|
||
return { flags, env };
|
||
},
|
||
|
||
render: ({ axisId, value, setValue, fc, base, s, renderChip, renderSelect }) => {
|
||
if (base.pdMode !== "decode") return null;
|
||
const setSlot = (k, v) => setValue({ ...value, [k]: v });
|
||
const hasRatios = (fc.hostRatios || []).length > 0;
|
||
return (
|
||
<div key={axisId} style={s.card}>
|
||
<div style={s.compactRow}>
|
||
<span style={s.axisTitle}>HiSparse</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>
|
||
{renderSelect(value.hostRatio, fc.hostRatios,
|
||
(v) => setSlot("hostRatio", v), base)}
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
},
|
||
},
|
||
|
||
// ---- Axis: Hierarchical KV Cache ----------------------------------------
|
||
// Enable + optional backend + write policy. `null` inherits the base cell,
|
||
// so a verified HiCache recipe remains byte-identical until it is changed.
|
||
hicache: {
|
||
initState: () => ({ enable: null, backend: null, writePolicy: "auto" }),
|
||
|
||
deriveFromBase: (cell, fc, h) => {
|
||
const flags = (cell && cell.flags) || [];
|
||
return {
|
||
enable: h.hasFlag(flags, "--enable-hierarchical-cache"),
|
||
backend: h.findFlagArg(flags, "--hicache-storage-backend"),
|
||
writePolicy: h.findFlagArg(flags, "--hicache-write-policy") || "auto",
|
||
};
|
||
},
|
||
|
||
apply: ({ flags, env, value, fc, sel, h, derived }) => {
|
||
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 };
|
||
}
|
||
|
||
const hasOverride = value.enable !== null
|
||
|| value.backend !== null
|
||
|| (value.writePolicy && value.writePolicy !== "auto");
|
||
if (!hasOverride) return { flags, env };
|
||
|
||
const backendOptions = fc.backends || [];
|
||
const ownedHeads = [
|
||
"--enable-hierarchical-cache", "--hicache-ratio", "--hicache-size",
|
||
"--hicache-write-policy", "--hicache-mem-layout", "--hicache-io-backend",
|
||
"--hicache-storage-backend", "--hicache-storage-prefetch-policy",
|
||
"--hicache-storage-backend-extra-config",
|
||
...((fc.requiredFlags || []).map((f) => f.split(/\s/)[0])),
|
||
...backendOptions.flatMap((o) => (o.flags || []).map((f) => f.split(/\s/)[0])),
|
||
];
|
||
const ownedEnvKeys = [
|
||
...(fc.requiredEnv || []),
|
||
...backendOptions.flatMap((o) => o.env || []),
|
||
].map((e) => e.split("=")[0]);
|
||
flags = h.stripFlagsByFirstToken(flags, ownedHeads);
|
||
if (ownedEnvKeys.length) env = h.stripEnvByPrefix(env, ownedEnvKeys);
|
||
|
||
const enabled = value.enable !== null
|
||
? value.enable : !!(derived && derived.enable);
|
||
const backend = value.backend !== null
|
||
? value.backend
|
||
: ((derived && derived.backend) || fc.defaultBackend || null);
|
||
if (enabled) {
|
||
const isAmd = sel && /^mi\d/.test(sel.hw);
|
||
const pdMode = h.findFlagArg(flags, "--disaggregation-mode") || "off";
|
||
const pdBackend = h.findFlagArg(flags, "--disaggregation-transfer-backend");
|
||
const roleOverride = (fc.roleOverrides || []).find((item) => {
|
||
if (!item || item.mode !== pdMode) return false;
|
||
if (item.transferBackend && item.transferBackend !== pdBackend) return false;
|
||
return !item.when || h.matchConstraint(sel, item.when);
|
||
});
|
||
const amdIo = roleOverride || (isAmd && fc.amdIo);
|
||
const ratio = (amdIo && amdIo.ratio) || 2;
|
||
const useAmdIo = isAmd && amdIo;
|
||
const adds = [
|
||
"--enable-hierarchical-cache",
|
||
`--hicache-ratio ${ratio}`,
|
||
];
|
||
if (!useAmdIo) {
|
||
adds.push("--hicache-size 0");
|
||
}
|
||
// Per-model configs can declare fc.amdIo = { memLayout, ioBackend, ratio }
|
||
// for AMD ROCm overrides (e.g. page_first_direct + direct on MI355X).
|
||
// Default (NVIDIA): page_first_direct + direct, ratio 2.
|
||
if (useAmdIo) {
|
||
adds.push(`--hicache-mem-layout ${amdIo.memLayout}`,
|
||
`--hicache-io-backend ${amdIo.ioBackend}`);
|
||
} else if (backend) {
|
||
adds.push("--hicache-mem-layout page_first_direct",
|
||
"--hicache-io-backend direct");
|
||
}
|
||
const writePolicy = (value.writePolicy && value.writePolicy !== "auto")
|
||
? value.writePolicy : ((amdIo && amdIo.writePolicy) || "write_through");
|
||
adds.push(`--hicache-write-policy ${writePolicy}`);
|
||
// When amdStorageFileOnly is set, AMD emits storage flags only for "file".
|
||
if ((isAmd && fc.amdStorageFileOnly) ? backend === "file" : !!backend) {
|
||
adds.push(`--hicache-storage-backend ${backend}`,
|
||
`--hicache-storage-prefetch-policy ${(amdIo && amdIo.prefetchPolicy) || "wait_complete"}`);
|
||
} else if (amdIo && amdIo.prefetchPolicy) {
|
||
adds.push(`--hicache-storage-prefetch-policy ${amdIo.prefetchPolicy}`);
|
||
}
|
||
const backendOption = backendOptions.find((o) => o.id === backend);
|
||
adds.push(...(backendOption?.flags || []), ...(fc.requiredFlags || []));
|
||
flags = h.insertBeforeTail(flags, adds);
|
||
env = [
|
||
...env,
|
||
...(backendOption?.env || []),
|
||
...(fc.requiredEnv || []),
|
||
];
|
||
}
|
||
return { flags, env };
|
||
},
|
||
|
||
render: ({ axisId, value, setValue, fc, base, s, renderChip, renderSelect, derived }) => {
|
||
if (fc.excludesHw && fc.excludesHw.includes(base.hw)) return null;
|
||
const setSlot = (k, v) => setValue({ ...value, [k]: v });
|
||
const hasBackends = (fc.backends || []).length > 0;
|
||
const hasPolicies = (fc.writePolicies || []).length > 0;
|
||
const enabled = value.enable !== null
|
||
? value.enable : !!(derived && derived.enable);
|
||
const hasAutoBackend = (fc.backends || []).some((o) => o.id === null);
|
||
const backend = value.backend !== null
|
||
? value.backend
|
||
: (hasAutoBackend ? null : ((derived && derived.backend) || fc.defaultBackend || null));
|
||
const writePolicy = value.writePolicy !== "auto"
|
||
? value.writePolicy : ((derived && derived.writePolicy) || "auto");
|
||
return (
|
||
<div key={axisId} style={s.card}>
|
||
<div style={s.compactRow}>
|
||
<span style={s.axisTitle}>HiCache</span>
|
||
{typeof fc.showWhen !== "function" && (
|
||
<span style={s.field}>
|
||
{renderChip("Enable", enabled, true,
|
||
() => setSlot("enable", !enabled))}
|
||
</span>
|
||
)}
|
||
{hasBackends && (
|
||
<span style={s.field}>
|
||
<span style={s.fieldLabel}>Storage</span>
|
||
{renderSelect(backend, fc.backends,
|
||
(v) => setSlot("backend", v), base)}
|
||
</span>
|
||
)}
|
||
{hasPolicies && (
|
||
<span style={s.field}>
|
||
<span style={s.fieldLabel}>Write</span>
|
||
{renderSelect(writePolicy, fc.writePolicies,
|
||
(v) => setSlot("writePolicy", v), base)}
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
},
|
||
},
|
||
|
||
// ---- Axis: Flag Selects (generic, config-declared) ----------------------
|
||
// A LIST of single-selects, each declared entirely in config:
|
||
// { 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). `default` may be an option id or a function of the
|
||
// base selection (re-evaluated on every base change).
|
||
flagSelects: {
|
||
initState: (fc, base) => {
|
||
const out = {};
|
||
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 = {};
|
||
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 || [])) {
|
||
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;
|
||
}
|
||
}
|
||
out[spec.id] = hit;
|
||
}
|
||
return out;
|
||
},
|
||
|
||
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 || [])) {
|
||
// 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;
|
||
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` 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 };
|
||
},
|
||
|
||
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);
|
||
if (!opts.length) continue;
|
||
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}>
|
||
<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;
|
||
},
|
||
},
|
||
|
||
};
|
||
|
||
// ==========================================================================
|
||
// 6. Apply pipeline + render command
|
||
// ==========================================================================
|
||
// Thread the base cell's (flags, env) through every declared axis's apply,
|
||
// in declaration order; collect render hints (pdDisagg's role banner).
|
||
const applyAllDeltas = (baseFlags, baseEnv, allDeltas, sel, derivedMap) => {
|
||
let flags = [...baseFlags];
|
||
let env = [...(baseEnv || [])];
|
||
let pdMode = null;
|
||
for (const [axisId, handler] of Object.entries(AXIS_HANDLERS)) {
|
||
const fc = pgFeatures[axisId];
|
||
if (!fc) continue;
|
||
const value = allDeltas[axisId];
|
||
if (value === undefined) continue;
|
||
const derived = derivedMap ? derivedMap[axisId] : null;
|
||
const specAlgorithm = (findFlagArg(
|
||
flags, "--speculative-algorithm") || "").toUpperCase() || null;
|
||
const liveSel = { ...sel, specAlgorithm };
|
||
const out = handler.apply({ flags, env, value, fc, sel: liveSel, h: helpers, derived });
|
||
flags = out.flags;
|
||
env = out.env;
|
||
if (handler.getRenderHints) {
|
||
const hints = handler.getRenderHints(
|
||
value, fc, { flags, env, sel: liveSel, h: helpers }) || {};
|
||
if (hints.pdMode) pdMode = hints.pdMode;
|
||
}
|
||
}
|
||
return { flags, env, pdMode };
|
||
};
|
||
|
||
// Renderer (same shape as _deployment.jsx). pdMode prepends a PD-role
|
||
// banner; mode is "python" | "docker"; cellEnv is decoupled from cell so
|
||
// 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);
|
||
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-size", "--dp", "--tp-size", "--tp"];
|
||
let at = -1;
|
||
for (const anchor of PARALLELISM_ANCHORS) {
|
||
at = f.findIndex((x) => x.split(/[\s=]/)[0] === anchor);
|
||
if (at !== -1) break;
|
||
}
|
||
if (at === -1) at = f.findIndex((x) => x.startsWith("--model-path"));
|
||
// PD roles need distinct rendezvous ports so prefill+decode don't collide on a
|
||
// shared head host; non-PD multi-node keeps :20000 (matches _deployment.jsx).
|
||
const distPort = (pdMode && PD_PORTS[pdMode]) ? PD_PORTS[pdMode].dist : 20000;
|
||
f.splice(at + 1, 0,
|
||
`--nnodes ${nnodes}`,
|
||
`--node-rank {{NODE_RANK}}`,
|
||
`--dist-init-addr {{NODE0_IP}}:${distPort}`);
|
||
}
|
||
let cmd;
|
||
if (mode === "docker") {
|
||
// Image keyed by `hw|quant|strategy` (most specific), then `hw|quant`, then
|
||
// `hw`; `:dev` if unmapped (matches _deployment.jsx). The strategy key covers
|
||
// a tier that needs its own build (e.g. a spec-decoding preview image), so the
|
||
// playground base must resolve it too or it hands back an image that cannot
|
||
// run the command.
|
||
const di = config.dockerImages || {};
|
||
const image = di[`${sel.hw}|${sel.quant}|${sel.strategy}`]
|
||
|| di[`${sel.hw}|${sel.quant}`] || di[sel.hw] || "lmsysorg/sglang:dev";
|
||
const dockerRunCommand = typeof config.dockerRunCommand === "function"
|
||
? config.dockerRunCommand(sel)
|
||
: (config.dockerRunCommand || "sglang serve");
|
||
const portFlag = f.find((x) => x.split(/[\s=]/)[0] === "--port");
|
||
const servePort = portFlag ? portFlag.slice("--port".length).trim() : "{{PORT}}";
|
||
const hostNetwork = multinode || pdMode || (typeof config.dockerHostNetworkWhen === "function"
|
||
&& config.dockerHostNetworkWhen(sel, { flags: f, env: cellEnv }));
|
||
// Mirrors `multiNodeDockerFlags` on the _deployment.jsx HARDWARE_CATALOG
|
||
// (Mintlify strips module state, so the engines cannot share it).
|
||
const HW_MULTINODE_DOCKER_FLAGS = {
|
||
"dgx-spark": [
|
||
"--ulimit memlock=-1:-1", "--cap-add IPC_LOCK", "--device /dev/infiniband",
|
||
],
|
||
};
|
||
const fabricFlags = HW_MULTINODE_DOCKER_FLAGS[sel.hw] || [];
|
||
const dockerLines = [
|
||
"docker run --gpus all",
|
||
" --shm-size 32g",
|
||
hostNetwork ? " --network host" : ` -p ${servePort}:${servePort}`,
|
||
...(multinode ? fabricFlags.map((x) => " " + x) : []),
|
||
" -v ~/.cache/huggingface:/root/.cache/huggingface",
|
||
...(config.dockerMounts || []).map((mount) => ` -v ${mount}`),
|
||
` --env "HF_TOKEN={{HF_TOKEN}}"`,
|
||
...cellEnv.map((e) => ` --env ${e}`),
|
||
" --ipc=host",
|
||
` ${image}`,
|
||
` ${dockerRunCommand}`,
|
||
...f.map((x) => " " + x),
|
||
];
|
||
cmd = dockerLines.join(" \\\n");
|
||
} else {
|
||
const flagBlock = f.map((x) => " " + x).join(" \\\n");
|
||
const envBlock = cellEnv.length ? cellEnv.join(" \\\n") + " \\\n" : "";
|
||
cmd = `${envBlock}sglang serve \\\n${flagBlock}`;
|
||
}
|
||
if (multinode && config.multiNodeHints && config.multiNodeHints[sel.hw]) {
|
||
const hint = config.multiNodeHints[sel.hw]
|
||
.map((line) => (line.length ? "# " + line : "#")).join("\n");
|
||
cmd = `${hint}\n${cmd}`;
|
||
}
|
||
cmd = interpolate(cmd, envValues, modelName);
|
||
if (multinode) {
|
||
const header =
|
||
`# Multi-node (${nnodes} nodes). Run the same command on every node with:\n` +
|
||
`# <node-rank> = 0 on the head node, 1..${nnodes - 1} on the others\n` +
|
||
`# <node0-ip> = IP of the head node (reachable from all others)`;
|
||
cmd = `${header}\n${cmd}`;
|
||
}
|
||
if (pdMode === "prefill" || pdMode === "decode") {
|
||
const sibling = pdMode === "prefill" ? "decode" : "prefill";
|
||
const routerCfg = config.playgroundFeatures
|
||
&& config.playgroundFeatures.pdDisagg
|
||
&& config.playgroundFeatures.pdDisagg.router;
|
||
const routerPort = (routerCfg && routerCfg.port) || 8000;
|
||
const routerLine = routerCfg
|
||
? `# 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
|
||
&& config.playgroundFeatures.hicache;
|
||
const pdBackend = findFlagArg(f, "--disaggregation-transfer-backend");
|
||
const hicacheEnabled = f.some((x) => x === "--enable-hierarchical-cache");
|
||
const hicacheNotice = hicacheEnabled && hicacheCfg
|
||
? (hicacheCfg.notices || []).find((item) => {
|
||
if (!item || item.mode !== pdMode) return false;
|
||
if (item.transferBackend && item.transferBackend !== pdBackend) return false;
|
||
return !item.when || matchConstraint(sel, item.when);
|
||
})
|
||
: null;
|
||
const noticeLine = hicacheNotice && hicacheNotice.text
|
||
? `# Note: ${hicacheNotice.text}\n` : "";
|
||
const banner =
|
||
`# === PD Disaggregation: ${pdMode.toUpperCase()} role ===\n` +
|
||
noticeLine +
|
||
`# Runs the ${pdMode} server. Also run the ${sibling} role on its peer host,\n` +
|
||
routerLine;
|
||
cmd = `${banner}\n${cmd}`;
|
||
}
|
||
return cmd;
|
||
};
|
||
|
||
// ==========================================================================
|
||
// 7. Diff (line-level, true LCS)
|
||
// ==========================================================================
|
||
// DP LCS + backtrace → unchanged / added / removed lines. Full LCS (not
|
||
// greedy) because apply may reorder flags; greedy would drop shared lines
|
||
// around a swap. Commands are ~15 lines so O(m·n) is trivial.
|
||
const computeDiff = (baseStr, pgStr) => {
|
||
const a = baseStr.split("\n");
|
||
const b = pgStr.split("\n");
|
||
const m = a.length, n = b.length;
|
||
const dp = Array(m + 1).fill(null).map(() => new Array(n + 1).fill(0));
|
||
for (let i = 1; i <= m; i++) {
|
||
for (let j = 1; j <= n; j++) {
|
||
if (a[i - 1] === b[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
|
||
else dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
|
||
}
|
||
}
|
||
const out = [];
|
||
let i = m, j = n;
|
||
while (i > 0 || j > 0) {
|
||
if (i > 0 && j > 0 && a[i - 1] === b[j - 1]) {
|
||
out.unshift({ line: a[i - 1], kind: "unchanged" });
|
||
i--; j--;
|
||
} else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
|
||
out.unshift({ line: b[j - 1], kind: "added" });
|
||
j--;
|
||
} else {
|
||
out.unshift({ line: a[i - 1], kind: "removed" });
|
||
i--;
|
||
}
|
||
}
|
||
return out;
|
||
};
|
||
|
||
// ==========================================================================
|
||
// 7b. Verified-cell submission helpers
|
||
// ==========================================================================
|
||
// Format a cell object as the cookbook config does, for paste into
|
||
// `cells: [...]`. Keeps {{MODEL_NAME}} etc. as raw placeholders.
|
||
const serializeCell = (sel, env, flags) => {
|
||
const matchEntries = [
|
||
`hw: ${JSON.stringify(sel.hw)}`,
|
||
`variant: ${JSON.stringify(sel.variant)}`,
|
||
`quant: ${JSON.stringify(sel.quant)}`,
|
||
`strategy: ${JSON.stringify(sel.strategy)}`,
|
||
`nodes: ${JSON.stringify(sel.nodes)}`,
|
||
].join(", ");
|
||
const fmtList = (items) => {
|
||
if (!items || items.length === 0) return "[]";
|
||
const lines = items.map((s) => ` ${JSON.stringify(s)},`).join("\n");
|
||
return `[\n${lines}\n ]`;
|
||
};
|
||
return [
|
||
" {",
|
||
` match: { ${matchEntries} },`,
|
||
" verified: true,",
|
||
` env: ${fmtList(env)},`,
|
||
` flags: ${fmtList(flags)},`,
|
||
" },",
|
||
].join("\n");
|
||
};
|
||
|
||
// GitHub Issue prefill URL. Query keys must match the `id:` values in
|
||
// .github/ISSUE_TEMPLATE/3-playground-verified-cell.yml. `config.github`
|
||
// overrides the repo/template; defaults match the SGLang repo.
|
||
const buildSubmitUrl = (sel, fields) => {
|
||
const gh = (config.github) || {};
|
||
const owner = gh.owner || "sgl-project";
|
||
const repo = gh.repo || "sglang";
|
||
const tmpl = gh.issueTemplate || "3-playground-verified-cell.yml";
|
||
const cookbookModel = gh.cookbookModel || "deepseek-ai/deepseek-v4";
|
||
const combo = `${sel.hw} / ${sel.variant} / ${sel.quant} / ${sel.strategy} / ${sel.nodes}`;
|
||
const params = new URLSearchParams({
|
||
template: tmpl,
|
||
title: `[Playground] Verified cell: ${combo}`,
|
||
model: cookbookModel,
|
||
combination: combo,
|
||
"cell-snippet": fields.cellSnippet || "",
|
||
"existing-cell": fields.existingCell || "",
|
||
"sglang-version": fields.sglangVersion || "",
|
||
"bench-result": fields.benchResult || "",
|
||
notes: fields.notes || "",
|
||
});
|
||
return `https://github.com/${owner}/${repo}/issues/new?${params.toString()}`;
|
||
};
|
||
|
||
// ==========================================================================
|
||
// 8. Style helper (dark-mode-aware)
|
||
// ==========================================================================
|
||
const makeStyles = (isDark) => ({
|
||
container: { maxWidth: "900px", margin: "0 auto", display: "flex", flexDirection: "column", gap: "6px" },
|
||
card: {
|
||
padding: "6px 10px",
|
||
border: `1px solid ${isDark ? "#374151" : "#e5e7eb"}`,
|
||
borderLeft: `3px solid ${isDark ? "#FDBA74" : "#FB923C"}`,
|
||
borderRadius: "4px",
|
||
background: isDark ? "#1f2937" : "#fff",
|
||
},
|
||
cardStack: { display: "flex", flexDirection: "column", gap: "6px" },
|
||
baseStrip: {
|
||
padding: "8px 12px",
|
||
borderRadius: "4px",
|
||
background: isDark ? "#064e3b" : "#d1fae5",
|
||
color: isDark ? "#a7f3d0" : "#065f46",
|
||
fontSize: "12px",
|
||
display: "flex", alignItems: "center", gap: "10px",
|
||
},
|
||
title: { fontSize: "13px", fontWeight: "600", color: isDark ? "#e5e7eb" : "inherit", marginBottom: "8px" },
|
||
compactRow: {
|
||
display: "flex", flexWrap: "wrap", alignItems: "center",
|
||
gap: "10px", rowGap: "4px",
|
||
},
|
||
axisTitle: {
|
||
fontSize: "12px", fontWeight: 700,
|
||
color: isDark ? "#FDBA74" : "#C2410C",
|
||
letterSpacing: "0.02em",
|
||
minWidth: "100px", flexShrink: 0,
|
||
},
|
||
field: { display: "inline-flex", alignItems: "center", gap: "4px" },
|
||
fieldLabel: {
|
||
fontSize: "11px", fontWeight: 500,
|
||
color: isDark ? "#9ca3af" : "#6b7280",
|
||
},
|
||
select: {
|
||
padding: "2px 6px",
|
||
border: `1px solid ${isDark ? "#4b5563" : "#d1d5db"}`,
|
||
borderRadius: "3px",
|
||
fontSize: "12px",
|
||
background: isDark ? "#111827" : "#fff",
|
||
color: isDark ? "#e5e7eb" : "#111827",
|
||
cursor: "pointer",
|
||
lineHeight: "1.4",
|
||
},
|
||
rowFlex: { display: "flex", flexWrap: "wrap", gap: "6px", alignItems: "center", flex: 1 },
|
||
subRow: { display: "flex", alignItems: "center", gap: "10px" },
|
||
subLabel: {
|
||
fontSize: "11px", fontWeight: 600,
|
||
color: isDark ? "#9ca3af" : "#6b7280",
|
||
minWidth: "96px", flexShrink: 0,
|
||
letterSpacing: "0.02em",
|
||
},
|
||
chipRow: { display: "flex", flexWrap: "wrap", gap: "6px", flex: 1 },
|
||
chip: {
|
||
padding: "3px 9px",
|
||
border: `1px solid ${isDark ? "#9ca3af" : "#d1d5db"}`,
|
||
borderRadius: "3px",
|
||
cursor: "pointer",
|
||
fontSize: "12px",
|
||
userSelect: "none",
|
||
background: isDark ? "#374151" : "#fff",
|
||
color: isDark ? "#e5e7eb" : "inherit",
|
||
textAlign: "center",
|
||
},
|
||
// Terracotta matches _deployment.jsx's selected button.
|
||
chipChecked: {
|
||
background: "#D45D44", color: "white", borderColor: "#D45D44",
|
||
},
|
||
chipDisabled: { cursor: "not-allowed", opacity: 0.4 },
|
||
commandWrap: {
|
||
position: "relative",
|
||
background: isDark ? "#111827" : "#f5f5f5",
|
||
borderRadius: "6px",
|
||
border: `1px solid ${isDark ? "#374151" : "#e5e7eb"}`,
|
||
overflow: "hidden",
|
||
},
|
||
commandHeader: {
|
||
display: "flex", flexWrap: "wrap", justifyContent: "space-between", alignItems: "center",
|
||
gap: "6px 10px",
|
||
padding: "6px 10px",
|
||
borderBottom: `1px solid ${isDark ? "#374151" : "#e5e7eb"}`,
|
||
background: isDark ? "#1f2937" : "#fafafa",
|
||
},
|
||
commandPre: {
|
||
padding: "12px 16px",
|
||
fontFamily: "'Menlo', 'Monaco', 'Courier New', monospace",
|
||
fontSize: "12px", lineHeight: "1.5",
|
||
color: isDark ? "#e5e7eb" : "#374151",
|
||
whiteSpace: "pre-wrap", overflowX: "auto", margin: 0,
|
||
},
|
||
// Amber callout for a prerequisite an axis option carries — rendered
|
||
// inside the axis card, so it reads as a condition on the pick rather
|
||
// than on the composed command.
|
||
axisNote: {
|
||
margin: "6px 0 0", padding: "6px 10px", borderRadius: "6px",
|
||
fontSize: "11px", lineHeight: "1.45",
|
||
background: isDark ? "#78350f" : "#fef3c7",
|
||
color: isDark ? "#fde68a" : "#92400e",
|
||
border: `1px solid ${isDark ? "#92400e" : "#fcd34d"}`,
|
||
},
|
||
// Amber callout under the playground command when the effective (post-
|
||
// override) command turns speculative decoding on without setting
|
||
// --max-running-requests (SGLang then caps it at 48).
|
||
mtpWarn: {
|
||
margin: "8px 0 0", padding: "8px 12px", borderRadius: "8px",
|
||
fontSize: "12px", lineHeight: "1.45",
|
||
background: isDark ? "#78350f" : "#fef3c7",
|
||
color: isDark ? "#fde68a" : "#92400e",
|
||
border: `1px solid ${isDark ? "#92400e" : "#fcd34d"}`,
|
||
},
|
||
diffLineUnchanged: { display: "block" },
|
||
diffLineAdded: {
|
||
display: "block",
|
||
background: isDark ? "rgba(16,185,129,0.15)" : "rgba(16,185,129,0.18)",
|
||
color: isDark ? "#a7f3d0" : "#065f46",
|
||
borderLeft: `3px solid #10b981`,
|
||
paddingLeft: "8px", marginLeft: "-8px",
|
||
},
|
||
diffLineRemoved: {
|
||
display: "block",
|
||
background: isDark ? "rgba(239,68,68,0.10)" : "rgba(239,68,68,0.10)",
|
||
color: isDark ? "#fca5a5" : "#991b1b",
|
||
textDecoration: "line-through",
|
||
opacity: 0.7,
|
||
borderLeft: `3px solid #ef4444`,
|
||
paddingLeft: "8px", marginLeft: "-8px",
|
||
},
|
||
// Two-state badge — matches _deployment.jsx's verified/unverified badge.
|
||
badge: (verified) => ({
|
||
display: "inline-flex", alignItems: "center", gap: "6px",
|
||
padding: "2px 8px", borderRadius: "10px",
|
||
background: verified ? (isDark ? "#064e3b" : "#d1fae5")
|
||
: (isDark ? "#78350f" : "#fef3c7"),
|
||
color: verified ? (isDark ? "#a7f3d0" : "#065f46")
|
||
: (isDark ? "#fde68a" : "#92400e"),
|
||
fontSize: "11px", fontWeight: 600,
|
||
}),
|
||
badgeDot: (verified) => ({
|
||
width: "8px", height: "8px", borderRadius: "50%",
|
||
background: verified ? "#10b981" : "#f59e0b",
|
||
}),
|
||
iconButton: {
|
||
padding: "4px 10px",
|
||
border: `1px solid ${isDark ? "#4b5563" : "#d1d5db"}`,
|
||
borderRadius: "4px",
|
||
background: isDark ? "#1f2937" : "#fff",
|
||
color: isDark ? "#e5e7eb" : "#374151",
|
||
fontSize: "11px", fontWeight: 500, cursor: "pointer",
|
||
display: "inline-flex", alignItems: "center", gap: "4px",
|
||
},
|
||
iconRow: { display: "inline-flex", flexWrap: "wrap", gap: "6px" },
|
||
runModeWrap: {
|
||
display: "inline-flex",
|
||
border: `1px solid ${isDark ? "#4b5563" : "#d1d5db"}`,
|
||
borderRadius: "10px",
|
||
overflow: "hidden",
|
||
fontSize: "11px", fontWeight: 600,
|
||
userSelect: "none",
|
||
},
|
||
runModeChip: (active) => ({
|
||
padding: "2px 10px", cursor: "pointer",
|
||
background: active ? (isDark ? "#1f2937" : "#fff") : "transparent",
|
||
color: active ? (isDark ? "#e5e7eb" : "#111827") : (isDark ? "#9ca3af" : "#6b7280"),
|
||
borderRight: `1px solid ${isDark ? "#4b5563" : "#d1d5db"}`,
|
||
}),
|
||
runModeChipLast: (active) => ({
|
||
padding: "2px 10px", cursor: "pointer",
|
||
background: active ? (isDark ? "#1f2937" : "#fff") : "transparent",
|
||
color: active ? (isDark ? "#e5e7eb" : "#111827") : (isDark ? "#9ca3af" : "#6b7280"),
|
||
}),
|
||
headerLeft: { display: "inline-flex", flexWrap: "wrap", alignItems: "center", gap: "8px" },
|
||
// Native <dialog> in top-layer mode (.showModal()) escapes Mintlify's
|
||
// `container-type: inline-size` trap that catches plain fixed-position
|
||
// modals. ::backdrop can't be styled inline (injected via useEffect).
|
||
dialog: {
|
||
background: isDark ? "#1f2937" : "#fff",
|
||
color: isDark ? "#e5e7eb" : "#111827",
|
||
borderRadius: "8px", padding: "20px",
|
||
maxWidth: "720px", width: "92%",
|
||
maxHeight: "calc(100vh - 80px)", overflowY: "auto",
|
||
border: `1px solid ${isDark ? "#374151" : "#e5e7eb"}`,
|
||
boxShadow: "0 10px 25px rgba(0,0,0,0.25)",
|
||
margin: "auto",
|
||
},
|
||
modalHeader: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "12px" },
|
||
modalTitle: { fontSize: "15px", fontWeight: 600 },
|
||
modalCloseBtn: {
|
||
background: "transparent", border: "none", color: "inherit",
|
||
fontSize: "20px", cursor: "pointer", padding: "0 6px", lineHeight: 1,
|
||
},
|
||
formField: { display: "flex", flexDirection: "column", gap: "4px", marginBottom: "10px" },
|
||
formLabel: { fontSize: "12px", fontWeight: 500, color: isDark ? "#9ca3af" : "#4b5563" },
|
||
formInput: {
|
||
padding: "6px 10px", fontSize: "13px",
|
||
border: `1px solid ${isDark ? "#4b5563" : "#d1d5db"}`,
|
||
borderRadius: "4px",
|
||
background: isDark ? "#111827" : "#fff",
|
||
color: isDark ? "#e5e7eb" : "#111827",
|
||
fontFamily: "'Menlo', 'Monaco', 'Courier New', monospace",
|
||
},
|
||
sectionHeading: {
|
||
fontSize: "12px", fontWeight: 600, textTransform: "uppercase",
|
||
letterSpacing: "0.04em",
|
||
color: isDark ? "#9ca3af" : "#6b7280",
|
||
margin: "12px 0 6px 0",
|
||
},
|
||
primaryBtn: {
|
||
padding: "6px 14px",
|
||
background: isDark ? "#FDBA74" : "#FB923C",
|
||
color: isDark ? "#7C2D12" : "white",
|
||
border: "none", borderRadius: "4px", cursor: "pointer",
|
||
fontSize: "13px", fontWeight: 500,
|
||
},
|
||
resetBtn: {
|
||
marginLeft: "auto", padding: "2px 8px", fontSize: "11px",
|
||
border: `1px solid ${isDark ? "#4b5563" : "#d1d5db"}`,
|
||
borderRadius: "3px",
|
||
background: "transparent",
|
||
color: isDark ? "#9ca3af" : "#6b7280",
|
||
cursor: "pointer",
|
||
},
|
||
switchBaseBtn: {
|
||
padding: "2px 8px", fontSize: "11px", fontWeight: 600,
|
||
border: `1px solid ${isDark ? "#FDBA74" : "#FB923C"}`,
|
||
borderRadius: "3px",
|
||
background: "transparent",
|
||
color: isDark ? "#FDBA74" : "#C2410C",
|
||
cursor: "pointer",
|
||
},
|
||
// Annotation next to the verified pill when the command matches a
|
||
// sibling cell (different strategy).
|
||
matchedHint: {
|
||
fontSize: "11px",
|
||
color: isDark ? "#9ca3af" : "#6b7280",
|
||
marginLeft: "8px",
|
||
display: "inline-flex", alignItems: "center", gap: "4px",
|
||
},
|
||
matchedSwitchBtn: {
|
||
marginLeft: "4px",
|
||
background: "transparent",
|
||
border: "none",
|
||
padding: 0,
|
||
color: isDark ? "#FDBA74" : "#C2410C",
|
||
cursor: "pointer",
|
||
fontSize: "11px", fontWeight: 600,
|
||
textDecoration: "underline",
|
||
textUnderlineOffset: "2px",
|
||
},
|
||
});
|
||
|
||
// ==========================================================================
|
||
// 9. React state + effects
|
||
// ==========================================================================
|
||
const [isDark, setIsDark] = useState(false);
|
||
useEffect(() => {
|
||
const check = () => {
|
||
const html = document.documentElement;
|
||
setIsDark(
|
||
html.classList.contains("dark") ||
|
||
html.getAttribute("data-theme") === "dark" ||
|
||
html.style.colorScheme === "dark"
|
||
);
|
||
};
|
||
check();
|
||
const observer = new MutationObserver(check);
|
||
observer.observe(document.documentElement, {
|
||
attributes: true,
|
||
attributeFilter: ["class", "data-theme", "style"],
|
||
});
|
||
return () => observer.disconnect();
|
||
}, []);
|
||
|
||
// Env / placeholder values, shared with _deployment.jsx via STORAGE_KEY.
|
||
const [env, setEnv] = useState(() => placeholderDefaults(config.placeholders));
|
||
useEffect(() => {
|
||
try {
|
||
const raw = window.localStorage.getItem(STORAGE_KEY);
|
||
if (raw) {
|
||
const parsed = JSON.parse(raw);
|
||
setEnv({ ...placeholderDefaults(config.placeholders), ...parsed });
|
||
}
|
||
} catch {}
|
||
}, []);
|
||
const saveEnv = (next) => {
|
||
setEnv(next);
|
||
try { window.localStorage.setItem(STORAGE_KEY, JSON.stringify(next)); } catch {}
|
||
};
|
||
|
||
// 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 = baseFallback();
|
||
if (typeof window === "undefined") return { ...fallback };
|
||
const raw = window.location.hash.replace(/^#/, "");
|
||
if (!raw) return { ...fallback };
|
||
const params = new URLSearchParams(raw);
|
||
const out = { ...fallback };
|
||
params.forEach((value, key) => { if (key in out) out[key] = value; });
|
||
return out;
|
||
};
|
||
const [base, setBase] = useState(() => initialBaseFromHash());
|
||
useEffect(() => {
|
||
const onHash = () => setBase(initialBaseFromHash());
|
||
const onSelEvent = (e) => {
|
||
const fallback = baseFallback();
|
||
const incoming = (e && e.detail) || {};
|
||
const next = { ...fallback };
|
||
for (const k of Object.keys(next)) {
|
||
if (incoming[k] !== undefined) next[k] = incoming[k];
|
||
}
|
||
setBase(next);
|
||
};
|
||
window.addEventListener("hashchange", onHash);
|
||
window.addEventListener("sglang-deploy-sel", onSelEvent);
|
||
return () => {
|
||
window.removeEventListener("hashchange", onHash);
|
||
window.removeEventListener("sglang-deploy-sel", onSelEvent);
|
||
};
|
||
}, []);
|
||
|
||
// 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, base);
|
||
}
|
||
return out;
|
||
};
|
||
const [deltas, setDeltas] = useState(initialDeltas);
|
||
|
||
// 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(initialDeltas());
|
||
}, [Object.keys(base).sort().map((k) => `${k}=${base[k]}`).join("&")]);
|
||
|
||
const [modal, setModal] = useState(null); // 'curl' | 'env' | 'submit' | null
|
||
|
||
// Callback-ref: show on mount. .showModal() (not .show()) gives the top
|
||
// layer plus native ESC / focus-trap / scroll-lock.
|
||
const openDialog = (el) => {
|
||
if (el && !el.open) {
|
||
try { el.showModal(); } catch { /* already open or unsupported */ }
|
||
}
|
||
};
|
||
|
||
// Click-outside-to-close: backdrop clicks hit the dialog element with
|
||
// coordinates outside its rect.
|
||
const onDialogClick = (e) => {
|
||
if (e.target !== e.currentTarget) return;
|
||
const r = e.currentTarget.getBoundingClientRect();
|
||
const { clientX: x, clientY: y } = e;
|
||
if (x < r.left || x > r.right || y < r.top || y > r.bottom) setModal(null);
|
||
};
|
||
|
||
// ::backdrop can't be styled inline — inject one dim-overlay rule.
|
||
useEffect(() => {
|
||
const ID = "__playground_dialog_backdrop";
|
||
if (document.getElementById(ID)) return undefined;
|
||
const style = document.createElement("style");
|
||
style.id = ID;
|
||
style.textContent = `dialog::backdrop { background: rgba(0, 0, 0, 0.5); }`;
|
||
document.head.appendChild(style);
|
||
return () => { const el = document.getElementById(ID); if (el) el.remove(); };
|
||
}, []);
|
||
|
||
const [copied, setCopied] = useState(false);
|
||
const [curlCopied, setCurlCopied] = useState(false);
|
||
const [routerCopied, setRouterCopied] = useState(false);
|
||
const [envDraft, setEnvDraft] = useState(env);
|
||
useEffect(() => { if (modal === "env") setEnvDraft(env); }, [modal, env]);
|
||
const [runMode, setRunMode] = useState("python");
|
||
|
||
// Submit-verified-cell modal state, reset each time the modal opens.
|
||
const [submitDraft, setSubmitDraft] = useState({
|
||
sglangVersion: "", benchResult: "", notes: "",
|
||
});
|
||
const [submitAttest, setSubmitAttest] = useState({
|
||
ranCommand: false, reachedReady: false, outputCorrect: false,
|
||
});
|
||
useEffect(() => {
|
||
if (modal === "submit") {
|
||
setSubmitDraft({ sglangVersion: "", benchResult: "", notes: "" });
|
||
setSubmitAttest({ ranCommand: false, reachedReady: false, outputCorrect: false });
|
||
}
|
||
}, [modal]);
|
||
|
||
// ==========================================================================
|
||
// 10. Derived values
|
||
// ==========================================================================
|
||
const s = makeStyles(isDark);
|
||
const baseCell = withOverlay(findCell(config.cells, base), base);
|
||
const modelName = resolveModelName(base);
|
||
|
||
// Per-axis state recovered from the base cell's flags (deriveFromBase).
|
||
const derivedMap = {};
|
||
if (baseCell) {
|
||
for (const [axisId, handler] of Object.entries(AXIS_HANDLERS)) {
|
||
const fc = pgFeatures[axisId];
|
||
if (!fc || !handler.deriveFromBase) continue;
|
||
derivedMap[axisId] = handler.deriveFromBase(baseCell, fc, helpers);
|
||
}
|
||
}
|
||
|
||
// 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; 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),
|
||
// explicit override else derived-from-base.
|
||
// cpStrategy — effective CP layout ("zigzag" / "interleave"; explicit
|
||
// override else baked-in-base else "interleave").
|
||
// cpSizeTarget — the only enable-able CP size (runtime derives
|
||
// attn_cp_size = tp/dp); null when TP is unknown or the cp
|
||
// knob opts out via `freeSize: true`.
|
||
// effTp — effective TP degree (override else derived).
|
||
// pdMode — live PD-Disagg role; gates the decode-only HiSparse card.
|
||
const attnDelta = deltas.attention || {};
|
||
const attnDerived = derivedMap.attention || {};
|
||
const attnKnobs = ((pgFeatures.attention || {}).knobs) || [];
|
||
const effTp = (attnDelta.tp !== null && attnDelta.tp !== undefined)
|
||
? attnDelta.tp
|
||
: (attnDerived.tp !== undefined ? attnDerived.tp : null);
|
||
// A picked value whose entry is disabled under the live facts is skipped
|
||
// by apply() and must not count as "on" (stale-state corner: e.g. CP=8
|
||
// picked, then TP switched to 4). Only explicit picks can go stale;
|
||
// derived-from-base values are always real.
|
||
const staleExplicit = (knobId, picked) => {
|
||
const knob = attnKnobs.find((k) => k.id === knobId);
|
||
const e = knob ? findEntry(knob.values || [], picked) : null;
|
||
return !!(e !== null && e !== undefined
|
||
&& evaluateChip(e, { ...base, effTp }).disabled);
|
||
};
|
||
const effDpAttn = (attnDelta.dpAttn !== null && attnDelta.dpAttn !== undefined)
|
||
? (staleExplicit("dpAttn", attnDelta.dpAttn) ? null : attnDelta.dpAttn)
|
||
: (attnDerived.dpAttn !== undefined ? attnDerived.dpAttn : null);
|
||
const dpAttnOn = (effDpAttn === true)
|
||
|| (typeof effDpAttn === "number" && effDpAttn > 0);
|
||
// Runtime derivation attn_cp_size = tp/dp: with DP-Attention off, the only
|
||
// enable-able CP size is TP. With DP-Attention on, sizes are NOT gated —
|
||
// CP + DP-Attention is an allowed experiment covered by a warning hint
|
||
// (null also when TP is unknown or the cp knob opts out via `freeSize`).
|
||
const dpDegEff = (typeof effDpAttn === "number" && effDpAttn > 0)
|
||
? effDpAttn : 1;
|
||
const cpKnobFreeSize = !!(attnKnobs.find((k) => k.id === "cp") || {}).freeSize;
|
||
const cpSizeTarget =
|
||
(!cpKnobFreeSize && dpDegEff === 1
|
||
&& typeof effTp === "number" && effTp > 0)
|
||
? effTp : null;
|
||
const cpSizeStale = (v) => typeof v === "number" && v > 1
|
||
&& cpSizeTarget !== null && v !== cpSizeTarget;
|
||
const effCp = (attnDelta.cp !== null && attnDelta.cp !== undefined)
|
||
? ((staleExplicit("cp", attnDelta.cp) || cpSizeStale(attnDelta.cp))
|
||
? null : attnDelta.cp)
|
||
: (attnDerived.cp !== undefined ? attnDerived.cp : null);
|
||
const cpOn = typeof effCp === "number" && effCp > 1;
|
||
const cpStrategy = ((attnDelta.cpStrategy
|
||
&& !staleExplicit("cpStrategy", attnDelta.cpStrategy))
|
||
? attnDelta.cpStrategy
|
||
: (attnDerived.cpStrategy !== undefined ? attnDerived.cpStrategy : null))
|
||
|| "interleave";
|
||
const constraintEffective = baseCell
|
||
? applyAllDeltas(baseCell.flags, baseCell.env, deltas, base, derivedMap)
|
||
: null;
|
||
const pdCardOwnsMode = ((pgFeatures.pdDisagg && pgFeatures.pdDisagg.modes) || []).length > 0;
|
||
const pdMode = pdCardOwnsMode
|
||
? ((constraintEffective && constraintEffective.pdMode) || "off")
|
||
: (base.pdMode || "off");
|
||
const specAlgorithm = constraintEffective
|
||
? ((findFlagArg(constraintEffective.flags,
|
||
"--speculative-algorithm") || "").toUpperCase() || null)
|
||
: null;
|
||
const constraintBase = {
|
||
...base, dpAttnOn, cpOn, cpStrategy, cpSizeTarget, effTp, pdMode,
|
||
specAlgorithm,
|
||
};
|
||
|
||
let baseCommand = "";
|
||
let playgroundCommand = "";
|
||
let diffLines = [];
|
||
let pgFlagsLatest = [];
|
||
let pgEnvLatest = [];
|
||
// Render-only ratio injection (before the host/port tail); skipped if the
|
||
// flags already carry the family, or the base cell sizes the pool explicitly
|
||
// with --max-mamba-cache-size (the ratio would contradict its slot count).
|
||
const withRatio = (fl, value) => {
|
||
if (!value) return fl;
|
||
if (fl.some((f) =>
|
||
f.startsWith("--mamba-full-memory-ratio") || f.startsWith("--max-mamba-cache-size"))) 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, 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, 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.
|
||
const matchedCell = baseCell
|
||
? findMatchingCell(config.cells, base, pgEnvLatest, pgFlagsLatest)
|
||
: null;
|
||
const playgroundVerified = !!(matchedCell && matchedCell.verified);
|
||
const matchedSiblingCell = (matchedCell
|
||
&& DIMENSIONS.some((d) => matchedCell.match[d] !== base[d]))
|
||
? matchedCell : null;
|
||
const pgSpecAlgoFlag = pgFlagsLatest.find(
|
||
(f) => f.split(/[\s=]/)[0] === "--speculative-algorithm");
|
||
const pgSpecHint =
|
||
!!pgSpecAlgoFlag &&
|
||
!pgFlagsLatest.some((f) => f.split(/[\s=]/)[0] === "--max-running-requests");
|
||
const specAlgoLabels = {
|
||
EAGLE: "MTP", EAGLE3: "MTP", FROZEN_KV_MTP: "MTP",
|
||
DSPARK: "DSpark", DFLASH: "DFlash", NGRAM: "N-gram",
|
||
STANDALONE: "standalone draft",
|
||
};
|
||
const pgSpecAlgoValue = pgSpecAlgoFlag
|
||
? (pgSpecAlgoFlag.split(/[\s=]/).filter(Boolean)[1] || "") : "";
|
||
const pgSpecAlgoName = specAlgoLabels[pgSpecAlgoValue.toUpperCase()]
|
||
|| pgSpecAlgoValue || "MTP";
|
||
|
||
// Interleave prefill-CP + DP-Attention hint on the EFFECTIVE command:
|
||
// deliberately allowed (combined support is planned upstream), but current
|
||
// releases assert dp_size == 1 for the interleave layout at startup.
|
||
const pgCpDpHint =
|
||
cpEnabledIn(pgFlagsLatest)
|
||
&& (bakedCpStrategy(pgFlagsLatest) || "interleave") === "interleave"
|
||
&& pgFlagsLatest.some((f) => f.split(/[\s=]/)[0] === "--enable-dp-attention");
|
||
|
||
// Submission snippets: proposed cell + existing cell at the same match.
|
||
const proposedCellSnippet = baseCell
|
||
? serializeCell(base, pgEnvLatest, pgFlagsLatest) : "";
|
||
const existingCellSnippet = baseCell
|
||
? serializeCell(base, baseCell.env || [], baseCell.flags) : "";
|
||
const submitUrl = baseCell ? buildSubmitUrl(base, {
|
||
cellSnippet: proposedCellSnippet,
|
||
existingCell: existingCellSnippet,
|
||
sglangVersion: submitDraft.sglangVersion,
|
||
benchResult: submitDraft.benchResult,
|
||
notes: submitDraft.notes,
|
||
}) : "";
|
||
const submitReady =
|
||
submitAttest.ranCommand && submitAttest.reachedReady && submitAttest.outputCorrect
|
||
&& submitDraft.sglangVersion.trim().length > 0;
|
||
|
||
// PD-Disagg router, if configured. When a PD role is active, cURL retargets
|
||
// to the router port and a companion router block renders below the command.
|
||
const pdRouter = (pdMode !== "off"
|
||
&& config.playgroundFeatures
|
||
&& config.playgroundFeatures.pdDisagg
|
||
&& config.playgroundFeatures.pdDisagg.router) || null;
|
||
const curlEnv = (pdRouter && pdRouter.port != null)
|
||
? { ...env, CURL_PORT: String(pdRouter.port) }
|
||
: env;
|
||
const curlText = interpolate(config.curl || "", curlEnv, modelName);
|
||
const routerText = pdRouter && pdRouter.command
|
||
? interpolate(pdRouter.command, {
|
||
...env,
|
||
PREFILL_PORT: PD_PORTS.prefill.serve,
|
||
DECODE_PORT: PD_PORTS.decode.serve,
|
||
ROUTER_PORT: pdRouter.port,
|
||
}, modelName)
|
||
: "";
|
||
|
||
const resetAll = () => setDeltas(initialDeltas());
|
||
|
||
const placeholderGroups = (() => {
|
||
const out = { command: [], curl: [] };
|
||
for (const [key, meta] of Object.entries(config.placeholders || {})) {
|
||
(out[meta.target] || (out[meta.target] = [])).push({ key, ...meta });
|
||
}
|
||
return out;
|
||
})();
|
||
|
||
const handleCopy = () => {
|
||
navigator.clipboard.writeText(playgroundCommand);
|
||
setCopied(true);
|
||
setTimeout(() => setCopied(false), 1200);
|
||
};
|
||
const copyCurl = () => {
|
||
navigator.clipboard.writeText(curlText);
|
||
setCurlCopied(true);
|
||
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
|
||
? 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)";
|
||
|
||
// ==========================================================================
|
||
// 11. Render helpers
|
||
// ==========================================================================
|
||
// Chip: checked when `current === value`; disabled chips are unclickable.
|
||
const renderChip = (label, current, value, onPick, opts = {}) => {
|
||
const checked = current === value;
|
||
const disabled = !!opts.disabled;
|
||
return (
|
||
<span
|
||
key={`${label}-${value === null ? "auto" : value}`}
|
||
style={{
|
||
...s.chip,
|
||
...(checked ? s.chipChecked : {}),
|
||
...(disabled ? s.chipDisabled : {}),
|
||
}}
|
||
title={disabled ? (opts.disabledReason || "Not available") : ""}
|
||
onClick={() => { if (!disabled) onPick(value); }}
|
||
>
|
||
{label}
|
||
</span>
|
||
);
|
||
};
|
||
|
||
// Dropdown over a chip-schema `entries` array. Hidden entries excluded;
|
||
// disabled get a "(n/a)" suffix. Uses the option index as the <select>
|
||
// value to dodge form-value serialization; `onPick` gets the original
|
||
// value. `labelFor` is an optional label resolver; `opts.hideValues`
|
||
// suppresses values (e.g. the inherit sentinel when a base default exists).
|
||
// `opts.disabled` grays the whole select (knob-level gating), with
|
||
// `opts.disabledReason` as the hover tooltip.
|
||
const renderSelect = (current, entries, onPick, base, labelFor, opts = {}) => {
|
||
const hideSet = new Set(opts.hideValues || []);
|
||
const items = [];
|
||
for (const entry of (entries || [])) {
|
||
const c = helpers.evaluateChip(entry, base);
|
||
if (c.hidden) continue;
|
||
if (hideSet.has(c.value)) continue;
|
||
const lbl = labelFor
|
||
? labelFor(c)
|
||
: (c.label !== undefined ? c.label
|
||
: c.value === null ? "Auto" : String(c.value));
|
||
items.push({ ...c, label: lbl });
|
||
}
|
||
let idx = items.findIndex((c) => c.value === current);
|
||
if (idx === -1) idx = 0;
|
||
return (
|
||
<select
|
||
style={{ ...s.select, ...(opts.disabled ? s.chipDisabled : {}) }}
|
||
disabled={!!opts.disabled}
|
||
title={opts.disabled ? (opts.disabledReason || "Not available") : ""}
|
||
value={idx}
|
||
onChange={(e) => {
|
||
const next = items[parseInt(e.target.value, 10)];
|
||
if (next && !next.disabled) onPick(next.value);
|
||
}}
|
||
>
|
||
{items.map((c, i) => (
|
||
<option
|
||
key={i}
|
||
value={i}
|
||
disabled={c.disabled}
|
||
>
|
||
{c.label}{c.disabled ? " (n/a)" : ""}
|
||
</option>
|
||
))}
|
||
</select>
|
||
);
|
||
};
|
||
|
||
// ==========================================================================
|
||
// 12. JSX render
|
||
// ==========================================================================
|
||
return (
|
||
<div style={s.container} className="not-prose">
|
||
{/* Inherited base summary */}
|
||
<div style={s.baseStrip}>
|
||
<span style={{ fontWeight: 600 }}>Inherited base from Deployment:</span>
|
||
<code style={{ fontFamily: "Menlo, monospace" }}>{baseSummary}</code>
|
||
{/* scrollIntoView (not hash nav) so the base-cell hash survives.
|
||
Target the configurator ITSELF: the "## Deployment" heading sits
|
||
above the install accordion and a screenful of prose, so landing on
|
||
the heading leaves the panel you came for off-screen. The heading
|
||
slugs stay as fallbacks for a page that renders no configurator. */}
|
||
<button
|
||
type="button"
|
||
style={s.switchBaseBtn}
|
||
onClick={() => {
|
||
const el = document.getElementById(DEPLOYMENT_COMPONENT_ID)
|
||
|| document.getElementById("deployment")
|
||
|| document.getElementById("deploy");
|
||
if (el) el.scrollIntoView({ behavior: "smooth", block: "start" });
|
||
}}
|
||
>
|
||
↑ Switch base
|
||
</button>
|
||
<button style={s.resetBtn} onClick={resetAll}>Reset all overrides</button>
|
||
</div>
|
||
|
||
{/* One card per declared axis (render() may return null to gate). */}
|
||
{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,
|
||
// constraintBase = 5 cell dims + cross-axis facts (dpAttnOn, pdMode).
|
||
fc, base: constraintBase, s, h: helpers, renderChip, renderSelect,
|
||
derived: derivedMap[axisId] || null,
|
||
});
|
||
})}
|
||
|
||
{/* Command box (diff vs verified base) */}
|
||
<div style={s.card}>
|
||
<div style={s.title}>Playground Command (compare with base)</div>
|
||
<div style={s.commandWrap}>
|
||
<div style={s.commandHeader}>
|
||
<div style={s.headerLeft}>
|
||
<div style={s.badge(playgroundVerified)}>
|
||
<span style={s.badgeDot(playgroundVerified)} />
|
||
{playgroundVerified ? "Verified" : "Not Verified"}
|
||
</div>
|
||
{/* Sibling-cell match: offer to switch the Deployment panel's base to it. */}
|
||
{matchedSiblingCell && (
|
||
<span style={s.matchedHint}>
|
||
matches <code style={{ fontFamily: "Menlo, monospace" }}>
|
||
{matchedSiblingCell.match.strategy}
|
||
</code>
|
||
<button
|
||
type="button"
|
||
style={s.matchedSwitchBtn}
|
||
onClick={() => {
|
||
const m = matchedSiblingCell.match;
|
||
setDeltas(initialDeltas());
|
||
const hash = new URLSearchParams(m).toString();
|
||
window.location.hash = hash;
|
||
window.dispatchEvent(new CustomEvent("sglang-deploy-sel",
|
||
{ detail: m }));
|
||
}}
|
||
>
|
||
switch base →
|
||
</button>
|
||
</span>
|
||
)}
|
||
<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>
|
||
</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>
|
||
{/* Submit only when the playground differs from the verified base. */}
|
||
{!playgroundVerified && baseCell && (
|
||
<button
|
||
style={{ ...s.iconButton, borderColor: isDark ? "#FDBA74" : "#FB923C",
|
||
color: isDark ? "#FDBA74" : "#C2410C", fontWeight: 600 }}
|
||
onClick={() => setModal("submit")}
|
||
title="I verified this command on my hardware — open a pre-filled GitHub issue to land it as a cookbook cell."
|
||
>
|
||
Submit ↗
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<pre style={s.commandPre}>
|
||
{baseCell ? diffLines.map((d, i) => (
|
||
<span
|
||
key={i}
|
||
style={
|
||
d.kind === "added" ? s.diffLineAdded :
|
||
d.kind === "removed" ? s.diffLineRemoved :
|
||
s.diffLineUnchanged
|
||
}
|
||
>
|
||
{d.kind === "added" ? "+ " : d.kind === "removed" ? "- " : " "}
|
||
{d.line}{"\n"}
|
||
</span>
|
||
)) : "# No verified base cell at the current Deployment selection.\n# Pick a supported hardware/variant in the Deployment panel to populate the playground base."}
|
||
</pre>
|
||
{pgSpecHint && (
|
||
<div style={s.mtpWarn}>
|
||
⚠️ Speculative decoding ({pgSpecAlgoName}) is on — SGLang resets <code>--max-running-requests</code> to <strong>48</strong> when it isn't set. Add <code>--max-running-requests <N></code> sized for your target concurrency.
|
||
</div>
|
||
)}
|
||
{pgCpDpHint && (
|
||
<div style={s.mtpWarn}>
|
||
⚠️ Interleave prefill-CP together with DP-Attention: current SGLang releases assert <code>dp_size == 1</code> for the interleave layout, so this command fails at startup. Combined CP + DP-Attention support is planned upstream — keep one of the two off until it lands.
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* PD-Disagg router companion (separate block so the role diff stays pure). */}
|
||
{pdRouter && routerText && (
|
||
<div style={s.card}>
|
||
<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>{" "}
|
||
on a same-host deployment). Client traffic (cURL) targets this router.
|
||
</div>
|
||
<div style={s.commandWrap}>
|
||
<div style={s.commandHeader}>
|
||
<div style={{ fontSize: 11, opacity: 0.7 }}>port {pdRouter.port}</div>
|
||
<button
|
||
style={s.iconButton}
|
||
onClick={() => {
|
||
navigator.clipboard.writeText(routerText);
|
||
setRouterCopied(true);
|
||
setTimeout(() => setRouterCopied(false), 1200);
|
||
}}
|
||
>
|
||
{routerCopied ? "✓ Copied" : "⧉ Copy"}
|
||
</button>
|
||
</div>
|
||
<pre style={s.commandPre}>{routerText}</pre>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* cURL modal — native <dialog> in top layer (escapes @container) */}
|
||
{modal === "curl" && (
|
||
<dialog ref={openDialog} style={s.dialog}
|
||
onClose={() => setModal(null)} onClick={onDialogClick}>
|
||
<div style={s.modalHeader}>
|
||
<div style={s.modalTitle}>cURL example</div>
|
||
<button style={s.modalCloseBtn} onClick={() => setModal(null)} aria-label="Close">×</button>
|
||
</div>
|
||
<div style={s.commandWrap}>
|
||
<div style={s.commandHeader}>
|
||
<div style={{ fontSize: 11, opacity: 0.7 }}>
|
||
Model: <code>{modelName || "(unresolved)"}</code>
|
||
</div>
|
||
<button style={s.iconButton} onClick={copyCurl}>
|
||
{curlCopied ? "✓ Copied" : "⧉ Copy"}
|
||
</button>
|
||
</div>
|
||
<pre style={s.commandPre}>{curlText}</pre>
|
||
</div>
|
||
{pdRouter && (
|
||
<p style={{ fontSize: 11, opacity: 0.85, marginTop: 8 }}>
|
||
<strong>PD-Disaggregation active</strong> — this targets the router on
|
||
{" "}<code>:{pdRouter.port}</code>; client traffic must not hit the role
|
||
servers directly.
|
||
</p>
|
||
)}
|
||
<p style={{ fontSize: 11, opacity: 0.7, marginTop: 8 }}>
|
||
Edit <code>CURL_HOST</code> / <code>CURL_PORT</code> in the Env panel.
|
||
</p>
|
||
</dialog>
|
||
)}
|
||
|
||
{/* Env modal — native <dialog> in top layer */}
|
||
{modal === "env" && (
|
||
<dialog ref={openDialog} style={s.dialog}
|
||
onClose={() => setModal(null)} onClick={onDialogClick}>
|
||
<div style={s.modalHeader}>
|
||
<div style={s.modalTitle}>Env / placeholder values</div>
|
||
<button style={s.modalCloseBtn} onClick={() => setModal(null)} aria-label="Close">×</button>
|
||
</div>
|
||
{placeholderGroups.curl.length > 0 && (
|
||
<div>
|
||
<div style={s.sectionHeading}>cURL placeholders</div>
|
||
{placeholderGroups.curl.map(({ key, label }) => (
|
||
<div key={key} style={s.formField}>
|
||
<label style={s.formLabel}>
|
||
{label} <code style={{ opacity: 0.6 }}>{`{{${key}}}`}</code>
|
||
</label>
|
||
<input
|
||
style={s.formInput}
|
||
value={envDraft[key] ?? ""}
|
||
onChange={(e) => setEnvDraft({ ...envDraft, [key]: e.target.value })}
|
||
/>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
{placeholderGroups.command.length > 0 && (
|
||
<div>
|
||
<div style={s.sectionHeading}>Command placeholders</div>
|
||
{placeholderGroups.command.map(({ key, label }) => (
|
||
<div key={key} style={s.formField}>
|
||
<label style={s.formLabel}>
|
||
{label} <code style={{ opacity: 0.6 }}>{`{{${key}}}`}</code>
|
||
</label>
|
||
<input
|
||
style={s.formInput}
|
||
value={envDraft[key] ?? ""}
|
||
onChange={(e) => setEnvDraft({ ...envDraft, [key]: e.target.value })}
|
||
/>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
<div style={{ display: "flex", justifyContent: "flex-end", gap: 8, marginTop: 16 }}>
|
||
<button style={{ ...s.iconButton, padding: "6px 14px" }} onClick={() => setModal(null)}>Cancel</button>
|
||
<button style={s.primaryBtn} onClick={() => { saveEnv(envDraft); setModal(null); }}>Save</button>
|
||
</div>
|
||
<p style={{ fontSize: 11, opacity: 0.7, marginTop: 10 }}>
|
||
Values persist in localStorage and are shared with the Deployment panel.
|
||
</p>
|
||
</dialog>
|
||
)}
|
||
|
||
{/* Submit-verified-cell modal — native <dialog> in top layer */}
|
||
{modal === "submit" && (
|
||
<dialog ref={openDialog} style={s.dialog}
|
||
onClose={() => setModal(null)} onClick={onDialogClick}>
|
||
<div style={s.modalHeader}>
|
||
<div style={s.modalTitle}>Submit verified cell</div>
|
||
<button style={s.modalCloseBtn} onClick={() => setModal(null)} aria-label="Close">×</button>
|
||
</div>
|
||
<p style={{ fontSize: 12, opacity: 0.85, marginTop: 0, marginBottom: 12 }}>
|
||
You've put together a combination that isn't in the verified
|
||
catalog yet. After you've run the command end-to-end on the
|
||
target hardware, this submits a pre-filled GitHub Issue that a
|
||
maintainer can convert into a PR.
|
||
</p>
|
||
|
||
<div style={s.sectionHeading}>Combination</div>
|
||
<code style={{ fontFamily: "Menlo, monospace", fontSize: 12 }}>
|
||
{base.hw} / {base.variant} / {base.quant} / {base.strategy} / {base.nodes}
|
||
</code>
|
||
{/* Overrides summary — the diff vs base in flag form. */}
|
||
{(() => {
|
||
const adds = diffLines.filter((d) => d.kind === "added");
|
||
const rems = diffLines.filter((d) => d.kind === "removed");
|
||
if (adds.length === 0 && rems.length === 0) return null;
|
||
return (
|
||
<>
|
||
<div style={{ ...s.sectionHeading, marginTop: 10 }}>
|
||
Overrides vs base ({adds.length} added · {rems.length} removed)
|
||
</div>
|
||
<pre style={{
|
||
margin: 0, padding: "8px 10px",
|
||
background: isDark ? "#111827" : "#f5f5f5",
|
||
border: `1px solid ${isDark ? "#374151" : "#e5e7eb"}`,
|
||
borderRadius: 4,
|
||
fontFamily: "'Menlo', 'Monaco', 'Courier New', monospace",
|
||
fontSize: 12, lineHeight: 1.4,
|
||
maxHeight: 160, overflowY: "auto",
|
||
whiteSpace: "pre-wrap",
|
||
}}>
|
||
{[...rems, ...adds].map((d, i) => (
|
||
<div
|
||
key={i}
|
||
style={d.kind === "added" ? s.diffLineAdded : s.diffLineRemoved}
|
||
>
|
||
{d.kind === "added" ? "+ " : "- "}
|
||
{d.line.replace(/^\s*/, "")}
|
||
</div>
|
||
))}
|
||
</pre>
|
||
</>
|
||
);
|
||
})()}
|
||
|
||
<div style={{ ...s.sectionHeading, marginTop: 14 }}>Attestation (all required)</div>
|
||
<div style={s.formField}>
|
||
<label style={{ fontSize: 12, display: "flex", alignItems: "flex-start", gap: 6 }}>
|
||
<input type="checkbox" checked={submitAttest.ranCommand}
|
||
onChange={(e) => setSubmitAttest({ ...submitAttest, ranCommand: e.target.checked })} />
|
||
I ran this exact command on the listed hardware.
|
||
</label>
|
||
<label style={{ fontSize: 12, display: "flex", alignItems: "flex-start", gap: 6 }}>
|
||
<input type="checkbox" checked={submitAttest.reachedReady}
|
||
onChange={(e) => setSubmitAttest({ ...submitAttest, reachedReady: e.target.checked })} />
|
||
The server reached READY and answered a cURL request successfully.
|
||
</label>
|
||
<label style={{ fontSize: 12, display: "flex", alignItems: "flex-start", gap: 6 }}>
|
||
<input type="checkbox" checked={submitAttest.outputCorrect}
|
||
onChange={(e) => setSubmitAttest({ ...submitAttest, outputCorrect: e.target.checked })} />
|
||
Output looked correct on at least one prompt.
|
||
</label>
|
||
</div>
|
||
|
||
<div style={{ ...s.sectionHeading, marginTop: 14 }}>SGLang version (required)</div>
|
||
<input
|
||
style={{ ...s.formInput, width: "100%", boxSizing: "border-box" }}
|
||
placeholder="sglang==0.5.4 (or git SHA abc1234)"
|
||
value={submitDraft.sglangVersion}
|
||
onChange={(e) => setSubmitDraft({ ...submitDraft, sglangVersion: e.target.value })}
|
||
/>
|
||
|
||
<div style={{ ...s.sectionHeading, marginTop: 14 }}>Benchmark result (optional)</div>
|
||
<input
|
||
style={{ ...s.formInput, width: "100%", boxSizing: "border-box" }}
|
||
placeholder="TTFT 95 ms / TPOT 18 ms / 1820 tok/s @ bs=64"
|
||
value={submitDraft.benchResult}
|
||
onChange={(e) => setSubmitDraft({ ...submitDraft, benchResult: e.target.value })}
|
||
/>
|
||
|
||
<div style={{ ...s.sectionHeading, marginTop: 14 }}>Notes / caveats (optional)</div>
|
||
<textarea
|
||
style={{ ...s.formInput, width: "100%", boxSizing: "border-box",
|
||
minHeight: 110, resize: "vertical", fontFamily: "inherit" }}
|
||
placeholder="Cluster config, env-var quirks, NIC mappings, multi-node bootstrap details, …"
|
||
value={submitDraft.notes}
|
||
onChange={(e) => setSubmitDraft({ ...submitDraft, notes: e.target.value })}
|
||
/>
|
||
|
||
<div style={{ display: "flex", justifyContent: "flex-end", gap: 8, marginTop: 16, alignItems: "center" }}>
|
||
{!submitReady && (
|
||
<span style={{ fontSize: 11, opacity: 0.7, marginRight: "auto" }}>
|
||
Tick all attestations and fill SGLang version to enable submit.
|
||
</span>
|
||
)}
|
||
<button style={{ ...s.iconButton, padding: "6px 14px" }} onClick={() => setModal(null)}>Cancel</button>
|
||
<a
|
||
href={submitReady ? submitUrl : undefined}
|
||
target="_blank" rel="noopener noreferrer"
|
||
onClick={(e) => { if (!submitReady) e.preventDefault(); else setModal(null); }}
|
||
style={{
|
||
...s.primaryBtn,
|
||
textDecoration: "none",
|
||
display: "inline-flex", alignItems: "center",
|
||
opacity: submitReady ? 1 : 0.4,
|
||
cursor: submitReady ? "pointer" : "not-allowed",
|
||
}}
|
||
>
|
||
Open submission on GitHub →
|
||
</a>
|
||
</div>
|
||
<p style={{ fontSize: 11, opacity: 0.7, marginTop: 10 }}>
|
||
The CTA opens a pre-filled GitHub Issue using the
|
||
<code> 3-playground-verified-cell.yml</code> template. A
|
||
maintainer with the listed hardware will review and convert it
|
||
into a cookbook PR.
|
||
</p>
|
||
</dialog>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|