Release CP draft-hidden buffers after EAGLE target prefill

CP draft shared-KV target prefill only needs the CP-local draft hidden side channel until draft prefill consumes it.  The reused ModelWorkerBatch was left with capture_draft_hidden_states enabled, and LogitsProcessorOutput retained the extra draft_hidden_states tensor past that point.  Restore the capture flags after target prefill, disable draft-hidden capture for draft prefill, and drop the consumed reference.

Constraint: Large CP prefill batches are memory-sensitive; an extra hidden tensor held across the speculative step materially increases peak memory.

Rejected: Leave capture_draft_hidden_states sticky across draft prefill | it can capture or retain hidden state outside the target-side CP-local side channel.

Confidence: high

Scope-risk: narrow

Directive: Keep CP-local draft hidden as a target-prefill-only side channel unless the draft worker explicitly owns a new lifetime contract.

Tested: g0034 cjy-glm5-new PYTHONPATH=python python -m pytest -q test/registered/unit/mem_cache/test_cp_shared_kv_runtime.py::TestCpSharedKVRuntimeHelpers::test_token_slot_remap_cache_distinguishes_same_storage_views test/registered/unit/mem_cache/test_cp_shared_kv_runtime.py::TestCpSharedKVRuntimeHelpers::test_paged_slot_remap_cache_distinguishes_same_storage_views test/registered/unit/speculative/test_eagle_worker_v2_cp_hidden.py

Not-tested: full speculative integration/E2E workload.
(cherry picked from commit f31ef2293ce0cdda2359a5f4713996d78b47f9df)
This commit is contained in:
laoyao0822
2026-06-22 01:05:49 +08:00
committed by leavelet
parent 1d7149df1c
commit f0c6b892b6
2 changed files with 208 additions and 14 deletions

View File

@@ -734,17 +734,38 @@ class EAGLEWorkerV2(BaseSpecWorker):
# Target prefill. Keep the target hidden side channel CP-local when CP
# draft shared-KV is enabled (NULL capture + draft-hidden flag), exactly
# like the legacy worker; otherwise capture the global FULL hidden.
if self._can_use_cp_draft_shared_kv(model_worker_batch):
model_worker_batch.capture_hidden_mode = CaptureHiddenMode.NULL
model_worker_batch.capture_draft_hidden_states = True
else:
model_worker_batch.capture_hidden_mode = CaptureHiddenMode.FULL
batch_output = self.target_worker.forward_batch_generation(
can_use_cp_draft_shared_kv = self._can_use_cp_draft_shared_kv(
model_worker_batch
)
if can_use_cp_draft_shared_kv:
old_capture_hidden_mode = model_worker_batch.capture_hidden_mode
old_capture_draft_hidden_states = (
model_worker_batch.capture_draft_hidden_states
)
model_worker_batch.capture_hidden_mode = CaptureHiddenMode.NULL
model_worker_batch.capture_draft_hidden_states = True
try:
batch_output = self.target_worker.forward_batch_generation(
model_worker_batch
)
finally:
# This side channel is target-prefill-only. Do not let the
# reused ModelWorkerBatch make the draft prefill capture an
# additional hidden tensor and hold it live for the rest of the
# speculative step.
model_worker_batch.capture_hidden_mode = old_capture_hidden_mode
model_worker_batch.capture_draft_hidden_states = (
old_capture_draft_hidden_states
)
else:
model_worker_batch.capture_hidden_mode = CaptureHiddenMode.FULL
batch_output = self.target_worker.forward_batch_generation(
model_worker_batch
)
# Draft prefill
model_worker_batch.capture_hidden_mode = CaptureHiddenMode.LAST
model_worker_batch.capture_draft_hidden_states = False
with self.draft_worker.draft_tp_context(
self.draft_worker.draft_runner.tp_group
), speculative_moe_backend_context(), speculative_moe_a2a_backend_context():
@@ -755,15 +776,24 @@ class EAGLEWorkerV2(BaseSpecWorker):
lo = batch_output.logits_output
cp_local = lo.draft_hidden_states is not None
draft_hidden = lo.draft_hidden_states if cp_local else lo.hidden_states
batch_output.next_draft_input = (
self.draft_worker._draft_extend_for_prefill(
model_worker_batch,
draft_hidden,
batch_output.next_token_ids,
lo.mm_input_embeds,
cp_local_hidden_states=cp_local,
try:
batch_output.next_draft_input = (
self.draft_worker._draft_extend_for_prefill(
model_worker_batch,
draft_hidden,
batch_output.next_token_ids,
lo.mm_input_embeds,
cp_local_hidden_states=cp_local,
)
)
)
finally:
if cp_local:
# The CP-local hidden is consumed by draft prefill above.
# Drop the extra LogitsProcessorOutput reference so the
# caching allocator can reuse this target-side buffer
# before the next large prefill.
lo.draft_hidden_states = None
draft_hidden = None
return batch_output
else:
if model_worker_batch.spec_info is None:

View File

@@ -0,0 +1,164 @@
import contextlib
import ast
from enum import IntEnum
from pathlib import Path
from types import SimpleNamespace
import torch
class CaptureHiddenMode(IntEnum):
NULL = 0
LAST = 1
FULL = 2
class _EnabledEnv:
def get(self):
return True
def _load_eagle_worker_v2_under_test():
source_path = (
Path(__file__).parents[4]
/ "python"
/ "sglang"
/ "srt"
/ "speculative"
/ "eagle_worker_v2.py"
)
module = ast.parse(source_path.read_text())
class_node = next(
node
for node in module.body
if isinstance(node, ast.ClassDef) and node.name == "EAGLEWorkerV2"
)
wanted = {
"_can_use_cp_draft_shared_kv",
"forward_batch_generation",
}
methods = [
node
for node in class_node.body
if isinstance(node, ast.FunctionDef) and node.name in wanted
]
test_class = ast.ClassDef(
name="_EAGLEWorkerV2UnderTest",
bases=[],
keywords=[],
body=methods,
decorator_list=[],
)
test_module = ast.Module(body=[test_class], type_ignores=[])
ast.fix_missing_locations(test_module)
namespace = {}
exec(
compile(test_module, str(source_path), "exec"),
{
"CaptureHiddenMode": CaptureHiddenMode,
"ModelWorkerBatch": object,
"envs": SimpleNamespace(SGLANG_CP_DRAFT_SHARED_KV=_EnabledEnv()),
"speculative_moe_backend_context": contextlib.nullcontext,
"speculative_moe_a2a_backend_context": contextlib.nullcontext,
},
namespace,
)
return namespace["_EAGLEWorkerV2UnderTest"]
class _ExtendMode:
def is_extend(self):
return True
class _TargetWorker:
def __init__(self, logits_output):
self.model_runner = SimpleNamespace(uses_cp_shared_kv=True)
self._logits_output = logits_output
self.capture_state_seen = None
def forward_batch_generation(self, model_worker_batch):
self.capture_state_seen = (
model_worker_batch.capture_hidden_mode,
model_worker_batch.capture_draft_hidden_states,
)
return SimpleNamespace(
logits_output=self._logits_output,
next_token_ids=torch.tensor([1], dtype=torch.int64),
)
class _DraftWorker:
def __init__(self):
self.draft_runner = SimpleNamespace(
tp_group=object(),
uses_cp_shared_kv=True,
model_config=SimpleNamespace(
hf_config=SimpleNamespace(
architectures=["DeepseekV3ForCausalLMNextN"],
)
),
)
self.capture_state_seen = None
self.prefill_args = None
def draft_tp_context(self, _tp_group):
return contextlib.nullcontext()
def _draft_extend_for_prefill(
self,
model_worker_batch,
draft_hidden,
next_token_ids,
mm_input_embeds,
*,
cp_local_hidden_states,
):
self.capture_state_seen = (
model_worker_batch.capture_hidden_mode,
model_worker_batch.capture_draft_hidden_states,
)
self.prefill_args = SimpleNamespace(
draft_hidden=draft_hidden,
cp_local_hidden_states=cp_local_hidden_states,
)
return "next-draft-input"
def test_cp_draft_hidden_capture_is_target_only_and_released():
draft_hidden = torch.randn(2, 4)
logits_output = SimpleNamespace(
draft_hidden_states=draft_hidden,
hidden_states=torch.randn(4, 4),
mm_input_embeds=None,
)
target_worker = _TargetWorker(logits_output)
draft_worker = _DraftWorker()
worker_cls = _load_eagle_worker_v2_under_test()
worker = worker_cls.__new__(worker_cls)
worker.target_worker = target_worker
worker.draft_worker = draft_worker
batch = SimpleNamespace(
forward_mode=_ExtendMode(),
is_extend_in_batch=False,
spec_info=None,
capture_hidden_mode=None,
capture_draft_hidden_states=False,
)
output = worker.forward_batch_generation(batch)
assert target_worker.capture_state_seen == (
CaptureHiddenMode.NULL,
True,
)
assert draft_worker.capture_state_seen == (
CaptureHiddenMode.LAST,
False,
)
assert draft_worker.prefill_args.cp_local_hidden_states is True
assert draft_worker.prefill_args.draft_hidden is draft_hidden
assert output.next_draft_input == "next-draft-input"
assert logits_output.draft_hidden_states is None
assert batch.capture_draft_hidden_states is False