[NV] [PR2] feat(synthetic-al): vLLM backend for synthetic-acceptance injection#2090
[NV] [PR2] feat(synthetic-al): vLLM backend for synthetic-acceptance injection#2090qiching wants to merge 4 commits into
Conversation
Two changes needed to run dsv4 fp4 multinode sweeps on the watchtower (Oracle) gb200 cluster: - Point the dynamo-vllm dsv4 MODEL_PATH at the base DeepSeek-V4-Pro checkpoint instead of the -NVFP4 re-quant. The pinned v0.20.1 container's deepseek_v4 loader doesn't define the NVFP4 export's extra quant params (e.g. ffn.experts.w13_input_scale) and KeyErrors at load; the base checkpoint loads and matches the recipe's served identity. - Add dsv4/dynamo-vllm to uses_watchtower_shared_fs() so the srt-slurm workspace/outputs land on a compute-visible shared FS (the runner home is not cross-mounted to compute nodes, otherwise the batch step dies).
Framework-agnostic plumbing for optionally overriding a recipe's speculative-decoding acceptance length with a synthetic value, gated on SYNTHETIC_ACCEPTANCE=true (no-op otherwise, so this commit changes no existing behavior). - runners/synthetic_injectors/: backend registry (register/get_injector). No backends registered yet -> the generic layer alone injects nothing. - runners/inject_synthetic_acceptance.py: generic driver. Handles flag gating, acceptance-length resolution (explicit env or reference-yaml lookup) and file I/O, then delegates the recipe rewrite to the framework backend resolved from FRAMEWORK. Errors clearly if the flag is on but no backend is registered. - runners/launch_gb200-nv.sh: invoke the driver after the name override and before srtctl apply. With no backend registered this is a no-op.
Register a dynamo-vllm backend with the generic driver. When SYNTHETIC_ACCEPTANCE=true and FRAMEWORK=dynamo-vllm, every speculative-config JSON entry in the recipe is rewritten to add rejection_sample_method=synthetic and synthetic_acceptance_length=<al>, so the vLLM engine emits a controlled mean acceptance length instead of running the real draft model. Rewrite uses the json module on the captured JSON (no string-munging), preserving the recipe's compact style. - runners/synthetic_injectors/vllm.py: rewrite() + spec_tokens_from_recipe(), self-registers under "dynamo-vllm". - runners/synthetic_injectors/__init__.py: import the backend so it registers on package import. Still inert until a config opts in via SYNTHETIC_ACCEPTANCE (added for dsv4 mtp2 in a follow-up).
|
Claude finished @qiching's task in 2m 55s —— View job Review
Found 1 blocking issue and 1 warning, both about the same failure mode: a run that opts in with
One heads-up (not blocking, assuming it lands in the stacked follow-up): |
| # Optionally inject synthetic acceptance into the recipe's speculative-config | ||
| # when SYNTHETIC_ACCEPTANCE=true (no-op otherwise). Must run after the name | ||
| # override and before srtctl apply so the rendered job picks it up. | ||
| python3 "$GITHUB_WORKSPACE/runners/inject_synthetic_acceptance.py" "$CONFIG_PATH" "$FRAMEWORK" |
There was a problem hiding this comment.
🔴 BLOCKING: Injector failure is silently ignored — the script's exit code is never checked.
Why it matters: This script has no set -e (only set -x), so every sys.exit("ERROR: ...") path in inject_synthetic_acceptance.py (missing reference YAML, unknown MODEL_PREFIX/num_spec_tokens, no injector registered for the framework) prints an error and then the launcher continues and submits the unmodified recipe. A sweep that explicitly opted in with SYNTHETIC_ACCEPTANCE=true would run real speculative decoding and record those numbers as synthetic-acceptance results — a silent data-corruption failure mode. All the fail-loud logic in the Python is defeated by this one line. Every neighboring critical command in this file uses || exit 1 (e.g. lines 458, 469, 476).
Fix:
| python3 "$GITHUB_WORKSPACE/runners/inject_synthetic_acceptance.py" "$CONFIG_PATH" "$FRAMEWORK" | |
| python3 "$GITHUB_WORKSPACE/runners/inject_synthetic_acceptance.py" "$CONFIG_PATH" "$FRAMEWORK" || exit 1 |
| if count == 0: | ||
| _log("WARNING: No speculative-config entries found to modify; leaving recipe unchanged") | ||
| return 0 |
There was a problem hiding this comment.
🟡 WARNING: When injection is explicitly enabled but nothing matches, this warns and exits 0 — the benchmark proceeds with real speculative decoding while the run is labeled synthetic.
Why it matters: The vLLM backend's regex only matches single-quoted JSON (speculative-config: '<json>'). If an upstream srt-slurm recipe switches to double quotes, block style, or a speculative_config key spelling, count is 0 and the sweep silently records real-MTP numbers as synthetic-acceptance results. A run that set SYNTHETIC_ACCEPTANCE=true has no legitimate reason to have zero speculative-config entries, so this should fail loudly like the other error paths.
Fix:
| if count == 0: | |
| _log("WARNING: No speculative-config entries found to modify; leaving recipe unchanged") | |
| return 0 | |
| if count == 0: | |
| sys.exit("ERROR: SYNTHETIC_ACCEPTANCE=true but no speculative-config entries were found in the recipe") |
| # Optionally inject synthetic acceptance into the recipe's speculative-config | ||
| # when SYNTHETIC_ACCEPTANCE=true (no-op otherwise). Must run after the name | ||
| # override and before srtctl apply so the rendered job picks it up. | ||
| python3 "$GITHUB_WORKSPACE/runners/inject_synthetic_acceptance.py" "$CONFIG_PATH" "$FRAMEWORK" |
There was a problem hiding this comment.
🔴 The synthetic-acceptance injection at runners/launch_gb200-nv.sh:497 is invoked without || exit 1, and the launcher does not use set -e, so any non-zero exit from inject_synthetic_acceptance.py is silently discarded. If a user opts into SYNTHETIC_ACCEPTANCE=true but misconfigures the run (e.g., unregistered framework, MODEL_PREFIX missing from speedbench-reference-al.yaml, missing num_spec_tokens / THINKING_MODE), the driver's sys.exit("ERROR: ...") guardrails fire but srtctl apply still proceeds with the ORIGINAL, uninjected recipe — reporting benchmark numbers as if synthetic AR was applied when it was not. Fix by appending || exit 1 to the python3 call, matching the convention already used by the nearby mkdir -p ... || exit 1 and rsync ... || exit 1 calls in this file.
Extended reasoning...
The bug
At runners/launch_gb200-nv.sh:497:
python3 "$GITHUB_WORKSPACE/runners/inject_synthetic_acceptance.py" "$CONFIG_PATH" "$FRAMEWORK"The launcher has set -x at the top (line 5) but no set -e — verified by grep. The injection call has no || exit 1 guard and its exit status is not checked before the script continues on to unset VIRTUAL_ENV and eventually srtctl apply.
Why the guardrails don't work
inject_synthetic_acceptance.py uses sys.exit("ERROR: ...") for five distinct opt-in misconfiguration paths, all of which are intended as hard failures when the user opts in to synthetic AR but misconfigures it:
get_injector(framework)returnsNone— no backend registered (onlydynamo-vllmis registered today, sodynamo-sglang/dynamo-trtwill hit this).- Reference YAML
speedbench-reference-al.yamlmissing. MODEL_PREFIX-derived key not found in the reference YAML.num_spec_tokens=<n>not found for that model.THINKING_MODEnot found in a thinking-matrix block.
Python's sys.exit(<str>) prints the message to stderr and exits with status 1. Bash silently discards that non-zero exit, and the launcher continues past the injection call.
The impact
Concrete failure mode: a user sets SYNTHETIC_ACCEPTANCE=true and MODEL_PREFIX=minimaxm2.5 (no MODEL_PREFIX_TO_YAML_KEY mapping, likely no top-level key in speedbench-reference-al.yaml). The driver logs ERROR: model key "minimaxm2.5" not found ... and exits 1, but the launcher proceeds to srtctl apply with the unmodified recipe. The sweep then runs against the real draft model and reports numbers under the assumption of synthetic AR — a silent methodological mismatch that poisons the AL curve.
Step-by-step proof
- User sets env:
SYNTHETIC_ACCEPTANCE=true MODEL_PREFIX=minimaxm2.5 FRAMEWORK=dynamo-vllm. - Launcher hits line 497:
python3 .../inject_synthetic_acceptance.py "$CONFIG_PATH" dynamo-vllm. - Inside
inject():SYNTHETIC_ACCEPTANCE == "true"→ proceed.get_injector("dynamo-vllm")returns the registered backend → proceed. _resolve_alreadsspeedbench-reference-al.yaml, computeskey = _yaml_key("minimaxm2.5") = "minimaxm2.5"(not inMODEL_PREFIX_TO_YAML_KEY), thendata.get("minimaxm2.5")returnsNone.sys.exit('ERROR: model key "minimaxm2.5" not found in ...')→ process exits with status 1.- Bash:
$?is now 1. No|| exit 1, noset -e. Execution continues. - Line 501+:
unset VIRTUAL_ENV;srtctl apply -f "$CONFIG_PATH" ...runs against the unmodified$CONFIG_PATH. - Benchmark completes and result JSONs are tagged
gb200,minimaxm2.5,fp8,...,infmax-YYYYMMDD— with no indication that synthetic AR was requested but not applied.
Consistency with the file's own convention
The launcher already establishes the pattern that critical steps must guard their exit codes: line 469 (mkdir -p ... || exit 1), line 476 (rsync ... || exit 1), and the whole elif ... minimaxm3 clone/checkout block chains || exit 1 on every step. The synthetic-AR injection is at least as consequential (silent-bad-benchmark-numbers vs. a fast failure) — the omission reads as an oversight, not a deliberate choice.
The fix
Append || exit 1 to the python3 call:
python3 "$GITHUB_WORKSPACE/runners/inject_synthetic_acceptance.py" "$CONFIG_PATH" "$FRAMEWORK" || exit 1One character trailing pipe. Matches the file's own convention. Preserves the driver's no-op-on-opt-out behavior (a successful exit 0 still falls through) and makes opt-in misconfiguration abort loudly, which is exactly what the driver's sys.exit guardrails were trying to establish.
No description provided.