from __future__ import annotations from typing import TYPE_CHECKING, Optional import torch from torch.nn.functional import scaled_dot_product_attention from sglang.srt.layers.attention.base_attn_backend import AttentionBackend from sglang.srt.layers.radix_attention import AttentionType from sglang.srt.mem_cache.memory_pool import KVWriteLoc from sglang.srt.mem_cache.swa_memory_pool import SWAKVPool from sglang.srt.model_executor.forward_batch_info import ForwardBatch if TYPE_CHECKING: from sglang.srt.layers.radix_attention import RadixAttention from sglang.srt.model_executor.model_runner import ModelRunner class TorchNativeAttnBackend(AttentionBackend): def __init__(self, model_runner: ModelRunner): super().__init__() self.forward_metadata = None self.device = model_runner.device # 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.use_sliding_window_kv_pool = ( isinstance(self.token_to_kv_pool, SWAKVPool) and self.token_to_kv_pool.swa_layer_nums > 0 ) # full->SWA translated out_cache_loc, computed once per forward self.swa_out_cache_loc = None @staticmethod def _make_sliding_window_mask( *, q_len: int, kv_len: int, sliding_window_size: int, device: torch.device, query_offset: int = 0, ) -> torch.Tensor: q_pos = torch.arange( query_offset, query_offset + q_len, device=device ).unsqueeze(1) k_pos = torch.arange(kv_len, device=device).unsqueeze(0) return (k_pos <= q_pos) & (k_pos >= q_pos - sliding_window_size) def init_forward_metadata(self, forward_batch: ForwardBatch): """Init the metadata for a forward pass.""" if self.use_sliding_window_kv_pool and forward_batch.out_cache_loc is not None: self.swa_out_cache_loc = ( self.token_to_kv_pool.translate_loc_from_full_to_swa( forward_batch.out_cache_loc ) ) else: self.swa_out_cache_loc = None def _run_sdpa_forward_extend( self, query: torch.Tensor, output: torch.Tensor, k_cache: torch.Tensor, v_cache: torch.Tensor, req_to_token: torch.Tensor, req_pool_indices: torch.Tensor, seq_lens: torch.Tensor, extend_prefix_lens: torch.Tensor, extend_seq_lens: torch.Tensor, encoder_lens: Optional[torch.Tensor] = None, scaling=None, enable_gqa=False, causal=False, is_cross_attn=False, sliding_window_size: Optional[int] = None, ): """Run the extend forward by using torch native sdpa op. Args: query: [num_tokens, num_heads, head_size] output: [num_tokens, num_heads, head_size] k_cache: [max_total_num_tokens, num_heads, head_size] v_cache: [max_total_num_tokens, num_heads, head_size] req_to_token: [max_num_reqs, max_context_len] req_pool_indices: [num_seqs] encoder_lens: [num_seqs] or None seq_lens: [num_seqs] extend_prefix_lens: [num_seqs] extend_seq_lens: [num_seqs] scaling: float or None enable_gqa: bool causal: bool is_cross_attn: bool Returns: output: [num_tokens, num_heads, head_size] """ assert seq_lens.shape[0] == extend_prefix_lens.shape[0] assert seq_lens.shape[0] == extend_seq_lens.shape[0] # [num_tokens, num_heads, head_size] -> [num_heads, num_tokens, head_size] query = query.movedim(0, query.dim() - 2) start_q, start_kv = 0, 0 for seq_idx in range(seq_lens.shape[0]): # TODO: this loop process a sequence per iter, this is inefficient. # Need optimize the performance later. extend_seq_len_q = extend_seq_lens[seq_idx] prefill_seq_len_q = extend_prefix_lens[seq_idx] seq_len_kv = seq_lens[seq_idx] end_q = start_q + extend_seq_len_q if encoder_lens is not None: if is_cross_attn: start_kv = 0 end_kv = encoder_lens[seq_idx] else: start_kv = encoder_lens[seq_idx] end_kv = start_kv + seq_len_kv else: start_kv = 0 end_kv = start_kv + seq_len_kv per_req_query = query[:, start_q:end_q, :] per_req_query_redudant = torch.empty( (per_req_query.shape[0], seq_len_kv, per_req_query.shape[2]), dtype=per_req_query.dtype, device=per_req_query.device, ) per_req_query_redudant[:, prefill_seq_len_q:, :] = per_req_query # get key and value from cache. per_req_tokens contains the kv cache # index for each token in the sequence. req_pool_idx = req_pool_indices[seq_idx] per_req_tokens = req_to_token[req_pool_idx, start_kv:end_kv] per_req_key = k_cache[per_req_tokens].movedim(0, query.dim() - 2) per_req_value = v_cache[per_req_tokens].movedim(0, query.dim() - 2) if not (per_req_query.dtype == per_req_key.dtype == per_req_value.dtype): # scaled_dot_product_attention() expects query, key, and value to have the same dtype per_req_key = per_req_key.to(per_req_query.dtype) per_req_value = per_req_value.to(per_req_query.dtype) attn_mask = None is_causal = causal if sliding_window_size is not None and sliding_window_size > -1: attn_mask = self._make_sliding_window_mask( q_len=seq_len_kv, kv_len=seq_len_kv, sliding_window_size=sliding_window_size, device=per_req_query.device, ) is_causal = False per_req_out_redudant = ( scaled_dot_product_attention( per_req_query_redudant.unsqueeze(0), per_req_key.unsqueeze(0), per_req_value.unsqueeze(0), attn_mask=attn_mask, enable_gqa=enable_gqa, scale=scaling, is_causal=is_causal, ) .squeeze(0) .movedim(query.dim() - 2, 0) ) output[start_q:end_q, :, :] = per_req_out_redudant[prefill_seq_len_q:, :, :] start_q, start_kv = end_q, end_kv return output def _run_sdpa_forward_decode( self, query: torch.Tensor, output: torch.Tensor, k_cache: torch.Tensor, v_cache: torch.Tensor, req_to_token: torch.Tensor, req_pool_indices: torch.Tensor, seq_lens: torch.Tensor, encoder_lens: Optional[torch.Tensor] = None, scaling=None, enable_gqa=False, causal=False, is_cross_attn=False, sliding_window_size: Optional[int] = None, ): """Run the decode forward by using torch native sdpa op. Args: query: [num_tokens, num_heads, head_size] output: [num_tokens, num_heads, head_size] k_cache: [max_total_num_tokens, num_heads, head_size] v_cache: [max_total_num_tokens, num_heads, head_size] req_to_token: [max_num_reqs, max_context_len] req_pool_indices: [num_seqs] seq_lens: [num_seqs] encoder_lens: [num_seqs] or None scaling: float or None enable_gqa: bool causal: bool is_cross_attn: bool Returns: output: [num_tokens, num_heads, head_size] """ # [num_tokens, num_heads, head_size] -> [num_heads, num_tokens, head_size] query = query.movedim(0, query.dim() - 2) start_q, start_kv = 0, 0 for seq_idx in range(seq_lens.shape[0]): # TODO: this loop process a sequence per iter, this is inefficient. # Need optimize the performance later. seq_len_q = 1 seq_len_kv = seq_lens[seq_idx] end_q = start_q + seq_len_q if encoder_lens is not None: if is_cross_attn: start_kv = 0 end_kv = encoder_lens[seq_idx] else: start_kv = encoder_lens[seq_idx] end_kv = start_kv + seq_len_kv else: start_kv = 0 end_kv = start_kv + seq_len_kv per_req_query = query[:, start_q:end_q, :] # get key and value from cache. per_req_tokens contains the kv cache # index for each token in the sequence. req_pool_idx = req_pool_indices[seq_idx] per_req_tokens = req_to_token[req_pool_idx, start_kv:end_kv] per_req_key = k_cache[per_req_tokens].movedim(0, query.dim() - 2) per_req_value = v_cache[per_req_tokens].movedim(0, query.dim() - 2) if not (per_req_query.dtype == per_req_key.dtype == per_req_value.dtype): # scaled_dot_product_attention() expects query, key, and value to have the same dtype per_req_key = per_req_key.to(per_req_query.dtype) per_req_value = per_req_value.to(per_req_query.dtype) attn_mask = None is_causal = causal if sliding_window_size is not None and sliding_window_size > -1: attn_mask = self._make_sliding_window_mask( q_len=seq_len_q, kv_len=seq_len_kv, sliding_window_size=sliding_window_size, device=per_req_query.device, query_offset=seq_len_kv - seq_len_q, ) is_causal = False per_req_out = ( scaled_dot_product_attention( per_req_query.unsqueeze(0), per_req_key.unsqueeze(0), per_req_value.unsqueeze(0), attn_mask=attn_mask, enable_gqa=enable_gqa, scale=scaling, is_causal=is_causal, ) .squeeze(0) .movedim(query.dim() - 2, 0) ) output[start_q:end_q, :, :] = per_req_out start_q, start_kv = end_q, end_kv return output def forward_extend( self, q, k, v, layer: RadixAttention, forward_batch: ForwardBatch, save_kv_cache=True, ): if layer.qk_head_dim != layer.v_head_dim: o = q.new_empty((q.shape[0], layer.tp_q_head_num * layer.v_head_dim)) else: o = torch.empty_like(q) if layer.is_cross_attention: cache_loc = forward_batch.encoder_out_cache_loc else: cache_loc = forward_batch.out_cache_loc if save_kv_cache and k is not None and v is not None: self.token_to_kv_pool.set_kv_buffer( layer, KVWriteLoc(cache_loc, self.swa_out_cache_loc), k, v ) use_gqa = layer.tp_q_head_num != layer.tp_k_head_num q_ = q.view(-1, layer.tp_q_head_num, layer.qk_head_dim) o_ = o.view(-1, layer.tp_q_head_num, layer.v_head_dim) causal = True if layer.is_cross_attention or layer.attn_type == AttentionType.ENCODER_ONLY: causal = False self._run_sdpa_forward_extend( q_, o_, self.token_to_kv_pool.get_key_buffer(layer.layer_id), self.token_to_kv_pool.get_value_buffer(layer.layer_id), self.req_to_token_pool.req_to_token, forward_batch.req_pool_indices, forward_batch.seq_lens, forward_batch.extend_prefix_lens, forward_batch.extend_seq_lens, forward_batch.encoder_lens, scaling=layer.scaling, enable_gqa=use_gqa, causal=causal, is_cross_attn=layer.is_cross_attention, sliding_window_size=( layer.sliding_window_size if causal and not layer.is_cross_attention and layer.sliding_window_size is not None and layer.sliding_window_size > -1 else None ), ) return o def forward_decode( self, q, k, v, layer: RadixAttention, forward_batch: ForwardBatch, save_kv_cache=True, ): # During torch.compile, there is a bug in rotary_emb that causes the # output value to have a 3D tensor shape. This reshapes the output correctly. q = q.reshape(-1, layer.tp_q_head_num * layer.qk_head_dim) if layer.qk_head_dim != layer.v_head_dim: o = q.new_empty((q.shape[0], layer.tp_q_head_num * layer.v_head_dim)) else: o = torch.empty_like(q) cache_loc = ( forward_batch.out_cache_loc if not layer.is_cross_attention else forward_batch.encoder_out_cache_loc ) if layer.is_cross_attention: cache_loc = forward_batch.encoder_out_cache_loc else: cache_loc = forward_batch.out_cache_loc if save_kv_cache and k is not None and v is not None: self.token_to_kv_pool.set_kv_buffer( layer, KVWriteLoc(cache_loc, self.swa_out_cache_loc), k, v ) use_gqa = layer.tp_q_head_num != layer.tp_k_head_num q_ = q.view(-1, layer.tp_q_head_num, layer.qk_head_dim) o_ = o.view(-1, layer.tp_q_head_num, layer.v_head_dim) self._run_sdpa_forward_decode( q_, o_, self.token_to_kv_pool.get_key_buffer(layer.layer_id), self.token_to_kv_pool.get_value_buffer(layer.layer_id), self.req_to_token_pool.req_to_token, forward_batch.req_pool_indices, forward_batch.seq_lens, forward_batch.encoder_lens, scaling=layer.scaling, enable_gqa=use_gqa, causal=False, is_cross_attn=layer.is_cross_attention, sliding_window_size=( layer.sliding_window_size if not layer.is_cross_attention and layer.sliding_window_size is not None and layer.sliding_window_size > -1 else None ), ) return o def support_triton(self): return False