This is an automated email from the ASF dual-hosted git repository.

tlopex pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tvm.git


The following commit(s) were added to refs/heads/main by this push:
     new 27c2e019d0 [Feature][Relax] Support shared-KV attention with 
configurable sliding windows (#20121)
27c2e019d0 is described below

commit 27c2e019d0ce6182158020c7534dda4a3ce981ae
Author: Akaash Parthasarathy <[email protected]>
AuthorDate: Wed Aug 12 14:19:27 2026 -0700

    [Feature][Relax] Support shared-KV attention with configurable sliding 
windows (#20121)
    
    Extend `PagedKVCache` for models whose logical attention layers reuse
    K/V cached by another physical layer.
    - Adds non-mutating `attention_with_shared_kv` support.
    - Makes per-layer sliding-window size configurable.
    - Corrects sliding-window masking during chunked prefill.
    
    This is intended to enable downstream Gemma 4 support in MLC-LLM and
    WebLLM.
---
 python/tvm/relax/frontend/nn/llm/_kernel_common.py |  15 +-
 .../tvm/relax/frontend/nn/llm/_prefill_kernels.py  |  36 ++++-
 python/tvm/relax/frontend/nn/llm/kv_cache.py       |  56 ++++++-
 src/runtime/vm/kv_state.cc                         |   7 +
 src/runtime/vm/kv_state.h                          |  15 ++
 src/runtime/vm/paged_kv_cache.cc                   | 131 ++++++++++++-----
 ...runtime_builtin_paged_attention_kv_cache_cpu.py | 161 +++++++++++++++++++--
 ...runtime_builtin_paged_attention_kv_cache_tir.py |  65 +++++++++
 8 files changed, 422 insertions(+), 64 deletions(-)

diff --git a/python/tvm/relax/frontend/nn/llm/_kernel_common.py 
b/python/tvm/relax/frontend/nn/llm/_kernel_common.py
index 6d7450e4fa..2de72360ee 100644
--- a/python/tvm/relax/frontend/nn/llm/_kernel_common.py
+++ b/python/tvm/relax/frontend/nn/llm/_kernel_common.py
@@ -135,6 +135,15 @@ def _causal_mask(causal, row, col, kv_len, qo_len):
     )
 
 
+def _causal_or_sliding_cross_mask(causal, row, col, kv_len, qo_len, 
sliding_window_size):
+    visible_past = T.max(sliding_window_size - row - 1, 0)
+    return T.if_then_else(
+        tirx.And(causal > 0, sliding_window_size > 0),
+        tirx.And(col < kv_len, col >= T.max(kv_len - visible_past, 0)),
+        _causal_mask(causal, row, col, kv_len, qo_len),
+    )
+
+
 def _declare_length_info(var_length_info, batch_size, sliding_window, 
elem_offset):
     return (
         T.match_buffer(var_length_info, (3, batch_size), "int32", 
elem_offset=elem_offset)
@@ -248,7 +257,7 @@ def _make_prefill_macros(tile_x, tile_y, tile_z, tile_o, 
bdx, num_warps, group_s
         S_smem: T.Buffer, m_smem: T.Buffer, d_smem: T.Buffer, m_prev_smem: 
T.Buffer,
         m_new: T.Buffer, m_prev: T.Buffer, d_new: T.Buffer,
         ty: T.int32, tx: T.int32, LH_start: T.int32, L_kv_start: T.int32,
-        causal: T.int32, kv_len: T.int32, qo_len: T.int32,
+        causal: T.int32, kv_len: T.int32, qo_len: T.int32, 
sliding_window_size: T.int32,
     ):
         # Phase 1: compute m_new = max(masked S over kv tile), d_new = d_prev 
* exp2(m_prev - m_new)
         for i in T.serial(T.ceildiv(tile_x, bdx * num_warps)):
@@ -259,7 +268,7 @@ def _make_prefill_macros(tile_x, tile_y, tile_z, tile_o, 
bdx, num_warps, group_s
                     m_new[i] = m_smem[row]
                     row_: T.let[T.int32] = (LH_start + row) // group_size
                     for j in T.serial(tile_z):
-                        if _causal_mask(causal, row=row_, col=L_kv_start + j, 
kv_len=kv_len, qo_len=qo_len):
+                        if _causal_or_sliding_cross_mask(causal, row=row_, 
col=L_kv_start + j, kv_len=kv_len, qo_len=qo_len, 
sliding_window_size=sliding_window_size):
                             m_new[i] = T.max(m_new[i], S_smem[row, j])
                     d_new[i] = d_smem[row] * T.exp2(m_prev[i] - m_new[i])
         # Phase 2: exp-and-scale S_smem; masked-out entries use -inf
@@ -270,7 +279,7 @@ def _make_prefill_macros(tile_x, tile_y, tile_z, tile_o, 
bdx, num_warps, group_s
                     # predicate sits inside loop so sync stays outside 
conditional branches
                     if row < tile_x:
                         row_: T.let[T.int32] = (LH_start + row) // group_size
-                        if _causal_mask(causal, row=row_, col=L_kv_start + j, 
kv_len=kv_len, qo_len=qo_len):
+                        if _causal_or_sliding_cross_mask(causal, row=row_, 
col=L_kv_start + j, kv_len=kv_len, qo_len=qo_len, 
sliding_window_size=sliding_window_size):
                             S_smem[row, j] = T.exp2(S_smem[row, j] - m_new[i])
                         else:
                             S_smem[row, j] = T.exp2(-5e4 - m_new[i])
diff --git a/python/tvm/relax/frontend/nn/llm/_prefill_kernels.py 
b/python/tvm/relax/frontend/nn/llm/_prefill_kernels.py
index 54e7424e71..9eeb18f8cf 100644
--- a/python/tvm/relax/frontend/nn/llm/_prefill_kernels.py
+++ b/python/tvm/relax/frontend/nn/llm/_prefill_kernels.py
@@ -40,6 +40,7 @@ from ._kernel_common import (
     _alloc_softmax_state_buffers,
     _alloc_tile_walk_state,
     _causal_mask,
+    _causal_or_sliding_cross_mask,
     _declare_length_info,
     _get_kv_chunk_len,
     _get_prefill_kernel_config,
@@ -51,7 +52,14 @@ from ._kernel_common import (
 
 
 def _attention_prefill_cpu(
-    h_kv, h_q, d, dtype, sliding_window: bool, rope_scaling: dict[str, Any], 
page_size: int = 16
+    h_kv,
+    h_q,
+    d,
+    dtype,
+    sliding_window: bool,
+    rope_scaling: dict[str, Any],
+    page_size: int = 16,
+    sliding_window_size: int = 1024,
 ):
     global_symbol = "batch_prefill_paged_kv_cpu"
     if sliding_window:
@@ -173,11 +181,14 @@ def _attention_prefill_cpu(
                                 S_val[0] *= sm_scale * math.log2(math.exp(1))
 
                                 # update m_val, d_val , O_local
-                                if _causal_mask(causal,
+                                if _causal_or_sliding_cross_mask(
+                                    causal,
                                     row=q_idx,
                                     col=row_idx,
                                     kv_len=kv_chunk_len[0],
-                                    qo_len=q_indptr[b_idx + 1] - 
q_indptr[b_idx]):
+                                    qo_len=q_indptr[b_idx + 1] - 
q_indptr[b_idx],
+                                    sliding_window_size=(sliding_window_size 
if sliding_window else 0),
+                                ):
                                     new_m[0] = T.max(m_val[0], S_val[0])
                                 else:
                                     S_val[0] = -5e4
@@ -203,7 +214,17 @@ def _attention_prefill_cpu(
     return batch_prefill_paged_kv_cpu
 
 
-def _attention_prefill(h_kv, h_q, d, dtype, sliding_window: bool, 
rope_scaling: dict[str, Any], target: Target, page_size: int = 16):
+def _attention_prefill(
+    h_kv,
+    h_q,
+    d,
+    dtype,
+    sliding_window: bool,
+    rope_scaling: dict[str, Any],
+    target: Target,
+    page_size: int = 16,
+    sliding_window_size: int = 1024,
+):
     NUM_BLKS, LOAD_VEC, group_size, bdx, num_warps, tile_x, tile_y, tile_z = 
_get_prefill_kernel_config(h_kv, h_q, d, dtype, target)
 
     global_symbol = "batch_prefill_paged_kv"
@@ -355,7 +376,7 @@ def _attention_prefill(h_kv, h_q, d, dtype, sliding_window: 
bool, rope_scaling:
                                         T.tvm_storage_sync("shared")
 
                                         compute_s_gemm(Q_smem, K_smem, 
S_local, S_smem, sm_scale)
-                                        softmax_update_causal(S_smem, m_smem, 
d_smem, m_prev_smem, m_new, m_prev, d_new, ty, tx, LH_start, L_kv_start, 
causal, kv_chunk_len[0], q_indptr[b_idx + 1] - q_indptr[b_idx])
+                                        softmax_update_causal(S_smem, m_smem, 
d_smem, m_prev_smem, m_new, m_prev, d_new, ty, tx, LH_start, L_kv_start, 
causal, kv_chunk_len[0], q_indptr[b_idx + 1] - q_indptr[b_idx], 
sliding_window_size if sliding_window else 0)
                                         compute_o_gemm(S_smem, V_smem, 
O_local, m_prev_smem, m_smem)
 
                                     paged_store_output_lse(output, lse, 
O_local, m_smem, d_smem, q_indptr, b_idx, by, LH_start)
@@ -459,7 +480,7 @@ def _attention_sequence_prefill(h_kv, h_q, d, dtype, 
target: Target, causal=0, s
                                 T.tvm_storage_sync("shared")
 
                                 compute_s_gemm(Q_smem, K_smem, S_local, 
S_smem, sm_scale)
-                                softmax_update_causal(S_smem, m_smem, d_smem, 
m_prev_smem, m_new, m_prev, d_new, ty, tx, LH_start, L_kv_start, causal, 
kv_len, qo_len)
+                                softmax_update_causal(S_smem, m_smem, d_smem, 
m_prev_smem, m_new, m_prev, d_new, ty, tx, LH_start, L_kv_start, causal, 
kv_len, qo_len, 0)
                                 compute_o_gemm(S_smem, V_smem, O_local, 
m_prev_smem, m_smem)
 
                             # Store O from smem to gmem
@@ -889,7 +910,7 @@ def _attention_prefill_ragged(h_kv, h_q, d_qk, d_v, dtype, 
rope_scaling: dict[st
                                         T.tvm_storage_sync("shared")
 
                                         compute_s_gemm(Q_smem, K_smem, 
S_local, S_smem, sm_scale)
-                                        softmax_update_causal(S_smem, m_smem, 
d_smem, m_prev_smem, m_new, m_prev, d_new, ty, tx, LH_start, L_kv_start, 
causal, kv_chunk_len[0], q_indptr[b_idx + 1] - q_indptr[b_idx])
+                                        softmax_update_causal(S_smem, m_smem, 
d_smem, m_prev_smem, m_new, m_prev, d_new, ty, tx, LH_start, L_kv_start, 
causal, kv_chunk_len[0], q_indptr[b_idx + 1] - q_indptr[b_idx], 0)
                                         compute_o_gemm(S_smem, V_smem, 
O_local, m_prev_smem, m_smem)
 
                                     paged_store_output_lse(output, lse, 
O_local, m_smem, d_smem, q_indptr, b_idx, by, LH_start)
@@ -1030,6 +1051,7 @@ def _attention_prefill_mla(h_q, d_latent, d_rope, dtype, 
sliding_window: bool, t
                                         m_new, m_prev, d_new,
                                         ty, tx, LH_start, L_kv_start,
                                         causal, kv_chunk_len[0], 
q_indptr[b_idx + 1] - q_indptr[b_idx],
+                                        0,
                                     )
 
                                     compute_o_gemm(S_smem, KV_smem, O_local, 
m_prev_smem, m_smem)
diff --git a/python/tvm/relax/frontend/nn/llm/kv_cache.py 
b/python/tvm/relax/frontend/nn/llm/kv_cache.py
index d7c35279c6..55f2491a4a 100644
--- a/python/tvm/relax/frontend/nn/llm/kv_cache.py
+++ b/python/tvm/relax/frontend/nn/llm/kv_cache.py
@@ -230,6 +230,46 @@ class PagedKVCache(Object):  # pylint: 
disable=too-few-public-methods
         lse = Tensor(_expr=bb.emit(rx.TupleGetItem(attn_results, 
1))).reshape(b, s, h_qo)
         return o, lse
 
+    def attention_with_shared_kv(
+        self,
+        source_layer_id: int,
+        q: Tensor,
+        current_k: Tensor,
+        current_v: Tensor,
+        sm_scale: float,
+    ) -> Tensor:
+        """Compute attention using K/V shared by another logical layer.
+
+        This operation does not append to the KV cache. The cache-owning 
source layer must run
+        before this operation in the active forward pass. ``current_k`` and 
``current_v`` are that
+        source layer's K/V for the active chunk. During prefill they are 
combined with the source
+        layer's paged past K/V; during decode the source layer has already 
appended them and the
+        operation reads the cache directly.
+        """
+        # pylint: disable=protected-access
+        b, s, h_qo, d_qk = q._expr.ty.shape
+        _, _, h_kv, _ = current_k._expr.ty.shape
+        _, _, _, d_v = current_v._expr.ty.shape
+        q = q.reshape(b * s, h_qo, d_qk)
+        current_k = current_k.reshape(b * s, h_kv, d_qk)
+        current_v = current_v.reshape(b * s, h_kv, d_v)
+        return Tensor(
+            _expr=rx.BlockBuilder.current().emit(
+                rx.call_dps_packed(
+                    "vm.builtin.attention_kv_cache_attention_with_shared_kv",
+                    [
+                        self._expr,
+                        rx.prim_value(source_layer_id),  # type: 
ignore[arg-type]
+                        rx.prim_value(sm_scale),
+                        q._expr,
+                        current_k._expr,
+                        current_v._expr,
+                    ],
+                    out_ty=rx.TensorType((b * s, h_qo, d_v), q.dtype),
+                )
+            )
+        ).reshape(b, s, h_qo, d_v)
+
     def append_mla_kv(self, layer_id: int, kv: Tensor) -> "PagedKVCache":
         """Fine-grained API that appends the MLA K/V data to KV cache."""
         # pylint: disable=protected-access
@@ -355,6 +395,8 @@ class FlashInferPagedKVCache(PagedKVCache):  # pylint: 
disable=too-few-public-me
         dtype: str,
         target: Target,
         name: str = "paged_kv_cache",
+        *,
+        layer_sliding_window_size: int = 1024,
     ) -> None:
         """Create a paged KV cache object with FlashInfer kernels.
 
@@ -399,6 +441,8 @@ class FlashInferPagedKVCache(PagedKVCache):  # pylint: 
disable=too-few-public-me
             The number of dimensions in the embedding that RoPE is applied to.
         enable_disaggregation : bool
             Whether to enable disaggregation in the KV cache.
+        layer_sliding_window_size : int
+            Window size used by layers whose attention kind is ``mha_sliding``.
         """
         assert rope_mode != RopeMode.INLINE, "FlashInfer RoPE does not support 
inline mode."
         rope_scaling = _prepare_yarn_rope_scaling(rope_scaling, rope_theta)
@@ -447,7 +491,7 @@ class FlashInferPagedKVCache(PagedKVCache):  # pylint: 
disable=too-few-public-me
             [
                 rx.Tuple([rx.StringImm("flashinfer"), 
rx.ExternFunc("batch_prefill_paged_run"), rx.ExternFunc("batch_prefill_plan")]),
                 rx.Tuple([rx.StringImm("flashinfer"), 
rx.ExternFunc("batch_decode_run"), rx.ExternFunc("batch_decode_plan")]),
-                rx.Tuple([rx.StringImm("tirx"), 
bb.add_func(_attention_prefill(num_key_value_heads, num_attention_heads, 
qk_head_dim, dtype, True, rope_scaling, target), 
"tir_attention_prefill_sliding_window")]),
+                rx.Tuple([rx.StringImm("tirx"), 
bb.add_func(_attention_prefill(num_key_value_heads, num_attention_heads, 
qk_head_dim, dtype, True, rope_scaling, target, 
sliding_window_size=layer_sliding_window_size), 
"tir_attention_prefill_sliding_window")]),
                 rx.Tuple([rx.StringImm("tirx"), 
bb.add_func(_attention_decode(num_key_value_heads, num_attention_heads, 
qk_head_dim, dtype, True, rope_scaling, target), 
"tir_attention_decode_sliding_window")]),
                 rx.Tuple([rx.StringImm("tirx"), 
bb.add_func(tree_attn_with_paged_kv_cache(num_key_value_heads, 
num_attention_heads, qk_head_dim, dtype, rope_scaling, target), 
"tir_attention_prefill_with_tree_mask_with_paged_kv_cache")]),
                 rx.Tuple([rx.StringImm("tirx"), 
bb.add_func(tree_attn(num_key_value_heads, num_attention_heads, qk_head_dim, 
dtype, rope_scaling, target), "tir_attention_prefill_with_tree_mask")]),
@@ -476,6 +520,7 @@ class FlashInferPagedKVCache(PagedKVCache):  # pylint: 
disable=too-few-public-me
                     prefill_chunk_size,
                     page_size,
                     support_sliding_window,
+                    layer_sliding_window_size,
                 ]
             ),
             layer_partition,
@@ -540,6 +585,8 @@ class TIRPagedKVCache(PagedKVCache):  # pylint: 
disable=too-few-public-methods
         dtype: str,
         target: Target,
         name: str = "paged_kv_cache",
+        *,
+        layer_sliding_window_size: int = 1024,
     ) -> None:
         """Create a paged KV cache object with TIR kernels.
 
@@ -586,6 +633,8 @@ class TIRPagedKVCache(PagedKVCache):  # pylint: 
disable=too-few-public-methods
             Whether to enable disaggregation in the KV cache.
         target : Target
             The target to build the model to.
+        layer_sliding_window_size : int
+            Window size used by layers whose attention kind is ``mha_sliding``.
         """
         rope_scaling = _prepare_yarn_rope_scaling(rope_scaling, rope_theta)
         attn_kind_single = attn_kind[0] if isinstance(attn_kind, list) else 
attn_kind
@@ -604,6 +653,7 @@ class TIRPagedKVCache(PagedKVCache):  # pylint: 
disable=too-few-public-methods
                     prefill_chunk_size,
                     page_size,
                     support_sliding_window,
+                    layer_sliding_window_size,
                 ]
             ),
             layer_partition,
@@ -630,7 +680,7 @@ class TIRPagedKVCache(PagedKVCache):  # pylint: 
disable=too-few-public-methods
                     rx.Tuple([rx.StringImm("tirx"), 
bb.add_func(_attention_prefill_ragged_cpu(num_key_value_heads, 
num_attention_heads, qk_head_dim, v_head_dim, dtype, rope_scaling), 
"tir_attention_prefill_ragged_cpu")]),
                     rx.Tuple([rx.StringImm("tirx"), 
bb.add_func(_attention_prefill_cpu(num_key_value_heads, num_attention_heads, 
qk_head_dim, dtype, False, rope_scaling), "tir_attention_prefill_cpu")]),
                     rx.Tuple([rx.StringImm("tirx"), 
bb.add_func(_attention_decode_cpu(num_key_value_heads, num_attention_heads, 
qk_head_dim, dtype, False, rope_scaling), "tir_attention_decode_cpu")]),
-                    rx.Tuple([rx.StringImm("tirx"), 
bb.add_func(_attention_prefill_cpu(num_key_value_heads, num_attention_heads, 
qk_head_dim, dtype, True, rope_scaling), 
"tir_attention_prefill_cpu_sliding_window")]),
+                    rx.Tuple([rx.StringImm("tirx"), 
bb.add_func(_attention_prefill_cpu(num_key_value_heads, num_attention_heads, 
qk_head_dim, dtype, True, rope_scaling, 
sliding_window_size=layer_sliding_window_size), 
"tir_attention_prefill_cpu_sliding_window")]),
                     rx.Tuple([rx.StringImm("tirx"), 
bb.add_func(_attention_decode_cpu(num_key_value_heads, num_attention_heads, 
qk_head_dim, dtype, True, rope_scaling), 
"tir_attention_decode_cpu_sliding_window")]),
                     rx.Tuple([rx.StringImm("tirx"), 
bb.add_func(tree_attn_cpu(num_key_value_heads, num_attention_heads, 
qk_head_dim, dtype, rope_scaling), 
"tir_attention_prefill_with_tree_mask_cpu")]),
                     rx.Tuple([rx.StringImm("tirx"), 
bb.add_func(tree_attn_with_paged_kv_cache_cpu(num_key_value_heads, 
num_attention_heads, qk_head_dim, dtype, rope_scaling), 
"tir_attention_prefill_with_tree_mask_with_paged_kv_cache_cpu")]),
@@ -650,7 +700,7 @@ class TIRPagedKVCache(PagedKVCache):  # pylint: 
disable=too-few-public-methods
                 [
                     rx.Tuple([rx.StringImm("tirx"), 
bb.add_func(_attention_prefill(num_key_value_heads, num_attention_heads, 
qk_head_dim, dtype, False, rope_scaling, target), "tir_attention_prefill")]),
                     rx.Tuple([rx.StringImm("tirx"), 
bb.add_func(_attention_decode(num_key_value_heads, num_attention_heads, 
qk_head_dim, dtype, False, rope_scaling, target), "tir_attention_decode")]),
-                    rx.Tuple([rx.StringImm("tirx"), 
bb.add_func(_attention_prefill(num_key_value_heads, num_attention_heads, 
qk_head_dim, dtype, True, rope_scaling, target), 
"tir_attention_prefill_sliding_window")]),
+                    rx.Tuple([rx.StringImm("tirx"), 
bb.add_func(_attention_prefill(num_key_value_heads, num_attention_heads, 
qk_head_dim, dtype, True, rope_scaling, target, 
sliding_window_size=layer_sliding_window_size), 
"tir_attention_prefill_sliding_window")]),
                     rx.Tuple([rx.StringImm("tirx"), 
bb.add_func(_attention_decode(num_key_value_heads, num_attention_heads, 
qk_head_dim, dtype, True, rope_scaling, target), 
"tir_attention_decode_sliding_window")]),
                     rx.Tuple([rx.StringImm("tirx"), 
bb.add_func(tree_attn_with_paged_kv_cache(num_key_value_heads, 
num_attention_heads, qk_head_dim, dtype, rope_scaling, target), 
"tir_attention_prefill_with_tree_mask_with_paged_kv_cache")]),
                     rx.Tuple([rx.StringImm("tirx"), 
bb.add_func(tree_attn(num_key_value_heads, num_attention_heads, qk_head_dim, 
dtype, rope_scaling, target), "tir_attention_prefill_with_tree_mask")]),
diff --git a/src/runtime/vm/kv_state.cc b/src/runtime/vm/kv_state.cc
index 05f951dc74..2d93563497 100644
--- a/src/runtime/vm/kv_state.cc
+++ b/src/runtime/vm/kv_state.cc
@@ -94,6 +94,13 @@ TVM_FFI_STATIC_INIT_BLOCK() {
              kv_cache->CrossAttention(layer_id, std::move(q_data), 
std::move(o_data),
                                       std::move(lse_data), sm_scale);
            })
+      .def("vm.builtin.attention_kv_cache_attention_with_shared_kv",
+           [](AttentionKVCache kv_cache, int64_t source_layer_id, double 
sm_scale, Tensor q_data,
+              Tensor current_k_data, Tensor current_v_data, Tensor o_data) {
+             kv_cache->AttentionWithSharedKV(source_layer_id, 
std::move(q_data),
+                                             std::move(current_k_data), 
std::move(current_v_data),
+                                             std::move(o_data), sm_scale);
+           })
       .def("vm.builtin.attention_kv_cache_append_mla_kv",
            [](AttentionKVCache kv_cache, int64_t layer_id, Tensor kv_data) {
              kv_cache->AppendMLAKV(layer_id, std::move(kv_data));
diff --git a/src/runtime/vm/kv_state.h b/src/runtime/vm/kv_state.h
index 198bd18d97..4e1694c899 100644
--- a/src/runtime/vm/kv_state.h
+++ b/src/runtime/vm/kv_state.h
@@ -206,6 +206,21 @@ class AttentionKVCacheObj : public KVStateObj {
   virtual void CrossAttention(int64_t layer_id, Tensor q_data, Tensor o_data, 
Tensor lse_data,
                               double sm_scale) = 0;
 
+  /*!
+   * \brief Compute attention with K/V shared by another logical layer.
+   *
+   * This operation does not append K/V. The cache-owning source layer must 
run before this
+   * operation in the active forward pass.
+   * \param source_layer_id The physical cache layer containing past K/V data.
+   * \param q_data The logical layer's input Q data.
+   * \param current_k_data The source layer's K data for the active chunk.
+   * \param current_v_data The source layer's V data for the active chunk.
+   * \param o_data The output O data.
+   * \param sm_scale The additional attention scaling factor.
+   */
+  virtual void AttentionWithSharedKV(int64_t source_layer_id, Tensor q_data, 
Tensor current_k_data,
+                                     Tensor current_v_data, Tensor o_data, 
double sm_scale) = 0;
+
   /*!
    * \brief Fine-grained API that appends the MLA K/V data to KV cache.
    * \param layer_id The model layer where the attention compute happens.
diff --git a/src/runtime/vm/paged_kv_cache.cc b/src/runtime/vm/paged_kv_cache.cc
index efd099e826..cd0722f865 100644
--- a/src/runtime/vm/paged_kv_cache.cc
+++ b/src/runtime/vm/paged_kv_cache.cc
@@ -103,6 +103,8 @@ class PagedAttentionKVCacheObj : public AttentionKVCacheObj 
{
   const bool support_sliding_window_;
   /*! \brief A boolean flag indicating if the KV cache has per layer sliding 
window. */
   const bool support_layer_sliding_window_;
+  /*! \brief Window size used by layers with per-layer sliding attention. */
+  const int64_t layer_sliding_window_size_;
   /*! \brief The attention kinds for each layer. */
   const std::vector<AttnKind> attn_kinds_;
 
@@ -284,6 +286,22 @@ class PagedAttentionKVCacheObj : public 
AttentionKVCacheObj {
   /*! \brief The device stream for KV transfer */
   TVMStreamHandle kv_transfer_stream_ = nullptr;
 
+  int32_t GetLayerSlidingWindowOffset(int64_t seq_length) const {
+    if (seq_length <= layer_sliding_window_size_) {
+      return 0;
+    }
+    return static_cast<int32_t>((seq_length - layer_sliding_window_size_) % 
page_size_);
+  }
+
+  int32_t GetLayerSlidingWindowNumPages(int64_t seq_length) const {
+    if (seq_length == 0) {
+      return 0;
+    }
+    int64_t window_length = std::min(seq_length, layer_sliding_window_size_);
+    return static_cast<int32_t>(
+        (GetLayerSlidingWindowOffset(seq_length) + window_length + page_size_ 
- 1) / page_size_);
+  }
+
  public:
   /*! \brief Constructor. Take the cache configuration and initialize the 
Tensors. */
   explicit PagedAttentionKVCacheObj(
@@ -291,9 +309,9 @@ class PagedAttentionKVCacheObj : public AttentionKVCacheObj 
{
       int64_t layer_id_end_offset, int64_t num_qo_heads, int64_t num_kv_heads, 
int64_t qk_head_dim,
       int64_t v_head_dim, std::vector<AttnKind> attn_kinds, int64_t 
reserved_num_seqs,
       int64_t num_total_pages, int64_t prefill_chunk_size, bool 
support_sliding_window,
-      RoPEMode rope_mode, double rotary_scale, double rotary_theta,
-      ffi::Optional<Tensor> rope_ext_factors, bool enable_kv_transfer, 
DLDataType dtype,
-      Device device, ffi::Optional<ffi::Function> f_transpose_append_mha,
+      int64_t layer_sliding_window_size, RoPEMode rope_mode, double 
rotary_scale,
+      double rotary_theta, ffi::Optional<Tensor> rope_ext_factors, bool 
enable_kv_transfer,
+      DLDataType dtype, Device device, ffi::Optional<ffi::Function> 
f_transpose_append_mha,
       ffi::Optional<ffi::Function> f_transpose_append_mla, ffi::Function 
f_compact_copy,
       std::unique_ptr<RaggedPrefillFunc> f_attention_prefill_ragged,
       std::unique_ptr<PagedPrefillFunc> f_attention_prefill,
@@ -320,6 +338,7 @@ class PagedAttentionKVCacheObj : public AttentionKVCacheObj 
{
                                     : support_sliding_window),
         support_layer_sliding_window_(std::find(attn_kinds.begin(), 
attn_kinds.end(),
                                                 AttnKind::kMHASliding) != 
attn_kinds.end()),
+        layer_sliding_window_size_(layer_sliding_window_size),
         attn_kinds_(std::move(attn_kinds)),
         rope_mode_(support_sliding_window && rope_mode != RoPEMode::kNone ? 
RoPEMode::kInline
                                                                           : 
rope_mode),
@@ -345,6 +364,8 @@ class PagedAttentionKVCacheObj : public AttentionKVCacheObj 
{
         f_copy_single_page_(std::move(f_copy_single_page)),
         f_debug_get_kv_(std::move(f_debug_get_kv)),
         device_(device) {
+    TVM_FFI_ICHECK_GT(layer_sliding_window_size_, 0)
+        << "Per-layer sliding window size must be positive.";
     // Note: For MLA, sliding window and disaggregation are disabled for now.
     if (std::find(attn_kinds_.begin(), attn_kinds_.end(), AttnKind::kMLA) != 
attn_kinds_.end()) {
       TVM_FFI_ICHECK(!support_sliding_window_) << "Sliding window not 
supported yet for MLA";
@@ -1024,12 +1045,12 @@ class PagedAttentionKVCacheObj : public 
AttentionKVCacheObj {
             }
 
             // For sliding window, the first page and last page will both be 
partially used
-            page_indptr_sliding_window_h.push_back(
-                page_indptr_sliding_window_h.back() +
+            int32_t num_layer_sliding_pages =
                 std::min(static_cast<int32_t>(block.page_ids.size()),
-                         static_cast<int32_t>(1024 / page_size_ +
-                                              (block.seq_length % page_size_ ? 
1 : 0))));
-            for (int i = page_indices_h.size() - 
page_indptr_sliding_window_h.back();
+                         GetLayerSlidingWindowNumPages(block.seq_length));
+            
page_indptr_sliding_window_h.push_back(page_indptr_sliding_window_h.back() +
+                                                   num_layer_sliding_pages);
+            for (int i = page_indices_h.size() - num_layer_sliding_pages;
                  i < static_cast<int32_t>(page_indices_h.size()); i++) {
               page_indices_sliding_window_h.push_back(page_indices_h[i]);
             }
@@ -1042,11 +1063,7 @@ class PagedAttentionKVCacheObj : public 
AttentionKVCacheObj {
                               page_size_ +
                           1);
             if (support_layer_sliding_window_) {
-              if (block.seq_length < 1024) {
-                sliding_window_offset_h.push_back(0);
-              } else {
-                sliding_window_offset_h.push_back(block.seq_length % 
page_size_);
-              }
+              
sliding_window_offset_h.push_back(GetLayerSlidingWindowOffset(block.seq_length));
             } else {
               sliding_window_offset_h.push_back(block.sliding_window_offset);
             }
@@ -1055,8 +1072,8 @@ class PagedAttentionKVCacheObj : public 
AttentionKVCacheObj {
 
             // If sliding window, we need to calculate the positional offset
             if (support_layer_sliding_window_) {
-              k_rope_pos_offset_sliding_window_h.push_back(
-                  std::max(0, block.start_pos + block.seq_length - 1024));
+              k_rope_pos_offset_sliding_window_h.push_back(std::max<int64_t>(
+                  0, block.start_pos + block.seq_length - 
layer_sliding_window_size_));
             }
           } else {
             // Blocks at maximum depth
@@ -1078,12 +1095,11 @@ class PagedAttentionKVCacheObj : public 
AttentionKVCacheObj {
               last_block_id = id;
             }
             page_indptr_h.push_back(page_indptr_h.back() + num_pages);
-            page_indptr_sliding_window_h.push_back(
-                page_indptr_sliding_window_h.back() +
-                std::min(static_cast<int32_t>(block.page_ids.size()),
-                         static_cast<int32_t>(1024 / page_size_ +
-                                              (block.seq_length % page_size_ ? 
1 : 0))));
-            for (int i = page_indices_h.size() - 
page_indptr_sliding_window_h.back();
+            int32_t num_layer_sliding_pages =
+                std::min(num_pages, 
GetLayerSlidingWindowNumPages(total_seq_length));
+            
page_indptr_sliding_window_h.push_back(page_indptr_sliding_window_h.back() +
+                                                   num_layer_sliding_pages);
+            for (int i = page_indices_h.size() - num_layer_sliding_pages;
                  i < static_cast<int32_t>(page_indices_h.size()); i++) {
               page_indices_sliding_window_h.push_back(page_indices_h[i]);
             }
@@ -1095,19 +1111,15 @@ class PagedAttentionKVCacheObj : public 
AttentionKVCacheObj {
                                                     page_size_ +
                                                 1);
             if (support_layer_sliding_window_) {
-              if (last_block.seq_length < 1024) {
-                sliding_window_offset_h.push_back(0);
-              } else {
-                sliding_window_offset_h.push_back(last_block.seq_length % 
page_size_);
-              }
+              
sliding_window_offset_h.push_back(GetLayerSlidingWindowOffset(total_seq_length));
             } else {
               
sliding_window_offset_h.push_back(last_block.sliding_window_offset);
             }
             sink_size_h.push_back(last_block.sink_length);
             k_rope_pos_offset_h.push_back(block.start_pos);
             if (support_layer_sliding_window_) {
-              k_rope_pos_offset_sliding_window_h.push_back(
-                  std::max(0, block.start_pos + block.seq_length - 1024));
+              k_rope_pos_offset_sliding_window_h.push_back(std::max<int64_t>(
+                  0, block.start_pos + total_seq_length - 
layer_sliding_window_size_));
             }
           }
         }
@@ -1466,12 +1478,52 @@ class PagedAttentionKVCacheObj : public 
AttentionKVCacheObj {
 
     if (attn_kind == AttnKind::kMHA) {
       MHACrossAttnInternal(local_layer_id, q_data, o_data, lse_data, sm_scale,
-                           /*is_first_kernel=*/true);
+                           /*is_first_kernel=*/true, /*causal=*/false);
     } else {
       MLACrossAttnInternal(local_layer_id, q_data, o_data, lse_data, sm_scale);
     }
   }
 
+  void AttentionWithSharedKV(int64_t source_layer_id, Tensor q_data, Tensor 
current_k_data,
+                             Tensor current_v_data, Tensor o_data, double 
sm_scale) final {
+    int64_t local_layer_id = source_layer_id - layer_id_begin_offset_;
+    TVM_FFI_ICHECK_GE(local_layer_id, 0);
+    TVM_FFI_ICHECK_LT(local_layer_id, num_layers_);
+    Tensor pages = pages_[local_layer_id];
+    TVM_FFI_ICHECK(q_data.DataType() == pages.DataType());
+    TVM_FFI_ICHECK(current_k_data.DataType() == pages.DataType());
+    TVM_FFI_ICHECK(current_v_data.DataType() == pages.DataType());
+    TVM_FFI_ICHECK(o_data.DataType() == pages.DataType());
+    TVM_FFI_ICHECK(attn_kinds_[source_layer_id] == AttnKind::kMHA ||
+                   attn_kinds_[source_layer_id] == AttnKind::kMHASliding)
+        << "Querying K/V from another logical layer is only supported for MHA 
caches.";
+
+    int64_t total_seq_length = 0;
+    for (int64_t seq_id = 0; seq_id < cur_batch_size_; ++seq_id) {
+      total_seq_length += cur_append_lengths_[seq_id];
+    }
+    TVM_FFI_ICHECK_EQ(q_data->ndim, 3);
+    TVM_FFI_ICHECK_EQ(current_k_data->ndim, 3);
+    TVM_FFI_ICHECK_EQ(current_v_data->ndim, 3);
+    TVM_FFI_ICHECK_EQ(o_data->ndim, 3);
+    TVM_FFI_ICHECK_EQ(q_data->shape[0], total_seq_length);
+    TVM_FFI_ICHECK_EQ(current_k_data->shape[0], total_seq_length);
+    TVM_FFI_ICHECK_EQ(current_v_data->shape[0], total_seq_length);
+    TVM_FFI_ICHECK_EQ(o_data->shape[0], total_seq_length);
+    TVM_FFI_ICHECK_EQ(q_data->shape[1], num_qo_heads_);
+    TVM_FFI_ICHECK_EQ(current_k_data->shape[1], num_kv_heads_);
+    TVM_FFI_ICHECK_EQ(current_v_data->shape[1], num_kv_heads_);
+    TVM_FFI_ICHECK_EQ(o_data->shape[1], num_qo_heads_);
+    TVM_FFI_ICHECK_EQ(q_data->shape[2], qk_head_dim_);
+    TVM_FFI_ICHECK_EQ(current_k_data->shape[2], qk_head_dim_);
+    TVM_FFI_ICHECK_EQ(current_v_data->shape[2], v_head_dim_);
+    TVM_FFI_ICHECK_EQ(o_data->shape[2], v_head_dim_);
+
+    ComputeStreamWaitForCopyStream();
+    TVM_FFI_ICHECK(!dirty_aux_data_device_);
+    AttentionInternal(source_layer_id, q_data, current_k_data, current_v_data, 
o_data, sm_scale);
+  }
+
   void AppendMLAKV(int64_t layer_id, Tensor kv_data) final {
     // Shape and dtype check.
     int64_t local_layer_id = layer_id - layer_id_begin_offset_;
@@ -2120,7 +2172,9 @@ class PagedAttentionKVCacheObj : public 
AttentionKVCacheObj {
     }
     bool self_attn_computed = !is_first_kernel;
     bool cross_attn_computed = MHACrossAttnInternal(
-        local_layer_id, q_data, output, merged_attn_lse_view_, sm_scale, 
is_first_kernel);
+        local_layer_id, q_data, output, merged_attn_lse_view_, sm_scale, 
is_first_kernel,
+        /*causal=*/!append_before_attn_ &&
+            attn_kinds_[local_layer_id + layer_id_begin_offset_] == 
AttnKind::kMHASliding);
     TVM_FFI_ICHECK(self_attn_computed || cross_attn_computed)
         << "Both self-attention and cross-attention are not computed.";
   }
@@ -2160,7 +2214,7 @@ class PagedAttentionKVCacheObj : public 
AttentionKVCacheObj {
 
   /*! \brief Compute cross-attention for MHA. Return if there is effective 
computation. */
   bool MHACrossAttnInternal(int64_t local_layer_id, Tensor q_data, Tensor 
o_data, Tensor lse_data,
-                            double sm_scale, bool is_first_kernel) {
+                            double sm_scale, bool is_first_kernel, bool 
causal) {
     std::unique_ptr<PagedPrefillFunc>& f_prefill =
         (!support_sliding_window_ &&
          attn_kinds_[local_layer_id + layer_id_begin_offset_] != 
AttnKind::kMHASliding)
@@ -2229,8 +2283,7 @@ class PagedAttentionKVCacheObj : public 
AttentionKVCacheObj {
         // Use prefill kernel for depth d
         TVM_FFI_ICHECK_NOTNULL(f_prefill);
         f_prefill->MHA(d, q_data, qo_indptr_on_depths_view_[d], 
pages_[local_layer_id], page_indptr,
-                       page_indices, length_info, q_rope_position_map_view_, 
k_rope_pos,
-                       /*causal=*/false,
+                       page_indices, length_info, q_rope_position_map_view_, 
k_rope_pos, causal,
                        /*rotary_mode=*/rope_mode_, rotary_scale, rotary_theta, 
sm_scale,
                        attn_output, attn_lse, compute_stream_);
       }
@@ -2550,12 +2603,15 @@ TVM_FFI_STATIC_INIT_BLOCK() {
           attn_kinds_vec.push_back(static_cast<AttnKind>(attn_kind));
         }
 
-        TVM_FFI_ICHECK_EQ(cache_config.size(), 5);
+        TVM_FFI_ICHECK(cache_config.size() == 5 || cache_config.size() == 6)
+            << "KV cache config must contain five legacy fields and an 
optional per-layer "
+               "sliding window size.";
         int64_t reserved_num_seqs = cache_config[0];
         int64_t total_token_capacity = cache_config[1];
         int64_t prefill_chunk_size = cache_config[2];
         int64_t page_size = cache_config[3];
         bool support_sliding_window = cache_config[4];
+        int64_t layer_sliding_window_size = cache_config.size() == 6 ? 
cache_config[5] : 1024;
         int64_t num_total_pages = (total_token_capacity + page_size - 1) / 
page_size + 1;
         if (support_sliding_window) {
           // When sliding window is enabled, each sequence may use two more 
pages at most.
@@ -2566,9 +2622,10 @@ TVM_FFI_STATIC_INIT_BLOCK() {
         ffi::ObjectPtr<PagedAttentionKVCacheObj> n = 
ffi::make_object<PagedAttentionKVCacheObj>(
             page_size, num_layers, layer_id_begin_offset, layer_id_end_offset, 
num_qo_heads,
             num_kv_heads, qk_head_dim, v_head_dim, attn_kinds_vec, 
reserved_num_seqs,
-            num_total_pages, prefill_chunk_size, support_sliding_window, 
RoPEMode(rope_mode),
-            rotary_scale, rotary_theta, std::move(rope_ext_factors), 
enable_kv_transfer,  //
-            init->dtype, init->device,                                         
           //
+            num_total_pages, prefill_chunk_size, support_sliding_window, 
layer_sliding_window_size,
+            RoPEMode(rope_mode), rotary_scale, rotary_theta, 
std::move(rope_ext_factors),
+            enable_kv_transfer,         //
+            init->dtype, init->device,  //
             std::move(f_transpose_append_mha), 
std::move(f_transpose_append_mla),
             std::move(f_compact_copy), std::move(f_attention_prefill_ragged),
             std::move(f_attention_prefill), std::move(f_attention_decode),
diff --git 
a/tests/python/relax/test_runtime_builtin_paged_attention_kv_cache_cpu.py 
b/tests/python/relax/test_runtime_builtin_paged_attention_kv_cache_cpu.py
index dfd0135178..ed86585e5d 100644
--- a/tests/python/relax/test_runtime_builtin_paged_attention_kv_cache_cpu.py
+++ b/tests/python/relax/test_runtime_builtin_paged_attention_kv_cache_cpu.py
@@ -71,6 +71,7 @@ fbegin_forward = None
 fend_forward = None
 fcommit_accepted_token_tree_nodes = None
 fattention_with_fuse_qkv = None
+fattention_with_shared_kv = None
 fis_empty = None
 fdebug_get_kv = None
 
@@ -92,10 +93,10 @@ fcompact_copy = None
 _COMPILED_KERNEL_CACHE = {}
 
 
-def set_global_func(head_dim, dtype):
+def set_global_func(head_dim, dtype, layer_sliding_window_size=1024):
     global fclear, fadd_sequence, fremove_sequence, ffork_sequence, 
fenable_sliding_window_for_seq
     global fpopn, fbegin_forward, fend_forward, 
fcommit_accepted_token_tree_nodes
-    global fattention_with_fuse_qkv, fis_empty, fdebug_get_kv
+    global fattention_with_fuse_qkv, fattention_with_shared_kv, fis_empty, 
fdebug_get_kv
     global ftranspose_append, fcopy_cache, fattn_prefill, fattn_decode
     global \
         fattn_prefill_ragged, \
@@ -120,6 +121,9 @@ def set_global_func(head_dim, dtype):
     fattention_with_fuse_qkv = tvm.get_global_func(
         "vm.builtin.attention_kv_cache_attention_with_fused_qkv"
     )
+    fattention_with_shared_kv = tvm.get_global_func(
+        "vm.builtin.attention_kv_cache_attention_with_shared_kv"
+    )
     fis_empty = tvm.get_global_func("vm.builtin.attention_kv_cache_empty")
     fdebug_get_kv = 
tvm.get_global_func("vm.builtin.attention_kv_cache_debug_get_kv")
 
@@ -135,6 +139,7 @@ def set_global_func(head_dim, dtype):
         rope_scale,
         rope_theta,
         json.dumps(rope_scaling, sort_keys=True),
+        layer_sliding_window_size,
     )
     builts = _COMPILED_KERNEL_CACHE.get(cache_key)
     if builts is None:
@@ -146,7 +151,15 @@ def set_global_func(head_dim, dtype):
                 num_kv_heads, num_qo_heads, head_dim, dtype, False, 
rope_scaling
             ),
             _attention_decode_cpu(num_kv_heads, num_qo_heads, head_dim, dtype, 
False, rope_scaling),
-            _attention_prefill_cpu(num_kv_heads, num_qo_heads, head_dim, 
dtype, True, rope_scaling),
+            _attention_prefill_cpu(
+                num_kv_heads,
+                num_qo_heads,
+                head_dim,
+                dtype,
+                True,
+                rope_scaling,
+                sliding_window_size=layer_sliding_window_size,
+            ),
             _attention_decode_cpu(num_kv_heads, num_qo_heads, head_dim, dtype, 
True, rope_scaling),
             _attention_prefill_ragged_cpu(
                 num_kv_heads, num_qo_heads, head_dim, head_dim, dtype, 
rope_scaling
@@ -187,24 +200,34 @@ def set_global_func(head_dim, dtype):
     ) = builts
 
 
-def create_kv_cache(head_dim, dtype, rope_mode, support_sliding_window):
+def create_kv_cache(
+    head_dim,
+    dtype,
+    rope_mode,
+    support_sliding_window,
+    layer_sliding_window_size=None,
+    attn_kinds=None,
+):
     fcreate = tvm.get_global_func("vm.builtin.paged_attention_kv_cache_create")
+    cache_config = [
+        reserved_nseq,
+        maximum_total_seq_length,
+        prefill_chunk_size,
+        page_size,
+        int(support_sliding_window),
+    ]
+    if layer_sliding_window_size is not None:
+        cache_config.append(layer_sliding_window_size)
+    if attn_kinds is None:
+        attn_kinds = [int(AttnKind.MHA) for _ in range(num_layers)]
     cache = fcreate(
-        tvm_ffi.Shape(
-            [
-                reserved_nseq,
-                maximum_total_seq_length,
-                prefill_chunk_size,
-                page_size,
-                int(support_sliding_window),
-            ]
-        ),
+        tvm_ffi.Shape(cache_config),
         tvm_ffi.Shape([0, num_layers]),
         num_qo_heads,
         num_kv_heads,
         head_dim,
         head_dim,  # v_head_dim
-        tvm_ffi.Shape([int(AttnKind.MHA) for _ in range(num_layers)]),
+        tvm_ffi.Shape(attn_kinds),
         False,  # enable_kv_transfer
         rope_mode,
         rope_scale,
@@ -570,6 +593,116 @@ def apply_attention(
     verify_cached_kv(kv_cache, seq_ids, cached_k, cached_v)
 
 
+def _causal_attention_reference(q, k, v, past_length, 
sliding_window_size=None):
+    k = np.repeat(k, num_qo_heads // num_kv_heads, axis=1)
+    v = np.repeat(v, num_qo_heads // num_kv_heads, axis=1)
+    scores = np.einsum("qhd,khd->hqk", q.astype("float32"), 
k.astype("float32")) * sm_scale
+    query_positions = past_length + np.arange(q.shape[0])
+    key_positions = np.arange(k.shape[0])
+    mask = key_positions[None, :] <= query_positions[:, None]
+    if sliding_window_size is not None:
+        mask &= key_positions[None, :] > query_positions[:, None] - 
sliding_window_size
+    scores = np.where(mask[None, :, :], scores, np.finfo("float32").min)
+    probabilities = scipy.special.softmax(scores, axis=-1)
+    return np.einsum("hqk,khd->qhd", probabilities, v.astype("float32"))
+
+
+def test_per_layer_sliding_window():
+    global head_dim, sm_scale, dtype
+    head_dim = 64
+    sm_scale = head_dim ** (-0.5)
+    dtype = "float32"
+    layer_window_size = 3
+    set_global_func(head_dim, dtype, layer_window_size)
+    kv_cache = create_kv_cache(
+        head_dim,
+        dtype,
+        RopeMode.NONE,
+        False,
+        layer_sliding_window_size=layer_window_size,
+        attn_kinds=[int(AttnKind.MHA_SLIDING)] + [int(AttnKind.MHA)] * 
(num_layers - 1),
+    )
+    fadd_sequence(kv_cache, 0)
+
+    rng = np.random.default_rng(0)
+    cached_k = np.empty((0, num_kv_heads, head_dim), dtype=dtype)
+    cached_v = np.empty((0, num_kv_heads, head_dim), dtype=dtype)
+    for append_length in [3, 2, 1]:
+        past_length = cached_k.shape[0]
+        q = rng.standard_normal((append_length, num_qo_heads, 
head_dim)).astype(dtype)
+        current_k = rng.standard_normal((append_length, num_kv_heads, 
head_dim)).astype(dtype)
+        current_v = rng.standard_normal((append_length, num_kv_heads, 
head_dim)).astype(dtype)
+
+        fbegin_forward(kv_cache, Shape([0]), Shape([append_length]), None)
+        qkv = tvm.runtime.tensor(np.concatenate([q, current_k, current_v], 
axis=1), device)
+        output = tvm.runtime.empty(q.shape, dtype, device=device)
+        fattention_with_fuse_qkv(kv_cache, 0, sm_scale, qkv, output)
+
+        cached_k = np.concatenate([cached_k, current_k], axis=0)
+        cached_v = np.concatenate([cached_v, current_v], axis=0)
+        expected = _causal_attention_reference(
+            q, cached_k, cached_v, past_length, layer_window_size
+        )
+        tvm.testing.assert_allclose(output.numpy(), expected, rtol=1e-3, 
atol=1e-3)
+        fend_forward(kv_cache)
+
+
[email protected](
+    ("source_attn_kind", "layer_window_size"),
+    [(AttnKind.MHA, None), (AttnKind.MHA_SLIDING, 3)],
+)
+def test_attention_with_shared_kv(source_attn_kind, layer_window_size):
+    global head_dim, sm_scale, dtype
+    head_dim = 64
+    sm_scale = head_dim ** (-0.5)
+    dtype = "float32"
+    set_global_func(head_dim, dtype, layer_window_size or 1024)
+    attn_kinds = [int(source_attn_kind)] + [int(AttnKind.MHA)] * (num_layers - 
1)
+    kv_cache = create_kv_cache(
+        head_dim,
+        dtype,
+        RopeMode.NONE,
+        False,
+        layer_sliding_window_size=layer_window_size or 1024,
+        attn_kinds=attn_kinds,
+    )
+    fadd_sequence(kv_cache, 0)
+
+    rng = np.random.default_rng(0)
+    cached_k = np.empty((0, num_kv_heads, head_dim), dtype=dtype)
+    cached_v = np.empty((0, num_kv_heads, head_dim), dtype=dtype)
+    for append_length in [3, 2, 1]:
+        past_length = cached_k.shape[0]
+        source_q = rng.standard_normal((append_length, num_qo_heads, 
head_dim)).astype(dtype)
+        current_k = rng.standard_normal((append_length, num_kv_heads, 
head_dim)).astype(dtype)
+        current_v = rng.standard_normal((append_length, num_kv_heads, 
head_dim)).astype(dtype)
+        shared_q = rng.standard_normal((append_length, num_qo_heads, 
head_dim)).astype(dtype)
+
+        fbegin_forward(kv_cache, Shape([0]), Shape([append_length]), None)
+        qkv = tvm.runtime.tensor(np.concatenate([source_q, current_k, 
current_v], axis=1), device)
+        source_output = tvm.runtime.empty(source_q.shape, dtype, device=device)
+        fattention_with_fuse_qkv(kv_cache, 0, sm_scale, qkv, source_output)
+
+        shared_output = tvm.runtime.empty(shared_q.shape, dtype, device=device)
+        fattention_with_shared_kv(
+            kv_cache,
+            0,
+            sm_scale,
+            tvm.runtime.tensor(shared_q, device),
+            tvm.runtime.tensor(current_k, device),
+            tvm.runtime.tensor(current_v, device),
+            shared_output,
+        )
+
+        cached_k = np.concatenate([cached_k, current_k], axis=0)
+        cached_v = np.concatenate([cached_v, current_v], axis=0)
+        expected = _causal_attention_reference(
+            shared_q, cached_k, cached_v, past_length, layer_window_size
+        )
+        tvm.testing.assert_allclose(shared_output.numpy(), expected, 
rtol=1e-3, atol=1e-3)
+        fend_forward(kv_cache)
+
+
 def test_paged_attention_kv_cache_prefill_and_decode(kv_cache_and_config):
     kv_cache, rope_mode, support_sliding_window = kv_cache_and_config
     if support_sliding_window and rope_mode == RopeMode.NORMAL:
diff --git 
a/tests/python/relax/test_runtime_builtin_paged_attention_kv_cache_tir.py 
b/tests/python/relax/test_runtime_builtin_paged_attention_kv_cache_tir.py
index fe7fc0ff07..bd9b566c23 100644
--- a/tests/python/relax/test_runtime_builtin_paged_attention_kv_cache_tir.py
+++ b/tests/python/relax/test_runtime_builtin_paged_attention_kv_cache_tir.py
@@ -18,7 +18,9 @@
 import functools
 import itertools
 import json
+import math
 
+import numpy as np
 import pytest
 import torch
 import tvm_ffi
@@ -91,6 +93,69 @@ fcompact_copy = None
 _COMPILED_KERNEL_CACHE = {}
 
 
[email protected]
[email protected](not env.has_gpu(), reason="need gpu")
[email protected]("device_type", ["cuda", "metal"])
+def test_paged_prefill_layer_sliding_window_mask(device_type):
+    if not tvm.testing.device_enabled(device_type):
+        pytest.skip(f"{device_type} not enabled")
+
+    dev = tvm.device(device_type)
+    target = tvm.target.Target.from_device(dev)
+    head_dim = 64
+    num_kv_heads = 2
+    num_qo_heads = 4
+    page_size = 16
+    tir_func = _attention_prefill(
+        num_kv_heads,
+        num_qo_heads,
+        head_dim,
+        "float16",
+        True,
+        {},
+        target,
+        page_size=page_size,
+        sliding_window_size=3,
+    )
+    built = tvm.tirx.build(tir_func, target=target)
+
+    q = np.zeros((2, num_qo_heads, head_dim), dtype="float16")
+    q_indptr = np.array([0, 2], dtype="int32")
+    pages = np.zeros((1, 2, num_kv_heads, page_size, head_dim), 
dtype="float16")
+    for position, value in enumerate([1, 3, 5]):
+        pages[0, 1, :, position, :] = value
+    page_indptr = np.array([0, 1], dtype="int32")
+    page_values = np.array([0], dtype="int32")
+    length_info = np.array([[3], [0], [0]], dtype="int32")
+    k_rope_pos_offset = np.array([0], dtype="int32")
+    q_rope_position = np.array([3, 4], dtype="int32")
+    output = np.zeros_like(q)
+    lse = np.zeros((2, num_qo_heads), dtype="float32")
+
+    args = [
+        tvm.runtime.tensor(array, device=dev)
+        for array in [
+            q,
+            q_indptr,
+            pages,
+            page_indptr,
+            page_values,
+            length_info,
+            k_rope_pos_offset,
+            q_rope_position,
+            output,
+            lse,
+        ]
+    ]
+
+    def run_and_check():
+        built.main(*args, 1, 0, 1.0, 10000.0, 1 / math.sqrt(head_dim))
+        expected = np.array([4, 5], dtype="float16")
+        tvm.testing.assert_allclose(args[8].numpy()[:, 0, 0], expected, 
rtol=1e-3, atol=1e-3)
+
+    tvm.testing.run_with_gpu_lock(run_and_check)
+
+
 def set_global_func(head_dim, dtype, target):
     global fclear, fadd_sequence, fremove_sequence, ffork_sequence, 
fenable_sliding_window_for_seq
     global fpopn, fbegin_forward, fend_forward, 
fcommit_accepted_token_tree_nodes

Reply via email to