[AMD][DSV4] feat: enable DSpark with fp8 unified_kv on gfx950 (#38901)
Co-authored-by: HAI <hixiao@gmail.com>
This commit is contained in:
@@ -78,6 +78,19 @@ def get_swa_ring_size(sliding_window: int, is_speculative: bool = False) -> int:
|
||||
return sliding_window + spec_extra
|
||||
|
||||
|
||||
def resolve_unified_kv_fp8(unified_fp8: Optional[bool] = None) -> bool:
|
||||
"""Per-pool fp8 layout. None follows SGLANG_DSV4_UNIFIED_KV_FP8.
|
||||
|
||||
A caller may pass False so this pool keeps the bf16 ring while the env
|
||||
stays on (target fused-Q still keys off the global switch).
|
||||
"""
|
||||
from sglang.kernels.ops.attention.dsv4.unified_kv_kernels.env_gate import (
|
||||
is_unified_kv_fp8,
|
||||
)
|
||||
|
||||
return is_unified_kv_fp8() if unified_fp8 is None else bool(unified_fp8)
|
||||
|
||||
|
||||
def _num_dsv4_physical_kv_pages(
|
||||
size: int, physical_page_size: int, logical_page_size: int
|
||||
) -> int:
|
||||
@@ -867,6 +880,7 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
|
||||
enable_hisparse: bool = False,
|
||||
online_mtp_max_draft_tokens: int = 0,
|
||||
num_req_slots: Optional[int] = None,
|
||||
unified_fp8: Optional[bool] = None,
|
||||
kv_source_layers: Sequence[int] = (),
|
||||
full_size: Optional[int] = None,
|
||||
is_draft_worker: bool = False,
|
||||
@@ -893,13 +907,6 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
|
||||
self.compressed_kv_layout_option = compressed_kv_layout
|
||||
c4_logical_size = c128_size * 32
|
||||
|
||||
logger.info(
|
||||
"Initialize DeepSeekV4TokenToKVPool with "
|
||||
f"{max_num_reqs=} {swa_size=} {c4_size=} "
|
||||
f"{c4_logical_size=} {c128_size=} "
|
||||
f"{c4_state_pool_size=} {c128_state_pool_size=}"
|
||||
)
|
||||
|
||||
self.max_num_reqs = max_num_reqs
|
||||
# PD preallocation can exceed max_num_reqs;
|
||||
# the SWA ring must cover every addressable req_pool_idx.
|
||||
@@ -915,7 +922,19 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
|
||||
|
||||
# Resolve the unified-kv gate before any sizing so the two cannot drift.
|
||||
self._unified_kv = is_unified_kv_triton()
|
||||
self._unified_kv_fp8 = is_unified_kv_fp8()
|
||||
self._unified_kv_fp8 = resolve_unified_kv_fp8(unified_fp8)
|
||||
logger.info(
|
||||
"Initialize DeepSeekV4TokenToKVPool with "
|
||||
f"{max_num_reqs=} {swa_size=} {c4_size=} "
|
||||
f"{c4_logical_size=} {c128_size=} "
|
||||
f"{c4_state_pool_size=} {c128_state_pool_size=} "
|
||||
f"unified={self._unified_kv} unified_fp8={self._unified_kv_fp8}"
|
||||
)
|
||||
if is_unified_kv_fp8() and not self._unified_kv_fp8:
|
||||
logger.info(
|
||||
"SGLANG_DSV4_UNIFIED_KV_FP8 is on; this pool stays bf16 "
|
||||
"(unified_fp8=False)"
|
||||
)
|
||||
# Uniform 512-dim e4m3 layout for the trtllm attention backend
|
||||
self.uniform_fp8 = (
|
||||
not self._unified_kv
|
||||
|
||||
@@ -133,6 +133,16 @@ def _get_dsv4_compress_state_dtypes() -> tuple[torch.dtype, torch.dtype]:
|
||||
_is_npu = is_npu()
|
||||
|
||||
|
||||
def unified_fp8_for_dsv4_pool(*, is_draft_worker: bool, spec_algorithm) -> bool:
|
||||
"""Per-pool fp8 layout. DSpark draft writers scatter bf16, so that pool
|
||||
stays a bf16 ring; MTP/EAGLE NextN follows the env."""
|
||||
from sglang.kernels.ops.attention.dsv4.unified_kv_kernels.env_gate import (
|
||||
is_unified_kv_fp8,
|
||||
)
|
||||
|
||||
return is_unified_kv_fp8() and not (is_draft_worker and spec_algorithm.is_dspark())
|
||||
|
||||
|
||||
def _should_enable_lazy_compaction() -> bool:
|
||||
"""Lazy compaction default — ON unless
|
||||
`SGLANG_DISABLE_LAZY_COMPACTION=1` (escape hatch for A/B / rollback).
|
||||
@@ -1365,6 +1375,11 @@ class KVCacheConfigurator:
|
||||
kv_layout=kv_layout, compressed_kv_layout=compressed_kv_layout
|
||||
)
|
||||
|
||||
unified_fp8 = unified_fp8_for_dsv4_pool(
|
||||
is_draft_worker=self.is_draft_worker,
|
||||
spec_algorithm=self.spec_algorithm,
|
||||
)
|
||||
|
||||
token_to_kv_pool = pool_cls(
|
||||
max_num_reqs=max_running_requests,
|
||||
# SWA ring is indexed by req_pool_idx; PD decode inflates req_to_token
|
||||
@@ -1392,6 +1407,7 @@ class KVCacheConfigurator:
|
||||
end_layer=self.layer_info.end_layer,
|
||||
enable_hisparse=get_memory().enable_hisparse,
|
||||
online_mtp_max_draft_tokens=(max_speculative_num_draft_tokens() or 0),
|
||||
unified_fp8=unified_fp8,
|
||||
kv_source_layers=kv_source_layers,
|
||||
full_size=full_max_total_num_tokens,
|
||||
**({"is_draft_worker": self.is_draft_worker} if not _is_npu else {}),
|
||||
|
||||
@@ -968,6 +968,9 @@ class DSV4PoolConfigurator(MemoryPoolConfigurator):
|
||||
is the request-scoped fixed pools that do not scale with full_token.
|
||||
"""
|
||||
|
||||
# object.__new__ stubs (SWA floor tests) skip __init__
|
||||
_dspark_draft_on_bf16 = False
|
||||
|
||||
def __init__(self, kvc: KVCacheConfigurator):
|
||||
self.kv_cache_dtype_str = kvc.kv_cache_dtype_str
|
||||
cfg = kvc.model_config
|
||||
@@ -986,6 +989,11 @@ class DSV4PoolConfigurator(MemoryPoolConfigurator):
|
||||
# Resolve the unified-kv gate before any sizing so the two cannot drift.
|
||||
self._unified = is_unified_kv_triton()
|
||||
self._unified_fp8 = is_unified_kv_fp8()
|
||||
# DSpark draft still allocates a bf16 ring; target fp8 * (T+1)/T would
|
||||
# under-count that ring (640 vs 1024). MTP keeps the old inflation.
|
||||
self._dspark_draft_on_bf16 = bool(
|
||||
self._unified_fp8 and kvc.spec_algorithm.is_dspark()
|
||||
)
|
||||
# Row width across both unified pools: 1024 B bf16, 640 B fp8.
|
||||
self._unified_row_bytes = dsv4_unified_row_bytes(
|
||||
self.qk_nope_head_dim, self.qk_rope_head_dim, self._unified_fp8
|
||||
@@ -1395,20 +1403,32 @@ class DSV4PoolConfigurator(MemoryPoolConfigurator):
|
||||
|
||||
def _fixed_swa_bytes(self, max_running_requests: int) -> int:
|
||||
"""Unified_kv SWA is a fixed per-request ring, sized by concurrency
|
||||
(num_req_slots) rather than by full_token. Return its byte footprint
|
||||
across all full layers, inflated for the draft worker the same way as the
|
||||
per-token coeff. Returns 0 on the non-unified path (where SWA is already
|
||||
accounted per-token)."""
|
||||
(num_req_slots) rather than by full_token. MTP inflates the target ring
|
||||
by _spec_infl; DSpark+fp8 adds a bf16 draft ring instead (640 vs 1024).
|
||||
Returns 0 on the non-unified path (SWA already counted per-token)."""
|
||||
if not self._unified:
|
||||
return 0
|
||||
num_req_slots = self._get_num_req_slots(max_running_requests)
|
||||
ring_bytes = (
|
||||
target_ring = (
|
||||
num_req_slots
|
||||
* self._swa_ring_size
|
||||
* self._unified_row_bytes
|
||||
* self.num_layers_total
|
||||
)
|
||||
return int(ring_bytes * self._spec_infl)
|
||||
if self._dspark_draft_on_bf16:
|
||||
from sglang.srt.mem_cache.deepseek_v4_memory_pool import (
|
||||
dsv4_unified_row_bytes,
|
||||
)
|
||||
|
||||
draft_row = dsv4_unified_row_bytes(
|
||||
self.qk_nope_head_dim, self.qk_rope_head_dim, fp8=False
|
||||
)
|
||||
# 1 layer is what the shipped DSpark drafts allocate. A multi-stage
|
||||
# draft would under-count by ~9 MB/layer (128-wide window, ~65 req
|
||||
# slots), which the (T+1)/T on bytes_per_full_token already covers.
|
||||
draft_ring = num_req_slots * self._swa_ring_size * draft_row
|
||||
return int(target_ring + draft_ring)
|
||||
return int(target_ring * self._spec_infl)
|
||||
|
||||
def _to_config(self, sizes: _DSV4PoolSizes) -> MemoryPoolConfig:
|
||||
full = sizes.full_max_total_num_tokens
|
||||
@@ -1486,6 +1506,7 @@ class DSV4PoolConfigurator(MemoryPoolConfigurator):
|
||||
logger.info(
|
||||
f"DSV4 memory calculation: unified={self._unified}, "
|
||||
f"unified_fp8={self._unified_fp8}, "
|
||||
f"dspark_draft_bf16={self._dspark_draft_on_bf16}, "
|
||||
f"bytes_per_full_token={self.bytes_per_full_token:.2f}, "
|
||||
f"available_bytes={available_bytes / (1 << 30):.2f} GB, "
|
||||
f"c128_state_fixed={c128_state_fixed_bytes / (1 << 30):.2f} GB, "
|
||||
|
||||
Reference in New Issue
Block a user