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 993863d083 [WebGPU] Parallelize prefill attention for 512-wide FP32 
heads (#20700)
993863d083 is described below

commit 993863d0831c15d31b5da37e609fc175791be718
Author: Akaash Parthasarathy <[email protected]>
AuthorDate: Tue Oct 13 19:59:15 2026 -0700

    [WebGPU] Parallelize prefill attention for 512-wide FP32 heads (#20700)
    
    WebGPU prefill with 512-wide FP32 heads spends most of each Q/K dot
    product in a serial loop. This affects Gemma 4 E2B `q4f32_1`; the
    small-tile fallback in #20658 makes the kernel fit in shared memory but
    leaves this bottleneck.
    
    Distribute each dot product across 32 threads in a 128-thread workgroup.
    For paged global attention, split the KV range into four dispatches and
    merge their outputs using log-sum-exp weights, so one dispatch does not
    process the entire context. Ragged and sliding attention use a single
    dispatch.
    
    Select this path only for WebGPU FP32 attention with 512-wide heads.
---
 .../tvm/relax/frontend/nn/llm/_prefill_kernels.py  |  10 +
 .../tvm/relax/frontend/nn/llm/_prefill_parallel.py | 486 +++++++++++++++++++++
 tests/python/relax/test_nn_llm_prefill_parallel.py | 151 +++++++
 3 files changed, 647 insertions(+)

diff --git a/python/tvm/relax/frontend/nn/llm/_prefill_kernels.py 
b/python/tvm/relax/frontend/nn/llm/_prefill_kernels.py
index fd98fe83b3..dc5e77576a 100644
--- a/python/tvm/relax/frontend/nn/llm/_prefill_kernels.py
+++ b/python/tvm/relax/frontend/nn/llm/_prefill_kernels.py
@@ -50,6 +50,10 @@ from ._kernel_common import (
     _rope,
     _schedule_prefill_kernel,
 )
+from ._prefill_parallel import (
+    _attention_prefill_paged_parallel,
+    _attention_prefill_ragged_parallel,
+)
 
 
 def _attention_prefill_cpu(
@@ -213,6 +217,10 @@ def _attention_prefill(
     page_size: int = 16,
     sliding_window_size: int = 1024,
 ):
+    if target.kind.name == "webgpu" and dtype == "float32" and d == 512:
+        return _attention_prefill_paged_parallel(
+            h_kv, h_q, rope_scaling, sliding_window, page_size, 
sliding_window_size
+        )
     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"
@@ -747,6 +755,8 @@ def _attention_prefill_ragged_cpu(h_kv, h_q, d_qk, d_v, 
dtype, rope_scaling: dic
     return batch_prefill_ragged_kv
 
 def _attention_prefill_ragged(h_kv, h_q, d_qk, d_v, dtype, rope_scaling: 
dict[str, Any], target: Target):
+    if target.kind.name == "webgpu" and dtype == "float32" and d_qk == d_v == 
512:
+        return _attention_prefill_ragged_parallel(h_kv, h_q, rope_scaling)
     NUM_BLKS, LOAD_VEC, group_size, bdx, num_warps, tile_x, tile_y, tile_z = 
_get_prefill_kernel_config(h_kv, h_q, d_qk, dtype, target, d_v=d_v)
     init_states, compute_s_gemm, softmax_update_causal, compute_o_gemm, _, 
advance_tile_batch, paged_store_output_lse, *_ = _make_prefill_macros(tile_x, 
tile_y, tile_z, d_v, bdx, num_warps, group_size)
 
diff --git a/python/tvm/relax/frontend/nn/llm/_prefill_parallel.py 
b/python/tvm/relax/frontend/nn/llm/_prefill_parallel.py
new file mode 100644
index 0000000000..73b7da44f9
--- /dev/null
+++ b/python/tvm/relax/frontend/nn/llm/_prefill_parallel.py
@@ -0,0 +1,486 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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.
+"""Parallel dot-product prefill for wide FP32 WebGPU attention heads."""
+
+from tvm.script import s_tir as Ts
+from tvm.script import tirx as T
+
+from ._kernel_common import _get_kv_chunk_len, _get_seq_offset, 
_length_info_buffer, _rope
+
+
+def _parallel_prefill_body(
+    h_kv,
+    h_q,
+    rope_scaling,
+    paged=False,
+    sliding_window=False,
+    page_size=16,
+    sliding_window_size=1024,
+    partitions=1,
+):
+    # One workgroup handles one query/head. Four groups of 32 threads each
+    # reduce a 512-element Q/K dot product, then update the online softmax.
+    dtype = "float32"
+
+    def kv_length(length, batch, length_info):
+        return (
+            _get_kv_chunk_len(length, page_size, batch, length_info, 
sliding_window)
+            if paged
+            else length
+        )
+
+    def kv_page(indptr, indices, length_info, batch, key):
+        if paged:
+            slot = _get_seq_offset(key, batch, length_info, sliding_window)
+            return indices[indptr[batch] + slot // page_size]
+        return indptr[batch] + key
+
+    def kv_offset(length_info, batch, key):
+        return _get_seq_offset(key, batch, length_info, sliding_window) % 
page_size if paged else 0
+
+    def kv_index(page, offset, head, feature, kind):
+        return (page, kind, head, offset, feature) if paged else (page, head, 
feature)
+
+    def load_key(
+        source, page, offset, position, head, feature, rotary_mode, 
rope_scale, rope_theta
+    ):
+        index = kv_index(page, offset, head, feature, 0)
+        return T.if_then_else(
+            rotary_mode == 1,
+            _rope(source, position, 512, rope_theta, rope_scale, index, dtype, 
rope_scaling),
+            source[index],
+        )
+
+    def load_value(source, page, offset, head, feature):
+        return source[kv_index(page, offset, head, feature, 1)]
+
+    @T.macro
+    def body(
+        q,
+        q_indptr,
+        k,
+        v,
+        kv_indptr,
+        q_rope_position,
+        k_rope_pos_offset,
+        output,
+        lse,
+        causal,
+        rotary_mode,
+        rope_scale,
+        rope_theta,
+        sm_scale,
+        page_values,
+        length_info,
+        part_idx=0,
+    ):
+        for row in T.parallel(q.shape[0], annotations={"thread_binding": 
"blockIdx.x"}):
+            for head in T.parallel(h_q, annotations={"thread_binding": 
"blockIdx.y"}):
+                for lane in T.parallel(128, annotations={"thread_binding": 
"threadIdx.x"}):
+                    with Ts.sblock("attention"):
+                        Ts.reads()
+                        Ts.writes()
+                        partial = Ts.sblock_alloc_tensor((128,), "float32", 
scope="shared")
+                        probabilities = Ts.sblock_alloc_tensor((4,), 
"float32", scope="shared")
+                        state = Ts.sblock_alloc_tensor((3,), "float32", 
scope="shared")
+                        query = Ts.sblock_alloc_tensor((16,), "float32", 
scope="local")
+                        accum = Ts.sblock_alloc_tensor((4,), "float32", 
scope="local")
+                        score = Ts.sblock_alloc_tensor((1,), "float32", 
scope="local")
+                        weights = Ts.sblock_alloc_tensor((4,), "float32", 
scope="local")
+                        denominator = Ts.sblock_alloc_tensor((1,), "float32", 
scope="local")
+                        rescale = Ts.sblock_alloc_tensor((1,), "float32", 
scope="local")
+                        b = Ts.sblock_alloc_tensor((1,), "int32", 
scope="local")
+                        b[0] = 0
+                        while row >= q_indptr[b[0] + 1]:
+                            b[0] += 1
+                        begin: T.let[T.int32] = kv_indptr[b[0]]
+                        count: T.let[T.int32] = kv_length(
+                            kv_indptr[b[0] + 1] - begin, b[0], length_info
+                        )
+                        limit: T.let[T.int32] = T.if_then_else(
+                            causal != 0 and not sliding_window,
+                            T.min(
+                                count,
+                                row
+                                - q_indptr[b[0]]
+                                + count
+                                - (q_indptr[b[0] + 1] - q_indptr[b[0]])
+                                + 1,
+                            ),
+                            count,
+                        )
+                        first: T.let[T.int32] = T.if_then_else(
+                            causal != 0 and sliding_window,
+                            T.max(
+                                count - T.max(sliding_window_size - (row - 
q_indptr[b[0]]) - 1, 0),
+                                0,
+                            ),
+                            0,
+                        )
+                        width: T.let[T.int32] = T.ceildiv(T.max(limit - first, 
0), partitions)
+                        partition_first: T.let[T.int32] = T.min(limit, first + 
part_idx * width)
+                        partition_limit: T.let[T.int32] = T.min(
+                            limit, first + (part_idx + 1) * width
+                        )
+                        kh: T.let[T.int32] = head // (h_q // h_kv)
+                        for j in T.unroll(16):
+                            feature: T.let[T.int32] = j * 32 + lane % 32
+                            query[j] = T.if_then_else(
+                                rotary_mode == 1,
+                                _rope(
+                                    q,
+                                    q_rope_position[row],
+                                    512,
+                                    rope_theta,
+                                    rope_scale,
+                                    (row, head, feature),
+                                    dtype,
+                                    rope_scaling,
+                                ),
+                                q[row, head, feature],
+                            )
+                        for j in T.unroll(4):
+                            accum[j] = 0
+                        if lane == 0:
+                            state[0] = -50000
+                            state[1] = 0
+                        T.gpu_storage_sync("shared")
+                        for tile in T.serial(T.ceildiv(partition_limit - 
partition_first, 4)):
+                            key: T.let[T.int32] = partition_first + tile * 4 + 
lane // 32
+                            score[0] = 0
+                            if key < partition_limit:
+                                page: T.let[T.int32] = kv_page(
+                                    kv_indptr, page_values, length_info, b[0], 
key
+                                )
+                                offset: T.let[T.int32] = 
kv_offset(length_info, b[0], key)
+                                for j in T.unroll(16):
+                                    feature: T.let[T.int32] = j * 32 + lane % 
32
+                                    score[0] += query[j] * load_key(
+                                        k,
+                                        page,
+                                        offset,
+                                        k_rope_pos_offset[b[0]] + key,
+                                        kh,
+                                        feature,
+                                        rotary_mode,
+                                        rope_scale,
+                                        rope_theta,
+                                    )
+                            partial[lane] = score[0]
+                            T.gpu_storage_sync("shared")
+                            for reduction in T.unroll(5):
+                                width: T.let[T.int32] = 16 >> reduction
+                                if lane % 32 < width:
+                                    partial[lane] += partial[lane + width]
+                                T.gpu_storage_sync("shared")
+                            if lane == 0:
+                                score[0] = state[0]
+                                for j in T.unroll(4):
+                                    weights[j] = T.if_then_else(
+                                        partition_first + tile * 4 + j < 
partition_limit,
+                                        partial[j * 32] * sm_scale * 
1.4426950408889634,
+                                        T.float32(-50000),
+                                    )
+                                    score[0] = T.max(score[0], weights[j])
+                                rescale[0] = T.exp2(state[0] - score[0])
+                                denominator[0] = state[1] * rescale[0]
+                                for j in T.unroll(4):
+                                    weights[j] = T.if_then_else(
+                                        partition_first + tile * 4 + j < 
partition_limit,
+                                        T.exp2(weights[j] - score[0]),
+                                        T.float32(0),
+                                    )
+                                    denominator[0] += weights[j]
+                            T.gpu_storage_sync("shared")
+                            if lane == 0:
+                                for j in T.unroll(4):
+                                    probabilities[j] = weights[j]
+                                state[0] = score[0]
+                                state[1] = denominator[0]
+                                state[2] = rescale[0]
+                            T.gpu_storage_sync("shared")
+                            for feature in T.unroll(4):
+                                accum[feature] *= state[2]
+                                for j in T.unroll(4):
+                                    if partition_first + tile * 4 + j < 
partition_limit:
+                                        page: T.let[T.int32] = kv_page(
+                                            kv_indptr,
+                                            page_values,
+                                            length_info,
+                                            b[0],
+                                            partition_first + tile * 4 + j,
+                                        )
+                                        offset: T.let[T.int32] = kv_offset(
+                                            length_info, b[0], partition_first 
+ tile * 4 + j
+                                        )
+                                        accum[feature] += probabilities[j] * 
load_value(
+                                            v, page, offset, kh, feature * 128 
+ lane
+                                        )
+                            T.gpu_storage_sync("shared")
+                        for feature in T.unroll(4):
+                            output[row, head, feature * 128 + lane] = 
T.if_then_else(
+                                state[1] > 0, accum[feature] / state[1], 
T.float32(0)
+                            )
+                        if lane == 0:
+                            lse[row, head] = T.if_then_else(
+                                state[1] > 0, state[0] + T.log2(state[1]), 
T.float32(-50000)
+                            )
+
+    return body
+
+
+def _attention_prefill_ragged_parallel(h_kv, h_q, rope_scaling):
+    batch, qlen, klen = [T.dynamic(x, "int32") for x in ["batch_size", 
"qo_len", "kv_len"]]
+    qi, ki, qp, kp = [
+        T.dynamic(x, "int32")
+        for x in [
+            "q_indptr_elem_offset",
+            "kv_indptr_elem_offset",
+            "q_rope_position_elem_offset",
+            "k_rope_pos_offset_elem_offset",
+        ]
+    ]
+    body = _parallel_prefill_body(h_kv, h_q, rope_scaling)
+
+    @Ts.function
+    def batch_prefill_ragged_kv(
+        q: T.Tensor((qlen, h_q, 512), "float32"),
+        q_indptr: T.Tensor((batch + 1,), "int32", elem_offset=qi),
+        k: T.Tensor((klen, h_kv, 512), "float32"),
+        v: T.Tensor((klen, h_kv, 512), "float32"),
+        kv_indptr: T.Tensor((batch + 1,), "int32", elem_offset=ki),
+        q_rope_position: T.Tensor((qlen,), "int32", elem_offset=qp),
+        k_rope_pos_offset: T.Tensor((batch,), "int32", elem_offset=kp),
+        output: T.Tensor((qlen, h_q, 512), "float32"),
+        lse: T.Tensor((qlen, h_q), "float32"),
+        causal: T.int32,
+        rotary_mode: T.int32,
+        rope_scale: T.float32,
+        rope_theta: T.float32,
+        sm_scale: T.float32,
+    ):
+        body(
+            q,
+            q_indptr,
+            k,
+            v,
+            kv_indptr,
+            q_rope_position,
+            k_rope_pos_offset,
+            output,
+            lse,
+            causal,
+            rotary_mode,
+            rope_scale,
+            rope_theta,
+            sm_scale,
+            kv_indptr,
+            kv_indptr,
+        )
+
+    return batch_prefill_ragged_kv.with_attr("tirx.is_scheduled", True)
+
+
+def _attention_prefill_paged_parallel(
+    h_kv, h_q, rope_scaling, sliding_window, page_size=16, 
sliding_window_size=1024
+):
+    if not sliding_window:
+        return _attention_prefill_paged_split(
+            h_kv, h_q, rope_scaling, sliding_window, page_size, 
sliding_window_size
+        )
+    batch, qlen, num_pages, nnz_pages = [
+        T.dynamic(x, "int32") for x in ["batch_size", "qo_len", "num_pages", 
"nnz_pages"]
+    ]
+    qi, pi, pv, li, qp, kp, po = [
+        T.dynamic(x, "int32")
+        for x in [
+            "q_indptr_elem_offset",
+            "page_indptr_elem_offset",
+            "page_values_elem_offset",
+            "length_info_elem_offset",
+            "q_rope_position_elem_offset",
+            "k_rope_pos_offset_elem_offset",
+            "pages_elem_offset",
+        ]
+    ]
+    body = _parallel_prefill_body(
+        h_kv, h_q, rope_scaling, True, sliding_window, page_size, 
sliding_window_size
+    )
+    symbol = "batch_prefill_paged_kv_sliding_window" if sliding_window else 
"batch_prefill_paged_kv"
+
+    @Ts.function
+    def batch_prefill_paged_kv(
+        q: T.Tensor((qlen, h_q, 512), "float32"),
+        q_indptr: T.Tensor((batch + 1,), "int32", elem_offset=qi),
+        pages: T.Tensor((num_pages, 2, h_kv, page_size, 512), "float32", 
elem_offset=po),
+        page_indptr: T.Tensor((batch + 1,), "int32", elem_offset=pi),
+        page_values: T.Tensor((nnz_pages,), "int32", elem_offset=pv),
+        length_info: _length_info_buffer(batch, sliding_window, li),
+        k_rope_pos_offset: T.Tensor((batch,), "int32", elem_offset=kp),
+        q_rope_position: T.Tensor((qlen,), "int32", elem_offset=qp),
+        output: T.Tensor((qlen, h_q, 512), "float32"),
+        lse: T.Tensor((qlen, h_q), "float32"),
+        causal: T.int32,
+        rotary_mode: T.int32,
+        rope_scale: T.float32,
+        rope_theta: T.float32,
+        sm_scale: T.float32,
+    ):
+        T.func_attr({"global_symbol": symbol})
+        body(
+            q,
+            q_indptr,
+            pages,
+            pages,
+            page_indptr,
+            q_rope_position,
+            k_rope_pos_offset,
+            output,
+            lse,
+            causal,
+            rotary_mode,
+            rope_scale,
+            rope_theta,
+            sm_scale,
+            page_values,
+            length_info,
+        )
+
+    return batch_prefill_paged_kv.with_attr("tirx.is_scheduled", True)
+
+
+def _attention_prefill_paged_split(
+    h_kv, h_q, rope_scaling, sliding_window, page_size=16, 
sliding_window_size=1024
+):
+    batch, qlen, num_pages, nnz_pages = [
+        T.dynamic(x, "int32") for x in ["batch_size", "qo_len", "num_pages", 
"nnz_pages"]
+    ]
+    qi, pi, pv, li, qp, kp, po = [
+        T.dynamic(x, "int32")
+        for x in [
+            "q_indptr_elem_offset",
+            "page_indptr_elem_offset",
+            "page_values_elem_offset",
+            "length_info_elem_offset",
+            "q_rope_position_elem_offset",
+            "k_rope_pos_offset_elem_offset",
+            "pages_elem_offset",
+        ]
+    ]
+    body = _parallel_prefill_body(
+        h_kv, h_q, rope_scaling, True, False, page_size, sliding_window_size, 
partitions=4
+    )
+    symbol = "batch_prefill_paged_kv_sliding_window" if sliding_window else 
"batch_prefill_paged_kv"
+
+    @Ts.function
+    def batch_prefill_paged_kv(
+        q: T.Tensor((qlen, h_q, 512), "float32"),
+        q_indptr: T.Tensor((batch + 1,), "int32", elem_offset=qi),
+        pages: T.Tensor((num_pages, 2, h_kv, page_size, 512), "float32", 
elem_offset=po),
+        page_indptr: T.Tensor((batch + 1,), "int32", elem_offset=pi),
+        page_values: T.Tensor((nnz_pages,), "int32", elem_offset=pv),
+        length_info: _length_info_buffer(batch, sliding_window, li),
+        k_rope_pos_offset: T.Tensor((batch,), "int32", elem_offset=kp),
+        q_rope_position: T.Tensor((qlen,), "int32", elem_offset=qp),
+        output: T.Tensor((qlen, h_q, 512), "float32"),
+        lse: T.Tensor((qlen, h_q), "float32"),
+        causal: T.int32,
+        rotary_mode: T.int32,
+        rope_scale: T.float32,
+        rope_theta: T.float32,
+        sm_scale: T.float32,
+    ):
+        T.func_attr({"global_symbol": symbol})
+        # Launch each KV partition separately so long contexts do not produce
+        # one long-running dispatch. Merge their softmax-weighted outputs 
below.
+        temporary = Ts.sblock_alloc_tensor(
+            (T.int64(4), T.Cast("int64", qlen), T.int64(h_q), T.int64(512)), 
"float32"
+        )
+        temporary_lse = Ts.sblock_alloc_tensor(
+            (T.int64(4), T.Cast("int64", qlen), T.int64(h_q)), "float32"
+        )
+        for part_idx in T.serial(4):
+            with Ts.sblock("part_idx"):
+                part_output = Ts.match_tensor(
+                    temporary[part_idx, 0:qlen, 0:h_q, 0:512], (qlen, h_q, 
512), offset_factor=1
+                )
+                part_lse = Ts.match_tensor(
+                    temporary_lse[part_idx, 0:qlen, 0:h_q], (qlen, h_q), 
offset_factor=1
+                )
+                body(
+                    q,
+                    q_indptr,
+                    pages,
+                    pages,
+                    page_indptr,
+                    q_rope_position,
+                    k_rope_pos_offset,
+                    part_output,
+                    part_lse,
+                    causal,
+                    rotary_mode,
+                    rope_scale,
+                    rope_theta,
+                    sm_scale,
+                    page_values,
+                    length_info,
+                    part_idx,
+                )
+        for row in T.parallel(qlen, annotations={"thread_binding": 
"blockIdx.x"}):
+            for head in T.parallel(h_q, annotations={"thread_binding": 
"blockIdx.y"}):
+                for lane in T.parallel(128, annotations={"thread_binding": 
"threadIdx.x"}):
+                    with Ts.sblock("merge"):
+                        Ts.reads()
+                        Ts.writes()
+                        weights = Ts.sblock_alloc_tensor((4,), "float32", 
scope="shared")
+                        local_weights = Ts.sblock_alloc_tensor((4,), 
"float32", scope="local")
+                        maximum = Ts.sblock_alloc_tensor((1,), "float32", 
scope="local")
+                        denominator = Ts.sblock_alloc_tensor((1,), "float32", 
scope="local")
+                        value = Ts.sblock_alloc_tensor((1,), "float32", 
scope="local")
+                        if lane == 0:
+                            maximum[0] = T.float32(-50000)
+                            for part in T.unroll(4):
+                                local_weights[part] = temporary_lse[part, row, 
head]
+                                maximum[0] = T.max(maximum[0], 
local_weights[part])
+                            denominator[0] = 0
+                            for part in T.unroll(4):
+                                local_weights[part] = T.if_then_else(
+                                    local_weights[part] > T.float32(-49999),
+                                    T.exp2(local_weights[part] - maximum[0]),
+                                    T.float32(0),
+                                )
+                                denominator[0] += local_weights[part]
+                            for part in T.unroll(4):
+                                weights[part] = local_weights[part] / T.max(
+                                    denominator[0], T.float32(1)
+                                )
+                            lse[row, head] = T.if_then_else(
+                                denominator[0] > 0,
+                                maximum[0] + T.log2(denominator[0]),
+                                T.float32(-50000),
+                            )
+                        T.gpu_storage_sync("shared")
+                        for feature in T.unroll(4):
+                            value[0] = 0
+                            for part in T.unroll(4):
+                                value[0] += (
+                                    weights[part] * temporary[part, row, head, 
feature * 128 + lane]
+                                )
+                            output[row, head, feature * 128 + lane] = value[0]
+
+    # Let the caller assign a symbol when scratch buffers are lifted.
+    return 
batch_prefill_paged_kv.without_attr("global_symbol").with_attr("tirx.is_scheduled",
 True)
diff --git a/tests/python/relax/test_nn_llm_prefill_parallel.py 
b/tests/python/relax/test_nn_llm_prefill_parallel.py
new file mode 100644
index 0000000000..7009302b5c
--- /dev/null
+++ b/tests/python/relax/test_nn_llm_prefill_parallel.py
@@ -0,0 +1,151 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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.
+import numpy as np
+import pytest
+
+import tvm
+import tvm.testing
+from tvm.relax.frontend.nn.llm._prefill_kernels import (
+    _attention_prefill,
+    _attention_prefill_ragged,
+)
+from tvm.relax.frontend.nn.llm._prefill_parallel import (
+    _attention_prefill_paged_parallel,
+    _attention_prefill_ragged_parallel,
+)
+
+
[email protected]("layout", ["ragged", "paged", "sliding"])
+def test_webgpu_parallel_prefill_compiles(layout):
+    target = tvm.target.Target("webgpu")
+    if layout == "ragged":
+        func = _attention_prefill_ragged(1, 8, 512, 512, "float32", {}, target)
+    else:
+        func = _attention_prefill(1, 8, 512, "float32", layout == "sliding", 
{}, target)
+    compiled = tvm.tirx.compile(func.with_attr("global_symbol", "main"), 
target=target)
+    assert compiled.mod.imports[0].inspect_source()
+
+
+def rope(values, positions):
+    frequency = positions[:, None] / 10000 ** (np.arange(256, dtype="float64") 
/ 256)
+    angles = np.concatenate([frequency, frequency], axis=-1)[:, None, :]
+    rotated = np.concatenate([-values[..., 256:], values[..., :256]], axis=-1)
+    return values * np.cos(angles) + rotated * np.sin(angles)
+
+
[email protected]
[email protected]("device_type", ["cuda", "metal"])
[email protected]("layout", ["ragged", "paged", "sliding"])
[email protected]("rotary", [0, 1])
[email protected]("causal", [0, 1])
[email protected]("first_kv_length", [1, 23])
+def test_parallel_prefill(device_type, layout, rotary, causal, 
first_kv_length):
+    if not tvm.testing.device_enabled(device_type):
+        pytest.skip(f"{device_type} not enabled")
+    device = tvm.device(device_type)
+    target = tvm.target.Target.from_device(device)
+    rng = np.random.default_rng(127)
+    lengths = [(7, first_kv_length), (17, 513)]
+    qi = np.array([0, 7, 24], dtype="int32")
+    ki = np.array([0, first_kv_length, first_kv_length + 513], dtype="int32")
+    q = rng.normal(0, 0.3, (24, 8, 512)).astype("float32")
+    k = rng.normal(0, 0.3, (first_kv_length + 513, 2, 512)).astype("float32")
+    v = rng.normal(size=(first_kv_length + 513, 2, 512)).astype("float32")
+    kp = np.array([32, 1234], dtype="int32")
+    qp = np.concatenate(
+        [np.arange(nk - nq, nk) + kp[b] for b, (nq, nk) in enumerate(lengths)]
+    ).astype("int32")
+    reference = np.zeros_like(q)
+    reference_lse = np.full((24, 8), -50000, dtype="float32")
+    scale = 512**-0.5
+    for b, (nq, nk) in enumerate(lengths):
+        query = q[qi[b] : qi[b + 1]].astype("float64")
+        keys = k[ki[b] : ki[b + 1]].astype("float64")
+        if rotary:
+            query = rope(query, qp[qi[b] : qi[b + 1]])
+            keys = rope(keys, np.arange(nk) + kp[b])
+        keys = np.repeat(keys, 4, axis=1)
+        values = np.repeat(v[ki[b] : ki[b + 1]], 4, axis=1)
+        scores = np.einsum("qhd,khd->hqk", query, keys) * scale
+        mask = np.ones((nq, nk), dtype=bool)
+        if causal:
+            if layout == "sliding":
+                mask = np.arange(nk)[None, :] >= np.maximum(
+                    nk - np.maximum(15 - np.arange(nq)[:, None], 0), 0
+                )
+            else:
+                mask = np.arange(nk)[None, :] <= np.arange(nq)[:, None] + nk - 
nq
+        for row in range(nq):
+            if not mask[row].any():
+                continue
+            scores_row = scores[:, row, mask[row]]
+            maximum = scores_row.max(axis=-1, keepdims=True)
+            weights = np.exp(scores_row - maximum)
+            denominator = weights.sum(axis=-1, keepdims=True)
+            reference[qi[b] + row] = np.einsum(
+                "hk,khd->hd", weights / denominator, values[mask[row]]
+            )
+            reference_lse[qi[b] + row] = ((maximum + np.log(denominator)) / 
np.log(2))[:, 0]
+    output = tvm.runtime.empty(q.shape, "float32", device)
+    lse = tvm.runtime.empty(reference_lse.shape, "float32", device)
+
+    def tensor(value):
+        return tvm.runtime.tensor(value, device)
+
+    if layout == "ragged":
+        func = _attention_prefill_ragged_parallel(2, 8, {})
+        args = [tensor(q), tensor(qi), tensor(k), tensor(v), tensor(ki), 
tensor(qp), tensor(kp)]
+    else:
+        sliding = layout == "sliding"
+        offsets = [3, 17] if sliding else [0, 0]
+        sinks = [0, 2] if sliding else [0, 0]
+        physical_lengths = [
+            nk + offset - sink for (_, nk), offset, sink in zip(lengths, 
offsets, sinks)
+        ]
+        counts = [(n + 15) // 16 for n in physical_lengths]
+        pi = np.array([0, counts[0], sum(counts)], dtype="int32")
+        indices = rng.permutation(sum(counts)).astype("int32")
+        pages = np.zeros((sum(counts), 2, 2, 16, 512), dtype="float32")
+        for b, (_, nk) in enumerate(lengths):
+            for row in range(nk):
+                slot = row if row < sinks[b] else row - sinks[b] + offsets[b]
+                page = indices[pi[b] + slot // 16]
+                pages[page, 0, :, slot % 16] = k[ki[b] + row]
+                pages[page, 1, :, slot % 16] = v[ki[b] + row]
+        last = [(n - 1) % 16 + 1 for n in physical_lengths]
+        info = np.array([last, offsets, sinks] if sliding else last, 
dtype="int32")
+        func = _attention_prefill_paged_parallel(2, 8, {}, sliding, 
sliding_window_size=16)
+        args = [
+            tensor(q),
+            tensor(qi),
+            tensor(pages),
+            tensor(pi),
+            tensor(indices),
+            tensor(info),
+            tensor(kp),
+            tensor(qp),
+        ]
+    module = tvm.compile(
+        tvm.IRModule({"main": func.with_attr("global_symbol", "main")}), 
target=target
+    )
+    module(*args, output, lse, causal, rotary, 1.0, 10000.0, scale)
+    tvm.testing.assert_allclose(output.numpy(), reference, rtol=2e-4, 
atol=2e-5)
+    tvm.testing.assert_allclose(lse.numpy(), reference_lse, rtol=2e-5, 
atol=2e-5)
+
+
+if __name__ == "__main__":
+    tvm.testing.main()

Reply via email to