Skip to content

Add speed-bench benchmark and resource server - #1232

Merged
bxyu-nvidia merged 12 commits into
NVIDIA-NeMo:mainfrom
gwarmstrong:georgea/migrate-gym-specdec
May 8, 2026
Merged

bxyu-nvidia merged 12 commits into
NVIDIA-NeMo:mainfrom
gwarmstrong:georgea/migrate-gym-specdec

Conversation

@gwarmstrong

Copy link
Copy Markdown
Contributor

Add speed-bench benchmark and resource server

Migrates the SPEED-Bench speculative-decoding throughput benchmark from NeMo Skills into NeMo Gym. Verification reads the model server's Prometheus /metrics counters before and after generation to derive acceptance length (AL) and acceptance rate (AR) — there is no notion of answer correctness; the metrics are server-cumulative properties of the run window.

Includes

  • resources_servers/speed_bench/ — new resource server that scrapes vLLM vllm:spec_decode_* (or SGLang sglang:spec_accept_*) Prometheus counters across the benchmark window and returns running aggregates per task.
  • responses_api_agents/speed_bench_agent/ — new fixed-replay multi-turn agent.
    • simple_agent sends a list of messages in one model call; SPEED-Bench's qualitative config has 185/880 multi-turn rows where Skills' SpecdecGenerationTask plays each user turn separately so the model sees its own intermediate replies. The new agent reproduces that turn-by-turn replay.
    • Single-turn rows collapse to a single model call (same shape as simple_agent).
  • benchmarks/speed-bench/ — qualitative + throughput_2k configs.
    • Each Gym config maps to its own per-config agent + resource server (so ng_prepare_benchmark's "exactly one dataset per agent" assertion is satisfied).
    • prepare.py and prepare_throughput_2k.py reuse Skills' _resolve_external_data helper (loaded by file path) so the 14-source interpolation logic isn't duplicated.
    • Each row's responses_create_params.input is a list of {"role": "user", "content": "<turn>"} messages with no interspersed assistants — speed_bench_agent plays them sequentially at rollout time.
  • responses_api_models/local_vllm_model/configs/Qwen/Qwen3-30B-A3B-Instruct-2507-ngram-specdec.yaml — ready-to-use local_vllm_model config that bakes ngram speculative decoding into vllm_serve_kwargs.speculative_config. Drop it into +config_paths=[…] alongside the speed_bench config and you have a working spec-decode-enabled vLLM out of the box.
  • nemo_gym/config_types.py — adds "NVIDIA Evaluation Dataset License Agreement" to the dataset license whitelist (SPEED-Bench is hosted under exactly that license; without the literal, ng_prepare_data fails Pydantic validation).

Validated against NeMo Skills

Two side-by-side runs on a single 8 × H100 node, Qwen3-30B-A3B target, vLLM ngram speculative decoding (num_speculative_tokens=3, prompt_lookup_max=5, prompt_lookup_min=2), temperature=1.0, top_p=0.95, max_output_tokens=1024, single seed each side. (Single seed because spec-decode metrics are server-cumulative on a single benchmark window — splitting into multiple seeds would change the semantic, not just produce variance estimates.)

=================================================================
qualitative · 880 prompts · 185 multi-turn · ~1125 output tokens
=================================================================
Metric                         Skills        Gym        Delta
-----------------------------------------------------------------
spec_acceptance_length         1.7661       1.7642     -0.11%
spec_acceptance_rate          25.54%       25.48%     -0.0656pp
avg_tokens                     1125         1125.30    +0.03%
num_entries                    880          880        0

=================================================================
throughput_2k · 1536 prompts · ~975 output tokens
=================================================================
Metric                         Skills        Gym        Delta
-----------------------------------------------------------------
spec_acceptance_length         1.6800       1.6806     +0.04%
spec_acceptance_rate          22.67%       22.69%     +0.0214pp
avg_tokens                     972          974.94     +0.30%
num_entries                    1536         1536       0

Both spec-decode quality metrics are within sampling noise on both configs; data-prep parity (row counts, src_id sequences, multi-turn row counts) is byte-equivalent. The natural drop from qualitative→throughput_2k (AL 1.77→1.68) reflects ngram lookup having a lower hit rate on long padded prompts; both pipelines see the drop identically.

Deferred

  • The throughput_{1k,8k,16k,32k} configs: prepare.py knows how to prepare them (--config throughput_8k etc.) but only qualitative and throughput_2k were validated end-to-end.
  • Skills' SGLang per-request metrics-file fallback (--export-metrics-to-file) is not ported — the Prometheus delta gives matching headline numbers and is sufficient for parity.
  • Per-position acceptance rates are emitted per-row but excluded from get_key_metrics() headline.

Notes for reviewers

  • The model server must be launched with speculative decoding enabled (e.g. --speculative-config '{"method": "ngram", ...}' or the vllm_serve_kwargs.speculative_config: block in the demo local_vllm_model config). Without it, every rollout records spec_decode_unavailable: true and AL/AR are null. Both READMEs document this prominently.
  • The SGLang scrape path is wired but not validated against an actual SGLang server in this PR — set server_type_for_metrics: sglang in the resources server config and ensure the model server is SGLang. Unit tests cover the parser, weighted-delta arithmetic, and the verify-time code path; missing piece is end-to-end SGLang validation.

gwarmstrong added 10 commits May 1, 2026 12:13
Adds the SPEED-Bench speculative-decoding benchmark to NeMo Gym, mirroring
nemo_skills' speed-bench:

- resources_servers/speed_bench/ scrapes vLLM `vllm:spec_decode_*` Prometheus
  counters before/after generation. verify() returns the running aggregate
  (acceptance length / rate / per-position rates) per task; compute_metrics
  surfaces the headline values from the latest-completing task. SGLang scrape
  is stubbed (NotImplementedError).
- responses_api_agents/speed_bench_agent/ replays multi-turn rows one user
  turn at a time, matching Skills' SpecdecGenerationTask. Single-turn rows
  collapse to a single model call (same shape as simple_agent).
- benchmarks/speed-bench/ chains the resources server and agent. prepare.py
  imports Skills' _resolve_external_data via file path so the 14-source
  interpolation logic doesn't have to be reproduced. Default configs are
  qualitative + throughput_2k; --config all is supported.
- Unit tests cover the Prometheus parser, running-delta math, metrics URL
  resolution, compute_metrics aggregation, and the verify() async path with
  a stubbed scrape.

Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>
ng_prepare_benchmark requires `prompt_config` for type: benchmark datasets.
speed-bench bakes the multi-turn user-message list into responses_create_params.input
in prepare.py, so no prompt template is needed — set prompt_config: null
explicitly (matching the mrcr pattern) so the schema validates.

Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>
speed_bench_agent_default in responses_api_agents/speed_bench_agent/configs/
speed_bench_agent.yaml referenced `speed_bench_resources_server`, but the
benchmark config.yaml's `_inherit_from: speed_bench_resources_server`
re-registers it as `speed_bench_speed_bench_resources_server` and the
original name disappears from the registry. The standalone agent config is
unused by the benchmark flow — `speed_bench_simple_agent` in the resources
server's own config already wires up the agent's entrypoint.

Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>
ng_prepare_benchmark requires exactly one dataset per agent. Define a
separate `speed_bench_<config>_simple_agent` per benchmark config rather
than stacking both datasets under one agent.

Updates the Gym run script and benchmark README to point at the new
qualitative agent name.

Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>
ng_prepare_benchmark calls prepare() with no args and asserts the returned
path matches benchmark_config.dataset.jsonl_fpath. With multiple configs,
each agent needs its own prepare module that returns its own path.

Refactors:
- prepare.py: now prepares the qualitative split (returns one Path).
- prepare_throughput_2k.py: prepares the throughput_2k split.
- _prepare_common.py: shared resolver+row-conversion helpers.

config.yaml's throughput_2k agent now points at prepare_throughput_2k.py.

Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>
importlib.import_module('benchmarks.speed-bench.prepare') doesn't add the
benchmark dir to sys.path (and the dotted name has a hyphen anyway), so
'from _prepare_common import ...' fails with ModuleNotFoundError.
Prepend the script directory to sys.path inside each prepare module so
the sibling helper resolves.

Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>
Mirrors Skills' fetch_sglang_spec_decode_metrics +
compute_sglang_spec_decode_delta:

- _parse_sglang_metrics(): extracts sglang:spec_accept_{length,rate} gauges
  and sglang:{num_requests_total, generation_tokens_total} counters.
- _weighted_delta_average(): generic helper to recover a benchmark-window
  average from two running-average gauges.
- _compute_sglang_running_delta(): emits acceptance_length, acceptance_rate,
  num_drafts (~delta_requests), draft_tokens (~delta_gen_tokens),
  accepted_tokens (delta_gen_tokens * ar_fraction). Per-position rates are
  not exposed by SGLang — empty list, matching Skills.
- verify() and _scrape_metrics() pick the SGLang path when
  server_type_for_metrics == 'sglang'.

7 new unit tests cover the parser, the weighted-delta arithmetic edge
cases (first window, no traffic), the full sglang delta computation, and
an end-to-end verify() exercise of the SGLang code path. 26 tests passing
total (was 19).

Skills also supports an SGLang per-request metrics-file path
(--export-metrics-to-file) — that's left as future work; Prometheus delta
is sufficient for parity comparisons.

Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>
Mirrors Skills' BaseMetrics._add_std_metrics for SpecdecMetrics' five
score keys (acceptance_length, acceptance_rate, num_drafts, draft_tokens,
accepted_tokens). When tasks have >1 rollouts each (e.g. +num_repeats=N),
compute_metrics() now emits four variance estimators per key:

- spec_<key>_avg              — mean across all (task, rollout) pairs
- spec_<key>_std_dev_across_runs — std-dev of per-run averages
- spec_<key>_std_err_across_runs — std_dev / sqrt(num_repeats)
- spec_<key>_avg_sample_std_dev  — mean of per-task std-devs

Single-seed runs (the default speed-bench shape on each side) skip this
block, matching Skills' max_k=1 behaviour.

get_key_metrics() additionally surfaces the headline std_err_across_runs
for acceptance_length and acceptance_rate so a reader can see at a glance
whether a Skills↔Gym Δ is within sampling noise.

Implementation uses stdlib statistics + math (no numpy dep). Edge cases
covered by tests:
- single-rollout / empty tasks → empty dict
- all-identical values → zero std-dev (avoids stdev's NaN at n=1)
- tasks with None values for a key → dropped per-key
- uneven k across tasks → uses min(k) (Skills enforces equality; we relax
  for robustness against partial outputs)

8 new unit tests, 34 passing total (was 26).

Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>
- nemo_gym/config_types.py: add 'NVIDIA Evaluation Dataset License Agreement'
  to the dataset license whitelist. SPEED-Bench is a publicly hosted
  NVIDIA dataset under exactly that license; without the literal,
  ng_prepare_data fails Pydantic validation.
- resources_servers/speed_bench/data/example_metrics.json: generated by
  ng_prepare_data ... +mode=example_validation (CI artifact).
- resources_servers/speed_bench/data/example.jsonl: regenerated via
  example_validation; agent_ref field now stamped per row (canonical
  shape for type=example datasets).
- responses_api_models/local_vllm_model/configs/Qwen/Qwen3-30B-A3B-Instruct-2507-ngram-specdec.yaml:
  ready-to-use local_vllm_model config that bakes ngram speculative
  decoding into vllm_serve_kwargs.speculative_config — cheapest path to
  a working spec-decode-enabled vLLM for speed-bench.
- Both READMEs reference the demo config and document that the model
  server MUST be launched with --speculative-config (or the equivalent
  vllm_serve_kwargs block) — without it the resources server records
  spec_decode_unavailable: true on every row.

Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>
…etadata

- Adds resources_servers/speed_bench/data/example_rollouts.jsonl: 5 real
  rollouts produced by ng_collect_rollouts on a vLLM-with-ngram-spec-dec
  cluster job against example.jsonl. Required CI artifact per the
  add-benchmark / migrate-benchmark skills.
- Fixes a Pydantic-schema bug: SpeedBenchVerifyRequest now declares
  verifier_metadata: Optional[Dict[str, Any]] explicitly so the per-row
  src_id / source / speed_config / num_turns / sub_category fields the
  prepare.py emits round-trip through verify() into rollouts.jsonl.
  Without this, the debug_compare_specdec.py src_id-based matching
  loses every row.

The committed example_rollouts.jsonl was produced before the
verifier_metadata fix landed (its rows have null verifier_metadata),
which is fine for CI validation — future runs will populate it.

Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>
@copy-pr-bot

copy-pr-bot Bot commented May 5, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

Gym's BenchmarkConfig.from_initial_config_dict asserts exactly one
type:benchmark dataset per config.yaml file (nemo_gym/benchmarks.py:83).
Our prior config.yaml carried two — qualitative + throughput_2k — which
caused tests/unit_tests/test_benchmarks.py::TestListBenchmarks::test_lists_found_benchmarks
to fail in CI with:
  AssertionError: Expected 1 benchmark dataset for config
  /home/runner/work/Gym/Gym/benchmarks/speed-bench/config.yaml,
  but found 2!

Split into per-config files following the ruler pattern:
- benchmarks/speed-bench/config_qualitative.yaml
- benchmarks/speed-bench/config_throughput_2k.yaml

Each carries exactly one benchmark dataset entry. README + recipe scripts
updated to reference the per-config paths explicitly.

list_benchmarks() globs **/config.yaml literally, so neither file is
auto-discovered — same as ruler's config_nemotron_3_256k.yaml. Users opt
into a config explicitly via +config_paths=.

Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>
@cwing-nvidia cwing-nvidia added the env-integration Integrate with existing environments or gyms external to NeMo Gym label May 7, 2026
…pecdec-fixmerge

Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>

# Conflicts:
#	README.md
@bxyu-nvidia
bxyu-nvidia merged commit 664a353 into NVIDIA-NeMo:main May 8, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

env-integration Integrate with existing environments or gyms external to NeMo Gym

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants