Skip to content

[8/8] specdec-release: conduct the drafter readiness flow end to end - #2936

Draft
Zhehao-Hu wants to merge 3 commits into
zhehaoh/specdec-gates-benchfrom
zhehaoh/specdec-release-skill
Draft

Zhehao-Hu wants to merge 3 commits into
zhehaoh/specdec-gates-benchfrom
zhehaoh/specdec-release-skill

Conversation

@Zhehao-Hu

@Zhehao-Hu Zhehao-Hu commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

[8/8] Day-0 speculative-decoding readiness flow. Merge in order 1 → 8. Base: [7/8] #2935. Uses everything before it.
Series: [1/8] #2839 · [2/8] #2930 · [3/8] #2931 · [4/8] #2932 · [5/8] #2933 · [6/8] #2934 · [7/8] #2935 · [8/8] #2936 (this)

What does this PR do?

Type of change: New feature

Completes the specdec-release agent skill. Asked "train a DFlash2 drafter for Qwen3-8B and tell me whether it's ready", the agent goes from no drafter to a readiness verdict in a fixed sequence, with a gate after every stage. It is a conductor: it submits the existing launcher pipelines and enforces the gates from [6/8] and [7/8]. It doesn't re-implement training, serving or benchmarking.

Scope: DFlash and DFlash2. Only dflash_online_training.sh writes the regression_check.json that gate_train reads, so EAGLE3 and DSpark (train_eagle*.sh) are refused before anything is submitted, as are Domino (no vLLM path) and LiLiCorr (SGLang only). Extending to EAGLE3 means making its training and serving scripts write the same verdict files.

setup ─▶ train pipeline (data → train+export → smoke) ─▶ freeze ─▶ A|B bench ─▶ closeout
         gate_train + gate_smoke                         freeze_drafter  gate_bench + gate_correctness (report)

Verdicts:

  • DRAFTER_READY: every gate passed. Ready for release review; it is not a publish decision, and the gates set no per-model performance bar.
  • INFEASIBLE: training, export or serving failed, and triage found no fix within the run.
  • ANOMALOUS: the measurement is invalid after the allowed retries, or freeze failed.

SYSTEMIC (cluster or credentials down) aborts the run, as in day0-release; it is not a verdict.

plugins/modelopt/skills/specdec-release/:

  • SKILL.md: inputs, the stage sequence, how to run each gate, state and resume, the triage table, and the report.
    • Inputs include existing_drafter, which skips training but not its gates, and approve_submissions: each | none. each is the default: the agent shows every cluster job (launcher, sbatch, weight staging, including a sub-agent's) and waits for an OK. One OK covers one exact job; a retry or a changed partition, image or command needs a new one.
    • Staging: Step 1 stages a target the cluster lacks: reuse a readable hf_local copy, else one download job into hf_local at a pinned revision, then check the shards. A 401/403 on a gated model is USER_CONFIG_ERROR. It lives here until it moves to the common skill for ptq and day0-release.
    • Do not advance past a failed gate, and never read a verdict from a log or a job state.
    • Watching: the agent starts watching in the same turn as every submission, and tracks result files rather than job states.
    • State: specdec_state.json records each stage's status, experiment and job ids, overrides and gate verdicts. On resume the agent re-runs nothing that passed, re-watches instead of resubmitting, and re-verifies the frozen drafter before benchmarking.
    • Log: every step goes to work_log.md as ACTION or HUMAN_INTERVENTION, so interventions can be counted.
    • Report: SPECDEC_REPORT.md, with the setup, gate verdicts, the frozen drafter's checksums and the A|B table.
    • Triage of CHECKPOINT_NOT_SERVABLE acts on the smoke JSON's stage, not the log's last traceback: server_timeout resubmits with a larger SERVER_TIMEOUT ([2/8]).
  • scripts/freeze_drafter.py: copies the export to /cicd/drafts/<target>/<algo>/<run_id> through a .partial sibling, re-hashes before the rename, writes SHA256SUMS, and makes the files read-only. It returns a manifest_sha256, and --verify --manifest-sha256 re-checks it on resume. The bench serves the frozen copy, which freeze proves byte-identical to the export at freeze time.
  • scripts/gate_train.py --export-only: checks an existing drafter's export without a training verdict.
  • tests/: freeze_drafter and --export-only cases, plus evals.json with 6 trigger and non-trigger prompts.

plugins/modelopt/skills/speculative-decoding/:

  • SKILL.md routes the end-to-end flow to specdec-release.
  • references/stages/configure.md gains "fit every task to the cluster":
    • set gpus_per_node to the node size on whole-node clusters;
    • when a training task's GPU count changes, scale gradient_accumulation_steps to keep the global batch.

Usage

Use specdec-release to train a DFlash2 drafter for Qwen/Qwen3-8B on OCI-JHB with the dev budget, and tell me whether the drafter is ready for release review.

Resume the spec-dec run in this directory.

Testing

  • Skills suite on this commit: 163 passed, 1 skipped. Launcher suite: 192 passed.

  • End-to-end on OCI-JHB (GB300) with Claude Code, unattended apart from submission approvals:

    Run Path Result
    Variance pass existing_drafter (M0 drafter) and full train (seed 1) 13 gate and freeze verdicts, all passed; both drafters DRAFTER_READY
    Rehearsal 1 (fresh session, only the plugin and a launcher env) full train DRAFTER_READY, 0 interventions. Exposed a fresh-venv launcher bug, fixed in Pin "leptonai<0.47" to fix tools/launcher CI job #2892 and [launcher] Add build-system so uv run installs modelopt_launcher #2921
    Rehearsal 2 full train, then killed and resumed in a new session DRAFTER_READY; the resume re-verified the freeze and re-ran nothing. 3 unlogged nudges, fixed here (watch after every submission, load the cluster in every shell, run dir)
    Rehearsal 3 full train, one session DRAFTER_READY, 0 interventions and no nudges. Watched and re-armed its own watchers, ran every remote command through remote_run, and caught two SSH failures at submit by checking squeue (nothing queued), retrying with approval
  • Results across the variance pass and rehearsals:

    • training loss 6.37–6.71, accuracy 0.167–0.175;
    • smoke AL 1.68–1.75;
    • bench AL 1.71–1.79, speedup 1.36–1.42 at C=1 and 1.23–1.33 at C=32;
    • greedy exact match 0.956–0.963.
  • In the rehearsals, the bench and smoke jobs exited 7–17 s after writing their results, with no hangs.

  • First new target, Qwen3.8-27B + DFlash2 (no prior example): DRAFTER_READY (loss 2.93, smoke AL 1.76, speedup 1.40/1.29/1.06 at C=1/8/32), with 6 interventions beyond approvals. They led to the staging step, the approval scope and stage-first triage above, and [2/8]'s SERVER_TIMEOUT.

  • Not run: the INFEASIBLE and ANOMALOUS triage paths on GPU (their gates' failure classes are unit-tested), local Docker job hosts, and DFlash end to end (in scope, but only DFlash2 was run).

Before your PR is "Ready for review"

Make sure you read and follow Contributor guidelines and your commits are signed (git commit -s -S).

Make sure you read and follow the Security Best Practices (e.g. avoiding hardcoded trust_remote_code=True, torch.load(..., weights_only=False), pickle, etc.).

  • Is this change backward compatible?: ✅ (new skill; one routing line and one configure note in speculative-decoding)
  • If you copied code from any other sources or added a new PIP dependency, did you follow guidance in CONTRIBUTING.md: N/A (stdlib only)
  • Did you write any new necessary tests?: ✅
  • Did you update Changelog?: ✅ (Misc entry)
  • Did you get Claude approval on this PR?: ❌ - not yet run

Additional Information

  • Consistent with plugins/modelopt/skills/day0-release: the same gate contract, verdict style and use of the common skill's remote_exec.sh.
  • About 590 lines of source, over the ~500 guideline. Roughly 300 of them are SKILL.md, which has to describe the whole flow in one place for the agent.
  • Next: release criteria (SPEED-Bench on GB200, thresholds with a repeat policy) and approve_submissions: none once the flow has a clean track record.

@copy-pr-bot

copy-pr-bot Bot commented Oct 22, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@Zhehao-Hu
Zhehao-Hu force-pushed the zhehaoh/specdec-gates-bench branch from b311267 to 160f9ed Compare October 22, 2026 18:00
@Zhehao-Hu
Zhehao-Hu force-pushed the zhehaoh/specdec-release-skill branch 2 times, most recently from a9fce93 to a85b1b8 Compare October 22, 2026 18:00
@Zhehao-Hu
Zhehao-Hu force-pushed the zhehaoh/specdec-gates-bench branch from 160f9ed to 3fe37d2 Compare October 22, 2026 22:09
@Zhehao-Hu
Zhehao-Hu force-pushed the zhehaoh/specdec-release-skill branch from a85b1b8 to 2b31c86 Compare October 22, 2026 22:09
@Zhehao-Hu
Zhehao-Hu force-pushed the zhehaoh/specdec-gates-bench branch from 3fe37d2 to c87329b Compare October 23, 2026 00:09
@Zhehao-Hu
Zhehao-Hu force-pushed the zhehaoh/specdec-release-skill branch from 2b31c86 to c5e24c0 Compare October 23, 2026 00:09
@Zhehao-Hu
Zhehao-Hu force-pushed the zhehaoh/specdec-gates-bench branch from c87329b to 1990db8 Compare October 23, 2026 04:51
@Zhehao-Hu
Zhehao-Hu force-pushed the zhehaoh/specdec-release-skill branch from 43ed4e5 to 00bff0b Compare October 23, 2026 04:51
Zhehao-Hu added a commit that referenced this pull request Oct 23, 2026
…strict (#2839)

> **[1/8] Day-0 speculative-decoding readiness flow.** Merge in order 1
→ 8. Base: `main`.
> Series: [1/8] #2839 (this) · [2/8] #2930 · [3/8] #2931 · [4/8] #2932 ·
[5/8] #2933 · [6/8] #2934 · [7/8] #2935 · [8/8] #2936

### What does this PR do?

Type of change: Bug fix


[common/check_regression.py](https://github.com/NVIDIA/Model-Optimizer/blob/main/tools/launcher/common/check_regression.py)
gates DFlash-family training runs on final loss and accuracy. Four
things stopped that gate from catching a bad run:

1. **It read the wrong checkpoint on long runs.** `find_trainer_state()`
sorted `checkpoint-*` as strings, so `checkpoint-8000` ranked after
`checkpoint-12000` and an older checkpoint's metrics were checked. Any
run whose saved steps differ in digit count was affected; the ~92K-step
full DFlash2 recipe in `hf_online_dflash2.yaml` saves every 4000 steps.
It now sorts by step number.
2. **An unreadable run passed.** With thresholds set, a missing
`trainer_state.json`, or one with no training logs, only warned and
exited 0. An empty threshold variable (`MAX_FINAL_LOSS=""`) crashed on
`float('')`.
3. **A regression never failed the job.** `dflash_online_training.sh`
runs the check as `check_regression.py … || true`, so Slurm reports
`COMPLETED` even when the log says `REGRESSION`. The verdict also
existed only as log text.
4. **A crashed run was still graded.** The script's `ERR` trap only
records a failure, so after a failed `accelerate launch` it went on to
export and run the check, and a checkpoint that survived the crash could
pass.

Changes:

- `check_regression.py`:
  - Sorts checkpoints by step number.
- Fails when thresholds are set but there are no metrics to check,
including a `trainer_state.json` of the wrong shape. An empty threshold
variable counts as unset. A threshold that isn't a finite number (`7,0`,
`nan`) fails as `METRICS_MISSING`.
- A NaN or infinite value for a metric that has a threshold fails as
`TRAIN_REGRESSION`.
- Takes `--json <path>` and writes `{pass, failure_class, trainer_state,
metrics, thresholds, messages}` as strict JSON (non-finite metrics as
`null`). `failure_class` is `null`, `TRAIN_REGRESSION` or
`METRICS_MISSING`. The verdict is serialized before the file is opened,
so a failure can't leave a truncated file.
- Its existing log lines (`=== Regression Check (…) ===`, `PASS:` /
`REGRESSION:`, `Regression check PASSED`) are unchanged, so existing log
readers keep working. A failure now also prints `Regression check
FAILED`.
- `dflash_online_training.sh` writes the verdict to
`${OUTPUT_DIR}/regression_check.json`, next to the exports.
`REGRESSION_STRICT=1` drops the `|| true`, so a regression fails the
task through the existing `ERR` trap and `exit_handler`. **The default
(`0` or unset) is unchanged (warn only)**, so the other examples with
possibly uncalibrated thresholds don't start failing. Any other value
fails the task at startup, before installing or training.
- `dflash_online_training.sh` also skips export and the check when
training fails, still reporting the failure like any `ERR`-trap failure,
and removes an old `regression_check.json` first thing, so no early exit
can leave an earlier run's verdict behind.
- `examples/Qwen/Qwen3-8B/hf_online_dflash2.yaml`:
  - Sets `REGRESSION_STRICT: "1"`.
- Recalibrates `MAX_FINAL_LOSS` from 5.0 to 7.0. The 5.0 was inherited
from `hf_online_dflash.yaml`, whose own reference curve only drops below
5.0 at step ~2500 of a 12.5K-step run. DFlash2 adds a selector loss term
and this example stops at 2000 steps, so 5.0 fails every healthy run.
`MIN_FINAL_ACC` stays at 0.15.
- The `speculative-decoding` skill's `dflash.md` and `dflash2.md`
describe the new gate: judge a run by `regression_check.json`, the two
failure classes, strict mode, and DFlash2's 7.0 threshold.

### Usage

```bash
MAX_FINAL_LOSS=7.0 MIN_FINAL_ACC=0.15 \
  python3 common/check_regression.py /scratchspace/dflash2_bs16 --json /scratchspace/dflash2_bs16/regression_check.json
echo $?   # 0 pass, 1 regression or nothing to check
```

```yaml
environment:
  - MAX_FINAL_LOSS: "7.0"
  - REGRESSION_STRICT: "1"   # fail the training task on a regression
```

### Testing

- `tools/launcher/tests/test_check_regression.py`: 4 new test functions
(13 cases). Run against `main`'s `check_regression.py`, 17 of the file's
18 cases fail:
- `test_latest_checkpoint_is_chosen_by_step_number` guards the
string-sort bug.
  - `test_empty_threshold_var_is_unset` guards the `float('')` crash.
- `test_cli_exit_code_and_json_verdict` is parametrized over a missing
`trainer_state.json`, a truncated one, two of the wrong shape, a NaN
loss, an unchecked infinite metric, a regression and a pass. Each case
checks the exit code and parses the verdict as strict JSON, so it covers
the missing-metrics hole and the `--json` contract.
- `test_invalid_threshold_writes_failed_verdict` covers `7,0`, `nan` and
`inf`.
- `tools/launcher/tests/test_dflash_online_training.py` (5 cases) runs
the real script in a sandbox: stubbed `pip`, `nvidia-smi`, `accelerate`,
exporter and `torch`, a checkpoint, and an old passing verdict. Each
case asserts the exact exit code. Against `main`'s script, all 5 fail:
- A failing launch leaves no verdict and exits 1; a succeeding one
grades the checkpoint and exits 0 with no `ERR`-trap error.
- With a checkpoint over the threshold, `REGRESSION_STRICT=1` exits 1
and `0` exits 0, both writing `TRAIN_REGRESSION`.
- `REGRESSION_STRICT=true` exits 1 without launching training and leaves
no verdict.
- Full launcher suite on this commit: `uv run --with pytest python3 -m
pytest tests/`, 183 passed.
- `dflash_online_training.sh`: `bash -n` passes.
- **GPU (OCI-JHB, GB300, global batch 8):** five 2000-step runs of this
example with this change passed the strict gate. Final loss was
6.37–6.71 and accuracy 0.167–0.177, against 7.0 / 0.15. The calibration
run that motivated 7.0 (before this change) finished at 6.51 / 0.181.
The seed-to-seed spread puts the loss margin at about 4%; see the
variance note under Additional Information.
- Not run: the strict-failure path and a real training crash on GPU
(covered by the sandbox test above).

### Before your PR is "*Ready for review*"

Make sure you read and follow [Contributor
guidelines](https://github.com/NVIDIA/Model-Optimizer/blob/main/CONTRIBUTING.md)
and your commits are signed (`git commit -s -S`).

Make sure you read and follow the [Security Best
Practices](https://github.com/NVIDIA/Model-Optimizer/blob/main/SECURITY.md#security-coding-practices-for-contributors)
(e.g. avoiding hardcoded `trust_remote_code=True`, `torch.load(...,
weights_only=False)`, `pickle`, etc.).

- Is this change backward compatible?: ✅ (strict mode is opt-in; log
output unchanged)
- If you copied code from any other sources or added a new PIP
dependency, did you follow guidance in `CONTRIBUTING.md`: N/A
- Did you write any new necessary tests?: ✅
- Did you update
[Changelog](https://github.com/NVIDIA/Model-Optimizer/blob/main/CHANGELOG.rst)?:
N/A
- Did you get Claude approval on this PR?: ❌ - not yet run

### Additional Information

- Part of the Day-0 speculative-decoding workflow: the `specdec-release`
skill ([8/8]) gates training on `regression_check.json`, so it needs a
verdict it can read and a job state that reflects it.
- **Why not reuse `day0-release/scripts/gate_run.py`?** The two sit at
different layers and check different things, so their schemas stay
independent on purpose:
- `gate_run.py` is an agent-side gate for an *evaluation* run. It reads
a run summary the agent builds from NEL output, and checks per-task
status, sample counts and judge errors. None of that exists for a
training run.
- `check_regression.py` runs *inside* the Slurm training task, on rank
0, and its verdict decides the task's exit status in strict mode. It
reads `trainer_state.json` against thresholds from the launcher YAML,
and has to work for launcher users who never load a skill. A launcher
script importing a skill script would also invert the dependency: skills
drive the launcher, not the other way round.
- The skill-side counterpart of `gate_run.py` for training is
`specdec-release/scripts/gate_train.py` ([6/8] #2934). It reads
`regression_check.json` and returns the day-0 gate shape (`{pass,
failure_class, detail, …}`), with `failure_class` from the shared triage
vocabulary. That is the adapter layer, so `check_regression.py` doesn't
need to match `gate_run.py`.
- What the two share is the convention: a boolean `pass`, and a
`failure_class` that is `null` on a pass. The launcher emits only
`METRICS_MISSING` and `TRAIN_REGRESSION`, and `gate_train.py` treats any
other value as `TRAIN_REGRESSION`, so the launcher can add detail fields
without breaking the gate.
- Other Qwen3-8B examples share the inherited `MAX_FINAL_LOSS: "5.0"`
(`hf_online_domino.yaml`, `hf_online_lilicorr.yaml`). They are left
unchanged and non-strict because there is no calibration run for them.
- Variance: a seed-1 retrain landed at loss 6.705, 4.2% under the 7.0
floor. Accuracy has more headroom (11%). Recalibrating from more seeds
is planned for the release gates, not this PR.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **New Features**
* Training runs with an output directory now produce a regression-check
verdict file reporting whether configured quality thresholds passed.
* Regression checks account for missing or invalid metrics and select
checkpoints by numeric step.

* **Bug Fixes**
* Failed training no longer proceeds to checkpoint export or regression
checks.
* Strict mode fails the task when regression checks fail; invalid
strict-mode values stop the run before training starts.

* **Documentation**
* Updated DFlash2 guidance and example settings for the final-loss
threshold and strict-mode behavior.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Zhehao Hu <zhehaoh@nvidia.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@Zhehao-Hu
Zhehao-Hu force-pushed the zhehaoh/specdec-release-skill branch from 00bff0b to f5f9786 Compare October 23, 2026 21:24
@Zhehao-Hu
Zhehao-Hu force-pushed the zhehaoh/specdec-gates-bench branch 2 times, most recently from 1146ba9 to 6d934c1 Compare October 23, 2026 21:35
@Zhehao-Hu
Zhehao-Hu force-pushed the zhehaoh/specdec-release-skill branch from f5f9786 to 34d9fd4 Compare October 23, 2026 21:35
Zhehao-Hu added a commit that referenced this pull request Oct 23, 2026
…ction (#2930)

> **[2/8] Day-0 speculative-decoding readiness flow.** Merge in order 1
→ 8. Base: `main` ([1/8] has merged).
> Series: [1/8] #2839 (merged) · [2/8] #2930 (this) · [3/8] #2931 ·
[4/8] #2932 · [5/8] #2933 · [6/8] #2934 · [7/8] #2935 · [8/8] #2936

### What does this PR do?

Type of change: Bug fix

The vLLM smoke test (`common/specdec/vllm_smoke_test.sh`) is the serving
check after draft training. It had six problems:

1. **It could serve the wrong checkpoint.** `ar_eval_mtbench.sh` and
`vllm_smoke_test.sh` picked the newest checkpoint by sorting on a
dash-separated field of the **whole path**, so a dash in a parent
directory (e.g. `/data/my-drafts/...`) picked the wrong one.
`quantize_drafter.sh` had a third, different sort. The smoke test also
ignored `exported-checkpoint-final`.
2. **Every throughput figure printed `?`.** Timing used `bc`, which the
vLLM image doesn't have.
3. **A drafter that never speculated passed.** With
`MIN_ACCEPTANCE_LENGTH` set, a vLLM log with no acceptance length only
warned.
4. **The acceptance length covered only the last few seconds.** It came
from the log's "Mean acceptance length" line, which covers only vLLM's
last logging interval.
5. **The verdict existed only as log text.**
6. **The server wait was fixed at 180 s.** Qwen3.8-27B with a DFlash2
drafter takes longer to start on GB300 (torch.compile, kernel JIT, CUDA
graph capture, GDN warmups), so every smoke attempt stopped at
`server_timeout`. The script's exit then tore the container down
mid-warmup, and the engine's next exec failed with a ptxas ENOENT that
read like the cause.

Changes:

- `service_utils.sh`: new `latest_checkpoint <dir> <prefix>`. It returns
`<dir>/<prefix>final` if present, else the highest numeric step, keyed
on the directory's own name. It replaces the three ad hoc sorts in
`ar_eval_mtbench.sh`, `vllm_smoke_test.sh` and `quantize_drafter.sh`.
- `vllm_smoke_test.sh`:
- Prefers `exported-checkpoint-final`, as `quantize_drafter.sh` already
did.
  - Times prompts with shell arithmetic and `python3` instead of `bc`.
- Computes the mean acceptance length over the whole run from vLLM's
cumulative `/metrics` counters: 1 +
`spec_decode_num_accepted_tokens_total` /
`spec_decode_num_drafts_total`.
- With `MIN_ACCEPTANCE_LENGTH` set, fails when no speculative drafts
were recorded.
- Fails loudly if `service_utils.sh` is missing (it used to be optional,
so the failure surfaced as a misleading `draft_missing`). Only a missing
file is fatal: `source` returns the status of the file's last command, a
rank-0 append that fails outside a launcher working directory.
- `SERVER_TIMEOUT` sets the server wait in seconds (default 180) and
appears in the timeout reason.
- Checks `MIN_ACCEPTANCE_LENGTH` (a finite number), and
`NUM_SPEC_TOKENS` and `SERVER_TIMEOUT` (positive integers without
leading zeros, since `NUM_SPEC_TOKENS` goes into vLLM's JSON config),
before any work, and fails with `stage: invalid_config` and a reason
naming the value. Both checks run after the `EXIT` trap is set, so even
these failures replace any earlier `SMOKE_JSON`.
- Optional `SMOKE_JSON=<path>` writes the verdict on every exit:
`{stage, pass, reason, method, target, draft, num_speculative_tokens,
prompts passed/failed, tokens, average tok/s, mean_acceptance_length,
min_acceptance_length}`. `stage` says how far it got (`invalid_config`,
`starting`, `draft_missing`, `server_died`, `server_timeout`, `serving`,
`completed`), so a downstream gate can tell "not servable" from "below
the floor". The file's parent directory is created if needed, and it is
strict JSON (a non-numeric value is written as `null`). The mean
acceptance length is checked against the floor and written at full
precision; only the log rounds it.

### Usage

```yaml
task_2:
  script: common/specdec/vllm_smoke_test.sh
  environment:
    - HF_MODEL_CKPT: <<global_vars.hf_model>>
    - DRAFT_CKPT_DIR: <<global_vars.output_dir>>      # newest export, final preferred
    - SPEC_METHOD: "dflash"
    - NUM_SPEC_TOKENS: "7"
    - MIN_ACCEPTANCE_LENGTH: "1.5"
    - SMOKE_JSON: <<global_vars.output_dir>>/smoke_check.json
    - SERVER_TIMEOUT: "900"                            # optional; default 180 s
```

### Testing

- `tools/launcher/tests/test_service_utils.py`: `latest_checkpoint` with
a dash in a parent directory, `final` winning over steps, numeric rather
than string order, and no checkpoint at all (3 cases).
- `tools/launcher/tests/test_vllm_smoke_test.py` runs the script as rank
0 from a directory with no `modules/` (stubbed `pip` and `nvidia-smi`),
up to the paths that stop before vLLM starts (6 cases). Against the
script without these fixes, all 6 fail:
  - It reaches `draft_missing` rather than stopping at `source`.
- `MIN_ACCEPTANCE_LENGTH` of `1,5` or `nan`, `NUM_SPEC_TOKENS` of `x` or
`08` and `SERVER_TIMEOUT=15m` each fail as `invalid_config` with a
strict-JSON `SMOKE_JSON`. Before, `NUM_SPEC_TOKENS=x` crashed in the
`EXIT` trap and wrote no JSON.
- `bash -n` on all four scripts. Launcher unit suite on this commit: 192
passed.
- **GPU (OCI-JHB, GB300, vLLM v0.31.0)**, through
`hf_online_dflash2.yaml`'s smoke task ([4/8]) and one smoke-only
submission, six runs in all:
- `latest_checkpoint` picked `exported-checkpoint-final` in every
training run.
- Per-prompt tok/s printed (e.g. 335 tok/s average), 8/8 prompts passed.
- Mean acceptance length 1.68–1.79 against the 1.5 floor;
`smoke_check.json` written each time.
- **GPU, Qwen3.8-27B + DFlash2 (same cluster and image):** four smoke
runs at the old fixed 180 s each wrote `smoke_check.json` with `stage:
server_timeout`. A fifth with `SERVER_TIMEOUT=900` passed at AL 1.76.
- Not run on GPU:
- the `server_died` and no-drafts failure paths. They exit through the
same `write_smoke_json` trap as the passing path;
- creating `SMOKE_JSON`'s parent directory. A smoke-only run hit the
missing directory before this was added; the rerun used an existing
directory.

### Before your PR is "*Ready for review*"

Make sure you read and follow [Contributor
guidelines](https://github.com/NVIDIA/Model-Optimizer/blob/main/CONTRIBUTING.md)
and your commits are signed (`git commit -s -S`).

Make sure you read and follow the [Security Best
Practices](https://github.com/NVIDIA/Model-Optimizer/blob/main/SECURITY.md#security-coding-practices-for-contributors)
(e.g. avoiding hardcoded `trust_remote_code=True`, `torch.load(...,
weights_only=False)`, `pickle`, etc.).

- Is this change backward compatible?: ✅ (`SMOKE_JSON` is optional. With
`MIN_ACCEPTANCE_LENGTH` set, a run with no speculative drafts now fails
instead of passing; that was the bug.)
- If you copied code from any other sources or added a new PIP
dependency, did you follow guidance in `CONTRIBUTING.md`: N/A
- Did you write any new necessary tests?: ✅ (`latest_checkpoint`; the
smoke script itself is validated on GPU as above)
- Did you update
[Changelog](https://github.com/NVIDIA/Model-Optimizer/blob/main/CHANGELOG.rst)?:
N/A
- Did you get Claude approval on this PR?: ❌ - not yet run

### Additional Information

- The `specdec-release` skill's `gate_smoke.py` ([6/8]) reads
`SMOKE_JSON`.



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **Improvements**
* Checkpoint selection now prefers a `final` checkpoint, or otherwise
chooses the highest-numbered checkpoint.
* Smoke tests validate numeric settings, use a configurable server
timeout, and report run results, failure reasons, and performance
metrics in an optional JSON file.
* Acceptance-length checks now use cumulative draft and accepted-token
counts; prompt failures and unmet acceptance thresholds result in a
failed test.
* **Tests**
* Added coverage for checkpoint selection and smoke-test configuration
failures.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Zhehao Hu <zhehaoh@nvidia.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@Zhehao-Hu
Zhehao-Hu force-pushed the zhehaoh/specdec-gates-bench branch from 6d934c1 to 828fea9 Compare October 28, 2026 00:21
@Zhehao-Hu
Zhehao-Hu force-pushed the zhehaoh/specdec-release-skill branch from 34d9fd4 to 643578c Compare October 28, 2026 00:21
@Zhehao-Hu
Zhehao-Hu force-pushed the zhehaoh/specdec-gates-bench branch from 828fea9 to 6ed607e Compare October 29, 2026 00:35
@Zhehao-Hu
Zhehao-Hu force-pushed the zhehaoh/specdec-release-skill branch from 643578c to 351897c Compare October 29, 2026 00:35
@Zhehao-Hu
Zhehao-Hu force-pushed the zhehaoh/specdec-gates-bench branch from 6ed607e to 723bdd1 Compare October 29, 2026 03:44
@Zhehao-Hu
Zhehao-Hu force-pushed the zhehaoh/specdec-release-skill branch from 351897c to b941f72 Compare October 29, 2026 03:44
@Zhehao-Hu
Zhehao-Hu force-pushed the zhehaoh/specdec-gates-bench branch from 723bdd1 to d3c9f5b Compare October 29, 2026 04:53
@Zhehao-Hu
Zhehao-Hu force-pushed the zhehaoh/specdec-release-skill branch from b941f72 to b281093 Compare October 29, 2026 04:53
@Zhehao-Hu
Zhehao-Hu force-pushed the zhehaoh/specdec-gates-bench branch from d3c9f5b to dbce061 Compare October 29, 2026 04:59
@Zhehao-Hu
Zhehao-Hu force-pushed the zhehaoh/specdec-release-skill branch from b281093 to 264fb27 Compare October 29, 2026 04:59
Zhehao-Hu and others added 3 commits October 29, 2026 21:42
Turn the specdec-release skill into a conductor that takes a target and an
algorithm from no drafter to a readiness verdict (DRAFTER_READY, INFEASIBLE
or ANOMALOUS) in a fixed sequence: setup, train pipeline (data, train +
export, smoke) gated by gate_train and gate_smoke, freeze, then a matched
A|B bench gated by gate_bench (gate_correctness reports only). It submits
the existing launcher pipelines and never advances past a failed gate.
Scope: DFlash and DFlash2, whose training script writes the regression
verdict; other algorithms are refused before anything is submitted.

- SKILL.md: inputs (existing_drafter, approve_submissions each|none), the
  stage sequence, how to run and read each gate, cluster fit, watching jobs
  by result files, specdec_state.json and resume rules, work_log.md with
  ACTION / HUMAN_INTERVENTION lines, triage, and SPECDEC_REPORT.md.
- freeze_drafter.py: copy the export through a .partial sibling, re-hash
  before the rename, write SHA256SUMS, make it read-only, return a
  manifest_sha256; --verify re-checks it on resume.
- gate_train.py --export-only: check an existing drafter's export without a
  training verdict.
- tests: freeze_drafter and --export-only cases; evals.json trigger and
  non-trigger prompts.
- speculative-decoding: route the end-to-end flow to specdec-release, and
  add fitting every task to the cluster (whole-node GPU counts, keeping the
  global batch via gradient_accumulation_steps) to the configure stage.

Signed-off-by: Zhehao Hu <zhehaoh@nvidia.com>
gate_smoke ([6/8]) now returns USER_CONFIG_ERROR when vllm_smoke_test.sh
stops at stage invalid_config. The triage row for that class said to re-run
the gate only, which cannot help: the bad MIN_ACCEPTANCE_LENGTH or
NUM_SPEC_TOKENS is in the YAML. The row now says to fix it there and
resubmit as a smoke-only submission, since training already passed its
gate. The exit-code note says the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Zhehao Hu <zhehaoh@nvidia.com>
…ge weights

From the first multi-model run (Qwen3.8-27B + DFlash2):

- Triage: CHECKPOINT_NOT_SERVABLE acts on the smoke JSON's stage, not the
  log's last traceback. The run's server_timeout ended in a ptxas ENOENT
  from the container teardown, and chasing it cost three smoke attempts and
  two probe jobs. server_timeout now resubmits with a larger SERVER_TIMEOUT.
- approve_submissions: each covers every cluster job (launcher, sbatch,
  weight staging) and sub-agents. One OK covers one exact job; a retry or a
  changed partition, image or command needs a new one. A delegated download
  had made three submissions on one approval.
- Step 1 stages a target the job host lacks: reuse a readable hf_local copy,
  else one download job into hf_local at a pinned revision, then verify the
  shards. It lives here until it moves to the common skill for ptq and
  day0-release.
- The closeout reports the greedy match next to its target-alone control.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Zhehao Hu <zhehaoh@nvidia.com>
@Zhehao-Hu
Zhehao-Hu force-pushed the zhehaoh/specdec-release-skill branch from 264fb27 to ae419c1 Compare October 29, 2026 21:43
@Zhehao-Hu
Zhehao-Hu force-pushed the zhehaoh/specdec-gates-bench branch from dbce061 to d3c8a87 Compare October 29, 2026 21:43

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant