Fix --mem-fraction-static not accounting for EAGLE draft model KV cache (#23862)

This commit is contained in:
cctry
2026-06-12 10:35:48 -07:00
committed by GitHub
parent 533b59d00c
commit 75998d0421
22 changed files with 688 additions and 295 deletions
+2
View File
@@ -321,6 +321,8 @@ def load_model(server_args, port_args, gpu_id, tp_rank):
model_runner = MlxModelRunnerStub(**runner_kwargs)
else:
model_runner = ModelRunner(**runner_kwargs)
model_runner.alloc_memory_pool()
model_runner.init_backends()
rank_print(f"max_total_num_tokens={model_runner.max_total_num_tokens}")
tokenizer = get_tokenizer(
server_args.tokenizer_path,
+27 -20
View File
@@ -12,6 +12,7 @@
# limitations under the License.
# ==============================================================================
import copy
import json
import logging
import math
@@ -198,7 +199,11 @@ class ModelConfig:
kwargs = {}
if override_config_file and override_config_file.strip():
kwargs["_configuration_file"] = override_config_file.strip()
self.hf_config = get_config(
# get_config() is cached. ModelConfig mutates hf_config for draft-model
# remapping and architecture-specific normalization, so each instance
# must own an isolated copy.
self.hf_config = copy.deepcopy(
get_config(
self.model_path,
trust_remote_code=trust_remote_code,
revision=revision,
@@ -206,6 +211,7 @@ class ModelConfig:
model_config_parser=model_config_parser,
**kwargs,
)
)
self.hf_text_config = get_hf_text_config(self.hf_config)
self.hf_generation_config = get_generation_config(
self.model_path,
@@ -589,27 +595,28 @@ class ModelConfig:
def _derive_model_shapes(self):
# Unify the config keys for hf_text_config
self.head_dim = getattr(
self.hf_text_config,
"head_dim",
self.hf_text_config.hidden_size // self.hf_text_config.num_attention_heads,
)
self.v_head_dim = getattr(
self.hf_text_config,
"v_head_dim",
self.head_dim,
self.head_dim = getattr(self.hf_text_config, "head_dim", None)
if self.head_dim is None:
self.head_dim = (
self.hf_text_config.hidden_size
// self.hf_text_config.num_attention_heads
)
setattr(self.hf_text_config, "head_dim", self.head_dim)
self.swa_head_dim = getattr(
self.hf_text_config,
"swa_head_dim",
self.head_dim,
)
self.swa_v_head_dim = getattr(
self.hf_text_config,
"swa_v_head_dim",
self.swa_head_dim,
)
self.v_head_dim = getattr(self.hf_text_config, "v_head_dim", None)
if self.v_head_dim is None:
self.v_head_dim = self.head_dim
setattr(self.hf_text_config, "v_head_dim", self.v_head_dim)
self.swa_head_dim = getattr(self.hf_text_config, "swa_head_dim", None)
if self.swa_head_dim is None:
self.swa_head_dim = self.head_dim
setattr(self.hf_text_config, "swa_head_dim", self.swa_head_dim)
self.swa_v_head_dim = getattr(self.hf_text_config, "swa_v_head_dim", None)
if self.swa_v_head_dim is None:
self.swa_v_head_dim = self.swa_head_dim
setattr(self.hf_text_config, "swa_v_head_dim", self.swa_v_head_dim)
# FIXME: temporary special judge for MLA architecture
if (
"DeepseekV2ForCausalLM" in self.hf_config.architectures
+39
View File
@@ -817,10 +817,49 @@ class Scheduler(
else:
self.external_corpus_manager = None
def init_target_memory_pool(self):
"""Allocate target KV cache pools if they have not been allocated yet."""
if (
self.tp_worker.model_runner.memory_pool_config is not None
and self.tp_worker.model_runner.req_to_token_pool is not None
and self.tp_worker.model_runner.token_to_kv_pool_allocator is not None
):
return
self.tp_worker.alloc_memory_pool()
def init_memory_pools(self):
"""Allocate KV cache pools for target and draft workers."""
self.init_target_memory_pool()
if self.draft_worker is not None:
pool, allocator = self.tp_worker.get_memory_pool()
self.draft_worker.alloc_memory_pool(
memory_pool_config=self.tp_worker.model_runner.memory_pool_config,
req_to_token_pool=pool,
token_to_kv_pool_allocator=allocator,
)
def init_all_backends(self):
"""Initialize attention backends and capture cuda graphs for all workers."""
self.tp_worker.init_backends()
if self.draft_worker is not None:
self.draft_worker.init_backends()
def init_model_worker(self):
# Load model weights.
self.init_tp_model_worker()
if self.spec_algorithm.is_frozen_kv_mtp():
# Frozen-KV MTP draft construction needs the target KV pool.
self.init_target_memory_pool()
self.maybe_init_draft_worker()
# Allocate KV cache pools for all workers.
# Memory profiling now sees all loaded weights.
self.init_memory_pools()
# Initialize attention backends and capture cuda graphs.
# TODO: make memory profile consider cuda graph memory as well
self.init_all_backends()
# Dispatch the model worker
if self.spec_algorithm.is_none():
self.model_worker = self.tp_worker
+43 -24
View File
@@ -292,24 +292,6 @@ class TpModelWorker(BaseTpWorker):
self.pp_group = get_pp_group()
self.world_group = get_world_group()
# Profile number of tokens
self.max_total_num_tokens = self.model_runner.max_total_num_tokens
self.max_prefill_tokens = server_args.max_prefill_tokens
self.max_running_requests = self.model_runner.max_running_requests
assert self.max_running_requests > 0, "max_running_request is zero"
self.max_queued_requests = server_args.max_queued_requests
assert (
self.max_queued_requests is None or self.max_queued_requests >= 1
), "If configured, max_queued_requests must be at least 1 for any work to be scheduled."
self.max_req_len = min(
self.model_config.context_len - 1,
self.model_runner.max_token_pool_size - 1,
)
self.max_req_input_len = self.max_req_len - 5
assert (
self.max_req_len > 0 and self.max_req_input_len > 0
), "Memory pool size is too small"
# Sync random seed across TP workers
self.random_seed = broadcast_pyobj(
[server_args.random_seed],
@@ -323,6 +305,39 @@ class TpModelWorker(BaseTpWorker):
self.enable_spec = server_args.speculative_algorithm is not None
self.hicache_layer_transfer_counter = None
def alloc_memory_pool(
self,
memory_pool_config: Optional[MemoryPoolConfig] = None,
req_to_token_pool: Optional[ReqToTokenPool] = None,
token_to_kv_pool_allocator: Optional[BaseTokenToKVPoolAllocator] = None,
):
"""Allocate KV cache pools only (no backends or cuda graphs)."""
if req_to_token_pool is not None:
self.req_to_token_pool = req_to_token_pool
self.model_runner.req_to_token_pool = req_to_token_pool
if token_to_kv_pool_allocator is not None:
self.token_to_kv_pool_allocator = token_to_kv_pool_allocator
self.model_runner.token_to_kv_pool_allocator = token_to_kv_pool_allocator
self.model_runner.alloc_memory_pool(memory_pool_config)
for mr in self.model_runner_list[1:]:
mr.req_to_token_pool = self.req_to_token_pool
mr.token_to_kv_pool_allocator = self.token_to_kv_pool_allocator
mr.alloc_memory_pool(memory_pool_config)
# Validation
assert self.model_runner.max_running_requests > 0, "max_running_request is zero"
max_req_len = min(
self.model_config.context_len - 1,
self.model_runner.max_token_pool_size - 1,
)
assert max_req_len > 0, "Memory pool size is too small"
def init_backends(self, disable_cuda_graph: bool = False):
"""Initialize attention backends and capture cuda graphs."""
self.model_runner.init_backends(disable_cuda_graph=disable_cuda_graph)
for mr in self.model_runner_list[1:]:
mr.init_backends(disable_cuda_graph=disable_cuda_graph)
def _init_model_config(self):
from sglang.srt.configs.model_config import ModelConfig
@@ -414,13 +429,17 @@ class TpModelWorker(BaseTpWorker):
self.model_runner.hisparse_coordinator = coordinator
def get_worker_info(self):
max_req_len = min(
self.model_config.context_len - 1,
self.model_runner.max_token_pool_size - 1,
)
return (
self.max_total_num_tokens,
self.max_prefill_tokens,
self.max_running_requests,
self.max_queued_requests,
self.max_req_len,
self.max_req_input_len,
self.model_runner.max_total_num_tokens,
self.server_args.max_prefill_tokens,
self.model_runner.max_running_requests,
self.server_args.max_queued_requests,
max_req_len,
max_req_len - 5,
self.random_seed,
self.device,
self.model_runner.forward_stream,
@@ -449,21 +449,36 @@ class ModelRunner(ModelRunnerKVCacheMixin):
# auxiliary hidden capture mode. TODO: expose this to server args?
self.eagle_use_aux_hidden_state = False
self.eagle_draft_num_layers = None
self.dflash_use_aux_hidden_state = False
self.dflash_target_layer_ids = None
self.dflash_draft_num_layers = None
if self.spec_algorithm.is_eagle3() and not self.is_draft_worker:
# load draft config
draft_model_config = ModelConfig.from_server_args(
if (
(self.spec_algorithm.is_eagle() or self.spec_algorithm.is_standalone())
and not self.is_draft_worker
and server_args.speculative_draft_model_path
):
# Load draft config to get layer count for KV cache sizing
draft_model_config = self._build_model_config(
server_args,
model_path=(server_args.speculative_draft_model_path),
model_path=server_args.speculative_draft_model_path,
model_revision=server_args.speculative_draft_model_revision,
is_draft_model=True,
)
self.eagle_use_aux_hidden_state = True
num_nextn_predict_layers = draft_model_config.num_nextn_predict_layers
if num_nextn_predict_layers is not None:
self.eagle_draft_num_layers = int(num_nextn_predict_layers)
else:
self.eagle_draft_num_layers = int(
max(
draft_model_config.num_hidden_layers,
draft_model_config.num_attention_layers,
)
)
if self.spec_algorithm.is_eagle3():
self.eagle_use_aux_hidden_state = True
try:
# get the aux layer from draft model config
eagle_config = getattr(
draft_model_config.hf_config, "eagle_config", None
)
@@ -478,12 +493,10 @@ class ModelRunner(ModelRunnerKVCacheMixin):
self.eagle_aux_hidden_state_layer_ids = None
if self.spec_algorithm.is_dflash() and not self.is_draft_worker:
from sglang.srt.speculative.dflash_utils import (
parse_dflash_draft_config,
)
from sglang.srt.speculative.dflash_utils import parse_dflash_draft_config
# Select target layers to capture for building DFlash context features.
draft_model_config = ModelConfig.from_server_args(
draft_model_config = self._build_model_config(
server_args,
model_path=(server_args.speculative_draft_model_path),
model_revision=server_args.speculative_draft_model_revision,
@@ -541,8 +554,9 @@ class ModelRunner(ModelRunnerKVCacheMixin):
if self.device == "cpu":
self.init_threads_binding()
# Get available memory before model loading
pre_model_load_memory = self.init_torch_distributed()
# Get available memory before model loading.
# Stored for later use by alloc_memory_pool().
self.pre_model_load_memory = self.init_torch_distributed()
# Initialize MooncakeTransferEngine
self.init_shared_mooncake_transfer_engine()
@@ -570,8 +584,8 @@ class ModelRunner(ModelRunnerKVCacheMixin):
self._linear_attn_registry_cache: Any = _UNSET
# Initialize the model runner
self.initialize(pre_model_load_memory)
# Load model weights and configure
self.initialize()
self.check_quantized_moe_compatibility()
if (
@@ -603,6 +617,16 @@ class ModelRunner(ModelRunnerKVCacheMixin):
self._model_update_group = {}
self._weights_send_group = {}
def _build_model_config(
self, server_args, model_path=None, model_revision=None, is_draft_model=False
):
return ModelConfig.from_server_args(
server_args,
model_path=model_path,
model_revision=model_revision,
is_draft_model=is_draft_model,
)
def init_msprobe(self):
# Init the msprobe
try:
@@ -632,7 +656,7 @@ class ModelRunner(ModelRunnerKVCacheMixin):
port=self.dist_port,
)
def initialize(self, pre_model_load_memory: float):
def initialize(self):
server_args = self.server_args
self.memory_saver_adapter = TorchMemorySaverAdapter.create(
@@ -783,14 +807,30 @@ class ModelRunner(ModelRunnerKVCacheMixin):
# Deduce KV cache dtype
self.configure_kv_cache_dtype()
# Init memory pool and attention backends
self.init_memory_pool(pre_model_load_memory)
# Snapshot free memory at the end of the weight-load phase. KV-pool
# profiling uses this instead of measuring at alloc_memory_pool()
# time: draft-model weights load between the two phases and must stay
# outside the --mem-fraction-static budget (deployments tune the
# fraction assuming draft weights live in the non-static slack).
self.post_model_load_memory = get_available_gpu_memory(
self.device,
self.gpu_id,
distributed=get_world_group().world_size > 1,
cpu_group=get_world_group().cpu_group,
)
def alloc_memory_pool(self, memory_pool_config: Optional[MemoryPoolConfig] = None):
"""Allocate KV cache memory pools only (no backends or cuda graphs)."""
if memory_pool_config is not None:
self.memory_pool_config = memory_pool_config
self.init_memory_pool(self.pre_model_load_memory)
# Must be called AFTER init_memory_pool so the pool object exists for
# canary to monkey-patch, and BEFORE init_device_graphs so warmup
# canary to monkey-patch, and BEFORE init_decode_cuda_graph so warmup
# forwards captured into the graph see the patched pool methods.
self.canary_manager = install_canary(
server_args=server_args,
server_args=self.server_args,
model_runner=self,
token_oracle_manager=self._token_oracle_manager,
)
@@ -798,18 +838,6 @@ class ModelRunner(ModelRunnerKVCacheMixin):
# Init ngram embedding token table
self.maybe_init_ngram_embedding()
# Init routed experts capturer
self.init_routed_experts_capturer()
self.init_indexer_capturer()
# TODO: Refactor device-specific init branches into platform interface (separate PR).
# Must be called BEFORE init_decode_cuda_graph() so CUDA graph capture
# runs with aux hidden state capture enabled.
self.init_aux_hidden_state_capture()
if self.device == "cuda" or self.device == "musa":
self.init_cublas()
if self.enable_hisparse:
from sglang.srt.managers.hisparse_coordinator import HiSparseCoordinator
from sglang.srt.mem_cache.sparsity import parse_hisparse_config
@@ -831,12 +859,36 @@ class ModelRunner(ModelRunnerKVCacheMixin):
),
host_to_device_ratio=hisparse_cfg.host_to_device_ratio,
)
self.init_routed_experts_capturer()
self.init_indexer_capturer()
self.attn_backend = None
self.decode_attn_backend = None
self.decode_attn_backend_group = []
self.decode_cuda_graph_runner = None
self.graph_mem_usage = 0
self.prefill_cuda_graph_runner = None
def init_backends(self, disable_cuda_graph: bool = False):
"""Initialize attention backends and capture cuda graphs."""
server_args = self.server_args
# TODO: Refactor device-specific init branches into platform interface (separate PR).
# Must be called BEFORE init_decode_cuda_graph() so CUDA graph capture
# runs with aux hidden state capture enabled.
self.init_aux_hidden_state_capture()
if self.device == "cuda" or self.device == "musa":
self.init_cublas()
self.init_attention_backend()
self.kernel_warmup()
self._pre_initialize_flashinfer_allreduce_workspace()
if not disable_cuda_graph:
self.init_decode_cuda_graph()
elif self.device == "cpu":
self.init_attention_backend()
if not disable_cuda_graph:
self.init_decode_cuda_graph()
elif self.device == "npu":
self.init_attention_backend()
@@ -851,10 +903,11 @@ class ModelRunner(ModelRunnerKVCacheMixin):
get_world_group().world_size,
get_world_group().cpu_group,
)
if not disable_cuda_graph:
self.init_decode_cuda_graph()
elif current_platform.is_out_of_tree():
self.init_attention_backend()
if current_platform.support_cuda_graph():
if current_platform.support_cuda_graph() and not disable_cuda_graph:
self.init_decode_cuda_graph()
else:
self.decode_cuda_graph_runner = None
@@ -864,10 +917,13 @@ class ModelRunner(ModelRunnerKVCacheMixin):
self.graph_mem_usage = 0
self.init_attention_backend()
if disable_cuda_graph:
self.decode_cuda_graph_runner = None
self.graph_mem_usage = 0
if server_args.forward_hooks:
register_forward_hooks(self.model, server_args.forward_hooks)
# Initialize piecewise CUDA graph
self.init_prefill_cuda_graph()
self.prealloc_symmetric_memory_pool()
@@ -2407,9 +2463,7 @@ class ModelRunner(ModelRunnerKVCacheMixin):
return
from sglang.srt.layers.communicator import FUSE_ALLREDUCE_MAX_BATCH_SIZE
from sglang.srt.layers.flashinfer_comm_fusion import (
pre_initialize_workspaces,
)
from sglang.srt.layers.flashinfer_comm_fusion import pre_initialize_workspaces
pre_initialize_workspaces(
max_token_num=FUSE_ALLREDUCE_MAX_BATCH_SIZE,
@@ -2706,6 +2760,13 @@ class ModelRunner(ModelRunnerKVCacheMixin):
seq_lens_sum=None,
seq_lens_cpu=None,
)
# MTP models (e.g. deepseek_nextn) read spec_info.hidden_states
# during forward; provide a dummy so warmup doesn't crash.
spec_info.hidden_states = torch.zeros(
(num_tokens, self.model_config.hidden_size),
dtype=self.dtype,
device=self.device,
)
elif self.spec_algorithm.is_dflash():
from sglang.srt.speculative.dflash_info import DFlashVerifyInput
@@ -64,6 +64,11 @@ _is_hip = is_hip()
class ModelRunnerKVCacheMixin:
def _profile_available_bytes(self: ModelRunner, pre_model_load_memory: int) -> int:
# Use the snapshot taken at the end of this runner's weight-load phase,
# not the current free memory: draft-model weights loaded after that
# point are charged to the non-static slack, not the static budget.
post_model_load_memory = getattr(self, "post_model_load_memory", None)
if post_model_load_memory is None:
post_model_load_memory = get_available_gpu_memory(
self.device,
self.gpu_id,
@@ -104,6 +104,24 @@ class DefaultPoolConfigurator(MemoryPoolConfigurator):
self._cell_size = self._compute_cell_size(mr, num_layers)
# EAGLE/STANDALONE: scale cell_size to account for draft model KV cache.
# Assumes draft and target share the same per-layer KV size (head_dim,
# num_kv_heads, dtype), which holds for EAGLE/MTP draft models that
# reuse the target architecture's attention config.
if (
mr.spec_algorithm.is_eagle() or mr.spec_algorithm.is_standalone()
) and not mr.is_draft_worker:
eagle_draft_num_layers = getattr(mr, "eagle_draft_num_layers", None)
if (
eagle_draft_num_layers is not None
and int(eagle_draft_num_layers) > 0
and int(num_layers) > 0
):
self._cell_size = int(
self._cell_size
* (1 + int(eagle_draft_num_layers) / int(num_layers))
)
# DFLASH: scale cell_size to account for draft model KV cache
if mr.spec_algorithm.is_dflash() and not mr.is_draft_worker:
from sglang.srt.speculative.dflash_utils import (
@@ -1118,6 +1118,12 @@ class DecodeCudaGraphRunner(BaseCudaGraphRunner):
seq_lens_sum=None,
seq_lens_cpu=None,
)
# MTP models (e.g. deepseek_nextn) read spec_info.hidden_states
spec_info.hidden_states = torch.zeros(
(num_tokens, self.model_runner.model_config.hidden_size),
dtype=self.model_runner.dtype,
device=self.model_runner.device,
)
elif self.model_runner.spec_algorithm.is_dflash():
from sglang.srt.speculative.dflash_info import DFlashVerifyInput
from sglang.srt.speculative.dflash_utils import (
+4 -3
View File
@@ -89,10 +89,11 @@ class Qwen3_5ForCausalLMMTP(nn.Module):
config.hidden_size, config.rms_norm_eps
)
self.pre_fc_norm_hidden = RMSNorm_cls(config.hidden_size, config.rms_norm_eps)
config.num_hidden_layers = 1
config.full_attention_interval = 1
mtp_config = copy.deepcopy(config)
mtp_config.num_hidden_layers = 1
mtp_config.full_attention_interval = 1
self.model = Qwen3_5ForCausalLM(
config,
mtp_config,
quant_config,
prefix=add_prefix("mtp", prefix),
is_nextn=True,
+4 -3
View File
@@ -69,10 +69,11 @@ class Qwen3NextForCausalLMMTP(Qwen3NextForCausalLM):
config.hidden_size, config.rms_norm_eps
)
self.pre_fc_norm_hidden = RMSNorm_cls(config.hidden_size, config.rms_norm_eps)
config.num_hidden_layers = 1
config.full_attention_interval = 1
mtp_config = copy.deepcopy(config)
mtp_config.num_hidden_layers = 1
mtp_config.full_attention_interval = 1
self.model = Qwen3NextModel(
config,
mtp_config,
quant_config,
prefix=add_prefix("model", prefix),
is_nextn=True,
-8
View File
@@ -1777,14 +1777,6 @@ class ServerArgs:
if gpu_mem is not None and gpu_mem > 60 * 1024:
reserved_mem = max(reserved_mem, 10 * 1024)
if self.speculative_algorithm is not None:
if self.speculative_algorithm == "STANDALONE":
# standalonedraft model and cuda graphs
reserved_mem += 6 * 1024
elif self.speculative_algorithm not in {"NGRAM", "DFLASH"}:
# eagle draft models and cuda graphs
reserved_mem += 4 * 1024
self.mem_fraction_static = (
round((gpu_mem - reserved_mem) / gpu_mem, 3)
if gpu_mem is not None
@@ -16,6 +16,19 @@ class BaseDraftWorker(ABC):
def draft_extend():
pass
def alloc_memory_pool(self, **kwargs):
pass
def init_backends(self):
"""Initialize standard backends (no cuda graphs) then draft-specific backends.
Subclasses should wrap this with their context managers (draft_tp_context,
speculative_moe_backend_context, etc.) rather than reimplementing the logic.
"""
self.draft_worker.init_backends(disable_cuda_graph=True)
self.init_attention_backend()
self.init_cuda_graphs()
class BaseSpecWorker(ABC):
@property
@@ -39,6 +52,12 @@ class BaseSpecWorker(ABC):
# TODO: move this abstract method to BaseTpWorker and call through self.model_runner
pass
def alloc_memory_pool(self, **kwargs):
pass
def init_backends(self):
pass
def on_verify_complete_cpu(
self, num_correct_drafts_per_req: list[int], batch_size: int = 0
) -> None:
@@ -40,7 +40,7 @@ from sglang.srt.speculative.triton_ops.dflash_accept_bonus import (
from sglang.srt.speculative.triton_ops.dflash_prepare_block import (
_prepare_dflash_draft_block_unchecked,
)
from sglang.srt.utils import is_cuda, is_hip, is_npu
from sglang.srt.utils import get_available_gpu_memory, is_cuda, is_hip, is_npu
_is_npu = is_npu()
@@ -102,17 +102,6 @@ class DFlashWorkerV2(BaseSpecWorker):
self._logged_first_verify = False
# Draft runner (separate KV cache + attention backend).
# Without draft windowing, the draft worker aliases the target request->token
# mapping and allocation state. With draft windowing enabled, the draft worker
# keeps a private compact req->token table over the same global KV index space,
# so radix-cache/prefix-hit KV remains reusable while draft attention sees only
# the recent window.
target_req_to_token_pool, target_token_to_kv_pool_allocator = (
target_worker.get_memory_pool()
)
shared_req_to_token_pool = (
None if self.use_compact_draft_cache else target_req_to_token_pool
)
draft_server_args = deepcopy(server_args)
draft_server_args.skip_tokenizer_init = True
draft_backend = draft_server_args.speculative_draft_attention_backend
@@ -168,9 +157,6 @@ class DFlashWorkerV2(BaseSpecWorker):
dp_rank=dp_rank,
nccl_port=nccl_port,
is_draft_worker=True,
req_to_token_pool=shared_req_to_token_pool,
token_to_kv_pool_allocator=target_token_to_kv_pool_allocator,
memory_pool_config=target_worker.model_runner.memory_pool_config,
)
set_global_server_args_for_scheduler(saved_server_args)
self.draft_model_runner = self._draft_worker.model_runner
@@ -288,6 +274,38 @@ class DFlashWorkerV2(BaseSpecWorker):
self.draft_model_runner.attn_backend,
)
def alloc_memory_pool(
self,
memory_pool_config=None,
req_to_token_pool=None,
token_to_kv_pool_allocator=None,
):
# Without draft windowing, the draft worker aliases the target
# request->token mapping and allocation state. With draft windowing
# enabled, the draft worker keeps a private compact req->token table
# over the same global KV index space, so radix-cache/prefix-hit KV
# remains reusable while draft attention sees only the recent window.
self._draft_worker.alloc_memory_pool(
memory_pool_config=memory_pool_config,
req_to_token_pool=(
None if self.use_compact_draft_cache else req_to_token_pool
),
token_to_kv_pool_allocator=token_to_kv_pool_allocator,
)
def init_backends(self):
disable_cuda_graph = False
if is_cuda() and not self.server_args.disable_cuda_graph:
available_mem = get_available_gpu_memory(self.device, self.gpu_id)
disable_cuda_graph = available_mem < 1.0
if disable_cuda_graph:
logger.warning(
"Disable DFLASH draft cuda graph because only %.2f GB GPU "
"memory is available after target backend initialization.",
available_mem,
)
self._draft_worker.init_backends(disable_cuda_graph=disable_cuda_graph)
def _init_fused_kv_helper(self) -> None:
"""Initialize the fused KV materialization helper with pre-stacked weights."""
try:
@@ -165,7 +165,7 @@ class EAGLEDraftExtendCudaGraphRunner(DecodeCudaGraphRunner):
else None
)
self.seq_len_fill_value = (
self.model_runner.attn_backend.get_cuda_graph_seq_len_fill_value()
self.draft_extend_attn_backend.get_cuda_graph_seq_len_fill_value()
)
seq_lens = torch.full(
(self.max_bs,), self.seq_len_fill_value, dtype=torch.int64
@@ -155,18 +155,7 @@ class EagleDraftWorker(BaseDraftWorker):
self._topk1_score_indices_prealloc = None
self._rebuild_topk1_chain_buffers()
# Do not capture cuda graph in `TpModelWorker` init,
# will capture later with init_cuda_graphs()
backup_decode_mode = server_args.cuda_graph_config.decode.backend
server_args.cuda_graph_config.decode.backend = Backend.DISABLED
# Share the allocator with a target worker.
# Draft and target worker own their own KV cache pools.
self.req_to_token_pool, self.token_to_kv_pool_allocator = (
target_worker.get_memory_pool()
)
# Init draft worker
# Load draft model weights only.
if server_args.enable_dp_attention and self.speculative_algorithm.is_eagle3():
ctx = draft_tp_context(get_attention_tp_group())
else:
@@ -174,7 +163,6 @@ class EagleDraftWorker(BaseDraftWorker):
with (
ctx
), speculative_moe_backend_context(), speculative_moe_a2a_backend_context():
# Init draft worker
self.draft_worker = TpModelWorker(
server_args=server_args,
gpu_id=gpu_id,
@@ -186,9 +174,6 @@ class EagleDraftWorker(BaseDraftWorker):
moe_dp_rank=moe_dp_rank,
nccl_port=nccl_port,
is_draft_worker=True,
req_to_token_pool=self.req_to_token_pool,
token_to_kv_pool_allocator=self.token_to_kv_pool_allocator,
memory_pool_config=target_worker.model_runner.memory_pool_config,
)
# Alias for better readability
@@ -201,21 +186,35 @@ class EagleDraftWorker(BaseDraftWorker):
self.eagle_use_aux_hidden_state = eagle_config.get(
"use_aux_hidden_state", True
)
self.init_token_map()
self.init_lm_head()
# Init attention backend and cuda graphs
self.draft_runner.server_args.cuda_graph_config.decode.backend = (
backup_decode_mode
)
self.draft_tp_context = (
draft_tp_context if server_args.enable_dp_attention else empty_context
)
with (
self.draft_tp_context(self.draft_runner.tp_group),
speculative_moe_backend_context(),
speculative_moe_a2a_backend_context(),
self.tree_mask_mode = TreeMaskMode.FULL_MASK
self.plan_stream, self.plan_stream_ctx = _get_plan_stream(self.device)
def alloc_memory_pool(
self,
memory_pool_config=None,
req_to_token_pool=None,
token_to_kv_pool_allocator=None,
):
"""Allocate draft KV cache pools (called by scheduler)."""
self.req_to_token_pool = req_to_token_pool
self.token_to_kv_pool_allocator = token_to_kv_pool_allocator
self.draft_worker.alloc_memory_pool(
memory_pool_config=memory_pool_config,
req_to_token_pool=req_to_token_pool,
token_to_kv_pool_allocator=token_to_kv_pool_allocator,
)
self.init_token_map()
self.init_lm_head()
def init_backends(self):
with self.draft_tp_context(
self.draft_runner.tp_group
), speculative_moe_backend_context(), speculative_moe_a2a_backend_context():
self.draft_worker.init_backends(disable_cuda_graph=True)
self.init_attention_backend()
if check_cuda_graph_backend(Phase.PREFILL, Backend.BREAKABLE):
self.draft_runner.init_prefill_cuda_graph(force_for_draft_worker=True)
@@ -224,10 +223,6 @@ class EagleDraftWorker(BaseDraftWorker):
if (c := self.draft_runner.canary_manager) is not None:
c.mark_init_finished()
self.tree_mask_mode = TreeMaskMode.FULL_MASK
self.plan_stream, self.plan_stream_ctx = _get_plan_stream(self.device)
def _rebuild_topk1_chain_buffers(self) -> None:
# For topk=1 the draft tree degenerates to a chain, so parent_list and
# top_scores_index are runtime-invariant. Must be rebuilt after any
@@ -844,10 +839,6 @@ class EAGLEWorkerV2(BaseSpecWorker):
server_args.speculative_algorithm
)
self.req_to_token_pool, self.token_to_kv_pool_allocator = (
target_worker.get_memory_pool()
)
# 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
@@ -879,7 +870,32 @@ class EAGLEWorkerV2(BaseSpecWorker):
self.plan_stream, self.plan_stream_ctx = _get_plan_stream(self.device)
# Build adaptive runtime states (must be after draft worker is fully initialized)
@property
def spec_v2_attn_backends(self) -> tuple:
# Every attn backend a spec_v2 forward touches; consumed by
# decide_needs_cpu_seq_lens to gate the seq_lens_cpu D2H.
return (
self._target_worker.model_runner.attn_backend,
self._draft_worker.draft_attn_backend,
self._draft_worker.draft_extend_attn_backend
or self._draft_worker.draft_runner.attn_backend,
)
def alloc_memory_pool(
self,
memory_pool_config=None,
req_to_token_pool=None,
token_to_kv_pool_allocator=None,
):
self._draft_worker.alloc_memory_pool(
memory_pool_config, req_to_token_pool, token_to_kv_pool_allocator
)
self.req_to_token_pool = req_to_token_pool
self.token_to_kv_pool_allocator = token_to_kv_pool_allocator
def init_backends(self):
self._draft_worker.init_backends()
# Build adaptive runtime states after target and draft backends exist.
if self.adaptive_controller is not None:
with (
self._draft_worker.draft_tp_context(
@@ -908,16 +924,6 @@ class EAGLEWorkerV2(BaseSpecWorker):
),
)
@property
def spec_v2_attn_backends(self) -> tuple:
# Every attn backend a spec_v2 forward touches; consumed by
# decide_needs_cpu_seq_lens to gate the seq_lens_cpu D2H.
return (
self._target_worker.model_runner.attn_backend,
self._draft_worker.draft_attn_backend,
self._draft_worker.draft_extend_attn_backend,
)
@property
def target_worker(self):
return self._target_worker
@@ -32,10 +32,7 @@ from sglang.srt.layers.moe.utils import (
)
from sglang.srt.managers.schedule_batch import ScheduleBatch
from sglang.srt.managers.tp_worker import TpModelWorker
from sglang.srt.model_executor.cuda_graph_config import (
Backend,
cuda_graph_fully_disabled,
)
from sglang.srt.model_executor.cuda_graph_config import cuda_graph_fully_disabled
from sglang.srt.model_executor.forward_batch_info import (
CaptureHiddenMode,
ForwardBatch,
@@ -117,17 +114,13 @@ class FrozenKVMTPDraftWorker(BaseDraftWorker, TpModelWorker):
f"{self.speculative_algorithm.name}."
)
# Defer cuda graph capture; we do it ourselves below.
backup_decode_mode = server_args.cuda_graph_config.decode.backend
server_args.cuda_graph_config.decode.backend = Backend.DISABLED
# Draft attention uses target req_to_token + KV allocator (read-only).
self.req_to_token_pool, self.token_to_kv_pool_allocator = (
target_worker.get_memory_pool()
)
target_cfg = target_worker.model_runner.memory_pool_config
draft_pool_config = MemoryPoolConfig(
self.draft_pool_config = MemoryPoolConfig(
max_total_num_tokens=64, # Dummy value
max_running_requests=target_cfg.max_running_requests,
)
@@ -153,7 +146,7 @@ class FrozenKVMTPDraftWorker(BaseDraftWorker, TpModelWorker):
is_draft_worker=True,
req_to_token_pool=self.req_to_token_pool,
token_to_kv_pool_allocator=self.token_to_kv_pool_allocator,
memory_pool_config=draft_pool_config,
memory_pool_config=self.draft_pool_config,
)
embed, head = self.target_worker.model_runner.model.get_embed_and_head()
@@ -170,27 +163,49 @@ class FrozenKVMTPDraftWorker(BaseDraftWorker, TpModelWorker):
if hasattr(self.draft_model_runner.model, "bind_frozen_kv_context"):
self._bind_kv_context()
self.draft_model_runner.server_args.cuda_graph_config.decode.backend = (
backup_decode_mode
)
self.draft_tp_context = (
draft_tp_context if server_args.enable_dp_attention else empty_context
)
self.draft_attn_backend = self._init_draft_attn_backend()
self.draft_model_runner.draft_attn_backend = self.draft_attn_backend
self.draft_attn_backend = None
self.cuda_graph_runner = None
# Frozen draft has no draft-extend forward (seed-select only); keep these
# None so inherited probes (spec_v2_attn_backends, adaptive) stay typed.
self.draft_extend_attn_backend = None
self.cuda_graph_runner_for_draft_extend = None
def alloc_memory_pool(
self,
memory_pool_config=None,
req_to_token_pool=None,
token_to_kv_pool_allocator=None,
):
# NOTE: call TpModelWorker explicitly -- BaseDraftWorker precedes it in
# the MRO and its alloc_memory_pool is a no-op stub.
TpModelWorker.alloc_memory_pool(
self,
memory_pool_config=self.draft_pool_config,
req_to_token_pool=(
req_to_token_pool
if req_to_token_pool is not None
else self.req_to_token_pool
),
token_to_kv_pool_allocator=(
token_to_kv_pool_allocator
if token_to_kv_pool_allocator is not None
else self.token_to_kv_pool_allocator
),
)
def init_backends(self):
with (
self.draft_tp_context(self.draft_model_runner.tp_group),
speculative_moe_backend_context(),
speculative_moe_a2a_backend_context(),
):
TpModelWorker.init_backends(self, disable_cuda_graph=True)
self.draft_attn_backend = self._init_draft_attn_backend()
self.draft_model_runner.draft_attn_backend = self.draft_attn_backend
self.init_cuda_graphs()
@property
@@ -135,18 +135,8 @@ class MultiLayerEagleDraftWorker(BaseDraftWorker):
self.speculative_num_steps * self.topk, self.speculative_num_draft_tokens
)
# Do not capture cuda graph in `TpModelWorker` init,
# will capture later with init_cuda_graphs()
backup_decode_mode = server_args.cuda_graph_config.decode.backend
server_args.cuda_graph_config.decode.backend = Backend.DISABLED
# Share the allocator with a target worker.
# Draft and target worker own their own KV cache pools.
self.req_to_token_pool, self.token_to_kv_pool_allocator = (
target_worker.get_memory_pool()
)
# Load draft model weights only.
with empty_context(), speculative_moe_backend_context():
# Init draft worker
self.draft_worker = TpModelWorker(
server_args=server_args,
gpu_id=gpu_id,
@@ -158,9 +148,6 @@ class MultiLayerEagleDraftWorker(BaseDraftWorker):
moe_dp_rank=moe_dp_rank,
nccl_port=nccl_port,
is_draft_worker=True,
req_to_token_pool=self.req_to_token_pool,
token_to_kv_pool_allocator=self.token_to_kv_pool_allocator,
memory_pool_config=target_worker.model_runner.memory_pool_config,
is_multi_layer_eagle=True,
)
@@ -174,7 +161,26 @@ class MultiLayerEagleDraftWorker(BaseDraftWorker):
# next step. Non-chain: each step uses the target model's hidden states.
draft_arch = self.draft_worker.model_config.hf_config.architectures[0]
self.chain_mtp_hidden_states = draft_arch in ["Step3p5MTP"]
self.draft_tp_context = (
draft_tp_context if server_args.enable_dp_attention else empty_context
)
self.tree_mask_mode = TreeMaskMode.FULL_MASK
self.plan_stream, self.plan_stream_ctx = _get_plan_stream(self.device)
def alloc_memory_pool(
self,
memory_pool_config=None,
req_to_token_pool=None,
token_to_kv_pool_allocator=None,
):
"""Allocate draft KV cache pools (called by scheduler)."""
self.req_to_token_pool = req_to_token_pool
self.token_to_kv_pool_allocator = token_to_kv_pool_allocator
self.draft_worker.alloc_memory_pool(
memory_pool_config=memory_pool_config,
req_to_token_pool=req_to_token_pool,
token_to_kv_pool_allocator=token_to_kv_pool_allocator,
)
self.init_lm_head()
# KV cache reversion buffer; sized to mirror req_to_token (indexed by
@@ -189,24 +195,11 @@ class MultiLayerEagleDraftWorker(BaseDraftWorker):
device=self.device,
)
# Init attention backend and cuda graphs
for i in range(self.speculative_num_steps):
self.draft_runner_list[i].server_args.cuda_graph_config.decode.backend = (
backup_decode_mode
)
self.draft_tp_context = (
draft_tp_context if server_args.enable_dp_attention else empty_context
)
with (
self.draft_tp_context(self.draft_runner_list[0].tp_group),
speculative_moe_backend_context(),
):
self.init_attention_backend()
self.init_cuda_graphs()
self.tree_mask_mode = TreeMaskMode.FULL_MASK
self.plan_stream, self.plan_stream_ctx = _get_plan_stream(self.device)
def init_backends(self):
with self.draft_tp_context(
self.draft_runner_list[0].tp_group
), speculative_moe_backend_context():
super().init_backends()
def mtp_model_runner(self, step: int):
return self.draft_runner_list[step]
@@ -230,6 +223,7 @@ class MultiLayerEagleDraftWorker(BaseDraftWorker):
self.draft_extend_attn_backend_list.append(
draft_backend_factory.create_draft_extend_backend()
)
if self.draft_extend_attn_backend_list[-1] is not None:
self.draft_runner_list[step].attn_backend = (
self.draft_extend_attn_backend_list[-1]
)
@@ -685,10 +679,6 @@ class MultiLayerEagleWorkerV2(BaseSpecWorker):
server_args.speculative_algorithm
)
self.req_to_token_pool, self.token_to_kv_pool_allocator = (
target_worker.get_memory_pool()
)
# 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
@@ -712,6 +702,21 @@ class MultiLayerEagleWorkerV2(BaseSpecWorker):
self.plan_stream, self.plan_stream_ctx = _get_plan_stream(self.device)
def alloc_memory_pool(
self,
memory_pool_config=None,
req_to_token_pool=None,
token_to_kv_pool_allocator=None,
):
self._draft_worker.alloc_memory_pool(
memory_pool_config, req_to_token_pool, token_to_kv_pool_allocator
)
self.req_to_token_pool = req_to_token_pool
self.token_to_kv_pool_allocator = token_to_kv_pool_allocator
def init_backends(self):
self._draft_worker.init_backends()
@property
def target_worker(self):
return self._target_worker
@@ -724,7 +729,13 @@ class MultiLayerEagleWorkerV2(BaseSpecWorker):
def spec_v2_attn_backends(self) -> tuple:
return (
self._target_worker.model_runner.attn_backend,
*self._draft_worker.draft_extend_attn_backend_list,
*(
backend or runner.attn_backend
for backend, runner in zip(
self._draft_worker.draft_extend_attn_backend_list,
self._draft_worker.draft_runner_list,
)
),
)
def clear_cache_pool(self):
+10 -7
View File
@@ -34,6 +34,14 @@ USE_FULL_MASK = True
class NGRAMWorker(BaseSpecWorker):
def alloc_memory_pool(self, **kwargs):
# The target memory pool does not exist yet when __init__ runs.
self.req_to_token_pool, self.token_to_kv_pool_allocator = (
self._target_worker.get_memory_pool()
)
self.max_batch_size = self.model_runner.max_running_requests
self._init_preallocated_tensors()
def __init__(
self,
server_args: ServerArgs,
@@ -57,15 +65,10 @@ class NGRAMWorker(BaseSpecWorker):
self.speculative_num_draft_tokens = server_args.speculative_num_draft_tokens
self.topk = server_args.speculative_eagle_topk
self.speculative_num_steps = server_args.speculative_num_steps
self.req_to_token_pool, self.token_to_kv_pool_allocator = (
target_worker.get_memory_pool()
)
self.max_batch_size = target_worker.max_running_requests
# req_to_token_pool / token_to_kv_pool_allocator are set in
# alloc_memory_pool(), after the target pools are allocated.
self.device = f"cuda:{gpu_id}" if gpu_id >= 0 else "cuda"
self._init_preallocated_tensors()
self.adaptive_controller = None
# rids of the last decode batch; used to erase corpus match state for
# requests that left the batch (see forward_batch_generation).
@@ -7,7 +7,6 @@ import torch
from sglang.srt.environ import envs
from sglang.srt.layers.moe.utils import speculative_moe_backend_context
from sglang.srt.managers.tp_worker import TpModelWorker
from sglang.srt.model_executor.cuda_graph_config import Backend
from sglang.srt.server_args import ServerArgs
from sglang.srt.speculative.adaptive_runtime_state import (
AdaptiveController,
@@ -83,18 +82,8 @@ class StandaloneDraftWorker(EagleDraftWorker):
self.speculative_num_steps * self.topk, self.speculative_num_draft_tokens
)
# Do not capture cuda graph in `TpModelWorker` init,
# will capture later with init_cuda_graphs()
backup_decode_mode = server_args.cuda_graph_config.decode.backend
server_args.cuda_graph_config.decode.backend = Backend.DISABLED
# Share the allocator with a target worker.
# Draft and target worker own their own KV cache pools.
self.req_to_token_pool, self.token_to_kv_pool_allocator = (
target_worker.get_memory_pool()
)
# Load draft model weights only.
with empty_context():
# Init draft worker
self.draft_worker = TpModelWorker(
server_args=server_args,
gpu_id=gpu_id,
@@ -106,34 +95,39 @@ class StandaloneDraftWorker(EagleDraftWorker):
moe_dp_rank=moe_dp_rank,
nccl_port=nccl_port,
is_draft_worker=True,
req_to_token_pool=self.req_to_token_pool,
token_to_kv_pool_allocator=self.token_to_kv_pool_allocator,
memory_pool_config=target_worker.model_runner.memory_pool_config,
)
# Alias for better readability
self.draft_runner = self.draft_worker.model_runner
self.init_token_map()
self.init_lm_head()
# Init attention backend and cuda graphs
self.draft_runner.server_args.cuda_graph_config.decode.backend = (
backup_decode_mode
)
self.draft_tp_context = (
draft_tp_context if server_args.enable_dp_attention else empty_context
)
with (
self.draft_tp_context(self.draft_runner.tp_group),
speculative_moe_backend_context(),
):
self.init_attention_backend()
self.init_cuda_graphs()
self.tree_mask_mode = TreeMaskMode.FULL_MASK
self.plan_stream, self.plan_stream_ctx = _get_plan_stream(self.device)
def alloc_memory_pool(
self,
memory_pool_config=None,
req_to_token_pool=None,
token_to_kv_pool_allocator=None,
):
"""Standalone: allocate pools without sharing embeddings."""
self.req_to_token_pool = req_to_token_pool
self.token_to_kv_pool_allocator = token_to_kv_pool_allocator
self.draft_worker.alloc_memory_pool(
memory_pool_config=memory_pool_config,
req_to_token_pool=req_to_token_pool,
token_to_kv_pool_allocator=token_to_kv_pool_allocator,
)
self.init_token_map()
self.init_lm_head()
def init_backends(self):
with self.draft_tp_context(
self.draft_runner.tp_group
), speculative_moe_backend_context():
super().init_backends()
def init_lm_head(self):
"""Override to prevent sharing embeddings and lm_head with target model."""
# For standalone worker, we don't share embeddings and lm_head
@@ -168,10 +162,6 @@ class StandaloneWorkerV2(EAGLEWorkerV2):
server_args.speculative_algorithm
)
self.req_to_token_pool, self.token_to_kv_pool_allocator = (
target_worker.get_memory_pool()
)
# 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
@@ -0,0 +1,71 @@
"""Unit tests for ModelConfig shape normalization."""
import unittest
from types import SimpleNamespace
from sglang.srt.configs.model_config import ModelConfig
from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.test_utils import CustomTestCase
register_cpu_ci(est_time=1, suite="base-a-test-cpu")
def _make_text_config(**overrides):
defaults = dict(
architectures=["MixtralForCausalLM"],
model_type="mixtral",
hidden_size=4096,
num_attention_heads=32,
num_hidden_layers=2,
vocab_size=32000,
num_key_value_heads=8,
)
defaults.update(overrides)
return SimpleNamespace(**defaults)
class TestModelConfigShapes(CustomTestCase):
def _derive_shapes(self, text_config):
model_config = ModelConfig.__new__(ModelConfig)
model_config.hf_config = text_config
model_config.hf_text_config = text_config
model_config._derive_model_shapes()
return model_config
def test_optional_head_dims_default_when_none(self):
text_config = _make_text_config(
head_dim=None,
v_head_dim=None,
swa_head_dim=None,
swa_v_head_dim=None,
)
model_config = self._derive_shapes(text_config)
self.assertEqual(model_config.head_dim, 128)
self.assertEqual(model_config.v_head_dim, 128)
self.assertEqual(model_config.swa_head_dim, 128)
self.assertEqual(model_config.swa_v_head_dim, 128)
self.assertEqual(text_config.head_dim, 128)
self.assertEqual(text_config.v_head_dim, 128)
self.assertEqual(text_config.swa_head_dim, 128)
self.assertEqual(text_config.swa_v_head_dim, 128)
def test_explicit_head_dims_are_preserved(self):
text_config = _make_text_config(
head_dim=128,
v_head_dim=96,
swa_head_dim=64,
swa_v_head_dim=48,
)
model_config = self._derive_shapes(text_config)
self.assertEqual(model_config.head_dim, 128)
self.assertEqual(model_config.v_head_dim, 96)
self.assertEqual(model_config.swa_head_dim, 64)
self.assertEqual(model_config.swa_v_head_dim, 48)
if __name__ == "__main__":
unittest.main()
@@ -83,6 +83,8 @@ def _make_model_runner(
mr.server_args = sa
spec = MagicMock()
spec.is_eagle.return_value = False
spec.is_standalone.return_value = False
spec.is_dflash.return_value = False
spec.is_none.return_value = True
mr.spec_algorithm = spec
@@ -303,6 +305,35 @@ class TestAllSWAConfigurator(unittest.TestCase):
self.assertEqual(config.swa_max_total_num_tokens, 500)
class TestEagleConfigurator(unittest.TestCase):
"""EAGLE: draft KV cache must be accounted for so total allocation fits in budget."""
def test_eagle_does_not_exceed_budget(self):
"""Total memory (target + draft KV cache) must not exceed available."""
available = 10_000_000
num_layers = 32
eagle_draft_num_layers = 4
mr = _make_model_runner(num_layers=num_layers)
mr.spec_algorithm.is_eagle.return_value = True
mr.spec_algorithm.is_standalone.return_value = False
mr.spec_algorithm.is_none.return_value = False
mr.eagle_draft_num_layers = eagle_draft_num_layers
with mock_cpu_env():
from sglang.srt.model_executor.pool_configurator import (
create_memory_pool_configurator,
)
cfg = create_memory_pool_configurator(mr)
config = cfg.calculate_pool_sizes(available, 1)
full_pt = _full_per_token(mr)
total_layers = num_layers + eagle_draft_num_layers
used = config.max_total_num_tokens * full_pt * total_layers
self.assertLessEqual(used, available)
class TestFactory(unittest.TestCase):
def test_default_for_non_swa(self):
mr = _make_model_runner(is_hybrid_swa=False)
@@ -8,11 +8,12 @@ slow path (`organize_draft_results`) for num_steps in {1, 2, 3, 4}.
import unittest
from types import SimpleNamespace
from unittest.mock import patch
import torch
from sglang.srt.speculative.eagle_utils import organize_draft_results
from sglang.srt.speculative.eagle_worker_v2 import EagleDraftWorker
from sglang.srt.speculative.eagle_worker_v2 import EagleDraftWorker, EAGLEWorkerV2
from sglang.srt.utils import get_device
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.test_utils import CustomTestCase
@@ -57,6 +58,20 @@ def _make_worker(num_steps: int, num_draft_tokens: int):
return worker
def _make_backend_factory(decode_backend, draft_extend_backend):
class FakeDraftBackendFactory:
def __init__(self, *args, **kwargs):
pass
def create_decode_backend(self):
return decode_backend
def create_draft_extend_backend(self):
return draft_extend_backend
return FakeDraftBackendFactory
class TestEagleWorkerV2Topk1FastPath(CustomTestCase):
def test_fast_path_matches_slow_path(self):
bs = 3
@@ -93,5 +108,68 @@ class TestEagleWorkerV2Topk1FastPath(CustomTestCase):
worker._rebuild_topk1_chain_buffers()
class TestEagleWorkerV2BackendFallback(CustomTestCase):
def test_preserves_initialized_backend_when_draft_extend_backend_is_unset(self):
worker = object.__new__(EagleDraftWorker)
existing_backend = object()
decode_backend = object()
worker.server_args = SimpleNamespace()
worker.draft_runner = SimpleNamespace(attn_backend=existing_backend)
worker.topk = 1
worker.speculative_num_steps = 2
with patch(
"sglang.srt.speculative.eagle_worker_v2.DraftBackendFactory",
_make_backend_factory(decode_backend, None),
):
worker.init_attention_backend()
self.assertIs(worker.draft_attn_backend, decode_backend)
self.assertIsNone(worker.draft_extend_attn_backend)
self.assertIs(worker.draft_runner.draft_attn_backend, decode_backend)
self.assertIs(worker.draft_runner.attn_backend, existing_backend)
def test_uses_draft_extend_backend_when_available(self):
worker = object.__new__(EagleDraftWorker)
existing_backend = object()
decode_backend = object()
draft_extend_backend = object()
worker.server_args = SimpleNamespace()
worker.draft_runner = SimpleNamespace(attn_backend=existing_backend)
worker.topk = 1
worker.speculative_num_steps = 2
with patch(
"sglang.srt.speculative.eagle_worker_v2.DraftBackendFactory",
_make_backend_factory(decode_backend, draft_extend_backend),
):
worker.init_attention_backend()
self.assertIs(worker.draft_attn_backend, decode_backend)
self.assertIs(worker.draft_extend_attn_backend, draft_extend_backend)
self.assertIs(worker.draft_runner.draft_attn_backend, decode_backend)
self.assertIs(worker.draft_runner.attn_backend, draft_extend_backend)
def test_spec_v2_attn_backends_include_draft_extend_fallback(self):
target_backend = object()
decode_backend = object()
fallback_backend = object()
worker = object.__new__(EAGLEWorkerV2)
worker._target_worker = SimpleNamespace(
model_runner=SimpleNamespace(attn_backend=target_backend)
)
worker._draft_worker = SimpleNamespace(
draft_attn_backend=decode_backend,
draft_extend_attn_backend=None,
draft_runner=SimpleNamespace(attn_backend=fallback_backend),
)
self.assertEqual(
worker.spec_v2_attn_backends,
(target_backend, decode_backend, fallback_backend),
)
if __name__ == "__main__":
unittest.main()