feat(mem_cache): unified memory pool for hybrid Mamba / SWA models (#29678)
Co-authored-by: lch1475369 <lch1475369@gmail.com>
This commit is contained in:
@@ -301,6 +301,15 @@ class Envs:
|
||||
SGLANG_OTLP_EXPORTER_SCHEDULE_DELAY_MILLIS = EnvInt(500)
|
||||
SGLANG_OTLP_EXPORTER_MAX_EXPORT_BATCH_SIZE = EnvInt(64)
|
||||
SGLANG_NATIVE_MOVE_KV_CACHE = EnvBool(False)
|
||||
# Disable lazy compaction in the unified memory pool allocator and
|
||||
# fall back to the per-free eager compaction. Used for production
|
||||
# A/B and quick rollback. Default False (lazy compaction on).
|
||||
SGLANG_DISABLE_LAZY_COMPACTION = EnvBool(False)
|
||||
# Sort the multi-ended allocator's free list after a merge (perf A/B knob).
|
||||
SGLANG_SORT_FREE_LIST_AFTER_MERGE = EnvBool(False)
|
||||
# Periodically log lazy-compaction stats per sub-pool (observability only).
|
||||
SGLANG_LOG_LAZY_COMPACTION_STATS = EnvBool(False)
|
||||
SGLANG_LOG_LAZY_COMPACTION_STATS_INTERVAL_SEC = EnvInt(30)
|
||||
SGLANG_ENABLE_TP_MEMORY_INBALANCE_CHECK = EnvBool(True)
|
||||
SGLANG_TEST_DISAGG_FAILURE_PROB = EnvFloat(0.0)
|
||||
|
||||
|
||||
@@ -179,7 +179,7 @@ class NPUMHATokenToKVPool(MHATokenToKVPool):
|
||||
layer_id_override: Optional[int] = None,
|
||||
dcp_kv_mask: Optional[torch.Tensor] = None,
|
||||
):
|
||||
loc, _ = unwrap_write_loc(loc_info)
|
||||
loc, _, _ = unwrap_write_loc(loc_info)
|
||||
if layer_id_override is not None:
|
||||
layer_id = layer_id_override
|
||||
else:
|
||||
@@ -441,7 +441,7 @@ class NPUMLATokenToKVPool(MLATokenToKVPool):
|
||||
cache_k: torch.Tensor,
|
||||
cache_v: torch.Tensor,
|
||||
):
|
||||
loc, _ = unwrap_write_loc(loc_info)
|
||||
loc, _, _ = unwrap_write_loc(loc_info)
|
||||
layer_id = layer.layer_id
|
||||
if cache_k.dtype != self.dtype:
|
||||
cache_k = cache_k.to(self.dtype)
|
||||
|
||||
@@ -35,15 +35,15 @@ class MambaAttnBackendBase(AttentionBackend):
|
||||
self.is_draft_worker = model_runner.is_draft_worker
|
||||
self.req_to_token_pool: HybridReqToTokenPool = model_runner.req_to_token_pool
|
||||
self.token_to_kv_pool = model_runner.token_to_kv_pool
|
||||
self.enable_unified_memory = model_runner.server_args.enable_unified_memory
|
||||
self.forward_metadata: ForwardMetadata = None
|
||||
self.state_indices_list = []
|
||||
# GDN ReplaySSM (slice 1b): per-bs STATIC per-row write-cursor buffers
|
||||
# for cuda-graph. Allocated lazily in init_cuda_graph_state only when
|
||||
# --enable-linear-replayssm is set; stays None otherwise.
|
||||
# Static (max_bs,) track-dest buffer captured by pointer, refreshed in-place
|
||||
# each replay; the captured track-save reads this, not the InputBuffer slot.
|
||||
self.mamba_track_indices_buf = None
|
||||
# Per-bs static write-cursor / force-flush buffers for cuda-graph; None
|
||||
# unless --enable-linear-replayssm is set.
|
||||
self.replayssm_write_pos_list = None
|
||||
# GDN ReplaySSM (slice 2b): per-bs STATIC per-row force-flush buffers
|
||||
# for cuda-graph, parallel to replayssm_write_pos_list. Same lifetime
|
||||
# (None unless the flag is on).
|
||||
self.replayssm_force_flush_list = None
|
||||
self.query_start_loc_list = []
|
||||
self.retrieve_next_token_list = []
|
||||
@@ -66,8 +66,10 @@ class MambaAttnBackendBase(AttentionBackend):
|
||||
forward_batch.mamba_clear_indices is not None
|
||||
and len(forward_batch.mamba_clear_indices) > 0
|
||||
):
|
||||
# mamba_pool is a pure PHYSICAL store; translate before zeroing or
|
||||
# clear_slots zeroes the wrong physical slots.
|
||||
self.req_to_token_pool.mamba_pool.clear_slots(
|
||||
forward_batch.mamba_clear_indices
|
||||
self._translate_mamba_indices(forward_batch.mamba_clear_indices)
|
||||
)
|
||||
if (
|
||||
forward_batch.mamba_cow_src_indices is not None
|
||||
@@ -75,22 +77,28 @@ class MambaAttnBackendBase(AttentionBackend):
|
||||
):
|
||||
ckpt_pool = getattr(self.req_to_token_pool, "mamba_ckpt_pool", None)
|
||||
if ckpt_pool is not None:
|
||||
# int8 checkpoints: dequantize the cached state (src = int8 ckpt slot)
|
||||
# into the request's active bf16 slot (dst).
|
||||
# int8 checkpoints: dequantize src int8 ckpt slot into the active bf16 dst.
|
||||
ckpt_pool.load_to_active(
|
||||
self.req_to_token_pool.mamba_pool,
|
||||
forward_batch.mamba_cow_src_indices,
|
||||
forward_batch.mamba_cow_dst_indices,
|
||||
)
|
||||
else:
|
||||
# mamba_pool is a pure PHYSICAL store; translate both COW slot ids.
|
||||
self.req_to_token_pool.mamba_pool.copy_from(
|
||||
forward_batch.mamba_cow_src_indices,
|
||||
forward_batch.mamba_cow_dst_indices,
|
||||
self._translate_mamba_indices(forward_batch.mamba_cow_src_indices),
|
||||
self._translate_mamba_indices(forward_batch.mamba_cow_dst_indices),
|
||||
)
|
||||
forward_batch.mamba_clear_indices = None
|
||||
forward_batch.mamba_cow_src_indices = None
|
||||
forward_batch.mamba_cow_dst_indices = None
|
||||
|
||||
def _translate_mamba_indices(self, mamba_indices: torch.Tensor) -> torch.Tensor:
|
||||
"""Virtual->physical mamba slot-id translate (identity for the non-unified
|
||||
pool). Must run everywhere mamba ids feed the SSM/conv kernels or mamba-pool
|
||||
state ops, incl. the cuda-graph replay-prep copy into ``state_indices_list``."""
|
||||
return self.req_to_token_pool.translate_mamba_indices(mamba_indices)
|
||||
|
||||
def _forward_metadata(self, forward_batch: ForwardBatch):
|
||||
bs = forward_batch.batch_size
|
||||
|
||||
@@ -106,7 +114,14 @@ class MambaAttnBackendBase(AttentionBackend):
|
||||
mamba_cache_indices = self.req_to_token_pool.get_mamba_indices(
|
||||
forward_batch.req_pool_indices
|
||||
)
|
||||
_real_bs = getattr(forward_batch, "_original_batch_size", None)
|
||||
# Translate virtual->physical BEFORE the padding sentinel below, so the
|
||||
# gather reads only real ids; padded rows are then poisoned to -1 (skipped).
|
||||
mamba_cache_indices = self._translate_mamba_indices(mamba_cache_indices)
|
||||
if forward_batch.mamba_track_indices is not None:
|
||||
forward_batch.mamba_track_indices = self._translate_mamba_indices(
|
||||
forward_batch.mamba_track_indices
|
||||
)
|
||||
_real_bs = forward_batch._original_batch_size
|
||||
if _real_bs is not None and _real_bs < mamba_cache_indices.shape[0]:
|
||||
mamba_cache_indices = mamba_cache_indices.clone()
|
||||
mamba_cache_indices[_real_bs:] = -1
|
||||
@@ -117,11 +132,8 @@ class MambaAttnBackendBase(AttentionBackend):
|
||||
query_start_loc = torch.arange(
|
||||
0, bs + 1, dtype=torch.int32, device=self.device
|
||||
)
|
||||
# GDN ReplaySSM (slice 1a): the ring cursor is a per-slot
|
||||
# decode-position counter shared by ALL GDN layers in this forward.
|
||||
# Manage it exactly ONCE here (not per-layer): snapshot this step's
|
||||
# value for the batch's slots, hand it to the layers, then advance
|
||||
# the persistent buffer mod L for the next step.
|
||||
# The ring cursor is a per-slot decode counter shared by all GDN layers;
|
||||
# manage it once here (snapshot, hand to layers, advance mod L), not per-layer.
|
||||
mamba_pool = getattr(self.req_to_token_pool, "mamba_pool", None)
|
||||
write_pos_buf = (
|
||||
getattr(mamba_pool, "replayssm_write_pos", None)
|
||||
@@ -130,27 +142,16 @@ class MambaAttnBackendBase(AttentionBackend):
|
||||
)
|
||||
if write_pos_buf is not None:
|
||||
slots = mamba_cache_indices.to(torch.long)
|
||||
# Padded rows carry slot == -1; clamp so the per-row gather stays
|
||||
# in-bounds (the kernel zeroes padded rows via state_idx < 0).
|
||||
# Padded rows carry slot == -1; clamp the gather in-bounds (kernel
|
||||
# zeroes padded rows via state_idx < 0).
|
||||
safe_slots = slots.clamp(min=0)
|
||||
replayssm_write_pos = write_pos_buf[safe_slots].clone()
|
||||
L = mamba_pool.linear_replayssm_cache_len
|
||||
# KDA (per-K gate) ships without radix coordination for now: no
|
||||
# track-boundary force-flush, so the ring flushes only at the
|
||||
# natural write_pos == L-1 wrap. GDN keeps the radix-aligned
|
||||
# force-flush (slice 2b). Gate on the pool's recorded gate type.
|
||||
# KDA has no radix coordination: flush only on the natural write_pos
|
||||
# == L-1 wrap. GDN adds the radix-aligned force-flush below.
|
||||
is_kda = getattr(mamba_pool, "replayssm_is_kda", False)
|
||||
# GDN ReplaySSM (slice 2b): per-row force-flush at the radix
|
||||
# track boundary. THE alignment: the radix mamba track snapshots
|
||||
# temporal[slot] when seq_lens_cpu % mamba_track_interval == 0
|
||||
# (extra_buffer: schedule_batch.prepare_for_decode builds
|
||||
# `mamba_track_mask = (seq_lens_cpu % mamba_track_interval == 0)`
|
||||
# off the SAME post-increment seq_lens_cpu used here). We source
|
||||
# the flush from the identical seq_lens + condition so the kernel
|
||||
# folds the ring into temporal[slot] on EXACTLY the steps the
|
||||
# snapshot reads it. seq_lens_cpu is the committed length AFTER
|
||||
# this decode token (incremented in prepare_for_decode before the
|
||||
# forward), matching the track. int32, one entry per batch row.
|
||||
# Force-flush on the radix track's seq_lens % mamba_track_interval
|
||||
# == 0 boundary so the ring folds into temporal[slot] when read.
|
||||
if not is_kda:
|
||||
force_flush_bool = self._replayssm_track_flush_mask(
|
||||
forward_batch.seq_lens_cpu, bs
|
||||
@@ -158,16 +159,11 @@ class MambaAttnBackendBase(AttentionBackend):
|
||||
replayssm_force_flush = force_flush_bool.to(
|
||||
device=self.device, dtype=torch.int32
|
||||
)
|
||||
# Advance only the VALID (non-padded) slots. Scatter over the
|
||||
# unique valid slots to avoid duplicate-index races (padded rows
|
||||
# all clamp to slot 0, which a real row may also occupy). A
|
||||
# forced flush empties the ring -> next write_pos is 0 (same as
|
||||
# the natural wrap at write_pos == L-1).
|
||||
# Advance only valid slots, scattered over unique slots (dup-index
|
||||
# race; padded rows clamp to 0); a forced flush -> next write_pos 0.
|
||||
valid_mask = slots >= 0
|
||||
valid_slots = slots[valid_mask]
|
||||
if valid_slots.numel() > 0:
|
||||
# Per-row "did this step flush?": natural wrap OR forced.
|
||||
# (KDA has no forced flush -> force_flush is None -> pure wrap.)
|
||||
flushed = replayssm_write_pos == (L - 1)
|
||||
if replayssm_force_flush is not None:
|
||||
flushed = flushed | (replayssm_force_flush != 0)
|
||||
@@ -176,9 +172,8 @@ class MambaAttnBackendBase(AttentionBackend):
|
||||
torch.zeros_like(replayssm_write_pos),
|
||||
(replayssm_write_pos + 1) % L,
|
||||
)
|
||||
# Dedup valid slots; for duplicates a scatter picks one
|
||||
# arbitrary row, but all rows of a given slot share the same
|
||||
# write_pos/flush, so the value is identical regardless.
|
||||
# Dedup: rows sharing a slot share write_pos/flush, so the
|
||||
# scattered value is identical regardless of which row wins.
|
||||
uniq_slots, inv = torch.unique(valid_slots, return_inverse=True)
|
||||
next_for_valid = next_pos[valid_mask]
|
||||
new_vals = torch.empty(
|
||||
@@ -190,9 +185,8 @@ class MambaAttnBackendBase(AttentionBackend):
|
||||
write_pos_buf[uniq_slots] = new_vals
|
||||
elif forward_batch.forward_mode.is_extend(include_draft_extend_v2=True):
|
||||
if forward_batch.forward_mode.is_draft_extend_v2():
|
||||
# HybridLinearAttnBackend.init_forward_metadata calls all sub-backends
|
||||
# unconditionally, but DRAFT_EXTEND_V2 only runs full-attn layers in
|
||||
# the draft model, so mamba metadata can be skipped.
|
||||
# DRAFT_EXTEND_V2 runs only full-attn layers in the draft model;
|
||||
# skip mamba metadata.
|
||||
query_start_loc = None
|
||||
elif forward_batch.forward_mode.is_target_verify():
|
||||
query_start_loc = torch.arange(
|
||||
@@ -208,7 +202,7 @@ class MambaAttnBackendBase(AttentionBackend):
|
||||
retrieve_next_sibling = (
|
||||
forward_batch.spec_info.retrieve_next_sibling
|
||||
)
|
||||
# retrieve_next_token is None during dummy run so skip tensor creation
|
||||
# None during dummy run
|
||||
if retrieve_next_token is not None:
|
||||
retrieve_parent_token = torch.empty_like(retrieve_next_token)
|
||||
else:
|
||||
@@ -245,6 +239,9 @@ class MambaAttnBackendBase(AttentionBackend):
|
||||
return ForwardMetadata(
|
||||
query_start_loc=query_start_loc,
|
||||
mamba_cache_indices=mamba_cache_indices,
|
||||
# Physical track destinations (None when tracking off); cuda-graph
|
||||
# supplies this via the static backend buffer in _replay_metadata.
|
||||
mamba_track_indices=getattr(forward_batch, "mamba_track_indices", None),
|
||||
retrieve_next_token=retrieve_next_token,
|
||||
retrieve_next_sibling=retrieve_next_sibling,
|
||||
retrieve_parent_token=retrieve_parent_token,
|
||||
@@ -263,8 +260,6 @@ class MambaAttnBackendBase(AttentionBackend):
|
||||
forward_batch: ForwardBatch,
|
||||
in_capture: bool = False,
|
||||
):
|
||||
# seq_lens_cpu is unused by _replay_metadata for the non-target-verify
|
||||
# case but kept in the contract for compatibility.
|
||||
self.forward_metadata = self._replay_metadata(
|
||||
forward_batch.batch_size,
|
||||
forward_batch.req_pool_indices,
|
||||
@@ -275,6 +270,7 @@ class MambaAttnBackendBase(AttentionBackend):
|
||||
0 if in_capture else getattr(forward_batch, "num_padding", None)
|
||||
),
|
||||
in_capture=in_capture,
|
||||
mamba_track_indices=getattr(forward_batch, "mamba_track_indices", None),
|
||||
)
|
||||
|
||||
def init_forward_metadata(self, forward_batch: ForwardBatch):
|
||||
@@ -284,28 +280,10 @@ class MambaAttnBackendBase(AttentionBackend):
|
||||
def _init_track_conv_indices(
|
||||
self, query_start_loc: torch.Tensor, forward_batch: ForwardBatch
|
||||
):
|
||||
"""
|
||||
Compute indices for extracting conv states from the input sequence during extend.
|
||||
|
||||
In Mamba models, the conv layer maintains a sliding window of recent inputs.
|
||||
After processing a prefill chunk, we need to save the last `conv_state_len` tokens
|
||||
of the processed region for prefix caching.
|
||||
|
||||
The key insight is that FLA (Flash Linear Attention) and Mamba2 processes sequences in chunks
|
||||
of the chunk size (FLA_CHUNK_SIZE=64 for FLA, mamba_chunk_size for Mamba2).
|
||||
We only track the conv state up to the last complete chunk boundary (aligned_len).
|
||||
|
||||
start_indices is the starting token index of the conv state to track in this extend batch.
|
||||
indices include all pos to track in this extend batch, conv_state_len for each req that
|
||||
needs to be tracked (i.e. mamba_track_mask is True)
|
||||
|
||||
Returns:
|
||||
indices: Tensor of shape [num_tracked_requests, conv_state_len] containing
|
||||
flattened positions into the packed input tensor.
|
||||
"""
|
||||
"""Flattened input positions of conv states to track during extend (up to
|
||||
the last complete chunk boundary, mamba_track_mask rows only)."""
|
||||
conv_state_len = self.conv_states_shape[-1]
|
||||
|
||||
# Calculate the end position of the last aligned chunk
|
||||
lens_to_track = (
|
||||
forward_batch.mamba_track_seqlens - forward_batch.extend_prefix_lens
|
||||
)
|
||||
@@ -314,7 +292,6 @@ class MambaAttnBackendBase(AttentionBackend):
|
||||
start_indices = query_start_loc[:-1] + aligned_len - conv_state_len
|
||||
start_indices = start_indices[forward_batch.mamba_track_mask]
|
||||
|
||||
# Create indices: [batch_size, conv_state_len]
|
||||
indices = start_indices.unsqueeze(-1) + torch.arange(
|
||||
conv_state_len,
|
||||
device=self.device,
|
||||
@@ -326,46 +303,11 @@ class MambaAttnBackendBase(AttentionBackend):
|
||||
def _init_track_ssm_indices(
|
||||
self, mamba_cache_indices: torch.Tensor, forward_batch: ForwardBatch
|
||||
):
|
||||
"""
|
||||
Compute source and destination indices for tracking SSM states for prefix caching.
|
||||
|
||||
After processing a prefill, we need to save the SSM recurrent state for prefix caching.
|
||||
The kernel outputs intermediate hidden states `h` at each chunk boundary,
|
||||
plus a `last_recurrent_state` at the end of the chunked prefill size.
|
||||
|
||||
The chunk size varies by model type:
|
||||
- FLA models: FLA_CHUNK_SIZE (64)
|
||||
- Mamba2 models: mamba_chunk_size (256)
|
||||
|
||||
The challenge is that sequences may or may not end on a chunk boundary:
|
||||
- Aligned case (len % chunk_size == 0): The to-cache state is stored in
|
||||
the last_recurrent_state.
|
||||
- Unaligned case (len % chunk_size != 0): The last_recurrent_state includes the
|
||||
unaligned position, but we only want state up to the last chunk boundary.
|
||||
We must extract from the intermediate `h` tensor at the appropriate chunk index.
|
||||
|
||||
We compute the src and dst indices for all requests that need to be cached
|
||||
(i.e. mamba_track_mask is True) based on the rule above.
|
||||
|
||||
For example (assuming chunk_size=64):
|
||||
1. If chunked prefill length is < chunk_size, then only final state has value.
|
||||
In this case we cache `final` state.
|
||||
2. If chunked prefill length == chunk_size, then only final state has value.
|
||||
In this case we cache pos chunk_size, from `final` state.
|
||||
3. If chunked prefill length > chunk_size and < 2 * chunk_size, then both h and
|
||||
final state have value. We cache pos chunk_size from `h` state.
|
||||
4. If chunked prefill length == 2 * chunk_size, then both h and final state have
|
||||
value. We cache pos 2 * chunk_size from `final` state. Note `h` doesn't include
|
||||
the final position.
|
||||
|
||||
Returns:
|
||||
track_ssm_h_src: Source indices into the packed `h` tensor (for unaligned seqs)
|
||||
track_ssm_h_dst: Destination cache slot indices (for unaligned seqs)
|
||||
track_ssm_final_src: Source indices into last_recurrent_state buffer (for aligned seqs)
|
||||
track_ssm_final_dst: Destination cache slot indices (for aligned seqs)
|
||||
"""
|
||||
"""src/dst indices to track SSM states for prefix caching: aligned seqs
|
||||
cache last_recurrent_state, unaligned cache intermediate `h` at the last
|
||||
chunk boundary."""
|
||||
mamba_cache_chunk_size = get_global_server_args().mamba_cache_chunk_size
|
||||
# Move to CPU to avoid kernel launches for masking operations
|
||||
# CPU to avoid kernel launches for the masking ops
|
||||
mamba_track_mask = forward_batch.mamba_track_mask.cpu()
|
||||
extend_seq_lens = forward_batch.extend_seq_lens.cpu()
|
||||
mamba_track_indices = forward_batch.mamba_track_indices.cpu()
|
||||
@@ -373,38 +315,33 @@ class MambaAttnBackendBase(AttentionBackend):
|
||||
mamba_track_seqlens = forward_batch.mamba_track_seqlens.cpu()
|
||||
prefix_lens = forward_batch.extend_prefix_lens.cpu()
|
||||
|
||||
# Calculate the number of hidden states per request
|
||||
if isinstance(self, Mamba2AttnBackend):
|
||||
num_h_states = extend_seq_lens // mamba_cache_chunk_size
|
||||
else:
|
||||
num_h_states = (extend_seq_lens - 1) // mamba_cache_chunk_size + 1
|
||||
|
||||
# Calculate the starting offset for each sequence in the packed batch
|
||||
track_ssm_src_offset = torch.zeros_like(num_h_states)
|
||||
track_ssm_src_offset[1:] = torch.cumsum(num_h_states[:-1], dim=0)
|
||||
|
||||
# Filter variables by track mask
|
||||
lens_to_track = mamba_track_seqlens - prefix_lens
|
||||
lens_masked = lens_to_track[mamba_track_mask]
|
||||
offset_masked = track_ssm_src_offset[mamba_track_mask]
|
||||
dst_masked = mamba_track_indices[mamba_track_mask]
|
||||
|
||||
# Determine if the sequence ends at a chunk boundary
|
||||
is_aligned = (lens_masked % mamba_cache_chunk_size) == 0
|
||||
|
||||
# Case 1: Aligned. Use last_recurrent_state from ssm_states.
|
||||
# Aligned: last_recurrent_state from ssm_states.
|
||||
track_ssm_final_src = mamba_cache_indices[mamba_track_mask][is_aligned]
|
||||
track_ssm_final_dst = dst_masked[is_aligned]
|
||||
|
||||
# Case 2: Unaligned. Use intermediate state from h.
|
||||
# TODO: if support mamba_cache_chunk_size % page size != 0, then need to modify this
|
||||
# Unaligned: intermediate state from h.
|
||||
# TODO: handle mamba_cache_chunk_size % page size != 0
|
||||
not_aligned = ~is_aligned
|
||||
track_ssm_h_src = offset_masked[not_aligned] + (
|
||||
lens_masked[not_aligned] // mamba_cache_chunk_size
|
||||
)
|
||||
track_ssm_h_dst = dst_masked[not_aligned]
|
||||
|
||||
# Move back to GPU
|
||||
return (
|
||||
track_ssm_h_src.to(self.device, non_blocking=True),
|
||||
track_ssm_h_dst.to(self.device, non_blocking=True),
|
||||
@@ -427,12 +364,8 @@ class MambaAttnBackendBase(AttentionBackend):
|
||||
)
|
||||
|
||||
def _replayssm_enabled(self) -> bool:
|
||||
"""True iff --enable-linear-replayssm allocated the persistent ring cursor.
|
||||
|
||||
The per-slot ``replayssm_write_pos`` buffer on MambaPool is None unless
|
||||
the flag is set, so it doubles as the on/off gate (same signal that
|
||||
``_forward_metadata`` / ``GDNAttnBackend.forward_decode`` already use).
|
||||
"""
|
||||
"""True iff --enable-linear-replayssm allocated the ring cursor
|
||||
(MambaPool.replayssm_write_pos doubles as the on/off gate)."""
|
||||
mamba_pool = getattr(self.req_to_token_pool, "mamba_pool", None)
|
||||
if mamba_pool is None:
|
||||
return False
|
||||
@@ -441,21 +374,12 @@ class MambaAttnBackendBase(AttentionBackend):
|
||||
def _replayssm_track_flush_mask(
|
||||
self, seq_lens_cpu: torch.Tensor, bs: int
|
||||
) -> torch.Tensor:
|
||||
"""Per-row bool flush mask == the radix mamba-track snapshot condition.
|
||||
|
||||
THE alignment (slice 2b): the radix mamba track snapshots temporal[slot]
|
||||
exactly when ``seq_lens_cpu % mamba_track_interval == 0`` (the same mask
|
||||
``schedule_batch.prepare_for_decode`` builds for extra_buffer at
|
||||
``mamba_track_mask = (seq_lens_cpu % mamba_track_interval == 0)``). Both
|
||||
read the SAME post-increment ``seq_lens_cpu`` (committed length AFTER
|
||||
this decode token), so the kernel force-flush fires on EXACTLY the steps
|
||||
the snapshot reads the checkpoint -- no off-by-one. Returns a CPU bool
|
||||
tensor of length ``bs`` (caller moves it to device as int32).
|
||||
"""
|
||||
"""Per-row (length bs) bool flush mask = the radix track's seq_lens_cpu %
|
||||
mamba_track_interval == 0, so force-flush and snapshot fire on the same
|
||||
steps (no off-by-one)."""
|
||||
interval = get_global_server_args().mamba_track_interval
|
||||
if seq_lens_cpu is None:
|
||||
# Decode without a CPU seq-len mirror should not happen for the
|
||||
# supported (no_buffer, radix-on) config, but stay safe: never flush.
|
||||
# Should not happen for the supported config; stay safe and never flush.
|
||||
return torch.zeros((bs,), dtype=torch.bool)
|
||||
mask = (seq_lens_cpu[:bs].to(torch.int64) % interval) == 0
|
||||
if mask.shape[0] < bs:
|
||||
@@ -468,16 +392,15 @@ class MambaAttnBackendBase(AttentionBackend):
|
||||
max_num_tokens % max_bs == 0
|
||||
), f"max_num_tokens={max_num_tokens} must be divisible by max_bs={max_bs}"
|
||||
draft_token_num = max_num_tokens // max_bs
|
||||
# GDN ReplaySSM (slice 1b): per-batch-size STATIC per-row write-cursor
|
||||
# buffers the kernel reads. Captured into the graph by pointer, so they
|
||||
# must be the SAME tensor objects refreshed in-place each replay. Sized
|
||||
# and indexed like state_indices_list ((i+1,), indexed [bs - 1]). Left
|
||||
# None when the flag is off so the dispatch falls through unchanged.
|
||||
# Per-bs static write-cursor / force-flush buffers, captured by pointer +
|
||||
# refreshed in-place each replay; sized like state_indices_list. None when off.
|
||||
self.replayssm_write_pos_list = [] if self._replayssm_enabled() else None
|
||||
# GDN ReplaySSM (slice 2b): static per-bs force-flush buffers, captured
|
||||
# by pointer and refreshed in-place per replay just like the write-pos
|
||||
# buffers. None when the flag is off.
|
||||
self.replayssm_force_flush_list = [] if self._replayssm_enabled() else None
|
||||
# int64 to match DecodeInputBuffers.mamba_track_indices + the track-save
|
||||
# kernel's int64 index load. Refreshed in-place by _replay_metadata.
|
||||
self.mamba_track_indices_buf = torch.zeros(
|
||||
(max_bs,), dtype=torch.int64, device=self.device
|
||||
)
|
||||
for i in range(max_bs):
|
||||
self.state_indices_list.append(
|
||||
torch.full(
|
||||
@@ -556,32 +479,26 @@ class MambaAttnBackendBase(AttentionBackend):
|
||||
else:
|
||||
raise ValueError(f"Invalid forward mode: {forward_mode=}")
|
||||
mamba_indices = self.req_to_token_pool.get_mamba_indices(req_pool_indices)
|
||||
# Captured Mamba kernels read state_indices_list as PHYSICAL ids; translate
|
||||
# before copying (no-op for non-unified pool).
|
||||
mamba_indices = self._translate_mamba_indices(mamba_indices)
|
||||
self.state_indices_list[bs - 1][: len(mamba_indices)].copy_(mamba_indices)
|
||||
|
||||
# GDN ReplaySSM (slice 1b): point at the STATIC per-bs write-cursor
|
||||
# buffer (no advance, no snapshot — capture records the pointer; its
|
||||
# zeros are overwritten in-place by _replay_metadata before each
|
||||
# replay). None when the flag is off. Same per-bs tensor object that
|
||||
# _replay_metadata refreshes, so the captured pointer stays valid.
|
||||
# Capture records the pointer to the static per-bs buffers; their zeros are
|
||||
# overwritten in-place by _replay_metadata before each replay. None when off.
|
||||
replayssm_write_pos = (
|
||||
self.replayssm_write_pos_list[bs - 1]
|
||||
if self.replayssm_write_pos_list is not None
|
||||
else None
|
||||
)
|
||||
# GDN ReplaySSM (slice 2b): point at the STATIC per-bs force-flush
|
||||
# buffer (same capture-by-pointer contract as write_pos; refreshed
|
||||
# in-place by _replay_metadata before each replay). None when off.
|
||||
replayssm_force_flush = (
|
||||
self.replayssm_force_flush_list[bs - 1]
|
||||
if self.replayssm_force_flush_list is not None
|
||||
else None
|
||||
)
|
||||
|
||||
# If topk > 1, we need to use retrieve_next_token and retrieve_next_sibling to handle the eagle tree custom attention mask
|
||||
if forward_mode.is_target_verify() and self.topk > 1:
|
||||
# They are None during cuda graph capture so skip the copy_...
|
||||
# self.retrieve_next_token_list[bs - 1].copy_(spec_info.retrieve_next_token)
|
||||
# self.retrieve_next_sibling_list[bs - 1].copy_(spec_info.retrieve_next_sibling)
|
||||
# retrieve_* are None during capture, so skip the copy.
|
||||
return ForwardMetadata(
|
||||
query_start_loc=self.query_start_loc_list[bs - 1],
|
||||
mamba_cache_indices=self.state_indices_list[bs - 1],
|
||||
@@ -608,6 +525,7 @@ class MambaAttnBackendBase(AttentionBackend):
|
||||
seq_lens_cpu: Optional[torch.Tensor],
|
||||
num_padding: Optional[int] = None,
|
||||
in_capture: bool = False,
|
||||
mamba_track_indices: Optional[torch.Tensor] = None,
|
||||
):
|
||||
if num_padding is None:
|
||||
if seq_lens_cpu is None:
|
||||
@@ -619,16 +537,22 @@ class MambaAttnBackendBase(AttentionBackend):
|
||||
# Make sure forward metadata is correctly handled for padding reqs
|
||||
req_pool_indices[bs - num_padding :] = 0
|
||||
mamba_indices = self.req_to_token_pool.get_mamba_indices(req_pool_indices)
|
||||
# Translate using the LIVE v2p table BEFORE the padding sentinel below;
|
||||
# captured Mamba kernels read state_indices_list as PHYSICAL ids.
|
||||
mamba_indices = self._translate_mamba_indices(mamba_indices)
|
||||
mamba_indices[bs - num_padding :] = -1
|
||||
self.state_indices_list[bs - 1][: len(mamba_indices)].copy_(mamba_indices)
|
||||
# GDN ReplaySSM (slice 1b): refresh the STATIC per-row write cursor the
|
||||
# kernel reads, mirroring the eager snapshot-then-advance in
|
||||
# _forward_metadata but writing in-place into the captured per-bs buffer
|
||||
# so the graph's recorded pointer stays valid across replays. Done once
|
||||
# per forward here (out_graph host op), not per layer. Skipped during
|
||||
# capture (in_capture): capture runs on dummy slots, so advancing the
|
||||
# persistent counter then would corrupt real per-slot ring positions;
|
||||
# the captured buffer's contents are irrelevant at capture time anyway.
|
||||
# Refresh the static track-dest buffer in-place (translated); the captured
|
||||
# track-save reads it, leaving the handed-in InputBuffer slot read-only.
|
||||
track_buf = None
|
||||
if mamba_track_indices is not None:
|
||||
track_buf = self.mamba_track_indices_buf
|
||||
track_buf[: len(mamba_track_indices)].copy_(
|
||||
self._translate_mamba_indices(mamba_track_indices)
|
||||
)
|
||||
# Refresh the static write cursor in-place (mirrors the eager
|
||||
# snapshot-then-advance). Skip the advance during capture: dummy slots
|
||||
# would corrupt real ring positions.
|
||||
replayssm_write_pos = None
|
||||
replayssm_force_flush = None
|
||||
if self.replayssm_write_pos_list is not None:
|
||||
@@ -636,29 +560,20 @@ class MambaAttnBackendBase(AttentionBackend):
|
||||
write_pos_buf = mamba_pool.replayssm_write_pos
|
||||
static_wp = self.replayssm_write_pos_list[bs - 1]
|
||||
static_ff = self.replayssm_force_flush_list[bs - 1]
|
||||
# Hand the full captured per-bs buffers to the kernel, mirroring how
|
||||
# mamba_cache_indices = self.state_indices_list[bs - 1] is the full
|
||||
# (bs,) tensor; the kernel indexes them per decode row.
|
||||
# Hand the full captured per-bs buffers to the kernel; it indexes per row.
|
||||
replayssm_write_pos = static_wp
|
||||
replayssm_force_flush = static_ff
|
||||
if write_pos_buf is not None:
|
||||
# mamba_indices: this replay's per-row physical slots (padded
|
||||
# rows == -1, same tensor fed to state_indices_list above).
|
||||
# this replay's per-row physical slots (padded rows == -1)
|
||||
slots = mamba_indices.to(torch.long)
|
||||
safe_slots = slots.clamp(min=0)
|
||||
# Snapshot THIS step's per-slot cursor into the captured buffer
|
||||
# the kernel reads (in-place copy_, never reassign the object).
|
||||
# Snapshot this step's cursor into the captured buffer in-place
|
||||
# (copy_, never reassign the object).
|
||||
static_wp[: len(mamba_indices)].copy_(write_pos_buf[safe_slots])
|
||||
# GDN ReplaySSM (slice 2b): refresh the captured force-flush
|
||||
# buffer in-place from THIS step's seq_lens. THE alignment: same
|
||||
# `seq_lens_cpu % mamba_track_interval == 0` the radix track uses
|
||||
# (see _replayssm_track_flush_mask / schedule_batch). During
|
||||
# capture (seq_lens_cpu is None) leave it zeroed: capture content
|
||||
# is irrelevant and decode replays overwrite it below.
|
||||
# Refresh the force-flush buffer in-place from this step's seq_lens
|
||||
# (same condition as the radix track). Zeroed during capture.
|
||||
force_flush_dev = None
|
||||
# KDA: no radix coordination -> leave static_ff zeroed and
|
||||
# force_flush_dev None so the advance below is a pure wrap,
|
||||
# matching the kernel (a zeroed force_flush flushes nothing).
|
||||
# KDA: no radix coordination -> leave zeroed so the advance is a pure wrap.
|
||||
is_kda = getattr(mamba_pool, "replayssm_is_kda", False)
|
||||
if (
|
||||
not is_kda
|
||||
@@ -672,10 +587,8 @@ class MambaAttnBackendBase(AttentionBackend):
|
||||
static_ff.zero_()
|
||||
if not in_capture:
|
||||
L = mamba_pool.linear_replayssm_cache_len
|
||||
# Advance only VALID (non-padded) slots. A forced flush
|
||||
# empties the ring -> next write_pos is 0 (same as the
|
||||
# natural wrap at write_pos == L-1). Use this step's snapshot
|
||||
# cursor (write_pos_buf[safe_slots]) + the flush flag.
|
||||
# Advance only valid (non-padded) slots; a forced flush empties
|
||||
# the ring -> next write_pos 0, like the natural L-1 wrap.
|
||||
valid_mask = slots >= 0
|
||||
valid_slots = slots[valid_mask]
|
||||
if valid_slots.numel() > 0:
|
||||
@@ -688,8 +601,7 @@ class MambaAttnBackendBase(AttentionBackend):
|
||||
torch.zeros_like(cur_pos),
|
||||
(cur_pos + 1) % L,
|
||||
)
|
||||
# Dedup; rows sharing a slot share write_pos+flush, so
|
||||
# the scattered value is identical for either row.
|
||||
# Dedup; rows sharing a slot share write_pos+flush.
|
||||
uniq_slots, inv = torch.unique(valid_slots, return_inverse=True)
|
||||
next_for_valid = next_pos[valid_mask]
|
||||
new_vals = torch.empty(
|
||||
@@ -726,7 +638,6 @@ class MambaAttnBackendBase(AttentionBackend):
|
||||
else:
|
||||
raise ValueError(f"Invalid forward mode: {forward_mode=}")
|
||||
|
||||
# If topk > 1, we need to use retrieve_next_token and retrieve_next_sibling to handle the eagle tree custom attention mask
|
||||
if forward_mode.is_target_verify() and self.topk > 1:
|
||||
if (
|
||||
spec_info is not None
|
||||
@@ -742,6 +653,7 @@ class MambaAttnBackendBase(AttentionBackend):
|
||||
return ForwardMetadata(
|
||||
query_start_loc=self.query_start_loc_list[bs - 1],
|
||||
mamba_cache_indices=self.state_indices_list[bs - 1],
|
||||
mamba_track_indices=track_buf,
|
||||
retrieve_next_token=self.retrieve_next_token_list[bs - 1],
|
||||
retrieve_next_sibling=self.retrieve_next_sibling_list[bs - 1],
|
||||
retrieve_parent_token=self.retrieve_parent_token_list[bs - 1],
|
||||
@@ -752,6 +664,7 @@ class MambaAttnBackendBase(AttentionBackend):
|
||||
return ForwardMetadata(
|
||||
query_start_loc=self.query_start_loc_list[bs - 1],
|
||||
mamba_cache_indices=self.state_indices_list[bs - 1],
|
||||
mamba_track_indices=track_buf,
|
||||
replayssm_write_pos=replayssm_write_pos,
|
||||
replayssm_force_flush=replayssm_force_flush,
|
||||
)
|
||||
@@ -769,27 +682,18 @@ class MambaAttnBackendBase(AttentionBackend):
|
||||
ssm_states: torch.Tensor,
|
||||
cache_indices: torch.Tensor,
|
||||
):
|
||||
"""
|
||||
Track and copy Mamba conv/SSM states during decode for prefix caching.
|
||||
|
||||
During decode, each token update modifies conv_states and ssm_states in-place
|
||||
at positions indexed by cache_indices (the working slots). For prefix caching,
|
||||
we need to copy these updated states to persistent cache slots (mamba_track_indices)
|
||||
so they can be prefix cached.
|
||||
|
||||
This delegates to `track_mamba_states_if_needed`, which performs:
|
||||
conv_states[mamba_track_indices[i]] = conv_states[cache_indices[i]]
|
||||
ssm_states[mamba_track_indices[i]] = ssm_states[cache_indices[i]]
|
||||
for all requests where mamba_track_mask[i] is True.
|
||||
"""
|
||||
"""Copy decode conv/SSM states to track slots for prefix caching. Track
|
||||
dests come from the metadata (under cuda-graph: the static buffer), so the
|
||||
InputBuffer registry slot is never mutated."""
|
||||
if forward_batch.mamba_track_mask is not None:
|
||||
track_mamba_states_if_needed(
|
||||
conv_states,
|
||||
ssm_states,
|
||||
cache_indices,
|
||||
forward_batch.mamba_track_mask,
|
||||
forward_batch.mamba_track_indices,
|
||||
self.forward_metadata.mamba_track_indices,
|
||||
forward_batch.batch_size,
|
||||
check_freed_slots=self.enable_unified_memory,
|
||||
)
|
||||
|
||||
def _track_mamba_state_extend(
|
||||
@@ -799,18 +703,8 @@ class MambaAttnBackendBase(AttentionBackend):
|
||||
ssm_states: torch.Tensor,
|
||||
forward_metadata: ForwardMetadata,
|
||||
):
|
||||
"""
|
||||
Track and copy SSM states during extend for prefix caching.
|
||||
|
||||
After the chunked prefill kernel runs, we need to save the SSM recurrent
|
||||
state at the last chunk boundary so it can be reused for prefix caching.
|
||||
The source of the state depends on whether the sequence length is aligned
|
||||
to the chunk size. See `_init_track_ssm_indices` for more details on how
|
||||
the source and destination indices are computed.
|
||||
|
||||
Note: Conv state tracking for extend is handled separately via gather operations
|
||||
using indices computed by `_init_track_conv_indices`.
|
||||
"""
|
||||
"""Copy extend SSM state at the last chunk boundary to track slots (source
|
||||
depends on chunk alignment; see `_init_track_ssm_indices`)."""
|
||||
if forward_metadata.has_mamba_track_mask:
|
||||
h = h.squeeze(0)
|
||||
|
||||
@@ -861,6 +755,7 @@ class Mamba2AttnBackend(MambaAttnBackendBase):
|
||||
0 if in_capture else getattr(forward_batch, "num_padding", None)
|
||||
),
|
||||
in_capture=in_capture,
|
||||
mamba_track_indices=getattr(forward_batch, "mamba_track_indices", None),
|
||||
)
|
||||
spec_info = forward_batch.spec_info
|
||||
draft_token_num = spec_info.draft_token_num if spec_info is not None else 1
|
||||
@@ -891,6 +786,12 @@ class Mamba2AttnBackend(MambaAttnBackendBase):
|
||||
use_triton_causal_conv: bool = False,
|
||||
):
|
||||
assert isinstance(self.forward_metadata, Mamba2Metadata)
|
||||
# Page-major stores state strided; only the stride-aware Triton causal-conv
|
||||
# reads it (CUDA causal_conv1d garbles it). A model may also force Triton.
|
||||
use_triton_causal_conv = (
|
||||
use_triton_causal_conv
|
||||
or get_global_server_args().enable_page_major_kv_layout
|
||||
)
|
||||
layer_cache = self.req_to_token_pool.mamba2_layer_cache(layer_id)
|
||||
mixer_out, intermediate_states = mixer.forward(
|
||||
hidden_states=hidden_states,
|
||||
@@ -922,8 +823,9 @@ class Mamba2AttnBackend(MambaAttnBackendBase):
|
||||
layer_cache.temporal,
|
||||
self.forward_metadata.mamba_cache_indices[-num_decodes:],
|
||||
forward_batch.mamba_track_mask[-num_decodes:],
|
||||
forward_batch.mamba_track_indices[-num_decodes:],
|
||||
self.forward_metadata.mamba_track_indices[-num_decodes:],
|
||||
num_decodes,
|
||||
check_freed_slots=self.enable_unified_memory,
|
||||
)
|
||||
|
||||
return mixer_out
|
||||
@@ -952,7 +854,6 @@ class HybridLinearAttnBackend(AttentionBackend):
|
||||
self.full_attn_backend = full_attn_backend
|
||||
self.linear_attn_backend = linear_attn_backend
|
||||
self.attn_backend_list = [full_attn_backend, linear_attn_backend]
|
||||
# Dispatcher aliases the full-attn backend's pool refs.
|
||||
self.token_to_kv_pool = full_attn_backend.token_to_kv_pool
|
||||
self.req_to_token_pool = full_attn_backend.req_to_token_pool
|
||||
self.max_context_len = getattr(full_attn_backend, "max_context_len", None)
|
||||
@@ -981,8 +882,8 @@ class HybridLinearAttnBackend(AttentionBackend):
|
||||
|
||||
def init_forward_metadata(self, forward_batch: ForwardBatch):
|
||||
if forward_batch.forward_mode.is_draft_extend_v2():
|
||||
# DRAFT_EXTEND_V2 only runs full-attn layers in the draft model,
|
||||
# so skip linear/mamba backend metadata which requires query_start_loc.
|
||||
# DRAFT_EXTEND_V2 runs only full-attn layers in the draft model; skip
|
||||
# linear/mamba metadata (it requires query_start_loc).
|
||||
self.full_attn_backend.init_forward_metadata(forward_batch)
|
||||
return
|
||||
for attn_backend in self.attn_backend_list:
|
||||
@@ -991,9 +892,8 @@ class HybridLinearAttnBackend(AttentionBackend):
|
||||
def init_mha_chunk_metadata(
|
||||
self, forward_batch: ForwardBatch, disable_flashinfer_ragged: bool = False
|
||||
):
|
||||
# Hybrid MLA models (Ring/Ling, Kimi-Linear) resolve this via
|
||||
# get_attn_backend(), which returns this wrapper; delegate to the
|
||||
# full-attn backend so its chunked/one-shot prefill metadata is planned.
|
||||
# Hybrid MLA models resolve get_attn_backend() to this wrapper; delegate
|
||||
# so the full-attn backend plans its chunked-prefill metadata.
|
||||
init = getattr(self.full_attn_backend, "init_mha_chunk_metadata", None)
|
||||
if init is not None:
|
||||
init(forward_batch, disable_flashinfer_ragged)
|
||||
@@ -1050,7 +950,6 @@ class HybridLinearAttnBackend(AttentionBackend):
|
||||
return self.full_attn_backend.forward_decode(
|
||||
q, k, v, layer, forward_batch, save_kv_cache, **kwargs
|
||||
)
|
||||
# Linear attention backend
|
||||
return self.linear_attn_backend.forward_decode(
|
||||
q=q,
|
||||
k=k,
|
||||
@@ -1081,7 +980,6 @@ class HybridLinearAttnBackend(AttentionBackend):
|
||||
return self.full_attn_backend.forward_extend(
|
||||
q, k, v, layer, forward_batch, save_kv_cache, **kwargs
|
||||
)
|
||||
# Linear attention backend
|
||||
return self.linear_attn_backend.forward_extend(
|
||||
q=q,
|
||||
k=k,
|
||||
@@ -1150,15 +1048,7 @@ class HybridLinearAttnBackend(AttentionBackend):
|
||||
mamba_steps_to_track: Optional[torch.Tensor],
|
||||
model,
|
||||
):
|
||||
"""
|
||||
Update mamba states after MTP verify using fully fused Triton kernel.
|
||||
|
||||
This replaces the original advanced indexing operations with a single fused
|
||||
gather-scatter kernel that also handles masking internally, avoiding:
|
||||
- index_elementwise_kernel from tensor[bool_mask]
|
||||
- index_select kernel launches
|
||||
- nonzero kernel launches
|
||||
"""
|
||||
"""Update mamba states after MTP verify via a fused gather-scatter kernel."""
|
||||
request_number = last_correct_step_indices.shape[0]
|
||||
|
||||
state_indices_tensor = (
|
||||
@@ -1176,16 +1066,14 @@ class HybridLinearAttnBackend(AttentionBackend):
|
||||
intermediate_state_cache = mamba_caches.intermediate_ssm
|
||||
intermediate_conv_window_cache = mamba_caches.intermediate_conv_window[0]
|
||||
|
||||
# Use fully fused kernel that handles masking internally
|
||||
# This avoids separate nonzero() and index_select() calls
|
||||
fused_mamba_state_scatter_with_mask(
|
||||
ssm_states,
|
||||
intermediate_state_cache,
|
||||
state_indices_tensor,
|
||||
last_correct_step_indices,
|
||||
)
|
||||
# conv intermediate uses the deduplicated sliding-window (overlapping)
|
||||
# layout, so it needs the strided-read scatter variant.
|
||||
# conv intermediate uses the deduplicated sliding-window layout, so it
|
||||
# needs the strided-read scatter variant.
|
||||
fused_conv_window_scatter_with_mask(
|
||||
conv_states,
|
||||
intermediate_conv_window_cache,
|
||||
@@ -1193,10 +1081,9 @@ class HybridLinearAttnBackend(AttentionBackend):
|
||||
last_correct_step_indices,
|
||||
)
|
||||
|
||||
# Track indices used for tracking mamba states for prefix cache
|
||||
# Track indices for prefix cache
|
||||
if mamba_track_indices is not None:
|
||||
assert mamba_steps_to_track is not None
|
||||
# Use fully fused kernel for track scatter operations
|
||||
fused_mamba_state_scatter_with_mask(
|
||||
ssm_states,
|
||||
intermediate_state_cache,
|
||||
|
||||
@@ -30,6 +30,11 @@ class ForwardMetadata:
|
||||
query_start_loc: torch.Tensor
|
||||
mamba_cache_indices: torch.Tensor
|
||||
mamba_cache_indices_gdn: Optional[torch.Tensor] = None
|
||||
# Mamba track DESTINATION slots (PHYSICAL, length == batch). Like
|
||||
# mamba_cache_indices: a backend-owned static buffer under cuda-graph (translated
|
||||
# in-place each replay), eager sets the translated decode tensor. The decode
|
||||
# track-save reads THIS, never forward_batch.mamba_track_indices.
|
||||
mamba_track_indices: Optional[torch.Tensor] = None
|
||||
# GDN ReplaySSM (slice 1a): per-decode-row snapshot of the ring write
|
||||
# cursor for THIS decode step (gathered from the persistent per-slot
|
||||
# buffer, then advanced once for the next step). int32, length == batch.
|
||||
@@ -179,6 +184,7 @@ class Mamba2Metadata(ForwardMetadata):
|
||||
return Mamba2Metadata(
|
||||
query_start_loc=forward_metadata.query_start_loc,
|
||||
mamba_cache_indices=forward_metadata.mamba_cache_indices,
|
||||
mamba_track_indices=forward_metadata.mamba_track_indices,
|
||||
retrieve_next_token=forward_metadata.retrieve_next_token,
|
||||
retrieve_next_sibling=forward_metadata.retrieve_next_sibling,
|
||||
retrieve_parent_token=forward_metadata.retrieve_parent_token,
|
||||
@@ -274,6 +280,7 @@ class Mamba2Metadata(ForwardMetadata):
|
||||
return Mamba2Metadata(
|
||||
query_start_loc=query_start_loc,
|
||||
mamba_cache_indices=forward_metadata.mamba_cache_indices,
|
||||
mamba_track_indices=forward_metadata.mamba_track_indices,
|
||||
retrieve_next_token=forward_metadata.retrieve_next_token,
|
||||
retrieve_next_sibling=forward_metadata.retrieve_next_sibling,
|
||||
retrieve_parent_token=forward_metadata.retrieve_parent_token,
|
||||
|
||||
@@ -23,6 +23,7 @@ def track_mamba_state_if_needed_kernel(
|
||||
conv_state_numel_per_row: tl.constexpr, # total elements per row
|
||||
ssm_state_numel_per_row: tl.constexpr, # total elements per row
|
||||
BLOCK_SIZE: tl.constexpr,
|
||||
check_freed_slots: tl.constexpr, # only the shared/unified KV pool emits -1
|
||||
):
|
||||
"""
|
||||
Track conv_states and ssm_states rows based on track mask.
|
||||
@@ -51,6 +52,12 @@ def track_mamba_state_if_needed_kernel(
|
||||
src_idx = tl.load(cache_indices_ptr + batch_idx).to(tl.int64)
|
||||
dst_idx = tl.load(mamba_track_indices_ptr + batch_idx).to(tl.int64)
|
||||
|
||||
# Skip freed slots (-1): `state_ptr + (-1)*stride` would fault. Only the unified
|
||||
# pool emits -1 tombstones (from the v2p translate); compiled out for static.
|
||||
if check_freed_slots:
|
||||
if src_idx < 0 or dst_idx < 0:
|
||||
return
|
||||
|
||||
# Copy conv_states
|
||||
# Each thread handles BLOCK_SIZE elements
|
||||
for offset in range(0, conv_state_numel_per_row, BLOCK_SIZE):
|
||||
@@ -82,6 +89,7 @@ def track_mamba_states_if_needed(
|
||||
mamba_track_mask: torch.Tensor,
|
||||
mamba_track_indices: torch.Tensor,
|
||||
batch_size: int,
|
||||
check_freed_slots: bool = False,
|
||||
):
|
||||
"""
|
||||
Track mamba states using Triton kernel for better performance.
|
||||
@@ -113,6 +121,7 @@ def track_mamba_states_if_needed(
|
||||
conv_state_numel_per_row,
|
||||
ssm_state_numel_per_row,
|
||||
BLOCK_SIZE,
|
||||
check_freed_slots,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -98,6 +98,9 @@ class ForwardMetadata:
|
||||
swa_attn_logits: Optional[torch.Tensor] = None
|
||||
# full->SWA translated out_cache_loc (SWA KV-store write target)
|
||||
swa_out_cache_loc: Optional[torch.Tensor] = None
|
||||
# PHYSICAL full-attn write target for the unified pool (eager: translated tensor;
|
||||
# cuda-graph: capture-stable buffer view). None for non-unified pools.
|
||||
out_cache_loc_full_physical: Optional[torch.Tensor] = None
|
||||
|
||||
|
||||
class TritonAttnBackend(AttentionBackend):
|
||||
@@ -132,39 +135,31 @@ class TritonAttnBackend(AttentionBackend):
|
||||
extend_attention_fwd_unified
|
||||
)
|
||||
self.build_unified_kv_indices = torch.compiler.disable(build_unified_kv_indices)
|
||||
# Split-KV EAGLE-verify kernel (ROCm/Triton). Registered here; enabled
|
||||
# below once topk is known (the path is only valid at topk == 1).
|
||||
# Split-KV EAGLE-verify kernel; enabled below once topk is known (valid only at topk == 1).
|
||||
self.verify_splitkv_fwd = torch.compiler.disable(verify_splitkv_fwd)
|
||||
|
||||
# Parse args
|
||||
self.skip_prefill = skip_prefill
|
||||
max_bs = model_runner.req_to_token_pool.size
|
||||
self.sliding_window_size = model_runner.sliding_window_size
|
||||
# Pool refs — captured at construction so they survive deletion of the
|
||||
# corresponding ForwardBatch fields.
|
||||
self.req_to_token_pool = model_runner.req_to_token_pool
|
||||
self.token_to_kv_pool = model_runner.token_to_kv_pool
|
||||
self.req_to_token = model_runner.req_to_token_pool.req_to_token
|
||||
self.token_to_kv_pool_allocator = model_runner.token_to_kv_pool_allocator
|
||||
self.use_sliding_window_kv_pool = isinstance(self.token_to_kv_pool, SWAKVPool)
|
||||
# Pass-through to the Triton attention wrappers so they can extract the
|
||||
# KV view strides and specialize on the PAGE_SIZE constexpr. At
|
||||
# page_size=1 the kernel path matches the slot-based envelope addresses.
|
||||
# `model_runner.page_size` defaults to 1 when `server_args.page_size` is
|
||||
# None, avoiding the Optional case here.
|
||||
# Lets the Triton wrappers specialize on PAGE_SIZE; page_size=1 is
|
||||
# byte-identical to the slot-based envelope.
|
||||
self.page_size = getattr(model_runner, "page_size", 1) or 1
|
||||
# Unified pool v2p hook (None = no-op): req_to_token holds VIRTUAL ids but
|
||||
# kernels need PHYSICAL. Applied eagerly so the captured graph has no translate.
|
||||
self._translate_kv_loc = getattr(
|
||||
self.token_to_kv_pool_allocator, "translate_kv_loc", None
|
||||
)
|
||||
self.num_draft_tokens = model_runner.server_args.speculative_num_draft_tokens
|
||||
self.speculative_num_steps = model_runner.server_args.speculative_num_steps
|
||||
self.topk = model_runner.server_args.speculative_eagle_topk or 0
|
||||
# Split-KV verify matches extend_attention_fwd only when the EAGLE tree
|
||||
# reduces to a pure-causal chain, i.e. topk == 1 (the same condition the
|
||||
# aiter backend's unified-verify uses). For topk > 1 the tree custom_mask
|
||||
# is not causal, so leave the path off and fall back to the baseline.
|
||||
# gfx95-only (MI350X/CDNA4): the kernel uses ROCm/CDNA Triton launch hints
|
||||
# (waves_per_eu, matrix_instr_nonkdim) and its block config is tuned and
|
||||
# validated only on gfx950. NVIDIA's Triton rejects those kwargs, and the
|
||||
# path is unvalidated on NV and on other AMD archs, so restrict it to gfx95
|
||||
# and fall back to extend_attention_fwd everywhere else.
|
||||
# Split-KV verify is bit-equivalent only for a pure-causal chain (topk==1)
|
||||
# and is gfx95-only; else fall back to extend_attention_fwd.
|
||||
self.use_verify_splitkv = (
|
||||
is_gfx95_supported()
|
||||
and envs.SGLANG_ENABLE_SPLITKV_VERIFY.get()
|
||||
@@ -179,12 +174,9 @@ class TritonAttnBackend(AttentionBackend):
|
||||
self.num_kv_head = model_runner.model_config.get_num_kv_heads(
|
||||
get_parallel().attn_tp_size
|
||||
)
|
||||
# The decode triton kernel derives attn_lse offsets from attn_logits
|
||||
# strides via integer division by v_head_dim (the "// Lv" trick in
|
||||
# _fwd_kernel_stage1/stage2), so attn_logits.shape[-1] must exactly
|
||||
# match the layer's v_head_dim. For hybrid SWA models where SWA and
|
||||
# full-attention layers use different v_head_dim (e.g. Gemma 4:
|
||||
# swa=256, full=512), we allocate a second buffer for SWA layers.
|
||||
# The decode kernel's "// Lv" stride trick requires attn_logits.shape[-1]
|
||||
# to exactly match the layer's v_head_dim, so hybrid SWA models with
|
||||
# differing SWA/full v_head_dim need a second buffer for SWA layers.
|
||||
full_v_head_dim = model_runner.model_config.v_head_dim
|
||||
swa_v_head_dim = model_runner.model_config.swa_v_head_dim
|
||||
if self.sliding_window_size is not None and swa_v_head_dim != full_v_head_dim:
|
||||
@@ -217,13 +209,9 @@ class TritonAttnBackend(AttentionBackend):
|
||||
self.max_context_len,
|
||||
)
|
||||
if _is_gfx942:
|
||||
# gfx942 (MI300X / MI325X) has 304 CUs, so #20479's next_power_of_2(sm_count)
|
||||
# rounds up to 512 — twice MI355X's natural cap of 256 — and the persistent
|
||||
# cuda_graph_attn_logits fp32 buffer hits ~4 GiB on Kimi-K2.6 (v_head_dim=512),
|
||||
# faulting in ROCm CUDA graph replay
|
||||
# (https://github.com/sgl-project/sglang/actions/runs/25513282022/job/74877480809).
|
||||
# Pin the cap at 256 so gfx942 matches the gfx950 (MI355X) behavior that we
|
||||
# already validated end-to-end.
|
||||
# gfx942's 304 CUs round up to 512 splits, doubling the persistent
|
||||
# fp32 attn_logits buffer to ~4 GiB on Kimi-K2.6 and faulting in
|
||||
# ROCm graph replay; pin to 256 to match validated gfx950 behavior.
|
||||
self.max_kv_splits = min(self.max_kv_splits, 256)
|
||||
if _is_cuda:
|
||||
self.use_pdl = is_arch_support_pdl()
|
||||
@@ -235,18 +223,15 @@ class TritonAttnBackend(AttentionBackend):
|
||||
and model_runner.server_args.chunked_prefill_size == -1
|
||||
)
|
||||
|
||||
# Decide whether enable deterministic inference with batch-invariant operations
|
||||
self.enable_deterministic = (
|
||||
model_runner.server_args.enable_deterministic_inference
|
||||
)
|
||||
|
||||
# Configure deterministic inference settings
|
||||
if self.enable_deterministic:
|
||||
# Use fixed split tile size for batch invariance
|
||||
# Fixed split tile size for batch invariance
|
||||
self.split_tile_size = get_int_env_var(
|
||||
"SGLANG_TRITON_DECODE_SPLIT_TILE_SIZE", 256
|
||||
)
|
||||
# Set static_kv_splits to False to use deterministic logic instead
|
||||
self.static_kv_splits = False
|
||||
else:
|
||||
self.split_tile_size = (
|
||||
@@ -258,14 +243,12 @@ class TritonAttnBackend(AttentionBackend):
|
||||
self.max_context_len + self.split_tile_size - 1
|
||||
) // self.split_tile_size
|
||||
|
||||
# Check arguments
|
||||
assert not (
|
||||
model_runner.sliding_window_size is not None
|
||||
and model_runner.model_config.is_encoder_decoder
|
||||
), "Sliding window and cross attention are not supported together"
|
||||
|
||||
# Initialize buffers
|
||||
# TODO(Jianan Ji): Make sure it behaves as expected when kv_indptr_buf is provided and sliding window is enabled
|
||||
# TODO(Jianan Ji): verify behavior when kv_indptr_buf is provided and sliding window is enabled
|
||||
if kv_indptr_buf is None:
|
||||
self.kv_indptr = torch.zeros(
|
||||
(max_bs + 1,), dtype=torch.int32, device=model_runner.device
|
||||
@@ -273,8 +256,7 @@ class TritonAttnBackend(AttentionBackend):
|
||||
else:
|
||||
self.kv_indptr = kv_indptr_buf
|
||||
|
||||
# If sliding window is enabled, we might need two sets of buffers
|
||||
# because of interleaved attention types (e.g. for Gemma3)
|
||||
# Sliding window may need a second buffer for interleaved attention types
|
||||
self.window_kv_indptr = None
|
||||
if self.sliding_window_size is not None and self.sliding_window_size > 0:
|
||||
if kv_indptr_buf is None:
|
||||
@@ -282,7 +264,6 @@ class TritonAttnBackend(AttentionBackend):
|
||||
(max_bs + 1,), dtype=torch.int32, device=model_runner.device
|
||||
)
|
||||
else:
|
||||
# When provided a buffer, create a clone for the second buffer
|
||||
self.window_kv_indptr = torch.zeros_like(kv_indptr_buf)
|
||||
|
||||
if not self.skip_prefill:
|
||||
@@ -294,7 +275,6 @@ class TritonAttnBackend(AttentionBackend):
|
||||
(max_bs + 1,), dtype=torch.int64, device=model_runner.device
|
||||
)
|
||||
|
||||
# Initialize forward metadata
|
||||
self.forward_metadata: ForwardMetadata = None
|
||||
|
||||
self.cuda_graph_custom_mask = None
|
||||
@@ -314,16 +294,13 @@ class TritonAttnBackend(AttentionBackend):
|
||||
num_group * num_seq == num_token
|
||||
), f"num_seq({num_seq}), num_token({num_token}), something goes wrong!"
|
||||
|
||||
# Legacy dynamic splitting logic (non-deterministic)
|
||||
if (
|
||||
self.static_kv_splits or self.device_core_count <= 0
|
||||
) and not self.enable_deterministic:
|
||||
num_kv_splits.fill_(self.max_kv_splits)
|
||||
return
|
||||
|
||||
# deterministic
|
||||
if self.split_tile_size is not None and self.enable_deterministic:
|
||||
# expand seq_lens to match num_token
|
||||
if num_group > 1:
|
||||
expanded_seq_lens = seq_lens.repeat_interleave(num_group)
|
||||
else:
|
||||
@@ -362,9 +339,8 @@ class TritonAttnBackend(AttentionBackend):
|
||||
kv_indices: Optional[torch.Tensor] = None,
|
||||
kv_start_idx: Optional[torch.Tensor] = None,
|
||||
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
# Build per-DCP-rank sharded KV indptr/indices. eager passes
|
||||
# kv_indices=None (allocate a fresh tensor); the cuda-graph path passes
|
||||
# a fixed address-stable buffer to fill in place.
|
||||
# Build per-DCP-rank sharded KV indptr/indices. eager passes kv_indices=None
|
||||
# (fresh tensor); cuda-graph passes an address-stable buffer to fill in place.
|
||||
dcp_lens = self._dcp_lens(lens, kv_start_idx)
|
||||
kv_indptr[1 : len(req_pool_indices) + 1] = torch.cumsum(dcp_lens, dim=0)
|
||||
kv_indptr = kv_indptr[: len(req_pool_indices) + 1]
|
||||
@@ -422,10 +398,8 @@ class TritonAttnBackend(AttentionBackend):
|
||||
seq_lens = seq_lens[:bs]
|
||||
req_pool_indices = req_pool_indices[:bs]
|
||||
if self.dcp_size > 1:
|
||||
# DCP: kv_indptr cumsum and kv_indices are per-rank sharded. Write
|
||||
# them into the same cuda-graph buffers that
|
||||
# _build_cuda_graph_forward_metadata reads back
|
||||
# (self.kv_indptr / self.cuda_graph_kv_indices).
|
||||
# DCP: per-rank sharded; write into the same cuda-graph buffers
|
||||
# _build_cuda_graph_forward_metadata reads back.
|
||||
_, _, dcp_seq_lens = self._dcp_kv_indices(
|
||||
req_pool_indices,
|
||||
seq_lens,
|
||||
@@ -439,10 +413,15 @@ class TritonAttnBackend(AttentionBackend):
|
||||
kv_indptr = self._fill_kv_indptr_and_indices(
|
||||
bs, seq_lens, req_pool_indices, self.cuda_graph_kv_indices
|
||||
)
|
||||
# Unified pool: VIRTUAL ids written here are translated to PHYSICAL in
|
||||
# init_forward_metadata_out_graph (replay-prep) so the captured graph
|
||||
# carries zero translate nodes.
|
||||
num_kv_splits_lens = seq_lens
|
||||
window_kv_indptr = self.window_kv_indptr
|
||||
window_kv_lens = None
|
||||
if self.sliding_window_size is not None and self.sliding_window_size > 0:
|
||||
# Unified pool: leave the window VIRTUAL too (translated alongside the
|
||||
# full kv_indices later); baseline SWA keeps the eager window translate.
|
||||
window_kv_indptr, _, window_kv_lens, _ = update_sliding_window_buffer(
|
||||
self.window_kv_indptr,
|
||||
self.req_to_token,
|
||||
@@ -452,6 +431,7 @@ class TritonAttnBackend(AttentionBackend):
|
||||
bs,
|
||||
token_to_kv_pool=self.token_to_kv_pool,
|
||||
window_kv_indices=self.cuda_graph_window_kv_indices,
|
||||
skip_full_to_swa_translation=(self._translate_kv_loc is not None),
|
||||
)
|
||||
return kv_indptr, window_kv_indptr, window_kv_lens, num_kv_splits_lens
|
||||
|
||||
@@ -462,12 +442,7 @@ class TritonAttnBackend(AttentionBackend):
|
||||
req_pool_indices: torch.Tensor,
|
||||
spec_info,
|
||||
):
|
||||
"""Fill all cuda-graph buffers for target_verify mode.
|
||||
|
||||
Returns the ForwardMetadata components:
|
||||
``(qo_indptr, kv_indptr, custom_mask, mask_indptr,
|
||||
window_kv_indptr, window_kv_indices, window_num_kv_splits, window_kv_offsets)``
|
||||
"""
|
||||
"""Fill all cuda-graph buffers for target_verify mode."""
|
||||
qo_indptr = self.qo_indptr[: bs + 1]
|
||||
qo_indptr[: bs + 1] = torch.arange(
|
||||
0,
|
||||
@@ -529,13 +504,10 @@ class TritonAttnBackend(AttentionBackend):
|
||||
forward_mode: ForwardMode,
|
||||
spec_info: Optional[SpecInput],
|
||||
):
|
||||
"""Fill QO + KV cuda-graph buffers for draft_extend mode.
|
||||
|
||||
Returns (qo_indptr, kv_indptr, num_tokens_per_bs).
|
||||
"""
|
||||
"""Fill QO + KV cuda-graph buffers for draft_extend mode."""
|
||||
seq_lens = seq_lens[:bs]
|
||||
# V2 draft-extend fills num_draft_tokens per req (the cuda-graph runner's
|
||||
# token layout); num_steps+1 only equals that when topk == 1.
|
||||
# V2 draft-extend fills num_draft_tokens per req; num_steps+1 only equals
|
||||
# that when topk == 1.
|
||||
num_tokens_per_bs = (
|
||||
self.num_draft_tokens
|
||||
if forward_mode.is_draft_extend_v2()
|
||||
@@ -549,17 +521,9 @@ class TritonAttnBackend(AttentionBackend):
|
||||
dtype=torch.int32,
|
||||
device=self.device,
|
||||
)
|
||||
# DRAFT_EXTEND_V2: seq_lens = prefix + extend (bumped on the draft-extend path).
|
||||
# Triton extend kernel receives extend K/V as separate tensors, so
|
||||
# kv_indptr/kv_indices must cover only the prefix portion.
|
||||
# extend_seq_lens_tensor is only attached to spec_info at real
|
||||
# replay (eagle_draft_extend_cuda_graph_runner.replay); during the
|
||||
# capture-time warmup it's absent, so fall back to zeros (matches
|
||||
# the pre-unification capture path in #26651). Clamp at 0 because
|
||||
# padded rows (raw_bs..bs) leave seq_lens at the fill value (1)
|
||||
# while extend_seq_lens stays at num_tokens_per_bs, which would
|
||||
# otherwise produce negative kv_lens; padded rows reference
|
||||
# reserved req-pool slot 0 and their output is discarded.
|
||||
# DRAFT_EXTEND_V2: kv_indptr/kv_indices cover only the prefix (extend K/V go
|
||||
# separately). Capture warmup lacks extend_seq_lens_tensor -> fall back to
|
||||
# zeros; clamp at 0 so padded rows (seq_lens==fill 1) don't go negative.
|
||||
if (
|
||||
spec_info is not None
|
||||
and getattr(spec_info, "extend_seq_lens_tensor", None) is not None
|
||||
@@ -586,8 +550,8 @@ class TritonAttnBackend(AttentionBackend):
|
||||
|
||||
if in_capture:
|
||||
assert forward_batch.encoder_lens is None, "Not supported"
|
||||
# Multi-step speculative decode: kv buffers come from spec_info
|
||||
# rather than the cuda-graph pool, so replay is not involved.
|
||||
# Multi-step spec decode: kv buffers come from spec_info, not the
|
||||
# cuda-graph pool, so replay is not involved.
|
||||
if forward_mode.is_decode_or_idle() and spec_info is not None:
|
||||
self.forward_metadata = ForwardMetadata(
|
||||
attn_logits=self.cuda_graph_attn_logits,
|
||||
@@ -614,9 +578,16 @@ class TritonAttnBackend(AttentionBackend):
|
||||
forward_mode=forward_mode,
|
||||
spec_info=spec_info,
|
||||
)
|
||||
out_cache_loc_full_physical = self._translate_cuda_graph_shared_pool_locs(
|
||||
forward_batch, bs
|
||||
)
|
||||
swa_out_cache_loc = self._fill_cuda_graph_swa_out_cache_loc(forward_batch)
|
||||
self.forward_metadata = self._build_cuda_graph_forward_metadata(
|
||||
bs, forward_mode, spec_info, swa_out_cache_loc
|
||||
bs,
|
||||
forward_mode,
|
||||
spec_info,
|
||||
swa_out_cache_loc,
|
||||
out_cache_loc_full_physical,
|
||||
)
|
||||
else:
|
||||
self._apply_cuda_graph_metadata(
|
||||
@@ -626,15 +597,16 @@ class TritonAttnBackend(AttentionBackend):
|
||||
forward_mode=forward_mode,
|
||||
spec_info=spec_info,
|
||||
)
|
||||
# Metadata view is reused from capture; just refill the buffer.
|
||||
# Metadata view is reused from capture; just refill the buffers.
|
||||
self._translate_cuda_graph_shared_pool_locs(forward_batch, bs)
|
||||
self._fill_cuda_graph_swa_out_cache_loc(forward_batch)
|
||||
|
||||
def _fill_cuda_graph_swa_out_cache_loc(
|
||||
self, forward_batch: ForwardBatch
|
||||
) -> Optional[torch.Tensor]:
|
||||
"""Refill the SWA write-target buffer from the live out_cache_loc and
|
||||
return the [:n] view (None for non-SWA / multi-step draft), so the
|
||||
captured store reads fresh slots on replay."""
|
||||
"""Refill the SWA write-target buffer from live out_cache_loc, returning the
|
||||
[:n] view (None for non-SWA / multi-step draft) so the captured store reads
|
||||
fresh slots on replay."""
|
||||
if not self.use_sliding_window_kv_pool:
|
||||
return None
|
||||
out_cache_loc = forward_batch.out_cache_loc
|
||||
@@ -650,6 +622,47 @@ class TritonAttnBackend(AttentionBackend):
|
||||
)
|
||||
return self.cuda_graph_swa_out_cache_loc[:n]
|
||||
|
||||
def _translate_cuda_graph_shared_pool_locs(
|
||||
self, forward_batch: ForwardBatch, bs: int
|
||||
) -> Optional[torch.Tensor]:
|
||||
"""Unified pool: eager v2p translate of the cuda-graph read+write LOC buffers,
|
||||
run BEFORE graph.replay() reading the live post-compaction v2p, so the
|
||||
captured graph carries zero translate nodes. No-op for non-unified pools.
|
||||
|
||||
Read buffers (full kv_indices, SWA window) are translated IN PLACE; the
|
||||
full-attn WRITE loc is RETURNED as the [:n] view of the backend-owned
|
||||
out_cache_loc_full_physical buffer. Eager .item() bounds are fine here
|
||||
(out-of-graph), so no in-graph translate variant is needed.
|
||||
"""
|
||||
if self._translate_kv_loc is None:
|
||||
return None
|
||||
# Full-attention read path.
|
||||
n_kv = int(self.kv_indptr[bs].item())
|
||||
if n_kv > 0:
|
||||
self.cuda_graph_kv_indices[:n_kv] = self._translate_kv_loc(
|
||||
self.cuda_graph_kv_indices[:n_kv]
|
||||
)
|
||||
# SWA window read path (hybrid-SWA unified pools only).
|
||||
if self.sliding_window_size is not None and self.sliding_window_size > 0:
|
||||
n_win = int(self.window_kv_indptr[bs].item())
|
||||
if n_win > 0:
|
||||
self.cuda_graph_window_kv_indices[:n_win] = (
|
||||
self.token_to_kv_pool.translate_loc_from_full_to_swa(
|
||||
self.cuda_graph_window_kv_indices[:n_win]
|
||||
)
|
||||
)
|
||||
# Full-attention write path: translate out_cache_loc -> physical into the
|
||||
# capture-stable buffer and RETURN the [:n] view.
|
||||
out_cache_loc = forward_batch.out_cache_loc
|
||||
n = out_cache_loc.shape[0]
|
||||
# Zero the padded tail first: a smaller replay batch leaves [n:] holding
|
||||
# stale ids that the captured store would write; send them to slot 0 (sink).
|
||||
self.cuda_graph_out_cache_loc_full_physical[n:].zero_()
|
||||
self.cuda_graph_out_cache_loc_full_physical[:n].copy_(
|
||||
self._translate_kv_loc(out_cache_loc)
|
||||
)
|
||||
return self.cuda_graph_out_cache_loc_full_physical[:n]
|
||||
|
||||
def init_forward_metadata(self, forward_batch: ForwardBatch):
|
||||
"""Init auxiliary variables for triton attention backend."""
|
||||
|
||||
@@ -663,20 +676,17 @@ class TritonAttnBackend(AttentionBackend):
|
||||
|
||||
if forward_batch.forward_mode.is_decode_or_idle():
|
||||
if spec_info is None or spec_info.kv_indptr is None:
|
||||
# kv_indptr is None for draft-extend's idle batch (no tree
|
||||
# indices); build plain metadata from seq_lens.
|
||||
# kv_indptr is None for draft-extend's idle batch; build from seq_lens.
|
||||
if self.dcp_size > 1:
|
||||
# DCP: per-rank sharded KV indices (shares _dcp_kv_indices
|
||||
# with the cuda-graph path). Building full contiguous
|
||||
# indices here would make each rank read the whole KV
|
||||
# instead of its owner shard.
|
||||
# DCP: per-rank sharded KV indices, else each rank reads the
|
||||
# whole KV instead of its owner shard.
|
||||
kv_indptr, kv_indices, _ = self._dcp_kv_indices(
|
||||
forward_batch.req_pool_indices,
|
||||
forward_batch.seq_lens,
|
||||
self.kv_indptr,
|
||||
)
|
||||
else:
|
||||
# gpu_only: seq_lens_sum may be None; ub-allocate is safe (ragged write).
|
||||
# gpu_only: seq_lens_sum may be None; over-allocate is safe (ragged write).
|
||||
seq_lens_sum = forward_batch.seq_lens_sum
|
||||
if seq_lens_sum is None:
|
||||
seq_lens_sum = bs * self.max_context_len
|
||||
@@ -689,7 +699,8 @@ class TritonAttnBackend(AttentionBackend):
|
||||
forward_batch.req_pool_indices,
|
||||
kv_indices,
|
||||
)
|
||||
# Sliding window
|
||||
if self._translate_kv_loc is not None:
|
||||
kv_indices = self._translate_kv_loc(kv_indices)
|
||||
if (
|
||||
self.sliding_window_size is not None
|
||||
and self.sliding_window_size > 0
|
||||
@@ -755,8 +766,7 @@ class TritonAttnBackend(AttentionBackend):
|
||||
dtype=torch.int32,
|
||||
device=self.device,
|
||||
)
|
||||
# Different with flashinfer kv_indptr and kv_indices construction.
|
||||
# gpu_only: seq_lens_sum may be None; ub-allocate is safe (ragged write).
|
||||
# gpu_only: seq_lens_sum may be None; over-allocate is safe (ragged write).
|
||||
seq_lens_sum = forward_batch.seq_lens_sum
|
||||
if seq_lens_sum is None:
|
||||
seq_lens_sum = bs * self.max_context_len
|
||||
@@ -771,7 +781,7 @@ class TritonAttnBackend(AttentionBackend):
|
||||
)
|
||||
|
||||
if self.sliding_window_size is not None and self.sliding_window_size > 0:
|
||||
# window_kv_offsets is used to calculate the start position in custom mask
|
||||
# window_kv_offsets gives the start position in custom mask
|
||||
(
|
||||
window_kv_indptr,
|
||||
window_kv_indices,
|
||||
@@ -808,8 +818,7 @@ class TritonAttnBackend(AttentionBackend):
|
||||
self.kv_indptr,
|
||||
)
|
||||
else:
|
||||
# gpu_only leaves _cpu unset; ub-allocate is safe (ragged write
|
||||
# from GPU tensor, extra tail unused).
|
||||
# gpu_only leaves _cpu unset; over-allocate is safe (ragged write).
|
||||
if forward_batch.extend_prefix_lens_cpu is not None:
|
||||
kv_indices_len = sum(forward_batch.extend_prefix_lens_cpu)
|
||||
else:
|
||||
@@ -825,7 +834,8 @@ class TritonAttnBackend(AttentionBackend):
|
||||
forward_batch.req_pool_indices,
|
||||
kv_indices,
|
||||
)
|
||||
# Sliding window
|
||||
if self._translate_kv_loc is not None:
|
||||
kv_indices = self._translate_kv_loc(kv_indices)
|
||||
if self.sliding_window_size is not None and self.sliding_window_size > 0:
|
||||
(
|
||||
window_kv_indptr,
|
||||
@@ -850,8 +860,7 @@ class TritonAttnBackend(AttentionBackend):
|
||||
mask_indptr = None
|
||||
attn_logits = None
|
||||
attn_lse = None
|
||||
# Caller usually supplies extend_seq_lens_cpu (eagle_info gpu_only
|
||||
# sets host-constant mirror); defensive GPU-max fallback if not.
|
||||
# Defensive GPU-max fallback when extend_seq_lens_cpu is absent.
|
||||
if forward_batch.extend_seq_lens_cpu is not None:
|
||||
max_extend_len = max(forward_batch.extend_seq_lens_cpu)
|
||||
else:
|
||||
@@ -864,6 +873,17 @@ class TritonAttnBackend(AttentionBackend):
|
||||
forward_batch.out_cache_loc
|
||||
)
|
||||
|
||||
# Unified pool full-attention WRITE loc (virtual out_cache_loc -> physical),
|
||||
# carried in the metadata (-> KVWriteLoc.full_loc). None for non-unified pools.
|
||||
out_cache_loc_full_physical = None
|
||||
if (
|
||||
self._translate_kv_loc is not None
|
||||
and forward_batch.out_cache_loc is not None
|
||||
):
|
||||
out_cache_loc_full_physical = self._translate_kv_loc(
|
||||
forward_batch.out_cache_loc
|
||||
)
|
||||
|
||||
self.forward_metadata = ForwardMetadata(
|
||||
attn_logits,
|
||||
attn_lse,
|
||||
@@ -880,6 +900,7 @@ class TritonAttnBackend(AttentionBackend):
|
||||
window_kv_offsets,
|
||||
swa_attn_logits=swa_attn_logits,
|
||||
swa_out_cache_loc=swa_out_cache_loc,
|
||||
out_cache_loc_full_physical=out_cache_loc_full_physical,
|
||||
)
|
||||
|
||||
def init_cuda_graph_state(
|
||||
@@ -970,12 +991,22 @@ class TritonAttnBackend(AttentionBackend):
|
||||
device=self.device,
|
||||
)
|
||||
|
||||
if self._translate_kv_loc is not None:
|
||||
# Unified pool full-attention write-target buffer, refilled at replay
|
||||
# (-> KVWriteLoc.full_loc). Capture-stable, mirrors cuda_graph_swa_out_cache_loc.
|
||||
self.cuda_graph_out_cache_loc_full_physical = torch.zeros(
|
||||
(max_num_tokens,),
|
||||
dtype=torch.int64,
|
||||
device=self.device,
|
||||
)
|
||||
|
||||
def _build_cuda_graph_forward_metadata(
|
||||
self,
|
||||
bs: int,
|
||||
forward_mode: ForwardMode,
|
||||
spec_info: Optional[SpecInput],
|
||||
swa_out_cache_loc: Optional[torch.Tensor] = None,
|
||||
out_cache_loc_full_physical: Optional[torch.Tensor] = None,
|
||||
) -> ForwardMetadata:
|
||||
"""Construct ForwardMetadata from the current cuda-graph buffer state.
|
||||
|
||||
@@ -1006,6 +1037,7 @@ class TritonAttnBackend(AttentionBackend):
|
||||
window_kv_offsets=None,
|
||||
swa_attn_logits=self.cuda_graph_swa_attn_logits,
|
||||
swa_out_cache_loc=swa_out_cache_loc,
|
||||
out_cache_loc_full_physical=out_cache_loc_full_physical,
|
||||
)
|
||||
elif forward_mode.is_target_verify():
|
||||
custom_mask = (
|
||||
@@ -1031,6 +1063,7 @@ class TritonAttnBackend(AttentionBackend):
|
||||
),
|
||||
window_kv_offsets=self.cuda_graph_window_kv_offsets if swa else None,
|
||||
swa_out_cache_loc=swa_out_cache_loc,
|
||||
out_cache_loc_full_physical=out_cache_loc_full_physical,
|
||||
)
|
||||
elif forward_mode.is_draft_extend_v2():
|
||||
return ForwardMetadata(
|
||||
@@ -1056,6 +1089,7 @@ class TritonAttnBackend(AttentionBackend):
|
||||
window_num_kv_splits=None,
|
||||
window_kv_offsets=None,
|
||||
swa_out_cache_loc=swa_out_cache_loc,
|
||||
out_cache_loc_full_physical=out_cache_loc_full_physical,
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Invalid forward mode: {forward_mode=} for CUDA Graph.")
|
||||
@@ -1183,6 +1217,7 @@ class TritonAttnBackend(AttentionBackend):
|
||||
loc_info = KVWriteLoc(
|
||||
forward_batch.out_cache_loc,
|
||||
self.forward_metadata.swa_out_cache_loc,
|
||||
full_loc=self.forward_metadata.out_cache_loc_full_physical,
|
||||
)
|
||||
if layer.k_scale is None:
|
||||
self._set_kv_buffer(forward_batch, layer, loc_info, k, v)
|
||||
@@ -1628,6 +1663,7 @@ class TritonAttnBackend(AttentionBackend):
|
||||
KVWriteLoc(
|
||||
forward_batch.out_cache_loc,
|
||||
self.forward_metadata.swa_out_cache_loc,
|
||||
full_loc=self.forward_metadata.out_cache_loc_full_physical,
|
||||
),
|
||||
k,
|
||||
v,
|
||||
@@ -1908,12 +1944,21 @@ def update_sliding_window_buffer(
|
||||
device=None,
|
||||
token_to_kv_pool=None,
|
||||
window_kv_indices=None,
|
||||
skip_full_to_swa_translation=False,
|
||||
):
|
||||
"""Fill window KV buffers for sliding-window attention.
|
||||
|
||||
Pass window_kv_indices to write into a pre-allocated buffer (CUDA-graph
|
||||
path); omit it (or pass None) to allocate a fresh tensor (eager path,
|
||||
requires device).
|
||||
Pass ``window_kv_indices`` to write into a pre-allocated buffer (CUDA-graph
|
||||
path); omit it (or pass ``None``) to allocate a fresh tensor (eager path,
|
||||
requires ``device``).
|
||||
|
||||
``skip_full_to_swa_translation=True`` leaves ``window_kv_indices`` as VIRTUAL
|
||||
full-token ids (no eager full->swa translate). The unified-memory-pool cuda-graph
|
||||
builder passes this so the window translate is deferred to
|
||||
``TritonAttnBackend._translate_cuda_graph_shared_pool_locs`` (run in
|
||||
``init_forward_metadata_out_graph``, BEFORE ``graph.replay()``), which reads
|
||||
the live v2p and rewrites the static window buffer to swa-physical in place;
|
||||
baseline SWA leaves it False (eager).
|
||||
"""
|
||||
window_kv_lens = torch.minimum(
|
||||
seq_lens,
|
||||
@@ -1935,7 +1980,9 @@ def update_sliding_window_buffer(
|
||||
window_kv_indices,
|
||||
req_to_token.stride(0),
|
||||
)
|
||||
if hasattr(token_to_kv_pool, "translate_loc_from_full_to_swa"):
|
||||
if not skip_full_to_swa_translation and hasattr(
|
||||
token_to_kv_pool, "translate_loc_from_full_to_swa"
|
||||
):
|
||||
kv_last_index = window_kv_indptr[-1]
|
||||
window_kv_indices[:kv_last_index] = (
|
||||
token_to_kv_pool.translate_loc_from_full_to_swa(
|
||||
|
||||
@@ -52,6 +52,9 @@ from sglang.srt.mem_cache.base_prefix_cache import (
|
||||
MatchPrefixParams,
|
||||
zero_match_result,
|
||||
)
|
||||
from sglang.srt.mem_cache.multi_ended_allocator import (
|
||||
UnifiedMambaTokenToKVPoolAllocator,
|
||||
)
|
||||
from sglang.srt.mem_cache.radix_cache import RadixCache, RadixKey, TreeNode
|
||||
from sglang.srt.server_args import ServerArgs, get_global_server_args
|
||||
|
||||
@@ -495,6 +498,34 @@ class PrefillAdder:
|
||||
|
||||
self.rem_swa_token_offset = 0
|
||||
|
||||
# Unified-pool joint budget: a new mamba state consumes shared-gap bytes
|
||||
# that `rem_total_tokens` (full KV) otherwise counts as free, so reserve
|
||||
# the gap per new mamba slot or admission over-commits. Gate on the
|
||||
# ALLOCATOR being the unified Mamba composite, NOT on `is_hybrid_ssm_cache`
|
||||
# (False for `ChunkCache`, which would skip the reservation on the
|
||||
# chunk-cache path): the gap coupling is a property of the byte buffer.
|
||||
self._mamba_slot_cost = 0
|
||||
if isinstance(
|
||||
self.token_to_kv_pool_allocator, UnifiedMambaTokenToKVPoolAllocator
|
||||
):
|
||||
self._mamba_slot_cost = (
|
||||
self.token_to_kv_pool_allocator.mamba_slot_full_token_cost()
|
||||
)
|
||||
|
||||
# `mamba_gap_reserve` is charged to `rem_total_tokens`, which INCLUDES
|
||||
# `full_evictable_size()` — but `alloc_req_slots` can only recover
|
||||
# MAMBA-recoverable bytes for a mamba slot (shared gap + peer holes +
|
||||
# mamba-evictable radix), NOT full-evictable. Gate new mamba slots on
|
||||
# that mamba-recoverable budget separately or an over-admit hits the
|
||||
# fail-loud `RuntimeError`. `None` outside the unified Mamba pool.
|
||||
self.rem_mamba_slots = None
|
||||
if self._mamba_slot_cost:
|
||||
self.rem_mamba_slots = (
|
||||
self.token_to_kv_pool_allocator.mamba_allocator.schedulable_available_size()
|
||||
)
|
||||
if self.is_hybrid_ssm_cache:
|
||||
self.rem_mamba_slots += self.tree_cache.mamba_evictable_size()
|
||||
|
||||
self.priority_scheduling_preemption_threshold = (
|
||||
priority_scheduling_preemption_threshold
|
||||
)
|
||||
@@ -602,6 +633,21 @@ class PrefillAdder:
|
||||
budget += self.ceil_paged_tokens(swa_host_hit_length)
|
||||
return budget
|
||||
|
||||
def _mamba_gap_budget_for_req(self, req: Req) -> int:
|
||||
"""Shared-gap reservation (full-token-equivalents) for a request's new
|
||||
mamba state. Charged only on the SHARED Mamba pool (`_mamba_slot_cost > 0`)
|
||||
and only when the req has no state yet (`mamba_pool_idx is None`, mirroring
|
||||
`HybridReqToTokenPool.alloc`); 0 keeps baseline / SWA / non-Mamba unchanged.
|
||||
|
||||
Conservative by design (`_mamba_slot_cost` rounds UP). Does NOT reserve
|
||||
radix COW headroom or locked-but-evictable bytes — that residual is
|
||||
backstopped by the fail-loud RuntimeError in `alloc_req_slots`. FIXME: if
|
||||
over-admission crashes under pressure, make this more conservative (e.g.
|
||||
multiply by `MAMBA_STATE_PER_REQ_PREFIX_CACHE`)."""
|
||||
if self._mamba_slot_cost and req.mamba_pool_idx is None:
|
||||
return self._mamba_slot_cost
|
||||
return 0
|
||||
|
||||
def ceil_paged_tokens(self, tokens: int) -> int:
|
||||
return -(-tokens // self.page_size) * self.page_size
|
||||
|
||||
@@ -609,6 +655,10 @@ class PrefillAdder:
|
||||
no_token = self.rem_total_tokens <= 0 or self.cur_rem_tokens <= 0
|
||||
if not no_token and self.is_hybrid_swa:
|
||||
no_token = self.rem_swa_tokens <= 0
|
||||
# Gate new mamba slots separately: rem_total_tokens' full_evictable can't
|
||||
# cover a mamba slot, which needs mamba-recoverable bytes (see __init__).
|
||||
if not no_token and self.rem_mamba_slots is not None:
|
||||
no_token = self.rem_mamba_slots <= 0
|
||||
if no_token:
|
||||
return AddReqResult.NO_TOKEN
|
||||
|
||||
@@ -630,14 +680,27 @@ class PrefillAdder:
|
||||
extend_input_len: int,
|
||||
max_new_tokens: int,
|
||||
retracted_stain: bool,
|
||||
mamba_gap_reserve: int = 0,
|
||||
):
|
||||
# TODO(lsyin): check this workaround logic, which only ensures the prefill will not out of memory, and may be too conservative
|
||||
extend_input_len = self.ceil_paged_tokens(extend_input_len)
|
||||
|
||||
# alloc_extend reserves an extra page_size per request to make sure the budget doesn't over-commit
|
||||
page_overhead = self.page_size
|
||||
self.rem_total_token_offset += extend_input_len + max_new_tokens + page_overhead
|
||||
self.cur_rem_token_offset += extend_input_len + page_overhead
|
||||
# `mamba_gap_reserve` (shared Mamba pool only; 0 otherwise) charges the new
|
||||
# mamba state's shared-gap cost to BOTH full budgets: the slot is allocated
|
||||
# immediately (counts against `cur_rem`) and held for the request lifetime
|
||||
# (counts against `rem_total`). See `_mamba_gap_budget_for_req`.
|
||||
self.rem_total_token_offset += (
|
||||
extend_input_len + max_new_tokens + page_overhead + mamba_gap_reserve
|
||||
)
|
||||
self.cur_rem_token_offset += (
|
||||
extend_input_len + page_overhead + mamba_gap_reserve
|
||||
)
|
||||
# The new mamba slot also consumes one mamba-recoverable slot (gated
|
||||
# separately so full_evictable can't cover it — see __init__).
|
||||
if mamba_gap_reserve and self.rem_mamba_slots is not None:
|
||||
self.rem_mamba_slots -= 1
|
||||
self.rem_input_tokens -= extend_input_len
|
||||
|
||||
if self.is_hybrid_swa:
|
||||
@@ -681,7 +744,13 @@ class PrefillAdder:
|
||||
|
||||
self.can_run_list.append(req)
|
||||
|
||||
self._update_prefill_budget(prefix_len, trunc_len, 0, req.retracted_stain)
|
||||
self._update_prefill_budget(
|
||||
prefix_len,
|
||||
trunc_len,
|
||||
0,
|
||||
req.retracted_stain,
|
||||
mamba_gap_reserve=self._mamba_gap_budget_for_req(req),
|
||||
)
|
||||
|
||||
def _req_inc_lock_ref(self, req: Req):
|
||||
result = self.tree_cache.inc_lock_ref(req.last_node)
|
||||
@@ -711,7 +780,11 @@ class PrefillAdder:
|
||||
else 0
|
||||
)
|
||||
self._update_prefill_budget(
|
||||
0, req.extend_range.length, max_new_tokens, req.retracted_stain
|
||||
0,
|
||||
req.extend_range.length,
|
||||
max_new_tokens,
|
||||
req.retracted_stain,
|
||||
mamba_gap_reserve=self._mamba_gap_budget_for_req(req),
|
||||
)
|
||||
|
||||
# Return based on remaining token availability
|
||||
@@ -755,6 +828,7 @@ class PrefillAdder:
|
||||
else 0
|
||||
),
|
||||
req.retracted_stain,
|
||||
mamba_gap_reserve=self._mamba_gap_budget_for_req(req),
|
||||
)
|
||||
|
||||
# Return if chunked prefill not finished
|
||||
@@ -782,6 +856,9 @@ class PrefillAdder:
|
||||
req.prefix_indices
|
||||
)
|
||||
paged_input = self.ceil_paged_tokens(cand_extend_input_len)
|
||||
# Shared Mamba pool: fold the new mamba state's shared-gap cost into the
|
||||
# budget gate so admission can't over-commit (0 for baseline / non-Mamba).
|
||||
paged_input += self._mamba_gap_budget_for_req(req)
|
||||
if paged_input > min(self.cur_rem_tokens, self.rem_total_tokens):
|
||||
return AddReqResult.NO_TOKEN
|
||||
if self.is_hybrid_swa:
|
||||
@@ -863,6 +940,7 @@ class PrefillAdder:
|
||||
req.extend_range.length,
|
||||
min(req.sampling_params.max_new_tokens, CLIP_MAX_NEW_TOKENS),
|
||||
req.retracted_stain,
|
||||
mamba_gap_reserve=self._mamba_gap_budget_for_req(req),
|
||||
)
|
||||
else:
|
||||
if self.rem_chunk_tokens <= 0:
|
||||
@@ -877,7 +955,13 @@ class PrefillAdder:
|
||||
)
|
||||
self.can_run_list.append(req)
|
||||
self.new_chunked_req = req
|
||||
self._update_prefill_budget(0, trunc_len, 0, req.retracted_stain)
|
||||
self._update_prefill_budget(
|
||||
0,
|
||||
trunc_len,
|
||||
0,
|
||||
req.retracted_stain,
|
||||
mamba_gap_reserve=self._mamba_gap_budget_for_req(req),
|
||||
)
|
||||
|
||||
return self.budget_state()
|
||||
|
||||
@@ -906,11 +990,9 @@ class PrefillAdder:
|
||||
if req.sampling_params.ignore_eos and getattr(self.tree_cache, "disable", True):
|
||||
return self.add_one_req_ignore_eos(req)
|
||||
|
||||
# Reserve page_size for page-alignment overhead. The paged allocator
|
||||
# may consume up to one extra page per request (see alloc_extend), and
|
||||
# _update_prefill_budget already accounts for this in the deduction.
|
||||
# Without this, admission is more optimistic than the actual budget
|
||||
# deduction, allowing over-admission when the pool is nearly full.
|
||||
# Reserve page_size for page-alignment overhead: the paged allocator may
|
||||
# consume one extra page per request (see alloc_extend), which
|
||||
# _update_prefill_budget also deducts.
|
||||
max_new = min(
|
||||
max(req.sampling_params.max_new_tokens - len(req.output_ids), 0),
|
||||
CLIP_MAX_NEW_TOKENS,
|
||||
@@ -919,6 +1001,9 @@ class PrefillAdder:
|
||||
req.prefix_indices
|
||||
)
|
||||
total_tokens = cand_extend_input_len + max_new + self.page_size
|
||||
# Shared Mamba pool: fold the new mamba state's shared-gap cost into
|
||||
# `total_tokens` so both `rem_total_tokens` gates reflect the joint budget.
|
||||
total_tokens += self._mamba_gap_budget_for_req(req)
|
||||
|
||||
# adjusting the input_tokens based on host_hit_length and page_size
|
||||
real_input_tokens = cand_extend_input_len - req.host_hit_length
|
||||
@@ -1009,6 +1094,7 @@ class PrefillAdder:
|
||||
CLIP_MAX_NEW_TOKENS,
|
||||
),
|
||||
req.retracted_stain,
|
||||
mamba_gap_reserve=self._mamba_gap_budget_for_req(req),
|
||||
)
|
||||
else:
|
||||
# Make sure at least one page is available
|
||||
@@ -1045,7 +1131,11 @@ class PrefillAdder:
|
||||
|
||||
self._req_inc_lock_ref(req)
|
||||
self._update_prefill_budget(
|
||||
prefix_len, trunc_len, 0, req.retracted_stain
|
||||
prefix_len,
|
||||
trunc_len,
|
||||
0,
|
||||
req.retracted_stain,
|
||||
mamba_gap_reserve=self._mamba_gap_budget_for_req(req),
|
||||
)
|
||||
|
||||
return self.budget_state()
|
||||
|
||||
@@ -1496,7 +1496,7 @@ class Scheduler(
|
||||
if self.device == "cpu":
|
||||
self.schedule_stream.synchronize = lambda: None # No-op for CPU
|
||||
# The global WAR barrier fences the scheduler's next shared-buffer write
|
||||
# on the previous forward's read of the shared pool.
|
||||
# on the previous forward's read of the unified memory pool.
|
||||
self._war_barrier_enabled = is_cuda() or envs.SGLANG_ENABLE_WAR_BARRIER.get()
|
||||
with self.device_module.StreamContext(self.schedule_stream):
|
||||
dispatch_event_loop(self)
|
||||
@@ -1580,6 +1580,14 @@ class Scheduler(
|
||||
# we can process the last batch immediately.
|
||||
if disable_overlap_for_batch:
|
||||
pop_and_process()
|
||||
# Opportunistic flush at the disable_overlap sync boundary:
|
||||
# forward_stream is idle (prev forward drained, next not launched),
|
||||
# so `_flush`'s non-urgent guard compacts freely. Sync-free, best-effort.
|
||||
if self.server_args.enable_unified_memory:
|
||||
try:
|
||||
self.token_to_kv_pool_allocator.flush_opportunistic()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Launch the current batch
|
||||
if batch:
|
||||
@@ -2527,7 +2535,6 @@ class Scheduler(
|
||||
release_kv_cache(req, self.tree_cache, is_insert=False)
|
||||
|
||||
self.chunked_req = None
|
||||
self._chunked_req_scheduled_last_iter = False
|
||||
self._pending_chunked_abort_req = None
|
||||
self.ipc_channels.send_to_tokenizer.send_output(AbortReq(rid=req.rid), req)
|
||||
logger.debug(f"Abort chunked prefill request. {req.rid=}")
|
||||
@@ -3237,6 +3244,20 @@ class Scheduler(
|
||||
self.batch_record_buf[self.batch_record_ct].extend(
|
||||
batch_result.extra_keep_alive_refs
|
||||
)
|
||||
if self.server_args.enable_unified_memory:
|
||||
# Record a `forward_done` event after the forward (before
|
||||
# copy_to_cpu); lazy-compaction `_flush` gates src reuse on
|
||||
# it. Only the unified pool's allocator exposes these hooks.
|
||||
allocator = self.token_to_kv_pool_allocator
|
||||
forward_done = self.device_module.Event()
|
||||
forward_done.record(stream=self.forward_stream)
|
||||
allocator.set_latest_forward_done_event(forward_done)
|
||||
# Write-set classification: hand the allocator this
|
||||
# forward's virtual out_cache_loc as a tensor ref (no GPU work).
|
||||
allocator.set_inflight_forward(
|
||||
forward_done,
|
||||
batch.out_cache_loc,
|
||||
)
|
||||
# FIXME(lsyin): maybe move this to forward_batch_generation
|
||||
batch_result.copy_done = self.device_module.Event()
|
||||
if batch_result.delay_sample_func is None:
|
||||
@@ -3497,6 +3518,12 @@ class Scheduler(
|
||||
if not self.is_fully_idle():
|
||||
return
|
||||
|
||||
if self.server_args.enable_unified_memory:
|
||||
try:
|
||||
self.token_to_kv_pool_allocator.flush_opportunistic()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# memory leak check (skipped for hisparse — pool counters intentionally
|
||||
# diverge during host-backup, see _get_swa_token_info clamp).
|
||||
if not self.enable_hisparse:
|
||||
|
||||
@@ -88,8 +88,16 @@ class SchedulerInvariantChecker:
|
||||
protected = self.tree_cache.full_protected_size()
|
||||
session_held = self.pool_stats_observer.session_held_full_tokens()
|
||||
total = self.full_tokens_per_layer
|
||||
elif self.is_hybrid_ssm and self.tree_cache.supports_mamba():
|
||||
protected = self.tree_cache.full_protected_size()
|
||||
elif self.is_hybrid_ssm:
|
||||
# Branch on cache type for the protected accessor (MambaRadixCache
|
||||
# splits full/mamba; ChunkCache only has the single protected_size).
|
||||
# Use the allocator's `.size` for `total`: static max_total_num_tokens for
|
||||
# non-unified pools, the dynamic byte-coordinated cap (matching
|
||||
# `available_size`) for the unified pool.
|
||||
if self.tree_cache.supports_mamba():
|
||||
protected = self.tree_cache.full_protected_size()
|
||||
else:
|
||||
protected = self.tree_cache.protected_size()
|
||||
session_held = self.pool_stats_observer.session_held_tokens()
|
||||
total = self.token_to_kv_pool_allocator.size
|
||||
else:
|
||||
|
||||
@@ -48,6 +48,13 @@ class MambaSlotAllocator:
|
||||
def available_size(self) -> int:
|
||||
return len(self.free_slots)
|
||||
|
||||
def schedulable_available_size(self) -> int:
|
||||
"""Planner-facing free count. Identity to ``available_size`` for the
|
||||
static pool (slot-count and byte-coordinated views coincide); the shared
|
||||
``UnifiedMambaSlotAllocator`` overrides it with the byte-coordinated view.
|
||||
Lets ``alloc_req_slots`` call it uniformly without a getattr fallback."""
|
||||
return self.available_size()
|
||||
|
||||
def alloc_group_begin(self, num_reqs: int):
|
||||
"""Pre-allocate a batch of slots for match_prefix to amortize overhead."""
|
||||
self._alloc_iter = None
|
||||
|
||||
@@ -112,6 +112,15 @@ class SWATokenToKVPoolAllocator(BaseTokenToKVPoolAllocator):
|
||||
def swa_available_size(self):
|
||||
return self.swa_attn_allocator.available_size()
|
||||
|
||||
# Slot-conservation views for the leak invariant. On the non-shared allocator
|
||||
# the static budget IS physical (conserve == physical); the shared composite
|
||||
# overrides these with the static-cap view.
|
||||
def _conserve_full_available_size(self):
|
||||
return self.full_available_size()
|
||||
|
||||
def _conserve_swa_available_size(self):
|
||||
return self.swa_available_size()
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
return min(self._size_full, self._size_swa)
|
||||
|
||||
@@ -87,11 +87,8 @@ def free_swa_out_of_window_slots(
|
||||
req.swa_evicted_seqlen = max(req.swa_evicted_seqlen, evict_floor)
|
||||
|
||||
# Subtract an extra page_size so the eviction frontier never reaches the
|
||||
# radix tree insert boundary (page_floor(seq_len)). This keeps at least one
|
||||
# page of non-evicted SWA KV for the tree to store as a non-tombstone node,
|
||||
# preserving cache reuse in multi-turn scenarios. Without this, leaf nodes
|
||||
# may become tombstoned, causing SWA memory leak.
|
||||
# See also: _insert_helper case 3 in swa_radix_cache.py (defensive counterpart).
|
||||
# radix tree insert boundary, keeping >=1 page of non-evicted SWA KV for the
|
||||
# tree to store as a non-tombstone node (else leaf nodes tombstone -> SWA leak).
|
||||
if drop_page_margin or envs.SGLANG_OPT_SWA_EVICT_DROP_PAGE_MARGIN.get():
|
||||
evict_threshold = pre_len - sliding_window_size
|
||||
else:
|
||||
@@ -220,9 +217,7 @@ def get_alloc_len_per_decode(server_args: Optional[ServerArgs] = None) -> int:
|
||||
if server_args.speculative_algorithm is None:
|
||||
return 1
|
||||
|
||||
# Spec decoding allocates max(topk * num_steps, num_draft_tokens) per
|
||||
# decode step (draft chain and verify block share the reservation).
|
||||
|
||||
# Spec decoding allocates max(topk * num_steps, num_draft_tokens) per decode step.
|
||||
spec_steps = server_args.speculative_num_steps or 1
|
||||
spec_topk = server_args.speculative_eagle_topk or 1
|
||||
spec_tokens = server_args.max_speculative_num_draft_tokens
|
||||
@@ -234,10 +229,9 @@ def get_alloc_len_per_decode(server_args: Optional[ServerArgs] = None) -> int:
|
||||
if page_size == 1 or spec_topk == 1 or not spec_algo.has_draft_kv():
|
||||
return max(spec_steps * spec_topk, spec_tokens)
|
||||
else:
|
||||
# page_size > 1 + topk > 1 (spec v2 tree): worst-case page-aligned tree
|
||||
# footprint. Per topk branch needs ceil((last_page_len + num_steps) / page)
|
||||
# pages; the partial tail page can be up to page_size - 1, and each branch
|
||||
# gets its own (duplicated) copy -- so reserve for all topk branches.
|
||||
# spec v2 tree (page>1, topk>1): worst-case page-aligned footprint per
|
||||
# topk branch is ceil((page_size-1 + num_steps) / page) pages, each branch
|
||||
# duplicated -- reserve for all topk branches.
|
||||
num_new_pages_per_topk = (
|
||||
(page_size - 1) + spec_steps + page_size - 1
|
||||
) // page_size
|
||||
@@ -256,12 +250,10 @@ def get_alloc_reserve_per_decode(server_args: Optional[ServerArgs] = None) -> in
|
||||
def get_req_to_token_extra_context_len(server_args: ServerArgs) -> int:
|
||||
"""req_to_token row headroom beyond the model context length.
|
||||
|
||||
Sized to hold the decode over-allocation (kv_committed_len +
|
||||
get_alloc_reserve_per_decode). The spec v2 page>1 topk>1 holey draft footprint
|
||||
can outgrow the default num_draft_tokens headroom (PR #26972).
|
||||
Sized to hold the decode over-allocation; the spec v2 page>1 topk>1 holey
|
||||
draft footprint can outgrow the default num_draft_tokens headroom.
|
||||
"""
|
||||
# FIXME(lsyin): this is the temporary fix for the context length issue when
|
||||
# using speculative decoding
|
||||
# FIXME(lsyin): temporary fix for the context length issue under spec decoding
|
||||
extra = 4 + (server_args.max_speculative_num_draft_tokens or 0)
|
||||
if (
|
||||
server_args.speculative_algorithm is not None
|
||||
@@ -327,13 +319,8 @@ def evict_from_tree_cache(tree_cache: BasePrefixCache | None, num_tokens: int):
|
||||
|
||||
|
||||
def _compute_dsv4_state_lens(batch, *, is_decode: bool):
|
||||
"""Per-req c{4,128}_state pool alloc lens (a ``DSV4StateLens``) for this
|
||||
alloc step. The DSV4-NPU allocator owns the computation (it also mutates the
|
||||
per-req cumulative state on each ``Req``); we just trigger it here, right
|
||||
before the paged alloc that consumes the result.
|
||||
|
||||
None on CUDA / non-V4 paths (allocator has no ``compute_dsv4_state_lens_*``)
|
||||
so the ``alloc_paged_token_slots_*`` forwarding stays a no-op.
|
||||
"""Per-req c{4,128}_state pool alloc lens (``DSV4StateLens``) for this step.
|
||||
None on CUDA / non-V4 paths (allocator has no ``compute_dsv4_state_lens_*``).
|
||||
"""
|
||||
allocator = batch.token_to_kv_pool_allocator
|
||||
if not hasattr(allocator, "compute_dsv4_state_lens_extend"):
|
||||
@@ -371,8 +358,7 @@ def alloc_paged_token_slots_extend(
|
||||
extra_alloc_kwargs = {}
|
||||
if is_dsv4:
|
||||
extra_alloc_kwargs["req_pool_indices"] = req_pool_indices
|
||||
# Pass the per-req tables in per call for the c-pool / state last_loc
|
||||
# lookup; the allocator holds no reference to the pool.
|
||||
# Per-call per-req tables for the c-pool / state last_loc lookup.
|
||||
if batch is not None:
|
||||
extra_alloc_kwargs["req_to_token_pool"] = batch.req_to_token_pool
|
||||
if dsv4_state_lens is not None:
|
||||
@@ -415,10 +401,20 @@ def alloc_req_slots(
|
||||
reqs: list[Req],
|
||||
tree_cache: BasePrefixCache | None,
|
||||
) -> list[int]:
|
||||
"""Allocate request slots from the pool."""
|
||||
"""Allocate request slots from the pool.
|
||||
|
||||
Fail-loud: raises ``RuntimeError`` if the pool can't satisfy the batch. An
|
||||
alloc failure here means the admission budget (``PrefillAdder``) was wrong
|
||||
and should surface rather than be masked.
|
||||
"""
|
||||
num_reqs = len(reqs)
|
||||
if isinstance(req_to_token_pool, HybridReqToTokenPool):
|
||||
mamba_available_size = req_to_token_pool.mamba_allocator.available_size()
|
||||
# Byte-coordinated for the shared allocator (accounts for the peer full
|
||||
# sub-pool's bytes); plain slot free count for the non-shared one.
|
||||
mamba_available_size = (
|
||||
req_to_token_pool.mamba_allocator.schedulable_available_size()
|
||||
)
|
||||
# Eviction headroom factor: 3x (or lazy variant) for radix COW, 1x for chunk.
|
||||
if tree_cache.supports_mamba():
|
||||
factor = (
|
||||
MAMBA_STATE_PER_REQ_PREFIX_CACHE_LAZY
|
||||
@@ -433,24 +429,19 @@ def alloc_req_slots(
|
||||
mamba_num = max(0, mamba_state_needed - mamba_available_size)
|
||||
tree_cache.evict(EvictParams(num_tokens=0, mamba_num=mamba_num))
|
||||
req_pool_indices = req_to_token_pool.alloc(reqs)
|
||||
|
||||
if req_pool_indices is None:
|
||||
raise RuntimeError(
|
||||
"alloc_req_slots runs out of memory. "
|
||||
"Please set a smaller number for `--max-running-requests`. "
|
||||
f"{req_to_token_pool.available_size()=}, "
|
||||
f"{num_reqs=}, "
|
||||
f"{req_to_token_pool.available_size()=}, {num_reqs=}, "
|
||||
)
|
||||
return req_pool_indices
|
||||
|
||||
|
||||
def _alloc_page_size(batch: ScheduleBatch) -> int:
|
||||
# DCP (HIP & CUDA only) swaps in a PagedTokenToKVPoolAllocator whose
|
||||
# page_size is server_args.page_size * dcp_size, so it can be > 1 even when
|
||||
# tree_cache.page_size (== server_args.page_size) is 1. Only on the HIP DCP
|
||||
# path do we branch on the real allocator's page_size so the paged path is
|
||||
# taken; everywhere else tree_cache.page_size is authoritative and the two
|
||||
# are equal (dcp_size == 1), so behavior is unchanged.
|
||||
# DCP swaps in an allocator whose page_size is server_args.page_size *
|
||||
# dcp_size, so it can be > 1 even when tree_cache.page_size is 1; branch on
|
||||
# the real allocator's page_size there. Elsewhere the two are equal.
|
||||
if (_is_hip or _is_cuda) and get_global_server_args().dcp_size > 1:
|
||||
return batch.tree_cache.token_to_kv_pool_allocator.page_size
|
||||
return batch.tree_cache.page_size
|
||||
@@ -462,10 +453,9 @@ def alloc_for_extend(
|
||||
"""
|
||||
Allocate KV cache for extend batch and write to req_to_token_pool.
|
||||
|
||||
Returns:
|
||||
out_cache_loc: allocated cache locations
|
||||
req_pool_indices_device: request pool indices as a device tensor
|
||||
req_pool_indices_cpu: request pool indices as a CPU tensor (host mirror)
|
||||
Returns ``(out_cache_loc, req_pool_indices_device, req_pool_indices_cpu)``
|
||||
(the last is the host/CPU mirror). ``alloc_req_slots`` raises ``RuntimeError``
|
||||
if the pool can't satisfy the batch (fail-loud — see its docstring).
|
||||
"""
|
||||
# free out-of-window swa tokens
|
||||
batch.maybe_evict_swa()
|
||||
@@ -478,7 +468,7 @@ def alloc_for_extend(
|
||||
prefix_lens_device = prefix_lens_cpu.to(batch.device, non_blocking=True)
|
||||
extend_lens_device = extend_lens_cpu.to(batch.device, non_blocking=True)
|
||||
|
||||
# Allocate req slots
|
||||
# Allocate req slots (raises RuntimeError if the pool is exhausted)
|
||||
req_pool_indices = alloc_req_slots(
|
||||
batch.req_to_token_pool, batch.reqs, batch.tree_cache
|
||||
)
|
||||
@@ -489,8 +479,6 @@ def alloc_for_extend(
|
||||
if _alloc_page_size(batch) == 1:
|
||||
out_cache_loc = alloc_token_slots(batch.tree_cache, batch.extend_num_tokens)
|
||||
else:
|
||||
# Since tree_cache.page_size is (page_size * dcp_world_size), for dcp
|
||||
# on cuda platform, always use alloc_paged_token_slots_extend
|
||||
# Paged allocation - build last_loc
|
||||
last_loc = [
|
||||
(t[-1:] if len(t) > 0 else torch.tensor([-1], device=batch.device))
|
||||
@@ -524,8 +512,7 @@ def alloc_for_extend(
|
||||
batch.req_to_token_pool,
|
||||
)
|
||||
|
||||
# DSV4-NPU hook: write c4/c128/swa per-req tables from the stashed bundle.
|
||||
# No-op on non-DSV4 paths (out_cache_loc_dsv4 stays None there).
|
||||
# DSV4-NPU hook: no-op on non-DSV4 paths.
|
||||
if _is_npu:
|
||||
maybe_write_dsv4_extend(
|
||||
batch,
|
||||
@@ -559,8 +546,7 @@ def alloc_paged_token_slots_decode(
|
||||
extra_alloc_kwargs = {}
|
||||
if is_dsv4:
|
||||
extra_alloc_kwargs["req_pool_indices"] = req_pool_indices
|
||||
# Per-call per-req tables for the last_loc lookup; the allocator holds
|
||||
# no reference to the pool.
|
||||
# Per-call per-req tables for the last_loc lookup.
|
||||
if batch is not None:
|
||||
extra_alloc_kwargs["req_to_token_pool"] = batch.req_to_token_pool
|
||||
if dsv4_state_lens is not None:
|
||||
@@ -633,8 +619,7 @@ def alloc_for_decode(batch: ScheduleBatch, token_per_req: int) -> torch.Tensor:
|
||||
(batch.req_pool_indices, locs), out_cache_loc.to(torch.int32)
|
||||
)
|
||||
|
||||
# DSV4-NPU hook: post-decode write of c4/c128/swa per-req tables from the
|
||||
# stashed bundle. No-op on non-DSV4 paths (out_cache_loc_dsv4 stays None).
|
||||
# DSV4-NPU hook: no-op on non-DSV4 paths.
|
||||
if _is_npu:
|
||||
maybe_write_dsv4_decode(
|
||||
batch,
|
||||
@@ -698,8 +683,7 @@ def release_kv_cache(req: Req, tree_cache: BasePrefixCache, is_insert: bool = Tr
|
||||
req.mamba_pool_idx is not None
|
||||
), "mamba state is freed while the tree cache does not manage mamba states"
|
||||
tree_cache.req_to_token_pool.free_mamba_cache(req)
|
||||
# The DSV4-NPU ReqToTokenPool subclass's free() additionally releases the
|
||||
# c4/c128 state pages; other ReqToTokenPool subclasses are a no-op here.
|
||||
# DSV4-NPU's free() also releases c4/c128 state pages; no-op for others.
|
||||
tree_cache.req_to_token_pool.free(req)
|
||||
|
||||
|
||||
|
||||
@@ -46,6 +46,9 @@ from sglang.srt.mem_cache.base_prefix_cache import (
|
||||
)
|
||||
from sglang.srt.mem_cache.events import KVCacheEventMixin
|
||||
from sglang.srt.mem_cache.memory_pool import HybridReqToTokenPool
|
||||
from sglang.srt.mem_cache.multi_ended_allocator import (
|
||||
UnifiedMambaTokenToKVPoolAllocator,
|
||||
)
|
||||
from sglang.srt.mem_cache.radix_cache import RadixKey
|
||||
from sglang.srt.mem_cache.utils import split_node_hash_value
|
||||
from sglang.srt.server_args import get_global_server_args
|
||||
@@ -420,9 +423,15 @@ class LRUList:
|
||||
|
||||
class MambaRadixCache(KVCacheEventMixin, BasePrefixCache):
|
||||
def __init__(self, params: CacheInitParams):
|
||||
assert isinstance(
|
||||
params.token_to_kv_pool_allocator, TokenToKVPoolAllocator
|
||||
) or isinstance(params.token_to_kv_pool_allocator, PagedTokenToKVPoolAllocator)
|
||||
assert (
|
||||
isinstance(params.token_to_kv_pool_allocator, TokenToKVPoolAllocator)
|
||||
or isinstance(
|
||||
params.token_to_kv_pool_allocator, PagedTokenToKVPoolAllocator
|
||||
)
|
||||
or isinstance(
|
||||
params.token_to_kv_pool_allocator, UnifiedMambaTokenToKVPoolAllocator
|
||||
)
|
||||
)
|
||||
self.req_to_token_pool: HybridReqToTokenPool = params.req_to_token_pool
|
||||
self.token_to_kv_pool_allocator = params.token_to_kv_pool_allocator
|
||||
self.mamba_cache_chunk_size = get_global_server_args().mamba_cache_chunk_size
|
||||
@@ -694,8 +703,12 @@ class MambaRadixCache(KVCacheEventMixin, BasePrefixCache):
|
||||
)
|
||||
else:
|
||||
mamba_value_donated = self._alloc_mamba_slot()
|
||||
# mamba_pool is a pure PHYSICAL store; translate both slot ids
|
||||
# virtual->physical (identity for the non-unified memory pool) before the copy.
|
||||
translate = self.req_to_token_pool.translate_mamba_indices
|
||||
self.req_to_token_pool.mamba_pool.copy_from(
|
||||
req.mamba_pool_idx.unsqueeze(0), mamba_value_donated
|
||||
translate(req.mamba_pool_idx.unsqueeze(0)),
|
||||
translate(mamba_value_donated),
|
||||
)
|
||||
|
||||
result = self.insert(
|
||||
|
||||
@@ -970,6 +970,14 @@ class HybridReqToTokenPool(ReqToTokenPool):
|
||||
def get_mamba_indices(self, req_indices: torch.Tensor) -> torch.Tensor:
|
||||
return self.req_index_to_mamba_index_mapping[req_indices]
|
||||
|
||||
def translate_mamba_indices(self, mamba_indices: torch.Tensor) -> torch.Tensor:
|
||||
"""Virtual->physical mamba-slot translate. Identity for a static pool
|
||||
(slots are physical); UnifiedHybridReqToTokenPool overrides it for the
|
||||
unified memory pool, where mamba slot ids are virtual. Callers translate
|
||||
before calling the pool's physical-id state ops (copy_from / clear_slots
|
||||
/ get_cpu_copy / load_cpu_copy)."""
|
||||
return mamba_indices
|
||||
|
||||
def mamba2_layer_cache(self, layer_id: int):
|
||||
assert layer_id in self.mamba_map
|
||||
if self.layer_transfer_counter is not None:
|
||||
@@ -1134,29 +1142,43 @@ class HybridReqToTokenPool(ReqToTokenPool):
|
||||
class KVWriteLoc:
|
||||
"""Write target(s) for ``KVCache.set_kv_buffer``.
|
||||
|
||||
``loc`` is the full-pool write location; ``swa_loc`` is the pre-translated
|
||||
full->SWA location for hybrid SWA pools (``None`` otherwise). Bundling them
|
||||
lets a backend issue one ``set_kv_buffer`` call regardless of pool type.
|
||||
All location info lives here (in the attention metadata), NOT in the pool:
|
||||
- ``loc``: the generic per-token write location (the allocated
|
||||
``out_cache_loc``). VIRTUAL under the unified memory pool (it indexes the
|
||||
virtual slot space); already physical for a non-unified memory pool.
|
||||
- ``swa_loc``: the pre-translated SWA-sub-pool PHYSICAL location for hybrid
|
||||
SWA pools (``None`` otherwise).
|
||||
- ``full_loc``: the pre-translated full-attention-sub-pool PHYSICAL location
|
||||
for the unified memory pool (``None`` otherwise), computed once per forward in
|
||||
attention metadata (``ForwardMetadata.out_cache_loc_full_physical``). The
|
||||
shared full pool writes it directly; the pool never translates (replacing
|
||||
the former per-layer v2p gather / ``set_full_loc`` pin).
|
||||
|
||||
``swa_loc`` and ``full_loc`` are the parallel pair (each a pre-resolved
|
||||
PHYSICAL loc into its sub-pool, mirroring ``swa_kv_pool`` / ``full_kv_pool``);
|
||||
``loc`` is the generic, possibly-virtual fallback. Bundling them lets a
|
||||
backend issue one ``set_kv_buffer`` call regardless of pool type.
|
||||
"""
|
||||
|
||||
loc: torch.Tensor
|
||||
swa_loc: Optional[torch.Tensor] = None
|
||||
full_loc: Optional[torch.Tensor] = None
|
||||
|
||||
def __post_init__(self):
|
||||
# swa_out_cache_loc is computed once at metadata-init time from the
|
||||
# full (possibly padded) out_cache_loc. Piecewise CUDA graphs later
|
||||
# narrow out_cache_loc to real_num_tokens per layer, so swa_loc can
|
||||
# be longer than loc. Slice to match since both are in the same
|
||||
# per-token order.
|
||||
# swa_loc / full_loc are resolved once at metadata-init from the full
|
||||
# (padded) out_cache_loc; piecewise/DP-padded paths later narrow loc per
|
||||
# layer, so slice these pre-resolved locs to match (same per-token order).
|
||||
if self.swa_loc is not None and self.swa_loc.shape[0] != self.loc.shape[0]:
|
||||
self.swa_loc = self.swa_loc[: self.loc.shape[0]]
|
||||
if self.full_loc is not None and self.full_loc.shape[0] != self.loc.shape[0]:
|
||||
self.full_loc = self.full_loc[: self.loc.shape[0]]
|
||||
|
||||
|
||||
def unwrap_write_loc(loc_info):
|
||||
"""Return ``(loc, swa_loc)`` from a ``KVWriteLoc`` or a bare loc tensor."""
|
||||
"""Return ``(loc, swa_loc, full_loc)`` from a ``KVWriteLoc`` or a bare loc."""
|
||||
if isinstance(loc_info, KVWriteLoc):
|
||||
return loc_info.loc, loc_info.swa_loc
|
||||
return loc_info, None
|
||||
return loc_info.loc, loc_info.swa_loc, loc_info.full_loc
|
||||
return loc_info, None, None
|
||||
|
||||
|
||||
class KVCache(abc.ABC):
|
||||
@@ -1648,7 +1670,7 @@ class MHATokenToKVPool(KVCache):
|
||||
layer_id_override: Optional[int] = None,
|
||||
dcp_kv_mask: Optional[torch.Tensor] = None,
|
||||
):
|
||||
loc, _ = unwrap_write_loc(loc_info)
|
||||
loc, _, _ = unwrap_write_loc(loc_info)
|
||||
# Catch stale slot ids here instead of as illegal-addr / silent KV
|
||||
# corruption in the store_kvcache write (gated on SGLANG_ENABLE_ASYNC_ASSERT).
|
||||
maybe_detect_oob(loc, 0, self.size + self.page_size, "set_kv_buffer (MHA)")
|
||||
@@ -2123,7 +2145,7 @@ class MHATokenToKVPoolFP4(MHATokenToKVPool):
|
||||
v_scale: Optional[float] = None,
|
||||
layer_id_override: Optional[int] = None,
|
||||
):
|
||||
loc, _ = unwrap_write_loc(loc_info)
|
||||
loc, _, _ = unwrap_write_loc(loc_info)
|
||||
maybe_detect_oob(loc, 0, self.size + self.page_size, "set_kv_buffer (MHA-FP4)")
|
||||
from sglang.srt.model_executor.runner import get_is_capture_mode
|
||||
|
||||
@@ -2346,6 +2368,9 @@ class HybridLinearKVPool(KVCache):
|
||||
qk_rope_head_dim: int = None,
|
||||
start_layer: Optional[int] = None,
|
||||
full_kv_pool_class: Optional[type] = None,
|
||||
# When provided (shared-KV-pool path), use this pool for the
|
||||
# full-attention layers instead of constructing one internally.
|
||||
full_kv_pool: Optional[KVCache] = None,
|
||||
):
|
||||
self.size = size
|
||||
self.dtype = dtype
|
||||
@@ -2357,8 +2382,15 @@ class HybridLinearKVPool(KVCache):
|
||||
self.head_num = head_num
|
||||
self.head_dim = head_dim
|
||||
self.mamba_pool = mamba_pool
|
||||
# virtual->physical mamba-slot translate for the HiCache offload path;
|
||||
# identity for a static pool, the allocator's `translate` for the unified pool.
|
||||
self._mamba_translate = lambda ids: ids
|
||||
self.use_mla = use_mla
|
||||
if not use_mla:
|
||||
if full_kv_pool is not None:
|
||||
# Shared-KV-pool path: the caller built a UnifiedMHATokenToKVPool
|
||||
# aliasing the shared byte buffer.
|
||||
self.full_kv_pool = full_kv_pool
|
||||
elif not use_mla:
|
||||
TokenToKVPoolClass = MHATokenToKVPool
|
||||
|
||||
if current_platform.is_out_of_tree():
|
||||
@@ -2492,11 +2524,16 @@ class HybridLinearKVPool(KVCache):
|
||||
v_scale: float = 1.0,
|
||||
dcp_kv_mask: Optional[torch.Tensor] = None,
|
||||
):
|
||||
# Write-location info lives in the metadata (`KVWriteLoc`). `full_loc` is the
|
||||
# unified pool's pre-translated PHYSICAL loc (None for a static pool, where
|
||||
# `loc` is already physical) — either way the pool writes a PHYSICAL loc.
|
||||
loc, _, full_loc = unwrap_write_loc(loc)
|
||||
layer_id = self._transfer_full_attention_id(layer.layer_id)
|
||||
if not self.use_mla:
|
||||
write_loc = full_loc if full_loc is not None else loc
|
||||
self.full_kv_pool.set_kv_buffer(
|
||||
None,
|
||||
loc,
|
||||
write_loc,
|
||||
cache_k,
|
||||
cache_v,
|
||||
k_scale,
|
||||
@@ -2518,8 +2555,9 @@ class HybridLinearKVPool(KVCache):
|
||||
|
||||
def get_cpu_copy(self, indices, mamba_indices=None):
|
||||
kv_cpu = self.full_kv_pool.get_cpu_copy(indices)
|
||||
# mamba_pool stores PHYSICAL ids; translate the (unified-pool virtual) ids first.
|
||||
mamba_cpu = (
|
||||
self.mamba_pool.get_cpu_copy(mamba_indices)
|
||||
self.mamba_pool.get_cpu_copy(self._mamba_translate(mamba_indices))
|
||||
if mamba_indices is not None
|
||||
else None
|
||||
)
|
||||
@@ -2529,7 +2567,9 @@ class HybridLinearKVPool(KVCache):
|
||||
kv_cpu, mamba_cpu = cache_cpu
|
||||
self.full_kv_pool.load_cpu_copy(kv_cpu, indices)
|
||||
if mamba_cpu is not None and mamba_indices is not None:
|
||||
self.mamba_pool.load_cpu_copy(mamba_cpu, mamba_indices)
|
||||
self.mamba_pool.load_cpu_copy(
|
||||
mamba_cpu, self._mamba_translate(mamba_indices)
|
||||
)
|
||||
|
||||
def get_v_head_dim(self):
|
||||
return self.full_kv_pool.get_value_buffer(0).shape[-1]
|
||||
@@ -2676,7 +2716,7 @@ class MLATokenToKVPool(KVCache):
|
||||
cache_k: torch.Tensor,
|
||||
cache_v: torch.Tensor,
|
||||
):
|
||||
loc, _ = unwrap_write_loc(loc_info)
|
||||
loc, _, _ = unwrap_write_loc(loc_info)
|
||||
maybe_detect_oob(loc, 0, self.size + self.page_size, "set_kv_buffer (MLA)")
|
||||
layer_id = layer.layer_id
|
||||
assert not self.dsa_kv_cache_store_fp8
|
||||
@@ -2882,7 +2922,7 @@ class MLATokenToKVPoolFP4(MLATokenToKVPool):
|
||||
cache_v: torch.Tensor,
|
||||
):
|
||||
# loc_info may be a KVWriteLoc; MLA pools have no SWA target.
|
||||
loc, _ = unwrap_write_loc(loc_info)
|
||||
loc, _, _ = unwrap_write_loc(loc_info)
|
||||
maybe_detect_oob(loc, 0, self.size + self.page_size, "set_kv_buffer (MLA-FP4)")
|
||||
layer_id = layer.layer_id
|
||||
assert not self.dsa_kv_cache_store_fp8
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -160,7 +160,7 @@ class SWAKVPool(BaseSWAKVPool):
|
||||
v_scale: float = 1.0,
|
||||
):
|
||||
# loc_info bundles the full loc and the pre-translated SWA loc.
|
||||
loc, swa_loc = unwrap_write_loc(loc_info)
|
||||
loc, swa_loc, _ = unwrap_write_loc(loc_info)
|
||||
layer_id = layer.layer_id
|
||||
layer_id_pool, is_swa_layer = self.layers_mapping[layer_id]
|
||||
if is_swa_layer:
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
# Copyright 2023-2026 SGLang Team
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ==============================================================================
|
||||
"""Virtual<->physical slot Triton kernels for the unified memory pool."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import torch
|
||||
import triton
|
||||
import triton.language as tl
|
||||
|
||||
# Fused take-physical-pages + bind for the alloc fast path. Invoked ONLY when
|
||||
# `_hole_count == 0`; otherwise the slow path drains holes first (Invariant B,
|
||||
# greedy hole reuse). Caller advances `watermark_physical` and checks overflow
|
||||
# BEFORE launch, passing the PRE-extension watermark. Cuda-graph safe (no
|
||||
# `.item()`, no tensor branching); runs on the scheduler thread.
|
||||
|
||||
|
||||
@triton.jit
|
||||
def alloc_bind_inplace_kernel(
|
||||
v_pages_ptr, # in: [N] int64 — virtual page ids
|
||||
v2p_ptr, # in/out: int64 — virtual_to_physical table
|
||||
p2v_ptr, # in/out: int64 — physical_to_virtual table
|
||||
out_phys_ptr, # out: [N] int64 — physical page ids
|
||||
N, # runtime: number of pages to allocate
|
||||
start_phys, # runtime: lowest physical page id in the new range
|
||||
BLOCK: tl.constexpr,
|
||||
):
|
||||
"""Fused: ascending arange + out_phys/v2p/p2v scatter.
|
||||
|
||||
Caller pre-adjusts `start_phys` per direction so the range is always
|
||||
ascending (grow-up: start_wm; grow-down: start_wm - N + 1), making the
|
||||
v->p mapping byte-identical to the `torch.arange` slow path.
|
||||
"""
|
||||
pid = tl.program_id(0)
|
||||
offs = pid * BLOCK + tl.arange(0, BLOCK)
|
||||
mask = offs < N
|
||||
|
||||
phys = (start_phys + offs).to(tl.int64)
|
||||
v = tl.load(v_pages_ptr + offs, mask=mask, other=0).to(tl.int64)
|
||||
|
||||
# Masked stores skip out-of-range lanes, and `other=0` keeps us off the
|
||||
# v2p[0]/p2v[0] padding-sink slot.
|
||||
tl.store(out_phys_ptr + offs, phys, mask=mask)
|
||||
tl.store(v2p_ptr + v, phys, mask=mask)
|
||||
tl.store(p2v_ptr + phys, v, mask=mask)
|
||||
|
||||
|
||||
ALLOC_BIND_BLOCK = 128
|
||||
|
||||
|
||||
def alloc_bind_inplace(
|
||||
v_pages: torch.Tensor,
|
||||
v2p: torch.Tensor,
|
||||
p2v: torch.Tensor,
|
||||
start_phys: int,
|
||||
) -> torch.Tensor:
|
||||
"""Allocate N ascending physical pages from `start_phys` and bind to `v_pages`.
|
||||
|
||||
Caller must advance `watermark_physical` by N and verify overflow BEFORE
|
||||
calling; this launcher does neither.
|
||||
"""
|
||||
N = int(v_pages.numel())
|
||||
if N == 0:
|
||||
return torch.empty(0, dtype=torch.int64, device=v_pages.device)
|
||||
if not v_pages.is_cuda:
|
||||
# Pure-torch CPU reference for the CUDA-only kernel.
|
||||
phys_pages = torch.arange(
|
||||
start_phys, start_phys + N, dtype=torch.int64, device=v_pages.device
|
||||
)
|
||||
v = v_pages.to(torch.int64)
|
||||
v2p[v] = phys_pages
|
||||
p2v[phys_pages] = v
|
||||
return phys_pages
|
||||
phys_pages = torch.empty(N, dtype=torch.int64, device=v_pages.device)
|
||||
grid = (triton.cdiv(N, ALLOC_BIND_BLOCK),)
|
||||
alloc_bind_inplace_kernel[grid](
|
||||
v_pages,
|
||||
v2p,
|
||||
p2v,
|
||||
phys_pages,
|
||||
N,
|
||||
start_phys,
|
||||
BLOCK=ALLOC_BIND_BLOCK,
|
||||
)
|
||||
return phys_pages
|
||||
@@ -333,8 +333,12 @@ class MambaComponent(TreeComponent):
|
||||
)
|
||||
else:
|
||||
mamba_value_donated = self._alloc_mamba_slot()
|
||||
# mamba_pool is a pure PHYSICAL store; translate both slot ids
|
||||
# virtual->physical (identity for the non-unified memory pool) first.
|
||||
translate = self.cache.req_to_token_pool.translate_mamba_indices
|
||||
self.cache.req_to_token_pool.mamba_pool.copy_from(
|
||||
req.mamba_pool_idx.unsqueeze(0), mamba_value_donated
|
||||
translate(req.mamba_pool_idx.unsqueeze(0)),
|
||||
translate(mamba_value_donated),
|
||||
)
|
||||
insert_params.mamba_value = mamba_value_donated
|
||||
return cache_len
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -62,6 +62,14 @@ if TYPE_CHECKING:
|
||||
from sglang.srt.model_executor.pool_configurator import MemoryPoolConfig
|
||||
|
||||
|
||||
def _should_enable_lazy_compaction() -> bool:
|
||||
"""Lazy compaction default — ON unless
|
||||
`SGLANG_DISABLE_LAZY_COMPACTION=1` (escape hatch for A/B / rollback).
|
||||
Centralized here so both unified-memory-pool factory call sites stay in sync.
|
||||
"""
|
||||
return not envs.SGLANG_DISABLE_LAZY_COMPACTION.get()
|
||||
|
||||
|
||||
# the ratio of mamba cache pool size to max_running_requests
|
||||
MAMBA_CACHE_SIZE_MAX_RUNNING_REQUESTS_RATIO = 3
|
||||
MAMBA_CACHE_V2_ADDITIONAL_RATIO_OVERLAP = 2
|
||||
@@ -341,10 +349,185 @@ class ModelRunnerKVCacheMixin:
|
||||
"attention, no HiSparse, and --kv-cache-dtype != fp4_e2m1."
|
||||
)
|
||||
|
||||
def _init_unified_mamba_pools(self: ModelRunner, max_num_reqs: int):
|
||||
"""Build the shared-KV-pool stack for a hybrid-Mamba model:
|
||||
one byte buffer split between the full-attn MHA KV pool and the
|
||||
per-request Mamba state pool, with virtual slot ids above the
|
||||
allocator."""
|
||||
from sglang.srt.mem_cache.unified_memory_pool import init_unified_mamba_pools
|
||||
|
||||
config = self.mambaish_config
|
||||
assert config is not None
|
||||
assert (
|
||||
not self.use_mla_backend
|
||||
), "unified memory pool does not support MLA-hybrid-Mamba yet"
|
||||
# The full sub-pool is page-aware (via `MultiEndedAllocator(page_size=...)`);
|
||||
# the mamba sub-pool stays page=1.
|
||||
assert self.page_size >= 1, f"page_size must be >= 1, got {self.page_size}"
|
||||
# Mirror the non-shared path's extra_max_context_len computation.
|
||||
extra_max_context_len = 4
|
||||
if self.server_args.speculative_num_draft_tokens is not None:
|
||||
extra_max_context_len += self.server_args.speculative_num_draft_tokens
|
||||
|
||||
mamba_layer_ids = [
|
||||
i
|
||||
for i in config.mamba2_cache_params.layers
|
||||
if self.start_layer <= i < self.end_layer
|
||||
]
|
||||
full_attention_layer_ids = [
|
||||
i
|
||||
for i in config.full_attention_layer_ids
|
||||
if self.start_layer <= i < self.end_layer
|
||||
]
|
||||
|
||||
bundle = init_unified_mamba_pools(
|
||||
device=self.device,
|
||||
kv_cache_dtype=self.kv_cache_dtype,
|
||||
head_num=self.model_config.get_num_kv_heads(get_attention_tp_size()),
|
||||
head_dim=self.model_config.head_dim,
|
||||
page_size=self.page_size,
|
||||
start_layer=self.start_layer,
|
||||
end_layer=self.end_layer,
|
||||
is_draft_worker=self.is_draft_worker,
|
||||
use_mla_backend=self.use_mla_backend,
|
||||
mamba_layer_ids=mamba_layer_ids,
|
||||
full_attention_layer_ids=full_attention_layer_ids,
|
||||
mamba2_cache_params=config.mamba2_cache_params,
|
||||
model_context_len=self.model_config.context_len,
|
||||
extra_max_context_len=extra_max_context_len,
|
||||
max_total_num_tokens=self.max_total_num_tokens,
|
||||
max_mamba_cache_size=self.server_args.max_mamba_cache_size,
|
||||
max_num_reqs=max_num_reqs,
|
||||
enable_memory_saver=self.server_args.enable_memory_saver,
|
||||
enable_mamba_extra_buffer=self.server_args.enable_mamba_extra_buffer(),
|
||||
speculative_num_draft_tokens=self.server_args.speculative_num_draft_tokens,
|
||||
disable_overlap_schedule=self.server_args.disable_overlap_schedule,
|
||||
need_sort=self.server_args.disaggregation_mode in ("decode", "prefill"),
|
||||
mamba_full_memory_ratio=self.server_args.mamba_full_memory_ratio,
|
||||
# Overlap mode: the allocator's `free` drops a wait_stream(forward_stream)
|
||||
# barrier so eager compaction serializes after the in-flight forward's
|
||||
# v2p/KV reads. Near-no-op in normal mode.
|
||||
forward_stream=self.forward_stream,
|
||||
# Lazy compaction: default ON, env-var escape hatch for rollback / A/B.
|
||||
lazy_compaction=_should_enable_lazy_compaction(),
|
||||
)
|
||||
self.req_to_token_pool = bundle.req_to_token_pool
|
||||
self.token_to_kv_pool = bundle.token_to_kv_pool
|
||||
self.token_to_kv_pool_allocator = bundle.token_to_kv_pool_allocator
|
||||
# Keep a reference so the shared byte buffer is not GC'd.
|
||||
self._unified_memory_pool = bundle.unified_memory_pool
|
||||
|
||||
def _init_unified_swa_pools(self: ModelRunner, max_num_reqs: int):
|
||||
"""Build the unified-pool stack for a hybrid-SWA model (Triton): one byte
|
||||
buffer split between the full-attention and SWA KV pools."""
|
||||
from sglang.srt.mem_cache.unified_memory_pool import init_unified_swa_pools
|
||||
|
||||
assert self.is_hybrid_swa, "_init_unified_swa_pools called on a non-SWA model"
|
||||
# Both sub-pools are page-aware; the SWA composite runs alloc_extend_kernel
|
||||
# once in virtual space and binds the new pages on both sub-allocators.
|
||||
assert self.page_size >= 1, f"page_size must be >= 1, got {self.page_size}"
|
||||
assert (
|
||||
not self.use_mla_backend
|
||||
), "unified memory pool does not support MLA-SWA hybrid yet"
|
||||
# Mirror the non-shared path's extra_max_context_len computation.
|
||||
extra_max_context_len = 4
|
||||
if self.server_args.speculative_num_draft_tokens is not None:
|
||||
extra_max_context_len += self.server_args.speculative_num_draft_tokens
|
||||
self.req_to_token_pool = ReqToTokenPool(
|
||||
size=max_num_reqs,
|
||||
max_context_len=self.model_config.context_len + extra_max_context_len,
|
||||
device=self.device,
|
||||
enable_memory_saver=self.server_args.enable_memory_saver,
|
||||
)
|
||||
|
||||
head_num = self.model_config.get_num_kv_heads(get_attention_tp_size())
|
||||
head_dim = self.model_config.head_dim
|
||||
if self.is_hybrid_swa_compress:
|
||||
# Asymmetric head dims between full and SWA (NPU compress path):
|
||||
# pull SWA-specific dims from the hf text config.
|
||||
v_head_dim = self.model_config.hf_text_config.v_head_dim
|
||||
swa_head_num = max(
|
||||
1,
|
||||
self.model_config.hf_text_config.swa_num_key_value_heads
|
||||
// get_attention_tp_size(),
|
||||
)
|
||||
swa_head_dim = self.model_config.hf_text_config.swa_head_dim
|
||||
swa_v_head_dim = self.model_config.hf_text_config.swa_v_head_dim
|
||||
else:
|
||||
v_head_dim = head_dim
|
||||
swa_head_num = head_num
|
||||
swa_head_dim = head_dim
|
||||
swa_v_head_dim = head_dim
|
||||
|
||||
# Filter layer ids to this worker's [start_layer, end_layer) range.
|
||||
swa_attention_layer_ids = [
|
||||
i
|
||||
for i in self.model_config.swa_attention_layer_ids
|
||||
if self.start_layer <= i < self.end_layer
|
||||
]
|
||||
full_attention_layer_ids = [
|
||||
i
|
||||
for i in self.model_config.full_attention_layer_ids
|
||||
if self.start_layer <= i < self.end_layer
|
||||
]
|
||||
|
||||
bundle = init_unified_swa_pools(
|
||||
device=self.device,
|
||||
kv_cache_dtype=self.kv_cache_dtype,
|
||||
head_num=head_num,
|
||||
head_dim=head_dim,
|
||||
v_head_dim=v_head_dim,
|
||||
swa_head_num=swa_head_num,
|
||||
swa_head_dim=swa_head_dim,
|
||||
swa_v_head_dim=swa_v_head_dim,
|
||||
page_size=self.page_size,
|
||||
start_layer=self.start_layer,
|
||||
end_layer=self.end_layer,
|
||||
swa_attention_layer_ids=swa_attention_layer_ids,
|
||||
full_attention_layer_ids=full_attention_layer_ids,
|
||||
full_max_total_num_tokens=self.full_max_total_num_tokens,
|
||||
swa_max_total_num_tokens=self.swa_max_total_num_tokens,
|
||||
enable_memory_saver=self.server_args.enable_memory_saver,
|
||||
need_sort=self.server_args.disaggregation_mode in ("decode", "prefill"),
|
||||
# Overlap mode: same wait_stream(forward_stream) rationale as
|
||||
# `_init_unified_mamba_pools`.
|
||||
forward_stream=self.forward_stream,
|
||||
# Lazy compaction: default ON, with env var escape hatch for rollback / A/B.
|
||||
lazy_compaction=_should_enable_lazy_compaction(),
|
||||
)
|
||||
self.token_to_kv_pool = bundle.token_to_kv_pool
|
||||
self.token_to_kv_pool_allocator = bundle.token_to_kv_pool_allocator
|
||||
# Keep a reference so the shared byte buffer is not GC'd.
|
||||
self._unified_memory_pool = bundle.unified_memory_pool
|
||||
|
||||
def _init_pools(self: ModelRunner):
|
||||
"""Initialize the memory pools."""
|
||||
max_num_reqs = self.max_running_requests
|
||||
|
||||
# Unified-pool fast path: build req_to_token + token_to_kv pool + allocator
|
||||
# from one byte buffer, then return. Gated to the target worker
|
||||
# (req_to_token_pool is None); supports hybrid Mamba and hybrid SWA (not DSV4).
|
||||
if (
|
||||
self.server_args.enable_unified_memory
|
||||
and self.server_args.disaggregation_mode == "null"
|
||||
and self.req_to_token_pool is None
|
||||
):
|
||||
if self.mambaish_config is not None:
|
||||
self._init_unified_mamba_pools(max_num_reqs)
|
||||
return
|
||||
if self.is_hybrid_swa and not is_deepseek_v4(self.model_config.hf_config):
|
||||
self._init_unified_swa_pools(max_num_reqs)
|
||||
return
|
||||
# Fail loud, not silently fall through to the normal pools (which would
|
||||
# leave the flag a no-op). The feature replaces the HYBRID pools only.
|
||||
raise ValueError(
|
||||
"--enable-unified-memory only supports hybrid Mamba and "
|
||||
"hybrid sliding-window-attention models (DeepSeek-V4 excluded); "
|
||||
f"the current model ({self.model_config.hf_config.architectures}) "
|
||||
"is neither, so the unified memory pool cannot be built. Drop "
|
||||
"--enable-unified-memory for this model."
|
||||
)
|
||||
|
||||
# Initialize req_to_token_pool
|
||||
if self.req_to_token_pool is None:
|
||||
max_spec_draft_tokens = self.server_args.max_speculative_num_draft_tokens
|
||||
|
||||
@@ -157,6 +157,11 @@ def build_replay_fb_view(
|
||||
encoder_lens=buffers.encoder_lens[:bs] if is_encoder_decoder else None,
|
||||
out_cache_loc=getattr(forward_batch, "out_cache_loc", None),
|
||||
out_cache_loc_dsv4=getattr(forward_batch, "out_cache_loc_dsv4", None),
|
||||
# The mamba-track registry slot (VIRTUAL ids) is the v2p translate SOURCE
|
||||
# for the backend, which copies the result into its own static buffer and
|
||||
# reads THAT in the decode track-save — this slot is never mutated. None
|
||||
# when mamba-track is disabled.
|
||||
mamba_track_indices=getattr(buffers, "mamba_track_indices", None),
|
||||
spec_info=forward_batch.spec_info,
|
||||
)
|
||||
|
||||
@@ -705,6 +710,9 @@ class DecodeCudaGraphRunner(BaseCudaGraphRunner):
|
||||
if self.enable_profile_cuda_graph:
|
||||
self._post_process_after_profile(prof)
|
||||
|
||||
# No pool-side pin to clear: the captured full-physical write loc rides the
|
||||
# backend's `ForwardMetadata.out_cache_loc_full_physical` (-> KVWriteLoc.full_loc).
|
||||
|
||||
def _capture_one_stream(self, stream_idx: Optional[int] = None) -> None:
|
||||
avail_mem = get_available_gpu_memory(
|
||||
self.model_runner.device,
|
||||
@@ -775,10 +783,15 @@ class DecodeCudaGraphRunner(BaseCudaGraphRunner):
|
||||
attn_backend.init_forward_metadata_out_graph(forward_batch, in_capture=True)
|
||||
|
||||
def run_once():
|
||||
# Must run inside the capture block: warmup mutations here are
|
||||
# undone by on_after_cuda_graph_warmup so capture starts clean.
|
||||
# Graph-recordable metadata-prep hook. The unified memory pool
|
||||
# records ZERO translate nodes here: all its read/write translates
|
||||
# run eagerly in `init_forward_metadata_out_graph` (replay-prep), so
|
||||
# the captured graph reads already-physical locs. Base no-op for triton.
|
||||
attn_backend.init_forward_metadata_in_graph(forward_batch)
|
||||
|
||||
# No invalidate_loc_cache() here: the unified pool translates its
|
||||
# locs in `init_forward_metadata_out_graph`, so no cache to invalidate.
|
||||
|
||||
forward_batch.dp_local_start_pos = forward_batch.dp_local_num_tokens = (
|
||||
None
|
||||
)
|
||||
@@ -834,6 +847,10 @@ class DecodeCudaGraphRunner(BaseCudaGraphRunner):
|
||||
if (c := self.model_runner.canary_manager) is not None
|
||||
else contextlib.nullcontext()
|
||||
)
|
||||
# Full-physical write loc lives in the attention metadata (the backend's
|
||||
# `out_cache_loc_full_physical` -> KVWriteLoc.full_loc), so the runner
|
||||
# wires no buffer here. (SWA write loc rides the `swa_out_cache_loc` rail.)
|
||||
|
||||
with canary_ctx:
|
||||
shape_key = self._make_graph_key(bs, stream_idx, variant_label)
|
||||
self.backend.capture_one(
|
||||
|
||||
@@ -783,6 +783,14 @@ class ServerArgs:
|
||||
"(layer-major) layout. Requires the Triton attention / linear-attn / "
|
||||
"Mamba backends.",
|
||||
] = False
|
||||
enable_unified_memory: A[
|
||||
bool,
|
||||
"Replace the statically-partitioned hybrid-model pools (full-attn KV + "
|
||||
"SWA/Mamba state) with one byte buffer split dynamically between "
|
||||
"sub-pools. Requires the Triton attention / linear-attn / Mamba "
|
||||
"backends; not yet compatible with PD disaggregation or speculative "
|
||||
"decoding.",
|
||||
] = False
|
||||
disable_chunked_prefix_cache: A[
|
||||
bool,
|
||||
"Disable chunked prefix cache feature for deepseek, which should save overhead for short sequences.",
|
||||
@@ -2717,6 +2725,8 @@ class ServerArgs:
|
||||
|
||||
self._handle_page_major_kv_layout()
|
||||
|
||||
self._handle_unified_memory_pool()
|
||||
|
||||
# Handle diffusion LLM inference.
|
||||
self._handle_dllm_inference()
|
||||
|
||||
@@ -6336,7 +6346,49 @@ class ServerArgs:
|
||||
"NCCL_ALGO is set to 'allreduce:tree' and custom all reduce is disabled for deterministic inference when TP size > 1."
|
||||
)
|
||||
|
||||
def _handle_unified_memory_pool(self):
|
||||
if not self.enable_unified_memory:
|
||||
return
|
||||
assert self.disaggregation_mode == "null", (
|
||||
"--enable-unified-memory is not yet compatible with PD " "disaggregation."
|
||||
)
|
||||
assert self.speculative_algorithm is None, (
|
||||
"--enable-unified-memory is not yet compatible with speculative "
|
||||
"decoding."
|
||||
)
|
||||
assert not (self.enable_hierarchical_cache or self.enable_lmcache), (
|
||||
"--enable-unified-memory is not yet compatible with hierarchical / "
|
||||
"host-tiered KV cache (--enable-hierarchical-cache / --enable-lmcache): "
|
||||
"the unified-memory-pool init wires up no host pools, and its device mamba / "
|
||||
"full-attention slots are VIRTUAL — the host-offload path does not "
|
||||
"translate them to physical."
|
||||
)
|
||||
assert self.dcp_size == 1, (
|
||||
"--enable-unified-memory is not yet compatible with decode context "
|
||||
"parallelism (--dcp-size > 1): the pool has no DCP-aware masked write "
|
||||
"path (UnifiedMHATokenToKVPool.set_kv_buffer asserts dcp_kv_mask is None), "
|
||||
"so a DCP run would boot and then fail on the first KV write."
|
||||
)
|
||||
# Only monolithic decode cuda-graph capture is wired; piecewise prefill
|
||||
# capture is not. Guard when the user opts into it.
|
||||
_cg_cfg = self.cuda_graph_config
|
||||
if _cg_cfg is not None and _cg_cfg.prefill.backend == Backend.TC_PIECEWISE:
|
||||
raise ValueError(
|
||||
"--enable-unified-memory supports monolithic (decode) "
|
||||
"cuda-graph capture only; disable piecewise prefill capture "
|
||||
"(e.g. --cuda-graph-backend-prefill=disabled)."
|
||||
)
|
||||
# The strided-layout Triton requirement is enforced via
|
||||
# --enable-page-major-kv-layout (implied by the unified pool in
|
||||
# _handle_page_major_kv_layout); the model-family gate is enforced at pool
|
||||
# construction in model_runner_kv_cache_mixin._init_pools.
|
||||
|
||||
def _handle_page_major_kv_layout(self):
|
||||
# The unified pool stores state in the page-major envelope-strided layout, so
|
||||
# enabling it implies --enable-page-major-kv-layout — routing it through the
|
||||
# single page-major path + stride-aware Triton asserts (set before the guard).
|
||||
if self.enable_unified_memory:
|
||||
self.enable_page_major_kv_layout = True
|
||||
if not self.enable_page_major_kv_layout:
|
||||
return
|
||||
# Only the Triton attention kernels read the strided 4-D envelope K/V
|
||||
|
||||
Reference in New Issue
Block a user