Inference spec decode train
Skill cfregly/gpu-perf-tune/plugins/profile-and-optimize/skills/inference-spec-decode-train
Train + validate a speculative-decoding draft head (EAGLE3 or DFlash) for an ARBITRARY target LLM, generalizing the GLM-5.1-only SpecForge recipe so any new model gets one. Parameterizes what that offline recipe hard-codes: target path, aux-hidden-states layers (derived [1, L/2-1, L-4] from config.json num_layers), chat template, draft-head config, and method. Wires the SpecForge prepare_hidden_states -> train_eagle3 / train_dflash -> convert-to-vLLM flow on Slurm, then gates on a measured acceptance-length A/B (dflash_vllm_eval.py + vLLM spec_decode_* counters) vs the standing config. This is the train-spec-decode phase of inference-model-optimize, usable standalone. Triggers on "train an EAGLE3 draft", "train a DFlash head", "build a speculative decoder for <model>", "spec-decode draft training", "draft model acceptance", "SpecForge for <model>", "generalize eagle3-train", or any combination of "train / build / validate" with "eagle3 / dflash / draft / speculative / spec-decode".From its SKILL.md
npx -y skills add cfregly/gpu-perf-tune --skill inference-spec-decode-trainAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
12.9 KB, ~3.2k tokens by cl100k_base, as published. Nobody here has run it
inference-spec-decode-train
Purpose
Give an arbitrary target LLM a speculative-decoding draft head. SpecForge's
worked EAGLE3 + DFlash recipes are hard-wired to one model
(the run_glm5p1_eagle3_offline.sh recipe bakes in the target path, the aux
layers [1,38,74], the glm5p1 chat template, and the GLM draft config). This
skill generalizes that stack so a new model gets the same train ->
validate -> promote loop without copy-pasting the GLM script.
It reuses SpecForge verbatim
(prepare_hidden_states.py, train_eagle3.py, train_dflash.py,
dflash_vllm_eval.py, convert_dflash_to_vllm.py, the slurm/*.sbatch
launchers) and only parameterizes the per-target inputs.
When to use
- A new model has a latency ceiling you want to beat with a trained draft head and no public draft exists.
- You want to train a DFlash head as an alternative to a model's built-in MTP.
- The
inference-model-optimizeorchestrator reached Phase 6.
Do not use this skill for:
deepseek_mtp/ngram-- there is nothing to train (MTP is a built-in head, ngram is prompt-driven). Just set--speculative-configand validate acceptance (Phase 7 of the orchestrator / the validation half here).- Consuming a pre-trained public draft (e.g.
lightseekorg/kimi-k2.6-eagle3) -- pull it and serve it. No training needed. - The acceptance A/B alone on an already-trained head -- you can run just the validation phase below.
Example prompts
- "Train an EAGLE3 draft head for MiniMax-M2.7 and A/B its acceptance."
- "Build a DFlash speculator for this 70-layer model."
- "Generalize the GLM eagle3-train recipe to
zai-org/GLM-5.2." /inference-spec-decode-train --target /models/base --method eagle3 --chat-template <name>
Prerequisites
The skill fails closed if any of these are not satisfied.
- The target base checkpoint (BF16 or FP8 -- NOT the NVFP4 serving copy) staged on a shared FS / PVC the Slurm trainer can read.
- The SpecForge trainer container built for the target arch -- a new arch may need a newer SGLang/transformers (a target arch class missing from the trainer's SGLang is a kill-gate. Reconcile versions before any GPU spend).
- The target
config.json(fornum_hidden_layers-> aux layers, and to confirm the arch is supported by the trainer's SGLang). - Free GPU nodes: plain K8s (e.g. GB300) via
kubectlGPU requests, or Slurm-on-K8s (e.g. B200) viasinfo/squeue. Training is a multi-hour job -- submit-and-queue, never preempt serving. - A chat-template name registered in SpecForge's
specforge/data/template.pyfor the target family (add one if absent, as was done forglm5p1).
Workflow
Phase 0: derive the per-target inputs + scaffold
State back the parameters and derive the EAGLE3 aux layers from
config.json: for an L-layer target the SpecForge convention is
[1, L//2 - 1, L - 4] (the GLM 78-layer target -> [1, 38, 74]). Resolve:
target-model-path,method(eagle3|dflash),chat-template,draft-config(a per-target draft-head JSON, seeded from the GLMglm5p1-eagle3.jsonwithnum_layers/hidden_size/vocab taken from the targetconfig.json),num-gpus/tp-size, dataset.
The scaffolder lays down spec-decode/ in the bundle:
spec-decode/configs/<slug>-eagle3.json(+<slug>-dflash.json) -- draft head config rendered from the targetconfig.json.spec-decode/run-<slug>-offline.sh-- the generalized 2-step recipe (parameterized clone ofrun_glm5p1_eagle3_offline.sh: env-drivenTARGET_MODEL_PATH,AUX_LAYERS,CHAT_TEMPLATE,DRAFT_CONFIG,METHOD).spec-decode/slurm/-- copies of the SpecForge*.sbatchlaunchers (prepare-data, capture, train, eval) with the target paths parameterized.spec-decode/dflash_vllm_eval.py+convert_dflash_to_vllm.py-- the in-engine acceptance eval + checkpoint converter (verbatim from SpecForge).spec-decode/deploy/<slug>-spec-canary.yaml-- the--speculative-configcanary deploy for the acceptance A/B.
Phase 1: prepare the offline dataset
Submit the data-prep sbatch (CPU): build the mixed UltraChat + ShareGPT JSONL through the target's chat template. Gate: the JSONL exists with the expected conversation count and renders cleanly through the chat template (a bad loss-mask / template is the GLM "training loss stays zero" failure mode).
Phase 2: capture target hidden states
Submit the capture sbatch: prepare_hidden_states.py holds the target resident
at --tp-size and dumps aux hidden states at the derived AUX_LAYERS. Offline
(capture-then-train) is mandatory for huge MoE targets -- hold the target only
during capture, then iterate draft training cheaply. Gate: hidden-state shards
written for the full dataset.
Phase 3: train the draft head (submit-and-queue)
Submit the train sbatch (train_eagle3.py or train_dflash.py). This is the
multi-hour job. Submit it as a self-driving Slurm job and do NOT babysit (it
re-queues through preemption). Gate: training acc_0 / loss is non-degenerate
(a healthy run lands acc_0 around ~0.6-0.7. A head stuck near zero acceptance
is a train/serve mismatch to debug, not a result to ship).
Phase 4: convert to a vLLM-loadable draft
Run convert_dflash_to_vllm.py (DFlash) / the EAGLE3 export to produce a
draft the serving vLLM can load via --speculative-config.model=<path>. Stage
it on the serving PVC (e.g. /models/target/speculator/<slug>/<sha>/).
Phase 5: validate acceptance (the gate that matters)
Deploy the --speculative-config canary and measure in-engine acceptance:
spec-decode/dflash_vllm_eval.py-- the authoritative in-engine acceptance (acceptance length per position) via vLLM metrics.- vLLM Prometheus
spec_decode_num_accepted_tokens/spec_decode_num_draft_tokens/spec_decode_num_accepted_tokens_per_posdeltas over a driven bench window.
Run it as a same-node A/B: trained-draft arm vs the standing config (e.g. MTP K=3, or no-spec). Gate: a measured acceptance-length win AND an end-to-end TPOT win under cudagraph (NOT eager -- an eager spec-decode "win" is host overhead, not GPU work). A draft head that loses -- even one whose training acceptance looked healthy -- is do-not-ship: keep the standing config and report why.
Teardown
Cancel any running Slurm jobs you own. Tear down the canary deploy by label
(kubectl delete deploy,pod -l experiment=<id-slug>). Keep the trained draft
artifact on the PVC until the parent run promotes or discards it.
Generalization map (GLM-hardcoded -> parameter)
| GLM-5.1 hardcoded value | Generalized parameter | Derivation |
|---|---|---|
TARGET_MODEL_PATH=/mnt/data/models/GLM-5.1-FP8 | --target-model-path | operator input (base, not NVFP4) |
--aux-hidden-states-layers 1,38,74 | AUX_LAYERS | [1, L//2-1, L-4] from config.json num_hidden_layers |
--chat-template glm5p1 | --chat-template | target family. Add to SpecForge template.py if absent |
configs/glm5p1-eagle3.json | spec-decode/configs/<slug>-eagle3.json | rendered from target config.json (hidden_size/vocab/layers) |
| EAGLE3 only | `--method eagle3 | dflash` |
TP_SIZE=8 (single B200 node) | --tp-size / --num-gpus | from the target's fit math |
Verdict rigor (DRAFT vs VERDICT)
Training acc_0 is a training-time signal, not a serve verdict. The
ship/no-ship claim is the serve acceptance + TPOT A/B in Phase 5, which
follows the standard rule: DRAFT unless same-node + >=3 trials + metric-isolated
(median TPOT/ITL) + both arms under matching cudagraph_mode. Acceptance length
must be measured in-engine (the vLLM counters), never inferred from end-to-end
latency alone.
Safety
- Never preempt serving -- training uses idle Slurm nodes only. Submit-and- queue with a self-driving Job, do not babysit or drain a serving node. A drained quiet window is for bench windows only, never for training.
- Standing config stays until a measured win -- the standing
--speculative-config(e.g. MTP K=3) is not changed until the Phase-5 A/B shows a real acceptance + TPOT win under cudagraph. - Experiment isolation -- the canary deploy + any draft-staging objects are
experiment-prefixed +
experiment=<id-slug>labeled. Teardown by label. - Local fork only -- keep any SpecForge changes on a local fork. No upstream PR / external outreach without explicit per-turn operator approval.
Source-of-truth references
- SpecForge
examples/run_glm5p1_eagle3_offline.sh-- the 2-step offline recipe the generalizedrun-<slug>-offline.shclones. - vLLM
vllm/config/speculative.py-- the--speculative-configschema (method,model,num_speculative_tokens,draft_tensor_parallel_size). docs/METHODOLOGY.md-- benchmark hygiene (eager vs cudagraph for spec-decode), kernel-work classification, and verdict rigor.
Contact
Open an issue in this repository.
Full-context reporting (no bare numbers)
Per the methodology canon "Every performance number carries its full context (no bare
numbers)" (docs/METHODOLOGY.md, "Full-context reporting"): every number this
skill emits MUST carry its full measurement-context descriptor, and every comparison MUST be
matched on it. A bare tok/s / TPOT / BW / %SoL / speedup is a defect - it cannot set a
default, ship a config, or appear in a report.
- Identity: model (+HF path), hardware (exact ceiling token
GB300/B200), quant, kv-cache dtype. - Parallelism: TP, DP (replicas), PP, EP, parallel_strategy.
- Serving cfg: max-num-seqs, max-num-batched-tokens, gpu-memory-utilization, max-model-len, cudagraph_mode/enforce_eager, async_scheduling, prefix-caching.
- Workload: dataset, ISL/OSL (or mean in/out tokens), concurrency, num-prompts.
- Regime: warm vs cold. Latency vs throughput tier.
- Stack: image/vllm commit, bench backend, serving engine.
- Grounding:
%SoL(+ ceiling key fromconfigs/sol-ceilings.yaml- never inline a peak), sol_rigor (L1-L4), trials n (mean±std), same-node, baseline named. - Per-number exact shape (no smoothing): when reporting more than one number, keep EACH with its own exact shape (ISL/OSL, concurrency, dataset, regime) - never normalize a set to one uniform descriptor that hides per-point variation (e.g.
c=1 @ ISL1024/OSL256+c=64 @ ISL4096/OSL512, NOT one shared "random").
If this skill produces a measurement (tok/s, latency, %SoL, speedup), follow the
rigor discipline: capture L1 zymtrace + L3 DCGM (L4 ncu where feasible)
Speed-of-Light and publish --strict. Canonical map: docs/METHODOLOGY.md. Skills that
do not produce measurements are exempt (docs/METHODOLOGY.md "Speed-of-light framing").
Next lever / BREAKTHROUGH (Grind Mandate)
If this skill emits a measured result, its output MUST end by naming the next perf lever,
its expected unlock (direction + rough magnitude), and the gate that proves/refutes it,
per the Grind Mandate (docs/METHODOLOGY.md, "Always be grinding"). A
measured win is the new floor, not the finish -- so do everything we can to find the next
BREAKTHROUGH: the highest-EV unlock toward Speed-of-Light (a new champion / kernel / router /
quant / parallelism / spec-decode win, or an unblocked stack), not just the next micro-lever.
Rank the candidate breakthrough levers by value x cost (the GRIND FRONTIER, perftunereport value_view), pursue the top, bank the rest with evidence. Record WHY a refuted lever loses,
update the standing frontier in the active bundle's HANDOFF.md. Never conclude
"exhausted/optimal/done" without an explicit next-lever frontier (an empty frontier AND a
documented SoL wall only). Delete this section ONLY if the skill produces no measurements.
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.