Skip to content

feat: Add adaptive speculative decoding engine and benchmark results - #17545

Open
tolani007 wants to merge 1 commit into
NVIDIA:mainfrom
tolani007:feature/adaptive-speculative-decoding
Open

feat: Add adaptive speculative decoding engine and benchmark results#17545
tolani007 wants to merge 1 commit into
NVIDIA:mainfrom
tolani007:feature/adaptive-speculative-decoding

Conversation

@tolani007

@tolani007 tolani007 commented Aug 12, 2026

Copy link
Copy Markdown

Description

Static speculative decoding uses a fixed draft length (k) for all input prompts. Creative and complex reasoning tasks cause low draft token acceptance rates. Low acceptance rates decrease inference throughput by up to 54% due to verification overhead.

This pull request adds an adaptive speculative decoding engine:

  1. AcceptanceMonitor: Tracks real-time token acceptance rates using an Exponential Moving Average (EMA).
  2. AdaptiveDraftController: Adjusts draft length (k) dynamically based on acceptance statistics.
  3. AdaptiveRunner: Executes generation with the selected draft length.

Empirical verification on Tesla T4 hardware demonstrates a 1.29x throughput increase (29% recovery) on hard prompts compared to static k=7 speculative decoding.

Test Coverage

The following tests validate the implementation:

  1. scripts/test_adaptive_logic.py: Unit tests for EMA monitoring, strategy transition thresholds, and trend detection.
  2. scripts/benchmark_baseline.py: Baseline benchmarking suite across easy, medium, and hard prompt categories.
  3. scripts/benchmark_adaptive.py: Comparative evaluation suite measuring static versus adaptive execution.

PR Checklist

  • Please check this after reviewing the above items as appropriate for this PR.

Summary

  • Added EMA-based acceptance monitoring with trend detection.
  • Added adaptive draft-length selection with warmup, bounded strategies, and reporting.
  • Added AdaptiveSpeculativeRunner for controller-driven generation and metrics.
  • Added CPU-only logic tests and benchmark scripts.
  • Added baseline and adaptive benchmark results, README documentation, and chart generation.
  • Reported up to 1.29× throughput improvement on hard prompts with Tesla T4 benchmarks.

Dev Engineer Review

  • The implementation adds the expected public APIs and separates monitoring, control, and execution responsibilities.
  • Benchmark scripts record reproducible per-prompt and aggregate results.
  • No configuration files or test-list files changed.
  • Review should verify TensorRT-LLM integration, acceptance estimation, GPU memory cleanup, and behavior when baseline results or speculative drafting are unavailable.
  • Review should confirm that benchmark timestamps and hardware-specific results are not treated as universal performance guarantees.

QA Engineer Review

No test changes.

@tolani007
tolani007 requested a review from a team as a code owner August 12, 2026 06:57
@tolani007
tolani007 requested review from BowenFu and QiJune August 12, 2026 06:57
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This PR adds adaptive speculative decoding with EMA-based acceptance monitoring and dynamic draft-length selection. It integrates the controller with TensorRT-LLM, adds vLLM baseline and adaptive benchmark scripts, stores benchmark results, generates charts, and documents usage and findings.

Changes

Adaptive speculative decoding

Layer / File(s) Summary
Acceptance monitoring and draft control
adaptive-spec-decode/src/acceptance_monitor.py, adaptive-spec-decode/src/draft_controller.py, adaptive-spec-decode/scripts/test_adaptive_logic.py
Tracks EMA acceptance rates and trends. Selects draft strategies and lengths. Tests warmup, trends, strategies, and reporting.
Adaptive generation integration
adaptive-spec-decode/src/adaptive_runner.py
Runs TensorRT-LLM generation with controller-selected draft lengths, target-only fallback, acceptance updates, traces, and metrics.
Baseline and adaptive benchmark pipeline
adaptive-spec-decode/scripts/benchmark_baseline.py, adaptive-spec-decode/scripts/benchmark_adaptive.py, adaptive-spec-decode/results/*.json
Benchmarks vanilla and fixed speculative decoding. Applies adaptive decisions to baseline data. Writes aggregate and per-prompt results.
Charting and usage documentation
adaptive-spec-decode/scripts/generate_charts.py, adaptive-spec-decode/README.md
Generates grouped throughput comparisons. Documents architecture, commands, benchmark results, and conclusions.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Prompt
  participant AdaptiveSpeculativeRunner
  participant AdaptiveDraftController
  participant AcceptanceMonitor
  participant TensorRTLLM
  Prompt->>AdaptiveSpeculativeRunner: submit prompt
  AdaptiveSpeculativeRunner->>AdaptiveDraftController: request draft decision
  AdaptiveDraftController->>AcceptanceMonitor: read acceptance state
  AcceptanceMonitor-->>AdaptiveDraftController: return rate and trend
  AdaptiveDraftController-->>AdaptiveSpeculativeRunner: return draft length
  AdaptiveSpeculativeRunner->>TensorRTLLM: generate tokens
  TensorRTLLM-->>AdaptiveSpeculativeRunner: return tokens and metrics
  AdaptiveSpeculativeRunner->>AcceptanceMonitor: update acceptance counts
Loading

Possibly related PRs

  • NVIDIA/TensorRT-LLM#17474: Both PRs address speculative-decoding acceptance metrics, but this PR adds adaptive monitoring and benchmarking.

Suggested reviewers: qijune

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the adaptive speculative decoding engine and its benchmark results.
Description check ✅ Passed The description explains the problem and solution, identifies test coverage, and includes the required checklist section.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 13

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@adaptive-spec-decode/scripts/benchmark_adaptive.py`:
- Around line 38-63: The adaptive benchmark currently replays static
measurements and fabricates acceptance counts instead of measuring the
controller-selected draft length. In
adaptive-spec-decode/scripts/benchmark_adaptive.py, replace the run-selection
and simulated-rate logic around monitor.update with execution of the selected
configuration and actual drafted/accepted token counters; regenerate
adaptive-spec-decode/results/adaptive.json from those runs, and remove or
qualify the adaptive speedup claims in adaptive-spec-decode/README.md until
measured results are available.
- Around line 89-100: Update the reporting flow around controller.get_report()
and the adaptive_results construction so controller_report.total_decisions
matches the ten emitted adaptive results by including the warmup decision in
controller history. If warmup cannot be recorded, rename the report field to
explicitly indicate that warmup decisions are excluded.

In `@adaptive-spec-decode/scripts/benchmark_baseline.py`:
- Line 48: Replace the semicolon-separated cleanup statements in the affected
benchmark paths with separate Python statements, including each occurrence of
`del llm; free_gpu()` referenced near the cleanup logic. Preserve the existing
operation order: delete `llm`, then call `free_gpu()`.
- Line 1: Add the required NVIDIA copyright header, using 2026 as the latest
meaningful modification year, to
adaptive-spec-decode/scripts/benchmark_baseline.py lines 1-1,
adaptive-spec-decode/scripts/benchmark_adaptive.py lines 1-4, and
adaptive-spec-decode/scripts/generate_charts.py lines 1-1 before their existing
content; add the Markdown-compatible header to adaptive-spec-decode/README.md
line 1 before the title.
- Around line 19-20: Remove hardcoded author-specific paths across the benchmark
workflow. In adaptive-spec-decode/scripts/benchmark_baseline.py (lines 19-20),
accept target and draft model paths via CLI options or configuration; in
adaptive-spec-decode/scripts/benchmark_adaptive.py (line 8), derive the
repository root from __file__; in
adaptive-spec-decode/scripts/generate_charts.py (lines 6-7), resolve inputs from
that root or explicit options and, at line 47, write charts to the same
configurable results directory; update adaptive-spec-decode/README.md (lines
24-36) with the configuration inputs and a complete portable command sequence.
- Around line 22-42: Annotate every affected function: add precise parameter and
return annotations plus Google-style docstrings to run_bench, free_gpu, and main
in adaptive-spec-decode/scripts/benchmark_baseline.py (lines 22-42), including
an explicit typed benchmark-record structure for run_bench results; annotate
main with -> None in adaptive-spec-decode/scripts/benchmark_adaptive.py (line
13); and annotate get_avgs in adaptive-spec-decode/scripts/generate_charts.py
(line 17) with the precise benchmark-record input and return types.
- Around line 24-35: Update the benchmark setup around SamplingParams to use
deterministic sampling with temperature=0.0 and an explicit seed, or otherwise
enforce equal token budgets with ignore_eos=True. Add the sampling
configuration, GPU and driver details, vLLM version, and model revisions to the
recorded meta output. Regenerate baseline.json and every report derived from it.

In `@adaptive-spec-decode/scripts/test_adaptive_logic.py`:
- Around line 2-6: Replace the hard-coded path insertion in
test_adaptive_logic.py with a project-root path derived from __file__, then
insert that resolved root into sys.path before importing AcceptanceMonitor and
AdaptiveDraftController so local and CI environments use the same imports.

In `@adaptive-spec-decode/src/acceptance_monitor.py`:
- Around line 9-61: Add complete type annotations and Google-style docstrings
across adaptive-spec-decode/src/acceptance_monitor.py lines 9-61 for
AcceptanceStats, AcceptanceMonitor, and every public method; apply the same
treatment to DraftDecision, AdaptiveDraftController, and their methods in
adaptive-spec-decode/src/draft_controller.py lines 8-80, and
AdaptiveSpeculativeRunner and its public methods in
adaptive-spec-decode/src/adaptive_runner.py lines 24-91. In
adaptive-spec-decode/scripts/test_adaptive_logic.py lines 8-64, add -> None to
every test function; annotate every function without changing behavior.
- Around line 27-30: Update AcceptanceMonitor.update to validate accepted before
the drafted == 0 early return: reject negative accepted values and any accepted
value greater than drafted, while preserving current_stats() for zero drafted
and normal rate calculation for valid counts.
- Around line 1-4: Add the standard NVIDIA copyright header, using 2026 as the
latest meaningful modification year, to
adaptive-spec-decode/src/acceptance_monitor.py (lines 1-4),
adaptive-spec-decode/src/draft_controller.py (lines 1-4),
adaptive-spec-decode/scripts/test_adaptive_logic.py (line 1), and
adaptive-spec-decode/src/adaptive_runner.py (lines 1-4).

In `@adaptive-spec-decode/src/adaptive_runner.py`:
- Around line 39-63: Update the generation logic around the LLM calls in the
adaptive runner to enable per-request performance metrics and derive controller
updates from the generated output’s
request_perf_metrics.speculative_decoding.total_accepted_draft_tokens and
total_draft_tokens, removing the throughput-based estimated_accept calculation.
Preserve zero metrics for target-only runs, but add bounded speculative probes
or equivalent recovery before a vanilla decision becomes permanent so the
monitor can resume adapting.

In `@adaptive-spec-decode/src/draft_controller.py`:
- Around line 37-39: The warmup branch in the draft controller must honor
configured draft bounds and record its decision. In the `not
self.monitor.is_warmed_up` path, bound the warmup `k=5` value using the existing
`min_draft` and `max_draft`, create the `DraftDecision`, append it to
`_decision_history`, then return that same decision.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: da447688-92f8-466b-98b7-13ada1b2c4d2

📥 Commits

Reviewing files that changed from the base of the PR and between a2e0fba and 2e8853e.

⛔ Files ignored due to path filters (1)
  • adaptive-spec-decode/results/throughput_comparison.png is excluded by !**/*.png
📒 Files selected for processing (11)
  • adaptive-spec-decode/README.md
  • adaptive-spec-decode/results/adaptive.json
  • adaptive-spec-decode/results/baseline.json
  • adaptive-spec-decode/scripts/benchmark_adaptive.py
  • adaptive-spec-decode/scripts/benchmark_baseline.py
  • adaptive-spec-decode/scripts/generate_charts.py
  • adaptive-spec-decode/scripts/test_adaptive_logic.py
  • adaptive-spec-decode/src/__init__.py
  • adaptive-spec-decode/src/acceptance_monitor.py
  • adaptive-spec-decode/src/adaptive_runner.py
  • adaptive-spec-decode/src/draft_controller.py

Comment on lines +38 to +63
# Select empirical run matching controller decision
if k == 0:
run_data = vanilla_data[i]
# Vanilla has no draft tokens
accepted = 0
drafted = 0
simulated_rate = 0.2 # low fallback signal
elif k <= 3:
run_data = spec_k3_data[i]
drafted = 3
# Estimate acceptance from speed ratio relative to vanilla
simulated_rate = max(0.2, min(0.9, run_data["tok_per_sec"] / vanilla_data[i]["tok_per_sec"]))
accepted = int(drafted * simulated_rate)
elif k <= 6:
run_data = spec_k5_data[i]
drafted = 5
simulated_rate = max(0.2, min(0.9, run_data["tok_per_sec"] / vanilla_data[i]["tok_per_sec"]))
accepted = int(drafted * simulated_rate)
else:
run_data = spec_k7_data[i]
drafted = 7
simulated_rate = max(0.2, min(0.9, run_data["tok_per_sec"] / vanilla_data[i]["tok_per_sec"]))
accepted = int(drafted * simulated_rate)

# Update real-time monitor
stats = monitor.update(drafted=drafted, accepted=accepted)

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.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Measure adaptive execution instead of replaying static measurements.

The script selects spec_k3, spec_k5, or spec_k7 records from the baseline file. It does not run the controller-selected configuration. It also derives accepted tokens from throughput ratios, although AcceptanceMonitor.update() requires actual drafted and accepted token counts. For example, adaptive.json reports chosen_k: 1 for id 8 but reuses the static k=3 throughput.

  • adaptive-spec-decode/scripts/benchmark_adaptive.py#L38-L63: execute each controller-selected draft length and update the monitor from actual speculative-decoding counters.
  • adaptive-spec-decode/results/adaptive.json#L2-L121: regenerate this artifact from measured adaptive runs after the benchmark is corrected.
  • adaptive-spec-decode/README.md#L9-L16: remove or qualify the adaptive speedup claims until regenerated measurements are available.
📍 Affects 3 files
  • adaptive-spec-decode/scripts/benchmark_adaptive.py#L38-L63 (this comment)
  • adaptive-spec-decode/results/adaptive.json#L2-L121
  • adaptive-spec-decode/README.md#L9-L16
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@adaptive-spec-decode/scripts/benchmark_adaptive.py` around lines 38 - 63, The
adaptive benchmark currently replays static measurements and fabricates
acceptance counts instead of measuring the controller-selected draft length. In
adaptive-spec-decode/scripts/benchmark_adaptive.py, replace the run-selection
and simulated-rate logic around monitor.update with execution of the selected
configuration and actual drafted/accepted token counters; regenerate
adaptive-spec-decode/results/adaptive.json from those runs, and remove or
qualify the adaptive speedup claims in adaptive-spec-decode/README.md until
measured results are available.

Comment on lines +89 to +100
out = {
"adaptive_results": results,
"controller_report": controller.get_report(),
"summary": {
"vanilla_avg": van_avg,
"spec_k3_avg": s3_avg,
"spec_k5_avg": s5_avg,
"spec_k7_avg": s7_avg,
"adaptive_avg": adapt_avg,
"speedup_vs_k7": adapt_avg / s7_avg,
"speedup_vs_k5": adapt_avg / s5_avg,
}

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.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Make total_decisions match the emitted results.

This script emits ten entries in adaptive_results. controller_report.total_decisions is nine because the warmup decision is not recorded in the controller history. Include warmup decisions in the report, or rename the field to state that it excludes warmup.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@adaptive-spec-decode/scripts/benchmark_adaptive.py` around lines 89 - 100,
Update the reporting flow around controller.get_report() and the
adaptive_results construction so controller_report.total_decisions matches the
ten emitted adaptive results by including the warmup decision in controller
history. If warmup cannot be recorded, rename the report field to explicitly
indicate that warmup decisions are excluded.

@@ -0,0 +1,92 @@
"""Baseline: Vanilla vs Static Speculative Decoding (vLLM)"""

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add the NVIDIA copyright header.

These are new files. Add the required NVIDIA copyright header before the existing content. Use 2026 as the latest meaningful modification year.

  • adaptive-spec-decode/scripts/benchmark_baseline.py#L1-L1: add the header before the module docstring.
  • adaptive-spec-decode/scripts/benchmark_adaptive.py#L1-L4: add the header before the module docstring.
  • adaptive-spec-decode/scripts/generate_charts.py#L1-L1: add the header before the imports.
  • adaptive-spec-decode/README.md#L1-L1: add a Markdown-compatible copyright header before the title.

As per coding guidelines, “Add the NVIDIA copyright header to all new files.”

📍 Affects 4 files
  • adaptive-spec-decode/scripts/benchmark_baseline.py#L1-L1 (this comment)
  • adaptive-spec-decode/scripts/benchmark_adaptive.py#L1-L4
  • adaptive-spec-decode/scripts/generate_charts.py#L1-L1
  • adaptive-spec-decode/README.md#L1-L1
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@adaptive-spec-decode/scripts/benchmark_baseline.py` at line 1, Add the
required NVIDIA copyright header, using 2026 as the latest meaningful
modification year, to adaptive-spec-decode/scripts/benchmark_baseline.py lines
1-1, adaptive-spec-decode/scripts/benchmark_adaptive.py lines 1-4, and
adaptive-spec-decode/scripts/generate_charts.py lines 1-1 before their existing
content; add the Markdown-compatible header to adaptive-spec-decode/README.md
line 1 before the title.

Source: Coding guidelines

Comment on lines +19 to +20
TARGET = "/teamspace/studios/this_studio/models/qwen-2.5-3b"
DRAFT = "/teamspace/studios/this_studio/models/qwen-2.5-0.5b"

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Remove author-specific filesystem paths from the benchmark workflow.

The baseline script requires /teamspace/studios/this_studio. The adaptive script imports from the same location. The chart script instead reads and writes under ~/adaptive-spec-decode. The documented commands cannot work reliably outside the author environment.

  • adaptive-spec-decode/scripts/benchmark_baseline.py#L19-L20: accept target and draft model paths through CLI options or a configuration file.
  • adaptive-spec-decode/scripts/benchmark_adaptive.py#L8-L8: resolve the repository root from __file__ instead of inserting a fixed path.
  • adaptive-spec-decode/scripts/generate_charts.py#L6-L7: resolve benchmark input paths from the repository root or explicit CLI options.
  • adaptive-spec-decode/scripts/generate_charts.py#L47-L47: use the same configurable result directory for chart output.
  • adaptive-spec-decode/README.md#L24-L36: document the configuration inputs and a complete portable command sequence.
📍 Affects 4 files
  • adaptive-spec-decode/scripts/benchmark_baseline.py#L19-L20 (this comment)
  • adaptive-spec-decode/scripts/benchmark_adaptive.py#L8-L8
  • adaptive-spec-decode/scripts/generate_charts.py#L6-L7
  • adaptive-spec-decode/scripts/generate_charts.py#L47-L47
  • adaptive-spec-decode/README.md#L24-L36
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@adaptive-spec-decode/scripts/benchmark_baseline.py` around lines 19 - 20,
Remove hardcoded author-specific paths across the benchmark workflow. In
adaptive-spec-decode/scripts/benchmark_baseline.py (lines 19-20), accept target
and draft model paths via CLI options or configuration; in
adaptive-spec-decode/scripts/benchmark_adaptive.py (line 8), derive the
repository root from __file__; in
adaptive-spec-decode/scripts/generate_charts.py (lines 6-7), resolve inputs from
that root or explicit options and, at line 47, write charts to the same
configurable results directory; update adaptive-spec-decode/README.md (lines
24-36) with the configuration inputs and a complete portable command sequence.

Comment on lines +22 to +42
def run_bench(name, llm, max_tokens=200):
print(f"\n{'='*60}\n{name}\n{'='*60}")
sampling = SamplingParams(max_tokens=max_tokens, temperature=0.7)
results = []
for i, prompt in enumerate(PROMPTS):
start = time.perf_counter()
output = llm.generate([prompt], sampling_params=sampling)
elapsed = time.perf_counter() - start
ntok = len(output[0].outputs[0].token_ids)
tps = ntok / elapsed
ptype = "easy" if i < 4 else ("medium" if i < 6 else "hard")
results.append({"id": i, "type": ptype, "tokens": ntok,
"elapsed": round(elapsed,3), "tok_per_sec": round(tps,2)})
print(f" [{ptype:6s}] {i}: {ntok} tok in {elapsed:.2f}s = {tps:.1f} tok/s")
return results

def free_gpu():
gc.collect()
torch.cuda.empty_cache()

def main():

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add precise function annotations and docstrings.

These functions have no annotations. The benchmark result structures also need explicit types instead of implicit dictionaries.

  • adaptive-spec-decode/scripts/benchmark_baseline.py#L22-L42: annotate run_bench, free_gpu, and main; add Google-style docstrings where externally usable.
  • adaptive-spec-decode/scripts/benchmark_adaptive.py#L13-L13: annotate main with -> None.
  • adaptive-spec-decode/scripts/generate_charts.py#L17-L17: annotate get_avgs with a precise benchmark-record type and return type.

As per coding guidelines, “Annotate every function” and “Use docstrings rather than comments for externally usable interfaces.”

📍 Affects 3 files
  • adaptive-spec-decode/scripts/benchmark_baseline.py#L22-L42 (this comment)
  • adaptive-spec-decode/scripts/benchmark_adaptive.py#L13-L13
  • adaptive-spec-decode/scripts/generate_charts.py#L17-L17
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@adaptive-spec-decode/scripts/benchmark_baseline.py` around lines 22 - 42,
Annotate every affected function: add precise parameter and return annotations
plus Google-style docstrings to run_bench, free_gpu, and main in
adaptive-spec-decode/scripts/benchmark_baseline.py (lines 22-42), including an
explicit typed benchmark-record structure for run_bench results; annotate main
with -> None in adaptive-spec-decode/scripts/benchmark_adaptive.py (line 13);
and annotate get_avgs in adaptive-spec-decode/scripts/generate_charts.py (line
17) with the precise benchmark-record input and return types.

Source: Coding guidelines

Comment on lines +1 to +4
"""
Real-time acceptance rate monitor for speculative decoding.
Tracks per-request acceptance rates using exponential moving average.
"""

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add the required NVIDIA copyright header to every new file.

  • adaptive-spec-decode/src/acceptance_monitor.py#L1-L4: Add the NVIDIA header with 2026 as the latest meaningful modification year.
  • adaptive-spec-decode/src/draft_controller.py#L1-L4: Add the NVIDIA header with 2026 as the latest meaningful modification year.
  • adaptive-spec-decode/scripts/test_adaptive_logic.py#L1-L1: Add the NVIDIA header with 2026 as the latest meaningful modification year.
  • adaptive-spec-decode/src/adaptive_runner.py#L1-L4: Add the NVIDIA header with 2026 as the latest meaningful modification year.

As per coding guidelines: “Source files must contain the NVIDIA copyright header with the year of the latest meaningful modification.”

📍 Affects 4 files
  • adaptive-spec-decode/src/acceptance_monitor.py#L1-L4 (this comment)
  • adaptive-spec-decode/src/draft_controller.py#L1-L4
  • adaptive-spec-decode/scripts/test_adaptive_logic.py#L1-L1
  • adaptive-spec-decode/src/adaptive_runner.py#L1-L4
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@adaptive-spec-decode/src/acceptance_monitor.py` around lines 1 - 4, Add the
standard NVIDIA copyright header, using 2026 as the latest meaningful
modification year, to adaptive-spec-decode/src/acceptance_monitor.py (lines
1-4), adaptive-spec-decode/src/draft_controller.py (lines 1-4),
adaptive-spec-decode/scripts/test_adaptive_logic.py (line 1), and
adaptive-spec-decode/src/adaptive_runner.py (lines 1-4).

Source: Coding guidelines

Comment on lines +9 to +61
@dataclass
class AcceptanceStats:
total_drafted: int = 0
total_accepted: int = 0
acceptance_rate: float = 0.0
timestamp: float = 0.0

class AcceptanceMonitor:
def __init__(self, ema_alpha=0.3, window_size=10, warmup_steps=3):
self.ema_alpha = ema_alpha
self.window_size = window_size
self.warmup_steps = warmup_steps
self._ema_rate = 0.5
self._history = deque(maxlen=window_size)
self._step_count = 0
self._total_drafted = 0
self._total_accepted = 0

def update(self, drafted, accepted):
if drafted == 0:
return self.current_stats()
instant_rate = accepted / drafted
self._ema_rate = (self.ema_alpha * instant_rate +
(1 - self.ema_alpha) * self._ema_rate)
self._step_count += 1
self._total_drafted += drafted
self._total_accepted += accepted
stats = AcceptanceStats(drafted, accepted, self._ema_rate, time.perf_counter())
self._history.append(stats)
return stats

def current_stats(self):
return AcceptanceStats(self._total_drafted, self._total_accepted,
self._ema_rate, time.perf_counter())

@property
def is_warmed_up(self):
return self._step_count >= self.warmup_steps

@property
def acceptance_rate(self):
return self._ema_rate

@property
def acceptance_trend(self):
if len(self._history) < 3:
return "unknown"
recent = list(self._history)[-3:]
if recent[-1].acceptance_rate > recent[0].acceptance_rate + 0.05:
return "improving"
elif recent[-1].acceptance_rate < recent[0].acceptance_rate - 0.05:
return "degrading"
return "stable"

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.

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Add required type annotations and public-interface docstrings.

  • adaptive-spec-decode/src/acceptance_monitor.py#L9-L61: Annotate all methods and add Google-style docstrings for AcceptanceStats, AcceptanceMonitor, and its public methods.
  • adaptive-spec-decode/src/draft_controller.py#L8-L80: Annotate all methods and add Google-style docstrings for DraftDecision, AdaptiveDraftController, and its public methods.
  • adaptive-spec-decode/scripts/test_adaptive_logic.py#L8-L64: Add -> None annotations to each test function.
  • adaptive-spec-decode/src/adaptive_runner.py#L24-L91: Annotate all methods and add Google-style docstrings for AdaptiveSpeculativeRunner and its public methods.

As per coding guidelines: “Annotate every function” and “Use docstrings rather than comments for externally usable interfaces, Google-style docstrings for classes and functions.”

📍 Affects 4 files
  • adaptive-spec-decode/src/acceptance_monitor.py#L9-L61 (this comment)
  • adaptive-spec-decode/src/draft_controller.py#L8-L80
  • adaptive-spec-decode/scripts/test_adaptive_logic.py#L8-L64
  • adaptive-spec-decode/src/adaptive_runner.py#L24-L91
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@adaptive-spec-decode/src/acceptance_monitor.py` around lines 9 - 61, Add
complete type annotations and Google-style docstrings across
adaptive-spec-decode/src/acceptance_monitor.py lines 9-61 for AcceptanceStats,
AcceptanceMonitor, and every public method; apply the same treatment to
DraftDecision, AdaptiveDraftController, and their methods in
adaptive-spec-decode/src/draft_controller.py lines 8-80, and
AdaptiveSpeculativeRunner and its public methods in
adaptive-spec-decode/src/adaptive_runner.py lines 24-91. In
adaptive-spec-decode/scripts/test_adaptive_logic.py lines 8-64, add -> None to
every test function; annotate every function without changing behavior.

Source: Coding guidelines

Comment on lines +27 to +30
def update(self, drafted, accepted):
if drafted == 0:
return self.current_stats()
instant_rate = accepted / drafted

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject invalid acceptance counts.

If accepted is negative or exceeds drafted, instant_rate becomes invalid. The controller can then select an incorrect strategy. Reject negative values and require accepted <= drafted before the zero-draft special case.

Proposed fix
     def update(self, drafted, accepted):
+        if drafted < 0 or accepted < 0 or accepted > drafted:
+            raise ValueError("accepted must be between 0 and drafted")
         if drafted == 0:
             return self.current_stats()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def update(self, drafted, accepted):
if drafted == 0:
return self.current_stats()
instant_rate = accepted / drafted
def update(self, drafted, accepted):
if drafted < 0 or accepted < 0 or accepted > drafted:
raise ValueError("accepted must be between 0 and drafted")
if drafted == 0:
return self.current_stats()
instant_rate = accepted / drafted
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@adaptive-spec-decode/src/acceptance_monitor.py` around lines 27 - 30, Update
AcceptanceMonitor.update to validate accepted before the drafted == 0 early
return: reject negative accepted values and any accepted value greater than
drafted, while preserving current_stats() for zero drafted and normal rate
calculation for valid counts.

Comment on lines +39 to +63
if decision.draft_length == 0:
llm = LLM(model=self.target_path)
start = time.perf_counter()
output = llm.generate([prompt], sampling_params=sampling)
elapsed = time.perf_counter() - start
ntok = len(output[0].outputs[0].token_ids)
self.monitor.update(drafted=0, accepted=0)
del llm
else:
spec_cfg = DraftTargetDecodingConfig(
max_draft_len=decision.draft_length,
speculative_model=self.draft_path,
)
llm = LLM(model=self.target_path, speculative_config=spec_cfg)
start = time.perf_counter()
output = llm.generate([prompt], sampling_params=sampling)
elapsed = time.perf_counter() - start
ntok = len(output[0].outputs[0].token_ids)
# Estimate acceptance from throughput
estimated_accept = min(0.95, max(0.1, ntok / (elapsed * 50)))
n_cycles = max(1, ntok // max(decision.draft_length, 1))
self.monitor.update(
drafted=decision.draft_length * n_cycles,
accepted=int(estimated_accept * decision.draft_length * n_cycles)
)

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.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate TensorRT-LLM result and speculative-decoding telemetry fields.
ast-grep outline tensorrt_llm/llmapi --items all --type class \
  --match 'Result|RequestOutput|Generation'

rg -n -C 4 \
  'accepted.*draft|draft.*accepted|acceptance.*rate|speculative.*metric' \
  tensorrt_llm adaptive-spec-decode

Repository: NVIDIA/TensorRT-LLM

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- adaptive-spec-decode files ---'
git ls-files adaptive-spec-decode

printf '%s\n' '--- runner and monitor references ---'
rg -n -C 6 \
  'class AcceptanceMonitor|def update|draft_length|monitor\.update|AcceptanceMonitor|speculative_decoding|total_accepted_draft_tokens|total_draft_tokens' \
  adaptive-spec-decode

printf '%s\n' '--- relevant telemetry definitions and access paths ---'
rg -n -C 8 \
  'class .*Speculative|speculative_decoding|total_accepted_draft_tokens|total_draft_tokens|acceptance_rate' \
  tensorrt_llm/serve tensorrt_llm/_torch/speculative \
  -g '*.py' | head -n 500

printf '%s\n' '--- runner structure ---'
wc -l adaptive-spec-decode/src/adaptive_runner.py
ast-grep outline adaptive-spec-decode/src/adaptive_runner.py --items all

Repository: NVIDIA/TensorRT-LLM

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- acceptance monitor ---'
cat -n adaptive-spec-decode/src/acceptance_monitor.py

printf '%s\n' '--- draft controller ---'
cat -n adaptive-spec-decode/src/draft_controller.py

printf '%s\n' '--- runner ---'
cat -n adaptive-spec-decode/src/adaptive_runner.py

printf '%s\n' '--- LLM API output and metrics references ---'
rg -n -C 5 \
  'class (RequestOutput|CompletionOutput|LLM)|def generate|perf_metrics|metrics|speculative_decoding' \
  tensorrt_llm/llmapi tensorrt_llm \
  -g '*.py' | grep -E 'llmapi|RequestOutput|CompletionOutput|perf_metrics|speculative_decoding|def generate|class RequestOutput' | head -n 300

printf '%s\n' '--- focused API files ---'
git ls-files tensorrt_llm/llmapi | head -n 100

Repository: NVIDIA/TensorRT-LLM

Length of output: 32781


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- RequestOutput fields ---'
sed -n '45,145p' tensorrt_llm/llmapi/llm.py

printf '%s\n' '--- generate implementation and perf-metrics option ---'
sed -n '549,575p' tensorrt_llm/llmapi/llm.py
sed -n '755,775p' tensorrt_llm/llmapi/llm.py
sed -n '1400,1420p' tensorrt_llm/llmapi/llm.py

printf '%s\n' '--- result base classes and metrics_dict ---'
rg -n -C 5 \
  'class (GenerationResult|.*Generation.*Result|DetokenizedGenerationResultBase)|metrics_dict|return_perf_metrics' \
  tensorrt_llm -g '*.py' | head -n 250

printf '%s\n' '--- perf metric collection and public retrieval ---'
rg -n -C 7 \
  'get_stats_async|return_perf_metrics|perf_metrics_output_dir|SpeculativeDecodingMetrics|total_accepted_draft_tokens' \
  tensorrt_llm/llmapi tensorrt_llm/serve \
  -g '*.py' | head -n 350

Repository: NVIDIA/TensorRT-LLM

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- per-request metric construction ---'
sed -n '300,380p' tensorrt_llm/executor/result.py
sed -n '740,780p' tensorrt_llm/executor/result.py
rg -n -C 8 \
  'def _build_perf_metrics_dict|request_perf_metrics|speculative_decoding' \
  tensorrt_llm/executor tensorrt_llm/_torch tensorrt_llm/serve \
  -g '*.py' | head -n 250

printf '%s\n' '--- output metric exposure ---'
rg -n -C 6 \
  'metrics_dict|candidate_metrics|return_perf_metrics' \
  tensorrt_llm/llmapi/llm.py tensorrt_llm/executor/result.py \
  -g '*.py' | head -n 250

printf '%s\n' '--- deterministic controller simulation ---'
python3 - <<'PY'
from dataclasses import dataclass

`@dataclass`
class Monitor:
    ema: float = 0.5
    alpha: float = 0.3
    steps: int = 0

    def update(self, drafted: int, accepted: int) -> None:
        if drafted == 0:
            return
        rate = accepted / drafted
        self.ema = self.alpha * rate + (1 - self.alpha) * self.ema
        self.steps += 1

def decision(monitor: Monitor) -> tuple[str, int]:
    if monitor.steps < 3:
        return "warmup", 5
    if monitor.ema >= 0.75:
        return "aggressive", 8
    if monitor.ema >= 0.50:
        return "moderate", 5
    if monitor.ema >= 0.30:
        return "conservative", 2
    return "vanilla", 0

monitor = Monitor()
for accepted in (0, 0, 0):
    monitor.update(10, accepted)
    print(f"after speculative sample: rate={monitor.ema:.3f}, decision={decision(monitor)}")

monitor.update(0, 0)
print(f"after target-only sample: rate={monitor.ema:.3f}, steps={monitor.steps}, decision={decision(monitor)}")

elapsed = 2.0
ntok = 100
draft_len = 5
estimated_accept = min(0.95, max(0.1, ntok / (elapsed * 50)))
cycles = max(1, ntok // draft_len)
print(
    f"throughput heuristic: estimated_accept={estimated_accept:.3f}, "
    f"drafted={draft_len * cycles}, accepted={int(estimated_accept * draft_len * cycles)}"
)
PY

Repository: NVIDIA/TensorRT-LLM

Length of output: 47518


Use TensorRT-LLM speculative metrics for controller updates.

  • Replace the throughput heuristic with request_perf_metrics.speculative_decoding.total_accepted_draft_tokens and total_draft_tokens from the generated output. Enable per-request performance metrics when required.
  • When target-only generation has no speculative metrics, update(0, 0) does not change the monitor. A vanilla decision therefore repeats indefinitely. Add bounded speculative probes or another recovery mechanism before selecting vanilla permanently.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@adaptive-spec-decode/src/adaptive_runner.py` around lines 39 - 63, Update the
generation logic around the LLM calls in the adaptive runner to enable
per-request performance metrics and derive controller updates from the generated
output’s request_perf_metrics.speculative_decoding.total_accepted_draft_tokens
and total_draft_tokens, removing the throughput-based estimated_accept
calculation. Preserve zero metrics for target-only runs, but add bounded
speculative probes or equivalent recovery before a vanilla decision becomes
permanent so the monitor can resume adapting.

Comment on lines +37 to +39
if not self.monitor.is_warmed_up:
return DraftDecision(5, "warmup", 0.3,
f"Warmup phase (step {self.monitor._step_count})")

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Apply draft bounds and record warmup decisions.

Line 38 always selects k=5. This bypasses min_draft and max_draft. If max_draft is less than 5, the controller returns a disallowed draft length.

This early return also omits warmup decisions from _decision_history. The controller report then disagrees with the generation trace. Create a bounded warmup decision, append it, and then return it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@adaptive-spec-decode/src/draft_controller.py` around lines 37 - 39, The
warmup branch in the draft controller must honor configured draft bounds and
record its decision. In the `not self.monitor.is_warmed_up` path, bound the
warmup `k=5` value using the existing `min_draft` and `max_draft`, create the
`DraftDecision`, append it to `_decision_history`, then return that same
decision.

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