[SPEC][1/N] feat: add adaptive speculative_num_steps for EAGLE topk=1 (#21599)
Co-authored-by: Qiaolin-Yu <liin1211@outlook.com>
This commit is contained in:
@@ -512,7 +512,14 @@ def set_global_graph_memory_pool(val):
|
||||
class CudaGraphRunner:
|
||||
"""A CudaGraphRunner runs the forward pass of a model with cuda graph and torch.compile."""
|
||||
|
||||
def __init__(self, model_runner: ModelRunner):
|
||||
def __init__(
|
||||
self,
|
||||
model_runner: ModelRunner,
|
||||
*,
|
||||
attn_backend=None,
|
||||
speculative_num_steps: Optional[int] = None,
|
||||
speculative_num_draft_tokens: Optional[int] = None,
|
||||
):
|
||||
# Parse args
|
||||
self.model_runner = model_runner
|
||||
self.device = model_runner.device
|
||||
@@ -551,6 +558,17 @@ class CudaGraphRunner:
|
||||
|
||||
self.dllm_config = DllmConfig.from_server_args(model_runner.server_args)
|
||||
self.is_dllm = self.dllm_config is not None
|
||||
self.attn_backend = attn_backend or model_runner.attn_backend
|
||||
self.speculative_num_steps = (
|
||||
model_runner.server_args.speculative_num_steps
|
||||
if speculative_num_steps is None
|
||||
else speculative_num_steps
|
||||
)
|
||||
self.speculative_num_draft_tokens = (
|
||||
model_runner.server_args.speculative_num_draft_tokens
|
||||
if speculative_num_draft_tokens is None
|
||||
else speculative_num_draft_tokens
|
||||
)
|
||||
|
||||
self.capture_forward_mode = ForwardMode.DECODE
|
||||
self.capture_hidden_mode = CaptureHiddenMode.NULL
|
||||
@@ -561,9 +579,7 @@ class CudaGraphRunner:
|
||||
if not self.model_runner.spec_algorithm.is_dflash():
|
||||
raise RuntimeError("This should not happen")
|
||||
self.capture_forward_mode = ForwardMode.TARGET_VERIFY
|
||||
self.num_tokens_per_bs = (
|
||||
self.model_runner.server_args.speculative_num_draft_tokens
|
||||
)
|
||||
self.num_tokens_per_bs = self.speculative_num_draft_tokens
|
||||
elif self.is_dllm:
|
||||
self.capture_forward_mode = ForwardMode.DLLM_EXTEND
|
||||
self.num_tokens_per_bs = self.dllm_config.block_size
|
||||
@@ -583,14 +599,12 @@ class CudaGraphRunner:
|
||||
# Attention backend
|
||||
self.max_bs = max(self.capture_bs)
|
||||
self.max_num_token = self.max_bs * self.num_tokens_per_bs
|
||||
self.model_runner.attn_backend.init_cuda_graph_state(
|
||||
self.max_bs, self.max_num_token
|
||||
)
|
||||
self.attn_backend.init_cuda_graph_state(self.max_bs, self.max_num_token)
|
||||
|
||||
# Init PDMux if needed
|
||||
self.maybe_init_pdmux()
|
||||
self.seq_len_fill_value = (
|
||||
self.model_runner.attn_backend.get_cuda_graph_seq_len_fill_value()
|
||||
self.attn_backend.get_cuda_graph_seq_len_fill_value()
|
||||
if self.dllm_config is None
|
||||
else self.dllm_config.block_size
|
||||
)
|
||||
@@ -964,7 +978,7 @@ class CudaGraphRunner:
|
||||
)
|
||||
|
||||
if stream_idx is None:
|
||||
attn_backend = self.model_runner.attn_backend
|
||||
attn_backend = self.attn_backend
|
||||
else:
|
||||
assert self.enable_pdmux
|
||||
attn_backend = self.model_runner.decode_attn_backend_group[stream_idx]
|
||||
@@ -1170,7 +1184,7 @@ class CudaGraphRunner:
|
||||
stream_idx = get_current_stream_idx()
|
||||
attn_backend = self.model_runner.decode_attn_backend_group[stream_idx]
|
||||
else:
|
||||
attn_backend = self.model_runner.attn_backend
|
||||
attn_backend = self.attn_backend
|
||||
attn_backend.init_forward_metadata_replay_cuda_graph(
|
||||
bs,
|
||||
buffers.req_pool_indices[:bs],
|
||||
@@ -1270,9 +1284,9 @@ class CudaGraphRunner:
|
||||
retrive_next_token=None,
|
||||
retrive_next_sibling=None,
|
||||
retrive_cum_len=None,
|
||||
spec_steps=self.model_runner.server_args.speculative_num_steps,
|
||||
spec_steps=self.speculative_num_steps,
|
||||
topk=self.model_runner.server_args.speculative_eagle_topk,
|
||||
draft_token_num=self.model_runner.server_args.speculative_num_draft_tokens,
|
||||
draft_token_num=self.speculative_num_draft_tokens,
|
||||
capture_hidden_mode=CaptureHiddenMode.FULL,
|
||||
seq_lens_sum=None,
|
||||
seq_lens_cpu=None,
|
||||
|
||||
@@ -509,6 +509,8 @@ class ServerArgs:
|
||||
speculative_moe_runner_backend: Optional[str] = None
|
||||
speculative_moe_a2a_backend: Optional[str] = None
|
||||
speculative_draft_model_quantization: Optional[str] = None
|
||||
speculative_adaptive: bool = False
|
||||
speculative_adaptive_config: Optional[str] = None
|
||||
|
||||
# Speculative decoding (ngram)
|
||||
speculative_ngram_min_bfs_breadth: int = 1
|
||||
@@ -3455,6 +3457,22 @@ class ServerArgs:
|
||||
"Currently ngram speculative decoding does not support dp attention."
|
||||
)
|
||||
|
||||
if self.speculative_adaptive:
|
||||
if self.speculative_algorithm not in ("EAGLE", "EAGLE3"):
|
||||
logger.warning(
|
||||
"speculative_adaptive is only supported with EAGLE/EAGLE3 and topk=1. "
|
||||
f"Current algorithm={self.speculative_algorithm}. "
|
||||
"Falling back to static params."
|
||||
)
|
||||
self.speculative_adaptive = False
|
||||
elif self.speculative_eagle_topk != 1:
|
||||
logger.warning(
|
||||
"speculative_adaptive is only supported with topk=1. "
|
||||
f"Current topk={self.speculative_eagle_topk}. "
|
||||
"Falling back to static params."
|
||||
)
|
||||
self.speculative_adaptive = False
|
||||
|
||||
def _handle_load_format(self):
|
||||
if (
|
||||
self.load_format == "auto" or self.load_format == "gguf"
|
||||
@@ -5290,6 +5308,18 @@ class ServerArgs:
|
||||
default=ServerArgs.speculative_ngram_external_corpus_max_tokens,
|
||||
help="Fail startup if the tokenized external ngram corpus exceeds this many tokens. Tune this based on your CPU memory budget.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--speculative-adaptive",
|
||||
action="store_true",
|
||||
help="Enable adaptive speculative decoding that dynamically adjusts num_steps based on acceptance rate.",
|
||||
default=ServerArgs.speculative_adaptive,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--speculative-adaptive-config",
|
||||
type=str,
|
||||
help="Path to a JSON config file for adaptive speculative decoding tuning knobs ",
|
||||
default=ServerArgs.speculative_adaptive_config,
|
||||
)
|
||||
|
||||
# Multi-layer Eagle speculative decoding
|
||||
parser.add_argument(
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Protocol
|
||||
|
||||
from sglang.srt.speculative.adaptive_spec_params import (
|
||||
AdaptiveSpeculativeParams,
|
||||
load_adaptive_config,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sglang.srt.layers.attention.base_attn_backend import AttentionBackend
|
||||
from sglang.srt.model_executor.cpu_graph_runner import CPUGraphRunner
|
||||
from sglang.srt.model_executor.cuda_graph_runner import CudaGraphRunner
|
||||
from sglang.srt.speculative.eagle_draft_cuda_graph_runner import (
|
||||
EAGLEDraftCudaGraphRunner,
|
||||
)
|
||||
from sglang.srt.speculative.eagle_draft_extend_cuda_graph_runner import (
|
||||
EAGLEDraftExtendCudaGraphRunner,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SpecRuntimeState:
|
||||
"""A complete set of runtime resources bound to a specific speculative
|
||||
decoding configuration.
|
||||
|
||||
Each decode round runs three stages — draft, verify, extend — and every
|
||||
stage has shape-dependent resources (attention backends and CUDA graphs)
|
||||
that must match the current configuration. Switching adaptive steps
|
||||
means swapping the entire state atomically.
|
||||
"""
|
||||
|
||||
# -- Configuration (determines shapes for all stages) --
|
||||
speculative_num_steps: int
|
||||
speculative_num_draft_tokens: int
|
||||
|
||||
# -- Draft stage: draft model multi-step autoregressive generation --
|
||||
draft_attn_backend: "AttentionBackend | None"
|
||||
cuda_graph_runner: "EAGLEDraftCudaGraphRunner | None"
|
||||
|
||||
# -- Verify stage: target model one-pass tree verification --
|
||||
target_attn_backend: "AttentionBackend"
|
||||
target_graph_runner: "CudaGraphRunner | CPUGraphRunner | None"
|
||||
|
||||
# -- Extend stage: draft model KV cache catch-up after verify --
|
||||
draft_extend_attn_backend: "AttentionBackend | None"
|
||||
cuda_graph_runner_for_draft_extend: "EAGLEDraftExtendCudaGraphRunner | None"
|
||||
|
||||
|
||||
class AdaptiveSpecWorker(Protocol):
|
||||
"""Protocol that a worker must implement to use AdaptiveController."""
|
||||
|
||||
speculative_num_steps: int
|
||||
|
||||
def build_adaptive_runtime_state(
|
||||
self, speculative_num_steps: int, speculative_num_draft_tokens: int
|
||||
) -> SpecRuntimeState: ...
|
||||
|
||||
def apply_runtime_state(self, state: SpecRuntimeState) -> None: ...
|
||||
|
||||
|
||||
class AdaptiveController:
|
||||
"""Facade that owns adaptive decision-making and runtime state switching.
|
||||
|
||||
Works with any worker that implements ``AdaptiveSpecWorker`` protocol:
|
||||
- ``build_adaptive_runtime_state(steps, draft_tokens)`` → runtime state
|
||||
- ``apply_runtime_state(state)`` → apply it to the worker
|
||||
|
||||
The worker only needs to:
|
||||
1. Call ``register()`` for the initial state, then ``init_states()``
|
||||
once during startup.
|
||||
2. Call ``on_verify_complete(accept_lengths)`` after each decode verify.
|
||||
"""
|
||||
|
||||
def __init__(self, worker: AdaptiveSpecWorker, config_path: str | None = None):
|
||||
self.worker = worker
|
||||
cfg = load_adaptive_config(config_path)
|
||||
self.params = AdaptiveSpeculativeParams(
|
||||
initial_steps=worker.speculative_num_steps,
|
||||
config=cfg,
|
||||
)
|
||||
self._states: dict[int, SpecRuntimeState] = {}
|
||||
|
||||
@property
|
||||
def candidate_steps(self) -> list[int]:
|
||||
return self.params.candidate_steps
|
||||
|
||||
def register(self, state: SpecRuntimeState, steps: int | None = None) -> None:
|
||||
"""Register a pre-built runtime state.
|
||||
|
||||
*steps* defaults to ``state.speculative_num_steps`` when not given.
|
||||
"""
|
||||
key = steps if steps is not None else state.speculative_num_steps
|
||||
self._states[key] = state
|
||||
|
||||
def init_states(self) -> None:
|
||||
"""Build and register runtime states for all candidate steps."""
|
||||
for steps in self.params.candidate_steps:
|
||||
if steps in self._states:
|
||||
continue
|
||||
state = self.worker.build_adaptive_runtime_state(
|
||||
speculative_num_steps=steps,
|
||||
speculative_num_draft_tokens=steps + 1,
|
||||
)
|
||||
self._states[steps] = state
|
||||
self._activate(self.params.current_steps)
|
||||
|
||||
def on_verify_complete(self, accept_lengths: list[int]) -> None:
|
||||
"""Feed verify results; switch runtime state if EMA warrants it."""
|
||||
if self.params.update(accept_lengths):
|
||||
self._activate(self.params.current_steps)
|
||||
|
||||
def _activate(self, speculative_num_steps: int) -> None:
|
||||
state = self._states.get(speculative_num_steps)
|
||||
if state is None:
|
||||
raise ValueError(
|
||||
f"Missing adaptive runtime state for steps={speculative_num_steps}"
|
||||
)
|
||||
self.worker.apply_runtime_state(state)
|
||||
@@ -0,0 +1,133 @@
|
||||
"""Adaptive speculative decoding parameters.
|
||||
|
||||
Adjusts speculative_num_steps at runtime based on observed acceptance lengths.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def load_adaptive_config(path: str | None) -> dict[str, object]:
|
||||
"""Load adaptive speculative config from a JSON file.
|
||||
|
||||
The file may contain any subset of the following keys:
|
||||
ema_alpha, update_interval, warmup_batches,
|
||||
down_hysteresis, up_hysteresis, candidate_steps
|
||||
|
||||
Returns an empty dict when *path* is ``None``.
|
||||
"""
|
||||
if path is None:
|
||||
return {}
|
||||
with open(path) as f:
|
||||
cfg = json.load(f)
|
||||
if not isinstance(cfg, dict):
|
||||
raise ValueError(
|
||||
"speculative_adaptive_config must be a JSON object, "
|
||||
f"got {type(cfg).__name__}"
|
||||
)
|
||||
return cfg
|
||||
|
||||
|
||||
class AdaptiveSpeculativeParams:
|
||||
"""Tracks acceptance rate via EMA and adapts num_steps accordingly.
|
||||
|
||||
The core idea: if drafts are consistently accepted, try more steps;
|
||||
if drafts are consistently rejected early, reduce steps to avoid waste.
|
||||
|
||||
Formula: target_steps = clamp(round(ema_accept_len) + 1, min_steps, max_steps)
|
||||
- Probes one step beyond observed acceptance
|
||||
- EMA smoothing prevents oscillation
|
||||
- Only updates every `update_interval` batches for stability
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
initial_steps: int,
|
||||
config: dict[str, object] | None = None,
|
||||
):
|
||||
cfg = config or {}
|
||||
# TODO: Wider range of candidate_steps (once lazy init is supported).
|
||||
self.candidate_steps = sorted(set(cfg.get("candidate_steps", [1, 3, 7])))
|
||||
assert (
|
||||
len(self.candidate_steps) >= 2
|
||||
), "candidate_steps must have at least 2 distinct values"
|
||||
|
||||
self.min_steps = self.candidate_steps[0]
|
||||
self.max_steps = self.candidate_steps[-1]
|
||||
self.ema_alpha = cfg.get("ema_alpha", 0.2)
|
||||
self.update_interval = cfg.get("update_interval", 5)
|
||||
self.warmup_batches = cfg.get("warmup_batches", 10)
|
||||
self.down_hysteresis = cfg.get("down_hysteresis", -0.25)
|
||||
self.up_hysteresis = cfg.get("up_hysteresis", 0.0)
|
||||
|
||||
self.current_steps = min(
|
||||
self.candidate_steps,
|
||||
key=lambda step: (abs(step - initial_steps), -step),
|
||||
)
|
||||
|
||||
# Initialize EMA at current steps - 1 (neutral starting point)
|
||||
self.ema_accept_len = float(self.current_steps - 1)
|
||||
self._batch_count = 0
|
||||
|
||||
logger.info(
|
||||
f"AdaptiveSpeculativeParams initialized: "
|
||||
f"steps={self.current_steps}, candidate_steps={self.candidate_steps}"
|
||||
)
|
||||
|
||||
def update(self, accept_lengths: list[int]) -> bool:
|
||||
"""Update EMA with observed accept lengths. Returns True if params changed.
|
||||
|
||||
Args:
|
||||
accept_lengths: Per-request accepted draft token counts from last verify.
|
||||
"""
|
||||
if not accept_lengths:
|
||||
return False
|
||||
|
||||
batch_avg = sum(accept_lengths) / len(accept_lengths)
|
||||
self.ema_accept_len = (
|
||||
1 - self.ema_alpha
|
||||
) * self.ema_accept_len + self.ema_alpha * batch_avg
|
||||
|
||||
self._batch_count += 1
|
||||
if self._batch_count <= self.warmup_batches:
|
||||
return False
|
||||
|
||||
if (self._batch_count - self.warmup_batches) % self.update_interval != 0:
|
||||
return False
|
||||
|
||||
return self._recompute_params()
|
||||
|
||||
def _recompute_params(self) -> bool:
|
||||
"""Recompute steps from EMA. Returns True if params changed."""
|
||||
old_steps = self.current_steps
|
||||
current_idx = self.candidate_steps.index(old_steps)
|
||||
|
||||
# TODO: Consider limiting step changes to avoid overshooting.
|
||||
while current_idx > 0:
|
||||
prev_step = self.candidate_steps[current_idx - 1]
|
||||
drop_threshold = prev_step - 0.5 + self.down_hysteresis
|
||||
if self.ema_accept_len <= drop_threshold:
|
||||
current_idx -= 1
|
||||
else:
|
||||
break
|
||||
|
||||
while current_idx < len(self.candidate_steps) - 1:
|
||||
current_step = self.candidate_steps[current_idx]
|
||||
rise_threshold = current_step - 0.5 + self.up_hysteresis
|
||||
if self.ema_accept_len > rise_threshold:
|
||||
current_idx += 1
|
||||
else:
|
||||
break
|
||||
|
||||
target = self.candidate_steps[current_idx]
|
||||
|
||||
if target != old_steps:
|
||||
self.current_steps = target
|
||||
logger.info(
|
||||
f"Adaptive spec params updated: steps {old_steps} -> {target} "
|
||||
f"(ema_accept_len={self.ema_accept_len:.2f})"
|
||||
)
|
||||
return True
|
||||
return False
|
||||
@@ -54,7 +54,13 @@ class EagleDraftInputBuffers(ForwardInputBuffers):
|
||||
|
||||
|
||||
class EAGLEDraftCudaGraphRunner:
|
||||
def __init__(self, eagle_worker: EAGLEWorker):
|
||||
def __init__(
|
||||
self,
|
||||
eagle_worker: EAGLEWorker,
|
||||
*,
|
||||
draft_attn_backend=None,
|
||||
speculative_num_steps: Optional[int] = None,
|
||||
):
|
||||
# Parse args
|
||||
self.eagle_worker = eagle_worker
|
||||
if not hasattr(eagle_worker, "model_runner"):
|
||||
@@ -72,8 +78,13 @@ class EAGLEDraftCudaGraphRunner:
|
||||
self.require_attn_tp_gather = require_attn_tp_gather(model_runner.server_args)
|
||||
self.tp_size = self.model_runner.tp_size
|
||||
self.dp_size = self.model_runner.dp_size
|
||||
self.speculative_num_steps = model_runner.server_args.speculative_num_steps
|
||||
self.speculative_num_steps = (
|
||||
model_runner.server_args.speculative_num_steps
|
||||
if speculative_num_steps is None
|
||||
else speculative_num_steps
|
||||
)
|
||||
self.topk = model_runner.server_args.speculative_eagle_topk
|
||||
self.draft_attn_backend = draft_attn_backend or model_runner.draft_attn_backend
|
||||
self.enable_profile_cuda_graph = (
|
||||
model_runner.server_args.enable_profile_cuda_graph
|
||||
)
|
||||
@@ -88,10 +99,8 @@ class EAGLEDraftCudaGraphRunner:
|
||||
self.max_bs = max(self.capture_bs)
|
||||
self.max_num_token = self.max_bs * self.num_tokens_per_bs
|
||||
|
||||
self.model_runner.draft_attn_backend.init_cuda_graph_state(
|
||||
self.max_bs, self.max_num_token
|
||||
)
|
||||
self.seq_len_fill_value = self.model_runner.draft_attn_backend.attn_backends[
|
||||
self.draft_attn_backend.init_cuda_graph_state(self.max_bs, self.max_num_token)
|
||||
self.seq_len_fill_value = self.draft_attn_backend.attn_backends[
|
||||
0
|
||||
].get_cuda_graph_seq_len_fill_value()
|
||||
seq_lens_cpu = torch.full(
|
||||
@@ -310,9 +319,7 @@ class EAGLEDraftCudaGraphRunner:
|
||||
)
|
||||
|
||||
# Attention backend
|
||||
self.model_runner.draft_attn_backend.init_forward_metadata_capture_cuda_graph(
|
||||
forward_batch
|
||||
)
|
||||
self.draft_attn_backend.init_forward_metadata_capture_cuda_graph(forward_batch)
|
||||
|
||||
# Run and capture
|
||||
def run_once():
|
||||
@@ -409,7 +416,7 @@ class EAGLEDraftCudaGraphRunner:
|
||||
buffers.seq_lens_cpu[:raw_bs].copy_(forward_batch.seq_lens_cpu)
|
||||
forward_batch.seq_lens_cpu = buffers.seq_lens_cpu[:bs]
|
||||
|
||||
self.model_runner.draft_attn_backend.init_forward_metadata_replay_cuda_graph(
|
||||
self.draft_attn_backend.init_forward_metadata_replay_cuda_graph(
|
||||
forward_batch, bs
|
||||
)
|
||||
self.raw_bs = raw_bs
|
||||
|
||||
@@ -56,7 +56,13 @@ class EagleDraftExtendInputBuffers(ForwardInputBuffers):
|
||||
|
||||
|
||||
class EAGLEDraftExtendCudaGraphRunner:
|
||||
def __init__(self, eagle_worker: EAGLEWorker):
|
||||
def __init__(
|
||||
self,
|
||||
eagle_worker: EAGLEWorker,
|
||||
*,
|
||||
draft_extend_attn_backend=None,
|
||||
speculative_num_steps: Optional[int] = None,
|
||||
):
|
||||
# Parse args
|
||||
self.eagle_worker = eagle_worker
|
||||
if not hasattr(eagle_worker, "model_runner"):
|
||||
@@ -77,8 +83,15 @@ class EAGLEDraftExtendCudaGraphRunner:
|
||||
self.require_attn_tp_gather = require_attn_tp_gather(model_runner.server_args)
|
||||
self.tp_size = self.model_runner.tp_size
|
||||
self.dp_size = self.model_runner.dp_size
|
||||
self.speculative_num_steps = model_runner.server_args.speculative_num_steps
|
||||
self.speculative_num_steps = (
|
||||
model_runner.server_args.speculative_num_steps
|
||||
if speculative_num_steps is None
|
||||
else speculative_num_steps
|
||||
)
|
||||
self.topk = model_runner.server_args.speculative_eagle_topk
|
||||
self.draft_extend_attn_backend = (
|
||||
draft_extend_attn_backend or eagle_worker.draft_extend_attn_backend
|
||||
)
|
||||
self.enable_profile_cuda_graph = (
|
||||
model_runner.server_args.enable_profile_cuda_graph
|
||||
)
|
||||
@@ -93,11 +106,11 @@ class EAGLEDraftExtendCudaGraphRunner:
|
||||
self.max_bs = max(self.capture_bs)
|
||||
self.max_num_token = self.max_bs * self.num_tokens_per_bs
|
||||
|
||||
self.eagle_worker.draft_extend_attn_backend.init_cuda_graph_state(
|
||||
self.draft_extend_attn_backend.init_cuda_graph_state(
|
||||
self.max_bs, self.max_num_token
|
||||
)
|
||||
self.seq_len_fill_value = (
|
||||
self.eagle_worker.draft_extend_attn_backend.get_cuda_graph_seq_len_fill_value()
|
||||
self.draft_extend_attn_backend.get_cuda_graph_seq_len_fill_value()
|
||||
)
|
||||
seq_lens_cpu = torch.full(
|
||||
(self.max_bs,), self.seq_len_fill_value, dtype=torch.int32
|
||||
@@ -362,11 +375,11 @@ class EAGLEDraftExtendCudaGraphRunner:
|
||||
spec_algorithm=self.model_runner.spec_algorithm,
|
||||
spec_info=spec_info,
|
||||
capture_hidden_mode=CaptureHiddenMode.LAST,
|
||||
attn_backend=self.eagle_worker.draft_extend_attn_backend,
|
||||
attn_backend=self.draft_extend_attn_backend,
|
||||
padded_static_len=self.padded_static_len,
|
||||
)
|
||||
|
||||
self.eagle_worker.draft_extend_attn_backend.init_forward_metadata_capture_cuda_graph(
|
||||
self.draft_extend_attn_backend.init_forward_metadata_capture_cuda_graph(
|
||||
bs=bs,
|
||||
num_tokens=num_tokens,
|
||||
req_pool_indices=req_pool_indices,
|
||||
@@ -493,7 +506,7 @@ class EAGLEDraftExtendCudaGraphRunner:
|
||||
forward_batch.spec_info.positions = buffers.positions[:num_tokens]
|
||||
forward_batch.spec_info.accept_length = buffers.accept_length[:bs]
|
||||
|
||||
self.eagle_worker.draft_extend_attn_backend.init_forward_metadata_replay_cuda_graph(
|
||||
self.draft_extend_attn_backend.init_forward_metadata_replay_cuda_graph(
|
||||
bs=bs,
|
||||
req_pool_indices=buffers.req_pool_indices,
|
||||
seq_lens=buffers.seq_lens,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import logging
|
||||
import time
|
||||
from contextlib import contextmanager
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
import torch
|
||||
@@ -24,6 +25,7 @@ from sglang.srt.mem_cache.common import (
|
||||
alloc_token_slots,
|
||||
get_last_loc,
|
||||
)
|
||||
from sglang.srt.model_executor.cuda_graph_runner import CudaGraphRunner
|
||||
from sglang.srt.model_executor.forward_batch_info import (
|
||||
CaptureHiddenMode,
|
||||
ForwardBatch,
|
||||
@@ -32,6 +34,10 @@ from sglang.srt.model_executor.forward_batch_info import (
|
||||
from sglang.srt.observability.req_time_stats import set_time_batch
|
||||
from sglang.srt.observability.trace import get_global_tracing_enabled
|
||||
from sglang.srt.server_args import ServerArgs
|
||||
from sglang.srt.speculative.adaptive_runtime_state import (
|
||||
AdaptiveController,
|
||||
SpecRuntimeState,
|
||||
)
|
||||
from sglang.srt.speculative.draft_utils import DraftBackendFactory
|
||||
from sglang.srt.speculative.eagle_draft_cuda_graph_runner import (
|
||||
EAGLEDraftCudaGraphRunner,
|
||||
@@ -105,6 +111,13 @@ class EAGLEWorker(TpModelWorker):
|
||||
server_args.speculative_algorithm
|
||||
)
|
||||
|
||||
# Adaptive speculative
|
||||
self.adaptive_controller: Optional[AdaptiveController] = None
|
||||
if server_args.speculative_adaptive:
|
||||
self.adaptive_controller = AdaptiveController(
|
||||
self, config_path=server_args.speculative_adaptive_config
|
||||
)
|
||||
|
||||
# Override the context length of the draft model to be the same as the target model.
|
||||
server_args.context_length = target_worker.model_runner.model_config.context_len
|
||||
|
||||
@@ -206,6 +219,20 @@ class EAGLEWorker(TpModelWorker):
|
||||
), speculative_moe_backend_context(), speculative_moe_a2a_backend_context():
|
||||
self.init_attention_backend()
|
||||
self.init_cuda_graphs()
|
||||
if self.adaptive_controller is not None:
|
||||
self.adaptive_controller.register(
|
||||
SpecRuntimeState(
|
||||
speculative_num_steps=self.speculative_num_steps,
|
||||
speculative_num_draft_tokens=self.speculative_num_draft_tokens,
|
||||
draft_attn_backend=self.draft_attn_backend,
|
||||
cuda_graph_runner=self.cuda_graph_runner,
|
||||
target_attn_backend=self.target_worker.model_runner.attn_backend,
|
||||
target_graph_runner=self.target_worker.model_runner.graph_runner,
|
||||
draft_extend_attn_backend=self.draft_extend_attn_backend,
|
||||
cuda_graph_runner_for_draft_extend=self.cuda_graph_runner_for_draft_extend,
|
||||
)
|
||||
)
|
||||
self.adaptive_controller.init_states()
|
||||
|
||||
# Some dummy tensors
|
||||
self.num_new_pages_per_topk = torch.empty(
|
||||
@@ -274,6 +301,130 @@ class EAGLEWorker(TpModelWorker):
|
||||
f"Capture draft extend cuda graph end. Time elapsed: {time.perf_counter() - tic:.2f} s. mem usage={(before_mem - after_mem):.2f} GB. avail mem={after_mem:.2f} GB."
|
||||
)
|
||||
|
||||
def apply_runtime_state(self, state: SpecRuntimeState):
|
||||
"""Apply a pre-built runtime state to this worker."""
|
||||
if self.speculative_num_steps == state.speculative_num_steps:
|
||||
return
|
||||
|
||||
logger.info(
|
||||
"Switch adaptive runtime state: "
|
||||
f"steps {self.speculative_num_steps} -> {state.speculative_num_steps}, "
|
||||
f"draft_tokens {self.speculative_num_draft_tokens} -> "
|
||||
f"{state.speculative_num_draft_tokens}"
|
||||
)
|
||||
|
||||
self.speculative_num_steps = state.speculative_num_steps
|
||||
self.speculative_num_draft_tokens = state.speculative_num_draft_tokens
|
||||
# Draft stage
|
||||
self.draft_attn_backend = state.draft_attn_backend
|
||||
self.draft_model_runner.draft_attn_backend = state.draft_attn_backend
|
||||
self.cuda_graph_runner = state.cuda_graph_runner
|
||||
# Verify stage
|
||||
self.target_worker.model_runner.attn_backend = state.target_attn_backend
|
||||
self.target_worker.model_runner.graph_runner = state.target_graph_runner
|
||||
# Extend stage
|
||||
self.draft_extend_attn_backend = state.draft_extend_attn_backend
|
||||
self.cuda_graph_runner_for_draft_extend = (
|
||||
state.cuda_graph_runner_for_draft_extend
|
||||
)
|
||||
# Sync server_args
|
||||
self.server_args.speculative_num_steps = state.speculative_num_steps
|
||||
self.server_args.speculative_num_draft_tokens = (
|
||||
state.speculative_num_draft_tokens
|
||||
)
|
||||
|
||||
def build_adaptive_runtime_state(
|
||||
self, speculative_num_steps: int, speculative_num_draft_tokens: int
|
||||
) -> SpecRuntimeState:
|
||||
"""Build a SpecRuntimeState for the given step configuration."""
|
||||
tic = time.perf_counter()
|
||||
before_mem = get_available_gpu_memory(self.device, self.gpu_id)
|
||||
|
||||
with self._override_worker_state(
|
||||
speculative_num_steps, speculative_num_draft_tokens
|
||||
):
|
||||
# Reuse existing init methods for draft attention backend and cuda graphs
|
||||
self.init_attention_backend()
|
||||
self.init_cuda_graphs()
|
||||
|
||||
# Capture target attention backend and CUDA graph
|
||||
target_model_runner = self.target_worker.model_runner
|
||||
backup_init = target_model_runner.init_new_workspace
|
||||
try:
|
||||
target_attn_backend = target_model_runner._get_attention_backend(
|
||||
init_new_workspace=True
|
||||
)
|
||||
finally:
|
||||
target_model_runner.init_new_workspace = backup_init
|
||||
|
||||
target_graph_runner = None
|
||||
if not self.server_args.disable_cuda_graph:
|
||||
target_graph_runner = CudaGraphRunner(
|
||||
target_model_runner,
|
||||
attn_backend=target_attn_backend,
|
||||
speculative_num_steps=speculative_num_steps,
|
||||
speculative_num_draft_tokens=speculative_num_draft_tokens,
|
||||
)
|
||||
|
||||
state = SpecRuntimeState(
|
||||
speculative_num_steps=speculative_num_steps,
|
||||
speculative_num_draft_tokens=speculative_num_draft_tokens,
|
||||
# Draft stage
|
||||
draft_attn_backend=self.draft_attn_backend,
|
||||
cuda_graph_runner=self.cuda_graph_runner,
|
||||
# Verify stage
|
||||
target_attn_backend=target_attn_backend,
|
||||
target_graph_runner=target_graph_runner,
|
||||
# Extend stage
|
||||
draft_extend_attn_backend=self.draft_extend_attn_backend,
|
||||
cuda_graph_runner_for_draft_extend=self.cuda_graph_runner_for_draft_extend,
|
||||
)
|
||||
|
||||
after_mem = get_available_gpu_memory(self.device, self.gpu_id)
|
||||
logger.info(
|
||||
f"Built adaptive runtime state steps={speculative_num_steps}: "
|
||||
f"elapsed={time.perf_counter() - tic:.2f}s, "
|
||||
f"mem={(before_mem - after_mem):.2f}GB"
|
||||
)
|
||||
|
||||
return state
|
||||
|
||||
@contextmanager
|
||||
def _override_worker_state(
|
||||
self, speculative_num_steps: int, speculative_num_draft_tokens: int
|
||||
):
|
||||
"""Temporarily override server_args and worker attributes for graph capture."""
|
||||
sa = self.server_args
|
||||
backup = (
|
||||
self.speculative_num_steps,
|
||||
self.speculative_num_draft_tokens,
|
||||
self.draft_attn_backend,
|
||||
self.draft_extend_attn_backend,
|
||||
getattr(self.draft_model_runner, "draft_attn_backend", None),
|
||||
getattr(self, "cuda_graph_runner", None),
|
||||
getattr(self, "cuda_graph_runner_for_draft_extend", None),
|
||||
sa.speculative_num_steps,
|
||||
sa.speculative_num_draft_tokens,
|
||||
)
|
||||
self.speculative_num_steps = speculative_num_steps
|
||||
self.speculative_num_draft_tokens = speculative_num_draft_tokens
|
||||
sa.speculative_num_steps = speculative_num_steps
|
||||
sa.speculative_num_draft_tokens = speculative_num_draft_tokens
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
(
|
||||
self.speculative_num_steps,
|
||||
self.speculative_num_draft_tokens,
|
||||
self.draft_attn_backend,
|
||||
self.draft_extend_attn_backend,
|
||||
self.draft_model_runner.draft_attn_backend,
|
||||
self.cuda_graph_runner,
|
||||
self.cuda_graph_runner_for_draft_extend,
|
||||
sa.speculative_num_steps,
|
||||
sa.speculative_num_draft_tokens,
|
||||
) = backup
|
||||
|
||||
@property
|
||||
def draft_model_runner(self):
|
||||
return self.model_runner
|
||||
@@ -353,6 +504,10 @@ class EAGLEWorker(TpModelWorker):
|
||||
batch.reqs, "set_spec_draft_extend_end_time", trace_only=True
|
||||
)
|
||||
|
||||
controller = getattr(self, "adaptive_controller", None)
|
||||
if controller is not None:
|
||||
controller.on_verify_complete(verify_output.accept_length_per_req_cpu)
|
||||
|
||||
return GenerationBatchResult(
|
||||
logits_output=logits_output,
|
||||
next_token_ids=verify_output.verified_id,
|
||||
@@ -634,7 +789,7 @@ class EAGLEWorker(TpModelWorker):
|
||||
retrive_cum_len=None,
|
||||
spec_steps=self.speculative_num_steps,
|
||||
topk=self.topk,
|
||||
draft_token_num=self.server_args.speculative_num_draft_tokens,
|
||||
draft_token_num=self.speculative_num_draft_tokens,
|
||||
capture_hidden_mode=CaptureHiddenMode.FULL,
|
||||
seq_lens_sum=forward_batch.seq_lens_sum,
|
||||
seq_lens_cpu=forward_batch.seq_lens_cpu,
|
||||
@@ -944,7 +1099,7 @@ class EAGLEWorker(TpModelWorker):
|
||||
seq_lens_backup = batch.seq_lens.clone()
|
||||
seq_lens_cpu_backup = batch.seq_lens_cpu.clone()
|
||||
req_pool_indices_backup = batch.req_pool_indices
|
||||
accept_length_backup = batch.spec_info.accept_length
|
||||
accept_length_backup = batch.spec_info.accept_length.clone()
|
||||
return_logprob_backup = batch.return_logprob
|
||||
|
||||
input_is_idle = batch.forward_mode.is_idle()
|
||||
@@ -1006,9 +1161,12 @@ class EAGLEWorker(TpModelWorker):
|
||||
else:
|
||||
forward_batch.can_run_dp_cuda_graph = False
|
||||
if not forward_batch.forward_mode.is_idle():
|
||||
self.draft_model_runner.attn_backend.init_forward_metadata(
|
||||
forward_batch
|
||||
attn_backend = (
|
||||
self.draft_extend_attn_backend
|
||||
or self.draft_model_runner.attn_backend
|
||||
)
|
||||
attn_backend.init_forward_metadata(forward_batch)
|
||||
forward_batch.attn_backend = attn_backend
|
||||
logits_output = self.draft_model_runner.forward(
|
||||
forward_batch, skip_attn_backend_init=True
|
||||
).logits_output
|
||||
|
||||
Reference in New Issue
Block a user