Skip to content

[5/8] specdec_bench: add a matched A|B benchmark runner and comparator - #2933

Draft
Zhehao-Hu wants to merge 2 commits into
zhehaoh/launcher-dflash2-examplesfrom
zhehaoh/specdec-ab-compare
Draft

Zhehao-Hu wants to merge 2 commits into
zhehaoh/launcher-dflash2-examplesfrom
zhehaoh/specdec-ab-compare

Conversation

@Zhehao-Hu

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

Copy link
Copy Markdown
Contributor

[5/8] Day-0 speculative-decoding readiness flow. Merge in order 1 → 8. Base: [4/8] #2932. New files only, so it can be reviewed in parallel with [1/8]–[4/8].
Series: [1/8] #2839 · [2/8] #2930 · [3/8] #2931 · [4/8] #2932 · [5/8] #2933 (this) · [6/8] #2934 · [7/8] #2935 · [8/8] #2936

What does this PR do?

Type of change: New feature

A drafter's speedup and correctness need the target alone (arm A) and the target plus drafter (arm B) measured on the same node, image, prompts and sampling. Run as separate jobs, node, image and load differences leak into the speedup. An earlier M0 run did exactly that: its two arms ran in separate jobs, arm A happened to be slow, and the C=32 speedup came out as 1.39. Matched in one job, it measures 1.27.

This PR adds a matched A|B runner and its comparator.

  • examples/specdec_bench/compare_ab.py summarizes run.py outputs laid out as [r<repeat>/]{A,B}_c<N>/ into ab_summary.json. Per row (repeat × concurrency) it records:

    • Output TPS of both arms, speedup, acceptance length (AL), step time and TTFT;
    • the temperature, and result-entry and turn counts;
    • each arm's algorithm, drafter, speculative-token count, request count and engine version, from configuration.json.

    A-vs-B greedy exact match (all turns and first turns, plus unpaired turns) is reported only at concurrency 1 and temperature 0. The same row's a_self_match compares arm A with arm A at the next concurrency: how far the target alone drifts with batch shape, the noise floor for the A-vs-B rate. The temperature follows run.py's precedence: --temperature, then runtime_params sampling_kwargs, else the engine default. A row lists problems (missing arm, other than one result entry, no responses, turn-count mismatch), and any problem makes the CLI exit 1.

  • tools/launcher/common/specdec_bench/ab_compare.sh runs both arms per repeat and concurrency in one job:

    • A first on odd repeats and B first on even ones, so run order doesn't favor an arm;
    • refuses a non-empty SAVE_ROOT (run.py appends to existing results), and a local DRAFT_MODEL path that doesn't exist (HF repo IDs pass through);
    • records the host and GPUs;
    • fails on a requirements install error, at the first failed arm, and when the comparator finds a problem.
  • examples/Qwen/Qwen3-8B/specdec_ab_dflash2.yaml: MT-Bench A|B sweep at concurrency 1/8/32 on vLLM v0.31.0, with draft_model passed per run.

Usage

uv run launch.py --yaml examples/Qwen/Qwen3-8B/specdec_ab_dflash2.yaml \
    pipeline.global_vars.draft_model=/cicd/drafts/Qwen3-8B/dflash2/<run_id> \
    pipeline.global_vars.output_dir=/scratchspace/ab --yes
# REPEATS / CONCURRENCIES / NUM_SPEC_TOKENS are task environment entries:
#   'pipeline.task_0.environment+=[{"REPEATS":"2"}]'

python examples/specdec_bench/compare_ab.py <save_root> --json <save_root>/ab_summary.json

Testing

  • tests/examples/specdec_bench/test_compare_ab.py: speedup, match and a_self_match reported only at concurrency 1 and temperature 0; no match under sampling; the CLI exit code reflects each problem type. 11 passed (pytest tests/examples/specdec_bench/test_compare_ab.py, CPU only). Launcher suite on this commit: 192 passed.
  • GPU (OCI-JHB, GB300, vLLM v0.31.0, TP 1, MT-Bench 80 × 2 turns, temperature 0, 7 speculative tokens, C = 1/8/32), 7 jobs on at least 4 nodes:
    • Every job wrote ab_summary.json with no problem rows and ended COMPLETED. The four jobs whose exit we timed ended 7–13 s after writing the summary.
    • Measurement noise (one drafter, 2 nodes × 2 repeats): AL CV ≤ 0.4%. Speedup CV is 0.2% at C=1 and 2–3% at C=8 and C=32. Means are 1.42 at C=1, 1.39 at C=8 and 1.27 at C=32.
    • Greedy exact match is 0.956–0.963 over all turns and 1.0 on first turns, in every run.
  • Why a_self_match: on Qwen3.8-27B (hybrid GDN + full attention) A-vs-B matched on only 21% of first turns, which looked like a speculative-decoding defect. Computed offline from that job's responses, the target alone matched itself across C=1/8/32 on 20–22% of first turns: the target's batch-variant numerics, not the drafter.
  • Not run: the TRT-LLM and SGLang engines through this runner.

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 files only)
  • 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?: ✅ (Speculative Decoding entry)
  • Did you get Claude approval on this PR?: ❌ - not yet run

Additional Information

  • The specdec-release skill's gate_bench.py ([7/8]) decides whether an ab_summary.json is a valid matched measurement. This PR only measures and flags structural problems.
  • At C=8 and C=32 a single run can differ from the mean by up to 6%. Use ≥ 2 repeats on ≥ 2 nodes before reading a throughput difference of under ~5%.

@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/launcher-dflash2-examples branch from e07f406 to 2370f99 Compare October 22, 2026 18:00
@Zhehao-Hu
Zhehao-Hu force-pushed the zhehaoh/specdec-ab-compare branch from 736ca54 to 5b37803 Compare October 22, 2026 18:00
@Zhehao-Hu
Zhehao-Hu force-pushed the zhehaoh/launcher-dflash2-examples branch from 2370f99 to efc7bce Compare October 22, 2026 22:09
@Zhehao-Hu
Zhehao-Hu force-pushed the zhehaoh/specdec-ab-compare branch 2 times, most recently from c9e2d8e to be214b9 Compare October 23, 2026 00:09
@Zhehao-Hu
Zhehao-Hu force-pushed the zhehaoh/launcher-dflash2-examples branch 2 times, most recently from 82d5b56 to 8a2bd57 Compare October 23, 2026 04:51
@Zhehao-Hu
Zhehao-Hu force-pushed the zhehaoh/specdec-ab-compare branch from be214b9 to 3561e93 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-ab-compare branch from 3561e93 to a2644a9 Compare October 23, 2026 21:24
@Zhehao-Hu
Zhehao-Hu force-pushed the zhehaoh/launcher-dflash2-examples branch from 8a2bd57 to 5b20c8d Compare October 23, 2026 21:24
@Zhehao-Hu
Zhehao-Hu force-pushed the zhehaoh/specdec-ab-compare branch from a2644a9 to c2d7804 Compare October 23, 2026 21:35
@Zhehao-Hu
Zhehao-Hu force-pushed the zhehaoh/launcher-dflash2-examples branch from 5b20c8d to 406058d 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 and others added 2 commits October 28, 2026 00:09
Speedup and correctness need the target alone (A) and target + draft (B)
measured on the same node, image, prompts and sampling. Running the arms as
separate jobs lets node, image and load differences leak into the speedup.

- examples/specdec_bench/compare_ab.py: summarizes run.py outputs laid out as
  [r<repeat>/]{A,B}_c<N>/ into ab_summary.json: Output TPS of both arms,
  speedup, AL, step time, TTFT, temperature, result-entry and turn counts,
  and each arm's algorithm, drafter, speculative-token count, request count
  and engine version from configuration.json. A-vs-B exact match (all turns
  and first turns, plus unpaired turns) is reported only at concurrency 1 and
  temperature 0; the temperature follows run.py's precedence (--temperature,
  then runtime_params sampling_kwargs, else the engine default). A row lists
  problems (missing arm, other than one result entry, no responses,
  turn-count mismatch) and any problem makes the CLI exit 1.
- tools/launcher/common/specdec_bench/ab_compare.sh: runs both arms per
  repeat and concurrency in one job, A first on odd repeats and B first on
  even ones so run order does not favor an arm; refuses a non-empty SAVE_ROOT
  (run.py appends to existing results) and a local DRAFT_MODEL path that does
  not exist (HF repo IDs pass through); records host and GPUs; fails on a
  requirements install error, at the first failed arm, and when the
  comparator finds a problem.
- examples/Qwen/Qwen3-8B/specdec_ab_dflash2.yaml: MT-Bench A|B sweep at
  concurrency 1/8/32 on vLLM v0.31.0; draft_model is passed per run.

Signed-off-by: Zhehao Hu <zhehaoh@nvidia.com>
At concurrency 1 and temperature 0 the A|B summary reports how often A and B
outputs match exactly. On Qwen3.8-27B (hybrid GDN + full attention) that was
21% of first turns, which looked like a speculative-decoding defect. The
target alone, with no drafter, matched itself across concurrencies on 20-22%
of first turns, the same rate: batch-variant numerics in the target, not the
drafter.

The concurrency-1 row now also carries a_self_match: arm A against arm A at
the next concurrency of the same repeat, when both are greedy. It is the
noise floor to read the A-vs-B rate against.

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/launcher-dflash2-examples branch from 406058d to c81e5eb Compare October 28, 2026 00:21
@Zhehao-Hu
Zhehao-Hu force-pushed the zhehaoh/specdec-ab-compare branch from c2d7804 to 6b56334 Compare October 28, 2026 00:21

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