Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
275 lines
13 KiB
Markdown
275 lines
13 KiB
Markdown
# Engine extension: add a new playground feature axis
|
|
|
|
Loaded on demand by the `cookbook-add-model` skill. **Rare** — adding a model
|
|
cookbook is data-only and never needs this. The current 7 built-in axes
|
|
(`attention`, `moe`, `parsers`, `speculative`, `pdDisagg`, `hicache`,
|
|
`hisparse`) already cover the SGLang feature surface most cookbooks need.
|
|
|
|
**Model-specific features are config DATA, not engine code.** The axis
|
|
handlers read options / flags / env / gating straight from
|
|
`config.playgroundFeatures`, so a model-specific feature is added as data on
|
|
an existing axis with NO engine edit — MegaMoE W4A4 is not its own axis, it's
|
|
config data on `moe` (a `megamoe` backend option + a `megamoeQuant`
|
|
sub-select). Reach for this file ONLY when a feature's *shape* — its
|
|
title + the flag family it strips + its option/state model — is something no
|
|
existing axis can express.
|
|
|
|
**When you do extend, build a GENERIC primitive, never a model-named handler.**
|
|
The right unit is a reusable shape (e.g. "a titled single-select that strips a
|
|
configurable flag family and splices the picked option's flags" — exactly the
|
|
`speculative` handler's shape, minus its hardcoded title + `--speculative-*`
|
|
strip list). Parameterize title, strip-prefixes, and options from config so
|
|
the next model of that shape is pure config. Do NOT add a `kvcache` /
|
|
`<model-feature>` handler that hardcodes one model's flag — that's the
|
|
model-specific-code-in-the-engine anti-pattern this architecture exists to
|
|
avoid. (Precedent: Nemotron3's "KV Cache DType" and Qwen3's mamba-cache select
|
|
are the SAME single-select shape → one generic primitive serves both, then
|
|
both are config.)
|
|
|
|
**A new axis is backward-compatible — zero churn on existing configs.** The
|
|
runtime is opt-in per key: the apply/render loop does `const fc =
|
|
pgFeatures[axisId]; if (!fc) continue;`, so any config that doesn't declare
|
|
the key never sees the axis. And a model-specific axis does NOT join the
|
|
opt-out "general axes ship on every cookbook" set (that set is an authoring
|
|
convention for NEW configs, not a runtime default) — so review-pr won't flag
|
|
existing pages for lacking it, and you never touch a merged config. Only the
|
|
models that expose the control declare it. Touches `_playground.jsx` only.
|
|
|
|
For the per-model config/cells/MDX reference see [authoring-reference.md](authoring-reference.md).
|
|
|
|
---
|
|
|
|
## 3.1 Decide
|
|
|
|
Before touching the engine, confirm:
|
|
|
|
- The feature cannot be expressed as data on an existing axis (a new MoE
|
|
backend belongs in `moe.backend.options`; a new parser in `parsers.items`;
|
|
a new spec preset in `speculative.options`). This is the common case —
|
|
most "new features" are new options, not new shapes.
|
|
- The *shape* is genuinely new (state model + strip pattern), AND you are
|
|
adding it as a GENERIC config-parameterized axis (title / strip-prefixes /
|
|
options all from config), not a one-model handler. If you'd hardcode a
|
|
specific flag like `--kv-cache-dtype`, stop — generalize the shape instead.
|
|
- The shape has a clean strip-prefix → emit-flag pattern.
|
|
|
|
If unsure, add it as data first (in one cookbook's config under an
|
|
existing axis) before promoting it to a built-in axis.
|
|
|
|
## 3.2 Pick the axis id and state shape
|
|
|
|
The axis id is the key in both `config.playgroundFeatures` and the
|
|
internal `deltas` object. Use camelCase, descriptive but short:
|
|
`mambaCache`, `attentionBackend`, `kvCacheDtype`.
|
|
|
|
The state shape is whatever `initState` returns. Common shapes:
|
|
|
|
- Single-select: a string sentinel (e.g. `"disabled"` / `"current"` / an
|
|
option id).
|
|
- Multi-toggle: `{[itemId]: bool}`.
|
|
- Sub-knobs: `{[knobId]: value | null}`.
|
|
- Compound (axis with its own internal sub-state, like PD-Disagg's
|
|
`{mode, ibDevice}`): a plain object.
|
|
|
|
Pick ONE "inherit base" sentinel and document it in the handler comment.
|
|
|
|
## 3.3 Implement the handler
|
|
|
|
Add one entry to `AXIS_HANDLERS` in `_playground.jsx`.
|
|
The handler owns everything: state init, apply (strip+insert), hidden-revert,
|
|
AND the JSX render. Engine main loop iterates `AXIS_HANDLERS` and calls each
|
|
method by name — adding a new axis is genuinely a one-place change.
|
|
|
|
Template:
|
|
|
|
```js
|
|
// ---- Axis: <Title> ----------------------------------------------------
|
|
// <one-paragraph description of what this axis controls and why it
|
|
// exists. Mention the SGLang feature it wraps and the strip/insert
|
|
// policy.>
|
|
<axisId>: {
|
|
initState: (fc) => /* initial state value */,
|
|
|
|
// Called when base cell changes. Return new value if the picked option
|
|
// is now hidden by a constraint; otherwise return value unchanged.
|
|
// Disabled picks are intentionally NOT auto-reverted (soft warning).
|
|
revertHidden: (value, fc, base, h) => {
|
|
// ... return value or a new value
|
|
return value;
|
|
},
|
|
|
|
// Pure function. Receives the current (flags, env) and returns the next
|
|
// (flags, env). Do NOT mutate inputs. The `value` argument is whatever
|
|
// initState returned. The `fc` argument is config.playgroundFeatures[axisId].
|
|
// The `sel` argument is the current base cell selection. The `h`
|
|
// argument is the helpers bundle (strip/insert primitives + anchors).
|
|
apply: ({ flags, env, value, fc, sel, h, derived }) => {
|
|
if (/* value is the inherit-base sentinel */) return { flags, env };
|
|
flags = h.stripFlagsByFirstToken(flags, [/* prefixes this axis owns */]);
|
|
if (/* an option is picked */) {
|
|
flags = h.insertAfter(flags, h.ANCHOR_NEAR_<X>, [/* new flags */]);
|
|
// or: flags = h.insertBeforeTail(flags, [/* new flags */]);
|
|
// if the axis mutates env:
|
|
// env = h.stripEnvByPrefix(env, fc.stripEnv || []);
|
|
// env = [...env, /* additional env vars */];
|
|
}
|
|
return { flags, env };
|
|
},
|
|
|
|
// Optional: read the base cell's flag array back into the same shape
|
|
// initState/apply use. Render shows this as the default selection
|
|
// (dropdown option or checked chip) when the state slot is the inherit
|
|
// sentinel — so the user sees the cell's actual --tp / MoE backend /
|
|
// spec preset instead of an opaque "Auto." When derive returns a real
|
|
// value, the inherit-sentinel option is hidden from the control. Apply
|
|
// also receives the derived
|
|
// value (as `derived`) and may use it as a no-op shortcut when the
|
|
// user's pick matches base. Skip when your axis owns flags that never
|
|
// appear in base cells (PD-Disagg / HiCache).
|
|
// deriveFromBase: (cell, fc, h) => ({ ... }) | null,
|
|
|
|
// Optional: hints for the renderer. Currently only pdDisagg uses this
|
|
// to report its role banner. Omit if not needed.
|
|
// getRenderHints: (value, fc) => ({ pdMode: ... }) | null,
|
|
|
|
// Returns the axis card JSX. The outer div MUST have key={axisId} so
|
|
// React can track it in the engine's map loop. Return null for
|
|
// axis-level gating (e.g. HiSparse when the live PD mode isn't `decode`). Lay out as a single
|
|
// compact horizontal row: title on the left, fields after.
|
|
render: ({ axisId, value, setValue, fc, base, s, h, renderChip, renderSelect, derived }) => {
|
|
if (/* axis-level gating fails */) return null;
|
|
return (
|
|
<div key={axisId} style={s.card}>
|
|
<div style={s.compactRow}>
|
|
<span style={s.axisTitle}>Axis Title</span>
|
|
{/* For multi-option fields, use renderSelect(...) — the default.
|
|
For on/off toggles or single-select chip groups, use
|
|
renderChip instead (see "Control choice" in the conventions
|
|
below). Read state from `value`; write via `setValue(next)`
|
|
(replaces the whole axis slot). */}
|
|
<span style={s.field}>
|
|
<span style={s.fieldLabel}>Field</span>
|
|
{renderSelect(value.slot, fc.entries, (v) =>
|
|
setValue({ ...value, slot: v }), base)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
```
|
|
|
|
**Important conventions**:
|
|
|
|
- Insert the entry in the position you want it rendered. `AXIS_HANDLERS`
|
|
is iterated in insertion order for both render and apply.
|
|
- Use `h.ANCHOR_NEAR_*` constants for insertion. Add a new anchor to the
|
|
helpers bundle if your axis needs to land somewhere new in the flag
|
|
block.
|
|
- Use lowercase HTML JSX tags only. Capitalized tags get rebound by
|
|
Mintlify.
|
|
- Inside `render`, read state via `value` (the slice for this axis).
|
|
Write state via `setValue(next)` (replaces the whole slice). For
|
|
compound axes, do `setValue({ ...value, [k]: nextK })`.
|
|
- Layout: one `s.compactRow` per axis card, `s.axisTitle` for the
|
|
leading label, one `s.field` per (label + input) pair.
|
|
- Control choice — `renderSelect` vs `renderChip`:
|
|
- `renderSelect(current, entries, onPick, base, labelFor?, opts?)` is
|
|
the **default** compact control (a `<select>` dropdown). It filters
|
|
hidden chips and disables greyed-out ones internally — no per-chip
|
|
`evaluateChip` loop needed in the render body. Most axes use it
|
|
(attention, moe, pdDisagg, hisparse, hicache). Pass
|
|
`{ hideValues: [<sentinel>] }` when your `deriveFromBase` resolved to
|
|
a real value, so the inherit-sentinel ("Auto" / "Inherited" /
|
|
"current") doesn't clutter the dropdown.
|
|
- `renderChip(label, current, value, onPick, { disabled?, disabledReason? })`
|
|
renders a **button** instead of a dropdown row. Use it for a chip
|
|
group when you want the options laid out as buttons. It serves two
|
|
shapes:
|
|
- **Multi-toggle** (Parsers) — one independent on/off chip per item;
|
|
`current` is that item's effective bool, `value` is `true`, so the
|
|
chip is "checked" when the item is on.
|
|
- **Single-select** (Speculative) — a radio-style group; pass the
|
|
group's effective value as `current` and each option's id as
|
|
`value`, so exactly one chip is checked (`current === value`).
|
|
Chip groups own their visibility/disable filtering: loop
|
|
`h.evaluateChip(opt, base)` in the render body, skip `c.hidden`,
|
|
filter the inherit-sentinel yourself when `deriveFromBase` resolved
|
|
to a real value, and forward `c.disabled` / `c.disableReason` into
|
|
`renderChip`'s opts (this is what surfaces a disabled chip's tooltip,
|
|
e.g. a "Coming soon" entry).
|
|
- Selected chips use the same terracotta (`#D45D44`) as the Deploy
|
|
panel's selected button, so both widgets read as one
|
|
visual system. Don't introduce a per-axis accent color.
|
|
- Default-from-base: if your axis can be read out of base cells'
|
|
flags, implement `deriveFromBase` and have your render show the
|
|
derived value when state is the sentinel (e.g.
|
|
`const eff = value.tp !== null ? value.tp : (derived && derived.tp)`).
|
|
This is what makes a fresh playground load show the user's actual
|
|
recipe instead of "auto." Flag-parsing helpers on `h`:
|
|
`parseIntFlag`, `hasFlag`, `findFlagArg`.
|
|
- **Avoid the `in` operator wrapped in unary** (`!(x in y)`). Mintlify's
|
|
AST walker crashes on it (`TypeError: this[e] is not a function`). Use
|
|
`obj.key === undefined` or `obj.id !== undefined` instead. Bare
|
|
`if (key in obj)` (no surrounding `!`) is fine.
|
|
|
|
## 3.4 Document the per-cookbook schema
|
|
|
|
Edit the file header in `_playground.jsx` to add your new axis to the
|
|
"Recognised keys" list, with a one-line description of its schema.
|
|
Optionally add a paragraph below explaining its strip/insert policy.
|
|
|
|
Update the §2.3 axis table in [authoring-reference.md](authoring-reference.md) to list the new axis.
|
|
|
|
## 3.5 Migrate cookbooks that need it
|
|
|
|
For each cookbook that should expose this axis, add a
|
|
`playgroundFeatures.<axisId>` entry to its config. Verify the chip group
|
|
renders, options apply correctly, and the diff matches expectations.
|
|
|
|
---
|
|
|
|
## Pitfalls (engine work)
|
|
|
|
**Insertion anchor misses** — `insertAfter` falls back to right-after
|
|
`--model-path` if none of its anchor prefixes are present. If your axis
|
|
emits flags that should land somewhere specific, include the most likely
|
|
anchor prefixes in your call. Order doesn't matter (set semantics).
|
|
|
|
**Conditional strips** — Some axes strip ONLY when overridden
|
|
(`attention.tp`, `moe.backend` (incl. the MegaMoE quant env), `speculative`).
|
|
Others strip
|
|
UNCONDITIONALLY whenever declared (`parsers`, `pdDisagg`, `hicache`). The
|
|
header comment in `AXIS_HANDLERS` documents which policy each axis uses;
|
|
follow the same pattern when adding a new axis. If unsure, prefer
|
|
conditional strip — it preserves base behavior when the user does not
|
|
opt in.
|
|
|
|
**Closure of `AXIS_HANDLERS`** — Inside a handler method, you can
|
|
reference `AXIS_HANDLERS.<otherAxis>` for cross-handler calls (no built-in
|
|
axis currently needs this, but it works because `AXIS_HANDLERS` is in
|
|
lexical scope). Do NOT use this for general logic — it tightly couples
|
|
handlers. Reserve it for one handler's helpers shared between its own
|
|
`render` and `revertHidden`.
|
|
|
|
---
|
|
|
|
## Review checklist for a new-axis PR
|
|
|
|
- [ ] `AXIS_HANDLERS` is the ONLY place that mentions the new axis id
|
|
(apart from per-cookbook config). No `if (axisId === '<new>')`
|
|
branches anywhere in the engine.
|
|
- [ ] `initState` is deterministic and idempotent (does not depend on
|
|
the base cell).
|
|
- [ ] `apply` is pure — does not mutate inputs.
|
|
- [ ] `revertHidden` returns the same reference when nothing changed
|
|
(avoids unnecessary re-renders).
|
|
- [ ] `render` returns `null` when axis-level gating fails (whole card
|
|
hidden) — does not render an empty placeholder.
|
|
- [ ] `render` sets `key={axisId}` on its outer element.
|
|
- [ ] No `!(x in y)` patterns introduced (Mintlify AST walker crashes).
|
|
- [ ] File header lists the new axis in "Recognised keys".
|
|
- [ ] The §2.3 table in `authoring-reference.md` lists the new axis.
|
|
- [ ] One existing cookbook config is updated to consume the new axis,
|
|
and visual verification shows the diff is correct.
|