Skip to content

[NV] [PR2] feat(synthetic-al): vLLM backend for synthetic-acceptance injection#2090

Open
qiching wants to merge 4 commits into
mainfrom
albecheng/synthetic-al-vllm
Open

[NV] [PR2] feat(synthetic-al): vLLM backend for synthetic-acceptance injection#2090
qiching wants to merge 4 commits into
mainfrom
albecheng/synthetic-al-vllm

Conversation

@qiching

@qiching qiching commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

No description provided.

root added 3 commits July 5, 2026 18:36
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).
@qiching qiching marked this pull request as ready for review July 8, 2026 17:05
@qiching qiching requested a review from a team July 8, 2026 17:05
@claude

claude Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Claude finished @qiching's task in 2m 55s —— View job


Review

  • Read the PR diff and changed files
  • Verify injector behavior and launcher error handling
  • Post review feedback

Found 1 blocking issue and 1 warning, both about the same failure mode: a run that opts in with SYNTHETIC_ACCEPTANCE=true can silently fall through to real speculative decoding and record those numbers as synthetic-acceptance results.

  • 🔴 runners/launch_gb200-nv.sh:497 — the injector call has no || exit 1 and the script has no set -e, so all of the Python script's fail-loud sys.exit paths are ignored and the unmodified recipe is submitted anyway.
  • 🟡 runners/inject_synthetic_acceptance.py:146-148 — the "no speculative-config entries matched" case warns and exits 0; for an explicitly opted-in run this should be a hard error (the regex only matches single-quoted JSON, so an upstream recipe formatting change would silently disable injection).

One heads-up (not blocking, assuming it lands in the stacked follow-up): benchmarks/speedbench-reference-al.yaml does not exist in the repo yet — the speedbench scripts only write it to $RESULTS_DIR — so the AL auto-lookup path can currently never succeed and enabled runs must set SYNTHETIC_ACCEPTANCE_LENGTH explicitly. The registry/backend split itself, the module-level import path, and the JSON rewrite logic all check out.

# 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"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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:

Suggested change
python3 "$GITHUB_WORKSPACE/runners/inject_synthetic_acceptance.py" "$CONFIG_PATH" "$FRAMEWORK"
python3 "$GITHUB_WORKSPACE/runners/inject_synthetic_acceptance.py" "$CONFIG_PATH" "$FRAMEWORK" || exit 1

Comment on lines +146 to +148
if count == 0:
_log("WARNING: No speculative-config entries found to modify; leaving recipe unchanged")
return 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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:

Suggested change
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")

Comment on lines +494 to +497
# 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"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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:

  1. get_injector(framework) returns None — no backend registered (only dynamo-vllm is registered today, so dynamo-sglang / dynamo-trt will hit this).
  2. Reference YAML speedbench-reference-al.yaml missing.
  3. MODEL_PREFIX-derived key not found in the reference YAML.
  4. num_spec_tokens=<n> not found for that model.
  5. THINKING_MODE not 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

  1. User sets env: SYNTHETIC_ACCEPTANCE=true MODEL_PREFIX=minimaxm2.5 FRAMEWORK=dynamo-vllm.
  2. Launcher hits line 497: python3 .../inject_synthetic_acceptance.py "$CONFIG_PATH" dynamo-vllm.
  3. Inside inject(): SYNTHETIC_ACCEPTANCE == "true" → proceed. get_injector("dynamo-vllm") returns the registered backend → proceed.
  4. _resolve_al reads speedbench-reference-al.yaml, computes key = _yaml_key("minimaxm2.5") = "minimaxm2.5" (not in MODEL_PREFIX_TO_YAML_KEY), then data.get("minimaxm2.5") returns None.
  5. sys.exit('ERROR: model key "minimaxm2.5" not found in ...') → process exits with status 1.
  6. Bash: $? is now 1. No || exit 1, no set -e. Execution continues.
  7. Line 501+: unset VIRTUAL_ENV; srtctl apply -f "$CONFIG_PATH" ... runs against the unmodified $CONFIG_PATH.
  8. 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 1

One 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.

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

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

1 participant