[HiCache] refactor: hicache normalization flow and compatibility checks (#19669)
This commit is contained in:
@@ -728,6 +728,10 @@ class HiCacheController:
|
||||
return host_indices, device_indices.index_select(0, idx)
|
||||
elif self.mem_pool_host.layout == "page_first_direct":
|
||||
return host_indices, device_indices.cpu()
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unsupported layout {self.mem_pool_host.layout!r} for io backend 'direct'"
|
||||
)
|
||||
elif self.io_backend == "kernel_ascend":
|
||||
return host_indices, device_indices.cpu()
|
||||
else:
|
||||
|
||||
@@ -58,13 +58,6 @@ class HiRadixCache(RadixCache):
|
||||
|
||||
def __init__(self, params: CacheInitParams, server_args: ServerArgs):
|
||||
self._enable_metrics_flag = params.enable_metrics
|
||||
if server_args.hicache_io_backend == "direct":
|
||||
# FIXME: move this logic into server_args parsing
|
||||
if server_args.hicache_mem_layout == "page_first":
|
||||
server_args.hicache_mem_layout = "page_first_direct"
|
||||
logger.warning(
|
||||
"Page first layout is not supported with direct IO backend, switching to page first direct layout"
|
||||
)
|
||||
|
||||
if not server_args.disable_hicache_numa_detect:
|
||||
bind_to_closest_numa_node_cuda()
|
||||
|
||||
@@ -2786,56 +2786,103 @@ class ServerArgs:
|
||||
)
|
||||
|
||||
def _handle_hicache(self):
|
||||
"""Normalize hicache-related knobs into a valid runtime configuration.
|
||||
|
||||
Resolution order:
|
||||
1) Layout <-> I/O compatibility for direct conflicts.
|
||||
2) Storage <-> layout compatibility (may rewrite layout).
|
||||
3) I/O <-> decode-attention compatibility (may rewrite I/O or decode backend).
|
||||
4) Re-run step (1) if step (3) changed I/O backend.
|
||||
"""
|
||||
# Skip all normalization when neither hicache nor decode-offload path is active.
|
||||
if not (
|
||||
self.enable_hierarchical_cache
|
||||
or self.disaggregation_decode_enable_offload_kvcache
|
||||
):
|
||||
return
|
||||
|
||||
# Step 1: Initial layout-io compatibility normalization.
|
||||
self._resolve_layout_io_compatibility()
|
||||
|
||||
# Step 2: Storage-layout normalization without changing io backend.
|
||||
self._resolve_storage_layout_compatibility()
|
||||
|
||||
# Step 3: IO-decode backend compatibility (may change io backend).
|
||||
io_changed = self._resolve_io_decode_attention_compatibility()
|
||||
|
||||
# Step 4: Re-normalize layout after io backend changes.
|
||||
if io_changed:
|
||||
self._resolve_layout_io_compatibility()
|
||||
|
||||
def _resolve_layout_io_compatibility(self):
|
||||
if (
|
||||
self.hicache_mem_layout == "page_first_direct"
|
||||
and self.hicache_io_backend == "kernel"
|
||||
):
|
||||
self.hicache_io_backend = "direct"
|
||||
logger.warning(
|
||||
"Kernel io backend does not support page first direct layout"
|
||||
"Kernel io backend does not support page first direct layout, switching to direct io backend"
|
||||
)
|
||||
|
||||
if (
|
||||
self.enable_hierarchical_cache
|
||||
or self.disaggregation_decode_enable_offload_kvcache
|
||||
) and self.hicache_io_backend == "kernel":
|
||||
# fix for the compatibility issue with FlashAttention3 decoding and HiCache kernel backend
|
||||
# Only override when the *effective* decode backend would be FA3.
|
||||
# Otherwise, respect the user's chosen attention backend (e.g., aiter on ROCm).
|
||||
effective_decode_backend = (
|
||||
self.decode_attention_backend
|
||||
if self.decode_attention_backend is not None
|
||||
else self.attention_backend
|
||||
self.hicache_mem_layout == "page_first"
|
||||
and self.hicache_io_backend == "direct"
|
||||
):
|
||||
self.hicache_mem_layout = "page_first_direct"
|
||||
logger.warning(
|
||||
"Page first layout is not supported with direct IO backend, switching to page first direct layout"
|
||||
)
|
||||
if effective_decode_backend == "fa3":
|
||||
if self.decode_attention_backend is None:
|
||||
# If decode backend wasn't explicitly set, pick a safe default that works with HiCache kernel IO.
|
||||
if not self.use_mla_backend():
|
||||
self.decode_attention_backend = (
|
||||
"flashinfer" if is_flashinfer_available() else "triton"
|
||||
)
|
||||
else:
|
||||
self.decode_attention_backend = (
|
||||
"flashinfer" if is_sm100_supported() else "triton"
|
||||
)
|
||||
else:
|
||||
# If user explicitly requested FA3 decode, fall back to direct IO.
|
||||
self.hicache_io_backend = "direct"
|
||||
logger.warning(
|
||||
"FlashAttention3 decode backend is not compatible with hierarchical cache. "
|
||||
"Setting hicache_io_backend to vanilla I/O, which may lead to suboptimal performance with small page sizes."
|
||||
)
|
||||
|
||||
if self.hicache_storage_backend == "mooncake":
|
||||
if self.hicache_mem_layout == "layer_first":
|
||||
if self.hicache_io_backend == "direct":
|
||||
self.hicache_mem_layout = "page_first_direct"
|
||||
elif self.hicache_io_backend == "kernel":
|
||||
self.hicache_mem_layout = "page_first"
|
||||
logger.warning(
|
||||
f"Mooncake storage backend does not support layer_first layout, "
|
||||
f"switching to {self.hicache_mem_layout} layout for {self.hicache_io_backend} io backend"
|
||||
)
|
||||
def _resolve_storage_layout_compatibility(self):
|
||||
if (
|
||||
self.hicache_storage_backend != "mooncake"
|
||||
or self.hicache_mem_layout != "layer_first"
|
||||
):
|
||||
return
|
||||
|
||||
if self.hicache_io_backend == "direct":
|
||||
new_layout = "page_first_direct"
|
||||
elif self.hicache_io_backend == "kernel":
|
||||
new_layout = "page_first"
|
||||
else:
|
||||
# Keep current behavior for unknown backends (e.g., kernel_ascend).
|
||||
new_layout = self.hicache_mem_layout
|
||||
|
||||
self.hicache_mem_layout = new_layout
|
||||
logger.warning(
|
||||
f"Mooncake storage backend does not support layer_first layout, "
|
||||
f"switching to {new_layout} layout for {self.hicache_io_backend} io backend"
|
||||
)
|
||||
|
||||
def _resolve_io_decode_attention_compatibility(self) -> bool:
|
||||
if self.hicache_io_backend != "kernel":
|
||||
return False
|
||||
|
||||
# Only patch settings when the effective decode backend is FA3.
|
||||
effective_decode_backend = (
|
||||
self.decode_attention_backend or self.attention_backend
|
||||
)
|
||||
if effective_decode_backend != "fa3":
|
||||
return False
|
||||
|
||||
if self.decode_attention_backend is not None:
|
||||
self.hicache_io_backend = "direct"
|
||||
logger.warning(
|
||||
"FlashAttention3 decode backend is not compatible with hierarchical cache. "
|
||||
"Setting hicache_io_backend to vanilla I/O, which may lead to suboptimal performance with small page sizes."
|
||||
)
|
||||
return True
|
||||
|
||||
# If decode backend is implicit, pick a safe backend without changing io backend.
|
||||
if not self.use_mla_backend():
|
||||
self.decode_attention_backend = (
|
||||
"flashinfer" if is_flashinfer_available() else "triton"
|
||||
)
|
||||
else:
|
||||
self.decode_attention_backend = (
|
||||
"flashinfer" if is_sm100_supported() else "triton"
|
||||
)
|
||||
return False
|
||||
|
||||
def _handle_speculative_decoding(self):
|
||||
if (
|
||||
|
||||
@@ -332,5 +332,99 @@ class TestSSLArgs(unittest.TestCase):
|
||||
self.assertTrue(server_args.enable_ssl_refresh)
|
||||
|
||||
|
||||
class TestHiCacheArgs(unittest.TestCase):
|
||||
def _make_args(self, **overrides) -> ServerArgs:
|
||||
args = ServerArgs(model_path="dummy")
|
||||
for key, value in overrides.items():
|
||||
setattr(args, key, value)
|
||||
return args
|
||||
|
||||
def _assert_hicache_fields(
|
||||
self,
|
||||
args: ServerArgs,
|
||||
*,
|
||||
expected_io_backend: str,
|
||||
expected_mem_layout: str,
|
||||
expected_decode_backend: str | None = None,
|
||||
):
|
||||
self.assertEqual(args.hicache_io_backend, expected_io_backend)
|
||||
self.assertEqual(args.hicache_mem_layout, expected_mem_layout)
|
||||
if expected_decode_backend is not None:
|
||||
self.assertEqual(args.decode_attention_backend, expected_decode_backend)
|
||||
|
||||
def test_hicache_io_backend_and_mem_layout_compatibility(self):
|
||||
cases = [
|
||||
{
|
||||
"name": "kernel_with_page_first_direct",
|
||||
"overrides": {
|
||||
"enable_hierarchical_cache": True,
|
||||
"hicache_io_backend": "kernel",
|
||||
"hicache_mem_layout": "page_first_direct",
|
||||
},
|
||||
"expected_io_backend": "direct",
|
||||
"expected_mem_layout": "page_first_direct",
|
||||
},
|
||||
{
|
||||
"name": "direct_with_page_first",
|
||||
"overrides": {
|
||||
"enable_hierarchical_cache": True,
|
||||
"hicache_io_backend": "direct",
|
||||
"hicache_mem_layout": "page_first",
|
||||
},
|
||||
"expected_io_backend": "direct",
|
||||
"expected_mem_layout": "page_first_direct",
|
||||
},
|
||||
{
|
||||
"name": "mooncake_with_layer_first",
|
||||
"overrides": {
|
||||
"enable_hierarchical_cache": True,
|
||||
"hicache_storage_backend": "mooncake",
|
||||
"hicache_io_backend": "direct",
|
||||
"hicache_mem_layout": "layer_first",
|
||||
},
|
||||
"expected_io_backend": "direct",
|
||||
"expected_mem_layout": "page_first_direct",
|
||||
},
|
||||
{
|
||||
"name": "fa3_kernel_with_explicit_decode_backend",
|
||||
"overrides": {
|
||||
"enable_hierarchical_cache": True,
|
||||
"hicache_io_backend": "kernel",
|
||||
"hicache_mem_layout": "page_first",
|
||||
"attention_backend": "triton",
|
||||
"decode_attention_backend": "fa3",
|
||||
},
|
||||
"expected_io_backend": "direct",
|
||||
"expected_mem_layout": "page_first_direct",
|
||||
},
|
||||
]
|
||||
|
||||
for case in cases:
|
||||
with self.subTest(case=case["name"]):
|
||||
args = self._make_args(**case["overrides"])
|
||||
args._handle_hicache()
|
||||
self._assert_hicache_fields(
|
||||
args,
|
||||
expected_io_backend=case["expected_io_backend"],
|
||||
expected_mem_layout=case["expected_mem_layout"],
|
||||
)
|
||||
|
||||
@patch.object(ServerArgs, "use_mla_backend", return_value=False)
|
||||
@patch("sglang.srt.server_args.is_flashinfer_available", return_value=False)
|
||||
def test_decode_attention_backend_with_implicit_fa3(
|
||||
self, _mock_flashinfer, _mock_use_mla_backend
|
||||
):
|
||||
args = self._make_args(
|
||||
enable_hierarchical_cache=True,
|
||||
hicache_io_backend="kernel",
|
||||
attention_backend="fa3",
|
||||
decode_attention_backend=None,
|
||||
)
|
||||
|
||||
args._handle_hicache()
|
||||
|
||||
self.assertEqual(args.decode_attention_backend, "triton")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user