[kv canary] Support UnifiedRadixCache in kv-canary and bracket nested model.forward (#30574)

This commit is contained in:
Lianmin Zheng
2026-07-10 10:58:55 -07:00
committed by GitHub
parent 3dc93a12ca
commit 7998fecfd1
5 changed files with 184 additions and 22 deletions
+15 -8
View File
@@ -102,15 +102,22 @@ def install_canary(
def _patch_model_forward(*, model_runner: ModelRunner, manager: CanaryManager) -> None:
def _with_canary_bracketing(original: Callable, *args: Any, **kwargs: Any) -> Any:
forward_batch = _extract_forward_batch(args, kwargs)
assert (
forward_batch is not None
), "kv-canary: patched model.forward called without a ForwardBatch"
with manager.model_forward_bracket_scope() as should_bracket:
if not should_bracket:
# Nested model.forward calls share the active SingleForwardManager.
# Only the outermost call may run kv-canary pre/post ops; otherwise
# the phase checker sees a second pre-op before the first post-op.
return original(*args, **kwargs)
canary_pre_ops_output = manager.pre_ops_maybe_inside_graph(forward_batch)
output = original(*args, **kwargs)
manager.post_ops_maybe_inside_graph(forward_batch, canary_pre_ops_output)
return output
forward_batch = _extract_forward_batch(args, kwargs)
assert (
forward_batch is not None
), "kv-canary: patched model.forward called without a ForwardBatch"
canary_pre_ops_output = manager.pre_ops_maybe_inside_graph(forward_batch)
output = original(*args, **kwargs)
manager.post_ops_maybe_inside_graph(forward_batch, canary_pre_ops_output)
return output
wrap_method(model_runner.model, "forward", wrapper=_with_canary_bracketing)
@@ -1,16 +1,22 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any
import torch
from sglang.srt.mem_cache.radix_cache import RadixCache
from sglang.srt.mem_cache.swa_radix_cache import SWARadixCache
from sglang.srt.mem_cache.unified_cache_components import (
BASE_COMPONENT_TYPE,
ComponentType,
)
from sglang.srt.mem_cache.unified_radix_cache import UnifiedRadixCache
if TYPE_CHECKING:
from sglang.srt.mem_cache.base_prefix_cache import BasePrefixCache
from sglang.srt.mem_cache.radix_cache import TreeNode
from sglang.srt.mem_cache.unified_radix_cache import UnifiedTreeNode
@dataclass(frozen=True, slots=True, kw_only=True)
@@ -34,7 +40,11 @@ def walk_radix_cache_for_canary(
req. ``swa_resident_only=True`` skips SWA-tombstoned nodes (slots evicted from the SWA
window)."""
cache_type = type(radix_cache)
if cache_type is not RadixCache and cache_type is not SWARadixCache:
if (
cache_type is not RadixCache
and cache_type is not SWARadixCache
and cache_type is not UnifiedRadixCache
):
raise NotImplementedError(
f"walk_radix_cache_for_canary does not support {cache_type.__name__}"
)
@@ -68,7 +78,7 @@ def walk_radix_cache_for_canary(
def _walk_radix_subtree(
*,
node: TreeNode,
node: TreeNode | UnifiedTreeNode,
radix_cache: BasePrefixCache,
depth: int,
parent_last_slot: int,
@@ -79,10 +89,7 @@ def _walk_radix_subtree(
unlocked_only: bool,
swa_resident_only: bool,
) -> None:
if isinstance(node.value, torch.Tensor):
node_slots = [int(s) for s in node.value.tolist()]
else:
node_slots = []
node_slots = _node_slots_for_canary(node=node, radix_cache=radix_cache)
if unlocked_only:
emit_slots = not is_root and _node_is_unlocked_for_canary(
@@ -105,7 +112,12 @@ def _walk_radix_subtree(
prev_slot_buf.append(prev)
chain_last_slot = slot
child_depth = depth + len(node_slots)
child_depth = depth + _node_len_for_canary(
node=node,
radix_cache=radix_cache,
node_slots=node_slots,
is_root=is_root,
)
for child in node.children.values():
_walk_radix_subtree(
node=child,
@@ -121,9 +133,40 @@ def _walk_radix_subtree(
)
def _node_slots_for_canary(
*,
node: TreeNode | UnifiedTreeNode,
radix_cache: BasePrefixCache,
) -> list[int]:
value: Any
if type(radix_cache) is UnifiedRadixCache:
value = node.component_data[BASE_COMPONENT_TYPE].value
else:
value = node.value
if isinstance(value, torch.Tensor):
return [int(s) for s in value.tolist()]
return []
def _node_len_for_canary(
*,
node: TreeNode | UnifiedTreeNode,
radix_cache: BasePrefixCache,
node_slots: list[int],
is_root: bool,
) -> int:
if type(radix_cache) is not UnifiedRadixCache:
return len(node_slots)
if is_root or node.key is None:
return len(node_slots)
return len(node.key)
def _node_is_unlocked_for_canary(
*,
node: TreeNode,
node: TreeNode | UnifiedTreeNode,
radix_cache: BasePrefixCache,
) -> bool:
if type(radix_cache) is RadixCache:
@@ -132,6 +175,9 @@ def _node_is_unlocked_for_canary(
if type(radix_cache) is SWARadixCache:
return node.full_lock_ref == 0
if type(radix_cache) is UnifiedRadixCache:
return node.component_data[BASE_COMPONENT_TYPE].lock_ref == 0
raise NotImplementedError(
f"walk_radix_cache_for_canary does not support {type(radix_cache).__name__}"
)
@@ -139,10 +185,15 @@ def _node_is_unlocked_for_canary(
def _node_is_swa_resident_for_canary(
*,
node: TreeNode,
node: TreeNode | UnifiedTreeNode,
radix_cache: BasePrefixCache,
) -> bool:
if type(radix_cache) is not SWARadixCache:
return True
if type(radix_cache) is SWARadixCache:
return not node.swa_tombstone
return not node.swa_tombstone
if type(radix_cache) is UnifiedRadixCache:
if not radix_cache.supports_swa():
return True
return node.component_data[ComponentType.SWA].value is not None
return True
@@ -61,6 +61,7 @@ class CanaryManager:
self._swa_allocator: Optional[SWATokenToKVPoolAllocator] = swa_allocator
self._outer_step_counter: int = 0
self._active_single_forward_manager_index: Optional[int] = None
self._model_forward_bracket_depth: int = 0
self._buffer_groups: tuple[CanaryBufferGroup, ...] = tuple(buffer_groups)
@@ -182,6 +183,22 @@ class CanaryManager:
)
self._active_single_forward_manager_index = None
@contextlib.contextmanager
def model_forward_bracket_scope(self) -> Iterator[bool]:
"""Return whether this is the outermost patched ``model.forward`` call.
Some model implementations enter another patched forward from inside the
top-level forward (for example, a vision-language model calling its inner
language model). Kv-canary owns one pre/post bracket per active
SingleForwardManager; nested brackets would run a second pre-op while the
phase checker is already in the first bracket.
"""
self._model_forward_bracket_depth += 1
try:
yield self._model_forward_bracket_depth == 1
finally:
self._model_forward_bracket_depth -= 1
def pre_ops_maybe_inside_graph(
self, forward_batch: ForwardBatch
) -> _PreOpsMaybeInsideGraphOutput:
@@ -264,9 +264,19 @@ class BaseRunner(ABC):
Supplied by warmup() (the decode runner's captured buffers when a graph
runner exists; a freshly-allocated dummy set in the eager path).
"""
mr = self.model_runner
canary_run_ctx = (
c.with_active_single_forward_manager(0)
if (c := mr.canary_manager) is not None
else empty_context()
)
def forward_fn():
self._dummy_run(batch_size=batch_size, buffers=buffers)
self._dummy_run(
batch_size=batch_size,
buffers=buffers,
run_ctx=canary_run_ctx,
)
run_flashinfer_autotune_forward(self.model_runner, forward_fn, skip_logits=True)