Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
ff499d1
feat(swa-hicache): add strict bit-exact SWA HiCache flag and offload-…
amd-danli103 Jul 23, 2026
b6c2f2a
feat(swa-hicache): version-namespace the SWA/state L3 pools
amd-danli103 Jul 23, 2026
cc55119
feat(swa-hicache): add SWA-window host offload pool
amd-danli103 Jul 23, 2026
d33a87a
feat(swa-hicache): register independent c4/indexer state L3 pools
amd-danli103 Jul 23, 2026
b437f1d
test(swa-hicache): host pool capture-done handshake tests
amd-danli103 Jul 23, 2026
9c7910e
feat(swa-hicache): capture SWA windows at prefill/decode page boundaries
amd-danli103 Jul 23, 2026
df6d3d4
feat(swa-hicache): capture c4/indexer overlap state alongside SWA win…
amd-danli103 Jul 23, 2026
9b1ba88
test(swa-hicache): capture + state-capture unit tests
amd-danli103 Jul 23, 2026
1af4f41
feat(swa-hicache): SWA window riding capture/restore with strict reus…
amd-danli103 Jul 23, 2026
63dc604
test(swa-hicache): reuse/restore + end-to-end bit-exact tests
amd-danli103 Jul 23, 2026
3a27cdb
test(swa-hicache): consolidate small unit-test files into main suites
amd-danli103 Jul 24, 2026
8f5cbdb
fix(swa-hicache): ring-paged L3 SWA restore, avoid sub-page radix split
amd-danli103 Jul 24, 2026
a51b631
refactor(swa-hicache): drop dead legacy state-capture path, test the …
amd-danli103 Jul 24, 2026
12c57c6
Merge upstream main (a358374) into feat/swa-hicache-treecore
amd-danli103 Aug 7, 2026
e89af57
Merge upstream main (5e60363) into feat/swa-hicache-treecore
amd-danli103 Aug 7, 2026
3d8ab45
fix(swa-hicache): position-addressed state restore, staging sweep, ho…
amd-danli103 Aug 8, 2026
de5c1d1
perf(swa-hicache): fuse c4 state restore into one H2D, drop per-layer…
amd-danli103 Aug 8, 2026
105732a
fix(swa-hicache): key strict SWA/c4 capture at the node boundary unde…
amd-danli103 Aug 8, 2026
197a773
fix(swa-hicache): do not read the strict ring geometry off a best-eff…
amd-danli103 Aug 8, 2026
6af69dc
test(swa-hicache): cover the overlap-prefix stitch and the bigram tai…
amd-danli103 Aug 8, 2026
be04d09
Merge upstream main (ee3ee83) into feat/swa-hicache-treecore
amd-danli103 Aug 10, 2026
60bd7ba
fix(swa-hicache): ship the c4 state restore on its per-layer path
amd-danli103 Aug 10, 2026
2cfa2e0
refactor(swa-hicache): drop the fused c4 state H2D
amd-danli103 Aug 11, 2026
d7abe65
Merge upstream main (fd1e04d) into feat/swa-hicache-treecore
amd-danli103 Aug 13, 2026
0a9ea5e
Merge upstream main (0d8c850) into feat/swa-hicache-treecore
amd-danli103 Aug 17, 2026
eccbb57
Merge upstream main (5362181) into feat/swa-hicache-treecore
amd-danli103 Aug 18, 2026
2ab63c2
Merge upstream main (d287880) into feat/swa-hicache-treecore
amd-danli103 Aug 20, 2026
95bdce2
Merge upstream main (1fa32d5) into feat/swa-hicache-treecore
amd-danli103 Aug 25, 2026
b04c8d9
Merge upstream main (3ce4f95) into feat/swa-hicache-treecore
amd-danli103 Aug 26, 2026
d156531
Merge upstream main (702de26) into feat/swa-hicache-treecore
amd-danli103 Aug 26, 2026
283df8f
Merge upstream main (03d06a76) into feat/swa-hicache-treecore
amd-danli103 Sep 9, 2026
a1df24b
test(swa-hicache): stub tree_core.page_size in the reuse-gate tests
amd-danli103 Sep 9, 2026
ca473de
fix(swa-hicache): address c4 state by request slot on HiCache capture…
amd-danli103 Sep 9, 2026
f64f76e
fix(swa-hicache): size c4 state staging by prefix windows, not cache …
amd-danli103 Sep 9, 2026
f52f0aa
fix(swa-hicache): restore the shadowed cleanup hook and req.kv.req_po…
amd-danli103 Sep 9, 2026
221fccc
refactor(swa-hicache): make the reuse anchor clamp opt-in per component
amd-danli103 Sep 9, 2026
ac83ee7
fix(swa-hicache): skip the page-rep SWA free in per-request ring mode
amd-danli103 Sep 10, 2026
b77a92c
fix(swa-hicache): sample the c4 state landing check on the restoring …
amd-danli103 Sep 10, 2026
e7eda05
fix(swa-hicache): fence the positional SWA restore on the forward stream
amd-danli103 Sep 10, 2026
00f6e73
fix(swa-hicache): coalesce adjacent kv-row ranges before the page-dis…
amd-danli103 Sep 10, 2026
c58ca70
Merge upstream main (480b14e) into feat/swa-hicache-treecore
amd-danli103 Sep 11, 2026
cd29a56
test(swa-hicache): read the stride default off the msgspec ServerArgs
amd-danli103 Sep 11, 2026
a24122d
fix(swa-hicache): drop the local ring skip, upstream #38571 covers it
amd-danli103 Sep 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions python/sglang/srt/arg_groups/field_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,7 @@
"hicache_host_memory_mode",
"hicache_ratio",
"hicache_size",
"hicache_swa_offload_page_stride",
"hicache_write_policy",
"hicache_io_backend",
"hicache_mem_layout",
Expand Down
8 changes: 8 additions & 0 deletions python/sglang/srt/arg_groups/fields/memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,14 @@ class Memory(msgspec.Struct):
int,
"The size of host KV cache memory pool in gigabytes. Overrides --hicache-ratio in either host memory mode.",
] = 0
hicache_swa_offload_page_stride: A[
int,
"Strict bit-exact SWA HiCache: offload one SWA sliding window every N "
"pages (plus the sequence tail window, always captured). 1 = per-page "
"(finest reuse granularity, most host memory). Larger N trades reuse "
"granularity for host memory; the SWA host pool is sized as "
"ceil(full_host_pages / N) + tail.",
] = 1
hicache_write_policy: A[
str,
Arg(
Expand Down
14 changes: 14 additions & 0 deletions python/sglang/srt/environ.py
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,20 @@ class Envs:
SGLANG_OPT_SWA_RADIX_CACHE_COMPACT = EnvBool(False)
SGLANG_OPT_SWA_SPLIT_LEAF_ON_INSERT = EnvBool(False)
SGLANG_OPT_SWA_RELEASE_LEAF_LOCK_AFTER_WINDOW = EnvBool(False)
# Strict bit-exact SWA HiCache for unified_kv (DeepSeek-V4): offload the SWA
# ring to host and restore it on reuse instead of reprefilling a 128-token
# tail. Sizing: --hicache-swa-offload-page-stride. OFF == #29417 best-effort (tail
# reprefill, no SWA host pool) -- cheaper for short-prefix workloads.
SGLANG_UNIFIED_KV_BIT_EXACT_HICACHE = EnvBool(False)
# Staging headroom, in absolute pages, for the c4/indexer state pools that
# ride the strict SWA windows. Default -1: auto = ceil(ISL/(page*stride)) + 1
# at per-rank batch 1, with ISL = min(200k, context_length). A row lives from
# capture until write_through BACKUP_HOST promote, so a long prefix can hold
# every strided window at once; do not scale by max_running_requests. Raise
# it when [SWA-HiCache] reports staging exhausted -- N concurrent prefills
# want roughly N times the auto value. 0 restores the legacy 1.5x-of-durable
# sizing.
SGLANG_SWA_HICACHE_STATE_STAGING_PAGES = EnvInt(-1)

# ===================================================================
# PD disaggregation runtime
Expand Down
428 changes: 428 additions & 0 deletions python/sglang/srt/layers/attention/deepseek_v4_backend_hip_radix.py

Large diffs are not rendered by default.

357 changes: 357 additions & 0 deletions python/sglang/srt/layers/attention/dsv4/compress_hip.py

Large diffs are not rendered by default.

35 changes: 35 additions & 0 deletions python/sglang/srt/layers/attention/dsv4/compressor_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,41 @@ def forward_unified(
kv_score_input = compressor.compute_kv_score(x, forward_batch)

state_pool = compressor.get_state_pool(self)

# Strict SWA-HiCache c4 overlap-state capture for the UNIFIED-KV prefill
# path. Legacy capture (compress_extend_paged) is dead here, so without
# this the state staging is empty and strict reuse is rejected. Must run
# BEFORE compress mutates kv_score_input; gated to the extend path (decode
# has its own capture_compress_state_windows_decode) and ratio==4.
if (
compressor.ratio == 4
and forward_batch.forward_mode.is_extend()
and not forward_batch.forward_mode.is_target_verify()
and getattr(
self.token_to_kv_pool,
(
"_c4_indexer_state_host_pool"
if compressor.is_in_indexer
else "_c4_state_host_pool"
),
None,
)
is not None
):
from sglang.srt.layers.attention.dsv4.compress_hip import (
capture_c4_state_windows_unified,
)

capture_c4_state_windows_unified(
backend=self,
state_pool=state_pool,
kv_score_input=kv_score_input,
forward_batch=forward_batch,
is_indexer=compressor.is_in_indexer,
layer_id=layer_id,
ratio=compressor.ratio,
)

from sglang.kernels.ops.attention.dsv4.unified_kv_kernels.env_gate import (
is_unified_kv_triton,
)
Expand Down
10 changes: 10 additions & 0 deletions python/sglang/srt/managers/schedule_batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -1507,6 +1507,7 @@ def init_next_round_input(
),
req=self,
cow_mamba=cow_mamba,
for_reuse=True,
)
)
if envs.SGLANG_RADIX_FORCE_MISS.get():
Expand Down Expand Up @@ -2605,6 +2606,15 @@ def prepare_for_extend(self):
self
)

# unified_kv SWA HiCache: restore reused sliding-window KV into each
# request positional ring now that req_pool_idx is known, before the
# first forward reads it. No-op unless a req stashed a window at
# load_back (see SWAComponent.restore_pending_swa_windows).
if self.tree_cache is not None and hasattr(
self.tree_cache, "restore_swa_windows"
):
self.tree_cache.restore_swa_windows(reqs, req_pool_indices_cpu)

# Set fields
input_embeds = []
all_replace_embeds: List[torch.Tensor] = []
Expand Down
4 changes: 3 additions & 1 deletion python/sglang/srt/managers/schedule_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ def match_prefix_for_req(
),
cow_mamba=cow_mamba,
req=req if include_req else None,
for_reuse=True,
)
)
if envs.SGLANG_RADIX_FORCE_MISS.get():
Expand Down Expand Up @@ -370,7 +371,8 @@ def _compute_prefix_matches(
token_ids=prefix_ids,
extra_key=extra_key,
cache_salt=cache_salt,
)
),
for_reuse=True,
)
)
if envs.SGLANG_RADIX_FORCE_MISS.get():
Expand Down
12 changes: 11 additions & 1 deletion python/sglang/srt/mem_cache/base_prefix_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ class MatchPrefixParams:
cow_mamba: bool = False
req: Optional[Req] = None

# True for cross-request reuse matches (scheduler-side lookups against the
# shared radix tree). False (default) for self-match lookups such as the
# one in `cache_unfinished_req`, which must keep trusting the device-only
# validators for the request's own freshly-computed nodes.
for_reuse: bool = False


@dataclasses.dataclass
class InsertParams:
Expand Down Expand Up @@ -392,8 +398,12 @@ def free_kv_row(self, kv: Any, ranges: list[tuple[int, int]]) -> None:
"""Give back ascending, disjoint, half-open row-position ranges
of the ``kv`` record's row; one call keeps a shared page freed once.
"""
from sglang.srt.mem_cache.common import free_kv_row_segments
from sglang.srt.mem_cache.common import (
free_kv_row_segments,
merge_adjacent_kv_row_ranges,
)

ranges = merge_adjacent_kv_row_ranges(ranges)
row = self.req_to_token_pool.req_to_token[kv.req_pool_idx]
free_kv_row_segments(
self.token_to_kv_pool_allocator,
Expand Down
14 changes: 14 additions & 0 deletions python/sglang/srt/mem_cache/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,20 @@ def page_align_floor(length: int, page_size: int) -> int:
return (length // page_size) * page_size


def merge_adjacent_kv_row_ranges(
ranges: list[tuple[int, int]],
) -> list[tuple[int, int]]:
# token-adjacent ranges can still share a page (EAGLE +1 on a page-aligned
# SWA branch). _page_disjoint rejects that; one segment frees the page once.
merged: list[tuple[int, int]] = []
for start, end in sorted((s, e) for s, e in ranges if s < e):
if merged and start <= merged[-1][1]:
merged[-1] = (merged[-1][0], max(merged[-1][1], end))
else:
merged.append((start, end))
return merged


def free_swa_out_of_window_slots(
req: Req,
pre_len: int,
Expand Down
63 changes: 63 additions & 0 deletions python/sglang/srt/mem_cache/deepseek_v4_memory_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -869,6 +869,38 @@ def unified_region_buffers(self, ratio: int) -> Tuple[List[torch.Tensor], int]:
)
return views, item_bytes

def swa_region_buffers(self) -> Tuple[List[torch.Tensor], int]:
"""
Page-granular views of the SWA ring region [0, swa_pages) of every
unified_kv layer, one indexed row per sliding-window page
(``unified_swa_ring_size`` consecutive ring rows). Mirrors
``unified_region_buffers`` so the SWA host pool transfers a whole window
per page and device rows match the host ``item_bytes`` in transfer_kv.
"""
assert self._unified_kv, "swa_region_buffers requires unified_kv layout"

swa_pages = self.unified_kv_pool.swa_pages
head_dim = self.unified_kv_pool.head_dim
rows_per_page = self.unified_swa_ring_size
assert swa_pages % rows_per_page == 0, (
f"swa_pages {swa_pages} not a multiple of ring size {rows_per_page}"
)
num_pages = swa_pages // rows_per_page

views: List[torch.Tensor] = []
for buf in self.unified_kv_pool.kv_buffer:
page_view = (
buf.narrow(0, 0, swa_pages)
.reshape(num_pages, rows_per_page * head_dim)
.view(torch.uint8)
)
views.append(page_view)

item_bytes = (
rows_per_page * head_dim * self.unified_kv_pool.kv_buffer[0].element_size()
)
return views, item_bytes

def get_state_buf_infos(self) -> Tuple[List[int], List[int], List[int]]:
data_ptrs: List[int] = []
data_lens: List[int] = []
Expand Down Expand Up @@ -898,6 +930,37 @@ def get_state_buf_infos(self) -> Tuple[List[int], List[int], List[int]]:

return data_ptrs, data_lens, item_lens

def get_swa_state_coupling_infos(self) -> List[Tuple[int, int]]:
"""FlexKV integration contract: the SWA-slot -> state-row mapping.

FlexKV registers the c4 / indexer state device buffers itself (handles
come from ``get_state_buf_infos``); it only needs the mapping to
co-place / co-fetch each state row with the SWA window it rides. Returns
one ``(swa_page_size, ring_size)`` per sidecar state pool, in the SAME
order/filtering as the state-pool entries of ``get_state_buf_infos``. For
a SWA device location ``swa_loc`` the coupled state row is
``(swa_loc // swa_page_size) * ring_size + (swa_loc % ring_size)`` when
``swa_loc >= 0`` else ``-1`` (== translate_from_swa_loc_to_state_loc).

On unified_kv the state row is owned by the request slot, not by swa_loc:
the authoritative mapping there is ``req_pool_idx * ring_size + pos %
ring_size`` (translate_from_req_position_to_state_loc), which the form
above only reproduces when ``swa_page_size == ring_size``. A connector
driving a unified_kv pool must use the req-position form.

Co-lifetime invariant the connector MUST honor: for a given (rid, B) the
SWA window and its coupled state rows are allocated/freed together and
must be co-present on fetch -- if any coupled row is missing, drop the
whole SWA window (recompute), never fetch with a desynced state.
"""
infos: List[Tuple[int, int]] = []
for pools in [self.compress_state_pools, self.indexer_compress_state_pools]:
for pool in pools:
if pool is None or pool.ratio == 128:
continue
infos.append((pool.swa_page_size, pool.ring_size))
return infos

def get_request_state_buf_infos(
self,
) -> Tuple[List[int], List[int], List[int]]:
Expand Down
10 changes: 10 additions & 0 deletions python/sglang/srt/mem_cache/hicache_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,16 @@ def __init__(
if attn_cp_size > 1:
self.config_suffix += f"_cp{attn_cp_rank}_{attn_cp_size}"

# Strict unified-kv SWA HiCache: the SWA window and its coupled c4/indexer
# overlap state persist as INDEPENDENT L3 pools coupled by key (sidecar
# C4_STATE->SWA + TRAILING_PAGES). The SWA blob is a pure window again (no
# A-gather tail packing), so its byte layout differs from the older
# ``_swapk1`` packed blobs -- namespace the whole cache by layout version so
# a stale packed blob cannot be read under the new independent layout. Bump
# the tag on any future SWA/state L3 layout change.
if envs.SGLANG_UNIFIED_KV_BIT_EXACT_HICACHE.get():
self.config_suffix += "_swaind1"

if not os.path.exists(self.file_path) and tp_rank == 0 and attn_cp_rank == 0:
os.makedirs(self.file_path)
logger.info(f"Created HiCacheFile storage directory at {self.file_path}")
Expand Down
Loading
Loading