Stabilize EAGLE draft cache hits under CP HiCache

The failing runs showed EAGLE accept length collapsing when draft cache-hit suffixes used the new partial-current splice path.  This keeps target partial-current reuse enabled, but returns EAGLE/NextN draft cache-hit suffixes to the previous full-materialize path with an explicit fallback warning until the draft splice path has value-level ETE proof.\n\nThe same change set also tightens the page-granular CP HiCache contract for scheduler-visible hits and makes the prefill-to-decode EAGLE handoff observable without cloning hot-path metadata.  Exact non-page CP hits are floored to a page boundary for new scheduling decisions, while internal unfinished-request refresh keeps its exact accounting.\n\nConstraint: CP shared KV and HiCache operate at page granularity; exposing token-precise CP tails to scheduler-visible cache hits can force non-page partial materialization.\nConstraint: EAGLE/NextN draft has only one executable layer, so draft prefetch and draft partial-current splice need a separate correctness contract from target layers.\nRejected: Keep draft partial-current splice enabled | remote logs correlate it with avg accept length around 0.068 and median 0.\nRejected: Clone decode metadata tensors on transfer | slot ownership until process_prebuilt consumes them avoids extra hot-path copies.\nConfidence: medium\nScope-risk: moderate\nDirective: Do not re-enable draft partial-current reuse without metadata/draft-KV value checks and ETE accept-length evidence.\nTested: g0034 container py_compile for touched modules.\nTested: g0034 container PYTHONPATH=python python -m pytest -q test/registered/unit/disaggregation/test_decode_queue_compaction.py test/registered/unit/mem_cache/test_cp_hicache_metadata.py test/registered/unit/mem_cache/test_cp_shared_kv_runtime.py -> 183 passed, 5 warnings, 2 subtests passed.\nNot-tested: Fresh ETE accept-length run after this exact commit; requires user-driven traffic restart.
This commit is contained in:
laoyao0822
2026-05-30 22:31:43 +08:00
parent 10296a5fef
commit b328baec7c
14 changed files with 920 additions and 21 deletions
@@ -42,6 +42,8 @@ from sglang.srt.disaggregation.utils import (
ReqToMetadataIdxAllocator,
TransferBackend,
append_cp_draft_state_buffers,
eagle_accept_debug_should_log,
eagle_accept_debug_tensor_digest,
get_kv_class,
is_mla_backend,
poll_and_all_reduce,
@@ -1172,6 +1174,25 @@ class DecodeTransferQueue:
return True
# Case 3: Success - commit the transfer
if (
not self.spec_algorithm.is_none()
and eagle_accept_debug_should_log("metadata_get")
):
logger.warning(
"[EAGLE_ACCEPT_DEBUG][metadata_get] rid=%s room=%s idx=%s "
"output_id=%s cached_tokens=%s actual_room=%s topk_p=%s "
"topk_index=%s hidden=%s",
str(getattr(decode_req.req, "rid", ""))[:8],
decode_req.req.bootstrap_room,
idx,
int(output_id[0].item()),
int(cached_tokens[0].item()),
int(actual_room),
eagle_accept_debug_tensor_digest(output_topk_p[:1]),
eagle_accept_debug_tensor_digest(output_topk_index[:1]),
eagle_accept_debug_tensor_digest(output_hidden_states),
)
decode_req.req.output_ids.append(output_id[0].item())
decode_req.req.cached_tokens = cached_tokens[0].item()
if not self.spec_algorithm.is_none():
@@ -6,10 +6,16 @@ from typing import TYPE_CHECKING
import torch
from sglang.srt.disaggregation.utils import (
eagle_accept_debug_should_log,
eagle_accept_debug_tensor_digest,
)
from sglang.srt.environ import envs
from sglang.srt.model_executor.forward_batch_info import CaptureHiddenMode, ForwardMode
from sglang.srt.sampling.sampling_batch_info import SamplingBatchInfo
logger = logging.getLogger(__name__)
_EAGLE_ACCEPT_PREBUILT_DEBUG_COUNTER = 0
if TYPE_CHECKING:
from sglang.srt.managers.overlap_utils import FutureMap
@@ -42,6 +48,7 @@ class ScheduleBatchDisaggregationDecodeMixin:
for i, req in enumerate(reqs):
req_pool_indices.append(req.req_pool_idx)
pre_len = len(req.prefix_indices)
chunk = self.req_to_token_pool.req_to_token[req.req_pool_idx][
: req.extend_input_len
]
@@ -51,7 +58,6 @@ class ScheduleBatchDisaggregationDecodeMixin:
out_cache_loc[offset : offset + req.extend_input_len] = chunk
offset += req.extend_input_len
pre_len = len(req.prefix_indices)
seq_len = len(req.origin_input_ids) + max(0, len(req.output_ids) - 1)
seq_lens.append(seq_len)
if len(req.output_ids) == 0:
@@ -66,6 +72,35 @@ class ScheduleBatchDisaggregationDecodeMixin:
pre_lens.append(pre_len)
req.extend_logprob_start_len = 0
if envs.SGLANG_EAGLE_ACCEPT_DEBUG.get():
global _EAGLE_ACCEPT_PREBUILT_DEBUG_COUNTER
_EAGLE_ACCEPT_PREBUILT_DEBUG_COUNTER += 1
counter = _EAGLE_ACCEPT_PREBUILT_DEBUG_COUNTER
if (
counter <= 16
or counter % 256 == 0
or (pre_len > 0 and (counter <= 128 or counter % 128 == 0))
):
logger.warning(
"[EAGLE_ACCEPT_DEBUG][prebuilt_prepare] rid=%s "
"pre_len=%s extend_input_len=%s fill_len=%s origin_len=%s "
"output_len=%s seq_len=%s cached_tokens=%s "
"req_pool_idx=%s out_chunk_start=%s expected_suffix_start=%s "
"out_chunk_len=%s",
str(getattr(req, "rid", ""))[:8],
pre_len,
req.extend_input_len,
len(req.fill_ids),
len(req.origin_input_ids),
len(req.output_ids),
seq_len,
int(getattr(req, "cached_tokens", 0) or 0),
req.req_pool_idx,
0,
pre_len,
int(chunk.numel()),
)
extend_input_logprob_token_ids = None
# Set fields
@@ -161,6 +196,29 @@ class ScheduleBatchDisaggregationDecodeMixin:
hidden_states_list = [req.hidden_states_tensor for req in self.reqs]
hidden_states = torch.stack(hidden_states_list, dim=0).to(self.device)
if eagle_accept_debug_should_log("prebuilt_state"):
req0 = self.reqs[0] if self.reqs else None
logger.warning(
"[EAGLE_ACCEPT_DEBUG][prebuilt_state] rid=%s bs=%s "
"num_states=%s output_ids=%s seq_lens=%s topk_p=%s "
"topk_index=%s hidden=%s",
str(getattr(req0, "rid", ""))[:8] if req0 is not None else None,
len(self.reqs),
num_states,
self.output_ids[: min(4, self.output_ids.numel())].detach()
.cpu()
.tolist(),
self.seq_lens[: min(4, self.seq_lens.numel())].detach()
.cpu()
.tolist(),
eagle_accept_debug_tensor_digest(topk_p[: min(1, topk_p.shape[0])]),
eagle_accept_debug_tensor_digest(
topk_index[: min(1, topk_index.shape[0])]
),
eagle_accept_debug_tensor_digest(
hidden_states[: min(1, hidden_states.shape[0])]
),
)
# local import to avoid circular import
from sglang.srt.speculative.eagle_info import EagleDraftInput
+81
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
import os
import random
import logging
from collections import deque
from contextlib import nullcontext
from enum import Enum
@@ -14,6 +15,8 @@ import torch.distributed as dist
from sglang.srt.environ import envs
from sglang.srt.utils import is_npu
logger = logging.getLogger(__name__)
if TYPE_CHECKING:
from sglang.srt.disaggregation.base.conn import KVArgs
from sglang.srt.disaggregation.common.conn import (
@@ -84,6 +87,63 @@ def poll_and_all_reduce_attn_cp_tp_group(
# Metadata Buffers
#########################
_EAGLE_ACCEPT_DEBUG_COUNTERS = {}
def eagle_accept_debug_should_log(
key: str,
*,
first: int = 16,
every: int = 256,
) -> bool:
if not envs.SGLANG_EAGLE_ACCEPT_DEBUG.get():
return False
count = _EAGLE_ACCEPT_DEBUG_COUNTERS.get(key, 0) + 1
_EAGLE_ACCEPT_DEBUG_COUNTERS[key] = count
return count <= first or (every > 0 and count % every == 0)
def eagle_accept_debug_tensor_digest(tensor: Any, *, sample: int = 64) -> str:
"""Small deterministic tensor summary for EAGLE handoff debugging.
This intentionally samples only a prefix. It is enabled only under
SGLANG_EAGLE_ACCEPT_DEBUG and is for handoff equality checks, not for full
numerical validation.
"""
if tensor is None:
return "None"
try:
t = torch.as_tensor(tensor)
except Exception as exc: # pragma: no cover - defensive debug helper
return f"unavailable({type(exc).__name__})"
shape = tuple(t.shape)
dtype = str(t.dtype).replace("torch.", "")
numel = int(t.numel())
if numel == 0:
return f"shape={shape} dtype={dtype} numel=0"
flat = t.detach().reshape(-1)
sample_count = min(sample, numel)
try:
sample_cpu = flat[:sample_count].cpu()
head_cpu = sample_cpu[: min(8, sample_count)]
head = head_cpu.tolist()
if sample_cpu.is_floating_point():
checksum = float(sample_cpu.float().sum().item())
abs_checksum = float(sample_cpu.float().abs().sum().item())
return (
f"shape={shape} dtype={dtype} sample={sample_count} "
f"sum={checksum:.6g} abs={abs_checksum:.6g} head={head}"
)
checksum = int(sample_cpu.to(torch.int64).sum().item())
return (
f"shape={shape} dtype={dtype} sample={sample_count} "
f"sum={checksum} head={head}"
)
except Exception as exc: # pragma: no cover - defensive debug helper
return f"shape={shape} dtype={dtype} numel={numel} digest_error={type(exc).__name__}"
def append_cp_draft_state_buffers(
kv_args: Any,
@@ -307,6 +367,27 @@ class MetadataBuffers:
self.output_hidden_states[req.metadata_buffer_index].copy_(
req.hidden_states_tensor
)
if eagle_accept_debug_should_log("metadata_set"):
logger.warning(
"[EAGLE_ACCEPT_DEBUG][metadata_set] rid=%s room=%s idx=%s "
"output_id=%s cached_tokens=%s topk=%s topk_p=%s "
"topk_index=%s hidden=%s",
str(getattr(req, "rid", ""))[:8],
getattr(req, "bootstrap_room", None),
req.metadata_buffer_index,
req.output_ids[0] if req.output_ids else None,
req.cached_tokens,
topk,
eagle_accept_debug_tensor_digest(
self.output_topk_p[req.metadata_buffer_index, :topk]
),
eagle_accept_debug_tensor_digest(
self.output_topk_index[req.metadata_buffer_index, :topk]
),
eagle_accept_debug_tensor_digest(
self.output_hidden_states[req.metadata_buffer_index]
),
)
# Store bootstrap_room for validation on decode side
self.bootstrap_room[req.metadata_buffer_index, 0] = (
req.bootstrap_room if req.bootstrap_room is not None else 0