From eed42a9dfa2d81358344689f04489517ee8c0510 Mon Sep 17 00:00:00 2001 From: Gino Lu Date: Thu, 19 Mar 2026 23:28:36 -0400 Subject: [PATCH 01/16] Add host-side Sparge block-map pipeline for sparse attention examples - Add sparge_tool.hpp: host-side Sparge block-map builder (mean-sim scoring, CDF/topk selection) and VSA delta-LUT converter. - Add test_sparge_jenga_sparse_attn.cpp and test_sparge_vsa_sparse_attn.cpp as end-to-end demos. - Update CMakeLists.txt to register both new executables. Note: block size is currently fixed at 128; flexible block size support is not yet addressed. --- example/ck_tile/50_sparse_attn/CMakeLists.txt | 22 + .../ck_tile/50_sparse_attn/sparge_tool.hpp | 408 +++++++++++++++++ .../test_sparge_jenga_sparse_attn.cpp | 422 +++++++++++++++++ .../test_sparge_vsa_sparse_attn.cpp | 429 ++++++++++++++++++ 4 files changed, 1281 insertions(+) create mode 100644 example/ck_tile/50_sparse_attn/sparge_tool.hpp create mode 100644 example/ck_tile/50_sparse_attn/test_sparge_jenga_sparse_attn.cpp create mode 100644 example/ck_tile/50_sparse_attn/test_sparge_vsa_sparse_attn.cpp diff --git a/example/ck_tile/50_sparse_attn/CMakeLists.txt b/example/ck_tile/50_sparse_attn/CMakeLists.txt index 65bb2077642..c916f642ebb 100644 --- a/example/ck_tile/50_sparse_attn/CMakeLists.txt +++ b/example/ck_tile/50_sparse_attn/CMakeLists.txt @@ -88,6 +88,17 @@ target_compile_options(${EXAMPLE_JENGA_SPARSE_ATTN} PRIVATE -Wno-float-equal ) +# Sparge + Jenga Example executable +set(EXAMPLE_SPARGE_JENGA_SPARSE_ATTN "tile_example_sparge_jenga_sparse_attn") +message(DEBUG "adding example ${EXAMPLE_SPARGE_JENGA_SPARSE_ATTN}") +add_executable(${EXAMPLE_SPARGE_JENGA_SPARSE_ATTN} EXCLUDE_FROM_ALL test_sparge_jenga_sparse_attn.cpp) +target_link_libraries(${EXAMPLE_SPARGE_JENGA_SPARSE_ATTN} ${SPARSE_ATTN_JENGA_INSTANCES}) +target_include_directories(${EXAMPLE_SPARGE_JENGA_SPARSE_ATTN} PRIVATE ${CMAKE_CURRENT_LIST_DIR}) +target_compile_options(${EXAMPLE_SPARGE_JENGA_SPARSE_ATTN} PRIVATE + -Wno-undefined-func-template + -Wno-float-equal +) + # ============================================================================ # VSA Sparse Attention # ============================================================================ @@ -153,4 +164,15 @@ target_compile_options(${EXAMPLE_VSA_SPARSE_ATTN} PRIVATE -Wno-float-equal ) +# Sparge + VSA Example executable +set(EXAMPLE_SPARGE_VSA_SPARSE_ATTN "tile_example_sparge_vsa_sparse_attn") +message(DEBUG "adding example ${EXAMPLE_SPARGE_VSA_SPARSE_ATTN}") +add_executable(${EXAMPLE_SPARGE_VSA_SPARSE_ATTN} EXCLUDE_FROM_ALL test_sparge_vsa_sparse_attn.cpp) +target_link_libraries(${EXAMPLE_SPARGE_VSA_SPARSE_ATTN} ${SPARSE_ATTN_VSA_INSTANCES}) +target_include_directories(${EXAMPLE_SPARGE_VSA_SPARSE_ATTN} PRIVATE ${CMAKE_CURRENT_LIST_DIR}) +target_compile_options(${EXAMPLE_SPARGE_VSA_SPARSE_ATTN} PRIVATE + -Wno-undefined-func-template + -Wno-float-equal +) + set_property(GLOBAL PROPERTY RULE_MESSAGES OFF) diff --git a/example/ck_tile/50_sparse_attn/sparge_tool.hpp b/example/ck_tile/50_sparse_attn/sparge_tool.hpp new file mode 100644 index 00000000000..49c69cc6f74 --- /dev/null +++ b/example/ck_tile/50_sparse_attn/sparge_tool.hpp @@ -0,0 +1,408 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ck_tile/core.hpp" +#include "ck_tile/host/host_tensor.hpp" + +namespace sparge { + +struct SpargeParams +{ + int BLKQ = 128; + int BLKK = 128; + + // Similarity gate threshold (TODO: per-head support). + float simthreshd1 = 0.6f; + + // Exactly one of the following should be used: + // - Use CDF threshold if topk < 0 + // - Both should be in [0, 1] <-- NEED TO CHECK THIS + float cdfthreshd = 0.98f; + float topk = -1.0f; + + // If true, treat Q/K as BHSD; otherwise BSHD (same convention as CK examples). + bool i_perm = true; +}; + +// Output format CK VSA expects. +struct VSALut +{ + ck_tile::HostTensor lut; // [B, Hq, Q_blk, K_blk] delta-encoded + ck_tile::HostTensor valid_block_num; // [B, Hq, Q_blk] +}; + +namespace detail { + +template +inline float to_f32(const T& x) +{ + return ck_tile::type_convert(x); +} + +// Read element from HostTensor with either BHSD or BSHD layout. +// Q: [B, Hq, Sq, D] if i_perm else [B, Sq, Hq, D] +// K: [B, Hk, Sk, D] if i_perm else [B, Sk, Hk, D] +template +inline float load(const ck_tile::HostTensor& X, bool i_perm, int b, int h, int s, int d) +{ + return i_perm ? to_f32(X(b, h, s, d)) : to_f32(X(b, s, h, d)); +} + +// Compute pooled mean vector of one block: mean over tokens in [s0, s1). +template +std::vector +pooled_mean_block(const ck_tile::HostTensor& X, bool i_perm, int b, int h, int s0, int s1, int d) +{ + std::vector mean(d, 0.0f); + const int bs = std::max(0, s1 - s0); + if(bs == 0) + return mean; + + for(int s = s0; s < s1; ++s) + { + for(int d_ = 0; d_ < d; ++d_) + { + mean[d_] += load(X, i_perm, b, h, s, d_); + } + } + const float inv = 1.0f / static_cast(bs); + for(int d_ = 0; d_ < d; ++d_) + mean[d_] *= inv; + return mean; +} + +// Compute "sim" flag of one block following SpargeAttn's intent: +// mean_sim = sum(Gram(x_hat)) / (BS_*BS_), where x_hat are token vectors normalized along D. +// +// Important: sum(Gram) = ||sum_i x_hat_i||^2, so we can compute it in O(BS_*D) exactly +// instead of O(BS_^2 * D). +template +bool sim_block_flag(const ck_tile::HostTensor& X, + bool i_perm, + int b, + int h, + int s0, + int s1, + int d, + float simthreshd1) +{ + const int bs = std::max(0, s1 - s0); + if(bs == 0) + return false; + + std::vector sum_hat(d, 0.0f); + + for(int s = s0; s < s1; ++s) + { + // Compute L2 norm over D. + float norm2 = 0.0f; + for(int d_ = 0; d_ < d; ++d_) + { + const float v = load(X, i_perm, b, h, s, d_); + norm2 += v * v; + } + float inv_norm = 1.0f; + // spargeAttn use eps to prevent division by zero + if(norm2 > 0.0f) + inv_norm = 1.0f / std::sqrt(norm2); + + // Accumulate normalized vector. + for(int d_ = 0; d_ < d; ++d_) + { + sum_hat[d_] += load(X, i_perm, b, h, s, d_) * inv_norm; + } + } + + float sum_gram = 0.0f; + for(int d_ = 0; d_ < d; ++d_) + sum_gram += sum_hat[d_] * sum_hat[d_]; + + const float denom = static_cast(bs) * static_cast(bs); + const float mean_sim = sum_gram / denom; + + return mean_sim > simthreshd1; +} + +inline int select_count_from_cdf(const std::vector& sorted_probs, float cdfthreshd) +{ + // Choose the smallest n such that cdf[n-1] >= cdfthreshd. + // Ensure at least 1. + if(sorted_probs.empty()) + return 0; + if(cdfthreshd <= 0.0f) + return 1; + + float c = 0.0f; + for(int i = 0; i < static_cast(sorted_probs.size()); ++i) + { + c += sorted_probs[i]; + if(c >= cdfthreshd) + return i + 1; + } + return static_cast(sorted_probs.size()); +} + +inline int select_count_from_topk(int K_blk, float topk) +{ + if(K_blk <= 0) + return 0; + int n = static_cast(std::floor(topk * static_cast(K_blk))); + n = std::max(1, n); + return n; +} + +} // namespace detail + +// Build one-hot block_map[b,hq,qb,kb] in {0,1}. +// - No causal mask +// - No attention sink +// - Logic matches SpargeAttn's structure: +// - score softmax is only over sim_kblocks; ~sim_kblocks are forced ON later +// - if a Q-block is not "similar", force the whole row ON +template +ck_tile::HostTensor build_block_map_meansim(const ck_tile::HostTensor& Q, + const ck_tile::HostTensor& K, + const SpargeParams& p) +{ + const auto qlens = Q.get_lengths(); + const auto klens = K.get_lengths(); + + const int B = static_cast(qlens[0]); + const int Hq = p.i_perm ? static_cast(qlens[1]) : static_cast(qlens[2]); + const int Sq = p.i_perm ? static_cast(qlens[2]) : static_cast(qlens[1]); + const int D = static_cast(qlens[3]); + + [[maybe_unused]] const int Bk = static_cast(klens[0]); + const int Hk = p.i_perm ? static_cast(klens[1]) : static_cast(klens[2]); + const int Sk = p.i_perm ? static_cast(klens[2]) : static_cast(klens[1]); + [[maybe_unused]] const int Dk = static_cast(klens[3]); + + assert(B == Bk && D == Dk && Hq % Hk == 0); + assert(p.BLKQ > 0 && p.BLKK > 0); + + const int nhead_ratio_qk = Hq / Hk; + const int Q_blk = ck_tile::integer_divide_ceil(Sq, p.BLKQ); + const int K_blk = ck_tile::integer_divide_ceil(Sk, p.BLKK); + + ck_tile::HostTensor block_map({B, Hq, Q_blk, K_blk}); + + // pooled_q: [B,Hq,Q_blk,D], pooled_k: [B,Hk,K_blk,D] + // sim_q: [B,Hq,Q_blk], sim_k: [B,Hk,K_blk] + std::vector pooled_q(static_cast(B) * Hq * Q_blk * D, 0.0f); + std::vector pooled_k(static_cast(B) * Hk * K_blk * D, 0.0f); + std::vector sim_q(static_cast(B) * Hq * Q_blk, 0); + std::vector sim_k(static_cast(B) * Hk * K_blk, 0); + + auto idx_pq = [&](int b, int hq, int qb, int d) { + return (((b * Hq + hq) * Q_blk + qb) * D + d); + }; + auto idx_pk = [&](int b, int hk, int kb, int d) { + return (((b * Hk + hk) * K_blk + kb) * D + d); + }; + auto idx_sq = [&](int b, int hq, int qb) { return ((b * Hq + hq) * Q_blk + qb); }; + auto idx_sk = [&](int b, int hk, int kb) { return ((b * Hk + hk) * K_blk + kb); }; + + for(int b = 0; b < B; ++b) + { + for(int hq = 0; hq < Hq; ++hq) + { + // Q blocks + for(int qb = 0; qb < Q_blk; ++qb) + { + const int s0 = qb * p.BLKQ; + const int s1 = std::min(Sq, (qb + 1) * p.BLKQ); + + // pooled mean + auto mean = detail::pooled_mean_block(Q, p.i_perm, b, hq, s0, s1, D); + for(int d = 0; d < D; ++d) + pooled_q[idx_pq(b, hq, qb, d)] = mean[d]; + + // sim flag + sim_q[idx_sq(b, hq, qb)] = + detail::sim_block_flag(Q, p.i_perm, b, hq, s0, s1, D, p.simthreshd1) ? 1 : 0; + } + } + + for(int hk = 0; hk < Hk; ++hk) + { + // K blocks + for(int kb = 0; kb < K_blk; ++kb) + { + const int s0 = kb * p.BLKK; + const int s1 = std::min(Sk, (kb + 1) * p.BLKK); + + auto mean = detail::pooled_mean_block(K, p.i_perm, b, hk, s0, s1, D); + for(int d = 0; d < D; ++d) + pooled_k[idx_pk(b, hk, kb, d)] = mean[d]; + + sim_k[idx_sk(b, hk, kb)] = + detail::sim_block_flag(K, p.i_perm, b, hk, s0, s1, D, p.simthreshd1) ? 1 : 0; + } + } + } + + const float scale = 1.0f / std::sqrt(static_cast(D)); + + // Main loop + for(int b = 0; b < B; ++b) + { + for(int hq = 0; hq < Hq; ++hq) + { + const int hk = hq / nhead_ratio_qk; + + for(int qb = 0; qb < Q_blk; ++qb) + { + const bool q_is_sim = (sim_q[idx_sq(b, hq, qb)] != 0); + + // If Q-block is not "similar", force dense row. + if(!q_is_sim) + { + for(int kb = 0; kb < K_blk; ++kb) + block_map(b, hq, qb, kb) = 1; + continue; + } + + // Compute scores over K blocks (only sim_kblocks participate in softmax; others set + // to -inf). + std::vector score(K_blk, -std::numeric_limits::infinity()); + for(int kb = 0; kb < K_blk; ++kb) + { + const bool k_is_sim = (sim_k[idx_sk(b, hk, kb)] != 0); + if(!k_is_sim) + { + block_map(b, hq, qb, kb) = 1; + continue; + } + + float dot = 0.0f; + for(int d = 0; d < D; ++d) + { + dot += pooled_q[idx_pq(b, hq, qb, d)] * pooled_k[idx_pk(b, hk, kb, d)]; + } + score[kb] = dot * scale; + } + + // Softmax over K_blk (numerically stable). If all -inf, probs become all zeros. + float maxv = -std::numeric_limits::infinity(); + for(int kb = 0; kb < K_blk; ++kb) + maxv = std::max(maxv, score[kb]); + + std::vector prob(K_blk, 0.0f); + if(std::isfinite(maxv)) + { + float sumexp = 0.0f; + for(int kb = 0; kb < K_blk; ++kb) + { + if(!std::isfinite(score[kb])) + continue; + const float e = std::exp(score[kb] - maxv); + prob[kb] = e; + sumexp += e; + } + if(sumexp > 0.0f) + { + const float inv = 1.0f / sumexp; + for(int kb = 0; kb < K_blk; ++kb) + prob[kb] *= inv; + } + else + { + // All exponentials underflowed: keep zeros. + std::fill(prob.begin(), prob.end(), 0.0f); + } + } + + // Sort indices by prob descending. + std::vector order(K_blk); + std::iota(order.begin(), order.end(), 0); + std::sort(order.begin(), order.end(), [&](int a, int c) { + if(prob[a] != prob[c]) + return prob[a] > prob[c]; + return a < c; // tie-breaker for determinism + }); + + // Determine how many to select. + int num_to_select = 0; + if(p.topk > 0.0f) + { + num_to_select = detail::select_count_from_topk(K_blk, p.topk); + } + else + { + // Use CDF threshold selection (smallest n s.t. cumulative prob >= cdfthreshd). + std::vector sorted_probs(K_blk); + for(int i = 0; i < K_blk; ++i) + sorted_probs[i] = prob[order[i]]; + num_to_select = detail::select_count_from_cdf(sorted_probs, p.cdfthreshd); + num_to_select = std::max(1, num_to_select); + } + + // Select top-kb blocks by order[0..num_to_select-1]. + for(int i = 0; i < num_to_select; ++i) + { + const int kb = order[i]; + block_map(b, hq, qb, kb) = 1; + } + } + } + } + + return block_map; +} + +// Convert one-hot block_map -> delta-encoded LUT + valid_block_num (CK VSA format). +template +VSALut block_map_to_vsa_lut_delta(const ck_tile::HostTensor& block_map) +{ + const auto lens = block_map.get_lengths(); + const int B = static_cast(lens[0]); + const int H = static_cast(lens[1]); + const int Q = static_cast(lens[2]); + const int K = static_cast(lens[3]); + + VSALut out{ + ck_tile::HostTensor({B, H, Q, K}), + ck_tile::HostTensor({B, H, Q}), + }; + + for(int b = 0; b < B; ++b) + { + for(int h = 0; h < H; ++h) + { + for(int q = 0; q < Q; ++q) + { + int32_t valid = 0; + int32_t prev = 0; + + for(int k = 0; k < K; ++k) + { + const bool on = static_cast(block_map(b, h, q, k)) != 0; + if(on) + { + out.lut(b, h, q, valid) = static_cast(k - prev); + prev = static_cast(k); + ++valid; + } + } + + out.valid_block_num(b, h, q) = valid; + + // Optional: zero-fill the unused tail for determinism. + for(int i = valid; i < K; ++i) + out.lut(b, h, q, i) = 0; + } + } + } + + return out; +} + +} // namespace sparge diff --git a/example/ck_tile/50_sparse_attn/test_sparge_jenga_sparse_attn.cpp b/example/ck_tile/50_sparse_attn/test_sparge_jenga_sparse_attn.cpp new file mode 100644 index 00000000000..0bd664adf68 --- /dev/null +++ b/example/ck_tile/50_sparse_attn/test_sparge_jenga_sparse_attn.cpp @@ -0,0 +1,422 @@ +// Copyright (c) Advanced Micro Devices, Inc., or its affiliates. +// SPDX-License-Identifier: MIT +// Demo: Sparge block-map -> Jenga sparse attention + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ck_tile/host.hpp" +#include "ck_tile/core.hpp" +#include "ck_tile/host/reference/reference_blocked_attention.hpp" +#include "ck_tile/core/utility/bit_cast.hpp" + +#include "jenga_sparse_attention.h" +#include "sparge_tool.hpp" + +// ============================================================================ +// Helper Functions +// ============================================================================ + +template +ck_tile::HostTensor make_qkv_tensor(ck_tile::index_t batch, + ck_tile::index_t nhead, + ck_tile::index_t seqlen, + ck_tile::index_t hdim, + bool i_perm) +{ + if(i_perm) + { + return ck_tile::HostTensor({batch, nhead, seqlen, hdim}); + } + return ck_tile::HostTensor({batch, seqlen, nhead, hdim}); +} + +template +ck_tile::HostTensor to_bhsd(const ck_tile::HostTensor& tensor, bool is_bhsd) +{ + auto lens = tensor.get_lengths(); + ck_tile::index_t batch = lens[0]; + ck_tile::index_t seqlen = is_bhsd ? lens[2] : lens[1]; + ck_tile::index_t nhead = is_bhsd ? lens[1] : lens[2]; + ck_tile::index_t hdim = lens[3]; + + ck_tile::HostTensor out({batch, nhead, seqlen, hdim}); + for(ck_tile::index_t b = 0; b < batch; ++b) + { + for(ck_tile::index_t h = 0; h < nhead; ++h) + { + for(ck_tile::index_t s = 0; s < seqlen; ++s) + { + for(ck_tile::index_t d = 0; d < hdim; ++d) + { + out(b, h, s, d) = is_bhsd ? tensor(b, h, s, d) : tensor(b, s, h, d); + } + } + } + } + return out; +} + +template +auto get_error_tolerance() +{ + double rtol = 1e-2; + double atol = 4e-2; + if constexpr(std::is_same_v) + { + atol = 2e-1; + rtol = 2e-1; + } + return ck_tile::make_tuple(rtol, atol); +} + +template +float to_float_for_compare(T value) +{ + return static_cast(value); +} + +template <> +float to_float_for_compare(ck_tile::bf16_t value) +{ +#if CK_TILE_USE_CUSTOM_DATA_TYPE + return static_cast(value); +#else + return ck_tile::bf16_to_float_raw(ck_tile::bit_cast(value)); +#endif +} + +// ============================================================================ +// Command line argument parser +// ============================================================================ + +auto create_args(int argc, char* argv[]) +{ + ck_tile::ArgParser arg_parser; + arg_parser.insert("v", "1", "0:no validation, 1:cpu validation") + .insert("b", "1", "batch size") + .insert("h", "4", "num of head for q") + .insert("h_k", "-1", "num of head for k/v, -1 means equal to h") + .insert("s", "4096", "seqlen_q") + .insert("s_k", "-1", "seqlen_k, -1 means equal to s") + .insert("d", "128", "head dim for q, k") + .insert("d_v", "-1", "head dim for v, -1 means equal to d") + .insert("prec", "fp16", "data type: fp16/bf16") + .insert("iperm", "1", "permute input, 1: b*h*s*d, 0: b*s*h*d") + .insert("operm", "1", "permute output") + .insert("seed", "42", "random seed") + .insert("warmup", "5", "warmup iterations") + .insert("repeat", "20", "benchmark iterations") + .insert("kname", "0", "print kernel name") + // Sparge-specific + .insert("blkq", "128", "Sparge BLKQ") + .insert("blkk", "128", "Sparge BLKK") + .insert("simthreshd1", "0.6", "Sparge sim threshold") + .insert("cdfthreshd", "0.98", "Sparge CDF threshold (used when topk < 0)") + .insert("topk", "-1.0", "Sparge topk ratio in (0,1]; if > 0, overrides cdfthreshd"); + + bool result = arg_parser.parse(argc, argv); + return std::make_tuple(result, arg_parser); +} + +// ============================================================================ +// Main Test Function +// ============================================================================ + +template +bool run_test(const ck_tile::ArgParser& arg_parser) +{ + int do_validation = arg_parser.get_int("v"); + ck_tile::index_t batch = arg_parser.get_int("b"); + ck_tile::index_t nhead = arg_parser.get_int("h"); + ck_tile::index_t nhead_k = arg_parser.get_int("h_k"); + ck_tile::index_t seqlen_q = arg_parser.get_int("s"); + ck_tile::index_t seqlen_k = arg_parser.get_int("s_k"); + ck_tile::index_t hdim_q = arg_parser.get_int("d"); + ck_tile::index_t hdim_v = arg_parser.get_int("d_v"); + bool i_perm = arg_parser.get_bool("iperm"); + bool o_perm = arg_parser.get_bool("operm"); + uint32_t seed = arg_parser.get_uint32("seed"); + int warmup = arg_parser.get_int("warmup"); + int repeat = arg_parser.get_int("repeat"); + int kname = arg_parser.get_int("kname"); + + // Sparge params + ck_tile::index_t blkq = arg_parser.get_int("blkq"); + ck_tile::index_t blkk = arg_parser.get_int("blkk"); + float simthreshd1 = arg_parser.get_float("simthreshd1"); + float cdfthreshd = arg_parser.get_float("cdfthreshd"); + float topk = arg_parser.get_float("topk"); + + if(nhead_k < 0) + nhead_k = nhead; + if(seqlen_k < 0) + seqlen_k = seqlen_q; + if(hdim_v < 0) + hdim_v = hdim_q; + + if(blkq != 128 || blkk != 128 || hdim_q != 128 || hdim_v != 128) + { + std::cout << "\n>>> TEST SKIPPED <<<" << std::endl; + std::cout << "Jenga/VSA kernel instances are generated for BLKQ=BLKK=128, " + "hdim_q=128, hdim_v=128 only." + << std::endl; + std::cout << "TEST SKIPPED" << std::endl; + return true; + } + + ck_tile::index_t BLKQ = blkq; + ck_tile::index_t BLKK = blkk; + + ck_tile::index_t num_q_blocks = (seqlen_q + BLKQ - 1) / BLKQ; + ck_tile::index_t num_k_blocks = (seqlen_k + BLKK - 1) / BLKK; + + std::cout << "============================================================" << std::endl; + std::cout << "[Sparge -> Jenga Sparse Attention Demo]" << std::endl; + std::cout << "============================================================" << std::endl; + std::cout << " Batch: " << batch << ", nhead_q: " << nhead << ", nhead_k: " << nhead_k + << std::endl; + std::cout << " seqlen_q: " << seqlen_q << ", seqlen_k: " << seqlen_k << std::endl; + std::cout << " hdim_q: " << hdim_q << ", hdim_v: " << hdim_v << std::endl; + std::cout << " BLKQ=" << BLKQ << ", BLKK=" << BLKK << std::endl; + std::cout << " num_q_blocks: " << num_q_blocks << ", num_k_blocks: " << num_k_blocks + << std::endl; + std::cout << " Sparge(simthreshd1=" << simthreshd1 << ", cdfthreshd=" << cdfthreshd + << ", topk=" << topk << ")" << std::endl; + std::cout << " i_perm: " << i_perm << ", o_perm: " << o_perm << std::endl; + + // Create host tensors + ck_tile::HostTensor q_host = make_qkv_tensor(batch, nhead, seqlen_q, hdim_q, i_perm); + ck_tile::HostTensor k_host = make_qkv_tensor(batch, nhead_k, seqlen_k, hdim_q, i_perm); + ck_tile::HostTensor v_host = make_qkv_tensor(batch, nhead_k, seqlen_k, hdim_v, i_perm); + ck_tile::HostTensor output_host = + o_perm ? ck_tile::HostTensor({batch, nhead, seqlen_q, hdim_v}) + : ck_tile::HostTensor({batch, seqlen_q, nhead, hdim_v}); + ck_tile::HostTensor output_ref({batch, nhead, seqlen_q, hdim_v}); + + std::cout << "\nInitializing tensors..." << std::endl; + ck_tile::FillUniformDistribution{-0.5f, 0.5f, seed}(q_host); + ck_tile::FillUniformDistribution{-0.5f, 0.5f, seed + 1}(k_host); + ck_tile::FillUniformDistribution{-0.5f, 0.5f, seed + 2}(v_host); + + // Build block map using Sparge tool + std::cout << "Building Sparge block map..." << std::endl; + sparge::SpargeParams p; + p.BLKQ = static_cast(BLKQ); + p.BLKK = static_cast(BLKK); + p.simthreshd1 = simthreshd1; + p.cdfthreshd = cdfthreshd; + p.topk = topk; + p.i_perm = i_perm; + + ck_tile::HostTensor block_relation_onehot = + sparge::build_block_map_meansim(q_host, k_host, p); + + // Print actual sparsity + std::size_t total_blocks = 0; + std::size_t active_blocks = 0; + for(ck_tile::index_t b = 0; b < batch; ++b) + { + for(ck_tile::index_t h = 0; h < nhead; ++h) + { + for(ck_tile::index_t qb = 0; qb < num_q_blocks; ++qb) + { + for(ck_tile::index_t kb = 0; kb < num_k_blocks; ++kb) + { + total_blocks++; + if(block_relation_onehot(b, h, qb, kb) != 0) + active_blocks++; + } + } + } + } + float actual_sparsity = + 1.0f - static_cast(active_blocks) / static_cast(total_blocks); + std::cout << " Actual sparsity: " << actual_sparsity << " (" << active_blocks << "/" + << total_blocks << " blocks active)" << std::endl; + + std::cout << "\n--- Running Jenga sparse attention kernel ---" << std::endl; + + try + { + if(kname) + { + jenga_sparse_attention(q_host, + k_host, + v_host, + block_relation_onehot, + output_host, + batch, + nhead, + nhead_k, + seqlen_q, + seqlen_k, + hdim_q, + hdim_v, + i_perm, + o_perm, + seqlen_q, + seqlen_k, + 1); + } + + for(int i = 0; i < warmup; ++i) + { + jenga_sparse_attention(q_host, + k_host, + v_host, + block_relation_onehot, + output_host, + batch, + nhead, + nhead_k, + seqlen_q, + seqlen_k, + hdim_q, + hdim_v, + i_perm, + o_perm, + seqlen_q, + seqlen_k, + 0); + } + + [[maybe_unused]] auto sync_status1 = hipDeviceSynchronize(); + auto start = std::chrono::high_resolution_clock::now(); + + for(int i = 0; i < repeat; ++i) + { + jenga_sparse_attention(q_host, + k_host, + v_host, + block_relation_onehot, + output_host, + batch, + nhead, + nhead_k, + seqlen_q, + seqlen_k, + hdim_q, + hdim_v, + i_perm, + o_perm, + seqlen_q, + seqlen_k, + 0); + } + + [[maybe_unused]] auto sync_status2 = hipDeviceSynchronize(); + auto end = std::chrono::high_resolution_clock::now(); + double avg_time_ms = + std::chrono::duration(end - start).count() / repeat; + + std::cout << "\n>>>> Jenga sparse attention average time: " << avg_time_ms << " ms <<<<" + << std::endl; + } + catch(const std::exception& e) + { + std::cerr << "Error during kernel execution: " << e.what() << std::endl; + return false; + } + + bool pass = true; + if(do_validation) + { + std::cout << "\n--- Performing CPU validation ---" << std::endl; + float scale = 1.0f / std::sqrt(static_cast(hdim_q)); + + std::cout << "Computing reference output..." << std::endl; + auto q_ref = to_bhsd(q_host, i_perm); + auto k_ref = to_bhsd(k_host, i_perm); + auto v_ref = to_bhsd(v_host, i_perm); + + ck_tile::reference_blocked_attention( + q_ref, k_ref, v_ref, block_relation_onehot, output_ref, BLKQ, BLKK, scale); + + auto [rtol, atol] = get_error_tolerance(); + + float max_diff = 0.0f; + float max_rel_diff = 0.0f; + std::size_t num_errors = 0; + + auto output_host_bhsd = to_bhsd(output_host, o_perm); + for(std::size_t i = 0; i < output_host_bhsd.mData.size(); ++i) + { + float gpu_val = to_float_for_compare(output_host_bhsd.mData[i]); + float ref_val = to_float_for_compare(output_ref.mData[i]); + float diff = std::abs(gpu_val - ref_val); + float rel_diff = (std::abs(ref_val) > 1e-6f) ? diff / std::abs(ref_val) : diff; + + max_diff = std::max(max_diff, diff); + max_rel_diff = std::max(max_rel_diff, rel_diff); + + if(diff > atol && rel_diff > rtol) + { + num_errors++; + if(num_errors <= 5) + { + std::cout << " Mismatch at index " << i << ": GPU=" << gpu_val + << ", Ref=" << ref_val << ", Diff=" << diff << std::endl; + } + } + } + + std::cout << "\nValidation results:" << std::endl; + std::cout << " Max absolute difference: " << max_diff << std::endl; + std::cout << " Max relative difference: " << max_rel_diff << std::endl; + std::cout << " Number of mismatches: " << num_errors << " / " + << output_host_bhsd.mData.size() << std::endl; + + if(num_errors == 0) + { + std::cout << "\n>>> VALIDATION PASSED <<<" << std::endl; + } + else + { + std::cout << "\n>>> VALIDATION FAILED <<<" << std::endl; + pass = false; + } + } + + std::cout << "\n" << (pass ? "TEST PASSED" : "TEST FAILED") << std::endl; + return pass; +} + +// ============================================================================ +// Main +// ============================================================================ + +int main(int argc, char* argv[]) +{ + auto [result, arg_parser] = create_args(argc, argv); + if(!result) + { + std::cerr << "Failed to parse arguments" << std::endl; + return -1; + } + + std::string prec = arg_parser.get_str("prec"); + + bool test_result = false; + if(prec == "fp16") + { + test_result = run_test(arg_parser); + } + else if(prec == "bf16") + { + test_result = run_test(arg_parser); + } + else + { + std::cerr << "Unsupported precision: " << prec << std::endl; + return -1; + } + + return test_result ? 0 : -1; +} diff --git a/example/ck_tile/50_sparse_attn/test_sparge_vsa_sparse_attn.cpp b/example/ck_tile/50_sparse_attn/test_sparge_vsa_sparse_attn.cpp new file mode 100644 index 00000000000..dd1d3e60bee --- /dev/null +++ b/example/ck_tile/50_sparse_attn/test_sparge_vsa_sparse_attn.cpp @@ -0,0 +1,429 @@ +// Copyright (c) Advanced Micro Devices, Inc., or its affiliates. +// SPDX-License-Identifier: MIT +// Demo: Sparge block-map -> (delta LUT) -> VSA sparse attention + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ck_tile/host.hpp" +#include "ck_tile/core.hpp" +#include "ck_tile/host/reference/reference_blocked_attention.hpp" +#include "ck_tile/core/utility/bit_cast.hpp" + +#include "jenga_sparse_attention.h" +#include "sparge_tool.hpp" + +// ============================================================================ +// Helper Functions +// ============================================================================ + +template +ck_tile::HostTensor make_qkv_tensor(ck_tile::index_t batch, + ck_tile::index_t nhead, + ck_tile::index_t seqlen, + ck_tile::index_t hdim, + bool i_perm) +{ + if(i_perm) + { + return ck_tile::HostTensor({batch, nhead, seqlen, hdim}); + } + return ck_tile::HostTensor({batch, seqlen, nhead, hdim}); +} + +template +ck_tile::HostTensor to_bhsd(const ck_tile::HostTensor& tensor, bool is_bhsd) +{ + auto lens = tensor.get_lengths(); + ck_tile::index_t batch = lens[0]; + ck_tile::index_t seqlen = is_bhsd ? lens[2] : lens[1]; + ck_tile::index_t nhead = is_bhsd ? lens[1] : lens[2]; + ck_tile::index_t hdim = lens[3]; + + ck_tile::HostTensor out({batch, nhead, seqlen, hdim}); + for(ck_tile::index_t b = 0; b < batch; ++b) + { + for(ck_tile::index_t h = 0; h < nhead; ++h) + { + for(ck_tile::index_t s = 0; s < seqlen; ++s) + { + for(ck_tile::index_t d = 0; d < hdim; ++d) + { + out(b, h, s, d) = is_bhsd ? tensor(b, h, s, d) : tensor(b, s, h, d); + } + } + } + } + return out; +} + +template +auto get_error_tolerance() +{ + double rtol = 1e-2; + double atol = 4e-2; + if constexpr(std::is_same_v) + { + atol = 2e-1; + rtol = 2e-1; + } + return ck_tile::make_tuple(rtol, atol); +} + +template +float to_float_for_compare(T value) +{ + return static_cast(value); +} + +template <> +float to_float_for_compare(ck_tile::bf16_t value) +{ +#if CK_TILE_USE_CUSTOM_DATA_TYPE + return static_cast(value); +#else + return ck_tile::bf16_to_float_raw(ck_tile::bit_cast(value)); +#endif +} + +// ============================================================================ +// Command line argument parser +// ============================================================================ + +auto create_args(int argc, char* argv[]) +{ + ck_tile::ArgParser arg_parser; + arg_parser.insert("v", "1", "0:no validation, 1:cpu validation") + .insert("b", "1", "batch size") + .insert("h", "4", "num of head for q") + .insert("h_k", "-1", "num of head for k/v, -1 means equal to h") + .insert("s", "4096", "seqlen_q") + .insert("s_k", "-1", "seqlen_k, -1 means equal to s") + .insert("d", "128", "head dim for q, k") + .insert("d_v", "-1", "head dim for v, -1 means equal to d") + .insert("prec", "fp16", "data type: fp16/bf16") + .insert("iperm", "1", "permute input, 1: b*h*s*d, 0: b*s*h*d") + .insert("operm", "1", "permute output") + .insert("seed", "42", "random seed") + .insert("warmup", "5", "warmup iterations") + .insert("repeat", "20", "benchmark iterations") + .insert("kname", "0", "print kernel name") + // Sparge-specific + .insert("blkq", "128", "Sparge BLKQ") + .insert("blkk", "128", "Sparge BLKK") + .insert("simthreshd1", "0.6", "Sparge sim threshold") + .insert("cdfthreshd", "0.98", "Sparge CDF threshold (used when topk < 0)") + .insert("topk", "-1.0", "Sparge topk ratio in (0,1]; if > 0, overrides cdfthreshd"); + + bool result = arg_parser.parse(argc, argv); + return std::make_tuple(result, arg_parser); +} + +// ============================================================================ +// Main Test Function +// ============================================================================ + +template +bool run_test(const ck_tile::ArgParser& arg_parser) +{ + int do_validation = arg_parser.get_int("v"); + ck_tile::index_t batch = arg_parser.get_int("b"); + ck_tile::index_t nhead = arg_parser.get_int("h"); + ck_tile::index_t nhead_k = arg_parser.get_int("h_k"); + ck_tile::index_t seqlen_q = arg_parser.get_int("s"); + ck_tile::index_t seqlen_k = arg_parser.get_int("s_k"); + ck_tile::index_t hdim_q = arg_parser.get_int("d"); + ck_tile::index_t hdim_v = arg_parser.get_int("d_v"); + bool i_perm = arg_parser.get_bool("iperm"); + bool o_perm = arg_parser.get_bool("operm"); + uint32_t seed = arg_parser.get_uint32("seed"); + int warmup = arg_parser.get_int("warmup"); + int repeat = arg_parser.get_int("repeat"); + int kname = arg_parser.get_int("kname"); + + // Sparge params + ck_tile::index_t blkq = arg_parser.get_int("blkq"); + ck_tile::index_t blkk = arg_parser.get_int("blkk"); + float simthreshd1 = arg_parser.get_float("simthreshd1"); + float cdfthreshd = arg_parser.get_float("cdfthreshd"); + float topk = arg_parser.get_float("topk"); + + if(nhead_k < 0) + nhead_k = nhead; + if(seqlen_k < 0) + seqlen_k = seqlen_q; + if(hdim_v < 0) + hdim_v = hdim_q; + + if(blkq != 128 || blkk != 128 || hdim_q != 128 || hdim_v != 128) + { + std::cout << "\n>>> TEST SKIPPED <<<" << std::endl; + std::cout << "VSA kernel instances are generated for BLKQ=BLKK=128, " + "hdim_q=128, hdim_v=128 only." + << std::endl; + std::cout << "TEST SKIPPED" << std::endl; + return true; + } + + ck_tile::index_t BLKQ = blkq; + ck_tile::index_t BLKK = blkk; + + ck_tile::index_t num_q_blocks = (seqlen_q + BLKQ - 1) / BLKQ; + ck_tile::index_t num_k_blocks = (seqlen_k + BLKK - 1) / BLKK; + + std::cout << "============================================================" << std::endl; + std::cout << "[Sparge -> VSA Sparse Attention Demo]" << std::endl; + std::cout << "============================================================" << std::endl; + std::cout << " Batch: " << batch << ", nhead_q: " << nhead << ", nhead_k: " << nhead_k + << std::endl; + std::cout << " seqlen_q: " << seqlen_q << ", seqlen_k: " << seqlen_k << std::endl; + std::cout << " hdim_q: " << hdim_q << ", hdim_v: " << hdim_v << std::endl; + std::cout << " BLKQ=" << BLKQ << ", BLKK=" << BLKK << std::endl; + std::cout << " num_q_blocks: " << num_q_blocks << ", num_k_blocks: " << num_k_blocks + << std::endl; + std::cout << " Sparge(simthreshd1=" << simthreshd1 << ", cdfthreshd=" << cdfthreshd + << ", topk=" << topk << ")" << std::endl; + std::cout << " i_perm: " << i_perm << ", o_perm: " << o_perm << std::endl; + + // Create host tensors + ck_tile::HostTensor q_host = make_qkv_tensor(batch, nhead, seqlen_q, hdim_q, i_perm); + ck_tile::HostTensor k_host = make_qkv_tensor(batch, nhead_k, seqlen_k, hdim_q, i_perm); + ck_tile::HostTensor v_host = make_qkv_tensor(batch, nhead_k, seqlen_k, hdim_v, i_perm); + ck_tile::HostTensor output_host = + o_perm ? ck_tile::HostTensor({batch, nhead, seqlen_q, hdim_v}) + : ck_tile::HostTensor({batch, seqlen_q, nhead, hdim_v}); + ck_tile::HostTensor output_ref({batch, nhead, seqlen_q, hdim_v}); + + std::cout << "\nInitializing tensors..." << std::endl; + ck_tile::FillUniformDistribution{-0.5f, 0.5f, seed}(q_host); + ck_tile::FillUniformDistribution{-0.5f, 0.5f, seed + 1}(k_host); + ck_tile::FillUniformDistribution{-0.5f, 0.5f, seed + 2}(v_host); + + // Build block map using Sparge tool + std::cout << "Building Sparge block map..." << std::endl; + sparge::SpargeParams p; + p.BLKQ = static_cast(BLKQ); + p.BLKK = static_cast(BLKK); + p.simthreshd1 = simthreshd1; + p.cdfthreshd = cdfthreshd; + p.topk = topk; + p.i_perm = i_perm; + + ck_tile::HostTensor block_relation_onehot = + sparge::build_block_map_meansim(q_host, k_host, p); + + // Convert to VSA LUT (delta-encoded) + valid_block_num + std::cout << "Converting block map to VSA LUT (delta)..." << std::endl; + auto vsa_lut = sparge::block_map_to_vsa_lut_delta(block_relation_onehot); + + // Print actual sparsity (based on one-hot) + std::size_t total_blocks = 0; + std::size_t active_blocks = 0; + for(ck_tile::index_t b = 0; b < batch; ++b) + { + for(ck_tile::index_t h = 0; h < nhead; ++h) + { + for(ck_tile::index_t qb = 0; qb < num_q_blocks; ++qb) + { + for(ck_tile::index_t kb = 0; kb < num_k_blocks; ++kb) + { + total_blocks++; + if(block_relation_onehot(b, h, qb, kb) != 0) + active_blocks++; + } + } + } + } + float actual_sparsity = + 1.0f - static_cast(active_blocks) / static_cast(total_blocks); + std::cout << " Actual sparsity: " << actual_sparsity << " (" << active_blocks << "/" + << total_blocks << " blocks active)" << std::endl; + + std::cout << "\n--- Running VSA sparse attention kernel ---" << std::endl; + + try + { + if(kname) + { + vsa_sparse_attention(q_host, + k_host, + v_host, + vsa_lut.lut, + vsa_lut.valid_block_num, + output_host, + batch, + nhead, + nhead_k, + seqlen_q, + seqlen_k, + hdim_q, + hdim_v, + i_perm, + o_perm, + seqlen_q, + seqlen_k, + 1); + } + + for(int i = 0; i < warmup; ++i) + { + vsa_sparse_attention(q_host, + k_host, + v_host, + vsa_lut.lut, + vsa_lut.valid_block_num, + output_host, + batch, + nhead, + nhead_k, + seqlen_q, + seqlen_k, + hdim_q, + hdim_v, + i_perm, + o_perm, + seqlen_q, + seqlen_k, + 0); + } + + [[maybe_unused]] auto sync_status1 = hipDeviceSynchronize(); + auto start = std::chrono::high_resolution_clock::now(); + + for(int i = 0; i < repeat; ++i) + { + vsa_sparse_attention(q_host, + k_host, + v_host, + vsa_lut.lut, + vsa_lut.valid_block_num, + output_host, + batch, + nhead, + nhead_k, + seqlen_q, + seqlen_k, + hdim_q, + hdim_v, + i_perm, + o_perm, + seqlen_q, + seqlen_k, + 0); + } + + [[maybe_unused]] auto sync_status2 = hipDeviceSynchronize(); + auto end = std::chrono::high_resolution_clock::now(); + double avg_time_ms = + std::chrono::duration(end - start).count() / repeat; + + std::cout << "\n>>>> VSA sparse attention average time: " << avg_time_ms << " ms <<<<" + << std::endl; + } + catch(const std::exception& e) + { + std::cerr << "Error during kernel execution: " << e.what() << std::endl; + return false; + } + + bool pass = true; + if(do_validation) + { + std::cout << "\n--- Performing CPU validation ---" << std::endl; + float scale = 1.0f / std::sqrt(static_cast(hdim_q)); + + std::cout << "Computing reference output..." << std::endl; + auto q_ref = to_bhsd(q_host, i_perm); + auto k_ref = to_bhsd(k_host, i_perm); + auto v_ref = to_bhsd(v_host, i_perm); + + ck_tile::reference_blocked_attention( + q_ref, k_ref, v_ref, block_relation_onehot, output_ref, BLKQ, BLKK, scale); + + auto [rtol, atol] = get_error_tolerance(); + + float max_diff = 0.0f; + float max_rel_diff = 0.0f; + std::size_t num_errors = 0; + + auto output_host_bhsd = to_bhsd(output_host, o_perm); + for(std::size_t i = 0; i < output_host_bhsd.mData.size(); ++i) + { + float gpu_val = to_float_for_compare(output_host_bhsd.mData[i]); + float ref_val = to_float_for_compare(output_ref.mData[i]); + float diff = std::abs(gpu_val - ref_val); + float rel_diff = (std::abs(ref_val) > 1e-6f) ? diff / std::abs(ref_val) : diff; + + max_diff = std::max(max_diff, diff); + max_rel_diff = std::max(max_rel_diff, rel_diff); + + if(diff > atol && rel_diff > rtol) + { + num_errors++; + if(num_errors <= 5) + { + std::cout << " Mismatch at index " << i << ": GPU=" << gpu_val + << ", Ref=" << ref_val << ", Diff=" << diff << std::endl; + } + } + } + + std::cout << "\nValidation results:" << std::endl; + std::cout << " Max absolute difference: " << max_diff << std::endl; + std::cout << " Max relative difference: " << max_rel_diff << std::endl; + std::cout << " Number of mismatches: " << num_errors << " / " + << output_host_bhsd.mData.size() << std::endl; + + if(num_errors == 0) + { + std::cout << "\n>>> VALIDATION PASSED <<<" << std::endl; + } + else + { + std::cout << "\n>>> VALIDATION FAILED <<<" << std::endl; + pass = false; + } + } + + std::cout << "\n" << (pass ? "TEST PASSED" : "TEST FAILED") << std::endl; + return pass; +} + +// ============================================================================ +// Main +// ============================================================================ + +int main(int argc, char* argv[]) +{ + auto [result, arg_parser] = create_args(argc, argv); + if(!result) + { + std::cerr << "Failed to parse arguments" << std::endl; + return -1; + } + + std::string prec = arg_parser.get_str("prec"); + + bool test_result = false; + if(prec == "fp16") + { + test_result = run_test(arg_parser); + } + else if(prec == "bf16") + { + test_result = run_test(arg_parser); + } + else + { + std::cerr << "Unsupported precision: " << prec << std::endl; + return -1; + } + + return test_result ? 0 : -1; +} From 9317fc4a8508cb53ec9bd829781d4781d84ce428 Mon Sep 17 00:00:00 2001 From: Gino Lu Date: Tue, 24 Mar 2026 05:57:54 -0400 Subject: [PATCH 02/16] Support 64x128 tile size in sparge fwd for Jenga and VSA paths --- example/ck_tile/50_sparse_attn/CMakeLists.txt | 116 ++- .../codegen/ops/sparge_fwd_jenga.py | 799 ++++++++++++++++++ .../codegen/ops/sparge_fwd_vsa.py | 799 ++++++++++++++++++ .../ck_tile/50_sparse_attn/fmha_fwd_trek.hpp | 6 + .../50_sparse_attn/jenga_sparge_attention.cpp | 189 +++++ .../50_sparse_attn/jenga_sparge_attention.h | 27 + .../test_sparge_jenga_sparse_attn.cpp | 14 +- .../test_sparge_vsa_sparse_attn.cpp | 14 +- .../50_sparse_attn/vsa_sparge_attention.cpp | 195 +++++ .../50_sparse_attn/vsa_sparge_attention.h | 28 + ...block_fmha_pipeline_qr_ks_vs_async_vsa.hpp | 2 +- 11 files changed, 2167 insertions(+), 22 deletions(-) create mode 100644 example/ck_tile/50_sparse_attn/codegen/ops/sparge_fwd_jenga.py create mode 100644 example/ck_tile/50_sparse_attn/codegen/ops/sparge_fwd_vsa.py create mode 100644 example/ck_tile/50_sparse_attn/jenga_sparge_attention.cpp create mode 100644 example/ck_tile/50_sparse_attn/jenga_sparge_attention.h create mode 100644 example/ck_tile/50_sparse_attn/vsa_sparge_attention.cpp create mode 100644 example/ck_tile/50_sparse_attn/vsa_sparge_attention.h diff --git a/example/ck_tile/50_sparse_attn/CMakeLists.txt b/example/ck_tile/50_sparse_attn/CMakeLists.txt index c916f642ebb..0ac86f6affa 100644 --- a/example/ck_tile/50_sparse_attn/CMakeLists.txt +++ b/example/ck_tile/50_sparse_attn/CMakeLists.txt @@ -1,8 +1,8 @@ -# Copyright (c) Advanced Micro Devices, Inc., or its affiliates. -# SPDX-License-Identifier: MIT -# CMakeLists.txt for sparse attention (Jenga and VSA) +#Copyright(c) Advanced Micro Devices, Inc., or its affiliates. +#SPDX - License - Identifier : MIT +#CMakeLists.txt for sparse attention(Jenga and VSA) -# Use SUPPORTED_GPU_TARGETS directly +#Use SUPPORTED_GPU_TARGETS directly set(INST_TARGETS ${SUPPORTED_GPU_TARGETS}) set(GPU_TARGETS ${SUPPORTED_GPU_TARGETS}) @@ -16,7 +16,7 @@ endif() message(STATUS "Building Sparse Attention (Jenga & VSA) for targets: ${INST_TARGETS}") -# Code generation scripts +#Code generation scripts file(GLOB_RECURSE CODE_GEN_SCRIPTS CONFIGURE_DEPENDS ${CMAKE_CURRENT_LIST_DIR}/generate.py ${CMAKE_CURRENT_LIST_DIR}/codegen/*.py @@ -88,11 +88,62 @@ target_compile_options(${EXAMPLE_JENGA_SPARSE_ATTN} PRIVATE -Wno-float-equal ) +# ============================================================================ +# Sparge Jenga (64x128 tile) +# ============================================================================ +set(SPARGE_JENGA_CODE_GEN_ARGS + ${CMAKE_CURRENT_LIST_DIR}/generate.py + --api sparge_fwd_jenga + --receipt 600 +) + +execute_process( + COMMAND ${Python3_EXECUTABLE} ${SPARGE_JENGA_CODE_GEN_ARGS} + --list_blobs ${CMAKE_CURRENT_BINARY_DIR}/sparge_jenga_blob_list.txt + RESULT_VARIABLE ret +) +if(ret AND NOT ret EQUAL 0) + message(FATAL_ERROR "Failed to generate Sparge Jenga kernel list") +endif() + +file(STRINGS ${CMAKE_CURRENT_BINARY_DIR}/sparge_jenga_blob_list.txt SPARGE_JENGA_GEN_BLOBS) + +add_custom_command( + OUTPUT ${SPARGE_JENGA_GEN_BLOBS} + COMMAND ${Python3_EXECUTABLE} ${SPARGE_JENGA_CODE_GEN_ARGS} + --output_dir ${CMAKE_CURRENT_BINARY_DIR} + DEPENDS ${CODE_GEN_SCRIPTS} + COMMENT "Generate CK Tile Sparge Jenga kernels" +) + +message(STATUS "Sparge Jenga kernel files to be generated: ${SPARGE_JENGA_GEN_BLOBS}") + +set(SPARGE_JENGA_INSTANCES "tile_sparge_jenga_instances") + +add_library(${SPARGE_JENGA_INSTANCES} OBJECT EXCLUDE_FROM_ALL + ${SPARGE_JENGA_GEN_BLOBS} + ${CMAKE_CURRENT_LIST_DIR}/jenga_sparge_attention.cpp +) +target_include_directories(${SPARGE_JENGA_INSTANCES} PRIVATE + ${CMAKE_CURRENT_LIST_DIR} + ${PROJECT_SOURCE_DIR}/include/ck_tile/ops/sparse_attn +) +set_source_files_properties(${SPARGE_JENGA_GEN_BLOBS} PROPERTIES LANGUAGE HIP) +set_source_files_properties(${CMAKE_CURRENT_LIST_DIR}/jenga_sparge_attention.cpp PROPERTIES LANGUAGE HIP) +set_property(TARGET ${SPARGE_JENGA_INSTANCES} PROPERTY HIP_ARCHITECTURES ${INST_TARGETS}) + +target_compile_options(${SPARGE_JENGA_INSTANCES} PRIVATE + -DCK_TILE_USE_BUFFER_ADDRESSING_BUILTIN + -DCK_TILE_FMHA_FWD_FAST_EXP2 + -Wno-undefined-func-template + -Wno-float-equal +) + # Sparge + Jenga Example executable set(EXAMPLE_SPARGE_JENGA_SPARSE_ATTN "tile_example_sparge_jenga_sparse_attn") message(DEBUG "adding example ${EXAMPLE_SPARGE_JENGA_SPARSE_ATTN}") add_executable(${EXAMPLE_SPARGE_JENGA_SPARSE_ATTN} EXCLUDE_FROM_ALL test_sparge_jenga_sparse_attn.cpp) -target_link_libraries(${EXAMPLE_SPARGE_JENGA_SPARSE_ATTN} ${SPARSE_ATTN_JENGA_INSTANCES}) +target_link_libraries(${EXAMPLE_SPARGE_JENGA_SPARSE_ATTN} ${SPARGE_JENGA_INSTANCES}) target_include_directories(${EXAMPLE_SPARGE_JENGA_SPARSE_ATTN} PRIVATE ${CMAKE_CURRENT_LIST_DIR}) target_compile_options(${EXAMPLE_SPARGE_JENGA_SPARSE_ATTN} PRIVATE -Wno-undefined-func-template @@ -164,11 +215,62 @@ target_compile_options(${EXAMPLE_VSA_SPARSE_ATTN} PRIVATE -Wno-float-equal ) +# ============================================================================ +# Sparge VSA (64x128 tile) +# ============================================================================ +set(SPARGE_VSA_CODE_GEN_ARGS + ${CMAKE_CURRENT_LIST_DIR}/generate.py + --api sparge_fwd_vsa + --receipt 600 +) + +execute_process( + COMMAND ${Python3_EXECUTABLE} ${SPARGE_VSA_CODE_GEN_ARGS} + --list_blobs ${CMAKE_CURRENT_BINARY_DIR}/sparge_vsa_blob_list.txt + RESULT_VARIABLE ret +) +if(ret AND NOT ret EQUAL 0) + message(FATAL_ERROR "Failed to generate Sparge VSA kernel list") +endif() + +file(STRINGS ${CMAKE_CURRENT_BINARY_DIR}/sparge_vsa_blob_list.txt SPARGE_VSA_GEN_BLOBS) + +add_custom_command( + OUTPUT ${SPARGE_VSA_GEN_BLOBS} + COMMAND ${Python3_EXECUTABLE} ${SPARGE_VSA_CODE_GEN_ARGS} + --output_dir ${CMAKE_CURRENT_BINARY_DIR} + DEPENDS ${CODE_GEN_SCRIPTS} + COMMENT "Generate CK Tile Sparge VSA kernels" +) + +message(STATUS "Sparge VSA kernel files to be generated: ${SPARGE_VSA_GEN_BLOBS}") + +set(SPARGE_VSA_INSTANCES "tile_sparge_vsa_instances") + +add_library(${SPARGE_VSA_INSTANCES} OBJECT EXCLUDE_FROM_ALL + ${SPARGE_VSA_GEN_BLOBS} + ${CMAKE_CURRENT_LIST_DIR}/vsa_sparge_attention.cpp +) +target_include_directories(${SPARGE_VSA_INSTANCES} PRIVATE + ${CMAKE_CURRENT_LIST_DIR} + ${PROJECT_SOURCE_DIR}/include/ck_tile/ops/sparse_attn +) +set_source_files_properties(${SPARGE_VSA_GEN_BLOBS} PROPERTIES LANGUAGE HIP) +set_source_files_properties(${CMAKE_CURRENT_LIST_DIR}/vsa_sparge_attention.cpp PROPERTIES LANGUAGE HIP) +set_property(TARGET ${SPARGE_VSA_INSTANCES} PROPERTY HIP_ARCHITECTURES ${INST_TARGETS}) + +target_compile_options(${SPARGE_VSA_INSTANCES} PRIVATE + -DCK_TILE_USE_BUFFER_ADDRESSING_BUILTIN + -DCK_TILE_FMHA_FWD_FAST_EXP2 + -Wno-undefined-func-template + -Wno-float-equal +) + # Sparge + VSA Example executable set(EXAMPLE_SPARGE_VSA_SPARSE_ATTN "tile_example_sparge_vsa_sparse_attn") message(DEBUG "adding example ${EXAMPLE_SPARGE_VSA_SPARSE_ATTN}") add_executable(${EXAMPLE_SPARGE_VSA_SPARSE_ATTN} EXCLUDE_FROM_ALL test_sparge_vsa_sparse_attn.cpp) -target_link_libraries(${EXAMPLE_SPARGE_VSA_SPARSE_ATTN} ${SPARSE_ATTN_VSA_INSTANCES}) +target_link_libraries(${EXAMPLE_SPARGE_VSA_SPARSE_ATTN} ${SPARGE_VSA_INSTANCES}) target_include_directories(${EXAMPLE_SPARGE_VSA_SPARSE_ATTN} PRIVATE ${CMAKE_CURRENT_LIST_DIR}) target_compile_options(${EXAMPLE_SPARGE_VSA_SPARSE_ATTN} PRIVATE -Wno-undefined-func-template diff --git a/example/ck_tile/50_sparse_attn/codegen/ops/sparge_fwd_jenga.py b/example/ck_tile/50_sparse_attn/codegen/ops/sparge_fwd_jenga.py new file mode 100644 index 00000000000..872da2326ea --- /dev/null +++ b/example/ck_tile/50_sparse_attn/codegen/ops/sparge_fwd_jenga.py @@ -0,0 +1,799 @@ +# Copyright (c) Advanced Micro Devices, Inc., or its affiliates. +# SPDX-License-Identifier: MIT +# generate kernel instances to speed up compilation + +import copy +from dataclasses import dataclass, field +import fnmatch +import itertools +import os +import os.path as path +from pathlib import Path +from typing import List, Optional, Tuple + +from codegen.cpp_symbol_map import ( + BOOL_MAP, + FWD_DTYPE_MAP, + LAYOUT_MAP, + MODE_MAP, + PIPELINE_ENUM_MAP, + PIPELINE_MAP, + get_mask_check_map, + get_mask_map, +) + +GEN_DIR = "" + + +def update_file(file_path, content): + """Update the file at file_path with the given content if it differs from the existing content. + + It avoids unnecessary touching of the file which triggers rebuilds + """ + + existing_content = "" + if path.exists(file_path): + with open(file_path, "r") as file: + existing_content = file.read() + if existing_content == content: + return + with open(file_path, "w") as file: + file.write(content) + + +DTYPE_BITS = {"fp32": 32, "fp16": 16, "bf16": 16} + +K0_MAX_SUBMAX_MAP = {32: 32, 64: 64, 96: 128, 128: 128, 192: 192, 256: 256} + +FMHA_FWD_KERNEL_HEADER = """// SPDX-License-Identifier: MIT +// Copyright (c) Advanced Micro Devices, Inc., or its affiliates.\n +// auto generated by generate.py +#include "ck_tile/ops/fmha/block/variants.hpp" +#include "fmha_fwd_trek.hpp" +#include "pipeline/block_fmha_pipeline_qr_ks_vs_async_jenga.hpp" +#include "kernel/fmha_fwd_jenga_kernel.hpp" + +""" + +# NOTE: Jenga sparse attention kernel has the following restrictions enforced by static_assert: +# - Group mode: NOT supported (batch mode only) +# - Bias: NOT supported (NO_BIAS only) +# - LSE output: NOT supported (false only) +# - Dropout: NOT supported (false only) +# - Logits soft-cap: NOT supported (false only) +# - FP8 static quantization: NOT supported (NO_SCALE only) +# The template below hardcodes these unsupported features accordingly. + +FMHA_FWD_KERNEL_BODY = """ +using fmha_dtype_{F_idx} = {F_dtype}; + +using fmha_block_tile_{F_idx} = ck_tile::sequence<{F_bm0}, {F_bn0}, {F_bk0}, {F_bn1}, {F_bk1}, {F_bk0max}>; + +using fmha_shape_{F_idx} = ck_tile::TileFmhaShape, + ck_tile::sequence<{F_wm0}, {F_wn0}, {F_wk0}>, + ck_tile::sequence<{F_rm1}, {F_rn1}, {F_rk1}>, + ck_tile::sequence<{F_wm1}, {F_wn1}, {F_wk1}>, + {F_vlayout}>; + +// TileFmhaTraits: spad, skpad, dpad, dvpad, has_logits_soft_cap, bias_enum, +// store_lse, has_dropout, has_randval, quant_scale_enum, occupancy, is_v_rowmajor_skip +using fmha_trait_{F_idx} = ck_tile::TileFmhaTraits<{F_spad}, + {F_skpad}, + {F_dpad}, + {F_dvpad}, + false, // has_logits_soft_cap - NOT supported + ck_tile::BlockAttentionBiasEnum::NO_BIAS, // bias - NOT supported + false, // store_lse - NOT supported + false, // has_dropout - NOT supported + false, // has_randval - NOT supported + ck_tile::BlockAttentionQuantScaleEnum::NO_SCALE, // FP8 quant - NOT supported + {F_occupancy}, + false>; + +using fmha_variant_{F_idx} = ck_tile::ComposedAttention<0, CK_TILE_FMHA_FWD_FAST_EXP2>; // logits_soft_cap=0 (NOT supported) + +using fmha_mask_{F_idx} = {F_mask}; + +using fmha_pipeline_problem_{F_idx} = ck_tile::BlockFmhaPipelineProblem< + typename FmhaSparseFwdTypeConfig::QDataType, + typename FmhaSparseFwdTypeConfig::KDataType, + typename FmhaSparseFwdTypeConfig::VDataType, + typename FmhaSparseFwdTypeConfig::SaccDataType, + typename FmhaSparseFwdTypeConfig::SMPLComputeDataType, + typename FmhaSparseFwdTypeConfig::BiasDataType, + typename FmhaSparseFwdTypeConfig::RandValOutputDataType, + typename FmhaSparseFwdTypeConfig::LSEDataType, + typename FmhaSparseFwdTypeConfig::PDataType, + typename FmhaSparseFwdTypeConfig::OaccDataType, + typename FmhaSparseFwdTypeConfig::ODataType, + fmha_shape_{F_idx}, + {F_mode}, + fmha_variant_{F_idx}, + fmha_mask_{F_idx}, + {F_trload}, + fmha_trait_{F_idx}>; + +using fmha_pipeline_{F_idx} = {F_pipeline}< + fmha_pipeline_problem_{F_idx}>; + +using fmha_epilogue_{F_idx} = + ck_tile::Default2DEpilogue::OaccDataType, + typename FmhaSparseFwdTypeConfig<{F_dtype}>::ODataType, + {F_spad}, {F_dvpad}>>; + +using fmha_kernel_{F_idx} = + ck_tile::FmhaFwdJengaKernel; + +using trait_{F_idx} = fmha_jenga_fwd_traits_<{F_hdim}, {F_dtype}, {F_bm0}, {F_bn0}, {F_bk0}, {F_bn1}, {F_bk1}, {F_bk0max}, {F_vlayout}, + {F_pipeline_enum}, false/*logits*/, fmha_mask_{F_idx}, {F_spad}, {F_skpad}, {F_dpad}, {F_dvpad}, {F_trload}>; + +#include + +template<> +float fmha_jenga_fwd_(const ck_tile::stream_config& s, fmha_jenga_fwd_args a) +{{ + using k_ = fmha_kernel_{F_idx}; + if(s.log_level_ > 0) + std::cout << ", " << "{F_kernel_name}" << std::flush; + auto [kargs, grids] = fmha_fwd_create_kargs_and_grids(a); + const dim3 blocks = k_::BlockSize(); + constexpr ck_tile::index_t kBlockPerCu = k_::kBlockPerCu; + return ck_tile::launch_kernel(s, ck_tile::make_kernel(k_{{}}, grids, blocks, 0, kargs)); +}} +""" + +FMHA_FWD_API_FILENAME = "sparge_jenga_fwd_api.cpp" +FMHA_FWD_API = """ +#include + +#include + +namespace {{ +bool get_num_cus(unsigned& num_cus) {{ + int device; + auto status = hipGetDevice(&device); + if(status != hipSuccess) {{ + fprintf(stderr, "failed to get device"); + return false; + }} + + hipDeviceProp_t props{{}}; + status = hipGetDeviceProperties(&props, device); + if(status != hipSuccess) {{ + fprintf(stderr, "failed to get device properties"); + return false; + }} + + num_cus = props.multiProcessorCount; + return true; +}} + +unsigned get_num_thread_blocks(unsigned batch, unsigned nheads, unsigned max_seqlen_q, unsigned kM0) {{ + const unsigned num_m_blocks = (max_seqlen_q + kM0 - 1) / kM0; + const unsigned num_n_blocks = 1; // we assume that num_n_blocks is always 1 + + return batch * nheads * num_m_blocks * num_n_blocks; +}} +}} // namespace + +float sparge_jenga_fwd(fmha_jenga_fwd_traits t, fmha_jenga_fwd_args a, const ck_tile::stream_config& s){{ + float r = -1; + + [[maybe_unused]] const float min_cu_util_rate = 0.8; // minimum CU utilization rate + + unsigned num_cus; + if (!get_num_cus(num_cus)) {{ + return r; + }} + + [[maybe_unused]] auto get_num_blocks = [&](unsigned kM0) {{ + return get_num_thread_blocks(a.batch, a.nhead_q, a.max_seqlen_q, kM0); + }}; + + const bool has_load_tr = ck_tile::is_load_tr_supported(); + +{F_dispatch} + return r; +}} +""" + +FMHA_FWD_API_PER_TRLOAD = """ {F_if}({F_trload_cond}){{ +{F_dtype_case} + }} +""" + +FMHA_FWD_API_PER_DTYPE = """ {F_if}(t.data_type.compare(\"{F_dtype}\") == 0){{ +{F_hdim_case} + }} +""" +FMHA_FWD_API_PER_HDIM_CASE = """ {F_if} (t.hdim_q <= {F_hdim} && t.hdim_v <= {F_hdim_v}) {{ +{F_inner_dispatch} + }} +""" + +FMHA_FWD_API_INNER_DISPATCH = """ {F_if}((t.is_v_rowmajor == {F_vlayout}) && ({F_mask_check}) && + ({F_scheck}) && ({F_seqtune}) && ({F_skcheck}) && ({F_dcheck}) && ({F_dvcheck}) && ({F_constraint})) {{ + using trait_ = fmha_jenga_fwd_traits_<{F_hdim}, {F_dtype}, {F_bm0}, {F_bn0}, {F_bk0}, {F_bn1}, {F_bk1}, {F_bk0max}, {F_vlayout}, {F_pipeline_enum}, false/*logits*/, {F_mask}, {F_spad}, {F_skpad}, {F_dpad}, {F_dvpad}, {F_trload}>; + return fmha_jenga_fwd_(s, a); + }} +""" + + +@dataclass +class CppConstraint: + bool_expr: str = None + + def __str__(self): + if self.bool_expr is None: + return "true" + else: + return f"{self.bool_expr}" + + def __and__(self, other): + return CppConstraint(f"({str(self)}) && ({str(other)})") + + +@dataclass +class FmhaFwdApiTrait: + pipeline_tag: str + # sync with fmha_fwd_traits<>, to generate fallback calls + hdim: str + dtype: str # data type + mode: str # value from MODE_MAP + bm0: int # tile size along q seqlen (block size) + bn0: int # tile size along qk seqlen + bk0: int # tile size along qk gemm unroll + bn1: int # tile size along v head_dim + bk1: int # tile size along kv gemm unroll + bk0max: int + vlayout: str + logits: str + mask: str + spad: str + skpad: str + dpad: str + dvpad: str + tr_load: str + constraint: CppConstraint + + @property + def name(self) -> str: + return ( + f"{self.hdim}-{self.dtype}-{self.mode}-{self.bm0}-{self.bn0}-{self.bk0}-{self.bn0}-{self.bk1}-{self.bk0max}-" + + f"{self.vlayout}-{self.logits}-{self.mask}-{self.spad}-{self.skpad}-{self.dpad}-{self.dvpad}" + ) + + @property + def scheck(self) -> str: + if self.mode == "group": + return "true/*group mode spad always true*/" # group mode only generate spad/skpad == true + if self.spad == "t": + return "true" # always support + return "true" + + @property + def seqtune(self) -> str: + return "true" + + @property + def skcheck(self) -> str: + if self.mode == "group": + return "true/*group mode skpad always true*/" # group mode only generate spad/skpad == true + if self.skpad == "t": + return f"a.seqlen_k == 0 || a.seqlen_k % {self.bn0} != 0" + return f"a.seqlen_k != 0 && a.seqlen_k % {self.bn0} == 0" + + @property + def dcheck(self) -> str: + vec = int((32 * 4) / DTYPE_BITS[self.dtype]) + if self.dpad == "t": + return f"a.hdim_q % {vec} == 0" + assert False + + @property + def dvcheck(self) -> str: + vec = int((32 * 4) / DTYPE_BITS[self.dtype]) + if self.dvpad == "t": + return f"a.hdim_v % {vec} == 0" + assert False + + +@dataclass +class FmhaFwdPipeline: + tag: str + + F_vlayout: str # row/col + F_spad: str # true/false + F_skpad: str # + F_dpad: str # + F_dvpad: str # + F_logits: str # t/f + F_mask: str # value from MASK_MAP + F_trload: str # true/false + F_constraint: CppConstraint = field(default_factory=CppConstraint) + + @property + def name(self) -> str: + def pad_name() -> str: + n = "" + if self.F_spad == "t": + n += "s" + if self.F_skpad == "t": + n += "sk" + if self.F_dpad == "t": + n += "d" + if self.F_dvpad == "t": + n += "dv" + if n != "": + n = "p" + n + return n + + pn = pad_name() + n = f"{self.tag}_v{self.F_vlayout[0]}" + if pn != "": + n += f"_{pn}" + else: + n += "_npad" + + if self.F_logits == "t": + n += "_logits" + else: + n += "_nlogits" + + n += "_nbias" + + if self.F_mask[0:2] == "s_": + if self.F_mask == "s_mask": + n += "_mask" + else: + n += "_nmask" + else: + if self.F_mask != "no": + n += f"_m{self.F_mask[0]}" + else: + n += "_nmask" + + n += "_nskip" + + n += "_nsquant" + + if self.F_trload == "t": + n += "_trload" + else: + n += "_ntrload" + + return n + + +class FmhaFwdApiPool: + def __init__(self, mask_impl): + self.pool = dict() + self.mask_impl = mask_impl + + def register_traits(self, trait: FmhaFwdApiTrait) -> None: + # TODO: do we need to check duplication? + if trait.dtype not in self.pool.keys(): + self.pool[trait.dtype] = dict() + hdim = trait.hdim, trait.bn1 + if hdim not in self.pool[trait.dtype].keys(): + self.pool[trait.dtype][hdim] = list() + + self.pool[trait.dtype][hdim].append(copy.copy(trait)) + + @property + def api(self) -> str: + tr_load_cond_map = {"t": "has_load_tr", "f": "true"} + + per_tr_load = str() + for tr_load in ["t", "f"]: + per_dtypes = str() + for i, dtype in enumerate(self.pool.keys()): + per_hdim_case = str() + for j, (hdim, hdim_v) in enumerate(self.pool[dtype].keys()): + traits = [ + t + for t in self.pool[dtype][(hdim, hdim_v)] + if tr_load == t.tr_load + ] + inners = str() + for k, trait in enumerate(traits): + if_k = "if" if k == 0 else "else if" + inners = inners + FMHA_FWD_API_INNER_DISPATCH.format( + F_if=if_k, + F_vlayout=LAYOUT_MAP[trait.vlayout], + F_pipeline_enum=PIPELINE_ENUM_MAP[trait.pipeline_tag], + # F_logits removed - hardcoded to false (NOT supported) + F_mask=get_mask_map(self.mask_impl)[trait.mask], + F_mask_check=get_mask_check_map(self.mask_impl)[trait.mask], + F_trload=BOOL_MAP[trait.tr_load], + F_scheck=trait.scheck, + F_seqtune=trait.seqtune, + F_skcheck=trait.skcheck, + F_dcheck=trait.dcheck, + F_dvcheck=trait.dvcheck, + F_constraint=trait.constraint, + F_spad=BOOL_MAP[trait.spad], + F_skpad=BOOL_MAP[trait.skpad], + F_dpad=BOOL_MAP[trait.dpad], + F_dvpad=BOOL_MAP[trait.dvpad], + F_bm0=trait.bm0, + F_bn0=trait.bn0, + F_bk0=trait.bk0, + F_bn1=trait.bn1, + F_bk1=trait.bk1, + F_bk0max=trait.bk0max, + F_hdim=hdim, + F_dtype=FWD_DTYPE_MAP[dtype], + ) + if_j = "if" if j == 0 else "else if" + per_hdim_case = per_hdim_case + FMHA_FWD_API_PER_HDIM_CASE.format( + F_if=if_j, F_hdim=hdim, F_hdim_v=hdim_v, F_inner_dispatch=inners + ) + if_i = "if" if i == 0 else "else if" + per_dtypes = per_dtypes + FMHA_FWD_API_PER_DTYPE.format( + F_if=if_i, F_dtype=dtype, F_hdim_case=per_hdim_case + ) + per_tr_load += FMHA_FWD_API_PER_TRLOAD.format( + F_if="if", + F_trload_cond=tr_load_cond_map[tr_load], + F_dtype_case=per_dtypes, + ) + if not per_tr_load: + # empty string we add some ignore to suppress warning in api + per_tr_load += " (void)t ; (void)s ; (void)a;" + return FMHA_FWD_KERNEL_HEADER + FMHA_FWD_API.format(F_dispatch=per_tr_load) + + +@dataclass +class FmhaFwdTileSize: + F_bm0: int # tile size along q seqlen (block size) + F_bn0: int # tile size along k seqlen + F_bk0: int # tile size along qk gemm unroll + F_bn1: int # tile size along v head_dim + F_bk1: int # tile size along kv gemm unroll + F_bk0max: int # total length of K0, used for pipeline that need load Q at once (or repeately load Q as a whole tile) + F_rm0: int # number of warps for gemm0 along q seqlen + F_rn0: int # number of warps for gemm0 along k seqlen + F_rk0: int # number of warps for gemm0 along head dim q (not used) + F_rm1: int # number of warps for gemm1 along q seqlen + F_rn1: int # number of warps for gemm1 along head dim v + F_rk1: int # number of warps for gemm1 along k seqlen (not used) + F_wm0: int # gemm0 warp size along m + F_wn0: int # gemm0 warp size along n + F_wk0: int # gemm0 warp size along k + F_wm1: int # gemm1 warp size along m + F_wn1: int # gemm1 warp size along n + F_wk1: int # gemm1 warp size along k + F_occupancy: int # occupancy, -1 will let pipeline decide the occupancy, other value will overwrite occupancy + F_constraint: CppConstraint = field(default_factory=CppConstraint) + + @property + def name(self) -> str: + return ( + f"b{self.F_bm0}x{self.F_bn0}x{self.F_bk0}x{self.F_bn1}x{self.F_bk1}x{self.F_bk0max}" + + f"_r{self.F_rm0}x{self.F_rn0}x{self.F_rk0}_r{self.F_rm1}x{self.F_rn1}x{self.F_rk1}" + + f"_w{self.F_wm0}x{self.F_wn0}x{self.F_wk0}_w{self.F_wm1}x{self.F_wn1}x{self.F_wk1}" + + ("" if self.F_occupancy == -1 else f"_o{self.F_occupancy}") + ) + + +@dataclass +class FmhaFwdKernel: + F_idx: int # this is not a tunable, but a counter to differentiate symbol + F_hdim: int # hdim + F_dtype: str # data type + F_mode: str # value from MODE_MAP + F_tile: FmhaFwdTileSize + F_pipeline: FmhaFwdPipeline + mask_impl: str + + @property + def template(self) -> str: + # kernel_body removed - unused + return FMHA_FWD_KERNEL_HEADER + FMHA_FWD_KERNEL_BODY.format( + F_idx=self.F_idx, + F_hdim=self.F_hdim, + F_dtype=FWD_DTYPE_MAP[self.F_dtype], + F_bm0=self.F_tile.F_bm0, + F_bn0=self.F_tile.F_bn0, + F_bk0=self.F_tile.F_bk0, + F_bn1=self.F_tile.F_bn1, + F_bk1=self.F_tile.F_bk1, + F_bk0max=self.F_tile.F_bk0max, + F_rm0=self.F_tile.F_rm0, + F_rn0=self.F_tile.F_rn0, + F_rk0=self.F_tile.F_rk0, + F_rm1=self.F_tile.F_rm1, + F_rn1=self.F_tile.F_rn1, + F_rk1=self.F_tile.F_rk1, + F_wm0=self.F_tile.F_wm0, + F_wn0=self.F_tile.F_wn0, + F_wk0=self.F_tile.F_wk0, + F_wm1=self.F_tile.F_wm1, + F_wn1=self.F_tile.F_wn1, + F_wk1=self.F_tile.F_wk1, + F_vlayout=LAYOUT_MAP[self.F_pipeline.F_vlayout], + F_spad=BOOL_MAP[self.F_pipeline.F_spad], + F_skpad=BOOL_MAP[self.F_pipeline.F_skpad], + F_dpad=BOOL_MAP[self.F_pipeline.F_dpad], + F_dvpad=BOOL_MAP[self.F_pipeline.F_dvpad], + # F_logits removed - hardcoded to false in template (NOT supported) + F_occupancy=self.F_tile.F_occupancy, + F_pipeline_enum=PIPELINE_ENUM_MAP[self.F_pipeline.tag], + F_mask=get_mask_map(self.mask_impl)[self.F_pipeline.F_mask], + F_mode=MODE_MAP[self.F_mode], + F_pipeline=PIPELINE_MAP[self.F_pipeline.tag], + F_trload=BOOL_MAP[self.F_pipeline.F_trload], + F_kernel_name=self.name, + ) + + @property + def name(self) -> str: + # TODO: we don't encode idx here + return ( + f"fmha_jenga_fwd_d{self.F_hdim}_{self.F_dtype}_{self.F_mode}_" + + self.F_tile.name + + "_" + + self.F_pipeline.name + ) + + @property + def filename(self) -> str: + return self.name + ".cpp" + + def api_trait(self) -> FmhaFwdApiTrait: + return FmhaFwdApiTrait( + pipeline_tag=self.F_pipeline.tag, + hdim=str(self.F_hdim), + dtype=self.F_dtype, + mode=self.F_mode, + bm0=self.F_tile.F_bm0, + bn0=self.F_tile.F_bn0, + bk0=self.F_tile.F_bk0, + bn1=self.F_tile.F_bn1, + bk1=self.F_tile.F_bk1, + bk0max=self.F_tile.F_bk0max, + vlayout=self.F_pipeline.F_vlayout, + mask=self.F_pipeline.F_mask, + logits=self.F_pipeline.F_logits, + spad=self.F_pipeline.F_spad, + skpad=self.F_pipeline.F_skpad, + dpad=self.F_pipeline.F_dpad, + dvpad=self.F_pipeline.F_dvpad, + tr_load=self.F_pipeline.F_trload, + constraint=self.F_tile.F_constraint & self.F_pipeline.F_constraint, + ) + + +class KernelComponentFactory: + # TODO: design a more practical way to do it + # this is current supported tile size per hdim + @staticmethod + def get_hdim_tile_size_dict(dtype: str) -> Optional[dict]: + if dtype == "fp16" or dtype == "bf16": + return { + # (32, 32) : [FmhaFwdTileSize(128, 64, 16, 32, 32, 32, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, -1)], + # (64, 64) : [FmhaFwdTileSize(16, 32, 64, 64, 32, 64, 1, 1, 1, 1, 1, 1, 16, 16, 32, 16, 16, 32, -1), + # FmhaFwdTileSize(32, 32, 64, 64, 32, 64, 1, 1, 1, 1, 1, 1, 32, 32, 16, 32, 32, 16, -1), + # FmhaFwdTileSize(128, 64, 32, 64, 32, 64, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, -1)], + # (96, 128) : [FmhaFwdTileSize(128, 128, 32, 128, 32, 96, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, -1)], + (128, 128): [ + FmhaFwdTileSize( + 64, + 128, + 64, + 128, + 64, + 128, + 4, + 1, + 1, + 4, + 1, + 1, + 16, + 16, + 16, + 16, + 16, + 16, + -1, + ), + ], + # (160,160) : [FmhaFwdTileSize(128, 128, 32, 160, 32, 160, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, 1)], + # (192,128) : [FmhaFwdTileSize(128, 128, 32, 128, 32, 192, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, -1)], + # (192,192) : [FmhaFwdTileSize(128, 128, 32, 192, 32, 192, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, 1)], + # (256,256) : [FmhaFwdTileSize(128, 128, 32, 256, 32, 256, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, -1)], + } + else: + return None + + # TODO: we don't support tuning yet, so pick up one value for vlayout/pipeline/pad + # support this in future + @staticmethod + def get_pipelines(dtype, hdim, hdim_v, receipt, mask_impl) -> List[FmhaFwdPipeline]: + # this function will populate a list possible pipelines + # TODO: the order of List matters! the later in this list will be also be checked later + # NOTE: logits soft-cap is NOT supported by Jenga sparse attention (enforced by static_assert) + pipelines = [] + if dtype in ["fp16", "bf16"]: + for logits, mask in itertools.product( + ["f"], # logits soft-cap NOT supported, always false + get_mask_map(mask_impl).keys(), + ): + if hdim == 256 and hdim_v == 256: + # jenga fmha only supports dim <= 192 for now. + continue + pipelines.append( + FmhaFwdPipeline( # fmt: skip + "qr_async", + "row", + "t", + "f", + "t", + "t", + logits, + mask, + "f", + ) + ) + pipelines.append( + FmhaFwdPipeline( # fmt: skip + "qr_async", + "row", + "t", + "t", + "t", + "t", + logits, + mask, + "f", + ) + ) + else: + assert False + return pipelines + + +class CustomFactory(KernelComponentFactory): + @staticmethod + def get_hdim_tile_size_dict(dtype: str) -> Optional[dict]: + result = KernelComponentFactory.get_hdim_tile_size_dict(dtype) + if dtype == "fp16" or dtype == "bf16": + if (128, 128) in result.keys(): + result[(128, 128)].insert( + 0, + FmhaFwdTileSize( + 64, + 128, + 64, + 128, + 64, + 128, + 4, + 1, + 1, + 4, + 1, + 1, + 16, + 16, + 16, + 16, + 16, + 16, + -1, + CppConstraint( + "get_num_blocks(128) < num_cus * min_cu_util_rate" + ), + ), + ) + return result + + +def get_fwd_blobs( + kernel_filter: Optional[str], receipt, optdim_list, mask_impl +) -> Tuple[FmhaFwdApiPool, List[FmhaFwdKernel]]: + gen = list() + api_pool = FmhaFwdApiPool(mask_impl) + + factory = ( + CustomFactory + if os.environ.get("CK_TILE_FMHA_FWD_CUSTOM_FACTORY", "0") == "1" + else KernelComponentFactory + ) + + # Only generate fp16/bf16 kernels for now. + # NOTE: Jenga sparse attention only supports batch mode (group mode NOT supported, enforced by static_assert) + for dtype in ["fp16", "bf16"]: + d = factory.get_hdim_tile_size_dict(dtype) + if d is None: + continue + for ((hdim, hdim_v), tiles), mode in itertools.product(d.items(), ["batch"]): + for tile, pipeline in itertools.product( + tiles, factory.get_pipelines(dtype, hdim, hdim_v, receipt, mask_impl) + ): + if pipeline.tag != "qr_async": + continue + k = FmhaFwdKernel( + F_idx=2, + F_hdim=hdim, + F_dtype=dtype, + F_mode=mode, + F_tile=tile, + F_pipeline=pipeline, + mask_impl=mask_impl, + ) + if kernel_filter != "": + if not fnmatch.fnmatch(k.name, kernel_filter): + continue + if optdim_list != [-1]: + if hdim not in optdim_list: + continue + # 2 - Flash attention integration + if receipt in (2, 3): + cond = dtype in ["fp16", "bf16"] + cond &= pipeline.F_vlayout == "row" + if not cond: + continue + # PyTorch integration + elif receipt == 4: + cond = dtype in ["fp16", "bf16"] + cond &= pipeline.F_vlayout == "row" + cond &= mode == "batch" + cond &= pipeline.F_logits == "f" + if not cond: + continue + # Aiter(mha_fwd) integration + elif receipt == 100: + cond = dtype in ["fp16", "bf16"] + cond &= mode == "batch" + cond &= pipeline.F_vlayout == "row" + if not cond: + continue + # Aiter(mha_varlen_fwd) integration + elif receipt == 200: + cond = dtype in ["fp16", "bf16"] + cond &= mode == "group" + cond &= pipeline.F_vlayout == "row" + if not cond: + continue + # aiter::mha_fwd C++ api integration + elif receipt == 600: + cond = dtype in ["fp16", "bf16"] + cond &= pipeline.F_vlayout == "row" + if not cond: + continue + + api_pool.register_traits(k.api_trait()) + gen.append(k) + + return (api_pool, gen) + + +def write_single_fwd_kernel(kernel: FmhaFwdKernel, autogen_dir: Path) -> None: + update_file(autogen_dir / kernel.filename, kernel.template) + + +def write_fwd_api(api_pool: FmhaFwdApiPool, autogen_dir: Path) -> None: + update_file(autogen_dir / FMHA_FWD_API_FILENAME, api_pool.api) + + +def write_blobs( + output_dir: Path, kernel_filter: str, receipt, optdim_list, mask_impl +) -> None: + api_pool, kernels = get_fwd_blobs(kernel_filter, receipt, optdim_list, mask_impl) + for kernel in kernels: + write_single_fwd_kernel(kernel, output_dir) + write_fwd_api(api_pool, output_dir) + + +def list_blobs( + file_path: Path, kernel_filter: str, receipt, optdim_list, mask_impl +) -> None: + with file_path.open("a") as f: + _, kernels = get_fwd_blobs(kernel_filter, receipt, optdim_list, mask_impl) + for kernel in kernels: + f.write((file_path.parent / GEN_DIR / kernel.filename).as_posix() + "\n") + f.write((file_path.parent / GEN_DIR / FMHA_FWD_API_FILENAME).as_posix() + "\n") diff --git a/example/ck_tile/50_sparse_attn/codegen/ops/sparge_fwd_vsa.py b/example/ck_tile/50_sparse_attn/codegen/ops/sparge_fwd_vsa.py new file mode 100644 index 00000000000..c9a389df3fa --- /dev/null +++ b/example/ck_tile/50_sparse_attn/codegen/ops/sparge_fwd_vsa.py @@ -0,0 +1,799 @@ +# Copyright (c) Advanced Micro Devices, Inc., or its affiliates. +# SPDX-License-Identifier: MIT +# generate kernel instances to speed up compilation + +import copy +from dataclasses import dataclass, field +import fnmatch +import itertools +import os +import os.path as path +from pathlib import Path +from typing import List, Optional, Tuple + +from codegen.cpp_symbol_map import ( + BOOL_MAP, + FWD_DTYPE_MAP, + LAYOUT_MAP, + MODE_MAP, + PIPELINE_ENUM_MAP, + PIPELINE_MAP, + get_mask_check_map, + get_mask_map, +) + +GEN_DIR = "" + + +def update_file(file_path, content): + """Update the file at file_path with the given content if it differs from the existing content. + + It avoids unnecessary touching of the file which triggers rebuilds + """ + + existing_content = "" + if path.exists(file_path): + with open(file_path, "r") as file: + existing_content = file.read() + if existing_content == content: + return + with open(file_path, "w") as file: + file.write(content) + + +DTYPE_BITS = {"fp32": 32, "fp16": 16, "bf16": 16} + +K0_MAX_SUBMAX_MAP = {32: 32, 64: 64, 96: 128, 128: 128, 192: 192, 256: 256} + +FMHA_FWD_KERNEL_HEADER = """// SPDX-License-Identifier: MIT +// Copyright (c) Advanced Micro Devices, Inc., or its affiliates.\n +// auto generated by generate.py +#include "ck_tile/ops/fmha/block/variants.hpp" +#include "fmha_fwd_trek.hpp" +#include "pipeline/block_fmha_pipeline_qr_ks_vs_async_vsa.hpp" +#include "kernel/fmha_fwd_vsa_kernel.hpp" + +""" + +# NOTE: VSA sparse attention kernel has the following restrictions enforced by static_assert: +# - Group mode: NOT supported (batch mode only) +# - Bias: NOT supported (NO_BIAS only) +# - LSE output: NOT supported (false only) +# - Dropout: NOT supported (false only) +# - Logits soft-cap: NOT supported (false only) +# - FP8 static quantization: NOT supported (NO_SCALE only) +# The template below hardcodes these unsupported features accordingly. + +FMHA_FWD_KERNEL_BODY = """ +using fmha_dtype_{F_idx} = {F_dtype}; + +using fmha_block_tile_{F_idx} = ck_tile::sequence<{F_bm0}, {F_bn0}, {F_bk0}, {F_bn1}, {F_bk1}, {F_bk0max}>; + +using fmha_shape_{F_idx} = ck_tile::TileFmhaShape, + ck_tile::sequence<{F_wm0}, {F_wn0}, {F_wk0}>, + ck_tile::sequence<{F_rm1}, {F_rn1}, {F_rk1}>, + ck_tile::sequence<{F_wm1}, {F_wn1}, {F_wk1}>, + {F_vlayout}>; + +// TileFmhaTraits: spad, skpad, dpad, dvpad, has_logits_soft_cap, bias_enum, +// store_lse, has_dropout, has_randval, quant_scale_enum, occupancy, is_v_rowmajor_skip +using fmha_trait_{F_idx} = ck_tile::TileFmhaTraits<{F_spad}, + {F_skpad}, + {F_dpad}, + {F_dvpad}, + false, // has_logits_soft_cap - NOT supported + ck_tile::BlockAttentionBiasEnum::NO_BIAS, // bias - NOT supported + false, // store_lse - NOT supported + false, // has_dropout - NOT supported + false, // has_randval - NOT supported + ck_tile::BlockAttentionQuantScaleEnum::NO_SCALE, // FP8 quant - NOT supported + {F_occupancy}, + false>; + +using fmha_variant_{F_idx} = ck_tile::ComposedAttention<0, CK_TILE_FMHA_FWD_FAST_EXP2>; // logits_soft_cap=0 (NOT supported) + +using fmha_mask_{F_idx} = {F_mask}; + +using fmha_pipeline_problem_{F_idx} = ck_tile::BlockFmhaPipelineProblem< + typename FmhaSparseFwdTypeConfig::QDataType, + typename FmhaSparseFwdTypeConfig::KDataType, + typename FmhaSparseFwdTypeConfig::VDataType, + typename FmhaSparseFwdTypeConfig::SaccDataType, + typename FmhaSparseFwdTypeConfig::SMPLComputeDataType, + typename FmhaSparseFwdTypeConfig::BiasDataType, + typename FmhaSparseFwdTypeConfig::RandValOutputDataType, + typename FmhaSparseFwdTypeConfig::LSEDataType, + typename FmhaSparseFwdTypeConfig::PDataType, + typename FmhaSparseFwdTypeConfig::OaccDataType, + typename FmhaSparseFwdTypeConfig::ODataType, + fmha_shape_{F_idx}, + {F_mode}, + fmha_variant_{F_idx}, + fmha_mask_{F_idx}, + {F_trload}, + fmha_trait_{F_idx}>; + +using fmha_pipeline_{F_idx} = ck_tile::BlockFmhaPipelineQRKSVSAsyncVSA< + fmha_pipeline_problem_{F_idx}>; + +using fmha_epilogue_{F_idx} = + ck_tile::Default2DEpilogue::OaccDataType, + typename FmhaSparseFwdTypeConfig<{F_dtype}>::ODataType, + {F_spad}, {F_dvpad}>>; + +using fmha_kernel_{F_idx} = + ck_tile::FmhaFwdVSAKernel; + +using trait_{F_idx} = fmha_vsa_fwd_traits_<{F_hdim}, {F_dtype}, {F_bm0}, {F_bn0}, {F_bk0}, {F_bn1}, {F_bk1}, {F_bk0max}, {F_vlayout}, + {F_pipeline_enum}, false/*logits*/, fmha_mask_{F_idx}, {F_spad}, {F_skpad}, {F_dpad}, {F_dvpad}, {F_trload}>; + +#include + +template<> +float fmha_vsa_fwd_(const ck_tile::stream_config& s, fmha_vsa_fwd_args a) +{{ + using k_ = fmha_kernel_{F_idx}; + if(s.log_level_ > 0) + std::cout << ", " << "{F_kernel_name}" << std::flush; + auto [kargs, grids] = fmha_fwd_create_kargs_and_grids(a); + const dim3 blocks = k_::BlockSize(); + constexpr ck_tile::index_t kBlockPerCu = k_::kBlockPerCu; + return ck_tile::launch_kernel(s, ck_tile::make_kernel(k_{{}}, grids, blocks, 0, kargs)); +}} +""" + +FMHA_FWD_API_FILENAME = "sparge_vsa_fwd_api.cpp" +FMHA_FWD_API = """ +#include + +#include + +namespace {{ +bool get_num_cus(unsigned& num_cus) {{ + int device; + auto status = hipGetDevice(&device); + if(status != hipSuccess) {{ + fprintf(stderr, "failed to get device"); + return false; + }} + + hipDeviceProp_t props{{}}; + status = hipGetDeviceProperties(&props, device); + if(status != hipSuccess) {{ + fprintf(stderr, "failed to get device properties"); + return false; + }} + + num_cus = props.multiProcessorCount; + return true; +}} + +unsigned get_num_thread_blocks(unsigned batch, unsigned nheads, unsigned max_seqlen_q, unsigned kM0) {{ + const unsigned num_m_blocks = (max_seqlen_q + kM0 - 1) / kM0; + const unsigned num_n_blocks = 1; // we assume that num_n_blocks is always 1 + + return batch * nheads * num_m_blocks * num_n_blocks; +}} +}} // namespace + +float sparge_vsa_fwd(fmha_vsa_fwd_traits t, fmha_vsa_fwd_args a, const ck_tile::stream_config& s){{ + float r = -1; + + [[maybe_unused]] const float min_cu_util_rate = 0.8; // minimum CU utilization rate + + unsigned num_cus; + if (!get_num_cus(num_cus)) {{ + return r; + }} + + [[maybe_unused]] auto get_num_blocks = [&](unsigned kM0) {{ + return get_num_thread_blocks(a.batch, a.nhead_q, a.max_seqlen_q, kM0); + }}; + + const bool has_load_tr = ck_tile::is_load_tr_supported(); + +{F_dispatch} + return r; +}} +""" + +FMHA_FWD_API_PER_TRLOAD = """ {F_if}({F_trload_cond}){{ +{F_dtype_case} + }} +""" + +FMHA_FWD_API_PER_DTYPE = """ {F_if}(t.data_type.compare(\"{F_dtype}\") == 0){{ +{F_hdim_case} + }} +""" +FMHA_FWD_API_PER_HDIM_CASE = """ {F_if} (t.hdim_q <= {F_hdim} && t.hdim_v <= {F_hdim_v}) {{ +{F_inner_dispatch} + }} +""" + +FMHA_FWD_API_INNER_DISPATCH = """ {F_if}((t.is_v_rowmajor == {F_vlayout}) && ({F_mask_check}) && + ({F_scheck}) && ({F_seqtune}) && ({F_skcheck}) && ({F_dcheck}) && ({F_dvcheck}) && ({F_constraint})) {{ + using trait_ = fmha_vsa_fwd_traits_<{F_hdim}, {F_dtype}, {F_bm0}, {F_bn0}, {F_bk0}, {F_bn1}, {F_bk1}, {F_bk0max}, {F_vlayout}, {F_pipeline_enum}, false/*logits*/, {F_mask}, {F_spad}, {F_skpad}, {F_dpad}, {F_dvpad}, {F_trload}>; + return fmha_vsa_fwd_(s, a); + }} +""" + + +@dataclass +class CppConstraint: + bool_expr: str = None + + def __str__(self): + if self.bool_expr is None: + return "true" + else: + return f"{self.bool_expr}" + + def __and__(self, other): + return CppConstraint(f"({str(self)}) && ({str(other)})") + + +@dataclass +class FmhaFwdApiTrait: + pipeline_tag: str + # sync with fmha_fwd_traits<>, to generate fallback calls + hdim: str + dtype: str # data type + mode: str # value from MODE_MAP + bm0: int # tile size along q seqlen (block size) + bn0: int # tile size along qk seqlen + bk0: int # tile size along qk gemm unroll + bn1: int # tile size along v head_dim + bk1: int # tile size along kv gemm unroll + bk0max: int + vlayout: str + logits: str + mask: str + spad: str + skpad: str + dpad: str + dvpad: str + tr_load: str + constraint: CppConstraint + + @property + def name(self) -> str: + return ( + f"{self.hdim}-{self.dtype}-{self.mode}-{self.bm0}-{self.bn0}-{self.bk0}-{self.bn0}-{self.bk1}-{self.bk0max}-" + + f"{self.vlayout}-{self.logits}-{self.mask}-{self.spad}-{self.skpad}-{self.dpad}-{self.dvpad}" + ) + + @property + def scheck(self) -> str: + if self.mode == "group": + return "true/*group mode spad always true*/" # group mode only generate spad/skpad == true + if self.spad == "t": + return "true" # always support + return "true" + + @property + def seqtune(self) -> str: + return "true" + + @property + def skcheck(self) -> str: + if self.mode == "group": + return "true/*group mode skpad always true*/" # group mode only generate spad/skpad == true + if self.skpad == "t": + return f"a.seqlen_k == 0 || a.seqlen_k % {self.bn0} != 0" + return f"a.seqlen_k != 0 && a.seqlen_k % {self.bn0} == 0" + + @property + def dcheck(self) -> str: + vec = int((32 * 4) / DTYPE_BITS[self.dtype]) + if self.dpad == "t": + return f"a.hdim_q % {vec} == 0" + assert False + + @property + def dvcheck(self) -> str: + vec = int((32 * 4) / DTYPE_BITS[self.dtype]) + if self.dvpad == "t": + return f"a.hdim_v % {vec} == 0" + assert False + + +@dataclass +class FmhaFwdPipeline: + tag: str + + F_vlayout: str # row/col + F_spad: str # true/false + F_skpad: str # + F_dpad: str # + F_dvpad: str # + F_logits: str # t/f + F_mask: str # value from MASK_MAP + F_trload: str # true/false + F_constraint: CppConstraint = field(default_factory=CppConstraint) + + @property + def name(self) -> str: + def pad_name() -> str: + n = "" + if self.F_spad == "t": + n += "s" + if self.F_skpad == "t": + n += "sk" + if self.F_dpad == "t": + n += "d" + if self.F_dvpad == "t": + n += "dv" + if n != "": + n = "p" + n + return n + + pn = pad_name() + n = f"{self.tag}_v{self.F_vlayout[0]}" + if pn != "": + n += f"_{pn}" + else: + n += "_npad" + + if self.F_logits == "t": + n += "_logits" + else: + n += "_nlogits" + + n += "_nbias" + + if self.F_mask[0:2] == "s_": + if self.F_mask == "s_mask": + n += "_mask" + else: + n += "_nmask" + else: + if self.F_mask != "no": + n += f"_m{self.F_mask[0]}" + else: + n += "_nmask" + + n += "_nskip" + + n += "_nsquant" + + if self.F_trload == "t": + n += "_trload" + else: + n += "_ntrload" + + return n + + +class FmhaFwdApiPool: + def __init__(self, mask_impl): + self.pool = dict() + self.mask_impl = mask_impl + + def register_traits(self, trait: FmhaFwdApiTrait) -> None: + # TODO: do we need to check duplication? + if trait.dtype not in self.pool.keys(): + self.pool[trait.dtype] = dict() + hdim = trait.hdim, trait.bn1 + if hdim not in self.pool[trait.dtype].keys(): + self.pool[trait.dtype][hdim] = list() + + self.pool[trait.dtype][hdim].append(copy.copy(trait)) + + @property + def api(self) -> str: + tr_load_cond_map = {"t": "has_load_tr", "f": "true"} + + per_tr_load = str() + for tr_load in ["t", "f"]: + per_dtypes = str() + for i, dtype in enumerate(self.pool.keys()): + per_hdim_case = str() + for j, (hdim, hdim_v) in enumerate(self.pool[dtype].keys()): + traits = [ + t + for t in self.pool[dtype][(hdim, hdim_v)] + if tr_load == t.tr_load + ] + inners = str() + for k, trait in enumerate(traits): + if_k = "if" if k == 0 else "else if" + inners = inners + FMHA_FWD_API_INNER_DISPATCH.format( + F_if=if_k, + F_vlayout=LAYOUT_MAP[trait.vlayout], + F_pipeline_enum=PIPELINE_ENUM_MAP[trait.pipeline_tag], + # F_logits removed - hardcoded to false (NOT supported) + F_mask=get_mask_map(self.mask_impl)[trait.mask], + F_mask_check=get_mask_check_map(self.mask_impl)[trait.mask], + F_trload=BOOL_MAP[trait.tr_load], + F_scheck=trait.scheck, + F_seqtune=trait.seqtune, + F_skcheck=trait.skcheck, + F_dcheck=trait.dcheck, + F_dvcheck=trait.dvcheck, + F_constraint=trait.constraint, + F_spad=BOOL_MAP[trait.spad], + F_skpad=BOOL_MAP[trait.skpad], + F_dpad=BOOL_MAP[trait.dpad], + F_dvpad=BOOL_MAP[trait.dvpad], + F_bm0=trait.bm0, + F_bn0=trait.bn0, + F_bk0=trait.bk0, + F_bn1=trait.bn1, + F_bk1=trait.bk1, + F_bk0max=trait.bk0max, + F_hdim=hdim, + F_dtype=FWD_DTYPE_MAP[dtype], + ) + if_j = "if" if j == 0 else "else if" + per_hdim_case = per_hdim_case + FMHA_FWD_API_PER_HDIM_CASE.format( + F_if=if_j, F_hdim=hdim, F_hdim_v=hdim_v, F_inner_dispatch=inners + ) + if_i = "if" if i == 0 else "else if" + per_dtypes = per_dtypes + FMHA_FWD_API_PER_DTYPE.format( + F_if=if_i, F_dtype=dtype, F_hdim_case=per_hdim_case + ) + per_tr_load += FMHA_FWD_API_PER_TRLOAD.format( + F_if="if", + F_trload_cond=tr_load_cond_map[tr_load], + F_dtype_case=per_dtypes, + ) + if not per_tr_load: + # empty string we add some ignore to suppress warning in api + per_tr_load += " (void)t ; (void)s ; (void)a;" + return FMHA_FWD_KERNEL_HEADER + FMHA_FWD_API.format(F_dispatch=per_tr_load) + + +@dataclass +class FmhaFwdTileSize: + F_bm0: int # tile size along q seqlen (block size) + F_bn0: int # tile size along k seqlen + F_bk0: int # tile size along qk gemm unroll + F_bn1: int # tile size along v head_dim + F_bk1: int # tile size along kv gemm unroll + F_bk0max: int # total length of K0, used for pipeline that need load Q at once (or repeately load Q as a whole tile) + F_rm0: int # number of warps for gemm0 along q seqlen + F_rn0: int # number of warps for gemm0 along k seqlen + F_rk0: int # number of warps for gemm0 along head dim q (not used) + F_rm1: int # number of warps for gemm1 along q seqlen + F_rn1: int # number of warps for gemm1 along head dim v + F_rk1: int # number of warps for gemm1 along k seqlen (not used) + F_wm0: int # gemm0 warp size along m + F_wn0: int # gemm0 warp size along n + F_wk0: int # gemm0 warp size along k + F_wm1: int # gemm1 warp size along m + F_wn1: int # gemm1 warp size along n + F_wk1: int # gemm1 warp size along k + F_occupancy: int # occupancy, -1 will let pipeline decide the occupancy, other value will overwrite occupancy + F_constraint: CppConstraint = field(default_factory=CppConstraint) + + @property + def name(self) -> str: + return ( + f"b{self.F_bm0}x{self.F_bn0}x{self.F_bk0}x{self.F_bn1}x{self.F_bk1}x{self.F_bk0max}" + + f"_r{self.F_rm0}x{self.F_rn0}x{self.F_rk0}_r{self.F_rm1}x{self.F_rn1}x{self.F_rk1}" + + f"_w{self.F_wm0}x{self.F_wn0}x{self.F_wk0}_w{self.F_wm1}x{self.F_wn1}x{self.F_wk1}" + + ("" if self.F_occupancy == -1 else f"_o{self.F_occupancy}") + ) + + +@dataclass +class FmhaFwdKernel: + F_idx: int # this is not a tunable, but a counter to differentiate symbol + F_hdim: int # hdim + F_dtype: str # data type + F_mode: str # value from MODE_MAP + F_tile: FmhaFwdTileSize + F_pipeline: FmhaFwdPipeline + mask_impl: str + + @property + def template(self) -> str: + # kernel_body removed - unused + return FMHA_FWD_KERNEL_HEADER + FMHA_FWD_KERNEL_BODY.format( + F_idx=self.F_idx, + F_hdim=self.F_hdim, + F_dtype=FWD_DTYPE_MAP[self.F_dtype], + F_bm0=self.F_tile.F_bm0, + F_bn0=self.F_tile.F_bn0, + F_bk0=self.F_tile.F_bk0, + F_bn1=self.F_tile.F_bn1, + F_bk1=self.F_tile.F_bk1, + F_bk0max=self.F_tile.F_bk0max, + F_rm0=self.F_tile.F_rm0, + F_rn0=self.F_tile.F_rn0, + F_rk0=self.F_tile.F_rk0, + F_rm1=self.F_tile.F_rm1, + F_rn1=self.F_tile.F_rn1, + F_rk1=self.F_tile.F_rk1, + F_wm0=self.F_tile.F_wm0, + F_wn0=self.F_tile.F_wn0, + F_wk0=self.F_tile.F_wk0, + F_wm1=self.F_tile.F_wm1, + F_wn1=self.F_tile.F_wn1, + F_wk1=self.F_tile.F_wk1, + F_vlayout=LAYOUT_MAP[self.F_pipeline.F_vlayout], + F_spad=BOOL_MAP[self.F_pipeline.F_spad], + F_skpad=BOOL_MAP[self.F_pipeline.F_skpad], + F_dpad=BOOL_MAP[self.F_pipeline.F_dpad], + F_dvpad=BOOL_MAP[self.F_pipeline.F_dvpad], + # F_logits removed - hardcoded to false in template (NOT supported) + F_occupancy=self.F_tile.F_occupancy, + F_pipeline_enum=PIPELINE_ENUM_MAP[self.F_pipeline.tag], + F_mask=get_mask_map(self.mask_impl)[self.F_pipeline.F_mask], + F_mode=MODE_MAP[self.F_mode], + F_pipeline=PIPELINE_MAP[self.F_pipeline.tag], + F_trload=BOOL_MAP[self.F_pipeline.F_trload], + F_kernel_name=self.name, + ) + + @property + def name(self) -> str: + # TODO: we don't encode idx here + return ( + f"fmha_vsa_fwd_d{self.F_hdim}_{self.F_dtype}_{self.F_mode}_" + + self.F_tile.name + + "_" + + self.F_pipeline.name + ) + + @property + def filename(self) -> str: + return self.name + ".cpp" + + def api_trait(self) -> FmhaFwdApiTrait: + return FmhaFwdApiTrait( + pipeline_tag=self.F_pipeline.tag, + hdim=str(self.F_hdim), + dtype=self.F_dtype, + mode=self.F_mode, + bm0=self.F_tile.F_bm0, + bn0=self.F_tile.F_bn0, + bk0=self.F_tile.F_bk0, + bn1=self.F_tile.F_bn1, + bk1=self.F_tile.F_bk1, + bk0max=self.F_tile.F_bk0max, + vlayout=self.F_pipeline.F_vlayout, + mask=self.F_pipeline.F_mask, + logits=self.F_pipeline.F_logits, + spad=self.F_pipeline.F_spad, + skpad=self.F_pipeline.F_skpad, + dpad=self.F_pipeline.F_dpad, + dvpad=self.F_pipeline.F_dvpad, + tr_load=self.F_pipeline.F_trload, + constraint=self.F_tile.F_constraint & self.F_pipeline.F_constraint, + ) + + +class KernelComponentFactory: + # TODO: design a more practical way to do it + # this is current supported tile size per hdim + @staticmethod + def get_hdim_tile_size_dict(dtype: str) -> Optional[dict]: + if dtype == "fp16" or dtype == "bf16": + return { + # (32, 32) : [FmhaFwdTileSize(128, 64, 16, 32, 32, 32, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, -1)], + # (64, 64) : [FmhaFwdTileSize(16, 32, 64, 64, 32, 64, 1, 1, 1, 1, 1, 1, 16, 16, 32, 16, 16, 32, -1), + # FmhaFwdTileSize(32, 32, 64, 64, 32, 64, 1, 1, 1, 1, 1, 1, 32, 32, 16, 32, 32, 16, -1), + # FmhaFwdTileSize(128, 64, 32, 64, 32, 64, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, -1)], + # (96, 128) : [FmhaFwdTileSize(128, 128, 32, 128, 32, 96, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, -1)], + (128, 128): [ + FmhaFwdTileSize( + 64, + 128, + 64, + 128, + 64, + 128, + 4, + 1, + 1, + 4, + 1, + 1, + 16, + 16, + 16, + 16, + 16, + 16, + -1, + ), + ], + # (160,160) : [FmhaFwdTileSize(128, 128, 32, 160, 32, 160, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, 1)], + # (192,128) : [FmhaFwdTileSize(128, 128, 32, 128, 32, 192, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, -1)], + # (192,192) : [FmhaFwdTileSize(128, 128, 32, 192, 32, 192, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, 1)], + # (256,256) : [FmhaFwdTileSize(128, 128, 32, 256, 32, 256, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, -1)], + } + else: + return None + + # TODO: we don't support tuning yet, so pick up one value for vlayout/pipeline/pad + # support this in future + @staticmethod + def get_pipelines(dtype, hdim, hdim_v, receipt, mask_impl) -> List[FmhaFwdPipeline]: + # this function will populate a list possible pipelines + # TODO: the order of List matters! the later in this list will be also be checked later + # NOTE: logits soft-cap is NOT supported by VSA sparse attention (enforced by static_assert) + pipelines = [] + if dtype in ["fp16", "bf16"]: + for logits, mask in itertools.product( + ["f"], # logits soft-cap NOT supported, always false + get_mask_map(mask_impl).keys(), + ): + if hdim == 256 and hdim_v == 256: + # vsa fmha only supports dim <= 192 for now. + continue + pipelines.append( + FmhaFwdPipeline( + "qr_async_vsa", + "row", + "t", + "f", + "t", + "t", + logits, + mask, + "f", + ) + ) + pipelines.append( + FmhaFwdPipeline( + "qr_async_vsa", + "row", + "t", + "t", + "t", + "t", + logits, + mask, + "f", + ) + ) + else: + assert False + return pipelines + + +class CustomFactory(KernelComponentFactory): + @staticmethod + def get_hdim_tile_size_dict(dtype: str) -> Optional[dict]: + result = KernelComponentFactory.get_hdim_tile_size_dict(dtype) + if dtype == "fp16" or dtype == "bf16": + if (128, 128) in result.keys(): + result[(128, 128)].insert( + 0, + FmhaFwdTileSize( + 64, + 128, + 64, + 128, + 64, + 128, + 4, + 1, + 1, + 4, + 1, + 1, + 16, + 16, + 16, + 16, + 16, + 16, + -1, + CppConstraint( + "get_num_blocks(128) < num_cus * min_cu_util_rate" + ), + ), + ) + return result + + +def get_fwd_blobs( + kernel_filter: Optional[str], receipt, optdim_list, mask_impl +) -> Tuple[FmhaFwdApiPool, List[FmhaFwdKernel]]: + gen = list() + api_pool = FmhaFwdApiPool(mask_impl) + + factory = ( + CustomFactory + if os.environ.get("CK_TILE_FMHA_FWD_CUSTOM_FACTORY", "0") == "1" + else KernelComponentFactory + ) + + # Only generate fp16/bf16 kernels for now. + # NOTE: VSA sparse attention only supports batch mode (group mode NOT supported, enforced by static_assert) + for dtype in ["fp16", "bf16"]: + d = factory.get_hdim_tile_size_dict(dtype) + if d is None: + continue + for ((hdim, hdim_v), tiles), mode in itertools.product(d.items(), ["batch"]): + for tile, pipeline in itertools.product( + tiles, factory.get_pipelines(dtype, hdim, hdim_v, receipt, mask_impl) + ): + if pipeline.tag != "qr_async_vsa": + continue + k = FmhaFwdKernel( + F_idx=1, + F_hdim=hdim, + F_dtype=dtype, + F_mode=mode, + F_tile=tile, + F_pipeline=pipeline, + mask_impl=mask_impl, + ) + if kernel_filter != "": + if not fnmatch.fnmatch(k.name, kernel_filter): + continue + if optdim_list != [-1]: + if hdim not in optdim_list: + continue + # 2 - Flash attention integration + if receipt in (2, 3): + cond = dtype in ["fp16", "bf16"] + cond &= pipeline.F_vlayout == "row" + if not cond: + continue + # PyTorch integration + elif receipt == 4: + cond = dtype in ["fp16", "bf16"] + cond &= pipeline.F_vlayout == "row" + cond &= mode == "batch" + cond &= pipeline.F_logits == "f" + if not cond: + continue + # Aiter(mha_fwd) integration + elif receipt == 100: + cond = dtype in ["fp16", "bf16"] + cond &= mode == "batch" + cond &= pipeline.F_vlayout == "row" + if not cond: + continue + # Aiter(mha_varlen_fwd) integration + elif receipt == 200: + cond = dtype in ["fp16", "bf16"] + cond &= mode == "group" + cond &= pipeline.F_vlayout == "row" + if not cond: + continue + # aiter::mha_fwd C++ api integration + elif receipt == 600: + cond = dtype in ["fp16", "bf16"] + cond &= pipeline.F_vlayout == "row" + if not cond: + continue + + api_pool.register_traits(k.api_trait()) + gen.append(k) + + return (api_pool, gen) + + +def write_single_fwd_kernel(kernel: FmhaFwdKernel, autogen_dir: Path) -> None: + update_file(autogen_dir / kernel.filename, kernel.template) + + +def write_fwd_api(api_pool: FmhaFwdApiPool, autogen_dir: Path) -> None: + update_file(autogen_dir / FMHA_FWD_API_FILENAME, api_pool.api) + + +def write_blobs( + output_dir: Path, kernel_filter: str, receipt, optdim_list, mask_impl +) -> None: + api_pool, kernels = get_fwd_blobs(kernel_filter, receipt, optdim_list, mask_impl) + for kernel in kernels: + write_single_fwd_kernel(kernel, output_dir) + write_fwd_api(api_pool, output_dir) + + +def list_blobs( + file_path: Path, kernel_filter: str, receipt, optdim_list, mask_impl +) -> None: + with file_path.open("a") as f: + _, kernels = get_fwd_blobs(kernel_filter, receipt, optdim_list, mask_impl) + for kernel in kernels: + f.write((file_path.parent / GEN_DIR / kernel.filename).as_posix() + "\n") + f.write((file_path.parent / GEN_DIR / FMHA_FWD_API_FILENAME).as_posix() + "\n") diff --git a/example/ck_tile/50_sparse_attn/fmha_fwd_trek.hpp b/example/ck_tile/50_sparse_attn/fmha_fwd_trek.hpp index 7349c3576e8..25e3513d2fa 100644 --- a/example/ck_tile/50_sparse_attn/fmha_fwd_trek.hpp +++ b/example/ck_tile/50_sparse_attn/fmha_fwd_trek.hpp @@ -277,6 +277,9 @@ struct fmha_jenga_fwd_traits float fmha_jenga_fwd(fmha_jenga_fwd_traits, fmha_jenga_fwd_args, const ck_tile::stream_config&); +// sparge jenga +float sparge_jenga_fwd(fmha_jenga_fwd_traits, fmha_jenga_fwd_args, const ck_tile::stream_config&); + template float fmha_jenga_fwd_(const ck_tile::stream_config&, fmha_jenga_fwd_args); @@ -322,6 +325,9 @@ using fmha_vsa_fwd_traits = fmha_jenga_fwd_traits; float fmha_vsa_fwd(fmha_vsa_fwd_traits, fmha_vsa_fwd_args, const ck_tile::stream_config&); +// sparge vsa +float sparge_vsa_fwd(fmha_vsa_fwd_traits, fmha_vsa_fwd_args, const ck_tile::stream_config&); + template float fmha_vsa_fwd_(const ck_tile::stream_config&, fmha_vsa_fwd_args); diff --git a/example/ck_tile/50_sparse_attn/jenga_sparge_attention.cpp b/example/ck_tile/50_sparse_attn/jenga_sparge_attention.cpp new file mode 100644 index 00000000000..88f3e08204e --- /dev/null +++ b/example/ck_tile/50_sparse_attn/jenga_sparge_attention.cpp @@ -0,0 +1,189 @@ +// Copyright (c) Advanced Micro Devices, Inc., or its affiliates. +// SPDX-License-Identifier: MIT +#include "jenga_sparge_attention.h" +#include "fmha_fwd_trek.hpp" +#include "ck_tile/core.hpp" +#include "ck_tile/host/host_tensor.hpp" +#include "ck_tile/host/device_memory.hpp" +#include + +template +ck_tile::HostTensor +jenga_sparge_attention(const ck_tile::HostTensor& TQ, + const ck_tile::HostTensor& TK, + const ck_tile::HostTensor& TV, + const ck_tile::HostTensor& Tblock_relation_onehot, + ck_tile::HostTensor& Y, + int batch, + int nhead, + int nhead_k, + int seqlen_q, + int seqlen_k, + int hdim_q, + int hdim_v, + bool i_perm, + bool o_perm, + int max_seqlen_q, + int max_seqlen_k, + int log_level) +{ + static_assert(std::is_same_v || + std::is_same_v, + "Jenga sparse attention supports fp16/bf16 only."); + std::string data_type = "fp16"; + if constexpr(std::is_same_v) + { + data_type = "bf16"; + } + + if(max_seqlen_q == 0) + max_seqlen_q = seqlen_q; + if(max_seqlen_k == 0) + max_seqlen_k = seqlen_k; + bool is_v_rowmajor = true; + float scale_s = 1.0 / ck_tile::sqrt(static_cast(hdim_q)); + std::string msk_str = "0"; + mask_info mask = mask_info::decode(msk_str, seqlen_q, seqlen_k); + + const ck_tile::index_t shape_seqlen_q = seqlen_q; + const ck_tile::index_t shape_seqlen_k = seqlen_k; + + ck_tile::stream_config stream_config{nullptr, + false, // time_kernel + log_level, + 0, + 1, + false}; + + ck_tile::DeviceMem q_buf(TQ.get_element_space_size_in_bytes()); + ck_tile::DeviceMem k_buf(TK.get_element_space_size_in_bytes()); + ck_tile::DeviceMem v_buf(TV.get_element_space_size_in_bytes()); + ck_tile::DeviceMem block_relation_buf(Tblock_relation_onehot.get_element_space_size_in_bytes()); + ck_tile::DeviceMem o_buf(Y.get_element_space_size_in_bytes()); + + q_buf.ToDevice(TQ.data()); + k_buf.ToDevice(TK.data()); + v_buf.ToDevice(TV.data()); + block_relation_buf.ToDevice(Tblock_relation_onehot.data()); + + const auto init_args = [&](auto& args) { + assert(nhead % nhead_k == 0); + const ck_tile::index_t stride_q = (i_perm ? hdim_q : nhead * hdim_q); + const ck_tile::index_t stride_k = (i_perm ? hdim_q : nhead_k * hdim_q); + const ck_tile::index_t stride_v = [&]() { + if(is_v_rowmajor) + return i_perm ? hdim_v : nhead_k * hdim_v; + else + return (i_perm ? shape_seqlen_k : nhead_k * shape_seqlen_k); + }(); + const ck_tile::index_t stride_o = (o_perm ? hdim_v : nhead * hdim_v); + const ck_tile::index_t nhead_stride_q = (i_perm ? shape_seqlen_q * hdim_q : hdim_q); + const ck_tile::index_t nhead_stride_k = i_perm ? shape_seqlen_k * hdim_q : hdim_q; + const ck_tile::index_t nhead_stride_v = [&]() { + if(is_v_rowmajor) + return i_perm ? shape_seqlen_k * hdim_v : hdim_v; + else + return i_perm ? hdim_v * shape_seqlen_k : shape_seqlen_k; + }(); + const ck_tile::index_t nhead_stride_o = (o_perm ? shape_seqlen_q * hdim_v : hdim_v); + const ck_tile::index_t batch_stride_q = (nhead * shape_seqlen_q * hdim_q); + const ck_tile::index_t batch_stride_k = nhead_k * shape_seqlen_k * hdim_q; + const ck_tile::index_t batch_stride_v = nhead_k * hdim_v * shape_seqlen_k; + const ck_tile::index_t batch_stride_o = (nhead * shape_seqlen_q * hdim_v); + + args.q_ptr = q_buf.GetDeviceBuffer(); + args.k_ptr = k_buf.GetDeviceBuffer(); + args.v_ptr = v_buf.GetDeviceBuffer(); + args.block_relation_onehot_ptr = block_relation_buf.GetDeviceBuffer(); + + args.batch = batch; + args.seqlen_q = shape_seqlen_q; + args.hdim_q = hdim_q; + args.hdim_v = hdim_v; + args.nhead_q = nhead; + args.nhead_k = nhead_k; + + args.stride_q = stride_q; + args.stride_k = stride_k; + args.stride_v = stride_v; + args.nhead_stride_q = nhead_stride_q; + args.nhead_stride_k = nhead_stride_k; + args.nhead_stride_v = nhead_stride_v; + args.batch_stride_q = batch_stride_q; + args.batch_stride_k = batch_stride_k; + args.batch_stride_v = batch_stride_v; + + args.o_ptr = o_buf.GetDeviceBuffer(); + + args.seqlen_k = shape_seqlen_k; + args.max_seqlen_q = max_seqlen_q; + + args.scale_s = scale_s; + + args.stride_o = stride_o; + args.nhead_stride_o = nhead_stride_o; + args.batch_stride_o = batch_stride_o; + + args.window_size_left = mask.left; + args.window_size_right = mask.right; + args.mask_type = static_cast(mask.type); + }; + + const auto init_traits = [&](auto& traits) { + traits.hdim_q = hdim_q; + traits.hdim_v = hdim_v; + traits.data_type = data_type; + traits.is_v_rowmajor = is_v_rowmajor; + traits.mask_type = mask.type; + }; + + fmha_jenga_fwd_traits fmha_traits; + init_traits(fmha_traits); + + fmha_jenga_fwd_args args; + init_args(args); + + sparge_jenga_fwd(fmha_traits, args, stream_config); + + o_buf.FromDevice(Y.data(), Y.get_element_space_size_in_bytes()); + + return Y; +} + +template ck_tile::HostTensor +jenga_sparge_attention(const ck_tile::HostTensor&, + const ck_tile::HostTensor&, + const ck_tile::HostTensor&, + const ck_tile::HostTensor&, + ck_tile::HostTensor&, + int, + int, + int, + int, + int, + int, + int, + bool, + bool, + int, + int, + int); + +template ck_tile::HostTensor +jenga_sparge_attention(const ck_tile::HostTensor&, + const ck_tile::HostTensor&, + const ck_tile::HostTensor&, + const ck_tile::HostTensor&, + ck_tile::HostTensor&, + int, + int, + int, + int, + int, + int, + int, + bool, + bool, + int, + int, + int); diff --git a/example/ck_tile/50_sparse_attn/jenga_sparge_attention.h b/example/ck_tile/50_sparse_attn/jenga_sparge_attention.h new file mode 100644 index 00000000000..6259fcc73cf --- /dev/null +++ b/example/ck_tile/50_sparse_attn/jenga_sparge_attention.h @@ -0,0 +1,27 @@ +// Copyright (c) Advanced Micro Devices, Inc., or its affiliates. +// SPDX-License-Identifier: MIT +#pragma once +#include +#include +#include "ck_tile/core.hpp" +#include "ck_tile/host/host_tensor.hpp" + +template +ck_tile::HostTensor +jenga_sparge_attention(const ck_tile::HostTensor& TQ, + const ck_tile::HostTensor& TK, + const ck_tile::HostTensor& TV, + const ck_tile::HostTensor& Tblock_relation_onehot, + ck_tile::HostTensor& Y, + int batch, + int nhead, + int nhead_k, + int seqlen_q, + int seqlen_k, + int hdim_q, + int hdim_v, + bool i_perm, + bool o_perm, + int max_seqlen_q, + int max_seqlen_k, + int log_level = 0); diff --git a/example/ck_tile/50_sparse_attn/test_sparge_jenga_sparse_attn.cpp b/example/ck_tile/50_sparse_attn/test_sparge_jenga_sparse_attn.cpp index 0bd664adf68..590e51db144 100644 --- a/example/ck_tile/50_sparse_attn/test_sparge_jenga_sparse_attn.cpp +++ b/example/ck_tile/50_sparse_attn/test_sparge_jenga_sparse_attn.cpp @@ -16,7 +16,7 @@ #include "ck_tile/host/reference/reference_blocked_attention.hpp" #include "ck_tile/core/utility/bit_cast.hpp" -#include "jenga_sparse_attention.h" +#include "jenga_sparge_attention.h" #include "sparge_tool.hpp" // ============================================================================ @@ -115,7 +115,7 @@ auto create_args(int argc, char* argv[]) .insert("repeat", "20", "benchmark iterations") .insert("kname", "0", "print kernel name") // Sparge-specific - .insert("blkq", "128", "Sparge BLKQ") + .insert("blkq", "64", "Sparge BLKQ") .insert("blkk", "128", "Sparge BLKK") .insert("simthreshd1", "0.6", "Sparge sim threshold") .insert("cdfthreshd", "0.98", "Sparge CDF threshold (used when topk < 0)") @@ -161,10 +161,10 @@ bool run_test(const ck_tile::ArgParser& arg_parser) if(hdim_v < 0) hdim_v = hdim_q; - if(blkq != 128 || blkk != 128 || hdim_q != 128 || hdim_v != 128) + if(blkq != 64 || blkk != 128 || hdim_q != 128 || hdim_v != 128) { std::cout << "\n>>> TEST SKIPPED <<<" << std::endl; - std::cout << "Jenga/VSA kernel instances are generated for BLKQ=BLKK=128, " + std::cout << "Sparge Jenga kernel instances are generated for BLKQ=64, BLKK=128, " "hdim_q=128, hdim_v=128 only." << std::endl; std::cout << "TEST SKIPPED" << std::endl; @@ -247,7 +247,7 @@ bool run_test(const ck_tile::ArgParser& arg_parser) { if(kname) { - jenga_sparse_attention(q_host, + jenga_sparge_attention(q_host, k_host, v_host, block_relation_onehot, @@ -268,7 +268,7 @@ bool run_test(const ck_tile::ArgParser& arg_parser) for(int i = 0; i < warmup; ++i) { - jenga_sparse_attention(q_host, + jenga_sparge_attention(q_host, k_host, v_host, block_relation_onehot, @@ -292,7 +292,7 @@ bool run_test(const ck_tile::ArgParser& arg_parser) for(int i = 0; i < repeat; ++i) { - jenga_sparse_attention(q_host, + jenga_sparge_attention(q_host, k_host, v_host, block_relation_onehot, diff --git a/example/ck_tile/50_sparse_attn/test_sparge_vsa_sparse_attn.cpp b/example/ck_tile/50_sparse_attn/test_sparge_vsa_sparse_attn.cpp index dd1d3e60bee..c0feb23e581 100644 --- a/example/ck_tile/50_sparse_attn/test_sparge_vsa_sparse_attn.cpp +++ b/example/ck_tile/50_sparse_attn/test_sparge_vsa_sparse_attn.cpp @@ -16,7 +16,7 @@ #include "ck_tile/host/reference/reference_blocked_attention.hpp" #include "ck_tile/core/utility/bit_cast.hpp" -#include "jenga_sparse_attention.h" +#include "vsa_sparge_attention.h" #include "sparge_tool.hpp" // ============================================================================ @@ -115,7 +115,7 @@ auto create_args(int argc, char* argv[]) .insert("repeat", "20", "benchmark iterations") .insert("kname", "0", "print kernel name") // Sparge-specific - .insert("blkq", "128", "Sparge BLKQ") + .insert("blkq", "64", "Sparge BLKQ") .insert("blkk", "128", "Sparge BLKK") .insert("simthreshd1", "0.6", "Sparge sim threshold") .insert("cdfthreshd", "0.98", "Sparge CDF threshold (used when topk < 0)") @@ -161,10 +161,10 @@ bool run_test(const ck_tile::ArgParser& arg_parser) if(hdim_v < 0) hdim_v = hdim_q; - if(blkq != 128 || blkk != 128 || hdim_q != 128 || hdim_v != 128) + if(blkq != 64 || blkk != 128 || hdim_q != 128 || hdim_v != 128) { std::cout << "\n>>> TEST SKIPPED <<<" << std::endl; - std::cout << "VSA kernel instances are generated for BLKQ=BLKK=128, " + std::cout << "Sparge VSA kernel instances are generated for BLKQ=64, BLKK=128, " "hdim_q=128, hdim_v=128 only." << std::endl; std::cout << "TEST SKIPPED" << std::endl; @@ -251,7 +251,7 @@ bool run_test(const ck_tile::ArgParser& arg_parser) { if(kname) { - vsa_sparse_attention(q_host, + vsa_sparge_attention(q_host, k_host, v_host, vsa_lut.lut, @@ -273,7 +273,7 @@ bool run_test(const ck_tile::ArgParser& arg_parser) for(int i = 0; i < warmup; ++i) { - vsa_sparse_attention(q_host, + vsa_sparge_attention(q_host, k_host, v_host, vsa_lut.lut, @@ -298,7 +298,7 @@ bool run_test(const ck_tile::ArgParser& arg_parser) for(int i = 0; i < repeat; ++i) { - vsa_sparse_attention(q_host, + vsa_sparge_attention(q_host, k_host, v_host, vsa_lut.lut, diff --git a/example/ck_tile/50_sparse_attn/vsa_sparge_attention.cpp b/example/ck_tile/50_sparse_attn/vsa_sparge_attention.cpp new file mode 100644 index 00000000000..5f9c2676ddb --- /dev/null +++ b/example/ck_tile/50_sparse_attn/vsa_sparge_attention.cpp @@ -0,0 +1,195 @@ +// Copyright (c) Advanced Micro Devices, Inc., or its affiliates. +// SPDX-License-Identifier: MIT +#include "vsa_sparge_attention.h" +#include "fmha_fwd_trek.hpp" +#include "ck_tile/core.hpp" +#include "ck_tile/host/host_tensor.hpp" +#include "ck_tile/host/device_memory.hpp" +#include + +template +ck_tile::HostTensor +vsa_sparge_attention(const ck_tile::HostTensor& TQ, + const ck_tile::HostTensor& TK, + const ck_tile::HostTensor& TV, + const ck_tile::HostTensor& TKV_block_idx, + const ck_tile::HostTensor& TKV_blocks, + ck_tile::HostTensor& Y, + int batch, + int nhead, + int nhead_k, + int seqlen_q, + int seqlen_k, + int hdim_q, + int hdim_v, + bool i_perm, + bool o_perm, + int max_seqlen_q, + int max_seqlen_k, + int log_level) +{ + static_assert(std::is_same_v || + std::is_same_v, + "VSA sparse attention supports fp16/bf16 only."); + std::string data_type = "fp16"; + if constexpr(std::is_same_v) + { + data_type = "bf16"; + } + + if(max_seqlen_q == 0) + max_seqlen_q = seqlen_q; + if(max_seqlen_k == 0) + max_seqlen_k = seqlen_k; + bool is_v_rowmajor = true; + float scale_s = 1.0 / ck_tile::sqrt(static_cast(hdim_q)); + std::string msk_str = "0"; + mask_info mask = mask_info::decode(msk_str, seqlen_q, seqlen_k); + + const ck_tile::index_t shape_seqlen_q = seqlen_q; + const ck_tile::index_t shape_seqlen_k = seqlen_k; + + ck_tile::stream_config stream_config{nullptr, + false, // time_kernel + log_level, + 0, + 1, + false}; + + ck_tile::DeviceMem q_buf(TQ.get_element_space_size_in_bytes()); + ck_tile::DeviceMem k_buf(TK.get_element_space_size_in_bytes()); + ck_tile::DeviceMem v_buf(TV.get_element_space_size_in_bytes()); + ck_tile::DeviceMem lut_buf(TKV_block_idx.get_element_space_size_in_bytes()); + ck_tile::DeviceMem valid_block_num_buf(TKV_blocks.get_element_space_size_in_bytes()); + ck_tile::DeviceMem o_buf(Y.get_element_space_size_in_bytes()); + + q_buf.ToDevice(TQ.data()); + k_buf.ToDevice(TK.data()); + v_buf.ToDevice(TV.data()); + lut_buf.ToDevice(TKV_block_idx.data()); + valid_block_num_buf.ToDevice(TKV_blocks.data()); + + const auto init_args = [&](auto& args) { + assert(nhead % nhead_k == 0); + const ck_tile::index_t stride_q = (i_perm ? hdim_q : nhead * hdim_q); + const ck_tile::index_t stride_k = (i_perm ? hdim_q : nhead_k * hdim_q); + const ck_tile::index_t stride_v = [&]() { + if(is_v_rowmajor) + return i_perm ? hdim_v : nhead_k * hdim_v; + else + return (i_perm ? shape_seqlen_k : nhead_k * shape_seqlen_k); + }(); + const ck_tile::index_t stride_o = (o_perm ? hdim_v : nhead * hdim_v); + const ck_tile::index_t nhead_stride_q = (i_perm ? shape_seqlen_q * hdim_q : hdim_q); + const ck_tile::index_t nhead_stride_k = i_perm ? shape_seqlen_k * hdim_q : hdim_q; + const ck_tile::index_t nhead_stride_v = [&]() { + if(is_v_rowmajor) + return i_perm ? shape_seqlen_k * hdim_v : hdim_v; + else + return i_perm ? hdim_v * shape_seqlen_k : shape_seqlen_k; + }(); + const ck_tile::index_t nhead_stride_o = (o_perm ? shape_seqlen_q * hdim_v : hdim_v); + const ck_tile::index_t batch_stride_q = (nhead * shape_seqlen_q * hdim_q); + const ck_tile::index_t batch_stride_k = nhead_k * shape_seqlen_k * hdim_q; + const ck_tile::index_t batch_stride_v = nhead_k * hdim_v * shape_seqlen_k; + const ck_tile::index_t batch_stride_o = (nhead * shape_seqlen_q * hdim_v); + + args.q_ptr = q_buf.GetDeviceBuffer(); + args.k_ptr = k_buf.GetDeviceBuffer(); + args.v_ptr = v_buf.GetDeviceBuffer(); + args.lut_ptr = lut_buf.GetDeviceBuffer(); + args.valid_block_num_ptr = valid_block_num_buf.GetDeviceBuffer(); + + args.batch = batch; + args.seqlen_q = shape_seqlen_q; + args.hdim_q = hdim_q; + args.hdim_v = hdim_v; + args.nhead_q = nhead; + args.nhead_k = nhead_k; + + args.stride_q = stride_q; + args.stride_k = stride_k; + args.stride_v = stride_v; + args.nhead_stride_q = nhead_stride_q; + args.nhead_stride_k = nhead_stride_k; + args.nhead_stride_v = nhead_stride_v; + args.batch_stride_q = batch_stride_q; + args.batch_stride_k = batch_stride_k; + args.batch_stride_v = batch_stride_v; + + args.o_ptr = o_buf.GetDeviceBuffer(); + + args.seqlen_k = shape_seqlen_k; + args.max_seqlen_q = max_seqlen_q; + + args.scale_s = scale_s; + + args.stride_o = stride_o; + args.nhead_stride_o = nhead_stride_o; + args.batch_stride_o = batch_stride_o; + + args.window_size_left = mask.left; + args.window_size_right = mask.right; + args.mask_type = static_cast(mask.type); + }; + + const auto init_traits = [&](auto& traits) { + traits.hdim_q = hdim_q; + traits.hdim_v = hdim_v; + traits.data_type = data_type; + traits.is_v_rowmajor = is_v_rowmajor; + traits.mask_type = mask.type; + }; + + fmha_vsa_fwd_traits fmha_traits; + init_traits(fmha_traits); + + fmha_vsa_fwd_args args; + init_args(args); + + sparge_vsa_fwd(fmha_traits, args, stream_config); + + o_buf.FromDevice(Y.data(), Y.get_element_space_size_in_bytes()); + + return Y; +} + +template ck_tile::HostTensor +vsa_sparge_attention(const ck_tile::HostTensor&, + const ck_tile::HostTensor&, + const ck_tile::HostTensor&, + const ck_tile::HostTensor&, + const ck_tile::HostTensor&, + ck_tile::HostTensor&, + int, + int, + int, + int, + int, + int, + int, + bool, + bool, + int, + int, + int); + +template ck_tile::HostTensor +vsa_sparge_attention(const ck_tile::HostTensor&, + const ck_tile::HostTensor&, + const ck_tile::HostTensor&, + const ck_tile::HostTensor&, + const ck_tile::HostTensor&, + ck_tile::HostTensor&, + int, + int, + int, + int, + int, + int, + int, + bool, + bool, + int, + int, + int); diff --git a/example/ck_tile/50_sparse_attn/vsa_sparge_attention.h b/example/ck_tile/50_sparse_attn/vsa_sparge_attention.h new file mode 100644 index 00000000000..d51a7e8c00b --- /dev/null +++ b/example/ck_tile/50_sparse_attn/vsa_sparge_attention.h @@ -0,0 +1,28 @@ +// Copyright (c) Advanced Micro Devices, Inc., or its affiliates. +// SPDX-License-Identifier: MIT +#pragma once +#include +#include +#include "ck_tile/core.hpp" +#include "ck_tile/host/host_tensor.hpp" + +template +ck_tile::HostTensor +vsa_sparge_attention(const ck_tile::HostTensor& TQ, + const ck_tile::HostTensor& TK, + const ck_tile::HostTensor& TV, + const ck_tile::HostTensor& TKV_block_idx, + const ck_tile::HostTensor& TKV_blocks, + ck_tile::HostTensor& Y, + int batch, + int nhead, + int nhead_k, + int seqlen_q, + int seqlen_k, + int hdim_q, + int hdim_v, + bool i_perm, + bool o_perm, + int max_seqlen_q, + int max_seqlen_k, + int log_level = 0); diff --git a/include/ck_tile/ops/sparse_attn/pipeline/block_fmha_pipeline_qr_ks_vs_async_vsa.hpp b/include/ck_tile/ops/sparse_attn/pipeline/block_fmha_pipeline_qr_ks_vs_async_vsa.hpp index 2b097ae5827..578ad7e6039 100644 --- a/include/ck_tile/ops/sparse_attn/pipeline/block_fmha_pipeline_qr_ks_vs_async_vsa.hpp +++ b/include/ck_tile/ops/sparse_attn/pipeline/block_fmha_pipeline_qr_ks_vs_async_vsa.hpp @@ -200,7 +200,7 @@ struct BlockFmhaPipelineQRKSVSAsyncVSA constexpr auto gemm_0 = Policy::template GetQKBlockGemm(); constexpr auto gemm_1 = Policy::template GetKVBlockGemm(); - int seqlen_k_start = kv_block_idx_ptr[0] * kM0; + int seqlen_k_start = kv_block_idx_ptr[0] * kN0; auto q_dram_window = make_tile_window(q_dram_block_window_tmp.get_bottom_tensor_view(), q_dram_block_window_tmp.get_window_lengths(), q_dram_block_window_tmp.get_window_origin(), From d1d457b82a63fdf6e68461194fa2c1098ace5f93 Mon Sep 17 00:00:00 2001 From: Gino Lu Date: Mon, 13 Apr 2026 03:34:08 -0400 Subject: [PATCH 03/16] Add sparge gpu pipeline in tile_example_sparge_vsa_sparse_attn --- example/ck_tile/50_sparse_attn/CMakeLists.txt | 34 +- .../50_sparse_attn/sparge_blockmap.cpp | 156 ++++++ .../ck_tile/50_sparse_attn/sparge_blockmap.h | 26 + .../50_sparse_attn/sparge_blockmap_inst.cpp | 88 +++ .../50_sparse_attn/sparge_blockmap_trek.hpp | 93 ++++ .../test_sparge_vsa_sparse_attn.cpp | 234 ++++++-- .../kernel/sparge_blockmap_kernel.hpp | 195 +++++++ .../pipeline/sparge_blockmap_pipeline.hpp | 521 ++++++++++++++++++ 8 files changed, 1296 insertions(+), 51 deletions(-) create mode 100644 example/ck_tile/50_sparse_attn/sparge_blockmap.cpp create mode 100644 example/ck_tile/50_sparse_attn/sparge_blockmap.h create mode 100644 example/ck_tile/50_sparse_attn/sparge_blockmap_inst.cpp create mode 100644 example/ck_tile/50_sparse_attn/sparge_blockmap_trek.hpp create mode 100644 include/ck_tile/ops/sparse_attn/kernel/sparge_blockmap_kernel.hpp create mode 100644 include/ck_tile/ops/sparse_attn/pipeline/sparge_blockmap_pipeline.hpp diff --git a/example/ck_tile/50_sparse_attn/CMakeLists.txt b/example/ck_tile/50_sparse_attn/CMakeLists.txt index 0ac86f6affa..169ed87ac3b 100644 --- a/example/ck_tile/50_sparse_attn/CMakeLists.txt +++ b/example/ck_tile/50_sparse_attn/CMakeLists.txt @@ -266,11 +266,41 @@ target_compile_options(${SPARGE_VSA_INSTANCES} PRIVATE -Wno-float-equal ) -# Sparge + VSA Example executable +# ============================================================================ +# Sparge BlockMap GPU Kernel (hand-written instantiation, no codegen) +# ============================================================================ +set(SPARGE_BLOCKMAP_INSTANCES "tile_sparge_blockmap_instances") + +add_library(${SPARGE_BLOCKMAP_INSTANCES} OBJECT EXCLUDE_FROM_ALL + ${CMAKE_CURRENT_LIST_DIR}/sparge_blockmap_inst.cpp + ${CMAKE_CURRENT_LIST_DIR}/sparge_blockmap.cpp +) +target_include_directories(${SPARGE_BLOCKMAP_INSTANCES} PRIVATE + ${CMAKE_CURRENT_LIST_DIR} + ${PROJECT_SOURCE_DIR}/include/ck_tile/ops/sparse_attn +) +set_source_files_properties( + ${CMAKE_CURRENT_LIST_DIR}/sparge_blockmap_inst.cpp + ${CMAKE_CURRENT_LIST_DIR}/sparge_blockmap.cpp + PROPERTIES LANGUAGE HIP +) +set_property(TARGET ${SPARGE_BLOCKMAP_INSTANCES} PROPERTY HIP_ARCHITECTURES ${INST_TARGETS}) + +target_compile_options(${SPARGE_BLOCKMAP_INSTANCES} PRIVATE + -DCK_TILE_USE_BUFFER_ADDRESSING_BUILTIN + -DCK_TILE_FMHA_FWD_FAST_EXP2 + -Wno-undefined-func-template + -Wno-float-equal +) + +# Sparge + VSA Example executable (now links blockmap kernel too) set(EXAMPLE_SPARGE_VSA_SPARSE_ATTN "tile_example_sparge_vsa_sparse_attn") message(DEBUG "adding example ${EXAMPLE_SPARGE_VSA_SPARSE_ATTN}") add_executable(${EXAMPLE_SPARGE_VSA_SPARSE_ATTN} EXCLUDE_FROM_ALL test_sparge_vsa_sparse_attn.cpp) -target_link_libraries(${EXAMPLE_SPARGE_VSA_SPARSE_ATTN} ${SPARGE_VSA_INSTANCES}) +target_link_libraries(${EXAMPLE_SPARGE_VSA_SPARSE_ATTN} + ${SPARGE_VSA_INSTANCES} + ${SPARGE_BLOCKMAP_INSTANCES} +) target_include_directories(${EXAMPLE_SPARGE_VSA_SPARSE_ATTN} PRIVATE ${CMAKE_CURRENT_LIST_DIR}) target_compile_options(${EXAMPLE_SPARGE_VSA_SPARSE_ATTN} PRIVATE -Wno-undefined-func-template diff --git a/example/ck_tile/50_sparse_attn/sparge_blockmap.cpp b/example/ck_tile/50_sparse_attn/sparge_blockmap.cpp new file mode 100644 index 00000000000..b9ac56c533c --- /dev/null +++ b/example/ck_tile/50_sparse_attn/sparge_blockmap.cpp @@ -0,0 +1,156 @@ +// Copyright (c) Advanced Micro Devices, Inc., or its affiliates. +// SPDX-License-Identifier: MIT +#include "sparge_blockmap.h" +#include "sparge_blockmap_trek.hpp" +#include "ck_tile/core.hpp" +#include "ck_tile/host/host_tensor.hpp" +#include "ck_tile/host/device_memory.hpp" +#include +#include + +template +sparge::VSALut sparge_blockmap_gpu(const ck_tile::HostTensor& TQ, + const ck_tile::HostTensor& TK, + ck_tile::HostTensor& block_map_out, + int batch, + int nhead_q, + int nhead_k, + int seqlen_q, + int seqlen_k, + int hdim_q, + bool i_perm, + float simthreshd1, + float cdfthreshd, + float topk, + int blkq, + int blkk, + int log_level) +{ + static_assert(std::is_same_v || + std::is_same_v, + "sparge_blockmap_gpu supports fp16/bf16 only."); + + std::string data_type = "fp16"; + if constexpr(std::is_same_v) + { + data_type = "bf16"; + } + + const ck_tile::index_t num_q_blocks = ck_tile::integer_divide_ceil(seqlen_q, blkq); + const ck_tile::index_t num_k_blocks = ck_tile::integer_divide_ceil(seqlen_k, blkk); + + const float scale = 1.0f / std::sqrt(static_cast(hdim_q)); + + // Allocate device memory + ck_tile::DeviceMem q_buf(TQ.get_element_space_size_in_bytes()); + ck_tile::DeviceMem k_buf(TK.get_element_space_size_in_bytes()); + + const std::size_t bmap_bytes = + static_cast(batch) * nhead_q * num_q_blocks * num_k_blocks * sizeof(uint8_t); + const std::size_t lut_bytes = + static_cast(batch) * nhead_q * num_q_blocks * num_k_blocks * sizeof(int32_t); + const std::size_t valid_bytes = + static_cast(batch) * nhead_q * num_q_blocks * sizeof(int32_t); + + ck_tile::DeviceMem bmap_buf(bmap_bytes); + ck_tile::DeviceMem lut_buf(lut_bytes); + ck_tile::DeviceMem valid_buf(valid_bytes); + + q_buf.ToDevice(TQ.data()); + k_buf.ToDevice(TK.data()); + bmap_buf.SetZero(); + lut_buf.SetZero(); + valid_buf.SetZero(); + + // Compute strides (assumes BHSD if i_perm, BSHD otherwise) + const ck_tile::index_t stride_q = i_perm ? hdim_q : nhead_q * hdim_q; + const ck_tile::index_t stride_k = i_perm ? hdim_q : nhead_k * hdim_q; + const ck_tile::index_t nhead_stride_q = + i_perm ? static_cast(seqlen_q) * hdim_q : hdim_q; + const ck_tile::index_t nhead_stride_k = + i_perm ? static_cast(seqlen_k) * hdim_q : hdim_q; + const ck_tile::index_t batch_stride_q = + static_cast(nhead_q) * seqlen_q * hdim_q; + const ck_tile::index_t batch_stride_k = + static_cast(nhead_k) * seqlen_k * hdim_q; + + ck_tile::stream_config stream_config{nullptr, false, log_level, 0, 1, false}; + + sparge_blockmap_args args; + args.q_ptr = q_buf.GetDeviceBuffer(); + args.k_ptr = k_buf.GetDeviceBuffer(); + args.batch = batch; + args.seqlen_q = seqlen_q; + args.seqlen_k = seqlen_k; + args.hdim_q = hdim_q; + args.nhead_q = nhead_q; + args.nhead_k = nhead_k; + args.stride_q = stride_q; + args.stride_k = stride_k; + args.nhead_stride_q = nhead_stride_q; + args.nhead_stride_k = nhead_stride_k; + args.batch_stride_q = batch_stride_q; + args.batch_stride_k = batch_stride_k; + args.simthreshd1 = simthreshd1; + args.cdfthreshd = cdfthreshd; + args.topk = topk; + args.scale = scale; + args.block_map_ptr = bmap_buf.GetDeviceBuffer(); + args.lut_ptr = lut_buf.GetDeviceBuffer(); + args.valid_block_num_ptr = valid_buf.GetDeviceBuffer(); + + sparge_blockmap_traits traits; + traits.data_type = data_type; + traits.hdim_q = hdim_q; + + sparge_blockmap_fwd(traits, args, stream_config); + + // Copy results back to host + bmap_buf.FromDevice(block_map_out.data(), bmap_bytes); + + sparge::VSALut vsa_lut{ + ck_tile::HostTensor({batch, nhead_q, num_q_blocks, num_k_blocks}), + ck_tile::HostTensor({batch, nhead_q, num_q_blocks}), + }; + lut_buf.FromDevice(vsa_lut.lut.data(), lut_bytes); + valid_buf.FromDevice(vsa_lut.valid_block_num.data(), valid_bytes); + + return vsa_lut; +} + +// Explicit template instantiations +template sparge::VSALut +sparge_blockmap_gpu(const ck_tile::HostTensor&, + const ck_tile::HostTensor&, + ck_tile::HostTensor&, + int, + int, + int, + int, + int, + int, + bool, + float, + float, + float, + int, + int, + int); + +template sparge::VSALut +sparge_blockmap_gpu(const ck_tile::HostTensor&, + const ck_tile::HostTensor&, + ck_tile::HostTensor&, + int, + int, + int, + int, + int, + int, + bool, + float, + float, + float, + int, + int, + int); diff --git a/example/ck_tile/50_sparse_attn/sparge_blockmap.h b/example/ck_tile/50_sparse_attn/sparge_blockmap.h new file mode 100644 index 00000000000..3057257ca14 --- /dev/null +++ b/example/ck_tile/50_sparse_attn/sparge_blockmap.h @@ -0,0 +1,26 @@ +// Copyright (c) Advanced Micro Devices, Inc., or its affiliates. +// SPDX-License-Identifier: MIT +#pragma once + +#include +#include "ck_tile/core.hpp" +#include "ck_tile/host/host_tensor.hpp" +#include "sparge_tool.hpp" + +template +sparge::VSALut sparge_blockmap_gpu(const ck_tile::HostTensor& TQ, + const ck_tile::HostTensor& TK, + ck_tile::HostTensor& block_map_out, + int batch, + int nhead_q, + int nhead_k, + int seqlen_q, + int seqlen_k, + int hdim_q, + bool i_perm, + float simthreshd1, + float cdfthreshd, + float topk, + int blkq, + int blkk, + int log_level = 0); diff --git a/example/ck_tile/50_sparse_attn/sparge_blockmap_inst.cpp b/example/ck_tile/50_sparse_attn/sparge_blockmap_inst.cpp new file mode 100644 index 00000000000..fbd18b9ff24 --- /dev/null +++ b/example/ck_tile/50_sparse_attn/sparge_blockmap_inst.cpp @@ -0,0 +1,88 @@ +// Copyright (c) Advanced Micro Devices, Inc., or its affiliates. +// SPDX-License-Identifier: MIT +// Hand-written template instantiation for SpargeBlockMapKernel (fp16, D=128). + +#include "sparge_blockmap_trek.hpp" +#include "ck_tile/ops/fmha/block/variants.hpp" + +#include + +// ============================================================================ +// Type configuration for block map kernel (reuses FmhaSparseFwdTypeConfig) +// ============================================================================ + +// fp16: D=128, kM0=64, kN0=128 +using bmap_fp16_block_tile = ck_tile::sequence<64, 128, 128, 128, 128, 128>; +// kM0 kN0 kK0 kN1 kK1 kQKHeaddim(D) + +using bmap_fp16_shape = + ck_tile::TileFmhaShape, // Gemm0BlockWarps + ck_tile::sequence<16, 16, 16>, // Gemm0WarpTile (unused by blockmap, but + // needed by shape) + ck_tile::sequence<4, 1, 1>, // Gemm1BlockWarps + ck_tile::sequence<16, 16, 16>, // Gemm1WarpTile + true>; // VLayout row-major + +using bmap_fp16_trait = ck_tile::TileFmhaTraits; // kIsVRowMajorSkip + +using bmap_fp16_variant = ck_tile::ComposedAttention<0, CK_TILE_FMHA_FWD_FAST_EXP2>; +using bmap_fp16_mask = ck_tile::GenericAttentionMask; + +using bmap_fp16_problem = ck_tile::BlockFmhaPipelineProblem; + +using bmap_fp16_pipeline = ck_tile::SpargeBlockMapPipeline; +using bmap_fp16_kernel = ck_tile::SpargeBlockMapKernel; + +// ============================================================================ +// Dispatch +// ============================================================================ + +float sparge_blockmap_fwd(sparge_blockmap_traits traits, + sparge_blockmap_args args, + const ck_tile::stream_config& s) +{ + if(traits.data_type == "fp16" && traits.hdim_q == 128) + { + using k_ = bmap_fp16_kernel; + if(s.log_level_ > 0) + std::cout << ", sparge_blockmap_fp16_d128" << std::flush; + auto [kargs, grids] = sparge_blockmap_create_kargs_and_grids(args); + const dim3 blocks = k_::BlockSize(); + constexpr ck_tile::index_t kBlockPerCu = k_::kBlockPerCu; + return ck_tile::launch_kernel( + s, ck_tile::make_kernel(k_{}, grids, blocks, 0, kargs)); + } + + if(s.log_level_ > 0) + std::cerr << "sparge_blockmap_fwd: unsupported config (data_type=" << traits.data_type + << ", hdim_q=" << traits.hdim_q << ")" << std::endl; + return -1.f; +} diff --git a/example/ck_tile/50_sparse_attn/sparge_blockmap_trek.hpp b/example/ck_tile/50_sparse_attn/sparge_blockmap_trek.hpp new file mode 100644 index 00000000000..1e7e33248a2 --- /dev/null +++ b/example/ck_tile/50_sparse_attn/sparge_blockmap_trek.hpp @@ -0,0 +1,93 @@ +// Copyright (c) Advanced Micro Devices, Inc., or its affiliates. +// SPDX-License-Identifier: MIT +#pragma once + +#include "ck_tile/core.hpp" +#include "ck_tile/host/kernel_launch.hpp" +#include "ck_tile/ops/common/tensor_layout.hpp" +#include "ck_tile/ops/fmha/pipeline/block_fmha_pipeline_problem.hpp" +#include "ck_tile/ops/fmha/pipeline/tile_fmha_shape.hpp" +#include "ck_tile/ops/sparse_attn/pipeline/sparge_blockmap_pipeline.hpp" +#include "ck_tile/ops/sparse_attn/kernel/sparge_blockmap_kernel.hpp" + +#include "fmha_fwd_trek.hpp" + +#include +#include + +// ============================================================================ +// Args and traits for sparge block map GPU kernel +// ============================================================================ +struct sparge_blockmap_args +{ + const void* q_ptr; + const void* k_ptr; + + ck_tile::index_t batch; + ck_tile::index_t seqlen_q; + ck_tile::index_t seqlen_k; + ck_tile::index_t hdim_q; + ck_tile::index_t nhead_q; + ck_tile::index_t nhead_k; + + ck_tile::index_t stride_q; + ck_tile::index_t stride_k; + ck_tile::index_t nhead_stride_q; + ck_tile::index_t nhead_stride_k; + ck_tile::index_t batch_stride_q; + ck_tile::index_t batch_stride_k; + + float simthreshd1; + float cdfthreshd; + float topk; + float scale; + + void* block_map_ptr; + void* lut_ptr; + void* valid_block_num_ptr; +}; + +struct sparge_blockmap_traits +{ + std::string data_type; + int hdim_q; +}; + +// ============================================================================ +// Create kernel args and grid dimensions +// ============================================================================ +template +auto sparge_blockmap_create_kargs_and_grids(sparge_blockmap_args args) +{ + assert(args.nhead_q % args.nhead_k == 0); + auto kargs = BlockMapKernel::MakeKargs(args.q_ptr, + args.k_ptr, + args.seqlen_q, + args.seqlen_k, + args.hdim_q, + args.nhead_q, + args.nhead_q / args.nhead_k, + args.stride_q, + args.stride_k, + args.nhead_stride_q, + args.nhead_stride_k, + args.batch_stride_q, + args.batch_stride_k, + args.simthreshd1, + args.cdfthreshd, + args.topk, + args.scale, + args.block_map_ptr, + args.lut_ptr, + args.valid_block_num_ptr); + + dim3 grids = BlockMapKernel::GridSize(args.batch, args.nhead_q, args.seqlen_q); + return ck_tile::make_tuple(kargs, grids); +} + +// ============================================================================ +// Hand-written template instantiation dispatch +// ============================================================================ +float sparge_blockmap_fwd(sparge_blockmap_traits traits, + sparge_blockmap_args args, + const ck_tile::stream_config& stream_config); diff --git a/example/ck_tile/50_sparse_attn/test_sparge_vsa_sparse_attn.cpp b/example/ck_tile/50_sparse_attn/test_sparge_vsa_sparse_attn.cpp index c0feb23e581..638a867b0f3 100644 --- a/example/ck_tile/50_sparse_attn/test_sparge_vsa_sparse_attn.cpp +++ b/example/ck_tile/50_sparse_attn/test_sparge_vsa_sparse_attn.cpp @@ -17,6 +17,7 @@ #include "ck_tile/core/utility/bit_cast.hpp" #include "vsa_sparge_attention.h" +#include "sparge_blockmap.h" #include "sparge_tool.hpp" // ============================================================================ @@ -198,53 +199,37 @@ bool run_test(const ck_tile::ArgParser& arg_parser) ck_tile::HostTensor output_host = o_perm ? ck_tile::HostTensor({batch, nhead, seqlen_q, hdim_v}) : ck_tile::HostTensor({batch, seqlen_q, nhead, hdim_v}); - ck_tile::HostTensor output_ref({batch, nhead, seqlen_q, hdim_v}); std::cout << "\nInitializing tensors..." << std::endl; ck_tile::FillUniformDistribution{-0.5f, 0.5f, seed}(q_host); ck_tile::FillUniformDistribution{-0.5f, 0.5f, seed + 1}(k_host); ck_tile::FillUniformDistribution{-0.5f, 0.5f, seed + 2}(v_host); - // Build block map using Sparge tool - std::cout << "Building Sparge block map..." << std::endl; - sparge::SpargeParams p; - p.BLKQ = static_cast(BLKQ); - p.BLKK = static_cast(BLKK); - p.simthreshd1 = simthreshd1; - p.cdfthreshd = cdfthreshd; - p.topk = topk; - p.i_perm = i_perm; - - ck_tile::HostTensor block_relation_onehot = - sparge::build_block_map_meansim(q_host, k_host, p); - - // Convert to VSA LUT (delta-encoded) + valid_block_num - std::cout << "Converting block map to VSA LUT (delta)..." << std::endl; - auto vsa_lut = sparge::block_map_to_vsa_lut_delta(block_relation_onehot); - - // Print actual sparsity (based on one-hot) - std::size_t total_blocks = 0; - std::size_t active_blocks = 0; - for(ck_tile::index_t b = 0; b < batch; ++b) - { - for(ck_tile::index_t h = 0; h < nhead; ++h) - { - for(ck_tile::index_t qb = 0; qb < num_q_blocks; ++qb) - { - for(ck_tile::index_t kb = 0; kb < num_k_blocks; ++kb) - { - total_blocks++; - if(block_relation_onehot(b, h, qb, kb) != 0) - active_blocks++; - } - } - } - } - float actual_sparsity = - 1.0f - static_cast(active_blocks) / static_cast(total_blocks); - std::cout << " Actual sparsity: " << actual_sparsity << " (" << active_blocks << "/" - << total_blocks << " blocks active)" << std::endl; - + // ================================================================== + // GPU: Build block map + VSA LUT in one kernel (always run) + // ================================================================== + std::cout << "Building Sparge block map + VSA LUT (GPU)..." << std::endl; + ck_tile::HostTensor block_map_gpu({batch, nhead, num_q_blocks, num_k_blocks}); + auto vsa_lut_gpu = sparge_blockmap_gpu(q_host, + k_host, + block_map_gpu, + batch, + nhead, + nhead_k, + seqlen_q, + seqlen_k, + hdim_q, + i_perm, + simthreshd1, + cdfthreshd, + topk, + static_cast(BLKQ), + static_cast(BLKK), + 0); + + // ================================================================== + // VSA sparse attention kernel (always run) + // ================================================================== std::cout << "\n--- Running VSA sparse attention kernel ---" << std::endl; try @@ -254,8 +239,8 @@ bool run_test(const ck_tile::ArgParser& arg_parser) vsa_sparge_attention(q_host, k_host, v_host, - vsa_lut.lut, - vsa_lut.valid_block_num, + vsa_lut_gpu.lut, + vsa_lut_gpu.valid_block_num, output_host, batch, nhead, @@ -276,8 +261,8 @@ bool run_test(const ck_tile::ArgParser& arg_parser) vsa_sparge_attention(q_host, k_host, v_host, - vsa_lut.lut, - vsa_lut.valid_block_num, + vsa_lut_gpu.lut, + vsa_lut_gpu.valid_block_num, output_host, batch, nhead, @@ -301,8 +286,8 @@ bool run_test(const ck_tile::ArgParser& arg_parser) vsa_sparge_attention(q_host, k_host, v_host, - vsa_lut.lut, - vsa_lut.valid_block_num, + vsa_lut_gpu.lut, + vsa_lut_gpu.valid_block_num, output_host, batch, nhead, @@ -332,17 +317,168 @@ bool run_test(const ck_tile::ArgParser& arg_parser) return false; } + // ================================================================== + // Sparsity statistics (always run, pure CPU read of HostTensor) + // ================================================================== + std::size_t total_blocks = 0; + std::size_t active_blocks = 0; + for(ck_tile::index_t b = 0; b < batch; ++b) + { + for(ck_tile::index_t h = 0; h < nhead; ++h) + { + for(ck_tile::index_t qb = 0; qb < num_q_blocks; ++qb) + { + for(ck_tile::index_t kb = 0; kb < num_k_blocks; ++kb) + { + total_blocks++; + if(block_map_gpu(b, h, qb, kb) != 0) + active_blocks++; + } + } + } + } + float actual_sparsity = + 1.0f - static_cast(active_blocks) / static_cast(total_blocks); + std::cout << "\n Actual sparsity: " << actual_sparsity << " (" << active_blocks << "/" + << total_blocks << " blocks active)" << std::endl; + + // ================================================================== + // Validation (only when -v=1) + // ================================================================== bool pass = true; if(do_validation) { std::cout << "\n--- Performing CPU validation ---" << std::endl; + + // CPU golden: block map + VSA LUT + std::cout << "Building Sparge block map (CPU golden)..." << std::endl; + sparge::SpargeParams p; + p.BLKQ = static_cast(BLKQ); + p.BLKK = static_cast(BLKK); + p.simthreshd1 = simthreshd1; + p.cdfthreshd = cdfthreshd; + p.topk = topk; + p.i_perm = i_perm; + + ck_tile::HostTensor block_relation_onehot = + sparge::build_block_map_meansim(q_host, k_host, p); + + std::cout << "Converting block map to VSA LUT (delta, CPU)..." << std::endl; + auto vsa_lut_cpu = sparge::block_map_to_vsa_lut_delta(block_relation_onehot); + + // Validate block map + std::cout << "\n--- Validating GPU block map vs CPU golden ---" << std::endl; + { + std::size_t bmap_mismatches = 0; + for(ck_tile::index_t b = 0; b < batch; ++b) + { + for(ck_tile::index_t h = 0; h < nhead; ++h) + { + for(ck_tile::index_t qb = 0; qb < num_q_blocks; ++qb) + { + for(ck_tile::index_t kb = 0; kb < num_k_blocks; ++kb) + { + if(block_map_gpu(b, h, qb, kb) != + block_relation_onehot(b, h, qb, kb)) + { + bmap_mismatches++; + if(bmap_mismatches <= 10) + { + std::cout + << " block_map mismatch at [" << b << "," << h << "," + << qb << "," << kb + << "]: GPU=" + << static_cast(block_map_gpu(b, h, qb, kb)) + << " CPU=" + << static_cast( + block_relation_onehot(b, h, qb, kb)) + << std::endl; + } + } + } + } + } + } + std::cout << " Block map mismatches: " << bmap_mismatches << " / " + << (batch * nhead * num_q_blocks * num_k_blocks) << std::endl; + if(bmap_mismatches > 0) + { + std::cout << ">>> GPU BLOCK MAP VALIDATION FAILED <<<" << std::endl; + pass = false; + } + else + { + std::cout << ">>> GPU BLOCK MAP VALIDATION PASSED <<<" << std::endl; + } + } + + // Validate VSA LUT + std::cout << "\n--- Validating GPU VSA LUT vs CPU golden ---" << std::endl; + { + std::size_t lut_mismatches = 0; + std::size_t valid_mismatches = 0; + for(ck_tile::index_t b = 0; b < batch; ++b) + { + for(ck_tile::index_t h = 0; h < nhead; ++h) + { + for(ck_tile::index_t qb = 0; qb < num_q_blocks; ++qb) + { + if(vsa_lut_gpu.valid_block_num(b, h, qb) != + vsa_lut_cpu.valid_block_num(b, h, qb)) + { + valid_mismatches++; + if(valid_mismatches <= 5) + { + std::cout + << " valid_block_num mismatch at [" << b << "," << h + << "," << qb + << "]: GPU=" << vsa_lut_gpu.valid_block_num(b, h, qb) + << " CPU=" << vsa_lut_cpu.valid_block_num(b, h, qb) + << std::endl; + } + } + for(ck_tile::index_t kb = 0; kb < num_k_blocks; ++kb) + { + if(vsa_lut_gpu.lut(b, h, qb, kb) != + vsa_lut_cpu.lut(b, h, qb, kb)) + { + lut_mismatches++; + if(lut_mismatches <= 10) + { + std::cout + << " LUT mismatch at [" << b << "," << h << "," << qb + << "," << kb + << "]: GPU=" << vsa_lut_gpu.lut(b, h, qb, kb) + << " CPU=" << vsa_lut_cpu.lut(b, h, qb, kb) + << std::endl; + } + } + } + } + } + } + std::cout << " LUT mismatches: " << lut_mismatches << std::endl; + std::cout << " valid_block_num mismatches: " << valid_mismatches << std::endl; + if(lut_mismatches == 0 && valid_mismatches == 0) + { + std::cout << ">>> GPU VSA LUT VALIDATION PASSED <<<" << std::endl; + } + else + { + std::cout << ">>> GPU VSA LUT VALIDATION FAILED <<<" << std::endl; + pass = false; + } + } + + // Validate attention output float scale = 1.0f / std::sqrt(static_cast(hdim_q)); - std::cout << "Computing reference output..." << std::endl; + std::cout << "\nComputing reference attention output..." << std::endl; auto q_ref = to_bhsd(q_host, i_perm); auto k_ref = to_bhsd(k_host, i_perm); auto v_ref = to_bhsd(v_host, i_perm); + ck_tile::HostTensor output_ref({batch, nhead, seqlen_q, hdim_v}); ck_tile::reference_blocked_attention( q_ref, k_ref, v_ref, block_relation_onehot, output_ref, BLKQ, BLKK, scale); @@ -374,7 +510,7 @@ bool run_test(const ck_tile::ArgParser& arg_parser) } } - std::cout << "\nValidation results:" << std::endl; + std::cout << "\nAttention validation results:" << std::endl; std::cout << " Max absolute difference: " << max_diff << std::endl; std::cout << " Max relative difference: " << max_rel_diff << std::endl; std::cout << " Number of mismatches: " << num_errors << " / " diff --git a/include/ck_tile/ops/sparse_attn/kernel/sparge_blockmap_kernel.hpp b/include/ck_tile/ops/sparse_attn/kernel/sparge_blockmap_kernel.hpp new file mode 100644 index 00000000000..ca177abf23a --- /dev/null +++ b/include/ck_tile/ops/sparse_attn/kernel/sparge_blockmap_kernel.hpp @@ -0,0 +1,195 @@ +// Copyright (c) Advanced Micro Devices, Inc., or its affiliates. +// SPDX-License-Identifier: MIT +#pragma once + +#include "ck_tile/core.hpp" +#include + +namespace ck_tile { + +template +struct SpargeBlockMapKernel +{ + using Pipeline = remove_cvref_t; + + static constexpr index_t kBlockSize = Pipeline::kBlockSize; + static constexpr index_t kBlockPerCu = Pipeline::kBlockPerCu; + + using QDataType = typename Pipeline::QDataType; + using KDataType = typename Pipeline::KDataType; + + static constexpr index_t kM0 = Pipeline::kM0; + static constexpr index_t kN0 = Pipeline::kN0; + static constexpr index_t D = Pipeline::D; + + static constexpr index_t kAlignment = 16 / sizeof(QDataType); + + struct Kargs + { + const void* q_ptr; + const void* k_ptr; + + index_t seqlen_q; + index_t seqlen_k; + index_t hdim_q; + + index_t nhead_q; + index_t nhead_ratio_qk; + + index_t stride_q; + index_t stride_k; + index_t nhead_stride_q; + index_t nhead_stride_k; + index_t batch_stride_q; + index_t batch_stride_k; + + float simthreshd1; + float cdfthreshd; + float topk; + float scale; + + void* block_map_ptr; + void* lut_ptr; + void* valid_block_num_ptr; + + index_t N_k; + }; + + CK_TILE_HOST static constexpr auto MakeKargs(const void* q_ptr, + const void* k_ptr, + index_t seqlen_q, + index_t seqlen_k, + index_t hdim_q, + index_t nhead_q, + index_t nhead_ratio_qk, + index_t stride_q, + index_t stride_k, + index_t nhead_stride_q, + index_t nhead_stride_k, + index_t batch_stride_q, + index_t batch_stride_k, + float simthreshd1, + float cdfthreshd, + float topk, + float scale, + void* block_map_ptr, + void* lut_ptr, + void* valid_block_num_ptr) + { + const index_t N_k = integer_divide_ceil(seqlen_k, kN0); + return Kargs{q_ptr, + k_ptr, + seqlen_q, + seqlen_k, + hdim_q, + nhead_q, + nhead_ratio_qk, + stride_q, + stride_k, + nhead_stride_q, + nhead_stride_k, + batch_stride_q, + batch_stride_k, + simthreshd1, + cdfthreshd, + topk, + scale, + block_map_ptr, + lut_ptr, + valid_block_num_ptr, + N_k}; + } + + CK_TILE_HOST static constexpr auto GridSize(index_t batch, index_t nhead_q, index_t seqlen_q) + { + const index_t Q_blk = integer_divide_ceil(seqlen_q, kM0); + return dim3(Q_blk, nhead_q, batch); + } + + CK_TILE_HOST static constexpr auto BlockSize() { return dim3(kBlockSize); } + + CK_TILE_DEVICE void operator()(Kargs kargs) const + { + const index_t qb = static_cast(blockIdx.x); + const index_t hq = static_cast(blockIdx.y); + const index_t b = static_cast(blockIdx.z); + + const index_t hk = hq / kargs.nhead_ratio_qk; + + // Q pointer for this (batch, head, q_block) + const auto* q_base = reinterpret_cast(kargs.q_ptr) + + b * kargs.batch_stride_q + hq * kargs.nhead_stride_q + + qb * kM0 * kargs.stride_q; + + // K pointer for this (batch, head_k) + const auto* k_base = reinterpret_cast(kargs.k_ptr) + + b * kargs.batch_stride_k + hk * kargs.nhead_stride_k; + + // Q DRAM view with OOB padding + const auto q_dram_naive = make_naive_tensor_view( + q_base, + make_tuple(kargs.seqlen_q - qb * kM0, D), + make_tuple(kargs.stride_q, 1), + number{}, + number<1>{}); + const auto q_dram = pad_tensor_view( + q_dram_naive, make_tuple(number{}, number{}), sequence{}); + + auto q_window = make_tile_window(q_dram, + make_tuple(number{}, number{}), + {0, 0}, + Pipeline::MakeQBlockDistribution()); + + // K DRAM view with OOB padding + const auto k_dram_naive = + make_naive_tensor_view(k_base, + make_tuple(kargs.seqlen_k, D), + make_tuple(kargs.stride_k, 1), + number{}, + number<1>{}); + const auto k_dram = pad_tensor_view( + k_dram_naive, make_tuple(number{}, number{}), sequence{}); + + auto k_window = make_tile_window(k_dram, + make_tuple(number{}, number{}), + {0, 0}, + Pipeline::MakeKBlockDistribution()); + + // Output pointers for this (batch, head, q_block) + const index_t N_k = kargs.N_k; + const index_t bmap_offset = + (b * kargs.nhead_q + hq) * integer_divide_ceil(kargs.seqlen_q, kM0) * N_k + qb * N_k; + auto* bmap_ptr = reinterpret_cast(kargs.block_map_ptr) + bmap_offset; + + int32_t* lut_out = nullptr; + int32_t* valid_out = nullptr; + if(kargs.lut_ptr != nullptr) + { + lut_out = reinterpret_cast(kargs.lut_ptr) + bmap_offset; + const index_t valid_offset = + (b * kargs.nhead_q + hq) * integer_divide_ceil(kargs.seqlen_q, kM0) + qb; + valid_out = reinterpret_cast(kargs.valid_block_num_ptr) + valid_offset; + } + + // Shared memory + __shared__ char smem[Pipeline::GetSmemSize()]; + + Pipeline{}(q_window, + k_window, + kargs.seqlen_q, + kargs.seqlen_k, + qb, + N_k, + kargs.nhead_ratio_qk, + kargs.simthreshd1, + kargs.cdfthreshd, + kargs.topk, + kargs.scale, + bmap_ptr, + lut_out, + valid_out, + static_cast(smem)); + } +}; + +} // namespace ck_tile diff --git a/include/ck_tile/ops/sparse_attn/pipeline/sparge_blockmap_pipeline.hpp b/include/ck_tile/ops/sparse_attn/pipeline/sparge_blockmap_pipeline.hpp new file mode 100644 index 00000000000..222e73c60e2 --- /dev/null +++ b/include/ck_tile/ops/sparse_attn/pipeline/sparge_blockmap_pipeline.hpp @@ -0,0 +1,521 @@ +// Copyright (c) Advanced Micro Devices, Inc., or its affiliates. +// SPDX-License-Identifier: MIT +#pragma once + +#include "ck_tile/core.hpp" +#include "ck_tile/ops/reduce.hpp" + +namespace ck_tile { + +template +struct SpargeBlockMapPipeline +{ + using Problem = remove_cvref_t; + using QDataType = remove_cvref_t; + using KDataType = remove_cvref_t; + using BlockFmhaShape = remove_cvref_t; + + static constexpr index_t kBlockSize = Problem::kBlockSize; + static constexpr index_t kM0 = BlockFmhaShape::kM0; + static constexpr index_t kN0 = BlockFmhaShape::kN0; + static constexpr index_t D = BlockFmhaShape::kQKHeaddim; + static constexpr index_t NumWarps = BlockFmhaShape::NumWarps; + static constexpr index_t WarpSize = get_warp_size(); + + static constexpr index_t KPerThread = 16 / sizeof(QDataType); + static constexpr index_t KThreads = D / KPerThread; + static constexpr index_t SeqThreadPerWarp = WarpSize / KThreads; + static constexpr index_t MPerThread = kM0 / (SeqThreadPerWarp * NumWarps); + static constexpr index_t NPerThread = kN0 / (SeqThreadPerWarp * NumWarps); + + static constexpr index_t kBlockPerCu = 1; + static constexpr index_t kMaxKBlocks = 1024; + + // LDS layout (non-overlapping, all used simultaneously in Phase 2): + // [0 .. kReduceBytes) cross-warp reduction scratch + // [kScoreOffset ..) scores[N_k] + // [kBmapOffset ..) block_map[N_k] + // [kSmallOffset ..) Phase 3 argmax scratch (2*NumWarps floats) + static constexpr index_t kReduceBytes = NumWarps * D * sizeof(float); + static constexpr index_t kScoreOffset = kReduceBytes; + static constexpr index_t kBmapOffset = kScoreOffset + kMaxKBlocks * sizeof(float); + static constexpr index_t kSmallOffset = kBmapOffset + kMaxKBlocks * sizeof(uint8_t); + + CK_TILE_HOST_DEVICE static constexpr index_t GetSmemSize() + { + return kSmallOffset + 2 * NumWarps * sizeof(float); + } + + CK_TILE_HOST_DEVICE static constexpr auto MakeQBlockDistribution() + { + return make_static_tile_distribution( + tile_distribution_encoding, + tuple, + sequence>, + tuple, sequence<1, 2>>, + tuple, sequence<2, 0>>, + sequence<1, 2>, + sequence<0, 1>>{}); + } + + CK_TILE_HOST_DEVICE static constexpr auto MakeKBlockDistribution() + { + return make_static_tile_distribution( + tile_distribution_encoding, + tuple, + sequence>, + tuple, sequence<1, 2>>, + tuple, sequence<2, 0>>, + sequence<1, 2>, + sequence<0, 1>>{}); + } + + // Extract tile data into a local float array via static_for (compile-time indices). + template + CK_TILE_DEVICE static void tile_to_float(const Tile& tile, float (&out)[BufSize]) + { + static_assert(Tile::get_thread_buffer_size() == BufSize); + const auto& buf = tile.get_thread_buffer(); + static_for<0, BufSize, 1>{}([&](auto i) { out[i.value] = type_convert(buf[i]); }); + } + + // Column-wise (dim=0) sum: accumulate SeqPerThread rows into KPerThread partial sums, + // then xor-shuffle across m_idx within warp. + template + CK_TILE_DEVICE static void column_reduce_thread_and_warp(const float* __restrict__ data, + float (&col_acc)[KPerThread]) + { + for(index_t k = 0; k < KPerThread; ++k) + col_acc[k] = 0.f; + + for(index_t m = 0; m < SeqPerThread; ++m) + for(index_t k = 0; k < KPerThread; ++k) + col_acc[k] += data[m * KPerThread + k]; + + for(index_t stride = KThreads; stride < WarpSize; stride *= 2) + for(index_t k = 0; k < KPerThread; ++k) + col_acc[k] += warp_shuffle(col_acc[k], __lane_id() ^ stride); + } + + // Cross-warp LDS reduction for column sums. + CK_TILE_DEVICE static void column_reduce_cross_warp(float (&col_acc)[KPerThread], + float* __restrict__ smem_reduce) + { + const index_t tid = static_cast(threadIdx.x); + const index_t warp_id = tid / WarpSize; + const index_t lane_id = tid % WarpSize; + const index_t k_idx = lane_id % KThreads; + const index_t m_idx = lane_id / KThreads; + + if(m_idx == 0) + for(index_t k = 0; k < KPerThread; ++k) + smem_reduce[warp_id * D + k_idx * KPerThread + k] = col_acc[k]; + __syncthreads(); + + for(index_t k = 0; k < KPerThread; ++k) + col_acc[k] = 0.f; + for(index_t w = 0; w < NumWarps; ++w) + for(index_t k = 0; k < KPerThread; ++k) + col_acc[k] += smem_reduce[w * D + k_idx * KPerThread + k]; + __syncthreads(); + } + + // Compute ||v||^2 per row: sum along KPerThread then xor-shuffle across k_idx. + template + CK_TILE_DEVICE static void row_reduce_sq_norm(const float* __restrict__ data, + float (&row_norms)[SeqPerThread], + index_t actual_seq) + { + const index_t tid = static_cast(threadIdx.x); + const index_t warp_id = tid / WarpSize; + const index_t m_idx = (tid % WarpSize) / KThreads; + + for(index_t m = 0; m < SeqPerThread; ++m) + { + float sq = 0.f; + for(index_t k = 0; k < KPerThread; ++k) + { + float v = data[m * KPerThread + k]; + sq += v * v; + } + for(index_t stride = 1; stride < KThreads; stride *= 2) + sq += warp_shuffle(sq, __lane_id() ^ stride); + + index_t gsq = m * (SeqThreadPerWarp * NumWarps) + warp_id * SeqThreadPerWarp + m_idx; + row_norms[m] = (gsq < actual_seq) ? sq : 0.f; + } + } + + // Column reduce of normalised rows: sum_hat[d] = sum_i data[i,d] / ||data[i,:]||. + template + CK_TILE_DEVICE static void column_reduce_normalised(const float* __restrict__ data, + const float* __restrict__ row_norms, + float (&col_acc)[KPerThread], + index_t actual_seq) + { + const index_t tid = static_cast(threadIdx.x); + const index_t warp_id = tid / WarpSize; + const index_t m_idx = (tid % WarpSize) / KThreads; + + for(index_t k = 0; k < KPerThread; ++k) + col_acc[k] = 0.f; + + for(index_t m = 0; m < SeqPerThread; ++m) + { + float inv_norm = (row_norms[m] > 0.f) ? (1.0f / __builtin_sqrtf(row_norms[m])) : 0.f; + index_t gsq = m * (SeqThreadPerWarp * NumWarps) + warp_id * SeqThreadPerWarp + m_idx; + if(gsq < actual_seq) + for(index_t k = 0; k < KPerThread; ++k) + col_acc[k] += data[m * KPerThread + k] * inv_norm; + } + + for(index_t stride = KThreads; stride < WarpSize; stride *= 2) + for(index_t k = 0; k < KPerThread; ++k) + col_acc[k] += warp_shuffle(col_acc[k], __lane_id() ^ stride); + } + + // Scalar reduce across k_idx lanes (within warp). + CK_TILE_DEVICE static float reduce_across_k(float v) + { + for(index_t stride = 1; stride < KThreads; stride *= 2) + v += warp_shuffle(v, __lane_id() ^ stride); + return v; + } + + // Full-block scalar reduce (warp xor + cross-warp LDS). + CK_TILE_DEVICE static float block_reduce_sum(float v, float* smem_small) + { + const index_t tid = static_cast(threadIdx.x); + const index_t warp_id = tid / WarpSize; + const index_t lane_id = tid % WarpSize; + + for(index_t stride = 1; stride < WarpSize; stride *= 2) + v += warp_shuffle(v, __lane_id() ^ stride); + if(lane_id == 0) + smem_small[warp_id] = v; + __syncthreads(); + if(tid == 0) + { + float s = 0.f; + for(index_t w = 0; w < NumWarps; ++w) + s += smem_small[w]; + smem_small[0] = s; + } + __syncthreads(); + return smem_small[0]; + } + + CK_TILE_DEVICE static float block_reduce_max(float v, float* smem_small) + { + const index_t tid = static_cast(threadIdx.x); + const index_t warp_id = tid / WarpSize; + const index_t lane_id = tid % WarpSize; + + for(index_t stride = 1; stride < WarpSize; stride *= 2) + v = max(v, warp_shuffle(v, __lane_id() ^ stride)); + if(lane_id == 0) + smem_small[warp_id] = v; + __syncthreads(); + if(tid == 0) + { + float s = smem_small[0]; + for(index_t w = 1; w < NumWarps; ++w) + s = max(s, smem_small[w]); + smem_small[0] = s; + } + __syncthreads(); + return smem_small[0]; + } + + // ====================================================================== + template + CK_TILE_DEVICE void operator()(const QWindowType& q_window_in, + const KWindowType& k_window_in, + index_t seqlen_q, + index_t seqlen_k, + index_t qb, + index_t N_k, + index_t /*nhead_ratio_qk*/, + float simthreshd1, + float cdfthreshd, + float topk, + float scale, + uint8_t* block_map_ptr, + int32_t* lut_ptr, + int32_t* valid_block_num_ptr, + void* smem_ptr) const + { + const index_t tid = static_cast(threadIdx.x); + + auto* smem_float = reinterpret_cast(smem_ptr); + auto* smem_scores = + reinterpret_cast(reinterpret_cast(smem_ptr) + kScoreOffset); + auto* smem_bmap = + reinterpret_cast(reinterpret_cast(smem_ptr) + kBmapOffset); + auto* smem_small = + reinterpret_cast(reinterpret_cast(smem_ptr) + kSmallOffset); + + const index_t bs_q = min(static_cast(kM0), seqlen_q - qb * kM0); + const float inv_bs_q = (bs_q > 0) ? (1.0f / static_cast(bs_q)) : 0.f; + + // ================================================================== + // Phase 1: Q Block Statistics + // ================================================================== + auto q_tile = load_tile(q_window_in); + + float q_data[MPerThread * KPerThread]; + tile_to_float(q_tile, q_data); + + // 1a. L2 norm per token + float psq[MPerThread]; + row_reduce_sq_norm(q_data, psq, bs_q); + + // 1b. Column sum -> mean + float pooled_q_mean[KPerThread]; + column_reduce_thread_and_warp(q_data, pooled_q_mean); + column_reduce_cross_warp(pooled_q_mean, smem_float); + for(index_t k = 0; k < KPerThread; ++k) + pooled_q_mean[k] *= inv_bs_q; + + // 1c. Normalised sum_hat + float sum_hat[KPerThread]; + column_reduce_normalised(q_data, psq, sum_hat, bs_q); + column_reduce_cross_warp(sum_hat, smem_float); + + // 1d. sim_q = ||sum_hat||^2 / bs_q^2 + float sh_sq = 0.f; + for(index_t k = 0; k < KPerThread; ++k) + sh_sq += sum_hat[k] * sum_hat[k]; + sh_sq = reduce_across_k(sh_sq); + const float denom_q = static_cast(bs_q) * static_cast(bs_q); + const bool sim_q = (denom_q > 0.f) && ((sh_sq / denom_q) > simthreshd1); + + // Not similar → force all K blocks ON, early exit + if(!sim_q) + { + for(index_t i = tid; i < N_k; i += kBlockSize) + block_map_ptr[i] = 1; + + if(lut_ptr != nullptr && tid == 0) + { + int32_t valid = 0, prev = 0; + for(index_t kb = 0; kb < N_k; ++kb) + { + lut_ptr[valid] = static_cast(kb) - prev; + prev = static_cast(kb); + ++valid; + } + for(index_t i = valid; i < N_k; ++i) + lut_ptr[i] = 0; + *valid_block_num_ptr = valid; + } + return; + } + + // ================================================================== + // Phase 2: K Block Loop + // ================================================================== + for(index_t i = tid; i < N_k; i += kBlockSize) + smem_bmap[i] = 0; + __syncthreads(); + + auto k_window = k_window_in; + + for(index_t kb = 0; kb < N_k; ++kb) + { + const index_t bs_k = min(static_cast(kN0), seqlen_k - kb * kN0); + const float inv_bs_k = (bs_k > 0) ? (1.0f / static_cast(bs_k)) : 0.f; + + auto k_tile = load_tile(k_window); + + float k_data[NPerThread * KPerThread]; + tile_to_float(k_tile, k_data); + + // K mean + float pooled_k_mean[KPerThread]; + column_reduce_thread_and_warp(k_data, pooled_k_mean); + column_reduce_cross_warp(pooled_k_mean, smem_float); + for(index_t k = 0; k < KPerThread; ++k) + pooled_k_mean[k] *= inv_bs_k; + + // dot(pooled_q_mean, pooled_k_mean) + float dot = 0.f; + for(index_t k = 0; k < KPerThread; ++k) + dot += pooled_q_mean[k] * pooled_k_mean[k]; + dot = reduce_across_k(dot); + + // K L2 norms + normalised sum_hat + float k_psq[NPerThread]; + row_reduce_sq_norm(k_data, k_psq, bs_k); + + float k_sum_hat[KPerThread]; + column_reduce_normalised(k_data, k_psq, k_sum_hat, bs_k); + column_reduce_cross_warp(k_sum_hat, smem_float); + + // sim_k + float ksh_sq = 0.f; + for(index_t k = 0; k < KPerThread; ++k) + ksh_sq += k_sum_hat[k] * k_sum_hat[k]; + ksh_sq = reduce_across_k(ksh_sq); + const float denom_k = static_cast(bs_k) * static_cast(bs_k); + const bool sim_k = (denom_k > 0.f) && ((ksh_sq / denom_k) > simthreshd1); + + if(tid == 0) + { + if(!sim_k) + { + smem_bmap[kb] = 1; + smem_scores[kb] = -numeric::infinity(); + } + else + { + smem_scores[kb] = dot * scale; + } + } + __syncthreads(); + + move_tile_window(k_window, {kN0, 0}); + } + + // ================================================================== + // Phase 3: Softmax + Selection + // ================================================================== + + // max + float lmax = -numeric::infinity(); + for(index_t i = tid; i < N_k; i += kBlockSize) + lmax = max(lmax, smem_scores[i]); + const float max_score = block_reduce_max(lmax, smem_small); + + // exp + sum + float lsum = 0.f; + for(index_t i = tid; i < N_k; i += kBlockSize) + { + float e = (smem_scores[i] > -numeric::infinity()) + ? __builtin_expf(smem_scores[i] - max_score) + : 0.f; + smem_scores[i] = e; + lsum += e; + } + const float sum_exp = block_reduce_sum(lsum, smem_small); + + // normalise + const float inv_sum = (sum_exp > 0.f) ? (1.0f / sum_exp) : 0.f; + for(index_t i = tid; i < N_k; i += kBlockSize) + smem_scores[i] *= inv_sum; + __syncthreads(); + + // Selection: iterative argmax + index_t num_to_select = + (topk > 0.f) + ? max(static_cast(1), static_cast(topk * static_cast(N_k))) + : N_k; + + float cumulative_prob = 0.f; + for(index_t round = 0; round < num_to_select; ++round) + { + // thread-local argmax + float best_val = -1.f; + index_t best_idx = 0; + for(index_t i = tid; i < N_k; i += kBlockSize) + { + if(smem_scores[i] > best_val || (smem_scores[i] == best_val && i < best_idx)) + { + best_val = smem_scores[i]; + best_idx = i; + } + } + + // warp argmax + for(index_t stride = 1; stride < WarpSize; stride *= 2) + { + float rv = warp_shuffle(best_val, __lane_id() ^ stride); + index_t ri = warp_shuffle(best_idx, __lane_id() ^ stride); + if(rv > best_val || (rv == best_val && ri < best_idx)) + { + best_val = rv; + best_idx = ri; + } + } + + // cross-warp argmax via LDS + const index_t lane_id = tid % WarpSize; + const index_t warp_id = tid / WarpSize; + if(lane_id == 0) + { + smem_small[warp_id] = best_val; + smem_small[NumWarps + warp_id] = bit_cast(static_cast(best_idx)); + } + __syncthreads(); + + if(tid == 0) + { + float bv = smem_small[0]; + index_t bi = bit_cast(smem_small[NumWarps]); + for(index_t w = 1; w < NumWarps; ++w) + { + float wv = smem_small[w]; + index_t wi = bit_cast(smem_small[NumWarps + w]); + if(wv > bv || (wv == bv && wi < bi)) + { + bv = wv; + bi = wi; + } + } + smem_small[0] = bv; + smem_small[1] = bit_cast(static_cast(bi)); + } + __syncthreads(); + + float g_val = smem_small[0]; + index_t g_idx = bit_cast(smem_small[1]); + + if(g_val <= 0.f) + break; + + if(tid == 0) + { + smem_bmap[g_idx] = 1; + smem_scores[g_idx] = -1.f; + } + __syncthreads(); + + if(topk > 0.f) + { + if(round + 1 >= num_to_select) + break; + } + else + { + cumulative_prob += g_val; + if(cumulative_prob >= cdfthreshd) + break; + } + } + + // ================================================================== + // Write outputs to global memory + // ================================================================== + for(index_t i = tid; i < N_k; i += kBlockSize) + block_map_ptr[i] = smem_bmap[i]; + + if(lut_ptr != nullptr && tid == 0) + { + int32_t valid = 0, prev = 0; + for(index_t kb = 0; kb < N_k; ++kb) + { + if(smem_bmap[kb] != 0) + { + lut_ptr[valid] = static_cast(kb) - prev; + prev = static_cast(kb); + ++valid; + } + } + for(index_t i = valid; i < N_k; ++i) + lut_ptr[i] = 0; + *valid_block_num_ptr = valid; + } + } +}; + +} // namespace ck_tile From c7e6e4f616b483f2a9aafd3e8d00238f02de77e5 Mon Sep 17 00:00:00 2001 From: Gino Lu Date: Tue, 14 Apr 2026 10:11:00 -0400 Subject: [PATCH 04/16] fix extra host side operations. --- example/ck_tile/50_sparse_attn/CMakeLists.txt | 4 - .../50_sparse_attn/sparge_blockmap.cpp | 156 --------- .../ck_tile/50_sparse_attn/sparge_blockmap.h | 26 -- .../test_sparge_vsa_sparse_attn.cpp | 296 ++++++++++-------- .../50_sparse_attn/vsa_sparge_attention.cpp | 195 ------------ .../50_sparse_attn/vsa_sparge_attention.h | 28 -- 6 files changed, 164 insertions(+), 541 deletions(-) delete mode 100644 example/ck_tile/50_sparse_attn/sparge_blockmap.cpp delete mode 100644 example/ck_tile/50_sparse_attn/sparge_blockmap.h delete mode 100644 example/ck_tile/50_sparse_attn/vsa_sparge_attention.cpp delete mode 100644 example/ck_tile/50_sparse_attn/vsa_sparge_attention.h diff --git a/example/ck_tile/50_sparse_attn/CMakeLists.txt b/example/ck_tile/50_sparse_attn/CMakeLists.txt index 169ed87ac3b..f234f631b6b 100644 --- a/example/ck_tile/50_sparse_attn/CMakeLists.txt +++ b/example/ck_tile/50_sparse_attn/CMakeLists.txt @@ -249,14 +249,12 @@ set(SPARGE_VSA_INSTANCES "tile_sparge_vsa_instances") add_library(${SPARGE_VSA_INSTANCES} OBJECT EXCLUDE_FROM_ALL ${SPARGE_VSA_GEN_BLOBS} - ${CMAKE_CURRENT_LIST_DIR}/vsa_sparge_attention.cpp ) target_include_directories(${SPARGE_VSA_INSTANCES} PRIVATE ${CMAKE_CURRENT_LIST_DIR} ${PROJECT_SOURCE_DIR}/include/ck_tile/ops/sparse_attn ) set_source_files_properties(${SPARGE_VSA_GEN_BLOBS} PROPERTIES LANGUAGE HIP) -set_source_files_properties(${CMAKE_CURRENT_LIST_DIR}/vsa_sparge_attention.cpp PROPERTIES LANGUAGE HIP) set_property(TARGET ${SPARGE_VSA_INSTANCES} PROPERTY HIP_ARCHITECTURES ${INST_TARGETS}) target_compile_options(${SPARGE_VSA_INSTANCES} PRIVATE @@ -273,7 +271,6 @@ set(SPARGE_BLOCKMAP_INSTANCES "tile_sparge_blockmap_instances") add_library(${SPARGE_BLOCKMAP_INSTANCES} OBJECT EXCLUDE_FROM_ALL ${CMAKE_CURRENT_LIST_DIR}/sparge_blockmap_inst.cpp - ${CMAKE_CURRENT_LIST_DIR}/sparge_blockmap.cpp ) target_include_directories(${SPARGE_BLOCKMAP_INSTANCES} PRIVATE ${CMAKE_CURRENT_LIST_DIR} @@ -281,7 +278,6 @@ target_include_directories(${SPARGE_BLOCKMAP_INSTANCES} PRIVATE ) set_source_files_properties( ${CMAKE_CURRENT_LIST_DIR}/sparge_blockmap_inst.cpp - ${CMAKE_CURRENT_LIST_DIR}/sparge_blockmap.cpp PROPERTIES LANGUAGE HIP ) set_property(TARGET ${SPARGE_BLOCKMAP_INSTANCES} PROPERTY HIP_ARCHITECTURES ${INST_TARGETS}) diff --git a/example/ck_tile/50_sparse_attn/sparge_blockmap.cpp b/example/ck_tile/50_sparse_attn/sparge_blockmap.cpp deleted file mode 100644 index b9ac56c533c..00000000000 --- a/example/ck_tile/50_sparse_attn/sparge_blockmap.cpp +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright (c) Advanced Micro Devices, Inc., or its affiliates. -// SPDX-License-Identifier: MIT -#include "sparge_blockmap.h" -#include "sparge_blockmap_trek.hpp" -#include "ck_tile/core.hpp" -#include "ck_tile/host/host_tensor.hpp" -#include "ck_tile/host/device_memory.hpp" -#include -#include - -template -sparge::VSALut sparge_blockmap_gpu(const ck_tile::HostTensor& TQ, - const ck_tile::HostTensor& TK, - ck_tile::HostTensor& block_map_out, - int batch, - int nhead_q, - int nhead_k, - int seqlen_q, - int seqlen_k, - int hdim_q, - bool i_perm, - float simthreshd1, - float cdfthreshd, - float topk, - int blkq, - int blkk, - int log_level) -{ - static_assert(std::is_same_v || - std::is_same_v, - "sparge_blockmap_gpu supports fp16/bf16 only."); - - std::string data_type = "fp16"; - if constexpr(std::is_same_v) - { - data_type = "bf16"; - } - - const ck_tile::index_t num_q_blocks = ck_tile::integer_divide_ceil(seqlen_q, blkq); - const ck_tile::index_t num_k_blocks = ck_tile::integer_divide_ceil(seqlen_k, blkk); - - const float scale = 1.0f / std::sqrt(static_cast(hdim_q)); - - // Allocate device memory - ck_tile::DeviceMem q_buf(TQ.get_element_space_size_in_bytes()); - ck_tile::DeviceMem k_buf(TK.get_element_space_size_in_bytes()); - - const std::size_t bmap_bytes = - static_cast(batch) * nhead_q * num_q_blocks * num_k_blocks * sizeof(uint8_t); - const std::size_t lut_bytes = - static_cast(batch) * nhead_q * num_q_blocks * num_k_blocks * sizeof(int32_t); - const std::size_t valid_bytes = - static_cast(batch) * nhead_q * num_q_blocks * sizeof(int32_t); - - ck_tile::DeviceMem bmap_buf(bmap_bytes); - ck_tile::DeviceMem lut_buf(lut_bytes); - ck_tile::DeviceMem valid_buf(valid_bytes); - - q_buf.ToDevice(TQ.data()); - k_buf.ToDevice(TK.data()); - bmap_buf.SetZero(); - lut_buf.SetZero(); - valid_buf.SetZero(); - - // Compute strides (assumes BHSD if i_perm, BSHD otherwise) - const ck_tile::index_t stride_q = i_perm ? hdim_q : nhead_q * hdim_q; - const ck_tile::index_t stride_k = i_perm ? hdim_q : nhead_k * hdim_q; - const ck_tile::index_t nhead_stride_q = - i_perm ? static_cast(seqlen_q) * hdim_q : hdim_q; - const ck_tile::index_t nhead_stride_k = - i_perm ? static_cast(seqlen_k) * hdim_q : hdim_q; - const ck_tile::index_t batch_stride_q = - static_cast(nhead_q) * seqlen_q * hdim_q; - const ck_tile::index_t batch_stride_k = - static_cast(nhead_k) * seqlen_k * hdim_q; - - ck_tile::stream_config stream_config{nullptr, false, log_level, 0, 1, false}; - - sparge_blockmap_args args; - args.q_ptr = q_buf.GetDeviceBuffer(); - args.k_ptr = k_buf.GetDeviceBuffer(); - args.batch = batch; - args.seqlen_q = seqlen_q; - args.seqlen_k = seqlen_k; - args.hdim_q = hdim_q; - args.nhead_q = nhead_q; - args.nhead_k = nhead_k; - args.stride_q = stride_q; - args.stride_k = stride_k; - args.nhead_stride_q = nhead_stride_q; - args.nhead_stride_k = nhead_stride_k; - args.batch_stride_q = batch_stride_q; - args.batch_stride_k = batch_stride_k; - args.simthreshd1 = simthreshd1; - args.cdfthreshd = cdfthreshd; - args.topk = topk; - args.scale = scale; - args.block_map_ptr = bmap_buf.GetDeviceBuffer(); - args.lut_ptr = lut_buf.GetDeviceBuffer(); - args.valid_block_num_ptr = valid_buf.GetDeviceBuffer(); - - sparge_blockmap_traits traits; - traits.data_type = data_type; - traits.hdim_q = hdim_q; - - sparge_blockmap_fwd(traits, args, stream_config); - - // Copy results back to host - bmap_buf.FromDevice(block_map_out.data(), bmap_bytes); - - sparge::VSALut vsa_lut{ - ck_tile::HostTensor({batch, nhead_q, num_q_blocks, num_k_blocks}), - ck_tile::HostTensor({batch, nhead_q, num_q_blocks}), - }; - lut_buf.FromDevice(vsa_lut.lut.data(), lut_bytes); - valid_buf.FromDevice(vsa_lut.valid_block_num.data(), valid_bytes); - - return vsa_lut; -} - -// Explicit template instantiations -template sparge::VSALut -sparge_blockmap_gpu(const ck_tile::HostTensor&, - const ck_tile::HostTensor&, - ck_tile::HostTensor&, - int, - int, - int, - int, - int, - int, - bool, - float, - float, - float, - int, - int, - int); - -template sparge::VSALut -sparge_blockmap_gpu(const ck_tile::HostTensor&, - const ck_tile::HostTensor&, - ck_tile::HostTensor&, - int, - int, - int, - int, - int, - int, - bool, - float, - float, - float, - int, - int, - int); diff --git a/example/ck_tile/50_sparse_attn/sparge_blockmap.h b/example/ck_tile/50_sparse_attn/sparge_blockmap.h deleted file mode 100644 index 3057257ca14..00000000000 --- a/example/ck_tile/50_sparse_attn/sparge_blockmap.h +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Advanced Micro Devices, Inc., or its affiliates. -// SPDX-License-Identifier: MIT -#pragma once - -#include -#include "ck_tile/core.hpp" -#include "ck_tile/host/host_tensor.hpp" -#include "sparge_tool.hpp" - -template -sparge::VSALut sparge_blockmap_gpu(const ck_tile::HostTensor& TQ, - const ck_tile::HostTensor& TK, - ck_tile::HostTensor& block_map_out, - int batch, - int nhead_q, - int nhead_k, - int seqlen_q, - int seqlen_k, - int hdim_q, - bool i_perm, - float simthreshd1, - float cdfthreshd, - float topk, - int blkq, - int blkk, - int log_level = 0); diff --git a/example/ck_tile/50_sparse_attn/test_sparge_vsa_sparse_attn.cpp b/example/ck_tile/50_sparse_attn/test_sparge_vsa_sparse_attn.cpp index 638a867b0f3..572b708f9ef 100644 --- a/example/ck_tile/50_sparse_attn/test_sparge_vsa_sparse_attn.cpp +++ b/example/ck_tile/50_sparse_attn/test_sparge_vsa_sparse_attn.cpp @@ -1,23 +1,17 @@ // Copyright (c) Advanced Micro Devices, Inc., or its affiliates. // SPDX-License-Identifier: MIT -// Demo: Sparge block-map -> (delta LUT) -> VSA sparse attention +// Demo: Sparge block-map -> (delta LUT) -> VSA sparse attention (all-in-device) #include -#include #include -#include #include -#include -#include -#include - #include "ck_tile/host.hpp" #include "ck_tile/core.hpp" #include "ck_tile/host/reference/reference_blocked_attention.hpp" #include "ck_tile/core/utility/bit_cast.hpp" -#include "vsa_sparge_attention.h" -#include "sparge_blockmap.h" +#include "sparge_blockmap_trek.hpp" +#include "fmha_fwd_trek.hpp" #include "sparge_tool.hpp" // ============================================================================ @@ -192,7 +186,7 @@ bool run_test(const ck_tile::ArgParser& arg_parser) << ", topk=" << topk << ")" << std::endl; std::cout << " i_perm: " << i_perm << ", o_perm: " << o_perm << std::endl; - // Create host tensors + // Create host tensors and fill with random data ck_tile::HostTensor q_host = make_qkv_tensor(batch, nhead, seqlen_q, hdim_q, i_perm); ck_tile::HostTensor k_host = make_qkv_tensor(batch, nhead_k, seqlen_k, hdim_q, i_perm); ck_tile::HostTensor v_host = make_qkv_tensor(batch, nhead_k, seqlen_k, hdim_v, i_perm); @@ -206,119 +200,157 @@ bool run_test(const ck_tile::ArgParser& arg_parser) ck_tile::FillUniformDistribution{-0.5f, 0.5f, seed + 2}(v_host); // ================================================================== - // GPU: Build block map + VSA LUT in one kernel (always run) + // Allocate device memory once, HtoD once // ================================================================== - std::cout << "Building Sparge block map + VSA LUT (GPU)..." << std::endl; - ck_tile::HostTensor block_map_gpu({batch, nhead, num_q_blocks, num_k_blocks}); - auto vsa_lut_gpu = sparge_blockmap_gpu(q_host, - k_host, - block_map_gpu, - batch, - nhead, - nhead_k, - seqlen_q, - seqlen_k, - hdim_q, - i_perm, - simthreshd1, - cdfthreshd, - topk, - static_cast(BLKQ), - static_cast(BLKK), - 0); + ck_tile::DeviceMem q_buf(q_host.get_element_space_size_in_bytes()); + ck_tile::DeviceMem k_buf(k_host.get_element_space_size_in_bytes()); + ck_tile::DeviceMem v_buf(v_host.get_element_space_size_in_bytes()); + ck_tile::DeviceMem o_buf(output_host.get_element_space_size_in_bytes()); + + q_buf.ToDevice(q_host.data()); + k_buf.ToDevice(k_host.data()); + v_buf.ToDevice(v_host.data()); + + const std::size_t bmap_bytes = + static_cast(batch) * nhead * num_q_blocks * num_k_blocks * sizeof(uint8_t); + const std::size_t lut_bytes = + static_cast(batch) * nhead * num_q_blocks * num_k_blocks * sizeof(int32_t); + const std::size_t valid_bytes = + static_cast(batch) * nhead * num_q_blocks * sizeof(int32_t); + + ck_tile::DeviceMem bmap_buf(bmap_bytes); + ck_tile::DeviceMem lut_buf(lut_bytes); + ck_tile::DeviceMem valid_buf(valid_bytes); + bmap_buf.SetZero(); + lut_buf.SetZero(); + valid_buf.SetZero(); // ================================================================== - // VSA sparse attention kernel (always run) + // Common stride calculations // ================================================================== - std::cout << "\n--- Running VSA sparse attention kernel ---" << std::endl; + assert(nhead % nhead_k == 0); + const float scale_s = 1.0f / std::sqrt(static_cast(hdim_q)); + + const ck_tile::index_t stride_q = i_perm ? hdim_q : nhead * hdim_q; + const ck_tile::index_t stride_k = i_perm ? hdim_q : nhead_k * hdim_q; + const ck_tile::index_t stride_v = i_perm ? hdim_v : nhead_k * hdim_v; + const ck_tile::index_t stride_o = o_perm ? hdim_v : nhead * hdim_v; + const ck_tile::index_t nhead_stride_q = i_perm ? seqlen_q * hdim_q : hdim_q; + const ck_tile::index_t nhead_stride_k = i_perm ? seqlen_k * hdim_q : hdim_q; + const ck_tile::index_t nhead_stride_v = i_perm ? seqlen_k * hdim_v : hdim_v; + const ck_tile::index_t nhead_stride_o = o_perm ? seqlen_q * hdim_v : hdim_v; + const ck_tile::index_t batch_stride_q = nhead * seqlen_q * hdim_q; + const ck_tile::index_t batch_stride_k = nhead_k * seqlen_k * hdim_q; + const ck_tile::index_t batch_stride_v = nhead_k * hdim_v * seqlen_k; + const ck_tile::index_t batch_stride_o = nhead * seqlen_q * hdim_v; + + std::string data_type = "fp16"; + if constexpr(std::is_same_v) + data_type = "bf16"; - try - { - if(kname) - { - vsa_sparge_attention(q_host, - k_host, - v_host, - vsa_lut_gpu.lut, - vsa_lut_gpu.valid_block_num, - output_host, - batch, - nhead, - nhead_k, - seqlen_q, - seqlen_k, - hdim_q, - hdim_v, - i_perm, - o_perm, - seqlen_q, - seqlen_k, - 1); - } + std::string msk_str = "0"; + mask_info mask = mask_info::decode(msk_str, seqlen_q, seqlen_k); - for(int i = 0; i < warmup; ++i) - { - vsa_sparge_attention(q_host, - k_host, - v_host, - vsa_lut_gpu.lut, - vsa_lut_gpu.valid_block_num, - output_host, - batch, - nhead, - nhead_k, - seqlen_q, - seqlen_k, - hdim_q, - hdim_v, - i_perm, - o_perm, - seqlen_q, - seqlen_k, - 0); - } + // ================================================================== + // GPU: Build block map + VSA LUT (always run, device-only) + // ================================================================== + std::cout << "Building Sparge block map + VSA LUT (GPU)..." << std::endl; + { + sparge_blockmap_args args; + args.q_ptr = q_buf.GetDeviceBuffer(); + args.k_ptr = k_buf.GetDeviceBuffer(); + args.batch = batch; + args.seqlen_q = seqlen_q; + args.seqlen_k = seqlen_k; + args.hdim_q = hdim_q; + args.nhead_q = nhead; + args.nhead_k = nhead_k; + args.stride_q = stride_q; + args.stride_k = stride_k; + args.nhead_stride_q = nhead_stride_q; + args.nhead_stride_k = nhead_stride_k; + args.batch_stride_q = batch_stride_q; + args.batch_stride_k = batch_stride_k; + args.simthreshd1 = simthreshd1; + args.cdfthreshd = cdfthreshd; + args.topk = topk; + args.scale = scale_s; + args.block_map_ptr = bmap_buf.GetDeviceBuffer(); + args.lut_ptr = lut_buf.GetDeviceBuffer(); + args.valid_block_num_ptr = valid_buf.GetDeviceBuffer(); + + sparge_blockmap_traits traits; + traits.data_type = data_type; + traits.hdim_q = hdim_q; + + sparge_blockmap_fwd(traits, args, ck_tile::stream_config{}); + } - [[maybe_unused]] auto sync_status1 = hipDeviceSynchronize(); - auto start = std::chrono::high_resolution_clock::now(); + // ================================================================== + // VSA sparse attention kernel (always run, LUT stays on device) + // ================================================================== + std::cout << "\n--- Running VSA sparse attention kernel ---" << std::endl; - for(int i = 0; i < repeat; ++i) - { - vsa_sparge_attention(q_host, - k_host, - v_host, - vsa_lut_gpu.lut, - vsa_lut_gpu.valid_block_num, - output_host, - batch, - nhead, - nhead_k, - seqlen_q, - seqlen_k, - hdim_q, - hdim_v, - i_perm, - o_perm, - seqlen_q, - seqlen_k, - 0); - } + fmha_vsa_fwd_args fmha_args; + fmha_args.q_ptr = q_buf.GetDeviceBuffer(); + fmha_args.k_ptr = k_buf.GetDeviceBuffer(); + fmha_args.v_ptr = v_buf.GetDeviceBuffer(); + fmha_args.lut_ptr = lut_buf.GetDeviceBuffer(); + fmha_args.valid_block_num_ptr = valid_buf.GetDeviceBuffer(); + fmha_args.o_ptr = o_buf.GetDeviceBuffer(); + fmha_args.batch = batch; + fmha_args.seqlen_q = seqlen_q; + fmha_args.seqlen_k = seqlen_k; + fmha_args.max_seqlen_q = seqlen_q; + fmha_args.hdim_q = hdim_q; + fmha_args.hdim_v = hdim_v; + fmha_args.nhead_q = nhead; + fmha_args.nhead_k = nhead_k; + fmha_args.scale_s = scale_s; + fmha_args.stride_q = stride_q; + fmha_args.stride_k = stride_k; + fmha_args.stride_v = stride_v; + fmha_args.stride_o = stride_o; + fmha_args.nhead_stride_q = nhead_stride_q; + fmha_args.nhead_stride_k = nhead_stride_k; + fmha_args.nhead_stride_v = nhead_stride_v; + fmha_args.nhead_stride_o = nhead_stride_o; + fmha_args.batch_stride_q = batch_stride_q; + fmha_args.batch_stride_k = batch_stride_k; + fmha_args.batch_stride_v = batch_stride_v; + fmha_args.batch_stride_o = batch_stride_o; + fmha_args.window_size_left = mask.left; + fmha_args.window_size_right = mask.right; + fmha_args.mask_type = static_cast(mask.type); + + fmha_vsa_fwd_traits fmha_traits; + fmha_traits.hdim_q = hdim_q; + fmha_traits.hdim_v = hdim_v; + fmha_traits.data_type = data_type; + fmha_traits.is_v_rowmajor = true; + fmha_traits.mask_type = mask.type; + + ck_tile::stream_config stream_config{nullptr, + true, + /* log_level = */ kname ? 1 : 0, + warmup, + repeat, + false}; + + float avg_time_ms = sparge_vsa_fwd(fmha_traits, fmha_args, stream_config); + + std::cout << "\n>>>> VSA sparse attention average time: " << avg_time_ms << " ms <<<<" + << std::endl; - [[maybe_unused]] auto sync_status2 = hipDeviceSynchronize(); - auto end = std::chrono::high_resolution_clock::now(); - double avg_time_ms = - std::chrono::duration(end - start).count() / repeat; + // DtoH: attention output (always needed) + o_buf.FromDevice(output_host.data(), output_host.get_element_space_size_in_bytes()); - std::cout << "\n>>>> VSA sparse attention average time: " << avg_time_ms << " ms <<<<" - << std::endl; - } - catch(const std::exception& e) - { - std::cerr << "Error during kernel execution: " << e.what() << std::endl; - return false; - } + // DtoH: block_map (needed for sparsity stats and validation) + ck_tile::HostTensor block_map_gpu({batch, nhead, num_q_blocks, num_k_blocks}); + bmap_buf.FromDevice(block_map_gpu.data(), bmap_bytes); // ================================================================== - // Sparsity statistics (always run, pure CPU read of HostTensor) + // Sparsity statistics (pure CPU, reads block_map HostTensor) // ================================================================== std::size_t total_blocks = 0; std::size_t active_blocks = 0; @@ -366,6 +398,14 @@ bool run_test(const ck_tile::ArgParser& arg_parser) std::cout << "Converting block map to VSA LUT (delta, CPU)..." << std::endl; auto vsa_lut_cpu = sparge::block_map_to_vsa_lut_delta(block_relation_onehot); + // DtoH: LUT + valid_block_num (only for validation) + sparge::VSALut vsa_lut_gpu{ + ck_tile::HostTensor({batch, nhead, num_q_blocks, num_k_blocks}), + ck_tile::HostTensor({batch, nhead, num_q_blocks}), + }; + lut_buf.FromDevice(vsa_lut_gpu.lut.data(), lut_bytes); + valid_buf.FromDevice(vsa_lut_gpu.valid_block_num.data(), valid_bytes); + // Validate block map std::cout << "\n--- Validating GPU block map vs CPU golden ---" << std::endl; { @@ -378,20 +418,16 @@ bool run_test(const ck_tile::ArgParser& arg_parser) { for(ck_tile::index_t kb = 0; kb < num_k_blocks; ++kb) { - if(block_map_gpu(b, h, qb, kb) != - block_relation_onehot(b, h, qb, kb)) + if(block_map_gpu(b, h, qb, kb) != block_relation_onehot(b, h, qb, kb)) { bmap_mismatches++; if(bmap_mismatches <= 10) { std::cout - << " block_map mismatch at [" << b << "," << h << "," - << qb << "," << kb - << "]: GPU=" - << static_cast(block_map_gpu(b, h, qb, kb)) - << " CPU=" - << static_cast( - block_relation_onehot(b, h, qb, kb)) + << " block_map mismatch at [" << b << "," << h << "," << qb + << "," << kb << "]: GPU=" + << static_cast(block_map_gpu(b, h, qb, kb)) << " CPU=" + << static_cast(block_relation_onehot(b, h, qb, kb)) << std::endl; } } @@ -429,28 +465,24 @@ bool run_test(const ck_tile::ArgParser& arg_parser) valid_mismatches++; if(valid_mismatches <= 5) { - std::cout - << " valid_block_num mismatch at [" << b << "," << h - << "," << qb - << "]: GPU=" << vsa_lut_gpu.valid_block_num(b, h, qb) - << " CPU=" << vsa_lut_cpu.valid_block_num(b, h, qb) - << std::endl; + std::cout << " valid_block_num mismatch at [" << b << "," << h + << "," << qb + << "]: GPU=" << vsa_lut_gpu.valid_block_num(b, h, qb) + << " CPU=" << vsa_lut_cpu.valid_block_num(b, h, qb) + << std::endl; } } for(ck_tile::index_t kb = 0; kb < num_k_blocks; ++kb) { - if(vsa_lut_gpu.lut(b, h, qb, kb) != - vsa_lut_cpu.lut(b, h, qb, kb)) + if(vsa_lut_gpu.lut(b, h, qb, kb) != vsa_lut_cpu.lut(b, h, qb, kb)) { lut_mismatches++; if(lut_mismatches <= 10) { std::cout << " LUT mismatch at [" << b << "," << h << "," << qb - << "," << kb - << "]: GPU=" << vsa_lut_gpu.lut(b, h, qb, kb) - << " CPU=" << vsa_lut_cpu.lut(b, h, qb, kb) - << std::endl; + << "," << kb << "]: GPU=" << vsa_lut_gpu.lut(b, h, qb, kb) + << " CPU=" << vsa_lut_cpu.lut(b, h, qb, kb) << std::endl; } } } diff --git a/example/ck_tile/50_sparse_attn/vsa_sparge_attention.cpp b/example/ck_tile/50_sparse_attn/vsa_sparge_attention.cpp deleted file mode 100644 index 5f9c2676ddb..00000000000 --- a/example/ck_tile/50_sparse_attn/vsa_sparge_attention.cpp +++ /dev/null @@ -1,195 +0,0 @@ -// Copyright (c) Advanced Micro Devices, Inc., or its affiliates. -// SPDX-License-Identifier: MIT -#include "vsa_sparge_attention.h" -#include "fmha_fwd_trek.hpp" -#include "ck_tile/core.hpp" -#include "ck_tile/host/host_tensor.hpp" -#include "ck_tile/host/device_memory.hpp" -#include - -template -ck_tile::HostTensor -vsa_sparge_attention(const ck_tile::HostTensor& TQ, - const ck_tile::HostTensor& TK, - const ck_tile::HostTensor& TV, - const ck_tile::HostTensor& TKV_block_idx, - const ck_tile::HostTensor& TKV_blocks, - ck_tile::HostTensor& Y, - int batch, - int nhead, - int nhead_k, - int seqlen_q, - int seqlen_k, - int hdim_q, - int hdim_v, - bool i_perm, - bool o_perm, - int max_seqlen_q, - int max_seqlen_k, - int log_level) -{ - static_assert(std::is_same_v || - std::is_same_v, - "VSA sparse attention supports fp16/bf16 only."); - std::string data_type = "fp16"; - if constexpr(std::is_same_v) - { - data_type = "bf16"; - } - - if(max_seqlen_q == 0) - max_seqlen_q = seqlen_q; - if(max_seqlen_k == 0) - max_seqlen_k = seqlen_k; - bool is_v_rowmajor = true; - float scale_s = 1.0 / ck_tile::sqrt(static_cast(hdim_q)); - std::string msk_str = "0"; - mask_info mask = mask_info::decode(msk_str, seqlen_q, seqlen_k); - - const ck_tile::index_t shape_seqlen_q = seqlen_q; - const ck_tile::index_t shape_seqlen_k = seqlen_k; - - ck_tile::stream_config stream_config{nullptr, - false, // time_kernel - log_level, - 0, - 1, - false}; - - ck_tile::DeviceMem q_buf(TQ.get_element_space_size_in_bytes()); - ck_tile::DeviceMem k_buf(TK.get_element_space_size_in_bytes()); - ck_tile::DeviceMem v_buf(TV.get_element_space_size_in_bytes()); - ck_tile::DeviceMem lut_buf(TKV_block_idx.get_element_space_size_in_bytes()); - ck_tile::DeviceMem valid_block_num_buf(TKV_blocks.get_element_space_size_in_bytes()); - ck_tile::DeviceMem o_buf(Y.get_element_space_size_in_bytes()); - - q_buf.ToDevice(TQ.data()); - k_buf.ToDevice(TK.data()); - v_buf.ToDevice(TV.data()); - lut_buf.ToDevice(TKV_block_idx.data()); - valid_block_num_buf.ToDevice(TKV_blocks.data()); - - const auto init_args = [&](auto& args) { - assert(nhead % nhead_k == 0); - const ck_tile::index_t stride_q = (i_perm ? hdim_q : nhead * hdim_q); - const ck_tile::index_t stride_k = (i_perm ? hdim_q : nhead_k * hdim_q); - const ck_tile::index_t stride_v = [&]() { - if(is_v_rowmajor) - return i_perm ? hdim_v : nhead_k * hdim_v; - else - return (i_perm ? shape_seqlen_k : nhead_k * shape_seqlen_k); - }(); - const ck_tile::index_t stride_o = (o_perm ? hdim_v : nhead * hdim_v); - const ck_tile::index_t nhead_stride_q = (i_perm ? shape_seqlen_q * hdim_q : hdim_q); - const ck_tile::index_t nhead_stride_k = i_perm ? shape_seqlen_k * hdim_q : hdim_q; - const ck_tile::index_t nhead_stride_v = [&]() { - if(is_v_rowmajor) - return i_perm ? shape_seqlen_k * hdim_v : hdim_v; - else - return i_perm ? hdim_v * shape_seqlen_k : shape_seqlen_k; - }(); - const ck_tile::index_t nhead_stride_o = (o_perm ? shape_seqlen_q * hdim_v : hdim_v); - const ck_tile::index_t batch_stride_q = (nhead * shape_seqlen_q * hdim_q); - const ck_tile::index_t batch_stride_k = nhead_k * shape_seqlen_k * hdim_q; - const ck_tile::index_t batch_stride_v = nhead_k * hdim_v * shape_seqlen_k; - const ck_tile::index_t batch_stride_o = (nhead * shape_seqlen_q * hdim_v); - - args.q_ptr = q_buf.GetDeviceBuffer(); - args.k_ptr = k_buf.GetDeviceBuffer(); - args.v_ptr = v_buf.GetDeviceBuffer(); - args.lut_ptr = lut_buf.GetDeviceBuffer(); - args.valid_block_num_ptr = valid_block_num_buf.GetDeviceBuffer(); - - args.batch = batch; - args.seqlen_q = shape_seqlen_q; - args.hdim_q = hdim_q; - args.hdim_v = hdim_v; - args.nhead_q = nhead; - args.nhead_k = nhead_k; - - args.stride_q = stride_q; - args.stride_k = stride_k; - args.stride_v = stride_v; - args.nhead_stride_q = nhead_stride_q; - args.nhead_stride_k = nhead_stride_k; - args.nhead_stride_v = nhead_stride_v; - args.batch_stride_q = batch_stride_q; - args.batch_stride_k = batch_stride_k; - args.batch_stride_v = batch_stride_v; - - args.o_ptr = o_buf.GetDeviceBuffer(); - - args.seqlen_k = shape_seqlen_k; - args.max_seqlen_q = max_seqlen_q; - - args.scale_s = scale_s; - - args.stride_o = stride_o; - args.nhead_stride_o = nhead_stride_o; - args.batch_stride_o = batch_stride_o; - - args.window_size_left = mask.left; - args.window_size_right = mask.right; - args.mask_type = static_cast(mask.type); - }; - - const auto init_traits = [&](auto& traits) { - traits.hdim_q = hdim_q; - traits.hdim_v = hdim_v; - traits.data_type = data_type; - traits.is_v_rowmajor = is_v_rowmajor; - traits.mask_type = mask.type; - }; - - fmha_vsa_fwd_traits fmha_traits; - init_traits(fmha_traits); - - fmha_vsa_fwd_args args; - init_args(args); - - sparge_vsa_fwd(fmha_traits, args, stream_config); - - o_buf.FromDevice(Y.data(), Y.get_element_space_size_in_bytes()); - - return Y; -} - -template ck_tile::HostTensor -vsa_sparge_attention(const ck_tile::HostTensor&, - const ck_tile::HostTensor&, - const ck_tile::HostTensor&, - const ck_tile::HostTensor&, - const ck_tile::HostTensor&, - ck_tile::HostTensor&, - int, - int, - int, - int, - int, - int, - int, - bool, - bool, - int, - int, - int); - -template ck_tile::HostTensor -vsa_sparge_attention(const ck_tile::HostTensor&, - const ck_tile::HostTensor&, - const ck_tile::HostTensor&, - const ck_tile::HostTensor&, - const ck_tile::HostTensor&, - ck_tile::HostTensor&, - int, - int, - int, - int, - int, - int, - int, - bool, - bool, - int, - int, - int); diff --git a/example/ck_tile/50_sparse_attn/vsa_sparge_attention.h b/example/ck_tile/50_sparse_attn/vsa_sparge_attention.h deleted file mode 100644 index d51a7e8c00b..00000000000 --- a/example/ck_tile/50_sparse_attn/vsa_sparge_attention.h +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Advanced Micro Devices, Inc., or its affiliates. -// SPDX-License-Identifier: MIT -#pragma once -#include -#include -#include "ck_tile/core.hpp" -#include "ck_tile/host/host_tensor.hpp" - -template -ck_tile::HostTensor -vsa_sparge_attention(const ck_tile::HostTensor& TQ, - const ck_tile::HostTensor& TK, - const ck_tile::HostTensor& TV, - const ck_tile::HostTensor& TKV_block_idx, - const ck_tile::HostTensor& TKV_blocks, - ck_tile::HostTensor& Y, - int batch, - int nhead, - int nhead_k, - int seqlen_q, - int seqlen_k, - int hdim_q, - int hdim_v, - bool i_perm, - bool o_perm, - int max_seqlen_q, - int max_seqlen_k, - int log_level = 0); From ab44b835667e29cf5ba844d2a7bdd52fc9f4cc17 Mon Sep 17 00:00:00 2001 From: Gino Lu Date: Wed, 22 Apr 2026 13:13:37 -0400 Subject: [PATCH 05/16] refactor to combine two kernel --- example/ck_tile/50_sparse_attn/CMakeLists.txt | 131 +-- .../codegen/ops/fmha_fwd_jenga.py | 141 +++- .../codegen/ops/fmha_fwd_vsa.py | 141 +++- .../codegen/ops/sparge_fwd_jenga.py | 799 ------------------ .../codegen/ops/sparge_fwd_vsa.py | 799 ------------------ .../ck_tile/50_sparse_attn/fmha_fwd_trek.hpp | 16 +- .../50_sparse_attn/jenga_sparge_attention.cpp | 189 ----- .../50_sparse_attn/jenga_sparge_attention.h | 27 - .../50_sparse_attn/sparge_blockmap_inst.cpp | 139 +++ .../50_sparse_attn/sparge_blockmap_trek.hpp | 13 + .../ck_tile/50_sparse_attn/test_sparge.cpp | 432 ++++++++++ .../test_sparge_jenga_sparse_attn.cpp | 422 --------- .../test_sparge_vsa_sparse_attn.cpp | 597 ------------- ...ock_fmha_pipeline_qr_ks_vs_async_jenga.hpp | 40 +- 14 files changed, 896 insertions(+), 2990 deletions(-) delete mode 100644 example/ck_tile/50_sparse_attn/codegen/ops/sparge_fwd_jenga.py delete mode 100644 example/ck_tile/50_sparse_attn/codegen/ops/sparge_fwd_vsa.py delete mode 100644 example/ck_tile/50_sparse_attn/jenga_sparge_attention.cpp delete mode 100644 example/ck_tile/50_sparse_attn/jenga_sparge_attention.h create mode 100644 example/ck_tile/50_sparse_attn/test_sparge.cpp delete mode 100644 example/ck_tile/50_sparse_attn/test_sparge_jenga_sparse_attn.cpp delete mode 100644 example/ck_tile/50_sparse_attn/test_sparge_vsa_sparse_attn.cpp diff --git a/example/ck_tile/50_sparse_attn/CMakeLists.txt b/example/ck_tile/50_sparse_attn/CMakeLists.txt index f234f631b6b..b20a661805f 100644 --- a/example/ck_tile/50_sparse_attn/CMakeLists.txt +++ b/example/ck_tile/50_sparse_attn/CMakeLists.txt @@ -88,68 +88,6 @@ target_compile_options(${EXAMPLE_JENGA_SPARSE_ATTN} PRIVATE -Wno-float-equal ) -# ============================================================================ -# Sparge Jenga (64x128 tile) -# ============================================================================ -set(SPARGE_JENGA_CODE_GEN_ARGS - ${CMAKE_CURRENT_LIST_DIR}/generate.py - --api sparge_fwd_jenga - --receipt 600 -) - -execute_process( - COMMAND ${Python3_EXECUTABLE} ${SPARGE_JENGA_CODE_GEN_ARGS} - --list_blobs ${CMAKE_CURRENT_BINARY_DIR}/sparge_jenga_blob_list.txt - RESULT_VARIABLE ret -) -if(ret AND NOT ret EQUAL 0) - message(FATAL_ERROR "Failed to generate Sparge Jenga kernel list") -endif() - -file(STRINGS ${CMAKE_CURRENT_BINARY_DIR}/sparge_jenga_blob_list.txt SPARGE_JENGA_GEN_BLOBS) - -add_custom_command( - OUTPUT ${SPARGE_JENGA_GEN_BLOBS} - COMMAND ${Python3_EXECUTABLE} ${SPARGE_JENGA_CODE_GEN_ARGS} - --output_dir ${CMAKE_CURRENT_BINARY_DIR} - DEPENDS ${CODE_GEN_SCRIPTS} - COMMENT "Generate CK Tile Sparge Jenga kernels" -) - -message(STATUS "Sparge Jenga kernel files to be generated: ${SPARGE_JENGA_GEN_BLOBS}") - -set(SPARGE_JENGA_INSTANCES "tile_sparge_jenga_instances") - -add_library(${SPARGE_JENGA_INSTANCES} OBJECT EXCLUDE_FROM_ALL - ${SPARGE_JENGA_GEN_BLOBS} - ${CMAKE_CURRENT_LIST_DIR}/jenga_sparge_attention.cpp -) -target_include_directories(${SPARGE_JENGA_INSTANCES} PRIVATE - ${CMAKE_CURRENT_LIST_DIR} - ${PROJECT_SOURCE_DIR}/include/ck_tile/ops/sparse_attn -) -set_source_files_properties(${SPARGE_JENGA_GEN_BLOBS} PROPERTIES LANGUAGE HIP) -set_source_files_properties(${CMAKE_CURRENT_LIST_DIR}/jenga_sparge_attention.cpp PROPERTIES LANGUAGE HIP) -set_property(TARGET ${SPARGE_JENGA_INSTANCES} PROPERTY HIP_ARCHITECTURES ${INST_TARGETS}) - -target_compile_options(${SPARGE_JENGA_INSTANCES} PRIVATE - -DCK_TILE_USE_BUFFER_ADDRESSING_BUILTIN - -DCK_TILE_FMHA_FWD_FAST_EXP2 - -Wno-undefined-func-template - -Wno-float-equal -) - -# Sparge + Jenga Example executable -set(EXAMPLE_SPARGE_JENGA_SPARSE_ATTN "tile_example_sparge_jenga_sparse_attn") -message(DEBUG "adding example ${EXAMPLE_SPARGE_JENGA_SPARSE_ATTN}") -add_executable(${EXAMPLE_SPARGE_JENGA_SPARSE_ATTN} EXCLUDE_FROM_ALL test_sparge_jenga_sparse_attn.cpp) -target_link_libraries(${EXAMPLE_SPARGE_JENGA_SPARSE_ATTN} ${SPARGE_JENGA_INSTANCES}) -target_include_directories(${EXAMPLE_SPARGE_JENGA_SPARSE_ATTN} PRIVATE ${CMAKE_CURRENT_LIST_DIR}) -target_compile_options(${EXAMPLE_SPARGE_JENGA_SPARSE_ATTN} PRIVATE - -Wno-undefined-func-template - -Wno-float-equal -) - # ============================================================================ # VSA Sparse Attention # ============================================================================ @@ -215,55 +153,6 @@ target_compile_options(${EXAMPLE_VSA_SPARSE_ATTN} PRIVATE -Wno-float-equal ) -# ============================================================================ -# Sparge VSA (64x128 tile) -# ============================================================================ -set(SPARGE_VSA_CODE_GEN_ARGS - ${CMAKE_CURRENT_LIST_DIR}/generate.py - --api sparge_fwd_vsa - --receipt 600 -) - -execute_process( - COMMAND ${Python3_EXECUTABLE} ${SPARGE_VSA_CODE_GEN_ARGS} - --list_blobs ${CMAKE_CURRENT_BINARY_DIR}/sparge_vsa_blob_list.txt - RESULT_VARIABLE ret -) -if(ret AND NOT ret EQUAL 0) - message(FATAL_ERROR "Failed to generate Sparge VSA kernel list") -endif() - -file(STRINGS ${CMAKE_CURRENT_BINARY_DIR}/sparge_vsa_blob_list.txt SPARGE_VSA_GEN_BLOBS) - -add_custom_command( - OUTPUT ${SPARGE_VSA_GEN_BLOBS} - COMMAND ${Python3_EXECUTABLE} ${SPARGE_VSA_CODE_GEN_ARGS} - --output_dir ${CMAKE_CURRENT_BINARY_DIR} - DEPENDS ${CODE_GEN_SCRIPTS} - COMMENT "Generate CK Tile Sparge VSA kernels" -) - -message(STATUS "Sparge VSA kernel files to be generated: ${SPARGE_VSA_GEN_BLOBS}") - -set(SPARGE_VSA_INSTANCES "tile_sparge_vsa_instances") - -add_library(${SPARGE_VSA_INSTANCES} OBJECT EXCLUDE_FROM_ALL - ${SPARGE_VSA_GEN_BLOBS} -) -target_include_directories(${SPARGE_VSA_INSTANCES} PRIVATE - ${CMAKE_CURRENT_LIST_DIR} - ${PROJECT_SOURCE_DIR}/include/ck_tile/ops/sparse_attn -) -set_source_files_properties(${SPARGE_VSA_GEN_BLOBS} PROPERTIES LANGUAGE HIP) -set_property(TARGET ${SPARGE_VSA_INSTANCES} PROPERTY HIP_ARCHITECTURES ${INST_TARGETS}) - -target_compile_options(${SPARGE_VSA_INSTANCES} PRIVATE - -DCK_TILE_USE_BUFFER_ADDRESSING_BUILTIN - -DCK_TILE_FMHA_FWD_FAST_EXP2 - -Wno-undefined-func-template - -Wno-float-equal -) - # ============================================================================ # Sparge BlockMap GPU Kernel (hand-written instantiation, no codegen) # ============================================================================ @@ -289,16 +178,20 @@ target_compile_options(${SPARGE_BLOCKMAP_INSTANCES} PRIVATE -Wno-float-equal ) -# Sparge + VSA Example executable (now links blockmap kernel too) -set(EXAMPLE_SPARGE_VSA_SPARSE_ATTN "tile_example_sparge_vsa_sparse_attn") -message(DEBUG "adding example ${EXAMPLE_SPARGE_VSA_SPARSE_ATTN}") -add_executable(${EXAMPLE_SPARGE_VSA_SPARSE_ATTN} EXCLUDE_FROM_ALL test_sparge_vsa_sparse_attn.cpp) -target_link_libraries(${EXAMPLE_SPARGE_VSA_SPARSE_ATTN} - ${SPARGE_VSA_INSTANCES} +# ---------------------------------------------------------------------------- +# Build unified Sparge test: combines blockmap, Jenga, and VSA attention +# for end-to-end evaluation and timing in a single executable. +# ---------------------------------------------------------------------------- +set(EXAMPLE_SPARGE "tile_example_sparge") +message(DEBUG "adding example ${EXAMPLE_SPARGE}") +add_executable(${EXAMPLE_SPARGE} EXCLUDE_FROM_ALL test_sparge.cpp) +target_link_libraries(${EXAMPLE_SPARGE} + ${SPARSE_ATTN_JENGA_INSTANCES} + ${SPARSE_ATTN_VSA_INSTANCES} ${SPARGE_BLOCKMAP_INSTANCES} ) -target_include_directories(${EXAMPLE_SPARGE_VSA_SPARSE_ATTN} PRIVATE ${CMAKE_CURRENT_LIST_DIR}) -target_compile_options(${EXAMPLE_SPARGE_VSA_SPARSE_ATTN} PRIVATE +target_include_directories(${EXAMPLE_SPARGE} PRIVATE ${CMAKE_CURRENT_LIST_DIR}) +target_compile_options(${EXAMPLE_SPARGE} PRIVATE -Wno-undefined-func-template -Wno-float-equal ) diff --git a/example/ck_tile/50_sparse_attn/codegen/ops/fmha_fwd_jenga.py b/example/ck_tile/50_sparse_attn/codegen/ops/fmha_fwd_jenga.py index a3d32652a98..1f0a78048d9 100644 --- a/example/ck_tile/50_sparse_attn/codegen/ops/fmha_fwd_jenga.py +++ b/example/ck_tile/50_sparse_attn/codegen/ops/fmha_fwd_jenga.py @@ -141,6 +141,17 @@ def update_file(file_path, content): constexpr ck_tile::index_t kBlockPerCu = k_::kBlockPerCu; return ck_tile::launch_kernel(s, ck_tile::make_kernel(k_{{}}, grids, blocks, 0, kargs)); }} + +template<> +void fmha_jenga_fwd_oneshot_(const ck_tile::stream_config& s, fmha_jenga_fwd_args a) +{{ + using k_ = fmha_kernel_{F_idx}; + auto [kargs, grids] = fmha_fwd_create_kargs_and_grids(a); + const dim3 blocks = k_::BlockSize(); + constexpr ck_tile::index_t kBlockPerCu = k_::kBlockPerCu; + ck_tile::make_kernel(k_{{}}, grids, blocks, 0, kargs)( + ck_tile::stream_config{{s.stream_id_}}); +}} """ FMHA_FWD_API_FILENAME = "fmha_jenga_fwd_api.cpp" @@ -219,6 +230,45 @@ def update_file(file_path, content): }} """ +FMHA_FWD_ONESHOT_API_FILENAME = "fmha_jenga_fwd_oneshot_api.cpp" +FMHA_FWD_ONESHOT_API = """ +#include "fmha_fwd_trek.hpp" +#include + +void fmha_jenga_fwd_oneshot(fmha_jenga_fwd_traits t, fmha_jenga_fwd_args a, const ck_tile::stream_config& s){{ + + const bool has_load_tr = ck_tile::is_load_tr_supported(); + +{F_dispatch} + std::cerr << "fmha_jenga_fwd_oneshot: no matching dispatch (dtype=" << t.data_type + << " hdim_q=" << t.hdim_q << " hdim_v=" << t.hdim_v + << " seqlen_q=" << a.seqlen_q << " seqlen_k=" << a.seqlen_k + << " mask=" << static_cast(t.mask_type) << ")" << std::endl; +}} +""" + +FMHA_FWD_ONESHOT_API_PER_TRLOAD = """ {F_if}({F_trload_cond}){{ +{F_dtype_case} + }} +""" + +FMHA_FWD_ONESHOT_API_PER_DTYPE = """ {F_if}(t.data_type.compare(\"{F_dtype}\") == 0){{ +{F_hdim_case} + }} +""" +FMHA_FWD_ONESHOT_API_PER_HDIM_CASE = """ {F_if} (t.hdim_q <= {F_hdim} && t.hdim_v <= {F_hdim_v}) {{ +{F_inner_dispatch} + }} +""" + +FMHA_FWD_ONESHOT_API_INNER_DISPATCH = """ {F_if}((t.is_v_rowmajor == {F_vlayout}) && ({F_mask_check}) && + ({F_scheck}) && ({F_seqtune}) && ({F_skcheck}) && ({F_dcheck}) && ({F_dvcheck}) && ({F_constraint})) {{ + using trait_ = fmha_jenga_fwd_traits_<{F_hdim}, {F_dtype}, {F_bm0}, {F_bn0}, {F_bk0}, {F_bn1}, {F_bk1}, {F_bk0max}, {F_vlayout}, {F_pipeline_enum}, false/*logits*/, {F_mask}, {F_spad}, {F_skpad}, {F_dpad}, {F_dvpad}, {F_trload}>; + fmha_jenga_fwd_oneshot_(s, a); + return; + }} +""" + @dataclass class CppConstraint: @@ -274,10 +324,7 @@ def scheck(self) -> str: @property def seqtune(self) -> str: - if self.bm0 == 128: - return "true/*fall back to largest tile*/" # group mode only generate spad/skpad == true - else: - return f"a.seqlen_q <= {self.bm0}" + return "true" @property def skcheck(self) -> str: @@ -447,6 +494,67 @@ def api(self) -> str: per_tr_load += " (void)t ; (void)s ; (void)a;" return FMHA_FWD_KERNEL_HEADER + FMHA_FWD_API.format(F_dispatch=per_tr_load) + @property + def oneshot_api(self) -> str: + tr_load_cond_map = {"t": "has_load_tr", "f": "true"} + + per_tr_load = str() + for tr_load in ["t", "f"]: + per_dtypes = str() + for i, dtype in enumerate(self.pool.keys()): + per_hdim_case = str() + for j, (hdim, hdim_v) in enumerate(self.pool[dtype].keys()): + traits = [ + t + for t in self.pool[dtype][(hdim, hdim_v)] + if tr_load == t.tr_load + ] + inners = str() + for k, trait in enumerate(traits): + if_k = "if" if k == 0 else "else if" + inners = inners + FMHA_FWD_ONESHOT_API_INNER_DISPATCH.format( + F_if=if_k, + F_vlayout=LAYOUT_MAP[trait.vlayout], + F_pipeline_enum=PIPELINE_ENUM_MAP[trait.pipeline_tag], + F_mask=get_mask_map(self.mask_impl)[trait.mask], + F_mask_check=get_mask_check_map(self.mask_impl)[trait.mask], + F_trload=BOOL_MAP[trait.tr_load], + F_scheck=trait.scheck, + F_seqtune=trait.seqtune, + F_skcheck=trait.skcheck, + F_dcheck=trait.dcheck, + F_dvcheck=trait.dvcheck, + F_constraint=trait.constraint, + F_spad=BOOL_MAP[trait.spad], + F_skpad=BOOL_MAP[trait.skpad], + F_dpad=BOOL_MAP[trait.dpad], + F_dvpad=BOOL_MAP[trait.dvpad], + F_bm0=trait.bm0, + F_bn0=trait.bn0, + F_bk0=trait.bk0, + F_bn1=trait.bn1, + F_bk1=trait.bk1, + F_bk0max=trait.bk0max, + F_hdim=hdim, + F_dtype=FWD_DTYPE_MAP[dtype], + ) + if_j = "if" if j == 0 else "else if" + per_hdim_case = per_hdim_case + FMHA_FWD_ONESHOT_API_PER_HDIM_CASE.format( + F_if=if_j, F_hdim=hdim, F_hdim_v=hdim_v, F_inner_dispatch=inners + ) + if_i = "if" if i == 0 else "else if" + per_dtypes = per_dtypes + FMHA_FWD_ONESHOT_API_PER_DTYPE.format( + F_if=if_i, F_dtype=dtype, F_hdim_case=per_hdim_case + ) + per_tr_load += FMHA_FWD_ONESHOT_API_PER_TRLOAD.format( + F_if="if", + F_trload_cond=tr_load_cond_map[tr_load], + F_dtype_case=per_dtypes, + ) + if not per_tr_load: + per_tr_load += " (void)t ; (void)s ; (void)a;" + return FMHA_FWD_KERNEL_HEADER + FMHA_FWD_ONESHOT_API.format(F_dispatch=per_tr_load) + @dataclass class FmhaFwdTileSize: @@ -582,6 +690,27 @@ def get_hdim_tile_size_dict(dtype: str) -> Optional[dict]: # FmhaFwdTileSize(128, 64, 32, 64, 32, 64, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, -1)], # (96, 128) : [FmhaFwdTileSize(128, 128, 32, 128, 32, 96, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, -1)], (128, 128): [ + FmhaFwdTileSize( # fmt: skip -- 64x128 tile matching blockmap kM0=64, kN0=128 + 64, + 128, + 64, + 128, + 64, + 128, + 4, + 1, + 1, + 4, + 1, + 1, + 16, + 16, + 16, + 16, + 16, + 16, + -1, + ), FmhaFwdTileSize( # fmt: skip 16, 32, @@ -780,7 +909,7 @@ def get_fwd_blobs( for tile, pipeline in itertools.product( tiles, factory.get_pipelines(dtype, hdim, hdim_v, receipt, mask_impl) ): - if tile.F_bm0 != 128 or tile.F_bn0 != 128: + if tile.F_bm0 != 64 or tile.F_bn0 != 128: continue if pipeline.tag != "qr_async": continue @@ -846,6 +975,7 @@ def write_single_fwd_kernel(kernel: FmhaFwdKernel, autogen_dir: Path) -> None: def write_fwd_api(api_pool: FmhaFwdApiPool, autogen_dir: Path) -> None: update_file(autogen_dir / FMHA_FWD_API_FILENAME, api_pool.api) + update_file(autogen_dir / FMHA_FWD_ONESHOT_API_FILENAME, api_pool.oneshot_api) def write_blobs( @@ -865,3 +995,4 @@ def list_blobs( for kernel in kernels: f.write((file_path.parent / GEN_DIR / kernel.filename).as_posix() + "\n") f.write((file_path.parent / GEN_DIR / FMHA_FWD_API_FILENAME).as_posix() + "\n") + f.write((file_path.parent / GEN_DIR / FMHA_FWD_ONESHOT_API_FILENAME).as_posix() + "\n") diff --git a/example/ck_tile/50_sparse_attn/codegen/ops/fmha_fwd_vsa.py b/example/ck_tile/50_sparse_attn/codegen/ops/fmha_fwd_vsa.py index 038738de246..217cfcfe2a4 100644 --- a/example/ck_tile/50_sparse_attn/codegen/ops/fmha_fwd_vsa.py +++ b/example/ck_tile/50_sparse_attn/codegen/ops/fmha_fwd_vsa.py @@ -141,6 +141,17 @@ def update_file(file_path, content): constexpr ck_tile::index_t kBlockPerCu = k_::kBlockPerCu; return ck_tile::launch_kernel(s, ck_tile::make_kernel(k_{{}}, grids, blocks, 0, kargs)); }} + +template<> +void fmha_vsa_fwd_oneshot_(const ck_tile::stream_config& s, fmha_vsa_fwd_args a) +{{ + using k_ = fmha_kernel_{F_idx}; + auto [kargs, grids] = fmha_fwd_create_kargs_and_grids(a); + const dim3 blocks = k_::BlockSize(); + constexpr ck_tile::index_t kBlockPerCu = k_::kBlockPerCu; + ck_tile::make_kernel(k_{{}}, grids, blocks, 0, kargs)( + ck_tile::stream_config{{s.stream_id_}}); +}} """ FMHA_FWD_API_FILENAME = "fmha_vsa_fwd_api.cpp" @@ -219,6 +230,45 @@ def update_file(file_path, content): }} """ +FMHA_FWD_ONESHOT_API_FILENAME = "fmha_vsa_fwd_oneshot_api.cpp" +FMHA_FWD_ONESHOT_API = """ +#include "fmha_fwd_trek.hpp" +#include + +void fmha_vsa_fwd_oneshot(fmha_vsa_fwd_traits t, fmha_vsa_fwd_args a, const ck_tile::stream_config& s){{ + + const bool has_load_tr = ck_tile::is_load_tr_supported(); + +{F_dispatch} + std::cerr << "fmha_vsa_fwd_oneshot: no matching dispatch (dtype=" << t.data_type + << " hdim_q=" << t.hdim_q << " hdim_v=" << t.hdim_v + << " seqlen_q=" << a.seqlen_q << " seqlen_k=" << a.seqlen_k + << " mask=" << static_cast(t.mask_type) << ")" << std::endl; +}} +""" + +FMHA_FWD_ONESHOT_API_PER_TRLOAD = """ {F_if}({F_trload_cond}){{ +{F_dtype_case} + }} +""" + +FMHA_FWD_ONESHOT_API_PER_DTYPE = """ {F_if}(t.data_type.compare(\"{F_dtype}\") == 0){{ +{F_hdim_case} + }} +""" +FMHA_FWD_ONESHOT_API_PER_HDIM_CASE = """ {F_if} (t.hdim_q <= {F_hdim} && t.hdim_v <= {F_hdim_v}) {{ +{F_inner_dispatch} + }} +""" + +FMHA_FWD_ONESHOT_API_INNER_DISPATCH = """ {F_if}((t.is_v_rowmajor == {F_vlayout}) && ({F_mask_check}) && + ({F_scheck}) && ({F_seqtune}) && ({F_skcheck}) && ({F_dcheck}) && ({F_dvcheck}) && ({F_constraint})) {{ + using trait_ = fmha_vsa_fwd_traits_<{F_hdim}, {F_dtype}, {F_bm0}, {F_bn0}, {F_bk0}, {F_bn1}, {F_bk1}, {F_bk0max}, {F_vlayout}, {F_pipeline_enum}, false/*logits*/, {F_mask}, {F_spad}, {F_skpad}, {F_dpad}, {F_dvpad}, {F_trload}>; + fmha_vsa_fwd_oneshot_(s, a); + return; + }} +""" + @dataclass class CppConstraint: @@ -274,10 +324,7 @@ def scheck(self) -> str: @property def seqtune(self) -> str: - if self.bm0 == 128: - return "true/*fall back to largest tile*/" # group mode only generate spad/skpad == true - else: - return f"a.seqlen_q <= {self.bm0}" + return "true" @property def skcheck(self) -> str: @@ -447,6 +494,67 @@ def api(self) -> str: per_tr_load += " (void)t ; (void)s ; (void)a;" return FMHA_FWD_KERNEL_HEADER + FMHA_FWD_API.format(F_dispatch=per_tr_load) + @property + def oneshot_api(self) -> str: + tr_load_cond_map = {"t": "has_load_tr", "f": "true"} + + per_tr_load = str() + for tr_load in ["t", "f"]: + per_dtypes = str() + for i, dtype in enumerate(self.pool.keys()): + per_hdim_case = str() + for j, (hdim, hdim_v) in enumerate(self.pool[dtype].keys()): + traits = [ + t + for t in self.pool[dtype][(hdim, hdim_v)] + if tr_load == t.tr_load + ] + inners = str() + for k, trait in enumerate(traits): + if_k = "if" if k == 0 else "else if" + inners = inners + FMHA_FWD_ONESHOT_API_INNER_DISPATCH.format( + F_if=if_k, + F_vlayout=LAYOUT_MAP[trait.vlayout], + F_pipeline_enum=PIPELINE_ENUM_MAP[trait.pipeline_tag], + F_mask=get_mask_map(self.mask_impl)[trait.mask], + F_mask_check=get_mask_check_map(self.mask_impl)[trait.mask], + F_trload=BOOL_MAP[trait.tr_load], + F_scheck=trait.scheck, + F_seqtune=trait.seqtune, + F_skcheck=trait.skcheck, + F_dcheck=trait.dcheck, + F_dvcheck=trait.dvcheck, + F_constraint=trait.constraint, + F_spad=BOOL_MAP[trait.spad], + F_skpad=BOOL_MAP[trait.skpad], + F_dpad=BOOL_MAP[trait.dpad], + F_dvpad=BOOL_MAP[trait.dvpad], + F_bm0=trait.bm0, + F_bn0=trait.bn0, + F_bk0=trait.bk0, + F_bn1=trait.bn1, + F_bk1=trait.bk1, + F_bk0max=trait.bk0max, + F_hdim=hdim, + F_dtype=FWD_DTYPE_MAP[dtype], + ) + if_j = "if" if j == 0 else "else if" + per_hdim_case = per_hdim_case + FMHA_FWD_ONESHOT_API_PER_HDIM_CASE.format( + F_if=if_j, F_hdim=hdim, F_hdim_v=hdim_v, F_inner_dispatch=inners + ) + if_i = "if" if i == 0 else "else if" + per_dtypes = per_dtypes + FMHA_FWD_ONESHOT_API_PER_DTYPE.format( + F_if=if_i, F_dtype=dtype, F_hdim_case=per_hdim_case + ) + per_tr_load += FMHA_FWD_ONESHOT_API_PER_TRLOAD.format( + F_if="if", + F_trload_cond=tr_load_cond_map[tr_load], + F_dtype_case=per_dtypes, + ) + if not per_tr_load: + per_tr_load += " (void)t ; (void)s ; (void)a;" + return FMHA_FWD_KERNEL_HEADER + FMHA_FWD_ONESHOT_API.format(F_dispatch=per_tr_load) + @dataclass class FmhaFwdTileSize: @@ -582,6 +690,27 @@ def get_hdim_tile_size_dict(dtype: str) -> Optional[dict]: # FmhaFwdTileSize(128, 64, 32, 64, 32, 64, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, -1)], # (96, 128) : [FmhaFwdTileSize(128, 128, 32, 128, 32, 96, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, -1)], (128, 128): [ + FmhaFwdTileSize( # fmt: skip -- 64x128 tile matching blockmap kM0=64, kN0=128 + 64, + 128, + 64, + 128, + 64, + 128, + 4, + 1, + 1, + 4, + 1, + 1, + 16, + 16, + 16, + 16, + 16, + 16, + -1, + ), FmhaFwdTileSize( # fmt: skip 16, 32, @@ -780,7 +909,7 @@ def get_fwd_blobs( for tile, pipeline in itertools.product( tiles, factory.get_pipelines(dtype, hdim, hdim_v, receipt, mask_impl) ): - if tile.F_bm0 != 128 or tile.F_bn0 != 128: + if tile.F_bm0 != 64 or tile.F_bn0 != 128: continue if pipeline.tag != "qr_async_vsa": continue @@ -846,6 +975,7 @@ def write_single_fwd_kernel(kernel: FmhaFwdKernel, autogen_dir: Path) -> None: def write_fwd_api(api_pool: FmhaFwdApiPool, autogen_dir: Path) -> None: update_file(autogen_dir / FMHA_FWD_API_FILENAME, api_pool.api) + update_file(autogen_dir / FMHA_FWD_ONESHOT_API_FILENAME, api_pool.oneshot_api) def write_blobs( @@ -865,3 +995,4 @@ def list_blobs( for kernel in kernels: f.write((file_path.parent / GEN_DIR / kernel.filename).as_posix() + "\n") f.write((file_path.parent / GEN_DIR / FMHA_FWD_API_FILENAME).as_posix() + "\n") + f.write((file_path.parent / GEN_DIR / FMHA_FWD_ONESHOT_API_FILENAME).as_posix() + "\n") diff --git a/example/ck_tile/50_sparse_attn/codegen/ops/sparge_fwd_jenga.py b/example/ck_tile/50_sparse_attn/codegen/ops/sparge_fwd_jenga.py deleted file mode 100644 index 872da2326ea..00000000000 --- a/example/ck_tile/50_sparse_attn/codegen/ops/sparge_fwd_jenga.py +++ /dev/null @@ -1,799 +0,0 @@ -# Copyright (c) Advanced Micro Devices, Inc., or its affiliates. -# SPDX-License-Identifier: MIT -# generate kernel instances to speed up compilation - -import copy -from dataclasses import dataclass, field -import fnmatch -import itertools -import os -import os.path as path -from pathlib import Path -from typing import List, Optional, Tuple - -from codegen.cpp_symbol_map import ( - BOOL_MAP, - FWD_DTYPE_MAP, - LAYOUT_MAP, - MODE_MAP, - PIPELINE_ENUM_MAP, - PIPELINE_MAP, - get_mask_check_map, - get_mask_map, -) - -GEN_DIR = "" - - -def update_file(file_path, content): - """Update the file at file_path with the given content if it differs from the existing content. - - It avoids unnecessary touching of the file which triggers rebuilds - """ - - existing_content = "" - if path.exists(file_path): - with open(file_path, "r") as file: - existing_content = file.read() - if existing_content == content: - return - with open(file_path, "w") as file: - file.write(content) - - -DTYPE_BITS = {"fp32": 32, "fp16": 16, "bf16": 16} - -K0_MAX_SUBMAX_MAP = {32: 32, 64: 64, 96: 128, 128: 128, 192: 192, 256: 256} - -FMHA_FWD_KERNEL_HEADER = """// SPDX-License-Identifier: MIT -// Copyright (c) Advanced Micro Devices, Inc., or its affiliates.\n -// auto generated by generate.py -#include "ck_tile/ops/fmha/block/variants.hpp" -#include "fmha_fwd_trek.hpp" -#include "pipeline/block_fmha_pipeline_qr_ks_vs_async_jenga.hpp" -#include "kernel/fmha_fwd_jenga_kernel.hpp" - -""" - -# NOTE: Jenga sparse attention kernel has the following restrictions enforced by static_assert: -# - Group mode: NOT supported (batch mode only) -# - Bias: NOT supported (NO_BIAS only) -# - LSE output: NOT supported (false only) -# - Dropout: NOT supported (false only) -# - Logits soft-cap: NOT supported (false only) -# - FP8 static quantization: NOT supported (NO_SCALE only) -# The template below hardcodes these unsupported features accordingly. - -FMHA_FWD_KERNEL_BODY = """ -using fmha_dtype_{F_idx} = {F_dtype}; - -using fmha_block_tile_{F_idx} = ck_tile::sequence<{F_bm0}, {F_bn0}, {F_bk0}, {F_bn1}, {F_bk1}, {F_bk0max}>; - -using fmha_shape_{F_idx} = ck_tile::TileFmhaShape, - ck_tile::sequence<{F_wm0}, {F_wn0}, {F_wk0}>, - ck_tile::sequence<{F_rm1}, {F_rn1}, {F_rk1}>, - ck_tile::sequence<{F_wm1}, {F_wn1}, {F_wk1}>, - {F_vlayout}>; - -// TileFmhaTraits: spad, skpad, dpad, dvpad, has_logits_soft_cap, bias_enum, -// store_lse, has_dropout, has_randval, quant_scale_enum, occupancy, is_v_rowmajor_skip -using fmha_trait_{F_idx} = ck_tile::TileFmhaTraits<{F_spad}, - {F_skpad}, - {F_dpad}, - {F_dvpad}, - false, // has_logits_soft_cap - NOT supported - ck_tile::BlockAttentionBiasEnum::NO_BIAS, // bias - NOT supported - false, // store_lse - NOT supported - false, // has_dropout - NOT supported - false, // has_randval - NOT supported - ck_tile::BlockAttentionQuantScaleEnum::NO_SCALE, // FP8 quant - NOT supported - {F_occupancy}, - false>; - -using fmha_variant_{F_idx} = ck_tile::ComposedAttention<0, CK_TILE_FMHA_FWD_FAST_EXP2>; // logits_soft_cap=0 (NOT supported) - -using fmha_mask_{F_idx} = {F_mask}; - -using fmha_pipeline_problem_{F_idx} = ck_tile::BlockFmhaPipelineProblem< - typename FmhaSparseFwdTypeConfig::QDataType, - typename FmhaSparseFwdTypeConfig::KDataType, - typename FmhaSparseFwdTypeConfig::VDataType, - typename FmhaSparseFwdTypeConfig::SaccDataType, - typename FmhaSparseFwdTypeConfig::SMPLComputeDataType, - typename FmhaSparseFwdTypeConfig::BiasDataType, - typename FmhaSparseFwdTypeConfig::RandValOutputDataType, - typename FmhaSparseFwdTypeConfig::LSEDataType, - typename FmhaSparseFwdTypeConfig::PDataType, - typename FmhaSparseFwdTypeConfig::OaccDataType, - typename FmhaSparseFwdTypeConfig::ODataType, - fmha_shape_{F_idx}, - {F_mode}, - fmha_variant_{F_idx}, - fmha_mask_{F_idx}, - {F_trload}, - fmha_trait_{F_idx}>; - -using fmha_pipeline_{F_idx} = {F_pipeline}< - fmha_pipeline_problem_{F_idx}>; - -using fmha_epilogue_{F_idx} = - ck_tile::Default2DEpilogue::OaccDataType, - typename FmhaSparseFwdTypeConfig<{F_dtype}>::ODataType, - {F_spad}, {F_dvpad}>>; - -using fmha_kernel_{F_idx} = - ck_tile::FmhaFwdJengaKernel; - -using trait_{F_idx} = fmha_jenga_fwd_traits_<{F_hdim}, {F_dtype}, {F_bm0}, {F_bn0}, {F_bk0}, {F_bn1}, {F_bk1}, {F_bk0max}, {F_vlayout}, - {F_pipeline_enum}, false/*logits*/, fmha_mask_{F_idx}, {F_spad}, {F_skpad}, {F_dpad}, {F_dvpad}, {F_trload}>; - -#include - -template<> -float fmha_jenga_fwd_(const ck_tile::stream_config& s, fmha_jenga_fwd_args a) -{{ - using k_ = fmha_kernel_{F_idx}; - if(s.log_level_ > 0) - std::cout << ", " << "{F_kernel_name}" << std::flush; - auto [kargs, grids] = fmha_fwd_create_kargs_and_grids(a); - const dim3 blocks = k_::BlockSize(); - constexpr ck_tile::index_t kBlockPerCu = k_::kBlockPerCu; - return ck_tile::launch_kernel(s, ck_tile::make_kernel(k_{{}}, grids, blocks, 0, kargs)); -}} -""" - -FMHA_FWD_API_FILENAME = "sparge_jenga_fwd_api.cpp" -FMHA_FWD_API = """ -#include - -#include - -namespace {{ -bool get_num_cus(unsigned& num_cus) {{ - int device; - auto status = hipGetDevice(&device); - if(status != hipSuccess) {{ - fprintf(stderr, "failed to get device"); - return false; - }} - - hipDeviceProp_t props{{}}; - status = hipGetDeviceProperties(&props, device); - if(status != hipSuccess) {{ - fprintf(stderr, "failed to get device properties"); - return false; - }} - - num_cus = props.multiProcessorCount; - return true; -}} - -unsigned get_num_thread_blocks(unsigned batch, unsigned nheads, unsigned max_seqlen_q, unsigned kM0) {{ - const unsigned num_m_blocks = (max_seqlen_q + kM0 - 1) / kM0; - const unsigned num_n_blocks = 1; // we assume that num_n_blocks is always 1 - - return batch * nheads * num_m_blocks * num_n_blocks; -}} -}} // namespace - -float sparge_jenga_fwd(fmha_jenga_fwd_traits t, fmha_jenga_fwd_args a, const ck_tile::stream_config& s){{ - float r = -1; - - [[maybe_unused]] const float min_cu_util_rate = 0.8; // minimum CU utilization rate - - unsigned num_cus; - if (!get_num_cus(num_cus)) {{ - return r; - }} - - [[maybe_unused]] auto get_num_blocks = [&](unsigned kM0) {{ - return get_num_thread_blocks(a.batch, a.nhead_q, a.max_seqlen_q, kM0); - }}; - - const bool has_load_tr = ck_tile::is_load_tr_supported(); - -{F_dispatch} - return r; -}} -""" - -FMHA_FWD_API_PER_TRLOAD = """ {F_if}({F_trload_cond}){{ -{F_dtype_case} - }} -""" - -FMHA_FWD_API_PER_DTYPE = """ {F_if}(t.data_type.compare(\"{F_dtype}\") == 0){{ -{F_hdim_case} - }} -""" -FMHA_FWD_API_PER_HDIM_CASE = """ {F_if} (t.hdim_q <= {F_hdim} && t.hdim_v <= {F_hdim_v}) {{ -{F_inner_dispatch} - }} -""" - -FMHA_FWD_API_INNER_DISPATCH = """ {F_if}((t.is_v_rowmajor == {F_vlayout}) && ({F_mask_check}) && - ({F_scheck}) && ({F_seqtune}) && ({F_skcheck}) && ({F_dcheck}) && ({F_dvcheck}) && ({F_constraint})) {{ - using trait_ = fmha_jenga_fwd_traits_<{F_hdim}, {F_dtype}, {F_bm0}, {F_bn0}, {F_bk0}, {F_bn1}, {F_bk1}, {F_bk0max}, {F_vlayout}, {F_pipeline_enum}, false/*logits*/, {F_mask}, {F_spad}, {F_skpad}, {F_dpad}, {F_dvpad}, {F_trload}>; - return fmha_jenga_fwd_(s, a); - }} -""" - - -@dataclass -class CppConstraint: - bool_expr: str = None - - def __str__(self): - if self.bool_expr is None: - return "true" - else: - return f"{self.bool_expr}" - - def __and__(self, other): - return CppConstraint(f"({str(self)}) && ({str(other)})") - - -@dataclass -class FmhaFwdApiTrait: - pipeline_tag: str - # sync with fmha_fwd_traits<>, to generate fallback calls - hdim: str - dtype: str # data type - mode: str # value from MODE_MAP - bm0: int # tile size along q seqlen (block size) - bn0: int # tile size along qk seqlen - bk0: int # tile size along qk gemm unroll - bn1: int # tile size along v head_dim - bk1: int # tile size along kv gemm unroll - bk0max: int - vlayout: str - logits: str - mask: str - spad: str - skpad: str - dpad: str - dvpad: str - tr_load: str - constraint: CppConstraint - - @property - def name(self) -> str: - return ( - f"{self.hdim}-{self.dtype}-{self.mode}-{self.bm0}-{self.bn0}-{self.bk0}-{self.bn0}-{self.bk1}-{self.bk0max}-" - + f"{self.vlayout}-{self.logits}-{self.mask}-{self.spad}-{self.skpad}-{self.dpad}-{self.dvpad}" - ) - - @property - def scheck(self) -> str: - if self.mode == "group": - return "true/*group mode spad always true*/" # group mode only generate spad/skpad == true - if self.spad == "t": - return "true" # always support - return "true" - - @property - def seqtune(self) -> str: - return "true" - - @property - def skcheck(self) -> str: - if self.mode == "group": - return "true/*group mode skpad always true*/" # group mode only generate spad/skpad == true - if self.skpad == "t": - return f"a.seqlen_k == 0 || a.seqlen_k % {self.bn0} != 0" - return f"a.seqlen_k != 0 && a.seqlen_k % {self.bn0} == 0" - - @property - def dcheck(self) -> str: - vec = int((32 * 4) / DTYPE_BITS[self.dtype]) - if self.dpad == "t": - return f"a.hdim_q % {vec} == 0" - assert False - - @property - def dvcheck(self) -> str: - vec = int((32 * 4) / DTYPE_BITS[self.dtype]) - if self.dvpad == "t": - return f"a.hdim_v % {vec} == 0" - assert False - - -@dataclass -class FmhaFwdPipeline: - tag: str - - F_vlayout: str # row/col - F_spad: str # true/false - F_skpad: str # - F_dpad: str # - F_dvpad: str # - F_logits: str # t/f - F_mask: str # value from MASK_MAP - F_trload: str # true/false - F_constraint: CppConstraint = field(default_factory=CppConstraint) - - @property - def name(self) -> str: - def pad_name() -> str: - n = "" - if self.F_spad == "t": - n += "s" - if self.F_skpad == "t": - n += "sk" - if self.F_dpad == "t": - n += "d" - if self.F_dvpad == "t": - n += "dv" - if n != "": - n = "p" + n - return n - - pn = pad_name() - n = f"{self.tag}_v{self.F_vlayout[0]}" - if pn != "": - n += f"_{pn}" - else: - n += "_npad" - - if self.F_logits == "t": - n += "_logits" - else: - n += "_nlogits" - - n += "_nbias" - - if self.F_mask[0:2] == "s_": - if self.F_mask == "s_mask": - n += "_mask" - else: - n += "_nmask" - else: - if self.F_mask != "no": - n += f"_m{self.F_mask[0]}" - else: - n += "_nmask" - - n += "_nskip" - - n += "_nsquant" - - if self.F_trload == "t": - n += "_trload" - else: - n += "_ntrload" - - return n - - -class FmhaFwdApiPool: - def __init__(self, mask_impl): - self.pool = dict() - self.mask_impl = mask_impl - - def register_traits(self, trait: FmhaFwdApiTrait) -> None: - # TODO: do we need to check duplication? - if trait.dtype not in self.pool.keys(): - self.pool[trait.dtype] = dict() - hdim = trait.hdim, trait.bn1 - if hdim not in self.pool[trait.dtype].keys(): - self.pool[trait.dtype][hdim] = list() - - self.pool[trait.dtype][hdim].append(copy.copy(trait)) - - @property - def api(self) -> str: - tr_load_cond_map = {"t": "has_load_tr", "f": "true"} - - per_tr_load = str() - for tr_load in ["t", "f"]: - per_dtypes = str() - for i, dtype in enumerate(self.pool.keys()): - per_hdim_case = str() - for j, (hdim, hdim_v) in enumerate(self.pool[dtype].keys()): - traits = [ - t - for t in self.pool[dtype][(hdim, hdim_v)] - if tr_load == t.tr_load - ] - inners = str() - for k, trait in enumerate(traits): - if_k = "if" if k == 0 else "else if" - inners = inners + FMHA_FWD_API_INNER_DISPATCH.format( - F_if=if_k, - F_vlayout=LAYOUT_MAP[trait.vlayout], - F_pipeline_enum=PIPELINE_ENUM_MAP[trait.pipeline_tag], - # F_logits removed - hardcoded to false (NOT supported) - F_mask=get_mask_map(self.mask_impl)[trait.mask], - F_mask_check=get_mask_check_map(self.mask_impl)[trait.mask], - F_trload=BOOL_MAP[trait.tr_load], - F_scheck=trait.scheck, - F_seqtune=trait.seqtune, - F_skcheck=trait.skcheck, - F_dcheck=trait.dcheck, - F_dvcheck=trait.dvcheck, - F_constraint=trait.constraint, - F_spad=BOOL_MAP[trait.spad], - F_skpad=BOOL_MAP[trait.skpad], - F_dpad=BOOL_MAP[trait.dpad], - F_dvpad=BOOL_MAP[trait.dvpad], - F_bm0=trait.bm0, - F_bn0=trait.bn0, - F_bk0=trait.bk0, - F_bn1=trait.bn1, - F_bk1=trait.bk1, - F_bk0max=trait.bk0max, - F_hdim=hdim, - F_dtype=FWD_DTYPE_MAP[dtype], - ) - if_j = "if" if j == 0 else "else if" - per_hdim_case = per_hdim_case + FMHA_FWD_API_PER_HDIM_CASE.format( - F_if=if_j, F_hdim=hdim, F_hdim_v=hdim_v, F_inner_dispatch=inners - ) - if_i = "if" if i == 0 else "else if" - per_dtypes = per_dtypes + FMHA_FWD_API_PER_DTYPE.format( - F_if=if_i, F_dtype=dtype, F_hdim_case=per_hdim_case - ) - per_tr_load += FMHA_FWD_API_PER_TRLOAD.format( - F_if="if", - F_trload_cond=tr_load_cond_map[tr_load], - F_dtype_case=per_dtypes, - ) - if not per_tr_load: - # empty string we add some ignore to suppress warning in api - per_tr_load += " (void)t ; (void)s ; (void)a;" - return FMHA_FWD_KERNEL_HEADER + FMHA_FWD_API.format(F_dispatch=per_tr_load) - - -@dataclass -class FmhaFwdTileSize: - F_bm0: int # tile size along q seqlen (block size) - F_bn0: int # tile size along k seqlen - F_bk0: int # tile size along qk gemm unroll - F_bn1: int # tile size along v head_dim - F_bk1: int # tile size along kv gemm unroll - F_bk0max: int # total length of K0, used for pipeline that need load Q at once (or repeately load Q as a whole tile) - F_rm0: int # number of warps for gemm0 along q seqlen - F_rn0: int # number of warps for gemm0 along k seqlen - F_rk0: int # number of warps for gemm0 along head dim q (not used) - F_rm1: int # number of warps for gemm1 along q seqlen - F_rn1: int # number of warps for gemm1 along head dim v - F_rk1: int # number of warps for gemm1 along k seqlen (not used) - F_wm0: int # gemm0 warp size along m - F_wn0: int # gemm0 warp size along n - F_wk0: int # gemm0 warp size along k - F_wm1: int # gemm1 warp size along m - F_wn1: int # gemm1 warp size along n - F_wk1: int # gemm1 warp size along k - F_occupancy: int # occupancy, -1 will let pipeline decide the occupancy, other value will overwrite occupancy - F_constraint: CppConstraint = field(default_factory=CppConstraint) - - @property - def name(self) -> str: - return ( - f"b{self.F_bm0}x{self.F_bn0}x{self.F_bk0}x{self.F_bn1}x{self.F_bk1}x{self.F_bk0max}" - + f"_r{self.F_rm0}x{self.F_rn0}x{self.F_rk0}_r{self.F_rm1}x{self.F_rn1}x{self.F_rk1}" - + f"_w{self.F_wm0}x{self.F_wn0}x{self.F_wk0}_w{self.F_wm1}x{self.F_wn1}x{self.F_wk1}" - + ("" if self.F_occupancy == -1 else f"_o{self.F_occupancy}") - ) - - -@dataclass -class FmhaFwdKernel: - F_idx: int # this is not a tunable, but a counter to differentiate symbol - F_hdim: int # hdim - F_dtype: str # data type - F_mode: str # value from MODE_MAP - F_tile: FmhaFwdTileSize - F_pipeline: FmhaFwdPipeline - mask_impl: str - - @property - def template(self) -> str: - # kernel_body removed - unused - return FMHA_FWD_KERNEL_HEADER + FMHA_FWD_KERNEL_BODY.format( - F_idx=self.F_idx, - F_hdim=self.F_hdim, - F_dtype=FWD_DTYPE_MAP[self.F_dtype], - F_bm0=self.F_tile.F_bm0, - F_bn0=self.F_tile.F_bn0, - F_bk0=self.F_tile.F_bk0, - F_bn1=self.F_tile.F_bn1, - F_bk1=self.F_tile.F_bk1, - F_bk0max=self.F_tile.F_bk0max, - F_rm0=self.F_tile.F_rm0, - F_rn0=self.F_tile.F_rn0, - F_rk0=self.F_tile.F_rk0, - F_rm1=self.F_tile.F_rm1, - F_rn1=self.F_tile.F_rn1, - F_rk1=self.F_tile.F_rk1, - F_wm0=self.F_tile.F_wm0, - F_wn0=self.F_tile.F_wn0, - F_wk0=self.F_tile.F_wk0, - F_wm1=self.F_tile.F_wm1, - F_wn1=self.F_tile.F_wn1, - F_wk1=self.F_tile.F_wk1, - F_vlayout=LAYOUT_MAP[self.F_pipeline.F_vlayout], - F_spad=BOOL_MAP[self.F_pipeline.F_spad], - F_skpad=BOOL_MAP[self.F_pipeline.F_skpad], - F_dpad=BOOL_MAP[self.F_pipeline.F_dpad], - F_dvpad=BOOL_MAP[self.F_pipeline.F_dvpad], - # F_logits removed - hardcoded to false in template (NOT supported) - F_occupancy=self.F_tile.F_occupancy, - F_pipeline_enum=PIPELINE_ENUM_MAP[self.F_pipeline.tag], - F_mask=get_mask_map(self.mask_impl)[self.F_pipeline.F_mask], - F_mode=MODE_MAP[self.F_mode], - F_pipeline=PIPELINE_MAP[self.F_pipeline.tag], - F_trload=BOOL_MAP[self.F_pipeline.F_trload], - F_kernel_name=self.name, - ) - - @property - def name(self) -> str: - # TODO: we don't encode idx here - return ( - f"fmha_jenga_fwd_d{self.F_hdim}_{self.F_dtype}_{self.F_mode}_" - + self.F_tile.name - + "_" - + self.F_pipeline.name - ) - - @property - def filename(self) -> str: - return self.name + ".cpp" - - def api_trait(self) -> FmhaFwdApiTrait: - return FmhaFwdApiTrait( - pipeline_tag=self.F_pipeline.tag, - hdim=str(self.F_hdim), - dtype=self.F_dtype, - mode=self.F_mode, - bm0=self.F_tile.F_bm0, - bn0=self.F_tile.F_bn0, - bk0=self.F_tile.F_bk0, - bn1=self.F_tile.F_bn1, - bk1=self.F_tile.F_bk1, - bk0max=self.F_tile.F_bk0max, - vlayout=self.F_pipeline.F_vlayout, - mask=self.F_pipeline.F_mask, - logits=self.F_pipeline.F_logits, - spad=self.F_pipeline.F_spad, - skpad=self.F_pipeline.F_skpad, - dpad=self.F_pipeline.F_dpad, - dvpad=self.F_pipeline.F_dvpad, - tr_load=self.F_pipeline.F_trload, - constraint=self.F_tile.F_constraint & self.F_pipeline.F_constraint, - ) - - -class KernelComponentFactory: - # TODO: design a more practical way to do it - # this is current supported tile size per hdim - @staticmethod - def get_hdim_tile_size_dict(dtype: str) -> Optional[dict]: - if dtype == "fp16" or dtype == "bf16": - return { - # (32, 32) : [FmhaFwdTileSize(128, 64, 16, 32, 32, 32, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, -1)], - # (64, 64) : [FmhaFwdTileSize(16, 32, 64, 64, 32, 64, 1, 1, 1, 1, 1, 1, 16, 16, 32, 16, 16, 32, -1), - # FmhaFwdTileSize(32, 32, 64, 64, 32, 64, 1, 1, 1, 1, 1, 1, 32, 32, 16, 32, 32, 16, -1), - # FmhaFwdTileSize(128, 64, 32, 64, 32, 64, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, -1)], - # (96, 128) : [FmhaFwdTileSize(128, 128, 32, 128, 32, 96, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, -1)], - (128, 128): [ - FmhaFwdTileSize( - 64, - 128, - 64, - 128, - 64, - 128, - 4, - 1, - 1, - 4, - 1, - 1, - 16, - 16, - 16, - 16, - 16, - 16, - -1, - ), - ], - # (160,160) : [FmhaFwdTileSize(128, 128, 32, 160, 32, 160, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, 1)], - # (192,128) : [FmhaFwdTileSize(128, 128, 32, 128, 32, 192, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, -1)], - # (192,192) : [FmhaFwdTileSize(128, 128, 32, 192, 32, 192, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, 1)], - # (256,256) : [FmhaFwdTileSize(128, 128, 32, 256, 32, 256, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, -1)], - } - else: - return None - - # TODO: we don't support tuning yet, so pick up one value for vlayout/pipeline/pad - # support this in future - @staticmethod - def get_pipelines(dtype, hdim, hdim_v, receipt, mask_impl) -> List[FmhaFwdPipeline]: - # this function will populate a list possible pipelines - # TODO: the order of List matters! the later in this list will be also be checked later - # NOTE: logits soft-cap is NOT supported by Jenga sparse attention (enforced by static_assert) - pipelines = [] - if dtype in ["fp16", "bf16"]: - for logits, mask in itertools.product( - ["f"], # logits soft-cap NOT supported, always false - get_mask_map(mask_impl).keys(), - ): - if hdim == 256 and hdim_v == 256: - # jenga fmha only supports dim <= 192 for now. - continue - pipelines.append( - FmhaFwdPipeline( # fmt: skip - "qr_async", - "row", - "t", - "f", - "t", - "t", - logits, - mask, - "f", - ) - ) - pipelines.append( - FmhaFwdPipeline( # fmt: skip - "qr_async", - "row", - "t", - "t", - "t", - "t", - logits, - mask, - "f", - ) - ) - else: - assert False - return pipelines - - -class CustomFactory(KernelComponentFactory): - @staticmethod - def get_hdim_tile_size_dict(dtype: str) -> Optional[dict]: - result = KernelComponentFactory.get_hdim_tile_size_dict(dtype) - if dtype == "fp16" or dtype == "bf16": - if (128, 128) in result.keys(): - result[(128, 128)].insert( - 0, - FmhaFwdTileSize( - 64, - 128, - 64, - 128, - 64, - 128, - 4, - 1, - 1, - 4, - 1, - 1, - 16, - 16, - 16, - 16, - 16, - 16, - -1, - CppConstraint( - "get_num_blocks(128) < num_cus * min_cu_util_rate" - ), - ), - ) - return result - - -def get_fwd_blobs( - kernel_filter: Optional[str], receipt, optdim_list, mask_impl -) -> Tuple[FmhaFwdApiPool, List[FmhaFwdKernel]]: - gen = list() - api_pool = FmhaFwdApiPool(mask_impl) - - factory = ( - CustomFactory - if os.environ.get("CK_TILE_FMHA_FWD_CUSTOM_FACTORY", "0") == "1" - else KernelComponentFactory - ) - - # Only generate fp16/bf16 kernels for now. - # NOTE: Jenga sparse attention only supports batch mode (group mode NOT supported, enforced by static_assert) - for dtype in ["fp16", "bf16"]: - d = factory.get_hdim_tile_size_dict(dtype) - if d is None: - continue - for ((hdim, hdim_v), tiles), mode in itertools.product(d.items(), ["batch"]): - for tile, pipeline in itertools.product( - tiles, factory.get_pipelines(dtype, hdim, hdim_v, receipt, mask_impl) - ): - if pipeline.tag != "qr_async": - continue - k = FmhaFwdKernel( - F_idx=2, - F_hdim=hdim, - F_dtype=dtype, - F_mode=mode, - F_tile=tile, - F_pipeline=pipeline, - mask_impl=mask_impl, - ) - if kernel_filter != "": - if not fnmatch.fnmatch(k.name, kernel_filter): - continue - if optdim_list != [-1]: - if hdim not in optdim_list: - continue - # 2 - Flash attention integration - if receipt in (2, 3): - cond = dtype in ["fp16", "bf16"] - cond &= pipeline.F_vlayout == "row" - if not cond: - continue - # PyTorch integration - elif receipt == 4: - cond = dtype in ["fp16", "bf16"] - cond &= pipeline.F_vlayout == "row" - cond &= mode == "batch" - cond &= pipeline.F_logits == "f" - if not cond: - continue - # Aiter(mha_fwd) integration - elif receipt == 100: - cond = dtype in ["fp16", "bf16"] - cond &= mode == "batch" - cond &= pipeline.F_vlayout == "row" - if not cond: - continue - # Aiter(mha_varlen_fwd) integration - elif receipt == 200: - cond = dtype in ["fp16", "bf16"] - cond &= mode == "group" - cond &= pipeline.F_vlayout == "row" - if not cond: - continue - # aiter::mha_fwd C++ api integration - elif receipt == 600: - cond = dtype in ["fp16", "bf16"] - cond &= pipeline.F_vlayout == "row" - if not cond: - continue - - api_pool.register_traits(k.api_trait()) - gen.append(k) - - return (api_pool, gen) - - -def write_single_fwd_kernel(kernel: FmhaFwdKernel, autogen_dir: Path) -> None: - update_file(autogen_dir / kernel.filename, kernel.template) - - -def write_fwd_api(api_pool: FmhaFwdApiPool, autogen_dir: Path) -> None: - update_file(autogen_dir / FMHA_FWD_API_FILENAME, api_pool.api) - - -def write_blobs( - output_dir: Path, kernel_filter: str, receipt, optdim_list, mask_impl -) -> None: - api_pool, kernels = get_fwd_blobs(kernel_filter, receipt, optdim_list, mask_impl) - for kernel in kernels: - write_single_fwd_kernel(kernel, output_dir) - write_fwd_api(api_pool, output_dir) - - -def list_blobs( - file_path: Path, kernel_filter: str, receipt, optdim_list, mask_impl -) -> None: - with file_path.open("a") as f: - _, kernels = get_fwd_blobs(kernel_filter, receipt, optdim_list, mask_impl) - for kernel in kernels: - f.write((file_path.parent / GEN_DIR / kernel.filename).as_posix() + "\n") - f.write((file_path.parent / GEN_DIR / FMHA_FWD_API_FILENAME).as_posix() + "\n") diff --git a/example/ck_tile/50_sparse_attn/codegen/ops/sparge_fwd_vsa.py b/example/ck_tile/50_sparse_attn/codegen/ops/sparge_fwd_vsa.py deleted file mode 100644 index c9a389df3fa..00000000000 --- a/example/ck_tile/50_sparse_attn/codegen/ops/sparge_fwd_vsa.py +++ /dev/null @@ -1,799 +0,0 @@ -# Copyright (c) Advanced Micro Devices, Inc., or its affiliates. -# SPDX-License-Identifier: MIT -# generate kernel instances to speed up compilation - -import copy -from dataclasses import dataclass, field -import fnmatch -import itertools -import os -import os.path as path -from pathlib import Path -from typing import List, Optional, Tuple - -from codegen.cpp_symbol_map import ( - BOOL_MAP, - FWD_DTYPE_MAP, - LAYOUT_MAP, - MODE_MAP, - PIPELINE_ENUM_MAP, - PIPELINE_MAP, - get_mask_check_map, - get_mask_map, -) - -GEN_DIR = "" - - -def update_file(file_path, content): - """Update the file at file_path with the given content if it differs from the existing content. - - It avoids unnecessary touching of the file which triggers rebuilds - """ - - existing_content = "" - if path.exists(file_path): - with open(file_path, "r") as file: - existing_content = file.read() - if existing_content == content: - return - with open(file_path, "w") as file: - file.write(content) - - -DTYPE_BITS = {"fp32": 32, "fp16": 16, "bf16": 16} - -K0_MAX_SUBMAX_MAP = {32: 32, 64: 64, 96: 128, 128: 128, 192: 192, 256: 256} - -FMHA_FWD_KERNEL_HEADER = """// SPDX-License-Identifier: MIT -// Copyright (c) Advanced Micro Devices, Inc., or its affiliates.\n -// auto generated by generate.py -#include "ck_tile/ops/fmha/block/variants.hpp" -#include "fmha_fwd_trek.hpp" -#include "pipeline/block_fmha_pipeline_qr_ks_vs_async_vsa.hpp" -#include "kernel/fmha_fwd_vsa_kernel.hpp" - -""" - -# NOTE: VSA sparse attention kernel has the following restrictions enforced by static_assert: -# - Group mode: NOT supported (batch mode only) -# - Bias: NOT supported (NO_BIAS only) -# - LSE output: NOT supported (false only) -# - Dropout: NOT supported (false only) -# - Logits soft-cap: NOT supported (false only) -# - FP8 static quantization: NOT supported (NO_SCALE only) -# The template below hardcodes these unsupported features accordingly. - -FMHA_FWD_KERNEL_BODY = """ -using fmha_dtype_{F_idx} = {F_dtype}; - -using fmha_block_tile_{F_idx} = ck_tile::sequence<{F_bm0}, {F_bn0}, {F_bk0}, {F_bn1}, {F_bk1}, {F_bk0max}>; - -using fmha_shape_{F_idx} = ck_tile::TileFmhaShape, - ck_tile::sequence<{F_wm0}, {F_wn0}, {F_wk0}>, - ck_tile::sequence<{F_rm1}, {F_rn1}, {F_rk1}>, - ck_tile::sequence<{F_wm1}, {F_wn1}, {F_wk1}>, - {F_vlayout}>; - -// TileFmhaTraits: spad, skpad, dpad, dvpad, has_logits_soft_cap, bias_enum, -// store_lse, has_dropout, has_randval, quant_scale_enum, occupancy, is_v_rowmajor_skip -using fmha_trait_{F_idx} = ck_tile::TileFmhaTraits<{F_spad}, - {F_skpad}, - {F_dpad}, - {F_dvpad}, - false, // has_logits_soft_cap - NOT supported - ck_tile::BlockAttentionBiasEnum::NO_BIAS, // bias - NOT supported - false, // store_lse - NOT supported - false, // has_dropout - NOT supported - false, // has_randval - NOT supported - ck_tile::BlockAttentionQuantScaleEnum::NO_SCALE, // FP8 quant - NOT supported - {F_occupancy}, - false>; - -using fmha_variant_{F_idx} = ck_tile::ComposedAttention<0, CK_TILE_FMHA_FWD_FAST_EXP2>; // logits_soft_cap=0 (NOT supported) - -using fmha_mask_{F_idx} = {F_mask}; - -using fmha_pipeline_problem_{F_idx} = ck_tile::BlockFmhaPipelineProblem< - typename FmhaSparseFwdTypeConfig::QDataType, - typename FmhaSparseFwdTypeConfig::KDataType, - typename FmhaSparseFwdTypeConfig::VDataType, - typename FmhaSparseFwdTypeConfig::SaccDataType, - typename FmhaSparseFwdTypeConfig::SMPLComputeDataType, - typename FmhaSparseFwdTypeConfig::BiasDataType, - typename FmhaSparseFwdTypeConfig::RandValOutputDataType, - typename FmhaSparseFwdTypeConfig::LSEDataType, - typename FmhaSparseFwdTypeConfig::PDataType, - typename FmhaSparseFwdTypeConfig::OaccDataType, - typename FmhaSparseFwdTypeConfig::ODataType, - fmha_shape_{F_idx}, - {F_mode}, - fmha_variant_{F_idx}, - fmha_mask_{F_idx}, - {F_trload}, - fmha_trait_{F_idx}>; - -using fmha_pipeline_{F_idx} = ck_tile::BlockFmhaPipelineQRKSVSAsyncVSA< - fmha_pipeline_problem_{F_idx}>; - -using fmha_epilogue_{F_idx} = - ck_tile::Default2DEpilogue::OaccDataType, - typename FmhaSparseFwdTypeConfig<{F_dtype}>::ODataType, - {F_spad}, {F_dvpad}>>; - -using fmha_kernel_{F_idx} = - ck_tile::FmhaFwdVSAKernel; - -using trait_{F_idx} = fmha_vsa_fwd_traits_<{F_hdim}, {F_dtype}, {F_bm0}, {F_bn0}, {F_bk0}, {F_bn1}, {F_bk1}, {F_bk0max}, {F_vlayout}, - {F_pipeline_enum}, false/*logits*/, fmha_mask_{F_idx}, {F_spad}, {F_skpad}, {F_dpad}, {F_dvpad}, {F_trload}>; - -#include - -template<> -float fmha_vsa_fwd_(const ck_tile::stream_config& s, fmha_vsa_fwd_args a) -{{ - using k_ = fmha_kernel_{F_idx}; - if(s.log_level_ > 0) - std::cout << ", " << "{F_kernel_name}" << std::flush; - auto [kargs, grids] = fmha_fwd_create_kargs_and_grids(a); - const dim3 blocks = k_::BlockSize(); - constexpr ck_tile::index_t kBlockPerCu = k_::kBlockPerCu; - return ck_tile::launch_kernel(s, ck_tile::make_kernel(k_{{}}, grids, blocks, 0, kargs)); -}} -""" - -FMHA_FWD_API_FILENAME = "sparge_vsa_fwd_api.cpp" -FMHA_FWD_API = """ -#include - -#include - -namespace {{ -bool get_num_cus(unsigned& num_cus) {{ - int device; - auto status = hipGetDevice(&device); - if(status != hipSuccess) {{ - fprintf(stderr, "failed to get device"); - return false; - }} - - hipDeviceProp_t props{{}}; - status = hipGetDeviceProperties(&props, device); - if(status != hipSuccess) {{ - fprintf(stderr, "failed to get device properties"); - return false; - }} - - num_cus = props.multiProcessorCount; - return true; -}} - -unsigned get_num_thread_blocks(unsigned batch, unsigned nheads, unsigned max_seqlen_q, unsigned kM0) {{ - const unsigned num_m_blocks = (max_seqlen_q + kM0 - 1) / kM0; - const unsigned num_n_blocks = 1; // we assume that num_n_blocks is always 1 - - return batch * nheads * num_m_blocks * num_n_blocks; -}} -}} // namespace - -float sparge_vsa_fwd(fmha_vsa_fwd_traits t, fmha_vsa_fwd_args a, const ck_tile::stream_config& s){{ - float r = -1; - - [[maybe_unused]] const float min_cu_util_rate = 0.8; // minimum CU utilization rate - - unsigned num_cus; - if (!get_num_cus(num_cus)) {{ - return r; - }} - - [[maybe_unused]] auto get_num_blocks = [&](unsigned kM0) {{ - return get_num_thread_blocks(a.batch, a.nhead_q, a.max_seqlen_q, kM0); - }}; - - const bool has_load_tr = ck_tile::is_load_tr_supported(); - -{F_dispatch} - return r; -}} -""" - -FMHA_FWD_API_PER_TRLOAD = """ {F_if}({F_trload_cond}){{ -{F_dtype_case} - }} -""" - -FMHA_FWD_API_PER_DTYPE = """ {F_if}(t.data_type.compare(\"{F_dtype}\") == 0){{ -{F_hdim_case} - }} -""" -FMHA_FWD_API_PER_HDIM_CASE = """ {F_if} (t.hdim_q <= {F_hdim} && t.hdim_v <= {F_hdim_v}) {{ -{F_inner_dispatch} - }} -""" - -FMHA_FWD_API_INNER_DISPATCH = """ {F_if}((t.is_v_rowmajor == {F_vlayout}) && ({F_mask_check}) && - ({F_scheck}) && ({F_seqtune}) && ({F_skcheck}) && ({F_dcheck}) && ({F_dvcheck}) && ({F_constraint})) {{ - using trait_ = fmha_vsa_fwd_traits_<{F_hdim}, {F_dtype}, {F_bm0}, {F_bn0}, {F_bk0}, {F_bn1}, {F_bk1}, {F_bk0max}, {F_vlayout}, {F_pipeline_enum}, false/*logits*/, {F_mask}, {F_spad}, {F_skpad}, {F_dpad}, {F_dvpad}, {F_trload}>; - return fmha_vsa_fwd_(s, a); - }} -""" - - -@dataclass -class CppConstraint: - bool_expr: str = None - - def __str__(self): - if self.bool_expr is None: - return "true" - else: - return f"{self.bool_expr}" - - def __and__(self, other): - return CppConstraint(f"({str(self)}) && ({str(other)})") - - -@dataclass -class FmhaFwdApiTrait: - pipeline_tag: str - # sync with fmha_fwd_traits<>, to generate fallback calls - hdim: str - dtype: str # data type - mode: str # value from MODE_MAP - bm0: int # tile size along q seqlen (block size) - bn0: int # tile size along qk seqlen - bk0: int # tile size along qk gemm unroll - bn1: int # tile size along v head_dim - bk1: int # tile size along kv gemm unroll - bk0max: int - vlayout: str - logits: str - mask: str - spad: str - skpad: str - dpad: str - dvpad: str - tr_load: str - constraint: CppConstraint - - @property - def name(self) -> str: - return ( - f"{self.hdim}-{self.dtype}-{self.mode}-{self.bm0}-{self.bn0}-{self.bk0}-{self.bn0}-{self.bk1}-{self.bk0max}-" - + f"{self.vlayout}-{self.logits}-{self.mask}-{self.spad}-{self.skpad}-{self.dpad}-{self.dvpad}" - ) - - @property - def scheck(self) -> str: - if self.mode == "group": - return "true/*group mode spad always true*/" # group mode only generate spad/skpad == true - if self.spad == "t": - return "true" # always support - return "true" - - @property - def seqtune(self) -> str: - return "true" - - @property - def skcheck(self) -> str: - if self.mode == "group": - return "true/*group mode skpad always true*/" # group mode only generate spad/skpad == true - if self.skpad == "t": - return f"a.seqlen_k == 0 || a.seqlen_k % {self.bn0} != 0" - return f"a.seqlen_k != 0 && a.seqlen_k % {self.bn0} == 0" - - @property - def dcheck(self) -> str: - vec = int((32 * 4) / DTYPE_BITS[self.dtype]) - if self.dpad == "t": - return f"a.hdim_q % {vec} == 0" - assert False - - @property - def dvcheck(self) -> str: - vec = int((32 * 4) / DTYPE_BITS[self.dtype]) - if self.dvpad == "t": - return f"a.hdim_v % {vec} == 0" - assert False - - -@dataclass -class FmhaFwdPipeline: - tag: str - - F_vlayout: str # row/col - F_spad: str # true/false - F_skpad: str # - F_dpad: str # - F_dvpad: str # - F_logits: str # t/f - F_mask: str # value from MASK_MAP - F_trload: str # true/false - F_constraint: CppConstraint = field(default_factory=CppConstraint) - - @property - def name(self) -> str: - def pad_name() -> str: - n = "" - if self.F_spad == "t": - n += "s" - if self.F_skpad == "t": - n += "sk" - if self.F_dpad == "t": - n += "d" - if self.F_dvpad == "t": - n += "dv" - if n != "": - n = "p" + n - return n - - pn = pad_name() - n = f"{self.tag}_v{self.F_vlayout[0]}" - if pn != "": - n += f"_{pn}" - else: - n += "_npad" - - if self.F_logits == "t": - n += "_logits" - else: - n += "_nlogits" - - n += "_nbias" - - if self.F_mask[0:2] == "s_": - if self.F_mask == "s_mask": - n += "_mask" - else: - n += "_nmask" - else: - if self.F_mask != "no": - n += f"_m{self.F_mask[0]}" - else: - n += "_nmask" - - n += "_nskip" - - n += "_nsquant" - - if self.F_trload == "t": - n += "_trload" - else: - n += "_ntrload" - - return n - - -class FmhaFwdApiPool: - def __init__(self, mask_impl): - self.pool = dict() - self.mask_impl = mask_impl - - def register_traits(self, trait: FmhaFwdApiTrait) -> None: - # TODO: do we need to check duplication? - if trait.dtype not in self.pool.keys(): - self.pool[trait.dtype] = dict() - hdim = trait.hdim, trait.bn1 - if hdim not in self.pool[trait.dtype].keys(): - self.pool[trait.dtype][hdim] = list() - - self.pool[trait.dtype][hdim].append(copy.copy(trait)) - - @property - def api(self) -> str: - tr_load_cond_map = {"t": "has_load_tr", "f": "true"} - - per_tr_load = str() - for tr_load in ["t", "f"]: - per_dtypes = str() - for i, dtype in enumerate(self.pool.keys()): - per_hdim_case = str() - for j, (hdim, hdim_v) in enumerate(self.pool[dtype].keys()): - traits = [ - t - for t in self.pool[dtype][(hdim, hdim_v)] - if tr_load == t.tr_load - ] - inners = str() - for k, trait in enumerate(traits): - if_k = "if" if k == 0 else "else if" - inners = inners + FMHA_FWD_API_INNER_DISPATCH.format( - F_if=if_k, - F_vlayout=LAYOUT_MAP[trait.vlayout], - F_pipeline_enum=PIPELINE_ENUM_MAP[trait.pipeline_tag], - # F_logits removed - hardcoded to false (NOT supported) - F_mask=get_mask_map(self.mask_impl)[trait.mask], - F_mask_check=get_mask_check_map(self.mask_impl)[trait.mask], - F_trload=BOOL_MAP[trait.tr_load], - F_scheck=trait.scheck, - F_seqtune=trait.seqtune, - F_skcheck=trait.skcheck, - F_dcheck=trait.dcheck, - F_dvcheck=trait.dvcheck, - F_constraint=trait.constraint, - F_spad=BOOL_MAP[trait.spad], - F_skpad=BOOL_MAP[trait.skpad], - F_dpad=BOOL_MAP[trait.dpad], - F_dvpad=BOOL_MAP[trait.dvpad], - F_bm0=trait.bm0, - F_bn0=trait.bn0, - F_bk0=trait.bk0, - F_bn1=trait.bn1, - F_bk1=trait.bk1, - F_bk0max=trait.bk0max, - F_hdim=hdim, - F_dtype=FWD_DTYPE_MAP[dtype], - ) - if_j = "if" if j == 0 else "else if" - per_hdim_case = per_hdim_case + FMHA_FWD_API_PER_HDIM_CASE.format( - F_if=if_j, F_hdim=hdim, F_hdim_v=hdim_v, F_inner_dispatch=inners - ) - if_i = "if" if i == 0 else "else if" - per_dtypes = per_dtypes + FMHA_FWD_API_PER_DTYPE.format( - F_if=if_i, F_dtype=dtype, F_hdim_case=per_hdim_case - ) - per_tr_load += FMHA_FWD_API_PER_TRLOAD.format( - F_if="if", - F_trload_cond=tr_load_cond_map[tr_load], - F_dtype_case=per_dtypes, - ) - if not per_tr_load: - # empty string we add some ignore to suppress warning in api - per_tr_load += " (void)t ; (void)s ; (void)a;" - return FMHA_FWD_KERNEL_HEADER + FMHA_FWD_API.format(F_dispatch=per_tr_load) - - -@dataclass -class FmhaFwdTileSize: - F_bm0: int # tile size along q seqlen (block size) - F_bn0: int # tile size along k seqlen - F_bk0: int # tile size along qk gemm unroll - F_bn1: int # tile size along v head_dim - F_bk1: int # tile size along kv gemm unroll - F_bk0max: int # total length of K0, used for pipeline that need load Q at once (or repeately load Q as a whole tile) - F_rm0: int # number of warps for gemm0 along q seqlen - F_rn0: int # number of warps for gemm0 along k seqlen - F_rk0: int # number of warps for gemm0 along head dim q (not used) - F_rm1: int # number of warps for gemm1 along q seqlen - F_rn1: int # number of warps for gemm1 along head dim v - F_rk1: int # number of warps for gemm1 along k seqlen (not used) - F_wm0: int # gemm0 warp size along m - F_wn0: int # gemm0 warp size along n - F_wk0: int # gemm0 warp size along k - F_wm1: int # gemm1 warp size along m - F_wn1: int # gemm1 warp size along n - F_wk1: int # gemm1 warp size along k - F_occupancy: int # occupancy, -1 will let pipeline decide the occupancy, other value will overwrite occupancy - F_constraint: CppConstraint = field(default_factory=CppConstraint) - - @property - def name(self) -> str: - return ( - f"b{self.F_bm0}x{self.F_bn0}x{self.F_bk0}x{self.F_bn1}x{self.F_bk1}x{self.F_bk0max}" - + f"_r{self.F_rm0}x{self.F_rn0}x{self.F_rk0}_r{self.F_rm1}x{self.F_rn1}x{self.F_rk1}" - + f"_w{self.F_wm0}x{self.F_wn0}x{self.F_wk0}_w{self.F_wm1}x{self.F_wn1}x{self.F_wk1}" - + ("" if self.F_occupancy == -1 else f"_o{self.F_occupancy}") - ) - - -@dataclass -class FmhaFwdKernel: - F_idx: int # this is not a tunable, but a counter to differentiate symbol - F_hdim: int # hdim - F_dtype: str # data type - F_mode: str # value from MODE_MAP - F_tile: FmhaFwdTileSize - F_pipeline: FmhaFwdPipeline - mask_impl: str - - @property - def template(self) -> str: - # kernel_body removed - unused - return FMHA_FWD_KERNEL_HEADER + FMHA_FWD_KERNEL_BODY.format( - F_idx=self.F_idx, - F_hdim=self.F_hdim, - F_dtype=FWD_DTYPE_MAP[self.F_dtype], - F_bm0=self.F_tile.F_bm0, - F_bn0=self.F_tile.F_bn0, - F_bk0=self.F_tile.F_bk0, - F_bn1=self.F_tile.F_bn1, - F_bk1=self.F_tile.F_bk1, - F_bk0max=self.F_tile.F_bk0max, - F_rm0=self.F_tile.F_rm0, - F_rn0=self.F_tile.F_rn0, - F_rk0=self.F_tile.F_rk0, - F_rm1=self.F_tile.F_rm1, - F_rn1=self.F_tile.F_rn1, - F_rk1=self.F_tile.F_rk1, - F_wm0=self.F_tile.F_wm0, - F_wn0=self.F_tile.F_wn0, - F_wk0=self.F_tile.F_wk0, - F_wm1=self.F_tile.F_wm1, - F_wn1=self.F_tile.F_wn1, - F_wk1=self.F_tile.F_wk1, - F_vlayout=LAYOUT_MAP[self.F_pipeline.F_vlayout], - F_spad=BOOL_MAP[self.F_pipeline.F_spad], - F_skpad=BOOL_MAP[self.F_pipeline.F_skpad], - F_dpad=BOOL_MAP[self.F_pipeline.F_dpad], - F_dvpad=BOOL_MAP[self.F_pipeline.F_dvpad], - # F_logits removed - hardcoded to false in template (NOT supported) - F_occupancy=self.F_tile.F_occupancy, - F_pipeline_enum=PIPELINE_ENUM_MAP[self.F_pipeline.tag], - F_mask=get_mask_map(self.mask_impl)[self.F_pipeline.F_mask], - F_mode=MODE_MAP[self.F_mode], - F_pipeline=PIPELINE_MAP[self.F_pipeline.tag], - F_trload=BOOL_MAP[self.F_pipeline.F_trload], - F_kernel_name=self.name, - ) - - @property - def name(self) -> str: - # TODO: we don't encode idx here - return ( - f"fmha_vsa_fwd_d{self.F_hdim}_{self.F_dtype}_{self.F_mode}_" - + self.F_tile.name - + "_" - + self.F_pipeline.name - ) - - @property - def filename(self) -> str: - return self.name + ".cpp" - - def api_trait(self) -> FmhaFwdApiTrait: - return FmhaFwdApiTrait( - pipeline_tag=self.F_pipeline.tag, - hdim=str(self.F_hdim), - dtype=self.F_dtype, - mode=self.F_mode, - bm0=self.F_tile.F_bm0, - bn0=self.F_tile.F_bn0, - bk0=self.F_tile.F_bk0, - bn1=self.F_tile.F_bn1, - bk1=self.F_tile.F_bk1, - bk0max=self.F_tile.F_bk0max, - vlayout=self.F_pipeline.F_vlayout, - mask=self.F_pipeline.F_mask, - logits=self.F_pipeline.F_logits, - spad=self.F_pipeline.F_spad, - skpad=self.F_pipeline.F_skpad, - dpad=self.F_pipeline.F_dpad, - dvpad=self.F_pipeline.F_dvpad, - tr_load=self.F_pipeline.F_trload, - constraint=self.F_tile.F_constraint & self.F_pipeline.F_constraint, - ) - - -class KernelComponentFactory: - # TODO: design a more practical way to do it - # this is current supported tile size per hdim - @staticmethod - def get_hdim_tile_size_dict(dtype: str) -> Optional[dict]: - if dtype == "fp16" or dtype == "bf16": - return { - # (32, 32) : [FmhaFwdTileSize(128, 64, 16, 32, 32, 32, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, -1)], - # (64, 64) : [FmhaFwdTileSize(16, 32, 64, 64, 32, 64, 1, 1, 1, 1, 1, 1, 16, 16, 32, 16, 16, 32, -1), - # FmhaFwdTileSize(32, 32, 64, 64, 32, 64, 1, 1, 1, 1, 1, 1, 32, 32, 16, 32, 32, 16, -1), - # FmhaFwdTileSize(128, 64, 32, 64, 32, 64, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, -1)], - # (96, 128) : [FmhaFwdTileSize(128, 128, 32, 128, 32, 96, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, -1)], - (128, 128): [ - FmhaFwdTileSize( - 64, - 128, - 64, - 128, - 64, - 128, - 4, - 1, - 1, - 4, - 1, - 1, - 16, - 16, - 16, - 16, - 16, - 16, - -1, - ), - ], - # (160,160) : [FmhaFwdTileSize(128, 128, 32, 160, 32, 160, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, 1)], - # (192,128) : [FmhaFwdTileSize(128, 128, 32, 128, 32, 192, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, -1)], - # (192,192) : [FmhaFwdTileSize(128, 128, 32, 192, 32, 192, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, 1)], - # (256,256) : [FmhaFwdTileSize(128, 128, 32, 256, 32, 256, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, -1)], - } - else: - return None - - # TODO: we don't support tuning yet, so pick up one value for vlayout/pipeline/pad - # support this in future - @staticmethod - def get_pipelines(dtype, hdim, hdim_v, receipt, mask_impl) -> List[FmhaFwdPipeline]: - # this function will populate a list possible pipelines - # TODO: the order of List matters! the later in this list will be also be checked later - # NOTE: logits soft-cap is NOT supported by VSA sparse attention (enforced by static_assert) - pipelines = [] - if dtype in ["fp16", "bf16"]: - for logits, mask in itertools.product( - ["f"], # logits soft-cap NOT supported, always false - get_mask_map(mask_impl).keys(), - ): - if hdim == 256 and hdim_v == 256: - # vsa fmha only supports dim <= 192 for now. - continue - pipelines.append( - FmhaFwdPipeline( - "qr_async_vsa", - "row", - "t", - "f", - "t", - "t", - logits, - mask, - "f", - ) - ) - pipelines.append( - FmhaFwdPipeline( - "qr_async_vsa", - "row", - "t", - "t", - "t", - "t", - logits, - mask, - "f", - ) - ) - else: - assert False - return pipelines - - -class CustomFactory(KernelComponentFactory): - @staticmethod - def get_hdim_tile_size_dict(dtype: str) -> Optional[dict]: - result = KernelComponentFactory.get_hdim_tile_size_dict(dtype) - if dtype == "fp16" or dtype == "bf16": - if (128, 128) in result.keys(): - result[(128, 128)].insert( - 0, - FmhaFwdTileSize( - 64, - 128, - 64, - 128, - 64, - 128, - 4, - 1, - 1, - 4, - 1, - 1, - 16, - 16, - 16, - 16, - 16, - 16, - -1, - CppConstraint( - "get_num_blocks(128) < num_cus * min_cu_util_rate" - ), - ), - ) - return result - - -def get_fwd_blobs( - kernel_filter: Optional[str], receipt, optdim_list, mask_impl -) -> Tuple[FmhaFwdApiPool, List[FmhaFwdKernel]]: - gen = list() - api_pool = FmhaFwdApiPool(mask_impl) - - factory = ( - CustomFactory - if os.environ.get("CK_TILE_FMHA_FWD_CUSTOM_FACTORY", "0") == "1" - else KernelComponentFactory - ) - - # Only generate fp16/bf16 kernels for now. - # NOTE: VSA sparse attention only supports batch mode (group mode NOT supported, enforced by static_assert) - for dtype in ["fp16", "bf16"]: - d = factory.get_hdim_tile_size_dict(dtype) - if d is None: - continue - for ((hdim, hdim_v), tiles), mode in itertools.product(d.items(), ["batch"]): - for tile, pipeline in itertools.product( - tiles, factory.get_pipelines(dtype, hdim, hdim_v, receipt, mask_impl) - ): - if pipeline.tag != "qr_async_vsa": - continue - k = FmhaFwdKernel( - F_idx=1, - F_hdim=hdim, - F_dtype=dtype, - F_mode=mode, - F_tile=tile, - F_pipeline=pipeline, - mask_impl=mask_impl, - ) - if kernel_filter != "": - if not fnmatch.fnmatch(k.name, kernel_filter): - continue - if optdim_list != [-1]: - if hdim not in optdim_list: - continue - # 2 - Flash attention integration - if receipt in (2, 3): - cond = dtype in ["fp16", "bf16"] - cond &= pipeline.F_vlayout == "row" - if not cond: - continue - # PyTorch integration - elif receipt == 4: - cond = dtype in ["fp16", "bf16"] - cond &= pipeline.F_vlayout == "row" - cond &= mode == "batch" - cond &= pipeline.F_logits == "f" - if not cond: - continue - # Aiter(mha_fwd) integration - elif receipt == 100: - cond = dtype in ["fp16", "bf16"] - cond &= mode == "batch" - cond &= pipeline.F_vlayout == "row" - if not cond: - continue - # Aiter(mha_varlen_fwd) integration - elif receipt == 200: - cond = dtype in ["fp16", "bf16"] - cond &= mode == "group" - cond &= pipeline.F_vlayout == "row" - if not cond: - continue - # aiter::mha_fwd C++ api integration - elif receipt == 600: - cond = dtype in ["fp16", "bf16"] - cond &= pipeline.F_vlayout == "row" - if not cond: - continue - - api_pool.register_traits(k.api_trait()) - gen.append(k) - - return (api_pool, gen) - - -def write_single_fwd_kernel(kernel: FmhaFwdKernel, autogen_dir: Path) -> None: - update_file(autogen_dir / kernel.filename, kernel.template) - - -def write_fwd_api(api_pool: FmhaFwdApiPool, autogen_dir: Path) -> None: - update_file(autogen_dir / FMHA_FWD_API_FILENAME, api_pool.api) - - -def write_blobs( - output_dir: Path, kernel_filter: str, receipt, optdim_list, mask_impl -) -> None: - api_pool, kernels = get_fwd_blobs(kernel_filter, receipt, optdim_list, mask_impl) - for kernel in kernels: - write_single_fwd_kernel(kernel, output_dir) - write_fwd_api(api_pool, output_dir) - - -def list_blobs( - file_path: Path, kernel_filter: str, receipt, optdim_list, mask_impl -) -> None: - with file_path.open("a") as f: - _, kernels = get_fwd_blobs(kernel_filter, receipt, optdim_list, mask_impl) - for kernel in kernels: - f.write((file_path.parent / GEN_DIR / kernel.filename).as_posix() + "\n") - f.write((file_path.parent / GEN_DIR / FMHA_FWD_API_FILENAME).as_posix() + "\n") diff --git a/example/ck_tile/50_sparse_attn/fmha_fwd_trek.hpp b/example/ck_tile/50_sparse_attn/fmha_fwd_trek.hpp index 25e3513d2fa..350d1803f66 100644 --- a/example/ck_tile/50_sparse_attn/fmha_fwd_trek.hpp +++ b/example/ck_tile/50_sparse_attn/fmha_fwd_trek.hpp @@ -277,13 +277,13 @@ struct fmha_jenga_fwd_traits float fmha_jenga_fwd(fmha_jenga_fwd_traits, fmha_jenga_fwd_args, const ck_tile::stream_config&); -// sparge jenga -float sparge_jenga_fwd(fmha_jenga_fwd_traits, fmha_jenga_fwd_args, const ck_tile::stream_config&); - template float fmha_jenga_fwd_(const ck_tile::stream_config&, fmha_jenga_fwd_args); -float fmha_jenga_fwd(fmha_jenga_fwd_args, const ck_tile::stream_config&); +template +void fmha_jenga_fwd_oneshot_(const ck_tile::stream_config&, fmha_jenga_fwd_args); + +void fmha_jenga_fwd_oneshot(fmha_jenga_fwd_traits, fmha_jenga_fwd_args, const ck_tile::stream_config&); // VSA uses the same traits structure as Jenga; aliases for clarity template float fmha_vsa_fwd_(const ck_tile::stream_config&, fmha_vsa_fwd_args); -float fmha_vsa_fwd(fmha_vsa_fwd_args, const ck_tile::stream_config&); +template +void fmha_vsa_fwd_oneshot_(const ck_tile::stream_config&, fmha_vsa_fwd_args); + +void fmha_vsa_fwd_oneshot(fmha_vsa_fwd_traits, fmha_vsa_fwd_args, const ck_tile::stream_config&); diff --git a/example/ck_tile/50_sparse_attn/jenga_sparge_attention.cpp b/example/ck_tile/50_sparse_attn/jenga_sparge_attention.cpp deleted file mode 100644 index 88f3e08204e..00000000000 --- a/example/ck_tile/50_sparse_attn/jenga_sparge_attention.cpp +++ /dev/null @@ -1,189 +0,0 @@ -// Copyright (c) Advanced Micro Devices, Inc., or its affiliates. -// SPDX-License-Identifier: MIT -#include "jenga_sparge_attention.h" -#include "fmha_fwd_trek.hpp" -#include "ck_tile/core.hpp" -#include "ck_tile/host/host_tensor.hpp" -#include "ck_tile/host/device_memory.hpp" -#include - -template -ck_tile::HostTensor -jenga_sparge_attention(const ck_tile::HostTensor& TQ, - const ck_tile::HostTensor& TK, - const ck_tile::HostTensor& TV, - const ck_tile::HostTensor& Tblock_relation_onehot, - ck_tile::HostTensor& Y, - int batch, - int nhead, - int nhead_k, - int seqlen_q, - int seqlen_k, - int hdim_q, - int hdim_v, - bool i_perm, - bool o_perm, - int max_seqlen_q, - int max_seqlen_k, - int log_level) -{ - static_assert(std::is_same_v || - std::is_same_v, - "Jenga sparse attention supports fp16/bf16 only."); - std::string data_type = "fp16"; - if constexpr(std::is_same_v) - { - data_type = "bf16"; - } - - if(max_seqlen_q == 0) - max_seqlen_q = seqlen_q; - if(max_seqlen_k == 0) - max_seqlen_k = seqlen_k; - bool is_v_rowmajor = true; - float scale_s = 1.0 / ck_tile::sqrt(static_cast(hdim_q)); - std::string msk_str = "0"; - mask_info mask = mask_info::decode(msk_str, seqlen_q, seqlen_k); - - const ck_tile::index_t shape_seqlen_q = seqlen_q; - const ck_tile::index_t shape_seqlen_k = seqlen_k; - - ck_tile::stream_config stream_config{nullptr, - false, // time_kernel - log_level, - 0, - 1, - false}; - - ck_tile::DeviceMem q_buf(TQ.get_element_space_size_in_bytes()); - ck_tile::DeviceMem k_buf(TK.get_element_space_size_in_bytes()); - ck_tile::DeviceMem v_buf(TV.get_element_space_size_in_bytes()); - ck_tile::DeviceMem block_relation_buf(Tblock_relation_onehot.get_element_space_size_in_bytes()); - ck_tile::DeviceMem o_buf(Y.get_element_space_size_in_bytes()); - - q_buf.ToDevice(TQ.data()); - k_buf.ToDevice(TK.data()); - v_buf.ToDevice(TV.data()); - block_relation_buf.ToDevice(Tblock_relation_onehot.data()); - - const auto init_args = [&](auto& args) { - assert(nhead % nhead_k == 0); - const ck_tile::index_t stride_q = (i_perm ? hdim_q : nhead * hdim_q); - const ck_tile::index_t stride_k = (i_perm ? hdim_q : nhead_k * hdim_q); - const ck_tile::index_t stride_v = [&]() { - if(is_v_rowmajor) - return i_perm ? hdim_v : nhead_k * hdim_v; - else - return (i_perm ? shape_seqlen_k : nhead_k * shape_seqlen_k); - }(); - const ck_tile::index_t stride_o = (o_perm ? hdim_v : nhead * hdim_v); - const ck_tile::index_t nhead_stride_q = (i_perm ? shape_seqlen_q * hdim_q : hdim_q); - const ck_tile::index_t nhead_stride_k = i_perm ? shape_seqlen_k * hdim_q : hdim_q; - const ck_tile::index_t nhead_stride_v = [&]() { - if(is_v_rowmajor) - return i_perm ? shape_seqlen_k * hdim_v : hdim_v; - else - return i_perm ? hdim_v * shape_seqlen_k : shape_seqlen_k; - }(); - const ck_tile::index_t nhead_stride_o = (o_perm ? shape_seqlen_q * hdim_v : hdim_v); - const ck_tile::index_t batch_stride_q = (nhead * shape_seqlen_q * hdim_q); - const ck_tile::index_t batch_stride_k = nhead_k * shape_seqlen_k * hdim_q; - const ck_tile::index_t batch_stride_v = nhead_k * hdim_v * shape_seqlen_k; - const ck_tile::index_t batch_stride_o = (nhead * shape_seqlen_q * hdim_v); - - args.q_ptr = q_buf.GetDeviceBuffer(); - args.k_ptr = k_buf.GetDeviceBuffer(); - args.v_ptr = v_buf.GetDeviceBuffer(); - args.block_relation_onehot_ptr = block_relation_buf.GetDeviceBuffer(); - - args.batch = batch; - args.seqlen_q = shape_seqlen_q; - args.hdim_q = hdim_q; - args.hdim_v = hdim_v; - args.nhead_q = nhead; - args.nhead_k = nhead_k; - - args.stride_q = stride_q; - args.stride_k = stride_k; - args.stride_v = stride_v; - args.nhead_stride_q = nhead_stride_q; - args.nhead_stride_k = nhead_stride_k; - args.nhead_stride_v = nhead_stride_v; - args.batch_stride_q = batch_stride_q; - args.batch_stride_k = batch_stride_k; - args.batch_stride_v = batch_stride_v; - - args.o_ptr = o_buf.GetDeviceBuffer(); - - args.seqlen_k = shape_seqlen_k; - args.max_seqlen_q = max_seqlen_q; - - args.scale_s = scale_s; - - args.stride_o = stride_o; - args.nhead_stride_o = nhead_stride_o; - args.batch_stride_o = batch_stride_o; - - args.window_size_left = mask.left; - args.window_size_right = mask.right; - args.mask_type = static_cast(mask.type); - }; - - const auto init_traits = [&](auto& traits) { - traits.hdim_q = hdim_q; - traits.hdim_v = hdim_v; - traits.data_type = data_type; - traits.is_v_rowmajor = is_v_rowmajor; - traits.mask_type = mask.type; - }; - - fmha_jenga_fwd_traits fmha_traits; - init_traits(fmha_traits); - - fmha_jenga_fwd_args args; - init_args(args); - - sparge_jenga_fwd(fmha_traits, args, stream_config); - - o_buf.FromDevice(Y.data(), Y.get_element_space_size_in_bytes()); - - return Y; -} - -template ck_tile::HostTensor -jenga_sparge_attention(const ck_tile::HostTensor&, - const ck_tile::HostTensor&, - const ck_tile::HostTensor&, - const ck_tile::HostTensor&, - ck_tile::HostTensor&, - int, - int, - int, - int, - int, - int, - int, - bool, - bool, - int, - int, - int); - -template ck_tile::HostTensor -jenga_sparge_attention(const ck_tile::HostTensor&, - const ck_tile::HostTensor&, - const ck_tile::HostTensor&, - const ck_tile::HostTensor&, - ck_tile::HostTensor&, - int, - int, - int, - int, - int, - int, - int, - bool, - bool, - int, - int, - int); diff --git a/example/ck_tile/50_sparse_attn/jenga_sparge_attention.h b/example/ck_tile/50_sparse_attn/jenga_sparge_attention.h deleted file mode 100644 index 6259fcc73cf..00000000000 --- a/example/ck_tile/50_sparse_attn/jenga_sparge_attention.h +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Advanced Micro Devices, Inc., or its affiliates. -// SPDX-License-Identifier: MIT -#pragma once -#include -#include -#include "ck_tile/core.hpp" -#include "ck_tile/host/host_tensor.hpp" - -template -ck_tile::HostTensor -jenga_sparge_attention(const ck_tile::HostTensor& TQ, - const ck_tile::HostTensor& TK, - const ck_tile::HostTensor& TV, - const ck_tile::HostTensor& Tblock_relation_onehot, - ck_tile::HostTensor& Y, - int batch, - int nhead, - int nhead_k, - int seqlen_q, - int seqlen_k, - int hdim_q, - int hdim_v, - bool i_perm, - bool o_perm, - int max_seqlen_q, - int max_seqlen_k, - int log_level = 0); diff --git a/example/ck_tile/50_sparse_attn/sparge_blockmap_inst.cpp b/example/ck_tile/50_sparse_attn/sparge_blockmap_inst.cpp index fbd18b9ff24..a2df5bac569 100644 --- a/example/ck_tile/50_sparse_attn/sparge_blockmap_inst.cpp +++ b/example/ck_tile/50_sparse_attn/sparge_blockmap_inst.cpp @@ -61,6 +61,57 @@ using bmap_fp16_problem = ck_tile::BlockFmhaPipelineProblem; using bmap_fp16_kernel = ck_tile::SpargeBlockMapKernel; +// ============================================================================ +// bf16: D=128, kM0=64, kN0=128 +// ============================================================================ + +using bmap_bf16_block_tile = ck_tile::sequence<64, 128, 128, 128, 128, 128>; + +using bmap_bf16_shape = + ck_tile::TileFmhaShape, + ck_tile::sequence<16, 16, 16>, + ck_tile::sequence<4, 1, 1>, + ck_tile::sequence<16, 16, 16>, + true>; + +using bmap_bf16_trait = ck_tile::TileFmhaTraits; + +using bmap_bf16_variant = ck_tile::ComposedAttention<0, CK_TILE_FMHA_FWD_FAST_EXP2>; +using bmap_bf16_mask = ck_tile::GenericAttentionMask; + +using bmap_bf16_problem = ck_tile::BlockFmhaPipelineProblem; + +using bmap_bf16_pipeline = ck_tile::SpargeBlockMapPipeline; +using bmap_bf16_kernel = ck_tile::SpargeBlockMapKernel; + // ============================================================================ // Dispatch // ============================================================================ @@ -81,8 +132,96 @@ float sparge_blockmap_fwd(sparge_blockmap_traits traits, s, ck_tile::make_kernel(k_{}, grids, blocks, 0, kargs)); } + if(traits.data_type == "bf16" && traits.hdim_q == 128) + { + using k_ = bmap_bf16_kernel; + if(s.log_level_ > 0) + std::cout << ", sparge_blockmap_bf16_d128" << std::flush; + auto [kargs, grids] = sparge_blockmap_create_kargs_and_grids(args); + const dim3 blocks = k_::BlockSize(); + constexpr ck_tile::index_t kBlockPerCu = k_::kBlockPerCu; + return ck_tile::launch_kernel( + s, ck_tile::make_kernel(k_{}, grids, blocks, 0, kargs)); + } + if(s.log_level_ > 0) std::cerr << "sparge_blockmap_fwd: unsupported config (data_type=" << traits.data_type << ", hdim_q=" << traits.hdim_q << ")" << std::endl; return -1.f; } + +// ============================================================================ +// Oneshot version: launches kernel without timing wrapper +// ============================================================================ + +void sparge_blockmap_fwd_oneshot(sparge_blockmap_traits traits, + sparge_blockmap_args args, + const ck_tile::stream_config& s) +{ + if(traits.data_type == "fp16" && traits.hdim_q == 128) + { + using k_ = bmap_fp16_kernel; + auto [kargs, grids] = sparge_blockmap_create_kargs_and_grids(args); + const dim3 blocks = k_::BlockSize(); + constexpr ck_tile::index_t kBlockPerCu = k_::kBlockPerCu; + ck_tile::make_kernel(k_{}, grids, blocks, 0, kargs)( + ck_tile::stream_config{s.stream_id_}); + return; + } + + if(traits.data_type == "bf16" && traits.hdim_q == 128) + { + using k_ = bmap_bf16_kernel; + auto [kargs, grids] = sparge_blockmap_create_kargs_and_grids(args); + const dim3 blocks = k_::BlockSize(); + constexpr ck_tile::index_t kBlockPerCu = k_::kBlockPerCu; + ck_tile::make_kernel(k_{}, grids, blocks, 0, kargs)( + ck_tile::stream_config{s.stream_id_}); + return; + } + + std::cerr << "sparge_blockmap_fwd_oneshot: unsupported config (data_type=" << traits.data_type + << ", hdim_q=" << traits.hdim_q << ")" << std::endl; +} + +// ============================================================================ +// Combined functions: blockmap + attention timed together via launch_kernel +// ============================================================================ + +float sparge_jenga_fwd(sparge_blockmap_traits bmap_t, sparge_blockmap_args bmap_a, + fmha_jenga_fwd_traits attn_t, fmha_jenga_fwd_args attn_a, + const ck_tile::stream_config& s) +{ + if(s.log_level_ > 0) + std::cout << ", sparge_blockmap_" << bmap_t.data_type << "_d" << bmap_t.hdim_q + << ", fmha_jenga_fwd_" << attn_t.data_type << "_d" << attn_t.hdim_q + << std::flush; + + return ck_tile::launch_kernel( + s, + [=](const ck_tile::stream_config& s_) { + sparge_blockmap_fwd_oneshot(bmap_t, bmap_a, s_); + }, + [=](const ck_tile::stream_config& s_) { + fmha_jenga_fwd_oneshot(attn_t, attn_a, s_); + }); +} + +float sparge_vsa_fwd_combined(sparge_blockmap_traits bmap_t, sparge_blockmap_args bmap_a, + fmha_vsa_fwd_traits attn_t, fmha_vsa_fwd_args attn_a, + const ck_tile::stream_config& s) +{ + if(s.log_level_ > 0) + std::cout << ", sparge_blockmap_" << bmap_t.data_type << "_d" << bmap_t.hdim_q + << ", fmha_vsa_fwd_" << attn_t.data_type << "_d" << attn_t.hdim_q + << std::flush; + + return ck_tile::launch_kernel( + s, + [=](const ck_tile::stream_config& s_) { + sparge_blockmap_fwd_oneshot(bmap_t, bmap_a, s_); + }, + [=](const ck_tile::stream_config& s_) { + fmha_vsa_fwd_oneshot(attn_t, attn_a, s_); + }); +} diff --git a/example/ck_tile/50_sparse_attn/sparge_blockmap_trek.hpp b/example/ck_tile/50_sparse_attn/sparge_blockmap_trek.hpp index 1e7e33248a2..6eaeb9ea77b 100644 --- a/example/ck_tile/50_sparse_attn/sparge_blockmap_trek.hpp +++ b/example/ck_tile/50_sparse_attn/sparge_blockmap_trek.hpp @@ -91,3 +91,16 @@ auto sparge_blockmap_create_kargs_and_grids(sparge_blockmap_args args) float sparge_blockmap_fwd(sparge_blockmap_traits traits, sparge_blockmap_args args, const ck_tile::stream_config& stream_config); + +void sparge_blockmap_fwd_oneshot(sparge_blockmap_traits traits, + sparge_blockmap_args args, + const ck_tile::stream_config& stream_config); + +// Combined functions: blockmap + attention with unified timing +float sparge_jenga_fwd(sparge_blockmap_traits, sparge_blockmap_args, + fmha_jenga_fwd_traits, fmha_jenga_fwd_args, + const ck_tile::stream_config&); + +float sparge_vsa_fwd_combined(sparge_blockmap_traits, sparge_blockmap_args, + fmha_vsa_fwd_traits, fmha_vsa_fwd_args, + const ck_tile::stream_config&); diff --git a/example/ck_tile/50_sparse_attn/test_sparge.cpp b/example/ck_tile/50_sparse_attn/test_sparge.cpp new file mode 100644 index 00000000000..7c30a10b062 --- /dev/null +++ b/example/ck_tile/50_sparse_attn/test_sparge.cpp @@ -0,0 +1,432 @@ +// Copyright (c) Advanced Micro Devices, Inc., or its affiliates. +// SPDX-License-Identifier: MIT +// Unified test for Sparge pipeline: blockmap generation + sparse attention (Jenga/VSA). + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ck_tile/host.hpp" +#include "ck_tile/core.hpp" +#include "ck_tile/host/reference/reference_blocked_attention.hpp" +#include "ck_tile/core/utility/bit_cast.hpp" + +#include "fmha_fwd_trek.hpp" +#include "sparge_blockmap_trek.hpp" +#include "sparge_tool.hpp" + +// ============================================================================ +// Helpers +// ============================================================================ + +template +ck_tile::HostTensor +make_qkv_tensor(ck_tile::index_t batch, ck_tile::index_t nhead, ck_tile::index_t seqlen, ck_tile::index_t hdim, bool i_perm) +{ + if(i_perm) + return ck_tile::HostTensor({batch, nhead, seqlen, hdim}); + return ck_tile::HostTensor({batch, seqlen, nhead, hdim}); +} + +template +ck_tile::HostTensor to_bhsd(const ck_tile::HostTensor& tensor, bool is_bhsd) +{ + auto lens = tensor.get_lengths(); + ck_tile::index_t batch = lens[0]; + ck_tile::index_t seqlen = is_bhsd ? lens[2] : lens[1]; + ck_tile::index_t nhead = is_bhsd ? lens[1] : lens[2]; + ck_tile::index_t hdim = lens[3]; + + ck_tile::HostTensor out({batch, nhead, seqlen, hdim}); + for(ck_tile::index_t b = 0; b < batch; ++b) + for(ck_tile::index_t h = 0; h < nhead; ++h) + for(ck_tile::index_t s = 0; s < seqlen; ++s) + for(ck_tile::index_t d = 0; d < hdim; ++d) + out(b, h, s, d) = is_bhsd ? tensor(b, h, s, d) : tensor(b, s, h, d); + return out; +} + +template +auto get_error_tolerance() +{ + double rtol = 1e-2; + double atol = 4e-2; + if constexpr(std::is_same_v) + { + atol = 2e-1; + rtol = 2e-1; + } + return ck_tile::make_tuple(rtol, atol); +} + +template +float to_float_for_compare(T value) +{ + return static_cast(value); +} + +template <> +float to_float_for_compare(ck_tile::bf16_t value) +{ +#if CK_TILE_USE_CUSTOM_DATA_TYPE + return static_cast(value); +#else + return ck_tile::bf16_to_float_raw(ck_tile::bit_cast(value)); +#endif +} + +// ============================================================================ +// Arg parser +// ============================================================================ +auto create_args(int argc, char* argv[]) +{ + ck_tile::ArgParser arg_parser; + arg_parser + .insert("v", "1", "0:no validation, 1:cpu validation") + .insert("pipeline", "jenga", "attention pipeline: jenga / vsa") + .insert("b", "1", "batch size") + .insert("h", "4", "num of head for q") + .insert("h_k", "-1", "num of head for k/v, -1 means equal to h") + .insert("s", "4096", "seqlen_q") + .insert("s_k", "-1", "seqlen_k, -1 means equal to s") + .insert("d", "128", "head dim for q, k") + .insert("d_v", "-1", "head dim for v, -1 means equal to d") + .insert("topk", "0.3", "topk ratio for blockmap (fraction of K-blocks to keep)") + .insert("cdfthreshd", "-1", "CDF threshold for blockmap (overrides topk if >= 0)") + .insert("simthreshd1", "0.6", "similarity threshold for blockmap") + .insert("prec", "fp16", "data type: fp16/bf16") + .insert("iperm", "1", "permute input, 1: b*h*s*d, 0: b*s*h*d") + .insert("operm", "1", "permute output") + .insert("seed", "42", "random seed") + .insert("warmup", "5", "warmup iterations") + .insert("repeat", "20", "benchmark iterations") + .insert("kname", "0", "print kernel name"); + + bool result = arg_parser.parse(argc, argv); + return std::make_tuple(result, arg_parser); +} + +// ============================================================================ +// Main test +// ============================================================================ +template +bool run_test(const ck_tile::ArgParser& arg_parser) +{ + int do_validation = arg_parser.get_int("v"); + std::string pipeline = arg_parser.get_str("pipeline"); + ck_tile::index_t batch = arg_parser.get_int("b"); + ck_tile::index_t nhead = arg_parser.get_int("h"); + ck_tile::index_t nhead_k = arg_parser.get_int("h_k"); + ck_tile::index_t seqlen_q = arg_parser.get_int("s"); + ck_tile::index_t seqlen_k = arg_parser.get_int("s_k"); + ck_tile::index_t hdim_q = arg_parser.get_int("d"); + ck_tile::index_t hdim_v = arg_parser.get_int("d_v"); + float topk = arg_parser.get_float("topk"); + float cdfthreshd = arg_parser.get_float("cdfthreshd"); + float simthreshd1 = arg_parser.get_float("simthreshd1"); + bool i_perm = arg_parser.get_bool("iperm"); + bool o_perm = arg_parser.get_bool("operm"); + uint32_t seed = arg_parser.get_uint32("seed"); + int warmup = arg_parser.get_int("warmup"); + int repeat = arg_parser.get_int("repeat"); + int kname = arg_parser.get_int("kname"); + + if(nhead_k < 0) nhead_k = nhead; + if(seqlen_k < 0) seqlen_k = seqlen_q; + if(hdim_v < 0) hdim_v = hdim_q; + + // If cdfthreshd >= 0, use CDF mode; otherwise use topk mode + if(cdfthreshd >= 0.0f) + topk = -1.0f; + + constexpr ck_tile::index_t BLKQ = 64; + constexpr ck_tile::index_t BLKK = 128; + + if(hdim_q != 128 || hdim_v != 128) + { + std::cout << "\n>>> TEST SKIPPED <<<\n" + << "Kernel instances are generated for hdim=128 only.\n"; + return true; + } + + ck_tile::index_t num_q_blocks = (seqlen_q + BLKQ - 1) / BLKQ; + ck_tile::index_t num_k_blocks = (seqlen_k + BLKK - 1) / BLKK; + + std::string prec_str = std::is_same_v ? "fp16" : "bf16"; + std::cout << "[" << pipeline << "|" << prec_str + << "] b=" << batch << " h=" << nhead << " s=" << seqlen_q + << " d=" << hdim_q << " topk=" << topk + << " sim1=" << simthreshd1 << std::flush; + + // ---- allocate host tensors ---- + auto q_host = make_qkv_tensor(batch, nhead, seqlen_q, hdim_q, i_perm); + auto k_host = make_qkv_tensor(batch, nhead_k, seqlen_k, hdim_q, i_perm); + auto v_host = make_qkv_tensor(batch, nhead_k, seqlen_k, hdim_v, i_perm); + auto output_host = o_perm ? ck_tile::HostTensor({batch, nhead, seqlen_q, hdim_v}) + : ck_tile::HostTensor({batch, seqlen_q, nhead, hdim_v}); + + ck_tile::HostTensor block_map_host({batch, nhead, num_q_blocks, num_k_blocks}); + ck_tile::HostTensor lut_host({batch, nhead, num_q_blocks, num_k_blocks}); + ck_tile::HostTensor valid_block_num_host({batch, nhead, num_q_blocks}); + + ck_tile::FillUniformDistribution{-0.5f, 0.5f, seed}(q_host); + ck_tile::FillUniformDistribution{-0.5f, 0.5f, seed + 1}(k_host); + ck_tile::FillUniformDistribution{-0.5f, 0.5f, seed + 2}(v_host); + + // ---- device tensors ---- + ck_tile::DeviceMem q_dev(q_host.get_element_space_size_in_bytes()); + ck_tile::DeviceMem k_dev(k_host.get_element_space_size_in_bytes()); + ck_tile::DeviceMem v_dev(v_host.get_element_space_size_in_bytes()); + ck_tile::DeviceMem o_dev(output_host.get_element_space_size_in_bytes()); + ck_tile::DeviceMem block_map_dev(block_map_host.get_element_space_size_in_bytes()); + ck_tile::DeviceMem lut_dev(lut_host.get_element_space_size_in_bytes()); + ck_tile::DeviceMem valid_bn_dev(valid_block_num_host.get_element_space_size_in_bytes()); + + q_dev.ToDevice(q_host.data()); + k_dev.ToDevice(k_host.data()); + v_dev.ToDevice(v_host.data()); + o_dev.SetZero(); + block_map_dev.SetZero(); + lut_dev.SetZero(); + valid_bn_dev.SetZero(); + + // ---- strides (BHSD when i_perm=true) ---- + auto q_strides = q_host.get_strides(); + auto k_strides = k_host.get_strides(); + auto v_strides = v_host.get_strides(); + auto o_strides = output_host.get_strides(); + + float scale_s = 1.0f / std::sqrt(static_cast(hdim_q)); + + // ---- build blockmap args ---- + sparge_blockmap_traits bmap_traits; + bmap_traits.data_type = std::is_same_v ? "fp16" : "bf16"; + bmap_traits.hdim_q = hdim_q; + + sparge_blockmap_args bmap_args; + bmap_args.q_ptr = q_dev.GetDeviceBuffer(); + bmap_args.k_ptr = k_dev.GetDeviceBuffer(); + bmap_args.batch = batch; + bmap_args.seqlen_q = seqlen_q; + bmap_args.seqlen_k = seqlen_k; + bmap_args.hdim_q = hdim_q; + bmap_args.nhead_q = nhead; + bmap_args.nhead_k = nhead_k; + bmap_args.stride_q = q_strides[i_perm ? 2 : 1]; + bmap_args.stride_k = k_strides[i_perm ? 2 : 1]; + bmap_args.nhead_stride_q = q_strides[i_perm ? 1 : 2]; + bmap_args.nhead_stride_k = k_strides[i_perm ? 1 : 2]; + bmap_args.batch_stride_q = q_strides[0]; + bmap_args.batch_stride_k = k_strides[0]; + bmap_args.simthreshd1 = simthreshd1; + bmap_args.cdfthreshd = (topk < 0.0f) ? cdfthreshd : -1.0f; + bmap_args.topk = topk; + bmap_args.scale = scale_s; + bmap_args.block_map_ptr = block_map_dev.GetDeviceBuffer(); + bmap_args.lut_ptr = (pipeline == "vsa") ? lut_dev.GetDeviceBuffer() : nullptr; + bmap_args.valid_block_num_ptr = (pipeline == "vsa") ? valid_bn_dev.GetDeviceBuffer() : nullptr; + + // ---- build attention args ---- + ck_tile::stream_config stream_cfg; + stream_cfg.stream_id_ = nullptr; + stream_cfg.time_kernel_ = true; + stream_cfg.log_level_ = kname; + stream_cfg.cold_niters_ = warmup; + stream_cfg.nrepeat_ = repeat; + + float avg_ms = -1.0f; + + if(pipeline == "jenga") + { + fmha_jenga_fwd_traits attn_traits; + attn_traits.hdim_q = hdim_q; + attn_traits.hdim_v = hdim_v; + attn_traits.data_type = std::is_same_v ? "fp16" : "bf16"; + attn_traits.is_v_rowmajor = true; + attn_traits.mask_type = mask_enum::no_mask; + + fmha_jenga_fwd_args attn_args; + attn_args.q_ptr = q_dev.GetDeviceBuffer(); + attn_args.k_ptr = k_dev.GetDeviceBuffer(); + attn_args.v_ptr = v_dev.GetDeviceBuffer(); + attn_args.block_relation_onehot_ptr = block_map_dev.GetDeviceBuffer(); + attn_args.o_ptr = o_dev.GetDeviceBuffer(); + attn_args.seqlen_q = seqlen_q; + attn_args.seqlen_k = seqlen_k; + attn_args.batch = batch; + attn_args.max_seqlen_q = seqlen_q; + attn_args.hdim_q = hdim_q; + attn_args.hdim_v = hdim_v; + attn_args.nhead_q = nhead; + attn_args.nhead_k = nhead_k; + attn_args.scale_s = scale_s; + attn_args.stride_q = q_strides[i_perm ? 2 : 1]; + attn_args.stride_k = k_strides[i_perm ? 2 : 1]; + attn_args.stride_v = v_strides[i_perm ? 2 : 1]; + attn_args.stride_o = o_strides[o_perm ? 2 : 1]; + attn_args.nhead_stride_q = q_strides[i_perm ? 1 : 2]; + attn_args.nhead_stride_k = k_strides[i_perm ? 1 : 2]; + attn_args.nhead_stride_v = v_strides[i_perm ? 1 : 2]; + attn_args.nhead_stride_o = o_strides[o_perm ? 1 : 2]; + attn_args.batch_stride_q = q_strides[0]; + attn_args.batch_stride_k = k_strides[0]; + attn_args.batch_stride_v = v_strides[0]; + attn_args.batch_stride_o = o_strides[0]; + attn_args.window_size_left = -1; + attn_args.window_size_right = -1; + attn_args.mask_type = 0; + + avg_ms = sparge_jenga_fwd(bmap_traits, bmap_args, attn_traits, attn_args, stream_cfg); + } + else if(pipeline == "vsa") + { + fmha_vsa_fwd_traits attn_traits; + attn_traits.hdim_q = hdim_q; + attn_traits.hdim_v = hdim_v; + attn_traits.data_type = std::is_same_v ? "fp16" : "bf16"; + attn_traits.is_v_rowmajor = true; + attn_traits.mask_type = mask_enum::no_mask; + + fmha_vsa_fwd_args attn_args; + attn_args.q_ptr = q_dev.GetDeviceBuffer(); + attn_args.k_ptr = k_dev.GetDeviceBuffer(); + attn_args.v_ptr = v_dev.GetDeviceBuffer(); + attn_args.lut_ptr = lut_dev.GetDeviceBuffer(); + attn_args.valid_block_num_ptr = valid_bn_dev.GetDeviceBuffer(); + attn_args.o_ptr = o_dev.GetDeviceBuffer(); + attn_args.seqlen_q = seqlen_q; + attn_args.seqlen_k = seqlen_k; + attn_args.batch = batch; + attn_args.max_seqlen_q = seqlen_q; + attn_args.hdim_q = hdim_q; + attn_args.hdim_v = hdim_v; + attn_args.nhead_q = nhead; + attn_args.nhead_k = nhead_k; + attn_args.scale_s = scale_s; + attn_args.stride_q = q_strides[i_perm ? 2 : 1]; + attn_args.stride_k = k_strides[i_perm ? 2 : 1]; + attn_args.stride_v = v_strides[i_perm ? 2 : 1]; + attn_args.stride_o = o_strides[o_perm ? 2 : 1]; + attn_args.nhead_stride_q = q_strides[i_perm ? 1 : 2]; + attn_args.nhead_stride_k = k_strides[i_perm ? 1 : 2]; + attn_args.nhead_stride_v = v_strides[i_perm ? 1 : 2]; + attn_args.nhead_stride_o = o_strides[o_perm ? 1 : 2]; + attn_args.batch_stride_q = q_strides[0]; + attn_args.batch_stride_k = k_strides[0]; + attn_args.batch_stride_v = v_strides[0]; + attn_args.batch_stride_o = o_strides[0]; + attn_args.window_size_left = -1; + attn_args.window_size_right = -1; + attn_args.mask_type = 0; + + avg_ms = sparge_vsa_fwd_combined(bmap_traits, bmap_args, attn_traits, attn_args, stream_cfg); + } + else + { + std::cerr << "Unknown pipeline: " << pipeline << " (use jenga or vsa)\n"; + return false; + } + + // ---- TFLOPS calculation (dense FMHA formula, so sparsity gains show as higher TFLOPS) ---- + std::size_t flop = static_cast(batch) * nhead * + (static_cast(2) * seqlen_q * seqlen_k * hdim_q + + static_cast(2) * seqlen_q * seqlen_k * hdim_v); + float tflops = (avg_ms > 0.f) ? static_cast(flop) / 1.E9f / avg_ms : 0.f; + + if(avg_ms > 0.f) + { + std::cout << std::fixed << ", " << std::setprecision(3) << avg_ms << " ms, " + << std::setprecision(2) << tflops << " TFlops" << std::flush; + } + + // ---- copy results back ---- + o_dev.FromDevice(output_host.data()); + block_map_dev.FromDevice(block_map_host.data()); + + // ---- count active blocks ---- + ck_tile::index_t total_blocks = batch * nhead * num_q_blocks * num_k_blocks; + ck_tile::index_t active_blocks = 0; + for(size_t i = 0; i < block_map_host.mData.size(); ++i) + if(block_map_host.mData[i]) + active_blocks++; + float actual_sparsity = 1.0f - static_cast(active_blocks) / static_cast(total_blocks); + std::cout << ", sparsity=" << std::setprecision(2) << actual_sparsity + << "(" << active_blocks << "/" << total_blocks << ")" << std::flush; + + // ---- validation ---- + bool pass = true; + if(do_validation) + { + auto q_ref = to_bhsd(q_host, i_perm); + auto k_ref = to_bhsd(k_host, i_perm); + auto v_ref = to_bhsd(v_host, i_perm); + + ck_tile::HostTensor output_ref({batch, nhead, seqlen_q, hdim_v}); + ck_tile::reference_blocked_attention( + q_ref, k_ref, v_ref, block_map_host, output_ref, BLKQ, BLKK, scale_s); + + auto [rtol, atol] = get_error_tolerance(); + + float max_diff = 0.0f; + size_t num_errors = 0; + + auto output_host_bhsd = to_bhsd(output_host, o_perm); + for(size_t i = 0; i < output_host_bhsd.mData.size(); ++i) + { + float gpu_val = to_float_for_compare(output_host_bhsd.mData[i]); + float ref_val = to_float_for_compare(output_ref.mData[i]); + float diff = std::abs(gpu_val - ref_val); + float rel_diff = (std::abs(ref_val) > 1e-6f) ? diff / std::abs(ref_val) : diff; + + max_diff = std::max(max_diff, diff); + + if(diff > atol && rel_diff > rtol) + num_errors++; + } + + pass = (num_errors == 0); + std::cout << ", " << (pass ? "PASS" : "FAIL") + << "(err=" << num_errors << "/" << output_host_bhsd.mData.size() + << " maxdiff=" << max_diff << ")"; + } + + std::cout << std::endl; + return pass; +} + +// ============================================================================ +// Main +// ============================================================================ +int main(int argc, char* argv[]) +{ + auto [result, arg_parser] = create_args(argc, argv); + if(!result) + { + std::cerr << "Failed to parse arguments\n"; + return -1; + } + + std::string prec = arg_parser.get_str("prec"); + + bool test_result = false; + if(prec == "fp16") + { + test_result = run_test(arg_parser); + } + else if(prec == "bf16") + { + test_result = run_test(arg_parser); + } + else + { + std::cerr << "Unsupported precision: " << prec << "\n"; + return -1; + } + + return test_result ? 0 : -1; +} diff --git a/example/ck_tile/50_sparse_attn/test_sparge_jenga_sparse_attn.cpp b/example/ck_tile/50_sparse_attn/test_sparge_jenga_sparse_attn.cpp deleted file mode 100644 index 590e51db144..00000000000 --- a/example/ck_tile/50_sparse_attn/test_sparge_jenga_sparse_attn.cpp +++ /dev/null @@ -1,422 +0,0 @@ -// Copyright (c) Advanced Micro Devices, Inc., or its affiliates. -// SPDX-License-Identifier: MIT -// Demo: Sparge block-map -> Jenga sparse attention - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "ck_tile/host.hpp" -#include "ck_tile/core.hpp" -#include "ck_tile/host/reference/reference_blocked_attention.hpp" -#include "ck_tile/core/utility/bit_cast.hpp" - -#include "jenga_sparge_attention.h" -#include "sparge_tool.hpp" - -// ============================================================================ -// Helper Functions -// ============================================================================ - -template -ck_tile::HostTensor make_qkv_tensor(ck_tile::index_t batch, - ck_tile::index_t nhead, - ck_tile::index_t seqlen, - ck_tile::index_t hdim, - bool i_perm) -{ - if(i_perm) - { - return ck_tile::HostTensor({batch, nhead, seqlen, hdim}); - } - return ck_tile::HostTensor({batch, seqlen, nhead, hdim}); -} - -template -ck_tile::HostTensor to_bhsd(const ck_tile::HostTensor& tensor, bool is_bhsd) -{ - auto lens = tensor.get_lengths(); - ck_tile::index_t batch = lens[0]; - ck_tile::index_t seqlen = is_bhsd ? lens[2] : lens[1]; - ck_tile::index_t nhead = is_bhsd ? lens[1] : lens[2]; - ck_tile::index_t hdim = lens[3]; - - ck_tile::HostTensor out({batch, nhead, seqlen, hdim}); - for(ck_tile::index_t b = 0; b < batch; ++b) - { - for(ck_tile::index_t h = 0; h < nhead; ++h) - { - for(ck_tile::index_t s = 0; s < seqlen; ++s) - { - for(ck_tile::index_t d = 0; d < hdim; ++d) - { - out(b, h, s, d) = is_bhsd ? tensor(b, h, s, d) : tensor(b, s, h, d); - } - } - } - } - return out; -} - -template -auto get_error_tolerance() -{ - double rtol = 1e-2; - double atol = 4e-2; - if constexpr(std::is_same_v) - { - atol = 2e-1; - rtol = 2e-1; - } - return ck_tile::make_tuple(rtol, atol); -} - -template -float to_float_for_compare(T value) -{ - return static_cast(value); -} - -template <> -float to_float_for_compare(ck_tile::bf16_t value) -{ -#if CK_TILE_USE_CUSTOM_DATA_TYPE - return static_cast(value); -#else - return ck_tile::bf16_to_float_raw(ck_tile::bit_cast(value)); -#endif -} - -// ============================================================================ -// Command line argument parser -// ============================================================================ - -auto create_args(int argc, char* argv[]) -{ - ck_tile::ArgParser arg_parser; - arg_parser.insert("v", "1", "0:no validation, 1:cpu validation") - .insert("b", "1", "batch size") - .insert("h", "4", "num of head for q") - .insert("h_k", "-1", "num of head for k/v, -1 means equal to h") - .insert("s", "4096", "seqlen_q") - .insert("s_k", "-1", "seqlen_k, -1 means equal to s") - .insert("d", "128", "head dim for q, k") - .insert("d_v", "-1", "head dim for v, -1 means equal to d") - .insert("prec", "fp16", "data type: fp16/bf16") - .insert("iperm", "1", "permute input, 1: b*h*s*d, 0: b*s*h*d") - .insert("operm", "1", "permute output") - .insert("seed", "42", "random seed") - .insert("warmup", "5", "warmup iterations") - .insert("repeat", "20", "benchmark iterations") - .insert("kname", "0", "print kernel name") - // Sparge-specific - .insert("blkq", "64", "Sparge BLKQ") - .insert("blkk", "128", "Sparge BLKK") - .insert("simthreshd1", "0.6", "Sparge sim threshold") - .insert("cdfthreshd", "0.98", "Sparge CDF threshold (used when topk < 0)") - .insert("topk", "-1.0", "Sparge topk ratio in (0,1]; if > 0, overrides cdfthreshd"); - - bool result = arg_parser.parse(argc, argv); - return std::make_tuple(result, arg_parser); -} - -// ============================================================================ -// Main Test Function -// ============================================================================ - -template -bool run_test(const ck_tile::ArgParser& arg_parser) -{ - int do_validation = arg_parser.get_int("v"); - ck_tile::index_t batch = arg_parser.get_int("b"); - ck_tile::index_t nhead = arg_parser.get_int("h"); - ck_tile::index_t nhead_k = arg_parser.get_int("h_k"); - ck_tile::index_t seqlen_q = arg_parser.get_int("s"); - ck_tile::index_t seqlen_k = arg_parser.get_int("s_k"); - ck_tile::index_t hdim_q = arg_parser.get_int("d"); - ck_tile::index_t hdim_v = arg_parser.get_int("d_v"); - bool i_perm = arg_parser.get_bool("iperm"); - bool o_perm = arg_parser.get_bool("operm"); - uint32_t seed = arg_parser.get_uint32("seed"); - int warmup = arg_parser.get_int("warmup"); - int repeat = arg_parser.get_int("repeat"); - int kname = arg_parser.get_int("kname"); - - // Sparge params - ck_tile::index_t blkq = arg_parser.get_int("blkq"); - ck_tile::index_t blkk = arg_parser.get_int("blkk"); - float simthreshd1 = arg_parser.get_float("simthreshd1"); - float cdfthreshd = arg_parser.get_float("cdfthreshd"); - float topk = arg_parser.get_float("topk"); - - if(nhead_k < 0) - nhead_k = nhead; - if(seqlen_k < 0) - seqlen_k = seqlen_q; - if(hdim_v < 0) - hdim_v = hdim_q; - - if(blkq != 64 || blkk != 128 || hdim_q != 128 || hdim_v != 128) - { - std::cout << "\n>>> TEST SKIPPED <<<" << std::endl; - std::cout << "Sparge Jenga kernel instances are generated for BLKQ=64, BLKK=128, " - "hdim_q=128, hdim_v=128 only." - << std::endl; - std::cout << "TEST SKIPPED" << std::endl; - return true; - } - - ck_tile::index_t BLKQ = blkq; - ck_tile::index_t BLKK = blkk; - - ck_tile::index_t num_q_blocks = (seqlen_q + BLKQ - 1) / BLKQ; - ck_tile::index_t num_k_blocks = (seqlen_k + BLKK - 1) / BLKK; - - std::cout << "============================================================" << std::endl; - std::cout << "[Sparge -> Jenga Sparse Attention Demo]" << std::endl; - std::cout << "============================================================" << std::endl; - std::cout << " Batch: " << batch << ", nhead_q: " << nhead << ", nhead_k: " << nhead_k - << std::endl; - std::cout << " seqlen_q: " << seqlen_q << ", seqlen_k: " << seqlen_k << std::endl; - std::cout << " hdim_q: " << hdim_q << ", hdim_v: " << hdim_v << std::endl; - std::cout << " BLKQ=" << BLKQ << ", BLKK=" << BLKK << std::endl; - std::cout << " num_q_blocks: " << num_q_blocks << ", num_k_blocks: " << num_k_blocks - << std::endl; - std::cout << " Sparge(simthreshd1=" << simthreshd1 << ", cdfthreshd=" << cdfthreshd - << ", topk=" << topk << ")" << std::endl; - std::cout << " i_perm: " << i_perm << ", o_perm: " << o_perm << std::endl; - - // Create host tensors - ck_tile::HostTensor q_host = make_qkv_tensor(batch, nhead, seqlen_q, hdim_q, i_perm); - ck_tile::HostTensor k_host = make_qkv_tensor(batch, nhead_k, seqlen_k, hdim_q, i_perm); - ck_tile::HostTensor v_host = make_qkv_tensor(batch, nhead_k, seqlen_k, hdim_v, i_perm); - ck_tile::HostTensor output_host = - o_perm ? ck_tile::HostTensor({batch, nhead, seqlen_q, hdim_v}) - : ck_tile::HostTensor({batch, seqlen_q, nhead, hdim_v}); - ck_tile::HostTensor output_ref({batch, nhead, seqlen_q, hdim_v}); - - std::cout << "\nInitializing tensors..." << std::endl; - ck_tile::FillUniformDistribution{-0.5f, 0.5f, seed}(q_host); - ck_tile::FillUniformDistribution{-0.5f, 0.5f, seed + 1}(k_host); - ck_tile::FillUniformDistribution{-0.5f, 0.5f, seed + 2}(v_host); - - // Build block map using Sparge tool - std::cout << "Building Sparge block map..." << std::endl; - sparge::SpargeParams p; - p.BLKQ = static_cast(BLKQ); - p.BLKK = static_cast(BLKK); - p.simthreshd1 = simthreshd1; - p.cdfthreshd = cdfthreshd; - p.topk = topk; - p.i_perm = i_perm; - - ck_tile::HostTensor block_relation_onehot = - sparge::build_block_map_meansim(q_host, k_host, p); - - // Print actual sparsity - std::size_t total_blocks = 0; - std::size_t active_blocks = 0; - for(ck_tile::index_t b = 0; b < batch; ++b) - { - for(ck_tile::index_t h = 0; h < nhead; ++h) - { - for(ck_tile::index_t qb = 0; qb < num_q_blocks; ++qb) - { - for(ck_tile::index_t kb = 0; kb < num_k_blocks; ++kb) - { - total_blocks++; - if(block_relation_onehot(b, h, qb, kb) != 0) - active_blocks++; - } - } - } - } - float actual_sparsity = - 1.0f - static_cast(active_blocks) / static_cast(total_blocks); - std::cout << " Actual sparsity: " << actual_sparsity << " (" << active_blocks << "/" - << total_blocks << " blocks active)" << std::endl; - - std::cout << "\n--- Running Jenga sparse attention kernel ---" << std::endl; - - try - { - if(kname) - { - jenga_sparge_attention(q_host, - k_host, - v_host, - block_relation_onehot, - output_host, - batch, - nhead, - nhead_k, - seqlen_q, - seqlen_k, - hdim_q, - hdim_v, - i_perm, - o_perm, - seqlen_q, - seqlen_k, - 1); - } - - for(int i = 0; i < warmup; ++i) - { - jenga_sparge_attention(q_host, - k_host, - v_host, - block_relation_onehot, - output_host, - batch, - nhead, - nhead_k, - seqlen_q, - seqlen_k, - hdim_q, - hdim_v, - i_perm, - o_perm, - seqlen_q, - seqlen_k, - 0); - } - - [[maybe_unused]] auto sync_status1 = hipDeviceSynchronize(); - auto start = std::chrono::high_resolution_clock::now(); - - for(int i = 0; i < repeat; ++i) - { - jenga_sparge_attention(q_host, - k_host, - v_host, - block_relation_onehot, - output_host, - batch, - nhead, - nhead_k, - seqlen_q, - seqlen_k, - hdim_q, - hdim_v, - i_perm, - o_perm, - seqlen_q, - seqlen_k, - 0); - } - - [[maybe_unused]] auto sync_status2 = hipDeviceSynchronize(); - auto end = std::chrono::high_resolution_clock::now(); - double avg_time_ms = - std::chrono::duration(end - start).count() / repeat; - - std::cout << "\n>>>> Jenga sparse attention average time: " << avg_time_ms << " ms <<<<" - << std::endl; - } - catch(const std::exception& e) - { - std::cerr << "Error during kernel execution: " << e.what() << std::endl; - return false; - } - - bool pass = true; - if(do_validation) - { - std::cout << "\n--- Performing CPU validation ---" << std::endl; - float scale = 1.0f / std::sqrt(static_cast(hdim_q)); - - std::cout << "Computing reference output..." << std::endl; - auto q_ref = to_bhsd(q_host, i_perm); - auto k_ref = to_bhsd(k_host, i_perm); - auto v_ref = to_bhsd(v_host, i_perm); - - ck_tile::reference_blocked_attention( - q_ref, k_ref, v_ref, block_relation_onehot, output_ref, BLKQ, BLKK, scale); - - auto [rtol, atol] = get_error_tolerance(); - - float max_diff = 0.0f; - float max_rel_diff = 0.0f; - std::size_t num_errors = 0; - - auto output_host_bhsd = to_bhsd(output_host, o_perm); - for(std::size_t i = 0; i < output_host_bhsd.mData.size(); ++i) - { - float gpu_val = to_float_for_compare(output_host_bhsd.mData[i]); - float ref_val = to_float_for_compare(output_ref.mData[i]); - float diff = std::abs(gpu_val - ref_val); - float rel_diff = (std::abs(ref_val) > 1e-6f) ? diff / std::abs(ref_val) : diff; - - max_diff = std::max(max_diff, diff); - max_rel_diff = std::max(max_rel_diff, rel_diff); - - if(diff > atol && rel_diff > rtol) - { - num_errors++; - if(num_errors <= 5) - { - std::cout << " Mismatch at index " << i << ": GPU=" << gpu_val - << ", Ref=" << ref_val << ", Diff=" << diff << std::endl; - } - } - } - - std::cout << "\nValidation results:" << std::endl; - std::cout << " Max absolute difference: " << max_diff << std::endl; - std::cout << " Max relative difference: " << max_rel_diff << std::endl; - std::cout << " Number of mismatches: " << num_errors << " / " - << output_host_bhsd.mData.size() << std::endl; - - if(num_errors == 0) - { - std::cout << "\n>>> VALIDATION PASSED <<<" << std::endl; - } - else - { - std::cout << "\n>>> VALIDATION FAILED <<<" << std::endl; - pass = false; - } - } - - std::cout << "\n" << (pass ? "TEST PASSED" : "TEST FAILED") << std::endl; - return pass; -} - -// ============================================================================ -// Main -// ============================================================================ - -int main(int argc, char* argv[]) -{ - auto [result, arg_parser] = create_args(argc, argv); - if(!result) - { - std::cerr << "Failed to parse arguments" << std::endl; - return -1; - } - - std::string prec = arg_parser.get_str("prec"); - - bool test_result = false; - if(prec == "fp16") - { - test_result = run_test(arg_parser); - } - else if(prec == "bf16") - { - test_result = run_test(arg_parser); - } - else - { - std::cerr << "Unsupported precision: " << prec << std::endl; - return -1; - } - - return test_result ? 0 : -1; -} diff --git a/example/ck_tile/50_sparse_attn/test_sparge_vsa_sparse_attn.cpp b/example/ck_tile/50_sparse_attn/test_sparge_vsa_sparse_attn.cpp deleted file mode 100644 index 572b708f9ef..00000000000 --- a/example/ck_tile/50_sparse_attn/test_sparge_vsa_sparse_attn.cpp +++ /dev/null @@ -1,597 +0,0 @@ -// Copyright (c) Advanced Micro Devices, Inc., or its affiliates. -// SPDX-License-Identifier: MIT -// Demo: Sparge block-map -> (delta LUT) -> VSA sparse attention (all-in-device) - -#include -#include -#include -#include "ck_tile/host.hpp" -#include "ck_tile/core.hpp" -#include "ck_tile/host/reference/reference_blocked_attention.hpp" -#include "ck_tile/core/utility/bit_cast.hpp" - -#include "sparge_blockmap_trek.hpp" -#include "fmha_fwd_trek.hpp" -#include "sparge_tool.hpp" - -// ============================================================================ -// Helper Functions -// ============================================================================ - -template -ck_tile::HostTensor make_qkv_tensor(ck_tile::index_t batch, - ck_tile::index_t nhead, - ck_tile::index_t seqlen, - ck_tile::index_t hdim, - bool i_perm) -{ - if(i_perm) - { - return ck_tile::HostTensor({batch, nhead, seqlen, hdim}); - } - return ck_tile::HostTensor({batch, seqlen, nhead, hdim}); -} - -template -ck_tile::HostTensor to_bhsd(const ck_tile::HostTensor& tensor, bool is_bhsd) -{ - auto lens = tensor.get_lengths(); - ck_tile::index_t batch = lens[0]; - ck_tile::index_t seqlen = is_bhsd ? lens[2] : lens[1]; - ck_tile::index_t nhead = is_bhsd ? lens[1] : lens[2]; - ck_tile::index_t hdim = lens[3]; - - ck_tile::HostTensor out({batch, nhead, seqlen, hdim}); - for(ck_tile::index_t b = 0; b < batch; ++b) - { - for(ck_tile::index_t h = 0; h < nhead; ++h) - { - for(ck_tile::index_t s = 0; s < seqlen; ++s) - { - for(ck_tile::index_t d = 0; d < hdim; ++d) - { - out(b, h, s, d) = is_bhsd ? tensor(b, h, s, d) : tensor(b, s, h, d); - } - } - } - } - return out; -} - -template -auto get_error_tolerance() -{ - double rtol = 1e-2; - double atol = 4e-2; - if constexpr(std::is_same_v) - { - atol = 2e-1; - rtol = 2e-1; - } - return ck_tile::make_tuple(rtol, atol); -} - -template -float to_float_for_compare(T value) -{ - return static_cast(value); -} - -template <> -float to_float_for_compare(ck_tile::bf16_t value) -{ -#if CK_TILE_USE_CUSTOM_DATA_TYPE - return static_cast(value); -#else - return ck_tile::bf16_to_float_raw(ck_tile::bit_cast(value)); -#endif -} - -// ============================================================================ -// Command line argument parser -// ============================================================================ - -auto create_args(int argc, char* argv[]) -{ - ck_tile::ArgParser arg_parser; - arg_parser.insert("v", "1", "0:no validation, 1:cpu validation") - .insert("b", "1", "batch size") - .insert("h", "4", "num of head for q") - .insert("h_k", "-1", "num of head for k/v, -1 means equal to h") - .insert("s", "4096", "seqlen_q") - .insert("s_k", "-1", "seqlen_k, -1 means equal to s") - .insert("d", "128", "head dim for q, k") - .insert("d_v", "-1", "head dim for v, -1 means equal to d") - .insert("prec", "fp16", "data type: fp16/bf16") - .insert("iperm", "1", "permute input, 1: b*h*s*d, 0: b*s*h*d") - .insert("operm", "1", "permute output") - .insert("seed", "42", "random seed") - .insert("warmup", "5", "warmup iterations") - .insert("repeat", "20", "benchmark iterations") - .insert("kname", "0", "print kernel name") - // Sparge-specific - .insert("blkq", "64", "Sparge BLKQ") - .insert("blkk", "128", "Sparge BLKK") - .insert("simthreshd1", "0.6", "Sparge sim threshold") - .insert("cdfthreshd", "0.98", "Sparge CDF threshold (used when topk < 0)") - .insert("topk", "-1.0", "Sparge topk ratio in (0,1]; if > 0, overrides cdfthreshd"); - - bool result = arg_parser.parse(argc, argv); - return std::make_tuple(result, arg_parser); -} - -// ============================================================================ -// Main Test Function -// ============================================================================ - -template -bool run_test(const ck_tile::ArgParser& arg_parser) -{ - int do_validation = arg_parser.get_int("v"); - ck_tile::index_t batch = arg_parser.get_int("b"); - ck_tile::index_t nhead = arg_parser.get_int("h"); - ck_tile::index_t nhead_k = arg_parser.get_int("h_k"); - ck_tile::index_t seqlen_q = arg_parser.get_int("s"); - ck_tile::index_t seqlen_k = arg_parser.get_int("s_k"); - ck_tile::index_t hdim_q = arg_parser.get_int("d"); - ck_tile::index_t hdim_v = arg_parser.get_int("d_v"); - bool i_perm = arg_parser.get_bool("iperm"); - bool o_perm = arg_parser.get_bool("operm"); - uint32_t seed = arg_parser.get_uint32("seed"); - int warmup = arg_parser.get_int("warmup"); - int repeat = arg_parser.get_int("repeat"); - int kname = arg_parser.get_int("kname"); - - // Sparge params - ck_tile::index_t blkq = arg_parser.get_int("blkq"); - ck_tile::index_t blkk = arg_parser.get_int("blkk"); - float simthreshd1 = arg_parser.get_float("simthreshd1"); - float cdfthreshd = arg_parser.get_float("cdfthreshd"); - float topk = arg_parser.get_float("topk"); - - if(nhead_k < 0) - nhead_k = nhead; - if(seqlen_k < 0) - seqlen_k = seqlen_q; - if(hdim_v < 0) - hdim_v = hdim_q; - - if(blkq != 64 || blkk != 128 || hdim_q != 128 || hdim_v != 128) - { - std::cout << "\n>>> TEST SKIPPED <<<" << std::endl; - std::cout << "Sparge VSA kernel instances are generated for BLKQ=64, BLKK=128, " - "hdim_q=128, hdim_v=128 only." - << std::endl; - std::cout << "TEST SKIPPED" << std::endl; - return true; - } - - ck_tile::index_t BLKQ = blkq; - ck_tile::index_t BLKK = blkk; - - ck_tile::index_t num_q_blocks = (seqlen_q + BLKQ - 1) / BLKQ; - ck_tile::index_t num_k_blocks = (seqlen_k + BLKK - 1) / BLKK; - - std::cout << "============================================================" << std::endl; - std::cout << "[Sparge -> VSA Sparse Attention Demo]" << std::endl; - std::cout << "============================================================" << std::endl; - std::cout << " Batch: " << batch << ", nhead_q: " << nhead << ", nhead_k: " << nhead_k - << std::endl; - std::cout << " seqlen_q: " << seqlen_q << ", seqlen_k: " << seqlen_k << std::endl; - std::cout << " hdim_q: " << hdim_q << ", hdim_v: " << hdim_v << std::endl; - std::cout << " BLKQ=" << BLKQ << ", BLKK=" << BLKK << std::endl; - std::cout << " num_q_blocks: " << num_q_blocks << ", num_k_blocks: " << num_k_blocks - << std::endl; - std::cout << " Sparge(simthreshd1=" << simthreshd1 << ", cdfthreshd=" << cdfthreshd - << ", topk=" << topk << ")" << std::endl; - std::cout << " i_perm: " << i_perm << ", o_perm: " << o_perm << std::endl; - - // Create host tensors and fill with random data - ck_tile::HostTensor q_host = make_qkv_tensor(batch, nhead, seqlen_q, hdim_q, i_perm); - ck_tile::HostTensor k_host = make_qkv_tensor(batch, nhead_k, seqlen_k, hdim_q, i_perm); - ck_tile::HostTensor v_host = make_qkv_tensor(batch, nhead_k, seqlen_k, hdim_v, i_perm); - ck_tile::HostTensor output_host = - o_perm ? ck_tile::HostTensor({batch, nhead, seqlen_q, hdim_v}) - : ck_tile::HostTensor({batch, seqlen_q, nhead, hdim_v}); - - std::cout << "\nInitializing tensors..." << std::endl; - ck_tile::FillUniformDistribution{-0.5f, 0.5f, seed}(q_host); - ck_tile::FillUniformDistribution{-0.5f, 0.5f, seed + 1}(k_host); - ck_tile::FillUniformDistribution{-0.5f, 0.5f, seed + 2}(v_host); - - // ================================================================== - // Allocate device memory once, HtoD once - // ================================================================== - ck_tile::DeviceMem q_buf(q_host.get_element_space_size_in_bytes()); - ck_tile::DeviceMem k_buf(k_host.get_element_space_size_in_bytes()); - ck_tile::DeviceMem v_buf(v_host.get_element_space_size_in_bytes()); - ck_tile::DeviceMem o_buf(output_host.get_element_space_size_in_bytes()); - - q_buf.ToDevice(q_host.data()); - k_buf.ToDevice(k_host.data()); - v_buf.ToDevice(v_host.data()); - - const std::size_t bmap_bytes = - static_cast(batch) * nhead * num_q_blocks * num_k_blocks * sizeof(uint8_t); - const std::size_t lut_bytes = - static_cast(batch) * nhead * num_q_blocks * num_k_blocks * sizeof(int32_t); - const std::size_t valid_bytes = - static_cast(batch) * nhead * num_q_blocks * sizeof(int32_t); - - ck_tile::DeviceMem bmap_buf(bmap_bytes); - ck_tile::DeviceMem lut_buf(lut_bytes); - ck_tile::DeviceMem valid_buf(valid_bytes); - bmap_buf.SetZero(); - lut_buf.SetZero(); - valid_buf.SetZero(); - - // ================================================================== - // Common stride calculations - // ================================================================== - assert(nhead % nhead_k == 0); - const float scale_s = 1.0f / std::sqrt(static_cast(hdim_q)); - - const ck_tile::index_t stride_q = i_perm ? hdim_q : nhead * hdim_q; - const ck_tile::index_t stride_k = i_perm ? hdim_q : nhead_k * hdim_q; - const ck_tile::index_t stride_v = i_perm ? hdim_v : nhead_k * hdim_v; - const ck_tile::index_t stride_o = o_perm ? hdim_v : nhead * hdim_v; - const ck_tile::index_t nhead_stride_q = i_perm ? seqlen_q * hdim_q : hdim_q; - const ck_tile::index_t nhead_stride_k = i_perm ? seqlen_k * hdim_q : hdim_q; - const ck_tile::index_t nhead_stride_v = i_perm ? seqlen_k * hdim_v : hdim_v; - const ck_tile::index_t nhead_stride_o = o_perm ? seqlen_q * hdim_v : hdim_v; - const ck_tile::index_t batch_stride_q = nhead * seqlen_q * hdim_q; - const ck_tile::index_t batch_stride_k = nhead_k * seqlen_k * hdim_q; - const ck_tile::index_t batch_stride_v = nhead_k * hdim_v * seqlen_k; - const ck_tile::index_t batch_stride_o = nhead * seqlen_q * hdim_v; - - std::string data_type = "fp16"; - if constexpr(std::is_same_v) - data_type = "bf16"; - - std::string msk_str = "0"; - mask_info mask = mask_info::decode(msk_str, seqlen_q, seqlen_k); - - // ================================================================== - // GPU: Build block map + VSA LUT (always run, device-only) - // ================================================================== - std::cout << "Building Sparge block map + VSA LUT (GPU)..." << std::endl; - { - sparge_blockmap_args args; - args.q_ptr = q_buf.GetDeviceBuffer(); - args.k_ptr = k_buf.GetDeviceBuffer(); - args.batch = batch; - args.seqlen_q = seqlen_q; - args.seqlen_k = seqlen_k; - args.hdim_q = hdim_q; - args.nhead_q = nhead; - args.nhead_k = nhead_k; - args.stride_q = stride_q; - args.stride_k = stride_k; - args.nhead_stride_q = nhead_stride_q; - args.nhead_stride_k = nhead_stride_k; - args.batch_stride_q = batch_stride_q; - args.batch_stride_k = batch_stride_k; - args.simthreshd1 = simthreshd1; - args.cdfthreshd = cdfthreshd; - args.topk = topk; - args.scale = scale_s; - args.block_map_ptr = bmap_buf.GetDeviceBuffer(); - args.lut_ptr = lut_buf.GetDeviceBuffer(); - args.valid_block_num_ptr = valid_buf.GetDeviceBuffer(); - - sparge_blockmap_traits traits; - traits.data_type = data_type; - traits.hdim_q = hdim_q; - - sparge_blockmap_fwd(traits, args, ck_tile::stream_config{}); - } - - // ================================================================== - // VSA sparse attention kernel (always run, LUT stays on device) - // ================================================================== - std::cout << "\n--- Running VSA sparse attention kernel ---" << std::endl; - - fmha_vsa_fwd_args fmha_args; - fmha_args.q_ptr = q_buf.GetDeviceBuffer(); - fmha_args.k_ptr = k_buf.GetDeviceBuffer(); - fmha_args.v_ptr = v_buf.GetDeviceBuffer(); - fmha_args.lut_ptr = lut_buf.GetDeviceBuffer(); - fmha_args.valid_block_num_ptr = valid_buf.GetDeviceBuffer(); - fmha_args.o_ptr = o_buf.GetDeviceBuffer(); - fmha_args.batch = batch; - fmha_args.seqlen_q = seqlen_q; - fmha_args.seqlen_k = seqlen_k; - fmha_args.max_seqlen_q = seqlen_q; - fmha_args.hdim_q = hdim_q; - fmha_args.hdim_v = hdim_v; - fmha_args.nhead_q = nhead; - fmha_args.nhead_k = nhead_k; - fmha_args.scale_s = scale_s; - fmha_args.stride_q = stride_q; - fmha_args.stride_k = stride_k; - fmha_args.stride_v = stride_v; - fmha_args.stride_o = stride_o; - fmha_args.nhead_stride_q = nhead_stride_q; - fmha_args.nhead_stride_k = nhead_stride_k; - fmha_args.nhead_stride_v = nhead_stride_v; - fmha_args.nhead_stride_o = nhead_stride_o; - fmha_args.batch_stride_q = batch_stride_q; - fmha_args.batch_stride_k = batch_stride_k; - fmha_args.batch_stride_v = batch_stride_v; - fmha_args.batch_stride_o = batch_stride_o; - fmha_args.window_size_left = mask.left; - fmha_args.window_size_right = mask.right; - fmha_args.mask_type = static_cast(mask.type); - - fmha_vsa_fwd_traits fmha_traits; - fmha_traits.hdim_q = hdim_q; - fmha_traits.hdim_v = hdim_v; - fmha_traits.data_type = data_type; - fmha_traits.is_v_rowmajor = true; - fmha_traits.mask_type = mask.type; - - ck_tile::stream_config stream_config{nullptr, - true, - /* log_level = */ kname ? 1 : 0, - warmup, - repeat, - false}; - - float avg_time_ms = sparge_vsa_fwd(fmha_traits, fmha_args, stream_config); - - std::cout << "\n>>>> VSA sparse attention average time: " << avg_time_ms << " ms <<<<" - << std::endl; - - // DtoH: attention output (always needed) - o_buf.FromDevice(output_host.data(), output_host.get_element_space_size_in_bytes()); - - // DtoH: block_map (needed for sparsity stats and validation) - ck_tile::HostTensor block_map_gpu({batch, nhead, num_q_blocks, num_k_blocks}); - bmap_buf.FromDevice(block_map_gpu.data(), bmap_bytes); - - // ================================================================== - // Sparsity statistics (pure CPU, reads block_map HostTensor) - // ================================================================== - std::size_t total_blocks = 0; - std::size_t active_blocks = 0; - for(ck_tile::index_t b = 0; b < batch; ++b) - { - for(ck_tile::index_t h = 0; h < nhead; ++h) - { - for(ck_tile::index_t qb = 0; qb < num_q_blocks; ++qb) - { - for(ck_tile::index_t kb = 0; kb < num_k_blocks; ++kb) - { - total_blocks++; - if(block_map_gpu(b, h, qb, kb) != 0) - active_blocks++; - } - } - } - } - float actual_sparsity = - 1.0f - static_cast(active_blocks) / static_cast(total_blocks); - std::cout << "\n Actual sparsity: " << actual_sparsity << " (" << active_blocks << "/" - << total_blocks << " blocks active)" << std::endl; - - // ================================================================== - // Validation (only when -v=1) - // ================================================================== - bool pass = true; - if(do_validation) - { - std::cout << "\n--- Performing CPU validation ---" << std::endl; - - // CPU golden: block map + VSA LUT - std::cout << "Building Sparge block map (CPU golden)..." << std::endl; - sparge::SpargeParams p; - p.BLKQ = static_cast(BLKQ); - p.BLKK = static_cast(BLKK); - p.simthreshd1 = simthreshd1; - p.cdfthreshd = cdfthreshd; - p.topk = topk; - p.i_perm = i_perm; - - ck_tile::HostTensor block_relation_onehot = - sparge::build_block_map_meansim(q_host, k_host, p); - - std::cout << "Converting block map to VSA LUT (delta, CPU)..." << std::endl; - auto vsa_lut_cpu = sparge::block_map_to_vsa_lut_delta(block_relation_onehot); - - // DtoH: LUT + valid_block_num (only for validation) - sparge::VSALut vsa_lut_gpu{ - ck_tile::HostTensor({batch, nhead, num_q_blocks, num_k_blocks}), - ck_tile::HostTensor({batch, nhead, num_q_blocks}), - }; - lut_buf.FromDevice(vsa_lut_gpu.lut.data(), lut_bytes); - valid_buf.FromDevice(vsa_lut_gpu.valid_block_num.data(), valid_bytes); - - // Validate block map - std::cout << "\n--- Validating GPU block map vs CPU golden ---" << std::endl; - { - std::size_t bmap_mismatches = 0; - for(ck_tile::index_t b = 0; b < batch; ++b) - { - for(ck_tile::index_t h = 0; h < nhead; ++h) - { - for(ck_tile::index_t qb = 0; qb < num_q_blocks; ++qb) - { - for(ck_tile::index_t kb = 0; kb < num_k_blocks; ++kb) - { - if(block_map_gpu(b, h, qb, kb) != block_relation_onehot(b, h, qb, kb)) - { - bmap_mismatches++; - if(bmap_mismatches <= 10) - { - std::cout - << " block_map mismatch at [" << b << "," << h << "," << qb - << "," << kb << "]: GPU=" - << static_cast(block_map_gpu(b, h, qb, kb)) << " CPU=" - << static_cast(block_relation_onehot(b, h, qb, kb)) - << std::endl; - } - } - } - } - } - } - std::cout << " Block map mismatches: " << bmap_mismatches << " / " - << (batch * nhead * num_q_blocks * num_k_blocks) << std::endl; - if(bmap_mismatches > 0) - { - std::cout << ">>> GPU BLOCK MAP VALIDATION FAILED <<<" << std::endl; - pass = false; - } - else - { - std::cout << ">>> GPU BLOCK MAP VALIDATION PASSED <<<" << std::endl; - } - } - - // Validate VSA LUT - std::cout << "\n--- Validating GPU VSA LUT vs CPU golden ---" << std::endl; - { - std::size_t lut_mismatches = 0; - std::size_t valid_mismatches = 0; - for(ck_tile::index_t b = 0; b < batch; ++b) - { - for(ck_tile::index_t h = 0; h < nhead; ++h) - { - for(ck_tile::index_t qb = 0; qb < num_q_blocks; ++qb) - { - if(vsa_lut_gpu.valid_block_num(b, h, qb) != - vsa_lut_cpu.valid_block_num(b, h, qb)) - { - valid_mismatches++; - if(valid_mismatches <= 5) - { - std::cout << " valid_block_num mismatch at [" << b << "," << h - << "," << qb - << "]: GPU=" << vsa_lut_gpu.valid_block_num(b, h, qb) - << " CPU=" << vsa_lut_cpu.valid_block_num(b, h, qb) - << std::endl; - } - } - for(ck_tile::index_t kb = 0; kb < num_k_blocks; ++kb) - { - if(vsa_lut_gpu.lut(b, h, qb, kb) != vsa_lut_cpu.lut(b, h, qb, kb)) - { - lut_mismatches++; - if(lut_mismatches <= 10) - { - std::cout - << " LUT mismatch at [" << b << "," << h << "," << qb - << "," << kb << "]: GPU=" << vsa_lut_gpu.lut(b, h, qb, kb) - << " CPU=" << vsa_lut_cpu.lut(b, h, qb, kb) << std::endl; - } - } - } - } - } - } - std::cout << " LUT mismatches: " << lut_mismatches << std::endl; - std::cout << " valid_block_num mismatches: " << valid_mismatches << std::endl; - if(lut_mismatches == 0 && valid_mismatches == 0) - { - std::cout << ">>> GPU VSA LUT VALIDATION PASSED <<<" << std::endl; - } - else - { - std::cout << ">>> GPU VSA LUT VALIDATION FAILED <<<" << std::endl; - pass = false; - } - } - - // Validate attention output - float scale = 1.0f / std::sqrt(static_cast(hdim_q)); - - std::cout << "\nComputing reference attention output..." << std::endl; - auto q_ref = to_bhsd(q_host, i_perm); - auto k_ref = to_bhsd(k_host, i_perm); - auto v_ref = to_bhsd(v_host, i_perm); - - ck_tile::HostTensor output_ref({batch, nhead, seqlen_q, hdim_v}); - ck_tile::reference_blocked_attention( - q_ref, k_ref, v_ref, block_relation_onehot, output_ref, BLKQ, BLKK, scale); - - auto [rtol, atol] = get_error_tolerance(); - - float max_diff = 0.0f; - float max_rel_diff = 0.0f; - std::size_t num_errors = 0; - - auto output_host_bhsd = to_bhsd(output_host, o_perm); - for(std::size_t i = 0; i < output_host_bhsd.mData.size(); ++i) - { - float gpu_val = to_float_for_compare(output_host_bhsd.mData[i]); - float ref_val = to_float_for_compare(output_ref.mData[i]); - float diff = std::abs(gpu_val - ref_val); - float rel_diff = (std::abs(ref_val) > 1e-6f) ? diff / std::abs(ref_val) : diff; - - max_diff = std::max(max_diff, diff); - max_rel_diff = std::max(max_rel_diff, rel_diff); - - if(diff > atol && rel_diff > rtol) - { - num_errors++; - if(num_errors <= 5) - { - std::cout << " Mismatch at index " << i << ": GPU=" << gpu_val - << ", Ref=" << ref_val << ", Diff=" << diff << std::endl; - } - } - } - - std::cout << "\nAttention validation results:" << std::endl; - std::cout << " Max absolute difference: " << max_diff << std::endl; - std::cout << " Max relative difference: " << max_rel_diff << std::endl; - std::cout << " Number of mismatches: " << num_errors << " / " - << output_host_bhsd.mData.size() << std::endl; - - if(num_errors == 0) - { - std::cout << "\n>>> VALIDATION PASSED <<<" << std::endl; - } - else - { - std::cout << "\n>>> VALIDATION FAILED <<<" << std::endl; - pass = false; - } - } - - std::cout << "\n" << (pass ? "TEST PASSED" : "TEST FAILED") << std::endl; - return pass; -} - -// ============================================================================ -// Main -// ============================================================================ - -int main(int argc, char* argv[]) -{ - auto [result, arg_parser] = create_args(argc, argv); - if(!result) - { - std::cerr << "Failed to parse arguments" << std::endl; - return -1; - } - - std::string prec = arg_parser.get_str("prec"); - - bool test_result = false; - if(prec == "fp16") - { - test_result = run_test(arg_parser); - } - else if(prec == "bf16") - { - test_result = run_test(arg_parser); - } - else - { - std::cerr << "Unsupported precision: " << prec << std::endl; - return -1; - } - - return test_result ? 0 : -1; -} diff --git a/include/ck_tile/ops/sparse_attn/pipeline/block_fmha_pipeline_qr_ks_vs_async_jenga.hpp b/include/ck_tile/ops/sparse_attn/pipeline/block_fmha_pipeline_qr_ks_vs_async_jenga.hpp index 67936c4353f..9fe8b365b00 100644 --- a/include/ck_tile/ops/sparse_attn/pipeline/block_fmha_pipeline_qr_ks_vs_async_jenga.hpp +++ b/include/ck_tile/ops/sparse_attn/pipeline/block_fmha_pipeline_qr_ks_vs_async_jenga.hpp @@ -318,26 +318,26 @@ struct BlockFmhaPipelineQRKSVSAsyncJenga { if(!block_relation_onehot[i_total_loops]) { - i_total_loops++; - if(i_total_loops < num_total_loop) - { - // move K tile windows - move_tile_window(k_dram_block_window, {kN0, 0}); - k_dram_window.set_window_origin(k_dram_block_window.get_window_origin()); - - if(block_relation_onehot[i_total_loops]) - { - async_load_tile_raw(k_lds_store(LdsSeq.at(number<0>{})), - k_dram_window, - number<-1>{}, - k_oob_ck, - k_pre_np); - } - move_tile_window(k_dram_window, {0, kK0}); - move_tile_window(v_dram_window, {0, kN0}); - continue; - } - break; + // scan-ahead: find the next active block in one shot + index_t next = i_total_loops + 1; + while(next < num_total_loop && !block_relation_onehot[next]) + next++; + if(next >= num_total_loop) + break; + const index_t delta = next - i_total_loops; + i_total_loops = next; + // jump K/V windows to the next active block + move_tile_window(k_dram_block_window, {kN0 * delta, 0}); + k_dram_window.set_window_origin(k_dram_block_window.get_window_origin()); + move_tile_window(v_dram_window, {0, kN0 * delta}); + // immediately prefetch the active K tile + async_load_tile_raw(k_lds_store(LdsSeq.at(number<0>{})), + k_dram_window, + number<-1>{}, + k_oob_ck, + k_pre_np); + move_tile_window(k_dram_window, {0, kK0}); + continue; } // STAGE 1, QK gemm From eca3cb3e0abdcb927a02f9b7b9ed786b9a9cdda2 Mon Sep 17 00:00:00 2001 From: Gino Lu Date: Fri, 24 Apr 2026 05:13:51 -0400 Subject: [PATCH 06/16] sparse_attn: add bm0 dispatch for sparge blockmap compatibility Add bm0 field to fmha_jenga_fwd_traits so callers can specify the preferred Q-tile size. Codegen now emits separate tile configs for bm0=64 (sparge blockmap) and bm0=128 (original), with CppConstraint guards to select the right kernel at runtime. End-to-end test passes for both jenga and vsa paths. Performance is known to be suboptimal at this stage; tile sizes and warp counts for the bm0=64 path have not been tuned. Co-Authored-By: Claude Opus 4.7 --- .../codegen/ops/fmha_fwd_jenga.py | 58 ++++++++++--------- .../codegen/ops/fmha_fwd_vsa.py | 58 ++++++++++--------- .../ck_tile/50_sparse_attn/fmha_fwd_trek.hpp | 2 +- .../ck_tile/50_sparse_attn/test_sparge.cpp | 2 + 4 files changed, 63 insertions(+), 57 deletions(-) diff --git a/example/ck_tile/50_sparse_attn/codegen/ops/fmha_fwd_jenga.py b/example/ck_tile/50_sparse_attn/codegen/ops/fmha_fwd_jenga.py index 1f0a78048d9..fc4b8642ddd 100644 --- a/example/ck_tile/50_sparse_attn/codegen/ops/fmha_fwd_jenga.py +++ b/example/ck_tile/50_sparse_attn/codegen/ops/fmha_fwd_jenga.py @@ -690,12 +690,12 @@ def get_hdim_tile_size_dict(dtype: str) -> Optional[dict]: # FmhaFwdTileSize(128, 64, 32, 64, 32, 64, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, -1)], # (96, 128) : [FmhaFwdTileSize(128, 128, 32, 128, 32, 96, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, -1)], (128, 128): [ - FmhaFwdTileSize( # fmt: skip -- 64x128 tile matching blockmap kM0=64, kN0=128 - 64, + FmhaFwdTileSize( # fmt: skip -- 128x128 tile (original, for old sparse attn test) 128, - 64, 128, - 64, + 32, + 128, + 32, 128, 4, 1, @@ -703,13 +703,36 @@ def get_hdim_tile_size_dict(dtype: str) -> Optional[dict]: 4, 1, 1, + 32, + 32, 16, + 32, + 32, 16, + -1, + CppConstraint("t.bm0 == 0 || t.bm0 == 128"), + ), + FmhaFwdTileSize( # fmt: skip -- 64x128 tile (for sparge blockmap kM0=64) + 64, + 128, + 32, + 128, + 32, + 128, + 2, + 1, + 1, + 2, + 1, + 1, + 32, + 32, 16, - 16, - 16, + 32, + 32, 16, -1, + CppConstraint("t.bm0 == 64"), ), FmhaFwdTileSize( # fmt: skip 16, @@ -774,27 +797,6 @@ def get_hdim_tile_size_dict(dtype: str) -> Optional[dict]: 16, -1, ), - FmhaFwdTileSize( # fmt: skip - 128, - 128, - 32, - 128, - 32, - 128, - 4, - 1, - 1, - 4, - 1, - 1, - 32, - 32, - 16, - 32, - 32, - 16, - -1, - ), ], # (160,160) : [FmhaFwdTileSize(128, 128, 32, 160, 32, 160, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, 1)], # (192,128) : [FmhaFwdTileSize(128, 128, 32, 128, 32, 192, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, -1)], @@ -909,7 +911,7 @@ def get_fwd_blobs( for tile, pipeline in itertools.product( tiles, factory.get_pipelines(dtype, hdim, hdim_v, receipt, mask_impl) ): - if tile.F_bm0 != 64 or tile.F_bn0 != 128: + if tile.F_bm0 not in (64, 128) or tile.F_bn0 != 128: continue if pipeline.tag != "qr_async": continue diff --git a/example/ck_tile/50_sparse_attn/codegen/ops/fmha_fwd_vsa.py b/example/ck_tile/50_sparse_attn/codegen/ops/fmha_fwd_vsa.py index 217cfcfe2a4..208877037f1 100644 --- a/example/ck_tile/50_sparse_attn/codegen/ops/fmha_fwd_vsa.py +++ b/example/ck_tile/50_sparse_attn/codegen/ops/fmha_fwd_vsa.py @@ -690,12 +690,12 @@ def get_hdim_tile_size_dict(dtype: str) -> Optional[dict]: # FmhaFwdTileSize(128, 64, 32, 64, 32, 64, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, -1)], # (96, 128) : [FmhaFwdTileSize(128, 128, 32, 128, 32, 96, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, -1)], (128, 128): [ - FmhaFwdTileSize( # fmt: skip -- 64x128 tile matching blockmap kM0=64, kN0=128 - 64, + FmhaFwdTileSize( # fmt: skip -- 128x128 tile (original, for old sparse attn test) 128, - 64, 128, - 64, + 32, + 128, + 32, 128, 4, 1, @@ -703,13 +703,36 @@ def get_hdim_tile_size_dict(dtype: str) -> Optional[dict]: 4, 1, 1, + 32, + 32, 16, + 32, + 32, 16, + -1, + CppConstraint("t.bm0 == 0 || t.bm0 == 128"), + ), + FmhaFwdTileSize( # fmt: skip -- 64x128 tile (for sparge blockmap kM0=64) + 64, + 128, + 32, + 128, + 32, + 128, + 2, + 1, + 1, + 2, + 1, + 1, + 32, + 32, 16, - 16, - 16, + 32, + 32, 16, -1, + CppConstraint("t.bm0 == 64"), ), FmhaFwdTileSize( # fmt: skip 16, @@ -774,27 +797,6 @@ def get_hdim_tile_size_dict(dtype: str) -> Optional[dict]: 16, -1, ), - FmhaFwdTileSize( # fmt: skip - 128, - 128, - 32, - 128, - 32, - 128, - 4, - 1, - 1, - 4, - 1, - 1, - 32, - 32, - 16, - 32, - 32, - 16, - -1, - ), ], # (160,160) : [FmhaFwdTileSize(128, 128, 32, 160, 32, 160, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, 1)], # (192,128) : [FmhaFwdTileSize(128, 128, 32, 128, 32, 192, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, -1)], @@ -909,7 +911,7 @@ def get_fwd_blobs( for tile, pipeline in itertools.product( tiles, factory.get_pipelines(dtype, hdim, hdim_v, receipt, mask_impl) ): - if tile.F_bm0 != 64 or tile.F_bn0 != 128: + if tile.F_bm0 not in (64, 128) or tile.F_bn0 != 128: continue if pipeline.tag != "qr_async_vsa": continue diff --git a/example/ck_tile/50_sparse_attn/fmha_fwd_trek.hpp b/example/ck_tile/50_sparse_attn/fmha_fwd_trek.hpp index 350d1803f66..62d40ffbe02 100644 --- a/example/ck_tile/50_sparse_attn/fmha_fwd_trek.hpp +++ b/example/ck_tile/50_sparse_attn/fmha_fwd_trek.hpp @@ -272,7 +272,7 @@ struct fmha_jenga_fwd_traits std::string data_type; bool is_v_rowmajor; mask_enum mask_type; - // TODO: padding check is inside this api + int bm0 = 0; // preferred Q-tile size; 0 = don't care (dispatch picks largest) }; float fmha_jenga_fwd(fmha_jenga_fwd_traits, fmha_jenga_fwd_args, const ck_tile::stream_config&); diff --git a/example/ck_tile/50_sparse_attn/test_sparge.cpp b/example/ck_tile/50_sparse_attn/test_sparge.cpp index 7c30a10b062..81a49ca006b 100644 --- a/example/ck_tile/50_sparse_attn/test_sparge.cpp +++ b/example/ck_tile/50_sparse_attn/test_sparge.cpp @@ -249,6 +249,7 @@ bool run_test(const ck_tile::ArgParser& arg_parser) attn_traits.data_type = std::is_same_v ? "fp16" : "bf16"; attn_traits.is_v_rowmajor = true; attn_traits.mask_type = mask_enum::no_mask; + attn_traits.bm0 = BLKQ; fmha_jenga_fwd_args attn_args; attn_args.q_ptr = q_dev.GetDeviceBuffer(); @@ -291,6 +292,7 @@ bool run_test(const ck_tile::ArgParser& arg_parser) attn_traits.data_type = std::is_same_v ? "fp16" : "bf16"; attn_traits.is_v_rowmajor = true; attn_traits.mask_type = mask_enum::no_mask; + attn_traits.bm0 = BLKQ; fmha_vsa_fwd_args attn_args; attn_args.q_ptr = q_dev.GetDeviceBuffer(); From b00e5449c8cd105dbfc8dcf933cf7849a57d2e04 Mon Sep 17 00:00:00 2001 From: Gino Lu Date: Tue, 5 May 2026 03:13:24 -0400 Subject: [PATCH 07/16] sparse_attn: split KStats kernel, add README + perf charts - Split SpargeKStatsKernel/Pipeline out of BlockMap (Kernel A produces per-block K stats workspace consumed by Kernel B), removing redundant K-stat recomputation across Q-blocks. - Add example/ck_tile/50_sparse_attn/README.md (status vs upstream pinned to ae5b629, unported items, usage, references). - Add example/ck_tile/50_sparse_attn/docs/{speedup_vs_sparsity,kernel_breakdown}.png + reusable plot_sparge_perf.py (b=2 h=32 s=16384 d=128 fp16 perf snapshot). Co-Authored-By: Claude Opus 4 --- example/ck_tile/50_sparse_attn/README.md | 45 +++ .../50_sparse_attn/docs/kernel_breakdown.png | Bin 0 -> 85047 bytes .../50_sparse_attn/docs/plot_sparge_perf.py | 258 ++++++++++++++++++ .../docs/speedup_vs_sparsity.png | Bin 0 -> 127494 bytes .../50_sparse_attn/sparge_blockmap_inst.cpp | 110 ++++++-- .../50_sparse_attn/sparge_blockmap_trek.hpp | 43 ++- .../ck_tile/50_sparse_attn/test_sparge.cpp | 33 ++- .../kernel/sparge_blockmap_kernel.hpp | 46 +++- .../kernel/sparge_kstats_kernel.hpp | 136 +++++++++ .../pipeline/sparge_blockmap_pipeline.hpp | 154 ++++++----- .../pipeline/sparge_kstats_pipeline.hpp | 110 ++++++++ 11 files changed, 839 insertions(+), 96 deletions(-) create mode 100644 example/ck_tile/50_sparse_attn/README.md create mode 100644 example/ck_tile/50_sparse_attn/docs/kernel_breakdown.png create mode 100644 example/ck_tile/50_sparse_attn/docs/plot_sparge_perf.py create mode 100644 example/ck_tile/50_sparse_attn/docs/speedup_vs_sparsity.png create mode 100644 include/ck_tile/ops/sparse_attn/kernel/sparge_kstats_kernel.hpp create mode 100644 include/ck_tile/ops/sparse_attn/pipeline/sparge_kstats_pipeline.hpp diff --git a/example/ck_tile/50_sparse_attn/README.md b/example/ck_tile/50_sparse_attn/README.md new file mode 100644 index 00000000000..c7191c8e828 --- /dev/null +++ b/example/ck_tile/50_sparse_attn/README.md @@ -0,0 +1,45 @@ +# Sparge Attention (Composable Kernel) + +A Composable Kernel port of [SpargeAttn](https://github.com/thu-ml/SpargeAttn) for AMD GPU. Both the block-map pipeline (mean-pool → cosine sim → pooled QK → top-k LUT) and the sparse FMHA stage run on-GPU. Two attention backends are exposed via `-pipeline=vsa` (default, faster) and `-pipeline=jenga` (async K/V load variant). + +## Status vs Upstream + +Implemented: +- per-block mean-pool, cosine similarity, pooled QK +- top-k / `cdfthreshd` block selection, BlockMap LUT +- sparse FMHA (both `vsa` and `jenga` backends) +- per-head `topk` / `simthreshd1` / `cdfthreshd` + +Not yet ported (upstream pinned to commit [`ae5b629`](https://github.com/thu-ml/SpargeAttn/tree/ae5b629ebb41e41f86b3ea2ab5a3283f13ac151a)): +- **K smoothing** — pre-pool `k -= km`; required for diffusion / video checkpoints (CogVideoX, Mochi-1, Flux, OpenSora, SD 3.5) ([spas_sage_attn/core.py:L53](https://github.com/thu-ml/SpargeAttn/blob/ae5b629ebb41e41f86b3ea2ab5a3283f13ac151a/spas_sage_attn/core.py#L53)) +- **is_causal mask in pooled score** — required for causal-LM prefill (Llama, Qwen) ([spas_sage_attn/utils.py:L338](https://github.com/thu-ml/SpargeAttn/blob/ae5b629ebb41e41f86b3ea2ab5a3283f13ac151a/spas_sage_attn/utils.py#L338)) +- **attention_sink** — column 0 forced ON; upstream is hard-wired to `True` at inference ([spas_sage_attn/autotune.py:L355](https://github.com/thu-ml/SpargeAttn/blob/ae5b629ebb41e41f86b3ea2ab5a3283f13ac151a/spas_sage_attn/autotune.py#L355)) +- **pv_threshold per-Q-tile skip in attn kernel** — pure perf, ~5–15% on the dominant attention slice ([spas_sage_attn/core.py:L265](https://github.com/thu-ml/SpargeAttn/blob/ae5b629ebb41e41f86b3ea2ab5a3283f13ac151a/spas_sage_attn/core.py#L265)) +- **Sort-based top-k selection** — replaces our O(N_k^2) iterative argmax; matters at long seqlen (s ≥ 16k) ([spas_sage_attn/utils.py:L345](https://github.com/thu-ml/SpargeAttn/blob/ae5b629ebb41e41f86b3ea2ab5a3283f13ac151a/spas_sage_attn/utils.py#L345)) +- **Q/K int8 quant fusion in pool kernel** — enables a downstream int8 GEMM0 in the attn kernel ([spas_sage_attn/utils.py:L371](https://github.com/thu-ml/SpargeAttn/blob/ae5b629ebb41e41f86b3ea2ab5a3283f13ac151a/spas_sage_attn/utils.py#L371)) + +## Performance + +At b=2 h=32 s=16384 fp16, sparge (vsa backend) reaches **1.78× FMHA throughput at topk=0.4** and **5.04× at topk=0.1**, and stays above 1.0× across the full topk range. + +![Speedup vs sparsity](docs/speedup_vs_sparsity.png) + +*Speedup vs FMHA, b=2 h=32 s=16384 d=128 fp16. Shape chosen to match Fig. 10 of the SpargeAttn paper ([arXiv:2502.18137](https://arxiv.org/abs/2502.18137); Mochi-1, 22K context, head_dim=128); s=16384 is the closest grid point. Gray-outlined points have >30% inter-rep spread.* + +![Kernel breakdown](docs/kernel_breakdown.png) + +*BlockMap (`_pre`) stacked on attention (`_attn`), b=2 h=32 d=128 fp16 topk=0.4. BlockMap is roughly 17% of total at s=16384.* + +## Usage + +```bash +ninja tile_example_sparge +./bin/tile_example_sparge -pipeline=vsa -b=2 -h=32 -s=16384 -d=128 -topk=0.4 -simthreshd1=0.001 +``` + +Add `-v=1` for CPU validation; use a small shape (`-b=1 -h=2 -s=512`), since full-shape CPU reference scales O(s²) and runs 30+ minutes at s=8k, hours at s=16k. + +## References + +- [SpargeAttn upstream](https://github.com/thu-ml/SpargeAttn) (pinned to [`ae5b629`](https://github.com/thu-ml/SpargeAttn/tree/ae5b629ebb41e41f86b3ea2ab5a3283f13ac151a)) +- [Paper — Zhang et al., arXiv:2502.18137](https://arxiv.org/abs/2502.18137) diff --git a/example/ck_tile/50_sparse_attn/docs/kernel_breakdown.png b/example/ck_tile/50_sparse_attn/docs/kernel_breakdown.png new file mode 100644 index 0000000000000000000000000000000000000000..8704334155cbb0b5351293e9406d594de2ebfc82 GIT binary patch literal 85047 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
    !*laDTdS?US#enL#ct3Jd|Tf0u3 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 literal 0 HcmV?d00001 diff --git a/example/ck_tile/50_sparse_attn/docs/plot_sparge_perf.py b/example/ck_tile/50_sparse_attn/docs/plot_sparge_perf.py new file mode 100644 index 00000000000..95a13d5f65c --- /dev/null +++ b/example/ck_tile/50_sparse_attn/docs/plot_sparge_perf.py @@ -0,0 +1,258 @@ +#!/usr/bin/env python3 +"""Plot sparge perf charts from full_grid.csv. + +Re-run with different fixed (b, h, s, dtype, topk) by editing the constants below. +No GPU / no srun / no rebuild — pure matplotlib from CSV. +""" +import os +import sys +import pandas as pd +import matplotlib.pyplot as plt +import numpy as np + +# ---------------------------------------------------------------------- +# Tunable constants — edit these to regenerate for a different point. +# ---------------------------------------------------------------------- +CSV_PATH = "/home/AMD/ginolu12/gino_tmp/full_grid.csv" +OUT_DIR = os.path.dirname(os.path.abspath(__file__)) + +# Chart 1 — speedup vs topk for one fixed (b, h, s, dtype) +CHART1_B = 2 +CHART1_H = 32 +CHART1_S = 16384 +CHART1_DTYPE = "fp16" +CHART1_HEAD_DIM = 128 # for title only + +# Chart 2 — kernel breakdown across s for fixed (b, h, dtype, topk) +CHART2_B = 2 +CHART2_H = 32 +CHART2_DTYPE = "fp16" +CHART2_TOPK = 0.4 +CHART2_S_LIST = [2048, 4096, 8192, 16384] +CHART2_HEAD_DIM = 128 # for title only + +DPI = 140 + +# ---------------------------------------------------------------------- +# Helpers +# ---------------------------------------------------------------------- +def is_fail(note): + if not isinstance(note, str): + return False + return "FAIL" in note + +def is_high_spread(note): + if not isinstance(note, str): + return False + return "HIGH_SPREAD" in note + +def load_data(): + df = pd.read_csv(CSV_PATH) + return df + +# ---------------------------------------------------------------------- +# Chart 1 +# ---------------------------------------------------------------------- +def plot_chart1(df, out_path): + sel = df[ + (df["b"] == CHART1_B) + & (df["h"] == CHART1_H) + & (df["s"] == CHART1_S) + & (df["dtype"] == CHART1_DTYPE) + ].copy() + sel = sel.sort_values("topk").reset_index(drop=True) + + if sel.empty: + print(f"[chart1] WARNING: no rows for b={CHART1_B} h={CHART1_H} s={CHART1_S} dtype={CHART1_DTYPE}") + return [], 0 + + # Drop fully failed rows but keep partial-fail rows; we'll mask per-series. + # Convert numeric columns + for col in ["sparge_jenga", "sparge_vsa", "sparse_jenga", "sparse_vsa", "fmha_us"]: + sel[col] = pd.to_numeric(sel[col], errors="coerce") + + fmha = sel["fmha_us"] + + # Compute speedups; rows with FAIL on a given column will have NaN already. + series = { + "sparge_vsa": fmha / sel["sparge_vsa"], + "sparge_jenga": fmha / sel["sparge_jenga"], + "sparse_vsa": fmha / sel["sparse_vsa"], + "sparse_jenga": fmha / sel["sparse_jenga"], + } + + style = { + "sparge_vsa": {"color": "#1f77b4", "marker": "o", "lw": 2.0}, + "sparge_jenga": {"color": "#ff7f0e", "marker": "s", "lw": 2.0}, + "sparse_vsa": {"color": "#2ca02c", "marker": "^", "lw": 1.5, "ls": "--"}, + "sparse_jenga": {"color": "#d62728", "marker": "v", "lw": 1.5, "ls": "--"}, + } + + fig, ax = plt.subplots(figsize=(8.5, 5.5), dpi=DPI) + + x = sel["topk"].to_numpy() + + # HIGH_SPREAD overlay first (under main markers) + hs_mask = sel["note"].apply(is_high_spread) + high_spread_cells = [] + if hs_mask.any(): + for _, row in sel[hs_mask].iterrows(): + high_spread_cells.append((row["topk"], row["max_spread_pct"])) + # gray ring underneath every series's data point at that x + for label, sp in series.items(): + xs_hs = x[hs_mask.to_numpy()] + ys_hs = sp[hs_mask.to_numpy()].to_numpy() + ax.scatter(xs_hs, ys_hs, s=180, facecolors="none", + edgecolors="gray", linewidths=1.5, zorder=2) + + for label, sp in series.items(): + st = style[label] + ax.plot(x, sp.to_numpy(), label=label, + color=st["color"], marker=st["marker"], + linewidth=st["lw"], linestyle=st.get("ls", "-"), + markersize=7, zorder=3) + + ax.axhline(1.0, color="black", linestyle=":", linewidth=1.2, label="fmha (baseline)", zorder=1) + + ax.set_xlabel("topk (kept fraction)") + ax.set_ylabel("speedup vs FMHA dense (×)") + ax.set_title( + f"Speedup vs FMHA " + f"(b={CHART1_B} h={CHART1_H} s={CHART1_S} d={CHART1_HEAD_DIM} {CHART1_DTYPE})" + ) + ax.grid(True, which="both", linestyle=":", alpha=0.6) + ax.set_xticks(np.arange(0.1, 0.71, 0.1)) + ax.legend(loc="best", framealpha=0.9) + + # Footnote about HIGH_SPREAD overlay + if high_spread_cells: + ax.text(0.01, -0.16, + "Gray rings: HIGH_SPREAD cells (high run-to-run variance)", + transform=ax.transAxes, fontsize=8, color="gray") + + fig.tight_layout() + fig.savefig(out_path, dpi=DPI, bbox_inches="tight") + plt.close(fig) + return high_spread_cells, os.path.getsize(out_path) + + +# ---------------------------------------------------------------------- +# Chart 2 +# ---------------------------------------------------------------------- +def plot_chart2(df, out_path): + sel = df[ + (df["b"] == CHART2_B) + & (df["h"] == CHART2_H) + & (df["dtype"] == CHART2_DTYPE) + & (np.isclose(df["topk"], CHART2_TOPK)) + & (df["s"].isin(CHART2_S_LIST)) + ].copy() + sel = sel.sort_values("s").reset_index(drop=True) + + if sel.empty: + print(f"[chart2] WARNING: no rows for b={CHART2_B} h={CHART2_H} dtype={CHART2_DTYPE} topk={CHART2_TOPK}") + return 0 + + for col in ["sparge_jenga_pre", "sparge_jenga_attn", + "sparge_vsa_pre", "sparge_vsa_attn", "fmha_us"]: + sel[col] = pd.to_numeric(sel[col], errors="coerce") + + s_vals = sel["s"].to_numpy() + n = len(s_vals) + idx = np.arange(n, dtype=float) + + width = 0.35 + offset = width / 2 + 0.02 + + fig, ax = plt.subplots(figsize=(9.0, 5.8), dpi=DPI) + + # Jenga bars (left of group) + jenga_pre = sel["sparge_jenga_pre"].to_numpy() + jenga_attn = sel["sparge_jenga_attn"].to_numpy() + vsa_pre = sel["sparge_vsa_pre"].to_numpy() + vsa_attn = sel["sparge_vsa_attn"].to_numpy() + fmha_vals = sel["fmha_us"].to_numpy() + + color_jenga_pre = "#fdbf6f" # light orange + color_jenga_attn = "#ff7f0e" # orange + color_vsa_pre = "#a6cee3" # light blue + color_vsa_attn = "#1f77b4" # blue + + bj_pre = ax.bar(idx - offset, jenga_pre, width, + color=color_jenga_pre, edgecolor="black", linewidth=0.6, + label="sparge_jenga _pre (BlockMap)") + bj_at = ax.bar(idx - offset, jenga_attn, width, bottom=jenga_pre, + color=color_jenga_attn, edgecolor="black", linewidth=0.6, + label="sparge_jenga _attn") + bv_pre = ax.bar(idx + offset, vsa_pre, width, + color=color_vsa_pre, edgecolor="black", linewidth=0.6, + label="sparge_vsa _pre (BlockMap)") + bv_at = ax.bar(idx + offset, vsa_attn, width, bottom=vsa_pre, + color=color_vsa_attn, edgecolor="black", linewidth=0.6, + label="sparge_vsa _attn") + + # Add total labels on top of each stack + totals_jenga = jenga_pre + jenga_attn + totals_vsa = vsa_pre + vsa_attn + for i in range(n): + ax.text(idx[i] - offset, totals_jenga[i], f"{totals_jenga[i]:.0f}", + ha="center", va="bottom", fontsize=8) + ax.text(idx[i] + offset, totals_vsa[i], f"{totals_vsa[i]:.0f}", + ha="center", va="bottom", fontsize=8) + + # FMHA reference: short horizontal dashed segment per group + seg_half = 0.40 + fmha_label_done = False + for i in range(n): + ax.hlines(fmha_vals[i], idx[i] - seg_half, idx[i] + seg_half, + colors="black", linestyles="dashed", linewidth=1.2, + label="fmha dense (reference)" if not fmha_label_done else None, + zorder=5) + ax.text(idx[i] + seg_half + 0.02, fmha_vals[i], + f"fmha {fmha_vals[i]:.0f}", fontsize=7, va="center", color="black") + fmha_label_done = True + + ax.set_xticks(idx) + ax.set_xticklabels([f"s={s}" for s in s_vals.astype(int)]) + ax.set_xlabel("sequence length (s)") + ax.set_ylabel("kernel time (µs)") + ax.set_title( + f"Sparge kernel time breakdown " + f"(b={CHART2_B} h={CHART2_H} d={CHART2_HEAD_DIM} {CHART2_DTYPE}, topk={CHART2_TOPK})" + ) + ax.grid(True, axis="y", linestyle=":", alpha=0.6) + ax.legend(loc="upper left", framealpha=0.9, fontsize=9) + + # log-y is too aggressive — leave linear; bars will just be tall. + fig.tight_layout() + fig.savefig(out_path, dpi=DPI, bbox_inches="tight") + plt.close(fig) + return os.path.getsize(out_path) + + +# ---------------------------------------------------------------------- +# Main +# ---------------------------------------------------------------------- +def main(): + os.makedirs(OUT_DIR, exist_ok=True) + df = load_data() + + chart1_path = os.path.join(OUT_DIR, "speedup_vs_sparsity.png") + chart2_path = os.path.join(OUT_DIR, "kernel_breakdown.png") + + hs_cells, size1 = plot_chart1(df, chart1_path) + size2 = plot_chart2(df, chart2_path) + + print(f"Wrote {chart1_path} ({size1} bytes)") + print(f"Wrote {chart2_path} ({size2} bytes)") + + if hs_cells: + print("HIGH_SPREAD cells in chart-1 selection:") + for topk, pct in hs_cells: + print(f" topk={topk} max_spread_pct={pct}") + else: + print("No HIGH_SPREAD cells in chart-1 selection.") + + +if __name__ == "__main__": + main() diff --git a/example/ck_tile/50_sparse_attn/docs/speedup_vs_sparsity.png b/example/ck_tile/50_sparse_attn/docs/speedup_vs_sparsity.png new file mode 100644 index 0000000000000000000000000000000000000000..9a2f053b0b46fab1620939cda92d4fc3efc067b3 GIT binary patch literal 127494 zcmd43c{rDA+ctb@phQU-LS%|0LNpo@A}J|T$P^helQLH*Wh`TgG9*Mv<}r#wDH)P^ z&P>Kkz5BJ+eLt)Fd7k&bZ~MM)+m=O!-|xE4^Ei)VKlXh;E%$7!iqs0o6gRZ^5w zBZxH-1hHyn9VNcw(zo^|J{@wD*K#~-W9sN^Xm3KO7&_WswsE{{ZnWcyiM@lljkTDN z$X=m?f;-F{9c>*B2@6~O*KY{f*k2Ofsqj@Bud?1&QQLta7;cjv6mOek9upJY+d);>w7*~G@MU!tv{0$*qD8O*~x5Ay2ZQm zsrAh%2U4BxaybcaO`*J)OrtGo!c)6QWpir%`e5&nWA>iucT+z`%^m!;)0RDUijBh9 z|2m!QI!^Du|0n*#JfHaIpK8u$dg%ZCGud@#SjB}l|MSzV|9}3_Eb7}gt#xZw;XjoK z+1I+`?L~$cFM8+Z9*7#ezf*2=Tz&3786 zw6wHb=Og>ArKSAiM_S_w|2MVLzy0Uuuc|~ST~%}mIrrssW#D#`#t?}(T#}ZyHm9)g zi!%3<4-bUswBu5(S8zF;bn8ARosK;*942AS?;uiF;A~@E(v+^3>UhnBP0Ec~^TmaC z{@cVldwb9AX?bpKl)=m-eBo`J{N2vcQCe13R`LRxo1b|8Uip^i!1vZD<7xHtdrM1h z&vkM(8dnC&7hD-tE%9(Cf9>11$H#-%zvbF-w->vo+O|zkJC1zM+Q8v4&v`z_n)dA3 zv&^il@!FZDkq7L!##-~vRv$m>j?Xs(Soa$(%}<)O<%>^GPoFEkT3c0Bmeb*JS4c1a z@r5Fny&@vlGc(0}H*o|?ugo8_{uLR?{9HGWer09F>`UC*=V@t%W@fj2X<3aXI^V3@ zxG~+<6ZcWFJj?3l?rwPL64evcby&-**RFBy*g^dI_3K1aOmHxvtgJldz`@FzI-T#f zb=x+-(o&g|j}9?xKji48%I}TcOi;Qnp!?$SV}kE=%-e?t9X{tdT*5NH`TUuVmSvyn zkR-(kmQzwI^TxUP`DJh4uBoV~u)T7nVt%s6Zn$wzRv@3aID@3?temawULN}O`Bz4% zIXF1>S$#j3HX0Tl8tPr-I%o9e#@ZiW;;CwCYIe}A>l_(*UUwysUGk1$x$l!R@d|rI zMQ_}^xw^2ZDC2UoXkLd?dU|@~Uc*(NVkGZ=O**ali+LTV(^hV8x;=Kw3r?n%mes^w z;`z~=lr`n$H=TZbz8)17W%sq@S3j#t#QAt5Bcohn-m3dCV*B^+@9*h}pSV<)pd>o{ zSHx^0S~4%4&TGNi8$EscwDpyOf`Wau+wx2k4KdtS;G8%!JJuFWVW981?Br7|FE1~0 zU8Z!@`DgEkFzW(QA0MBZckg_*v9P>CM1A-e5ff2mk!pVU%1CFR&JMbljC;<%{PgIs z7$V9#KPYHZbxqB=Yb|wCC6-O;L&Cai*RNmSnh_ZtEvBid*@$KC@9Zp9vHALx)^)Pm zr!`;FWx6w5L`=+hv?b?~RZF&sudgo+@%8IhW9g-do0NemLx&uH?3ii}UEz=Z(2`@5 zNlDAHmpIA4Rg}EbuC6yK!%Z(@TMxO;j&#i$U%FJ;+SaDMI6E4Zn8>?-xoEE47&l}; z+&?go>9M?ESKZvwl1X0f@#Dv>&m+XOPMo--HnaHa*9SJ|lPCSvhLhtYJez1(B~ErN z9<=LzOWm|Ml=>pqgqDUzYq&8@JL%M;NODPB)l^j<+b(b6JGGaao14;$T%X1?9WiHT z=dl7^tjovf{i$w8qW9aF5|OuVQB~L0YF=BubNSoLqg{FS_V#KP%Y}tUBrd+Evo3n7 z68$P&ui%yG#|L&Dv{*3v;dgJ}?v<33Y<#N1x|8v4>P#e}VfBnIm$#YE!-s5zbM3B* z%F4%btlMl6k32hfc8|3cT%G8oc=hU)?Nq;tpl&YR%uvHu$2{*!37d9*?8UC7<}AyI zGNPK9M(oBna;?c28oICN&; zUK%=uYI8Fi+d4Ru3zsb2sINbFTaK$W&w&aFjZ7Uhw6vQU8GV~GFIncF%ZiGQHtwki zxH|n~^Q+9v$?3tfI|Y^%w|@sgf*cbTp(FNl|w zmy>Um@bsw>e)74gsWOE7_)GJzJM)~H(sXX!WfCq!79f|x-d>c1$X!sz)7iNZU)k-bW;Bw1QSI(XH zoc{6Uz_p(|3$vqlZ{J?`_T9TKMCI+kzzRg7?NI$LEghZew1JTkf1HLcvT|YDQU}ct zUUy7eve$7YxGen|h{XwPYSR36Up7LumUl+6wz;o5BJ9HP@5e4x_-}iTM1K+yjzZ*9 zy)Zw&m$*FoJu9oxK}nD?bEnD!{2ME)YZKn#v8d8~?73}6We|tvfmmV5h`PGENI9-u z&MT+^)$TS4iHXUtt%|30Q))Oom)^|CR(<#o5;cdrvTxcgD0q>~2gJAAo4GCWYgqV3 zKg6Hh>F(~njbNmsQ%w`Ec_4n5!k{hR>3(!mmZc`8m;28TeAG?Lxc+SfGNw+K6iUoH z6p<*@7}urwjK*nQ0n7fHs1q}~Df~VDA`({L_l*^Xk6JlkWzwDR#f{(U?lx+CX?ToM zrmtNYr9UNW{QYXGjr?s6=5u3hx;1Tq#|bDba%fy{uLzX5;ypCjBo+pOFt9lr=Uoju%dy@);(0 zf4}0VTSrGvA7|^KsHm7{EEalUZnCG6S<+=6%RV#0*x2~BbsI~E+muS4aY1O3{9Q)6 zb!)zrczWhl7hap9+_`fn{w-=;x^?STgXM*pAB}0O&!0cfoy_{4V-s}Hp}!L`E6||E zuMrsVfZL91JcqW8;}dtX~NZPY8cl9rWERxO~5Qj|&#w+RZVrHNw%Z&6hJ z%+Ee$-8}oXs5mER+aH94T>mvSQ2y>6`3A}H4t(OKA4<9|sGkz%Le(2u4d-d(6PUbDydk7h>koWFVqa!0#dZ+of z#sS6(}Qt7vE%&2!}-(T+{kHdKJ;>NtZJOP_biPjQNjtC(=S={mD z2fo;I!`X@BK5@nofrejSnJD`VpGab2U{KEu3X)!7CO51vLf^pDR3pru;@R&4Utxi& z0}~z2GUv~;TqyC_BX)Wr1_-INqC&x158;`cn@c=fau2x|_?6F{t`vv-V#UmTf#p&! zZ|^lHqeR!LJlJ zv25ay@{W#XEnZo$^uBc~9;e-``4z8`v2liV+abr1Z<~WSq{DSc2!!NS_CAE$4o6^& zF$VF^ChfuIzA)Xj@_Dxb zzr@MsIt?>o`#&e2<VYix2~y(tT{3`YJZ>abb7;sgaAcu z1%EHHyw6){X)(u0x;|}`5Y{iIA%4^+Z$UBItt-LJy*ef)=3C)4v6Ct)UrGK{I*+}wi0!qoNp zQh4|6+e&nHcdydX(Ftn|i{VhO{kXh1*VWZ^9hiv$?=3gg|M5$CM0 zabQG*W?qZ5Z6^`PAsvXbY}{FT-J(G&e5Yqqy>DFy1wvYM>d=*ub=PIQUccVwL;D=Z zq;-7Ox-EZKmNZb-{QP`rdpkSP`Qyhrz|yg#=Rnn0uU;j$;bms#vAc|W=$M#pISn^H z(aEuH`0llR%a)rH6ShAF2cMv}3|cALnp?OT0uv{6O2iFj>jraP#$r zf-5Sf?Z`k;$;rF6tXm_;{~D>vFF2>MmX9xq+GaW5 z?U#LC+d$$lwU2E7__(k`w2YYn32impv5m|i$>g1Y6E$p+<9r^2Hyd>xQ$Zs`Wb#$KSz zf1tT~VIk?-RR0LB?8KwHt*yGVz*u98#b{%wZ5|;_?8?l}HYL6-{h9)jsQreZ*4EW! zTU=QjkGg)Hg8bno_9Lpfv7w<`O##nGMn-aV-kkVteRd!_H)Sb3d9XI)Rn9y0ijI!h z0@{-yco8w;xk+{S2tbkGo;?93CMFrd!E`_|`zfEz1$$RnZx>o#?j9W6H8oe$QyHX< zqqmKi9V-~u$QL%TxqH7|RtIfsHqzYKcubu%R(vfbrAF@d!8G5gf!Y_ZC1h`3c~ZeE zd%JOdFe$coh1uTL)&TW%BYDl_J#00v6_-!SHb1z=-8N$*mAXS$wdNCxEw#! z)veL1`3;oOI*lq%!gNz}bL}x^Nn|aA+2QfRa+UjvJgppI8yE$Hb&dlSA*>tBdhP`T z&{lu?6dqk-(;+n$RGW~%r(RF3%Afo)_>C>nC%{5asUkJ^yJyZT7 zf3Z2%-=8MRsY*gfsWN*0M|@DO$;!&24G_roL!lxSp%%JoeMyCP@1xtcr?pM2q;Gz0 zzQs8t)#4_#vH#3?hkkm-i|2KxziimB;eIsQ;%XWi8fJo#jZIL@21r&QcC`}=rA>R` zhlZC}wZx|T_bx0oH#dLiyOHr*s^rljr+d*!pBA5=46oAot*~Ou_h(A63{BN;D#-~#QEqx`t<43<$+I+7y^ei*iBd?aHm^W0&GXW zi>C(`IFD=O3gepzr=#wB2+xjp#7er&2QzHl`V#y25*()1`^nv>$2BciMo~=BAI&TCaO8z2qLaO}=9?Ye-?(c{)b7%Ss0u{>*j( z1vxl4xK!o$RIN0HOxOKLuX`ffKw?l5WUsS-d7`U4Sdr)Iz$0gnGYPhBsHLm;w^0E; zuO?8=DguNPe@Gy#iYz$z5}qfYO)A5=vtB?U2BWkZH70r5F*!(TVIA@EQZ0MlH6)9* z#xzJ1$8dFdgLMv9R+g^i*@>o9ef+4>FX2-sO-V_~kiOXNI{v1vj{eP?H?3Qm6LQ|F zM2X4|vfz+aryX73E@F6nq;45Shuy{H_i{eRerq;wSx)*bTkQI4wk2BLyBA|4ZyX%j z0I2Oa-mYhzfB*iLIiA$BNvA&4MDH`F*ZLssk-P-%f+I1IQ$%Dl;H;dTorufKkPkrL z?yMLtT;19EULG488$$Ie>j!XH4N?m7Jx`RftXdeQq@;ed=85fZtO?t#`4R_YD+eHL*oy=&$DUD%gL>Aj{bRz!{at0 z=X94n*r(e80rCn8l+!aaUka}|F$98Z^N^U=T^eCELr##Qjg1o$458G4JcY~39 zoSV!1du36WZTt3+=;&&_1NaLzbZKd2PWSKMr;vGiW~i(D7U2a-1u@J{-E{wgT<^dD z_ntkpv$M0U%;F?{Bok+nW!ZG9YkEkn{a{^F({ZG|kkHVCI-(SRRKXYPb^~DI$khpA z`{66S{YtD46KGY{%5QBto~n^Xw|O%KsF&%{7ABHE10v}H8wm!bVbjQOtM)?e(Yvf$ zwrl}!Ns0B?y^;M0x<7?Z5N|JYa&DEBl+?8h)+e9MyP2*$Gx(KCGhO%00kE~Mx)nQ5 zJ>(aw{q$)gxWfZiMwqXRwI-|Sm3kV7@=wi={Cu@4w0HXRlVcXNjCu?pE|~Wk5Elxs zUeC5_;W}`Dd0}C}!lt&NLA}e7ii(PmxpF1@dy7T`I&tBt1@!!p$aNCdt+#=stdBgo z^YF+udaU$XLQwnFny*h)&gKR3Du;KL`E1YvoVYUHPAe@febv=99uNcMXI529stS6o z*5AVCUn^RBAnAU=Iq~-P?!?XXt(2Fc6aTpEiAccT2B1i96*H}ov9?ZcmA*)Fx!_}c zRQcZmhKqPCUA6Wa@Lc&tMbfX~!uo+^aX4Om>Egvslo8*ksBO3>jj&@8Z*C!1@*X${GY_8ndhuXbJR(x*-oZMlwOcS9SSAfWu}hvVOQmYXKfu^RqfS?1^GFGX1G z9-TS&C}H(A=fS$QR^MNTL^Jg5FWd)i1gijP9wStCUNEyZd|8X25=T|GTJ zH0>v1cdAGIq%06>(>Zw--OeO}rM$G10ND}o>(rTE0LH7om*>u$JLi2`>em`jaO##N zXd5aMl_T~VloEv=%Pb%u%Mk0I(V>vdG%~Y=RgQ$BYj5-S?{Vnas3St8X7=pdsgP{N z0zsiFbk{l>nl-9FdD81{-cYX1(GskP(D(3Lzh&q9BL|*13(x60GlZlU7`>x@em?8O z;7r0|`m=((*GZvVbsHGvb2NxM93g2{vx!p)P6EU6K~iP0o-2M>!5(CP@8V*qrqBEA zdlmDN<_Q@Tzl>{Brc2XbPv7?UmqTAjQWBsnNPY;+NT;>VbCve?cCuE3YOR>jNd1`f z7Ny?+p+KBqkyO@C2nToIjjUwQRpxWTM&IQq_wtc*a*v)bEzfn#17C)wC%p!%8;^re zJpk-XiFhbQGeb7ty2WnWNaes%T0P5Q;8Z$=s`{->92&`E;CAD14su=RgmB#ACnV&F zT-&ZyU=FX}xUs4xMv@hUO)XU_8A&YOdA$97b#*zynnWyXsHkuV*~tDJ=zH?h;c?sQ zrv8J;ib%M2V0WoqrB4hf*=TAs26G-G+vy{&voxq6RI2vF3DbkuJUl$8n?{>6`5-|A zM@QdXoEvw@S>J)lhK^1DC9I355K{zX(Iwj=16ar$>CqNl+PaX-+>-UO0lnLsU7T4z5=H5al3;=e29sP+QdxG%A83&Kp*C)efDbN)-)C61G`1%7|ojjVtt= z5e$lr6WWDx8zsRuUli*l)n9YmgTK&Vy0xyWj=CsFIimDbaGOb z=4XcXn%D6N>SWV^e*J-T?~@~OE(g_&3#Z=2#iehykT_wj{T|{Kmg}5_?$3#d(l7DH z^N!nUCd3Jz7zq1#0&H+9I>4)M43 z$ZOY)r~ee9#5whUa_m+&Rx$FR!_Mr>&2bu5cSw$}sfnq$xLB*$?Vv&sdjP7ZF?L-v zKsn3(R8>z;&rSRVH$WCZ2eoUKVkMWFlc2!hBJsYMNn2Z6!0PyyNezkFhp%rLU5gEQ zm9kLqbf;9M`XuN$vh<3Gi2V4TwHrr7XE4_(rBJvb>P=n!uZ`-OCh!|wSubUC2 zU^vtlG)IsSIY1nObCJDxF`;faFF!vHO(juEo2$cOe6q_i8F&rIcR2ZZcXzjaWemR+ z-k;j_!1g0o>A>OBQTw1w%Otk!QbB|QJ+4+2{dCXKUyzTFZ+fgvC(-KH{JiaN{e{_( z@6%t7bCB%CkERzKjKUXmQy0T$SIAukXIQnoo|bm4sy*l9s*-%=%J_U_ip1*nd?)3u zJHs& z73hj0bYE_kMZ=xwcBG9li4%j_)%h+n0&_(=`L{lh)QF^TqD{+PKfhgbkz138##y9j ze>@5eU8AqBFL(TSymExl0p~F`G)@9~rx)1U3a_2*mzb|h*d+ffIpZxUFhZ}$8*F-^ zKTw?0;Xc=9(H!CDw;l@X#?BHd+0ZQG%8djs?fFEGSDf#d%Mt$sfh3<6o(~CN9jQ0I z#OgHWJ6UGi^(f?3nkw_qUqJ&fmhl17kZ4e_&~mQn6V>ugqq3FCRXxf&%^K+vul`6HhwUOoCYyKT} z?UCNzx1-KKK0gtZlzbG;#K>4NGCI1Akg#kN%ySB}`@P~$=@k_j`4Y)TgcwPEQLL6b z0-@yFCh4UebGQ4^S4nuTxKVoH(5TNpn|PHla5Aj2S($@R(xLxkVpE>mFVWSd{{H^J zu^g44UB*x3aJ!##8a%FD`R<(vdNfL}?UF99>#V@9JzI(a0V4{MkkSifZsJz-FL}X;N{`jD=aKb=>-cb&Jcl=qz@2sVw z%EV1Y4W2o3=DA9=1Pu`_VyuugqO6T-z#z3y^jbYtLxiZQsPOgi^|k+cR!vO{G6gdM zdPsO``+H_%l5-;84I-!L*Qu$gX6#86A_$Du^7Pp}_l@8jC=4Kolh-FYE{{^{aOH|A zp@lyRlj^VA=I`fs#ziujVs+`# z($dA1?Dro&7;az^>J9we`{BMogYa^opm0##I1^$GBDu-z_HElfoX@`efp`fKTs{8< zKyB!vYteHcY>~edSa-DIt#g@Lf~Hu!*2(Zs$IQXdwswe{l-R7}{2s&-v5xb+VRiVP zRt`x*>n0^FdBvnutX@WXXTG4-ZA8ay-A+=6V~2r= z^Kx@-t5G`r!JY=>6%+s_W!kgup{Az(hEDMP#}}F}*oW&9H;^Aq28lm{F8RroRRpkB zezd7eSU-%UoB8_nMRXgNQAqn%-sqn@_ZIP3LF`2Wfg(Uk#bOc?rj3n_m(f>*NeHEB zXZl7xcpwTXhm_C9f^-QuG^_=8?^aHnOW%jI8#R|vSh2x;^}T7yQM|2XN3o=HG4$$Q zmQ}msX##U7pPi|AbYR-nb4Pqdm!v{Q0PpP8OU&+{=dDT!~m zH7~Pf?gEyy8EQ~(qmz?UU-5=~h-F>B&iI6dG0Yvj4E%*c#%c*M!ud%%Nd$$4p3_T` zd4g`{I#92@be%69I>4wws*P;HIyt$ykpXN6?;ZApKjOK=z^7LEoUANy;N}OdE_LII z$BrFq1n=J8*Y^&>e^f?>NKP8KbuS>h_i2km4ZFuUWD-g?9XWF3q6gDHQpk zN0y?VbB78tHf01q2dLjDI6#~WmBR&h&EYbFtH9`fLW}nir6Cf0`PB6EMdYh= z+usKd9=wcX{2W3XdG1N79!wxfI!j#o#0IYDm%Z>T@OUk>-EuGyRmgXd5U5ZLjQ~6@ zk#-x`<%O))Lx5T&zwb0u@3)PWH51g12>RSBeN|y6ge^J>ua!e}d;9R=Ls0S;frbr$ zzb^u7=$0Rs4^T#?Hw|K!Gy|DBwNbL+a_b3{`?-PMdgCS@H;M(r1Yv6H@Wg zPvyo`P2v6@Ki>NX1VoX`;OKbqm@kbXD3v~sIYsHORZGvh(c)bG^zh(whlC z@M+z_XBIv5#_EfDXCGY|{hnCAsGXgmu%MSvbNX7x@_y@9gDpEx8UW6at4O{-B>IEXdmJI9 z4>ATD8^enh#|lV|M96u2hE@&Be(M^NZhs*t2hwTgQ2bOF|-?)jztJzM8Zh zz*C1_Dersb+Z|4|P;Vw$Y)Uvmz>F8o#XPNsaWIW!Mo7(=?2X~m+NT$&h7xp(RKlR5 zkevNKy^Wl5tIcF=NMqr%k%IAJl9GjZdS=jI0k*`~n&>Mu(slh1poR#}2ST^#;y5WE z2zil~MW-N~Iyj#0VoAH;YY;tshR2&O?l3(){Y2L-j0nKi0-LDVERC|M()G1-Se_WPIFamQNIu4m#LKK0ntM zgG2GZ%$I7zG18uaxXmu?oj}_00OjApXj#xMJrIhof?00^u~$N((U#YH>6X3H{B3;r zivInX{8~}RKH0iJ@sWit3RoHH?rz-*G#*g$NedUdGEk&uszmYoj~~~P_{%((UUubJ z9*>(GK8{8G{h9naVNc%l?5u`m(V>Ri`V8aaq_>QEU^|Gb&6_t*OiiWsrOYR$Q$?Ba zRq=Llw1RF|N35`aXD!73|2m%pFt10{5 zlRv^c2p=~5_y77#ejVf}8F7B{buK6}PVkgK(7rcs-h5j9-TBvN@@tMk7op*EwTVARzGbW?_QDB5s~L9h0Ca#d% ztQ5si^|?aULD8Wg6nW_3)`HQ-uEjqvP(e+Ne(sQz)P<+Ms0N%nclsi+&sgF&#+*Q0 z<7I&92?7;^&QI=rZ7r3mXiBjnk1W_jLPn5r_jAsJ2culKLp{b4ysxK&mQ-{~Nhu^5 zoU`6j-;t5aKH?WIU0O}txqFwqm6(`MR@?#t)Uar^4nvtNbo+JBbs1e~RQOPD??pLz z`40`My1Fz3TI(B%Jl!z@!LhNM;4Xn+N$`#*;;nI6IkX{)JZFzJ`xjrGluf6U-^OtF z9Z^+l*3436`g(dm?uiol( zABKK3yB52}N$mWBgPKHqipQy{>c3xPWFR5V|AaK3iI~9hL&d2x`M$5V1q1}7sVOup zKjfd%v&Jj$-o5*RM})F8-?U!3PDfwe$)7U?x@Qyy3v?Gts?(l74-N@=(=u+qYe7i( z$?lFmG7{RAz4SZ!)TZ?=$VL=bg8*~>Se`R(FEdk8t*$=68i91Z)=%z+(*2!BQOdve znLU)_>XM4|o>*(_znCb$P9M966TMNCY=0WsNRJFGBZH{A< z>Q~!AM>=OTG&EjjW%3v2fU z=NvhF_^?WZ%x$*+OcBY}=H}+;4X&&5lSj?~oVYznDIN3|feWS+ye@ut6jq!eTow0X zN~W<;VqzjmLB7n$cmwf(EEa!T)q_tcQW;qThQrA4%+N2Gg@B z??bLStu`gglwQ_;yQ{&Ff90j)`fnLHh1Hm5)%C|GB*UNOZ0X{b8cbp0w86UJJ ze}4{nl~=Qezv64}Cg-2M%CL&z zy<|K$qs5jtM}N)7$T~myiB$WPnSW5>qpl%?Gha>Uw;wYW+MLE2e%o%M`6y8usbTz> z?{NY9`oA|W-}v|s2RG*-eSVfF`(LQ~7wzxP7*{w_JZ*E+Tr#@B#ogO%w}oyU*Fevw zp2g^n)x^-Yrt%#FM_JNu2h)Q_BD|oz{?|$PcirM)jF?ssXgPb4d~w&IjuTCnh#WTY zy&*$t7lfSFI$3?8H}Yd{VYlwNVB`9dmVW0K19|TyK51zVaBdHTZqjZ0vp)EXU zj*o9ba|RO^>GuI$oZcz{Ve2k{Vi{%xa2iRkd_losSZ8m>#c}2JJ$m%03xKMo=48#I z!%U!0_wL;*qpC`mQ$xCRky_s16yw`k`Hs6tBmldRl>5SaE3w0eSs??7gAJuZqXk1~ zd2Q_`P!DH^kHDLBB> zl1p~wD*(+tONHM8a`QfafRp#oQ79>2?e7{XW>1~a$i1_da80jN*U_t@rlEO@VGGDU zdyU?%b6XfpV%}#)2a?Ygp5-W$SZU9V@H|1Z?130o0m0Ai*H4q_p@t1$Wka<|1HwBH zw_Rgnfl*?YjE3ry4g3w8Uzxy{^}Osns&VJ=@Ld37L)?6%*rko2lL=zC#`98`UW3EK zeZgM!Kn|!R1z}K~J}oCspT3oovwvl-tTLzVTo4DwrFYuv#l5S?IzSkmR35VqW>{X@-lS9$eo ze6+jxZ8M9j-`~izYyRRomj>W{;-%WHcXZ}&ab^cC@Km2GIcFDh2AIjE#>s<4pz zOWreUt_gg@5sxKiz!<2n5m6CW3b5OmOCG^PQIQ4Yhs9Rv*+(`=`Z_BS&zQK4^SlXTfiilT-1^C##lfu<-xmsd7K zJ$gh7qD)RfAr6X&Xwh}jPdd_3!a+*H5L7BuPAe%L1rateJF4@xva%e=V$gwsfx!nQ zultd6`x|VNNxv0U3~EWf`_ZoMkK>bjTZ)m?24Nv|QXSM3v|D20ZwgYj;L*%z)T%DrgY-DVmz?OOnH z-hnk;k7$X)TVZ!9@9`>6AOP$w`A>t*!HiD;3Kusoue@nHYTAyi)G*sJM-5`Ug1S=j zo`fKUS4c>RZaF`Hl@U|6ksw#&zSt_0Q?&=rAc6?tl z9Mk`92>#U!s72TK*Sl7QYn27-CwK&H-tt6hdgffgo^%1|4m3C)YbYs=!0xSEx6TU= zxTWP~Qc)kfa)4-^KW?IwRB~;~H(J7?o}V-kfYlp80K-8+K-gZlVZ-jZZw?L))sHk|M2ss8FlO|vz*)GzHg?nFgeH2` zx`}h&aYkTQy+LZRXs_SFY`+2Yp+CDM3%Iu)mbjH)103@U3#$mqjcm7ZJm4>4gHVGE zUk<{DA?C%MA46xm6A%I$Q=d28tIwut+56diM?jI>K7}yXZ|jry{8RhXqU*O;^5;Be z)+;ISIwEczBqml@nI^nYUh`~LC}8a>il--|ZtObyl!Tm5o;*1KV=IKWbwKoDfok^l zuga_xlXSGTDfkBuI$}Fu3GRYECrY~=%ta|~q$*8*GeU;o?cqgRva2;Gu8(QYd2cT- z!l&h21gcTH`THH3-Ss2VrD@Lier6bw61# zW=FJ%Qs)l$TTo)XeSNEDKIJ3CnjDlF@>krI(rBHuOp3x2%tmzDAxDyQ_(j8#+eHs%B3-#mP z?1RMZK%qRL>AAUx;iKGo8suOaLHHB}llziqj&1Xi+3_rc>Y|b>|7cqQ6Sn;}T1u2X z0XDWmK#2Lyo11JQ%DhIoW)jlvv<|r&80d?JMYmk-!Uc9D(Ikxk6Nh9=@(`!wrLMe*{{u2u|gJg9kY! zBp78rl(s?J%e^+Wv#_ubjqY92kOeyzIrx^1g1sI#O}4*v%88#pWl{1@Jv>0+>FEi- zj#eUz4lG=_aW)uxBtaRIU;zfKP8jWNot-P;Tg~%jNrw9Nmt7tR4ePra#yFrcZca`L z(u;xK*T{$q_DXfBOBk0RM>;%y^)ZoymtZn~G^9q~pt}YM;Q-MZO41;AVRI8=zz3aAel4UwjgL;{K@n1xTk1pe|3>4*Icm@7=6P-OoVx|o*Zs|t}v8Z1JC;xxc5oj zR44oL8+aKEt*ry$_(s*%H*E&v$tfsEgJM4q%z&x7^(2--HX!Fsavs9Uv9d5MOwL9? zFeITQ_F^*~ofnE=iuwR~La|bDIKkhttyoFf9%q1@5g=ArFDX@3R8W#mp~iH*4d97J zT5>kQnF`HiKj~ISbm{beuui!*drm+=pbVyzUQvj%zK|Oz;1wkuL+B1{=O+xY@uV+E zbifcpI0xM3_hL@VmmK88H%a;pppAEbMd+)l>cn)qy?P!R7k0r=LZ}+g-PTAF6S?%V zcZfc#aXDiy2pFOM(4c$w9rpMWx1XY8`&YeGa&iTSN|d&>>5W<`+DHntI4BHh+e^&q z29<}n3J2?01lx#lyCadT-+A@0c|zqq0Ss)QwZDP!3NC(rw4?7y^9UskvlmQ6-B6r9 zJ02`Nqb($^(p(#pZ^iAXqq{xc`UUSU7kjoZ-WVVwWFUBn>#pbg{p639FJ*+Qz2VKO z4jSk3f$`TBnP}@6ZyTB>Y{G#F*x0VuM0%4zf{Nz2nwXjz0UyKuFwXtE%e6;MCH}O= zc86VE^xc^~ob{=NuJ#!T%rv~)zfM1}d#SR=Xi1*NmPPezB7K>8t5#8xGo9M~ zKUurbqO1GGL}ikm&RFEKjAkJRVEUyL#A5)9I2rRKM221bF!(Fl`eptlc+bS;9 zAhP-)r9tVMJ{I4Yr_%2h`8udp-wX?3SaBH_)?zuYQOw2JaPra7#dXQ%|5PWib{?Dc zb)Z270)+MeRx1LqBj7Lwc@yNXG0vTZ%^VN|rYDakR+FYenA*>{N@uAwIVc4vpMMJY zA@jj(A?@;cL&wxFd+YX>`Ok`hKQKEvs7qhQ^yhok8Q#pgHUR?cl9|iZi)4b3_9*Fd>;}(&ndf$-nECj`WTr;%qk5?FO#% zS7^^2cT~TIKmU&?c=qK!$gF{TED^7fl4t?Nd!KGtvr2F*Khm}6_Xm%+m)G^)wri9AWLi87ZZlQ zdvq#)27lem-K%A%^JA3TB<;6neKzK#nLbfrC(Yr^?tFjTBA4sG*!6l7^0q*(ZQj0J zZ_Mqu@^ODl$w1fTo>2}?!+YQP1Ae@$vj1<3$-u-EpPQ@o)r5}g*x&I}0c_RZ-_IZ< z7QalRwJq$3h6LB1RY@_iDFJo2H=f-Y;axBBPY=<>eO-pQF7tR$bX~}k;kwfvT%6jmfv+Hm5Aexp`v*_3E6)V+@OW3Pt6yL><~KV zVC-AUB%FF-YqAI;`wjHAQs%FYyBO|OCkDc|cgO-!ZJX^#N$?Et8lUcE|H$~4=7D7x zqGQq+y+x$APDUr~HH4W>6MGEp5zG92apj>Oc{#KGV~?6Dv%iR= z3$7^ny^=a8Dv&swm(bGv+Fw9#(q;c(fIj8Ys&U`;L+&>G$=CAR5B8bI)xQEzZDVz6`n8E7`C3Zvz2NV%e^&h$Fut%#K>_%J z=OK^}SO5t^rWGXd&1`Hm1Z-pgSL6c#NGmeRHOYTqi(n^si&^-->}zvldL*G@ZEgMX z)vFtDKA?POn0{n|Mf}5_@sSZ49B;rPBiQ-in)L?6hs`I~gsjbED@8skz|rw7Ok|Vj zPVx8#l)B^A(U_;0Ul@Mz1tp1er+@#>0z*bFWbE$)DJa8z7c$u~c1L>n0gEsiN`vkN zSrMdrwICB_;K1HIe!K(Y&>L};x1jG{wz3kj>y`tvzpJ%apz?R;o7FY3()xo_FRVxh z?!TVSp{BfQ^=c8<*~|Mbf1@Mk=FkI^&=*tVWcv+|es8!t=1wr|)`@X8a^MjbxC4$q z=t$o_%tqvhSnDgY@jexE#1AF73;L!!K&Gs$EJ=6j?U3Q5{N%83XFj4J=X9fTt_&k3u_}AbeI#D>{ zF#0-SaX1LYiyW6GM+?cBcWj6(>{sD}+BZ<*yYWu;q=32#4zn_NAW287E=IKxb_$M; zuXR^Prc}Q8^s8y-s){+zCQHA6NRk8#+{s$Pto~U@)o}5yt2gz_J$)`6J3VbtmzuB4 zedxIW4%Gi%xjp*x8)c53N^CLU&+}&J{Oy{#)l@T$|Ja6x@cp7xjJvE2R25(>nS>z3 z!^49?ybGomKu=@4$exsmiHRs3i1iFax9w2so0E@FN7+X z5&pgyMd=1j2Z-0zfAH{OZb(mvngcG={3lPYxVp^4<@g!yhka(B36MfPLp7g1`9?%A z0X>n?4|}=ou-kmsEzJ8$do0grVwyZ4I_BQJ8+iIgCk%vOgl~a_JTajBNS0Aiah)?err;Ll#xdzar(5E&9h8X$ zhv&(YG$4Lx0K*bLd0M4TWNO~|3za+!-^I@_bM)x-s$t1f?*4C%Q}xWf@7Qe1N66EU zj%OdY2+GHT@BAl&DWwU#-(}99(tBJra`jQeWRuhre(qIE{9g!-ZRic2sPgY`Yi!i$ z&w?3*)a;|ou`s0fk6qG*$H7$>tW%#g(^k-{An^#2PNTNe#;Ed15FTs?YzaI?1T4ut zb0YKQ%Ti5!cix-;u=M2UJKWJ>=31)=5VoYbA2NeD5a$sp>uaw#HQKxVP$H@P-0PFwaR^B0vboKZ9f}DB+I+1*y2*!2#!vBL` zffV-^Oe;AZPRPIu4;SeTJWXXaj1)&toeDf@!(o2moeBi#3=zU{wr(Wrkf>zU9v zzn1P4vMWc;mNziifyusG=;qNKVP=PeRpN56d3{)*=ecml1sw8RbJG6|(x+=^=nnV} zabuGw-;Vf`OOryHfmmcNmFp>)w6tM zoE^DSUGjubVaCdm&B>4IJ8Mz9>X zIQ0H(yB6Y>b2?L+W`x_Xz1vkluR(9u}3fjvgA9#Lih(sB#tMtPM zrLJOzJtbfPQ4{!EO`Tr7e(f!@L&n0wBEX76b!9$rz!TAUzdBFq8uOhPM^F4d>}>U! zVfn%AYGSL>otKki6&5M}KVGHu-wS>q^G8*`h#n7a3t!HzExKpjC z-Z^LKkc0}skwQAn3%W@@~Comd^Mgh`08KojD%0DQf>!uT)X=EFrCat&W2-3 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%0DM8@)^74_XO<|N^3Sn#o#m2xhv zWc9?8uE$;*zM7E+5|V^^bEWA0Fj(CBdz=4B>^o}GL+XPtGO3^eY){ZVX?2b(3J}MY zk*6SX1mGzHdUgIznGShU&{N#R-66CNoScSv$F$%usk5fG1WU1C&UR#}R#(B(s_>w< z^Ixx?ycCw)_e6s0)4#q>y>pxexGIzOkFOyPM=-NeOqERdfyc7@x%-ZCb1E48G+NT! z@1i9}Wo+Q(HAwp|1w#YH&;5iE+ne5n*(=6rA3qvBx;yU)GR}k@gIY z*cD**qP6u*M=ka`!qYyf`o9jz(+8AzUW2^Y` z(3%#X@F;jTl9Jqv&(b#F;j!Yt`hyR3XT2!b!lO1O7d>G{?Mu0aWCk477T&qAOD#7+ zYty5J(1H$}3B3HopOXgj|9NAghry@7hD+IumSC%oL@M!dL2r54_1IRk@C6id09r2d zKlF^0Yi$GA54Uo2dpX=g%nD%PT;^!qn|gYBxE38a({MP>(!o1cQL%u6jeuOCZQE|P zx*#<);lYLDXA5k%sN9V5a4XZHbQ@$`pw%O0tf-$BEu6J&xibHUeM@2#(!kRly~o-; z0_TXC>-ce(66VV3aQ~pXC;a$xORc0hQvhsT`ieiar2cE;inw>r6W{F*8<*oWw+h=s zn7Mvj&gJuSoaI*WKlOvVdG6~7{tH;@TiJhDx1zWmgWSt>LQZ5M3Im|5RT`7-E^h9g z_xVrKuKQN|Z@NfK>Q!OZtTDYoS2Qn0_P>$86xpbGbIit$U*fDv%#yx=(Bva6p^55Z zrbPnu_m=kY$u!E>*=c)J6r_I1*HE&Fg^}KO1ud-wghJMX@943&e@1Q(p#rdxNK#HMDi4ApinzISRp^EAAs?mQW?AcMpR2q@v)Y(`r=U zm-?~iCmb~Mc%B`$2u=VIxdxW|NfrwJ@eo1^;pTd(OhA2 zbJCi9*x@E&`q6>g)$CX74ybVfzGR+svy~dQ5F~}rP)JaEcMs^GEx@V(i^L=U#Wbj# z6RW?#Oxwip{c6_C**gv_pqX`Cb8hh8+5exHjcWb0?57hHyDm8k5U&bT;yEN&l z=uJm%iQ*&t!+$$9r^u}!3v*R{N0>8ROXzgKkUI}pK}2fhdvs2Ai&wVKearbriGZ#>0ZcaBsG z)}!&6!6_*zvC{Gtt)H?PuR0#k>M2-6@h}uzXm-hrIn_-uJ1cLaKFd08P^;{4X1urh`r#KF7EdJt z7lCxt=3XvWwl%kn&(J2p4o`0(#}!r&1SubZ_-ouLzTe^YCkScd>>SCu;!OP9)6Jy} zHRn2dRjBBdxZ#b52-T&qP#6q;UHqwSWaI%98rWzh>Hk03$4WgTl|zw;4>pK64v537 zu5Jx1*UOeKUq}JbsDnrd(6emtd1;%PdLkuX8_xn9%bwL)@F+h969&BFX?67q0J~(P zLbvl^LJ;z-wFq>%>|kaDX-^phIobvWOHn#zTxeN)_R2W@$jHd~KpJXlbm(NB4q37* z7Ew-E6D?c8YRa{`5nYAkF5I?$U7qdpUA9iY|6;3 zgI74yyEv6-Dz~k5g)@v#(?#ln%!R#$v(`i7KZFKO?c#o;EgqfMSf;Ud@Wd~SgyK&Q zKxZg34|ryy3H$%g5KZaK$YNOfHUkSpk23|*`Za8Mjpo#jEh+V%=5*y1ypqV>se5*lk5+~((s%sSwu=^ zJQ83TSt~V9aPBQ()UB?na!1W#l;^>L_-K8I8bZ0nkiqJ9r4|AA8F&PZuWYtMOy7lS1oSYb15;GMLV%2-1#fYrVU>yDO@NK%G-Ph0SiWiY_Wbhv^Bf$sg`}inJ+j!8C@zd>w0mxM?gu z@_rNeJ2zR^qa?+IqKcABfD1kZM14HM!ED|uWeo4z3JT4#WglZTiyT9Qyq#vg-L4)X zX&aQcK)`a0z=~J5wS_~hLbAp!8r4x2QUJM$EIH~1Yi+t5zrMV zsh&nliE&T6f~v43E!;ID2y(tV=$*o1Hbx+xe$VUX+k<0$5*k^_z5$HG6I`lCuHU$_ zUMc6IvZBV&Aw4dw0!A>K1d>3W6$(rWLi!;SB1j#Y$c{i8b94C|+P<-RDid!&Is_Dk zZ3U4-R46zB;1fwLn%!OU<^@FD?kTPSWD$0aCCGKTjtlb%5>kxKIdOszCBd`vh0lLV zy?|puM7nD-7z9425SNtC&{z1CP8$h}(_XH3*=mun0=>zCYbBe)a7bhNfnV4hCVpsx zW+N)ykI!z_L0g(?0G~+`UBqQm!hDv4jcH(0l)U|eW!S4qXiBK(;%Az%H9pKo@Q?Sw zod1k5`7^?#ZqfDElpB_24Zdfxs!g>SW~aA&qd6i;UIeIN4dwy#B(N^Gxf88BPS0J1hjLyk4H1oPa6+K^eo{-q`+h0uhA z9Ksc8S?Im^_4?1cW^uWO7vATL&@cOHB!)Hf3z{MGD2!iv@J#I(*Y4N0niQ?8c@iBn z4NhC3LckV4X~MPEG|%5DqcvyiTM<2=tj(|J@( zY=M8<{-qRHf+!RSwR0_kqN@_6<>Xc&Y3mp&bR=;$mKNAU0FTd^^_kC~q>teh|d1_+BWS+~C zu^j~sFALTvPin}}BHH%nRuiB$_5Il+ajV=u7p~CKl3F<``ikY$)ILl!)Rdowxgu0( zxqWy*EwislzSsaOTW9%NY;+4L8f2113vy4B5p&qhylv}Nx`|LwAv;tqVc9F(7# zhwSP>c0rq7MxbkeWj|%<=-_51xx*TqwbBHQN zR6R02mVv{%p_Q-#v0$r7WId?=JnOz2MOUUj0brSeaOZW}S<#Rs9Hw#KE(nNL09bZ^CInhR_fk4nB<|I0#8FAXi*knm6S4G$?A0SX=XvX>LHp zAp<38eH_i7dwXw#%-jr~jf@l{UN)4~IB%E0I#1^8NcnqUc?u6|e(TTLHH*WL* zM@yQ20<^*(KriAWtBI@e0$+G9W=;{{8js9#!b*jPx;fLVC&LVt-p_%GCka%G7BAj{ zork#OvF!lL?pm^R6#}F{ydwmMaMeu2yc-vJm19!NSZG1gm(vN`5aPIig5t8-oGm)z zR`R0c{QQ;cJNM`)-&PB5u#oNM{FC;y?fVJrrg+kJR~)<&I#Bq=w)DsH7wVtF&`ajz zV#d8Rp}& z06U_YybT43NG||XVqa>K==$SWeS~?2_dBE#k$f-wvEk)XfV0EOGYny0FP2{xE=qC~ zFX4QLew5|!!uOUp#|A4&srzZG!d8hUd-_W7zbm>TL>>$14t3tdpF&_xyS_W|DkGkP z@qqbelxda1WqTn017Q!lxJSu2k=B4i=0a>g)#3a(is~D^HK~yWE2;fH9vh>EPif`g zmv=^ayQ3DGRdUNqV?-W98p;jM~bfTaA+)wGTVS&*t1R`r#VbF;f8yM)^ zHX3TkX&(5Re8MM3;wJqXB|{T#$@b&Fbbi}?BsTj<;OYGzEgGr`pNNRmK3?PJx3bo~ zex2O4#{XN(x2evAeocB;eM2j}!aoFm1M9#2)Me#eIenkOxG6aV(9PcCKaQ(@d`yXS zzPg06%#rcVIWCuH^{*951AHp~`wB^Nom*#OA<%+|_BiVHB(Yi48^l*7Qyr$QG^zAx zH9+|7QHxA#*%HEjy@x0(gxhWpd*N2t`iGFL_|DFm=$}F| zuFlV&p6hVTE|ElV@^$*!|BXj@q>j3J&rGj^}wD+n1zIx#mAZH?!$m zz6Pfli1a{&qP&S!PI@vyVY^@;hVIBbi^o5z_ zhifVB`MZ0xzPi12?rJ+vt9*>EE>pud@A@xPqQnN~AmXw>pFu{?l0u^hbKx)NfmncpWn^YH&I@NI8gmGf zh_Wk`JLY%|9Ct*f|L~!1ibz=jfLtI0Pm$K+3D1Y&%&2^Jn1gFLP5=-W#(5sL{j&$7em0QXg9);V@AE?Sf&&w(^$6gB(a zyT?MN7jN74P`$Prfnnb;wi&}k6O<>DmJ#nPvP5u$u%)Q?s@KwQQ}>`ycz7U0`8X$j z(=Z~n$9H(cFRSmAW)YX{K@mCN1fUReUNqy!sVI=gVlyP}8r0XKpIfu{pmio=m7%f< zbGXq7wFDW*442J;0|&JG>SMqxa9=J4EF%W$_?)rvauCS`mO-yzSv813tyq&c0p6k1 zJg2Kmh0XwRnrbnfhmflQ9}lT(RdRktj{y=PiBs6rRddWAR8O7kZ=%Q1j}EjH9va|h zaKEh~RNu#s+PQjA7LwzX*aQ&~0}8$b3=~}it2mu!E**?(5Q8A5jsWd&5gsNUMVROT zVRxcPSe_jKQyIzx6aWlluraPI2|+?5%Bi7cf0bV_HIS~IAx;k^M;tZ}eKZiHtLmcf z?YLBLdTaZw`xhIELb=@!XXF04|9$`Xwm>>VA)po#JuDg?Mn%cX%TsV+ak0!Bz$INr zs?$@rHz5#c8v{6mVH0Az=t>$J1HvNw{BA325_O>QF z3@?KU5SGk*4wf)$p3kU?kVZeuOo2T@FEAg`575*vB|{1^GZbO)yr`9Ykn6ui%tadk zNEmY5-_!jE_GlHriZwFAaXADO;iI|HP{zF+Y;(DUg%h`-C@dzP3Oc>MGs_5>`C8O<4FIaWj_4 z6eFj<`XCALZnXPO65#=_b;X_qCYRKKo%d&|D2Y49*uP*bO>}(>0(&?)-7yQAjICBy zRedS*A=6|%)l;-v1UJm-1C*5vgk}XL|LSaB^&DcwxK$d!6{te-stWKRK%@x#YdTm1 z&hSaNLNNRg(ZXb$SQxjD#9QDN7_bID`;sQo&dEPv0$NpyUuY=D=sdJs$#^Pv@87Si zp9_c#^K#)O=51K;2+tGq>}c$7761;-M_BJ!MqsIKgETGwOXh=I!L?#Q@zjnrwM zA3Y5IvR@5H@O<1r?)I5~WPJEYa1<;)xd8~6O00kvCJw==1T8J`)*{;2ECMmR1jT}# zq5x5xW@mud-GqO4QvDbA;~9N5$U}3-;pDpmJqe+pg^M^8AUQFstcoBj^ZBY zHbX;8@=@@ey3IxvGP!|t4Ifzq7^!U@VCV)!NkI{tdD2^eLgr>7#9PsRZ5Wufsu)N+ zpgY~V&lDvx<$Ucd`eKqZLz)AK(EQZiBVLVkG)UK?d%tO?aIM?>>37Gn%Zx&vw6oiI zKEA$J`O?{hK3)kX1Ob%D{avF*QPb#WIpN75dkK%PNlDxl48r`38*o;V*y~32Nu6} z?cUwBnU7v4NV(mSMK7y2M?;J27>-auoangCFL!w zGT~|p*u+0yNwzLGsL2=HJ%Ru#O(!ARY;!```T3rfw{P*ow2FQN@xjJ)_BU~kd=CmN zJduL_z}y~SWEXSPwUMRKz=>6IR#8#iw47`QjOHeldP#4?(QCdxIKosE6{G93q(s{> z%WpZvS~Gtor@hlOu>_i$V6{E^yZ!y5ywNhhPE@ee)80J8{EapHpUp*yG}i550J9MD z-b4gvRrO!$!Z(+2EWpH`yToq<=S+~0CW2CtSDccEYMkttzYDp#Mdqpw3|P^MB6!bt zfL5_JIupY{nUKe$WTe?=*EA->#^F-IsvYz*f25*UIlPW~({c{t!zpPS=9; zi_=2@J}InLlIx`Fb-S|^G$YELqL5FrVf&{bIyE9R0x9T z4H6(P(fOHm=p@OYBuq=Dn9FIAU2bHl@&a*PZuTUO%tKdx(7|Mjs!-YFL!&cE=l~Ib zSa>u{d+6L~EVc|Sqbrj}aRh&GVuJ2iLJ({x~ zwsee|{)nQXn>*cM(u^RU&{Ir(a^HiPySQZix*w}5Oyhyk*IXcTi7SwQfg>JO&(%#9{W)BB+D$li$A zbju*IL$m~hOPdIXhm6AR^2f_*X~psLxa+ACSCeQo_12~A2Q8|)L5+%nqS>N9cjWAy zKbL+K=RuqPTUeRT;4&d#5|Gq$;D0gc+q$n_JjN7<5Djt~%YcnlYW6_zRt5~(e6&Y~ z6eOgKfxXpYs7-Jv@PJLfIyN`*dcf$&&<&DNhrM$RBjX+H)m6I(@bCiu(nTF*J2SPH zz*-0)+=7GyVEyHQ8y?nMs1OAoxs7Q>KbB)7#L>K-#B7k+f~Y`C0GxL#lc=96BA41uS}jXmO?x-ygj%ppP zO3WE_fWK%OqVqugETbUG62ZRFaxGDtPR?L}TJbpq2BCaohW~_5Q`R$SP(vSr8Kj4y zASl6w;?&goBZBFTm1Gr9dDqaMMw_qM_N=c0k#Xo>@Uv&sr>Q@Jlja{5DF3RwbGQbi z>H>-x)2Co8ZrW>!9FW!SWts2t6SopcJdsfUj6+$WwWgo?aZZ)a&^QcHKE6Rw&)k0M zPW}d z@qcj%a~1G88}7|Dnf=j8WWK>#?XQ)Q2^TW58*yM})B^zH*SxW!KQ~$rDxzWoMyVA)zi6H8qXa)CG-|vGm6? zHeP=ebT0VoUe=oAFr&Qf_lR}y*W%<@C)XMg0uO-BGJ0Q-8!P=WQ%=aqtyWkxF0QPR z_uucue9@KU3n^rKM||GMxs4Oy(mzYiJFlQ60y&0=_06jd^EMu}zA+|w$sW!d-nDqD z^bjNt$8&5SZ$BNiA2OFyh+^QE57Jmewxqj$eh(-1WAyCywLdmQ4AVqfvCkGriaPOM z$8LENQZ3*~&?DZH-_2gmDmVA^PUp6}O2ILGmKn@w8h^Q6iUOQTzCzSw7A5XcW_d!# zsQ95eO~-0B-4zIpu?3QUZC(VgOA;ik&lZdnzJ7kbkvlhS-hHQrxt!Hb3&ef5UdYK_Ffg6mY^|nF0*8Ltw2g1TVVh;(g3Z4CQ_xYh>I) z$C@!mao9RZp&=L3dDm3_b@~68rGN-8b(Pn|M2HxHg$oz@8zaU9$O3^6 zNb)8!4gtM3w(uw1`s*}5evdbO_9RlNGPp%$9XuNL#_?_gJ#n>O|CL1DCl-U!6BezajHye9i1y#tK*d%89Ltw>qB9Olu&G1QP|bwU@&-CT#lWlKx{114b}#0305c!>-GxdGy})H|un8 zJA$3;M8+1`zuf)ujkTmY36lP0S^RY$Bnh<(JzoRc8?`Ge(>jT*iUqA-G-0x6v=;o| zen_pm^%EQ$JUIP9PzUB$qjtK9{_E&gA4QwX(Q6TXje6^iHIt1mL`_g=#Ggx8@^6w2 zXc*BLCbi|zL5&T#Wo)2%6>3(?(KO9`7jQBpt{!1LY5hYIYygjtvlT}I!2YuXVj}{! znml4^LE>+by9-?6CPtfK-OzwZTTCGt4ajG5Mm`ib5Cj8*-ozm<#VGt2SniV!NjlxY zO{uFZdKfF6(D2WL@8=;%hG{hN_S7To8n6`)EKG=r{e)L1sTCm00Z6%_y?KZ#pz$%M z0yS)^&=k_YN5Vm#(17{we#Y>W@HL0{7>^1+h^qt5&1o2S%x%Cj~l^*!m|{qL=w{27||fEOAZ zrce}+VZBjNQFpN}al8DO1C7;C=i+bpupt|8@RjY{6ltE8E%Meaxnwu3vSK#5LBB$l zf8DT5i$0WaWUnHnKQ0KWIFRPbz1$LP=nHQnBMak2Qh1`jug-XEL`8YI>*Z6gVHW1p z_#yJl{Gz2GzbDU-@|n(hK+-=OAXU}Pe0+4s6Fq$X{B|-x7W-EJJKBW9W=HmZ8Xfz# zohoO2@RlST{hml2e;-mPFP-%+Lkgvj%C4T+Fy{VFVej&( zm#s{QOl3}fG}Qm@KIe})R)_3oFJR$k*?-6VvG>zQH&Z7>PiEzRW!*~?wq$!)b;ZzZE;n}wP|VApE-tmi zq`jN1Eo&b;A3D7$l4Zt2?TUesX5gQui)9Zyme*>$dvuNpz!-w+y;0@g2F0hYH}_DFRyHn1U2ham@M?hgG)o$PjBB&skm3eZ%-o^fN_SPe*1oVfLZY zE37)aVSLH)KfVnQ->of{_xkU<*z}b2b_?b}V6^a&Ni(-6XRe5WZUf^~Uz5rHz|>f( zCQ$^)`EcI$52sr)%lC5Rc}}g_yvH2vw24N*UsvgzQ1_tc+c6`HfD~eAw$5AY)PAYe zR6R%~hG8(zR?L%aSMak8TNPD3f89yPHb>ix*@sj4)1%6@*`Lo8r!}$tIcwq8YN{o%F&fpe1ZJ^WO#`XS6 zA+M!JxTnlRT`|!ODthx8;Tq-^mUVe6{>rdB%HD0vJ^NvPmKEpccAyB|2m7GvK>CC> zl3Kg%iyu~1v!B&XkUzfo!=BWa0^Oa-X_tiGarDNwJ3QE55hL$;ULjrrABNmn%Q1~W zlbRT0$_Am88^yx0g4%V!UJkn{44K=|QxO#xlFaiC#X*X58^BKl*z-sXRq%!I!GGXD zV@O=sY3KJ}m7Yz~@PvBIT$l>Ucgou!=6hto^3Q_#=&~ArlghG|bOXD=%;u@DPV-el zUkmUgvXBTbXb^S)HwEMOBt2d74AlV$PbT=Qx9?=D1-0@TTBkF*!S&)d^t5ZR5qF~Urs0NH!tEzgg!zEZ^LS6~OU zLy#W=)VQFB(gZHE53K6I zsR5jCNJX{6Zc|l+!0OE$9F#X}@|xJ9d;C_b8)LTXJ)OU&W%sQc}y8+`LXE z03l{+>)oJ!Zkv^StNqI|+$L1-vcGpteKTB^aowY4S(-ia=|j zOH)a9K^;ng9EyBr3{koZs^OXTW?^BLYbdYqr7_GqP!}&MO)G!sA&IqD7L#P+XQpBH zi_yz4q!r(1Ki(T1?ZGe`4wOW-s2LZD?W!zfNDJVV-Sy2%f$yO>W*q799XuEce5mTu zx#KlGHw?J3%!7jK>P2>T*L-e_^owFDtgDqYV&YO4JKq&hHV*~2JEjg-8g}$X8y=?- z-Rb;2C(i}{3!t1culoDiigAsq^e0aM?$v)$*HR|`MSP$7nAO(i5jV}M2b;;es%uu! z>`Um+EQt1~%xs%W>vj%N99P$pNbO#{v0EJ=dFq$Ug1Z`3GzaQ44~dApf8uT2Y?w z4Ci2^D1`5XC6=XDWcq@0?P->ipIh$4Fm9MBUh#L0=jvO9UAq|-}Fgd zc4h%xwbqYY~@5SlZ$8sCzSJKl7L2})}~yxh%u?I*raKLzh}K}|g- zG@r}=^85jt_GzI3o0(w=xr4V~`hB{vW#1mZEuyPBER~-XGZfTy(w4Vr6sg($<$t`@ zRN1=WvAdHC6qk!0oou$EJMAFQG5(CJPP2FJ`;L8n#UEa@yfyhCvMaTwq0DdIdd6FC zI;r%mPefSiIq`lsZ}03-Zq8fFw+9{N_mzCG6}y8Ej;5D&Rz~YKBJCkf@eD zKlC`4StU9aEL4)i8)y0J|2_1QYD_J#>BNGNvbDEo*Z`qav2V1V)@! z(1X7Oi$)L`Y`z5}m7LvXyIW}X2pAl6o~p~Y<2ty1th6dcyKrI+%fPpuyUNPbF69P1 z7SBZLtwu6;HKvZmH{5LP|FGn6Dtl1vs+T42#rv%DjGcVwCrOQg0 zNk^qkGx=h|I)R=R?%F59mO|fq9K*+7b%iaT<6Q0^D7_^EXJ4Yv$>sFqAjUlx_ll@8 z9&G@6c%`p3JFQ7?*VmEm8#{vn=sx!j6}UdH)(9M=&h{8Q6#2A)bLD8`&3oqcLS{jk zQOwhPJ!NKl>bW1Wq)?qP(e$#ctc+Q$?xV51QDVm*FYPP+VC#umy-YQ$geNVfq0$uS zrztskd4W@Tb?19r^_HqR94a@SnKPrcosY;`DeNhy7-7<_u#8KB+u3-#-%)Y$0%ScF zc0DO5Tod+6qY|S;U|=Q^U>pX+<>f?hZDLRf-(a% z5rNGZrY(izPW7>lbBgRvDR8Doki#D`og*mpZSWR%MoOXm#t=2fS5QyB<_L zn4zBt!nN$Mw`4!2}lcSbNhV-E)^U(s$7!! z;e2nZK+sV|w*Q{<@4B*b<}slNy`1I8rwhPhOG@mTE49WEf@}qEF4>)x@}Q^&-V5A! z+29^)-TA&H|Bhr$mPhe$=P5^F&CktSb9kjP9PnVh+HtSUaKCRv(5o-o((MXg45$ai zBy6aNS*;}g-?hCzBsT*oAON3bwY9YqrcoGP146BEc6}DWMAx{uUYoCHKh*k(pU&&( z?9gG_7g~Mtto_~D_3Jnv9?Oqw=(Bw=KodBqWT0Xp^1Z-?-stObD_qyRzrC)&hix4k zf^y|BQ)G9K=3?k>VM5J)Fxf_@;kVc??^DO8^GsE*_U-*XeI?vWGSkL&Jl7{W|dG zwRcPB!H(o+wQ_~@(GZ@Cd)ByyWbLjycc`(Cy)C?r@|Gdj^PZ0of2xE~cTLRd0LkB6 zFC%VKFETFE%leKn0WgE|k)zq+V!q%V?-v;-F2@uf(@|MR-hqJ8b_?Y;UC4}g4cLIKMcMhhvTuA+V1UMxWmrKz_ zML~6gxS&Uv5Fg<%BnTN`8OoBr*AJ-R+JBB<90r?K4Um%8Z!jr)4NbGG5B@eSne@2H zrciCpd{sfarj%XKE=hf@9XFjeope+_M7cQb=Qud?{gCvs|4RL8*f@W`}v zKMwLLG*dOmpxYZ(1G>c>wJ|RS6_I%mdK;uCo}+f-4dJl%a4lpWJzuP^th+NxAtcLO zw8hR#a5T2ykmez~sM2+TgL_t9IdWXf1&gGVA(yVC={M(EI(^pc%nUJlU|%P?QN%&q zA{uJAUjz4?^@@y)Op#@)G>1s(Rb<+fUqu9y`MOn?E*kP@^&Z_S$1QrSsLn!^r+aTy z%d*gE{-vAu3i$*WC>v{>YD`z+c7F7mC+}eEk88+TXtQb7%o0h?kww$EU(ic7uJc}p zsl=e>zVcFuf=&-$FI6)x#tDf#s|ji%Pe?@kd0_P6^ya*aHi*pvj% zV2?b5&h%d!ZH!El#*G{7+V+Lx$KtUxNnj5m%R``khIAymEP=y0Ial#XxZ}3dMIUM> ztiCCCdbcANE5>lIpM9JUXuZ z&EIUwK2NpWW5+)4Jwh+v)3^p$$~ug-e*a)=G2nho@7TA#>v9$RJ;s^}F-g@L%TH?}**q_M#$Qk(M(_WXQ}>VBGi()n;B{U%j|MBMqOwfP+(4@kPc5TZ7R2 z-XuLPyjLTj_J|WYJUo?eGla=$V-26ah}+s8fYBtt-ph-2Hh5p^K5p5ARjTTjL{@RV zm9{;q9~05KDd%AB$jM%V-K`%s&bmCmFdgw-i9VO^({U6X|2Wtm_SWyHiD}kypao|c zVc*Zb>FU%uxUYiu>bi7~L&}S(=2AG3$g)^+PxiKKT>gX?{hQucbw}P}do8yOOwP!&B_(Psx7haIO(0=k>t_okNU>nVV7>-D zJHTBXyFoyk$i4~2VSEi$`MXcn`x60U^`m6O?ddy zW%h2(g&#k6KRh(mdy{gYFMP^qN{;WFQIv(!UlsaBB|C?0`%So&F4t>t6xYrjtTJ$7 zN-odipD?KFGRw(75HP~MV7R_1Xlk@1k}{U^A!yIEQ-X!~KYu=#OEw-8blu|P<2i(T zPtS&p%Ga(=dR(}Et({KGmJ!cbj+B@@yS#>m96|cs*7LP7PH*Y;B5P#0EZLuD{=1;U zb#GB#ng=Bt4a072)PB%0=<+eU4?b+@BXQa(vg}eIwoZ5VzUi|r;LqT94Y8ZK+EsbG zFYWZ>f{}*3A_dv^#6CEv2Oi|_=P7Em?b#GY^E7!jT~+Wro2>nG=(iYgZs#0^-;^NH zxz-rah+W_WB9-;p^>)&_R5rtUCG#q&d0@)c+CkyT$Xok~Cwh*J*QH6-R(U@#^otg_N$Z$WUW@_40`uX`vGK_qo@M! z)^#yL>w{i>78H`KtWNjHJvg9tD0f49*djyKkRMwWw!Phyd2jkmTbd2~|E#-XWZj13 z@|iU4_F@%%^>ceZaBgSl;>M5DlI_FRg_dFG&5B$TXeO<;a)_5!zR}ws_I>PW+=_Xd z17E*l!1A}98QhZpbDsFgL@;1>!&j0Vr)A&A2F`*2VZ#&LMY|r$#*v}!SqEZviDO+* zkoLu_qHzBT)!8DJ56%@IzHVzaT)3HQXXNdkue#&9!qn8m^*Ki~KU*KHw`!=^M_)bh z!~DZ-N2lh=#yLs%L>!PP4#)r5h>)TiHAZ3gBuFq2B!M}Rd_f#%F|g~t^8JgZn_e2| z3Jp&^xk>l_7E@$IX^x$P>g;E(MRxf?S13#O6_#4hOcM)lv-Q(-#!4oTpzO*FP<+AQ)6)@$aqk&z`7SLm~5A*B!-` zBRAW{Bu>_LcX}Fd>Bi~S2j`iyy6?b*vBb=fjqN$govL>FY^Rt`9g7x!G&5$oBO~Iz zjaqlCCAV|u?+#LiF>a2%dwpQ3tUQpB;{u@{>cdR#hl+AzCgso1FspeE<_AAtSGCf2 z@@weHg(z%oV@LM|4ho*=y&HZnS@Q^ev^}@8?r&bYCR7@sJcq{LoJM-oX+Sm*AY8CB zMLh|f%N@Y3Z~LvI=&&;%J8{B9^LPS|)Qc;P%7$0kq?8NX^XiOgJh3-~!!anfo$;;V z=0W33s?A&io_JqFjZ@zfBBVQl|GV28`SmEkgN7KZUUc>7r8Zjcc z&N^^!ml*UG31^Fon)Li7)0)546%O_}oaRdU7P;~`uhk&?g-0ubHD6;#d^;{)HL)tr z>EqGL#k8Cp#r=1sJ8XaZw)KFx4>(NhM*fEO{M;-?FTU%y!C{Bok}4#Po05YKe}tql zgS#`7Y)YG&U1e#nU{h+nbX{=Xec4ntd3bNVK+=vJ)Is6@riz+ zX^!U@Kh*f0eWrbu93utwv-Bxd>_Pqh(j5nW`_&ZSLQ7H4qnxV-K3JK1M76*2>sl`(nU3I~?__L!hs zz1qCyA{mPZZM+xhO<`C&X>R_gNw4khJCUK?5%r#wJt|ef6)WUY4?IsDrDj%+U8%CO z+2m{VqyAv&bMycG)O{D+Zvs2Gjm+5q#7+j}Tx*2xQ7zky1uDiTWqljf`+v|L6P};p zpLFNw%*;#I?+_lAZKc|d>E+Dlw@3GOQQZx@sPisrHrY03scHXrLyZUOBOZ3a zJW639Cx3!}n~&}NP3eYyhtxU`>m*Rqkp2U}ECmX2H@nfEI+zPl`PgA zmvf8nd3caY>RIkBvw#1cw)0)QSKn{a zIn8p}BID=@Zq|+jF1a;<%;h&aY3|hAY0Lv*$;>Qe7=LK7eEe@Z(ES6vNGB#4_B=Ri zkeXM+a=Go9@MPOh`!WQwsNC3_fQaMs?id6plM<@B1YPEJk`>}8_K;r#FIc@t8j zpWY-Me;+z_3dy|bluDKBk91o5{fm{rScHA#)f-6EP*D)inrsZTa?F_XXjBbaEy^I* z=XE>D@3@lD*9-M($LB?u{aO}1b=$&e$nskK?~YgTGKjf&2Pgks4W6&G61md@Q}@qi z#hx5$d~1BcNp)>Q(5B6`hV}6rgY8|H*KG^)&bUzfPJpIw4b7kR$7`wF2!?FKx+u#? zbKI})t7$n==7TW<_X?wbs7tfeVOtW(VUrBWopIb%&s)@8t*15fZ5OMznfF|OS;)qh zoh+MrYhR02RM(z&`f_#ui3_n#rE}^*N{JAPP`%FR^Zf9N{g~cw@VnelX}ETe#pREP z6}DW+ddDr9c+gLyjTSN-xx;oyY81Sl_pL`6nORAL*6uG4Hr*t*D&J zd&c^a(3bQQ*Ed~1X(Hf>SoW1qcTS5wkSi3)Igo3pp;mzg=7A0MQMzsaso|kOY+VGyt5pV9wKb9~a4Dkj z!YR_LqZt3YAq6Joj=dIJrDi&hOkv0zeb7?xgO^-q`r_M1*8yJdwdsE|qQov`lhPuc zDwFgzIVQ<2=iuJ24^rDE2f1ZmM5~pHsyi*gCE4`(y?kkZM#s>XR}J?fdSXoXEZ~`b zdBJ5ri<@_tcX_~H46OO~{UNVL;N#h+Umd)5gER#I@J{I1dm!w{EbDu&wA^4%KzT)T zcE4M~d65Y2-QjJXKdx?4?o6Av^vHY>g(~sPWr3owOVdC~!8b z)zU^@ecsa3oGa5uH40n;jKty^z8QO~e82QG+p%$;6I4o5Ui4Z(avfb=(S$qs@A`^) zb>U{J7(C3-@(E&3;AqjpRQ|Aa{HSQXODa%I=|a$Fh;%hkl4p)_-9ev&?h(pL@01s|?Kn^g#7_HYP>idp4jsFl^1@g2Xir8W|Y+hH)yBe?-Q) z5un|*beawcP3yc*PY0+BZ{mH<%P`usLP5anO;mwYUAhtFY}H6D6PtWf+(sUDtl;zZ^Q~&#o?CR}(flFr#!ZzVdHaEhVNZJK#& zRA-v?dtPQK>-tovc|>O4?yA=COss!ZvW|x7$EbXf@DHxy$a&2H%iSOvD{5>C=2?%L z{&$zns0fu7N1_TbltUp6Z%f8_2dB8rS*wKIvDGI_cF2^(Tw>|JI=01LrP-u7Av9!m z(oRJ6@;l>XU9q!)vV&f6N$JOxlnUwNREFP2Oj4GOtoSs!rns~3Q^MapcOsQcvj#^Z zf-?~=oc4|{@XNjT4Sp`8J*U3Vun-< zU}_tA>Ekwu7y0LETdEEf_b=PwH`}&cmBFBJU+rbP_fHBxPA&Yz>)!hyG2r)9jI1Y)wS(c-@;7fd;*&@EtPhGXHU`dd-AFEX5?Wg(WFq^nQT?=D zpJtkXaQ_%(GpDFK;LisZ;?0rLTM~W$+Z=N0)x`9_jKHO-h}jvr)wFa8b|ON&f#b^zigp1B&KO}s@1+G&08QsuVRP? z$e_Bw^oQz~J=+%W=;}Aksjb{+CH?2)daTj6$M+t@5Zm|e{T4din3K8Q#^6=Pg}C`l z*UwD*zQis7MNkq|(Qf$p;%sz=z*XT_tS27wCng#T*L|Zt6fR78yCFTuiF@{;8dr1J z?=M4AMlKx_lUhKWl?0e$Oy|>RE)o%2vTmk7DR9uARmJLiLoG+blf?C1gRefZw=YZX z&}Z5g)&Z{Zar^4Db9zeKUT?e0ZW^nawLVfKS&d7;7w_Zto6CKrCI(dg)#*V2)cP-= zGl9@KrMe*?>}NL^wDek4adv^GcTTP7&T|tRt1i8++OvkoA}U3?q1rW`uKa3xSVlN! zHtO+%j~&%ly%QL2Ont<0Hz2Ys_`?gH_-~s5@vn~Hj%dDmnr)XHw{!f3e?4#K_vG%r zcrm&@R2s0p4jOC%ZXtKHT7Mrl_szRc*6?5a5|gXANk2U#I9va2fn|(d^3ZM8kNYkJ zyC}6ZMZ8|LqxyriY*7VoPj6pI;)#n7)Z}RaBszYNCA=cpeEtLwEu?mTz84Yk3X#Em zuqppPe0>E}Re8HN4k8KyDuU7i0wO7?qzTf}4IJjfCT9ikBm4nA<_zY7m{ zcz}k9rQ2gs$^`%FQxbodE$+OagdBV4==ss>UTjo9eL}PhNhF$@b!#gGAJQ2btEkS% zf5=sc2h?NWU+39QAlD-WoH^h*Jn>GHQU?}F#3liAF~Gmbu2 zl9J7M)iQI_4@Xp~CI&LeFg$j~f?b&oTP%I^xrLZp*%m^YEJG zY~<25WB)JSW+uK9B%4SwL=@=J5Idw9PZuQh!3SRtS-28J*Xs79O_Y02dYA0#)i|d#>nv*2(*}DOaM!7pUh5=E|z?uNR{io5C1g{W63=)3Fi%09I$Uu>pxCas|tkTWyUvM)Q zoYVt7w5%g#y*h;O5SWoT*Js==m8?3xEy2h7<+v-Q~)P!#qR z@M1F$jWJo;b1ui?4`_@BI3}x)2WRIx#51EUnB#}z)VN2ObqzQLir3lI+mtM(9^tw# z?j*Y|uVCoS7_QAsurY19{a=((*nYD;*mdiIsoD_I6UIiTy_pDemqYLbZ1k#ILT0^w z*OZbWFoxg;P-x0MN|CJb?7WV0IJ~0$zQD~nIaUG}4^sfag}Fz7`*9ZG3}(Ia^~36Q zJf?g>`1WmRCW!jsdnMor+tRN|CdXxS0AcR@cMLIUE>A8-LfI6~C3NLZd`l20`0F1K z2tEW@wq!2e)y!ddp%uC~Jv~nV+Qx^kOX8LIdc$2Slu9%I37!2&nfRRqVzu5igV6_? z*0xpa9SzOg9P_}d`%84?a15DFjJ>$nB7)JL>Ax39#UDOxTM z;!VRB-9d=Rgl5b{YJQPf6?!Jt0i_t6j+`8{a}l&3Jz}ey*6Tdk!)`WM|EOEQiuw#p z%G@G0|Jv##=2a=IMR7XA*;^i?S{`mo#0z^a0M0B6z-X|~yCjpa?+%}{S_JK{r`AW( zn~t`rV2+>J*XcI$P6+_t`CItK!LVxifelM>*J*nio@jS#By*cS+FE}YHB7j4Ygxxl zB2r=u3~#W9XZ0@1>$Qbkc_-44b9WG<$ns>A|T5idPI4$iU{0^T=|1Qg#%P+R=V zy@?vph-TNhpF7(LPRKjZ=L>rNFIGCq0Nv}rl5h93{W;X{q;i68^Li(6o@dCGI*%CBt00jDX{`(YsI^q_?_F!%$jh0nVNQHEmBhn$0-?%#hg z6A3wA4D_>2nU=syB8o>+=)sT;S6theoP2f1`I2|d(*a>dM&z$UaTEp^F5qQCS`|h^ zYik{dJfov=E;m~+rM?9HCCUNzV|b9{nTdiVbZznx%0WXTRuK;*X-eEMx`e4bP*bZZ z6aSB@Q>18nCU+>>rjRy5&dF}=RBJo|9S27z2PFxKn2JiIK0hc#J1eWoUe?yjLqo(f z_N(wK&mb9Ha4q17s@dUSBY+>}I>UZ^$2)@wYiam02r41oHkVW!gqG)PKavDK4j7-o zVF#rWB z{68X8wlsX`q)_0-jaOi{(FdXDw3^me*{m7gEh4%3Uy9FpwH25S_M$`!P45ivj-f0S z3Zos6g&gLVA7IszhLX_&R?*AvT-RuHc7*RPz+!Q{Lt{jsMef9@Th5b1I^@9`zr*`T ztyeD(8}`pQ*S=8{RflKJYV-X})UPP9v$9e@n|)dClQ|tM7dylA#VjvNP8L4G0+Svb zXb{<68S(c=g)yi@KfyaV`H>570hTgrYG-MH@P^S-MMK|_HbBx9=bc-eonxuEA}c)0 zJ1OJ%I;=)DHYvArckQb>zE7d>^Nt^8^z3?%kgc!0;&I5mx?L6NE~zCoGy z>vlfy~m$Noa+X=k>G+6958`SsSjJO~AFNkc9!J}t@>VunIZqgJ<*L%v&4mxlX zXii?{_a%~rjDwceWqTA$_rWSAO@cdqvjH5*en0!h5?h;hmY+?)Lz|&0rrkGpY-RRg zci2J2NpYiBbqX$_D|c@W-;h%gD&MTKAk58Hh^&^!>c+|i3U5B8-4eA~xmTV4*c%2O&(nar@uT0q$aw|iVKITqi?!53wcsA;R?G>u+`GfJA*Z39f_7aYv zFY_z4J0WGaTw0}B>Gjm=3=cgn39~7`bo?@UCG-ONLS4~S)O&dej9sHKQD%~Y)0J8K zyx6|K78->h8{P<|EDcq&B~YznWT+Ls=?u3auv=iU`*w@wAw^=q8|1vOIeYFFn0PbV#_#a?X>M zn>S2%tzN}dhs8VQd{+A{#fIbykN)K;J=~HETNJVS_mZ3sTn{>iBnuVZ>SAsCS$eBvm#b!#fmI6-ee!np?WgT|Vv_Z^^OI|K6K9 zYA~*q8Yos3pdf_=dXH}j_3xa*KukAaCz)DV(pu3?gXJQN{vVHOl#MWp1;`?2T<*i9 zQmGirLG@UYQ#pGd4}PbYi|cP@lCc-!v0jWf&(*mEk$ zS?GudDW3edrknI_k@bSrBM($eu42eIxV<-C{%rg|R+d*y3ZtWsOJgLqMWQ9bzJA=GO(aVwoJr0o1o+| zy3=KYxpy*ZRJF~+jR}8&Io94FKYOR>vMdP>Noc-Ti_rPtXrDZKQkB@zN#`)2@<&PK zqYL>W#l@ulVl4)>6_wXN&4sSBZjFqNJGk^L{`kZbqQY&b=u0Z`e{juQ5lfQbfrYdt z__u$gzPz%MsYXOroFZmR%hx;ALfxyaYvgt&q-spmMS81ZM+tCK`;%<`wq2UIT4;4H%I8?@BkU>6Q33CzJ5J|((}cc zF%R$27OAPGo6_)Z!1;OIa5it-DL~(CdF`s!$;@r^Sl8!49#w+xl;f})z5^I!K_}hP z2=DX%LBFc5a7P>;Zpgr9RtKF|B)6WOMj&*cV!sXyL_y)KoagJNu(xlKAp#iR#(a6v z1&FM?O*Bj&UTR1n%U1Dfz^J>3oy___dSVM>6Zi5lcn91nCNcmOlgzpmaplDM9pO2K zvr^~uw+4E6H;?~6*Mb8jT{+;DfC=j>e4Qb-UjJwN2V^}jUnxr;Z0^j7;Eq_|z$FfL za8NpVx5ij~EZ!}_re5TZE)O2$98Ci;M)+o|+scUpZ#|8CM%@^B=bOf2p58BM>jyK# zY!$gvco;z6s@k?iFKYcP=L2pP|8F!DQ|L3@|#@VZ%$XQymTJr3Avzkg1m;HxWBgR-nBNVo;YA9R?f zyPeg5Ocf$OYVA9WrT5k2`92#R6Zp`j6z zLtjPF0UJK)q_if{?5+*Bh&h%ae;L+Uxxp>&#x!+MBK(_JI7R{k6rgU~1BMkFt^5x~ zL?$6nF9%o67@R%fFbu*krY?+kCr*&1Wf{svSBf@Kc^av_Gw<2i(CYv#pHqoS-MW+k zrMin3rp{|pC5Nf9zWG*jJPXWgZK3sZ*uvFWJBsxhI0p>>I7TI6xZtDS*aP(`37_2! zh_9IrQ`Y(bGltt%k2373wqRWuoP@U@5lchHkdbB`9)g%TADEkLE@FFyR10TO2Ntk2 z)qxwL@r9V?nw524fc{)mWWiG^!)Kjb8B2A7Lwl;R_E=qleVQ8e6D8mJ++^&VlXE=( zh7y9RFyo2sJUrzD4wmu(Fb@XnU@njY&^+1-d&xkxr!YdAKx0}_nxY0atgfId&8|#( zX4l%dd&YwpsVwqI-58M5zeBiX>P%JZy~9hP&eE+DTLY< zJrJH$Ys31IReE__>R(_a4>b))$-NC(b}p6i{3|8iK+|)A?g}ahvW^U}6w_0FRB96h zl+slx>G@ol4}FSx1WHv_7R z)huc<(u4QizH0`eZ`z+Awl0_~(mn}MXZ`&K1zQMeh>USZZWaPcpfSn!IVQFoL?aZL z4IufwO>V&;;7!l2x`}3dM6GL)+LA06Bi^I3Uu0gwZTeCoj!!M(8R0knZdyj#2$JfA zf&fj)hUpJ`uBvNR1SVzaV~Jb#J2M>)!RsBz!9Sm*W8#^F3m$EBCcp5l23}K(Xy%-? z=0BlM)em#fI5h+6-sr_q?P!Tf5xSFg+>Y$ZAk!ofA7<5n{Dg#Fx`FTCwF1SO8)iIX z+EnYLA0~5{cX(7KbmQ%$_ZrCc9I>pVc(m*f!{f}ZEN>{wd>E%%pe28*`R`lc9JMC1Hqkd4atH-LkN`{8^_e$M_fz)iwIWvn>)qk z#HSCUt<-y|AKq&9@aR5Sm!SD9^({6+wD%pYhRcF6soN1znMqkuUk^xy(B~~5*eFqY zk_Q$AWD8$JnF>?Qorn}x&IVlTnVy}sfd*eF7P$}H8I1owwmTNLAcnI8csV|YrB?ud zoC(8`Wj`SPQe=vSbMQCQs)B@Xn>4@M zd1tCvRv=XxpgnvgT|^%aX3-iWE~Nri>WtkH-ZQ##G!eoF{jG4$USFLsMrKnj zi&o~y?m$K}s6dtz?B=}YStcSe>S?50ytB>2^c=UJLEdkk|gXE^;)jFo5b zfrN&7qaMdaz+54B68xu69h6|y0&a6Et9%7%S5n;PK3hLy$Ut!R!uA7>-<;etMYmD)!cISFZizfGIz*|GK|dd!3-GcHNZ7=WaeQtFM%*`g z^!tlWF1?;c6sILK5`~_!U(75iG!tuh|Kq2oW+9+wAm`Mp`JUxe#;00mvo0lh>_L_C zu~56K*^CET$4LPv@p|Fv&mb9jH%FTtru5FdF=fAM8bMx)q{fEuX%;iF(HD&xZqyDC z_P0{GqZQ3-X>sQm?i;m*7vuN)0i7U#2gkqNHJutZTy%$4+9N+TG*eHi+s><rlL< zWX}Wx_-w#pfPY9UM|gN#o6nx>Ihls;oV*wI;W&Nbor_z;uINgzCH%pjG0$$XUVhH& z`_jW%iiG;QoIp40b=<1kdswhmhB`?yFfbt0KB)aMAXmc>Mh(T)p0lFFmoGnj^5ku= z{#hOBzun1N?PTNdBvg_K;0NRj1Cpt4Zi#7umM*n{@zJBGb)7EHiS53-+U~CY`<^i* zkD@p@+E0cOzpU&JEBMRQ*^M_6`Q13YttpMtIXo21a4BH8$321)x=RNh@Zgypz&OG3 z-JiGo$#HUzwQXay&hM0plN+_YNq26A(A+Bd%K1{l$}FdCcqm;JaLSp9&htF+03)ey z73rM9FOHP_o^P}S@Y)6t#1+0=`?3>!+5XF>JTWjJbZW@e6sf*-q7-R9bA=!?J@;Jx zfMjj)*gNPu2@GegDOF<0l*oJiVR`X=Y!r&DIzA`Bmrjqv`dCOwpcB*5F@j^%+!fF+ zXpX1EfZUKv2}`~C7Bc(2Xq0foF?Cz4sFPaBOyLIO+`xsR=jEg$ij!;V1xEM4_6l4o z(swx9Orlm~rg}#^z{&{#LySZuPF$Jh;i=z^49t$U^I#*%)2H~D7Pm|Co_73r=1J1N z%!vQVqy&KNq+YAW1cZ4`7p1vPO@NvXKo7j~3D6KaSZtcKVn~ul%s;pcj^9Rpqy{(+ z_4eR)7aDh+AqajS6Vm439Bv*_r1NptJW{_!t8?BCa?5-{v7_F~&_XAx3Oc(2)_b4< z`_1plh;ZYn0=OzMHIdq@WB1=3Oq3t5j*Fj78x{WA_jAb42Md?oQ2(9nK34=~jI_72 zZO2d}_F?LAi&#^G{zMi#sy`n0^`YS&wSjQ|Im48+QhR1TzQOeaNRbRJ(I-y}MKIzyn;e&0~@vXkU2=}iyaa678 zF=rpU1rUf7f<`CI`t;DN*ROLAtB^w46++}|M>x?cKVv- z@wu6y%w%3$vzY}EO=HgF0D4;6pytxq+*$H?Z#dheK7Xv|i4q?bef6dcuHw~A`ri)? zP{VOTekG`ULMub0v5N&x=|(*FPuVgpuYN&9a9`SGhDf#XkRgC!yhD_E=p0a$5T!$^ zq^JJp6RD!_T3#FQx*%>?bTg;!+HBEcvKERuz>h{b!*BX3!_GVWi z(%fm{t;qZAD=&Y<5&O+*3vUOxs{cK9W0r2_JcrhS!klr|$Bc7>(l}p0ZoL~sX+97X zx99oEd{nNEKkW)YVZ>4^BASTxDZ-Plt}Y}u%wbfm&_vb4b8BD1H_H)zyDGN9+DewA zSd(;K0LXS!qa`3QQVEaczr_E-JCJKY$u1qw_L*p0d;YHlEliAcO>F{jUIiu0g+&R9ghVlfQ|QSU-A|6)Sa?)H zOY^4fec7n0gz~|}62bXnXLi`;04_4LPq0z-e~BwA%j*WO|NTE+{}K|pHb2knBoX5{ z@`6N?mmrT)#{VUj_xL7N5mOG%d~EC&{zY07!6Pnc=MzECBm<1p$Nc5Yn9RbhZwXu1WA}olm>coHJpsvcYwAqadx-0AN1r1a}cF zh0xyQ{^raU%)QMje2pWF;@wU1y0#$+4U?jlRlutqW=eOWrd9x33Wt@u)u+-zmy!IRjbFb2FWZN{kp+Oo}|#h z66c>nc53S9K0Y{E#a;F|0wp-Z6`Az}ZUho&-;C)u6xt79zM@HRjrX}Tc0ox0=AJV5 z!}Ri+<%QL;3Ga~&_T^uF>WHn17`xPIjV3AE zg#$8gFN05bs&p=X3X-9*$06MXX;(t9hChui6H7U@>Jd$us-BT~r@{lDPp>jfeenEN z!9P(GHs1H;*z1I=`HgK z5BMg%Ecb`wu1W}k5==FID7kFO{A%0y0V< zX`|50a-@t1$O*`OVbMkIA_0L4A4gnu6L3mPMRVo0)Vxru?Us5{oxbICNV1zH?1Rp2 zaZY=y9#Y2@dBk1j+4rJ|8*s73>1L!K9%dA^aSzEZ1k|=T|o%Q#b{4 zWNhqBA^sm9MJ@R zw|c(?94$1@z7hUo2|QQ&@I{2wAaa0iL;|kf1IORqR4`L;4i!Y#Ba7B_V+fI;X7W!^ zZec*sZD+~*HxM#&3d))4i?Dg3#?d=Ei=scY%a6E!Zv3H;KdZC8(DSf*yG(xwU4wnj zW|?aUWYH&{yz%NtSBW;fWc5xK;o(>WsM5>Yyyz2$UZ%^w0q@ z2(LCkgcgdj5Wov~~@fNxfYJB^j{A;EgRL*(N|#hf{F(u!fE>0)$|=8HAtce{1F4J?cgM7 z277_7YRV(-?M^^A26>1aobDWMQ?V1NYLt$+voGOVIi$yKD81iBS52+gC#V!*CL9tJ zewOJ2+_R~<#c+67ko>$%5veCcU+bjx^h2Att91Bq@3_eCsZ zTSXs0Qf2}eU=cVVHqLTs;E(#L;EkG5g4=F;>=>DtYWZfX9GiSG!}Cn!55qLrd`nLK z{t2Wy+oqNw*BB;tTYL3-sgG^%ltzzwWw$MUJS4c(TY4VoIP-` zbtS(mDbAB*fWFQFRfGC26ySu4dY)-6Jj4LS;b6@NtpKDl(FhAu!M725cEsSl0b#R+ zZ;F3(i$O0yoGI8sGu}c}?YF)p^(=~nTx6)68r@+2rS;$y@0#r-v#h8AC0^RrCY!4< zD-r9brTKYPECvamFv9Uko<(a%suIg{Tr2W>-2JcaumwxV9Afv01fF%7e`M!!5Oa#q zGL9T+dggPY|Qke`x!pg~%hsYS0TCeV<2UpB(lxYXm4JC_OK_Vzn+&}>Yi2KlF&PQ?xgnAtikP#i?y#K8vA@Q4c z@6-Te&MxKck`j}JoKryhZdD(?NQ}_VL)Bfn8=}UoayykwD@)m6<1*{My8uVBT$^T8 zDf6JpAG_N>i{S{oWqQIaa>(}rNU?Wf(I3CutklA>ruh4LF8t^O>MIZ@&Nn}T4QP>- z1DY2hPaw>ld57Xnnk+=^|@cw!~K^9RLcLanCLj((!L^!h!?D4>i!#&Vgxig9aeP_vX(U6(_ zP54?2O>)^!UfJEd*ih{8U4X@}MsUo_bSs+DNK#^|Q@5+p{|$}ao{h!}?*i6FgI-Gi z*)@IaTbf(*+32~Ng?bC>{vplsTC-19f}dsZ-R?_@1n6sIcX6ie-JnFT-abXyV>sdb zYxMns)5P+V0#GRUuBw8*6wXmttvaA^8>Fr(Bq{0aaEk#_dun|0V+Sc`1pe3c+_x7E z1+|I^hpWj=loCXL_{qfIuWOCNDB-r021MqzqSZx{Fy=9`RwiKzpziLJ&@)lZzYb0@ z-$+tq&ELpZb$Uj<5!6P@bkg)}mRrMQ^>MkVQ{oI}316v_vDAHvXqftL-2eSn#N)DaAmzhzYgkS4;L_J%=V;}Y) z(=h(t3Hq7YoimshgQe<2!|Vec6{&U{O&f4AEsIF`31jL)eGeL%xCb{i)Mh$I2I52q zg=^8Y9)>DiI^k=Z6?Q^GtGBeNj?0=%FD>Y5uT?$bvo8+<(>lY)IdOeEy7jdA6Fv=T;8hJoY2kf{V<*oXlKVwVb>2 zr}n{_c%z!a*p>aSrYcBTcpTs1V5)B%hkBa*bhLa+Fq}Y3+E0xFW7THu8MuC;d#)Fx zfVlI9_+Q>NBn!wzT?4pbFm@Y)42E3D?JB@1fN_8Wj0#kGDS$E-@rAVvpv>A0f7kr#!Y(=|{_WB%A&K5lvJ%scs zt@av%wj#M9XS~wgEzTE?;6=7923j_6S>C`r7Zw_pKV%GLRD*vRs3+b6Wo=#bqcMV6 zwH9QhP_WAh5H>_U+_IT|j7SN=)@tQY)M)(O{>Bo~&5m6wo0m2D*AhuGJh7TjT77QC zJYB|pYlVNNN;@Z^wgO#6rfb7-%d#5tZWCMe_~mh#Ra`bBz_EXB@qxRaf>G7kOvqh7 zGa~{2(d67r)1tS!(ruZWGjgCMRh}w;69@_fgQeJ-MxPLY;T0W9v%SQOdc8RX2!Xm5 zbr}ig_%mV(vL1o8{M5Gy(A4Hak3TdifSu{8TiQTv{Wgbf<5PBwFOImAQTD4-dJRnL zB?q&PwUrw+nV;b!_CjfUJUWcfRx9$>a(eE?7{Z;=P?TpA7!XtJhp8i`G{ttlBC6if zQt5*(Qw%EI41cUyUOhg~jQ7ic7idF8tIr&fZxft#Ri;3^vVR$kfCM!uOWc7Z#&2LQ zM0{khoTWPi(z#A9F8V_{Nw5B8#|=$(eL<(bd`ez3FK0|@)uSJym{bWtW0*VYMN>li zuQCW3xZrvOG~yypo+w-37g=n&7aX5Q?Yva}+21s~e$(ExJ6Zk-1Ipo{+G!a}=jc?j z&o=aH0WZT|H_83|iXe@0&<^mYW-S9&MmCl+;Um}`ewp>Ar@tMcK4=+AHI-=@B)w~x zOJFcx=6j*#@!IP(VIgbd>@)d%5^h(9i1%3nBr@6F*;Gs4HO&`{Kr9adCT3@c5U(9% zus3mJJ*Wq}#hy(4NrQ{zFLb(G5#}VWh-12z8=I^L{Ml zth@zlly8xRMrXG!DqN3W<(As9O*7(ECafv@~ zJ)3I)l4P7djnD>|a^qjA^nXc&^Xtf5Hg-{ueIjaPbOS8-Y7w$L%4}Ejueu283&tm4 zG1Y(DZex})kbeiIEm`1q3&gD4^^oH=DbZ9l6YMiW47cRvp*G*@(}_zf-jwI_cMVES z1-l+Ub?TZ3UxNS}4}v2Kio7!+f?q6vSCskv8=YLLX9%Dw5HRg!0j6DxI$!zOH$4;C z)(c9iM_-H`WA2sGRV&|f&dv3KQ+Xx%ai&CW6wj91uEqT;8gbbg^c*&U$Rn&BHP368 zVG@F(F>&sl98n+B|M8Y6d@E9GX`iO`5hqhx@OsqziMZ|bhX( z?Jw@|lVP+`3L330^8!7=oz?7MaN-_M&>L4o3bb+roqYM!yqZ;v3J5^4@yW*_MD^3B z&?=q~KR;|}$oLGvB!mhmn~a1pg0zZXX+)>+r{fXn)}PC)X+QW9fc#V~tkS|M|J&T% z)U3g0_wxdHVducMq7PB9NQ~!AD{Lg+ zXTQ4C{n{nL9YOOiBROF=InOc5 zQlONX9Z}2rH_)rafE5B`gCVNb8rJ>9aWeJY>}@a|>O+|n2uTmaFDdq;n(4})@_ZAf z`Z~txU){SySF%j>#7^Y#I$QOHqjjcm^Ra`Kr0sy>C4)2mje4_tRKcrV_ZEFomJSlh zDJvO0C&ZSPme8Us@#M(`kR^h(Fa&fn0B`{2uY`8Ax1VX{M}LAusGfCKNC6oDEEgG{ z^!CHhf%p{QW}bc>B=f%~&rDb`?=wK7YP_+*-%kJw;3j%30k(QVu=vAc7p8iBfGM1u zopT`^3_h4A-~+`mW@Y^OQ+#*J`bHU0r>;7?%HtnrpLL$YtwMND=Aq|ZDe{YrCx}90 zhD1uBY!@R&x?VM~*zQeoxx{IBM?OFj6UZqNyG1%6=M|APz>0WXSa^(M6jFZ9oWJ^n zg-jCTP8~UMf=08WXf-XTa2LnQ%;XR3O5sFH?Hu!9lyQ7zSiaP85ghYO2)V8 zIYM^mN$mn*rGSIval-i~sDGP6-qQndgW(yrNSV_0NQa$gDOZ84&bR94E zxR5d%&GSnSgUh3#VTHfiS9FaGB`NViqSxclu2(gtoHra)0B_LR;;ctUM+m40fjNZ% z=3`Y9pu#@*nEoVD^qS?^IuFy!j6_>2L;B(XCMIjz26(h|T%X_@f8Off`oG!`PYI3r<+FVXp$L#aD*i>2j z9B2XTM?J`FEhvR(U&<#bvjOWx0_P?WlW#(9`SFjOH)AiNXGAw8Pj6`lO#$zmSOmqs z4UG%~1HsMYw2c#D!{i!-Y{g6mi*|e~RRlF)@`^7DA~-Fx1~?TPoG zXDK8y@-gt`L&cVJLHsLU_SxF&;LU2~Ta;K1YM?lDpSQ&5)z)Y$DC)Y}XF_fvZ9%GV zPxS`2%OGg|W!A{7kIUWm`*ar+r?UXo{4=(bcwo0HVf@B5Mlp`G~iq8_>>> zN+D30goB?tcQ^)zZ6NaiMeA3YVh+clFtj(Iu^==kG`pg?G!#!BEF82lcHVGWe|4ay z?dmbgisz-d-Ou_6CGPSntv4oOzfS??9Ja>rg)K1c3j(1T2NWBC@WaSiZ5Xr*kc9CW z%Cg-64p%~V&D%A{@U{`tz4D}^qdRrss!>Yuz)!pO#ncz-GfFbvhZ#)S%ZGnly06pi zIKGgq8K~bNVE5{kDAfb>yjao<$|}{szw$?2-KC2cPkF_}kiqxuOrpIe)vQpr@7l)B z`xwizEZwN*VBNs_+C7RT|Co&1YJKjyb)Vlcp)%ud=9 zY5a^$#-B3^$0LUgeG+0qw|{B}-ympclImb~#;p)=}b_qf0_?e3~- z746z0IXO898Sf|yNSl(UqR;l7#mubl`uOx%)5;(dmg(9gd(#EaEh4@vS1|H|$R|3x zWOQ3mT_t&ogoL*G;VN-_y`0}TY}WVYpXzlKYeXdAYtqoBJawl7f?{|Whqx#RlA4F? zBeG^^r|UwCD3JtoUClZItxdRoN; zD%+&e`&z$$CQ`3BaM7Q>dWS6L%;o0URN166PppPiEG#VUq-T2kA^1m>HR`+)zGmYz zPsqG{b#TujJV~YLVmrU^9ZbFLllZ{(_xH}++Mk%EY+L#S@j#WfLQzo*$m#x6^*~ZO zh!7yCwzI%k{Xl?rkp1LrU9GqNBWXy2C*z6lf$_Xp(rPi7esuv+r zG}=0&j-#*FGK*h1FS20Bc^*(03Jb%^q&gpUQ<<$psi3tv_DyC&xOLA#wonnRkYJ{- zC$8FV@eQ7;-RWn?j;o&MEqP)Y_=Ut++SEuD428+JT^K{@et#%^L{fP zV7r<|G{j|O3_(b1x7Zs9V+|5)g#cENln?4>14zox&@AtrK-=2c+XLNiZXa{TBT%R3 zVS`jTN0le#bxofR+^-{|=?nj-UP_4%3>lqWxV?(-o9gT5nsz$7T2k-Vx1Ko8hC?n( zEavqt-^u*Ry_KJik+(KbrP}>g)|jv&>AIz_mE0*c*3>GzK5*%8;m?Q;70FncbV5dI z0!qDsJbeTKgyS&$fEl&;~OOK-!ovL+K-R3qpQ^kS34u6@`MJ9Ioy| z{Kz~?SX&t{D%Z7HzHr9GCEnkbPf90WZJ?cIxcU6-Cy zI0SNJT7wNer=wcitPC6C#M@lt-vJE$FX57YlrCSI`>33!FGA*Ey>76e(yWI*$o}XY z{s474aC5G^z=_^*T+t{EwqyhEEQS$uiq(Y~F_&;lj+|y%@oT_Tu47AhQF=6SO!TDM z-(l-WBBkr%VH4^qAz@>GWz5Sl_jEJeb2!9nLeA^gF(6!}z{q;0mj!~tMI6%V-;Yep z@HxbHG=p$Fqn}I~(_Mb>c711=L!4=K$GzRsgIiPKBmw5P!jbbG>_UH@?}x%|U5sA{ z*&JNo^`6S)&MMe{yZDc=7Ntf(JwWUA;8Upp?;ekEQKQ~7c!Xd;fKwe}WJLyc_%q}k z;cF;{5lb?u%EQj3uP40Dx=hbPpCK9jMD zY0tk%6vwZ7rdG=1Fly+9C0n=Na;{MTeZ9J4Di_P4Iy<`6+0Eg=kn799R}K&Mm1{fM zr3pw3wVM)zG%Ha7Kl-=34RupJ@y7M*KD44i3cI%<4!k+?mNyKt{tWR>FQAz`-(gt| zF5w|0rDg9O|7&GLjvdu0*e1^sRC6%Vf`$?{HuWMxXlA;4KJr+TpPr5m3noqHS|ylE z&Un{wXtF}R7Fg_TY&f3e^#18RJZ>U*3HSqWPU<}uE?UnhWkaR${RaekX+xBbFh!UyvBxw3q%@*wl)qCMR)B ztk%3-ue|Ni=TlnGpQsm`Ha`|-k6{7(AyGzGXhK3TToCZ|>*0`*qndf_2`)uo3|NLB znB2$LSG7o2S2xq3i;kWi{D#bUQJ|oQ3jq%&$m|+-cIx4E^^!HAP*Y2dA=l~XRD+-t zBg5oIbO&tDMCl(s1&{_}6=#Kocchz^@Fjt?ux@N@EDsG8z8M@Y@>WumXnWoKK#>C6 zkM=pPue0N%8m4lSvF8PCX~!&tq?EEc-gkUV2+8As;s?1%6VgtRZVHzS|2mixU7el5GMzk)6h>5i58zuWJk<6jVS)CIq(`Rh3u^d%2_5gj zco>zjWa_LVl6@Q-8rt#EbM8Mr%KvyKxsrn1So}IU)e6qAWY0ibHAt0J2TYx*OT({N zg_=6s*%i#3P<*jxVY}Y9{zs4R+m zwCWwclev|INHOj;R@NZYY;?0qZeV^C zw9hKSD|i2!4GJz|ftGVXsXA`vmJArV;L8Y4+R+Zt!Or~D2MjMcB01mn|sM+GJMxyH<+ zM0GOQZ11dlXvUxuCtyh>VNLKs31bQUlXfqyY7aOvm#BND>qr)?TdzKLs7n1#3RGUe zf`R}?5Z~Y}-MUpMwq1v$fzZXPKseuwrF7bouXuQIZSLuS z(c|-llslF<1X(Y9c6Ey92pb58^ve8!fT;ol^J3sz=gJbt`}-Nb{a!*U zMgk!G8*6@im0H0G2~@D*Cj%WqBq)^%Gw|xxRx-dh;Y0Qop_N<_-|d?oL*vxtsxFt| zZ`n`J$S0{6rY_!8^&=qUG3J={L@#eG4Thbut$u)>{R}S=SuCTuO__(_+%bS&L4fnL zcWyzKj!M1(9vIRK1MHUv!yv#@wJ3ihAVbLMb?FeR6}3>(r*9bTxgXdFb}VY+GEJvG zMbOKCXk27ak7vdnpIEBtE|QOGH~C|wl`h+!?myeCl_Zeu1r7_{jKDNKf<(rCShz}q zbl9Nw2pG)=5C~NS;&1@N?BG%p6PfaL-^_A=l?k#9S5IU*{-Y+_y zbSZjH&$u_BKm4#nAaQfs<-=PHna`txk*@r_U%&OFb{~;Hy07$yHs$eqNI%x$&@OGs z2~wtrUp{2UB*U}oC(-_wPpI6IHBP|!J(!80Ucs0sFL8$63@Y6__j&b*wi*TSIhYt)7kmCIlBI=>@m~Dq}9*H*Ez1jfP zJ%LQA20_^`6R0qCgSMQ~FIu&cTcz%X;4{%!chEc=t6qQj-Aj~``e#&1A=8&vAizg_ zgP@JI|Ax*~&ZdOQx%}vPyGmB*cd2~-rP)_4ZWSlGG@UdVS}}tT zH)ZTy#FLr|sQ-CPWOUAU780+rZt3w+?468`+9qwI$J*eaKta6%5dPPp;fach3apCt zAn1+~aJG5ve@+__D!j&d)sKrp`!zc)uDNI;Lg@!XkKRrR9R>JUR^^)Y#jg>2=+>L4 z$bx3qdTFdW0b&9l^>315V5}L4J-nyz9>QnMIBm7!%?)8csx9w= zj}gM)*IGI(AGb+hk0(`8St}=%b7lRC(EE^l)0`Dt*SY-!1~50lnSuwEWq``wwX$Ld z_bjlZk<-(9kg6gDF{&!iSrHhYCH~k}4CAfcMOX(rJs$u zg-k07Pm*4`@9n%{m8mV0@6z;)BMQ2?pc3rOZw?{oj=pgDvcd-gD`UMiG+D^ou&;>! zfQv2^3-gMKW?vvZt&8T~G4;Lwh~s4ijmu^`UlTI?>`A{OBd~TlyJ8&&9_QhG5F+6O z_kSdK4S~x@RAgHAxXtDdXB3Qnr;Iv3y{-#W&;^~vI`!F+XM!tggmrENEi3Jxm@#ZU zgd$FMu#O3&zTUZZH=oZ!L2<5Ak0XCgfqATPa;%T;PwB@G6SKxfIw0Mi_ATCO-1@yl z5Hug$=B#*>0+FD3;xX%gvLJ-W?nXx9kVxu}De0HtvGC^g>r*(CxNJASMq+zbVPL;$ zF6g>blSuGkQNes6?Q=o%%=4V2ozRw zclZ9wHwex;H+OgE$=3`{J0q9kOSZBhiZ&3=f;XUtdJ~@|*Dql!*8g_ke0lZDt?W~e zgy;o7e~UxE&d2z7=s9dW9X2mQd=lSVeoKnnX$>vd;p$|7;`}s|tXc=z zYmuD>I0S_rXB`b4@zQ^ptrf!8w?)j>1MEW@zeA@$(=ZI6PrZf0FDQzpc*h3=1vEaX ze>Av~t@8u(ZAg}s`3Xl%i^lQ3=|^qXxYg9I2LCsm1(adjIGdBzw1YVtGsgn9^-`yu zN{NRUph5z+s?C#Ws?RKgZO8tv@tsE&v{Lv@MY437rF7dDagHNFIW~#Vwp_5Ub z(@I^<_8H-GAw#G`CS71~()UvIYqt2#|@yWhN$GgXsXCVEmvZN(OJ>(r~Ff@n)9Z z{}wSWJuy)L?Ce>PT5#VNJc10bG74@+rh`r6+Pc8{P%U=*WJtHl>`Wn>*Hv?g0g{tn=!&O^}?QdaYtA^s_} zZT!$$1cpc3eTyhlJ^kQ2HFBDXhlk(`OL=p8ss)!j)i$6LXD&!JH4PUBuQJNtnw$M& z9D*gdB00(8OxM(_AI`(qK5vF(L&d>`WQ=_8WkCfOrO_Hjww>pr#k^m@G|xv6oIQE? z@HEubQ^N{~9HU?wWZ{F(g-d-cdOm@cwOMTb6^Y*bC%A$MH&n7y-z1w4eosCTIQRL* zkF-kc5vdW?{PuZso1OFrrB@SbhL=BN40^tfbyGbo{{r@+D*k;Ni_vyD3JF4RuD zO2%SUNh0qOA{%Ccg-=~Go|lXL<1m%m-6gD7R>8hS|1KVHeAN^^5S^ht*lf2Php<2& zpt3AqvbzJfx}Dii)VInQE5pUX+w&CZje1ZG@Dz=WL0;{r)K8)=Z3sE_hviFb4~&yKMYW&-GkPEg#SV{MF$4yS8T8g@R6QHdz%A8DA|(s5S*MCq+rwR%-~T8ie`g92UiBv92-{9ugl7xmJ_WzISR z?Lp)aPz8V@QVR!fP76p_bbB;hR%@^kiXIeb>3?<&a7D0cSDn9`sn)a6G2Wx5MSRFX zA$_PPxH5f)w(4ckj*{`6*$M&nn6SuPy|&Vd_#f3Hv9JMYB%~JewN8$`ypBn=a_|;< z^fK#)dQ9q>MzoNMtiYS)wehx$X$*hCasRvh;9gsX;!-e+6JEWlvVcSfUxPYN19vb=o}#`T(M z)QyivDyzmA|Btb^j;eCsy2nvblu#r@T9igXLQ+5h>5v8~C8fJd5kW*ix?8$Kx>Fhn z=`QK+4gA*D^S<}q_kREQjd6~LamL`B{p{xxbImo^ocvF(t13T2xa@hu4EUxRy5=Rt zS8{8e7Y`g4#8Dd3B&l>qSU|d%IX~_nA^{FNWNTy%!?{?;s{lmIuoj-(@O39gmstcl zhE#jooPdU)rIe`WE483#yO=;;(Yd#mEy1rq^KJu+xq8JYe3AB)bh7`{sq|6H`#{;y z3|^Xm@M|=o{Tnm2Qp*lA~I(Yf|T4w#5Wt$zZ@l!%C}@mGbIn$lj8aiU74a!pTGJ+#EOF z^u+v_me7Dq&M1+Yvr_hOnIUMYS|6^d!F!*Y9 z|Nr0AGN~>a>X8nQNywIc2WG6s>OT2DB>yG*1sPJ*UQVH0p+D(^wM3r?R@O^fzXyE4 zF*xZkeO#nTf)PXgQ_t(Thv%kx5NhcHhKyMS^-|Y%ZPL;*))gz|dLI2Bj|XI>9DfJx zD+F8N$~>({&TYrxK$aa+)eRKR6Ss@*D2O%$8;BkBUq^u3f0p6Aq2ed__4JB&xJtlk z?3*8Hy@mpzp+Pk|Xtx9Su3hM-w3wF4Uie-lW;Jz+GUiZyTgrE^zw2xFF)$)fZI+Ll z6!R@%XQOLWZ%Uv$zOsrfWDm}}?2I~Bm&||Lf*4Jz(}#Th zT36$GDk>@21he_DL~%$+OgiKh(Bam;@iDS1|4e>(&u)9jiTCQ60p7#wnpfq9a;bJk zmLAIF?fz9c+8>>IX1%hDD;Q{oIOz;zMQ+&ePU)cX$X?D42ehD35aaiTP3su#^K1A% zHxO(eh;GrBDE0fbx3y6~AT21pp8@VPlbq{>@9?dHbD2L74c-|IfTB*c(W zQ5g_3v_jAlgt1O*V^c@Dw=oci>+=R`0i`79hb|boaf))nvxRz1HM?3JB^M!WsI^-# zUM69s%AVY&zBoJe5gZ~|QtWXz))l7A2rl@-ZTtfc+?%@7W0mG0M(Xu>-EkS`#BePt z1Op)Fz~P(Wv|hy2I}3!@Shk!R4Kq#{FhCWmpOtQ9mGiHFYbaR7*}`FP@fdpKoUSKq zgoK1u+bzT>FbjnYc7{Rv6Z)t~%Va1}b(szwN2?{!ULULc_*6ueH+FkN>(Ubj`gEMVa60`X{9b%eZ4MyF*(Ar;|npjw7NP;?fD4-`ffN`a zv8fR?558Lr<`O$#(_cWt*GZMczfcGnc}uz9!@o_&BINc?=__2@R=CR$_YgO{hkCJm zM1GBf&_p$$4MygHgn$r3gOMS~^;9z}{|J~NluL>W5Ey6{3c%O~WM2>G{^5Uij{lmC z*~!3cf`GjtBQpos)F3svBeikuWv>PkT)clipWr3L7@{wdVxXlT%AkKTwYK#i($ z$g+0Y?WCUOU3q@1fdui-|u zodzY0cB?;Wm@CZYbbqA~+&Va5w2SGpiUb#Da(6^MD6<~v!0wxaDaPC{Od{7Y%9Z|= z8~Yb$niQh+L7V0-EG78NDI5Kue%IF4{u2eWwPgW9C1^g4U#Bs+`xIm2JU<~(;fIOG zcczATCOo!;%&&9|zCQ@GC{H_8qe*0}R4u`F_RH$+2Cbfhh3M_rWs+ItSvR!CU`Ly_ z=O^w64qtokz)<6dmbEJnxo9HGIvG|&NqzZSj=s-Do-f(3Z`810s`s!)4nCvTPKgO%Qzv^`W zlaVLT8lpbxAFSD0_v<_1aD`o@ue%obzW8bR?=H6#ec8O-;2B4OIt;vl+lE^aV9GgYJ)1fZeV3Fa3ea8AfZ7O*Baii!Ug{kfAFxu>pnyR;Bo?)^ zD_OWx2|m6+<_LnYm3})5*IzO>^k1>L$aK94q*O{J$6PiDXxg7iFEEhvll85wXiy>L zbYL&o_CJfLx>@DRX5DS-d2IjON+>1#wYUroQ_}R)V@pdR$w&b=0fCSHbmiG3v4V(5 z)*9C6_)0%ZUZUAWbwp4cj3F*XV~+2*M?3B>>3C{NGtGz&27oy)ni}5^@DyLKjbV7J~JLSR87ee&*M8D3>o^e&;c^ z0K=K!A@CLZ$9K7OWJW5o1V+lFzm>BY4bUPHxyMxuDB3~IL&EPCbGlL4gM^!piTsp| zHGGWH?bISl2v= zL3!>O(K%2D3GUk6)v}Oz!3BW&*8mphl|N>ytp*z(R+#;SVW+g+;~2rmzax zz=PRX!RMZL1hZRNXJ6O9<~%k|iJLr2jeh&sfW5+3h1;`Ls|C-}yo# z)By{4HZ$#|jx&JR#6(x|+%YppFo~sGtTtmbz95{?R{ceE*c6Y-Z$`1)#pI;o`N@;Z zVQ(-g1O}Y(yeYI9;$F%DsdMt_-JSHqez}+#r1%W zlk3vga%T>tOD(1*-n!{sO56=DagOC<9aZD&dNKRI zBrxJcIqQc+Y8dUlfZqXxDj*eLBR4THilk-)@F<4F|2+gfgEiXpwBHcyZDEx60WL#WZ-nn3$hqd*H-~z;>n~$*Kfy{wp^8?OdM`wp_UBH!x81D%7Wv2qlvVLk% z)!ZQ^?E;Dz;~6xSBDifcvFQIXmwALtHJLZGRK~b|eLA;N{Jf(aJ1K^nAJvUws}c2w zSUHQ^+Z&W0>s@{8ljle1@E3TrQCpfQXZaA=@8-c*hwI+ES2E{ma@{oaB1_or+yRG6 z0xs8fOS}t>E~u<_atv1R7|(f^;&=bnh3u(fIYdN?E@wb9J*;I~C1+hEkJmB+-Ge?8 zI)1C@&GFsxcG`IVxKAG?k-{cx(x&I z87-mLqm8YICC@Oz{Jg&Tg4V+9L%Dt36$Xj~tMghk$v97D=J%fEyC{#Nr6Rf( z78h|yNu@x23^S_G!A=}nh0r!WKnIDCjOyTYN9&fTLQO++ZG8D0_5YG8*+YHE^>I9D zajh70F|bDJH%Zb4-qg7J&}C~|+vdUVd#WzN@*|~Zu`*meOkP@d9s7T1=OLqOHa4U| zOaPsHy+y50J3;)7{{k2yJt9T@ns@IY( z8%?0fb0GD<+J~Xrp4N{``M+pfqbODA&WLyRN6hPvn^zEC+?eyNHe-*zaqHHfVN7mY z^x%x6lPAc|7orA?hLA=k(dIRT)sxg6wLZizfo(~4joz7iFM({dG zbmG+X5Y6;(JHwo+SpHj1%u}4vDTCeq6&vmq#9=K}_b;d~kYf1C#E85Y}{r=DFKNdl@ri+dS4?lMl#$~jXGqsX@=Pq(ehoRz8CO8ElSqBFX&oe8FK{z=Of;M1gwcKLn@*fq5 zAPNYroov9wYySsaXa}TOX=e)kAvC;) zRT?(*g+Fz5y92p~T%SUyj5POZ%SgC$t**v|?LbGi2thn!L}x0_kUq|z?bmR71kMLQ z2)ajGGvKZGZNQbxVfqqWn1V(|%f1;#ZJKd+lsTdDf$kQo*{f3VDq8k2 z!q~zxFQgwPYtL2Ba5u=^J~`X5N285Dw|j=d>$H~uViAa0S3}OM1FMJ}Sq9EPR6?R? zZKzOB0yu<^!NEkV+9VSnk1!X_Fcw;`G!wwdx0pTTvc1_A%~1rxeiZOEYKEw{H~S|Q z7^@C%RxvniI+JAliHlvZZ*5c>WInrDTjEkFQ!TX?J#CljzN^X;JyP`u&fCNyH9y>#$kwWzEaU}qXUFz$9xz>(&8M<6 zJEfep(p=l6&pPF@iv=w{6ih-EPgsRl7NWl^q+1;Hfg0%OI6Pj=Mv9W|p3k>eVxB2s zRW&LQ&1<}&Dqq7S^p_X+`)AMt*|30q9Avn0Ldz+0P&*T5KB@q-!9c1v+zGvfdL78x z2AYP$AG3AgvS&o-@NO((>D&?Mcf;p<0RlJ#K%6sQg+RZk)n&ZpsoF z!S7x}#SboV-=yQYt15N}`=%6B%n5_Du0^8FSS7yZO##Hl3U{V0~= z6i+1EWc9DN({<{5owJ1G4?G@4Elza*aZzL+H)E-|G+L9^BTBT-s9LD_hy(g~rkE`) z?4;qI(wA?L-~=&!4362u4f<*q6#R7_Rg}V$>@(I~NfezZ%S+L-^r~HjPT|7+t0!(> z*!td5suofjv%NaBU6+XZJ~Zjv^?PuXwWn&c2>ihfo;X_89}4=Q&hx6{#Qi7>@qEC- zNHrt+t!W|TvqV~DMX*5P^_`Xo@%t~-xj~sQeOMOM`P%axue%57h13PjZ_O}Q{9osI zAv!&jw*|JQKis-j7{+ck{v+vyFUs5Vg}mQRdp`qjl80qaUHA`oFr}rWeDo&UIh}jU zMuhZ>+dFAvrVN*SnEd48yt917#WJX~c5H0S@pq#4<-<_u;DXU0w@pRj)ttaZiPEty znCg;oEU)cU$WX8Xd-s$z<7uF~Tqzvi>wNOcn}(sYqqQ3EAjj3_Ibpn!gSzr%SJ46whUmHJ-5lm~y6GA*gH-f+F2E92m^;>cqhjcquR62#%0+j~-!-a@@Y3HnytozUyZ9YhymAqV#QP zsrO#F&J3x`SxjEQDmQ$%KcBJ9;ktyS*!AandQ@OF3k?COUwYJ@qPN3esi|EOm6D2` z4HRuam%sAVmbaMXrWuERgVcq`mm*=?D1$ghufjqW6tl9KYv2&{;Q%8qe8o7oS@8+? zm4$j$2}r2$0ckP$J5Nv4nuG6oek`DfIlh{}qJC}dtxTwGj& zUA3l+u<$h?wm4&@t2CL|qx~Q>;+>&AIe-Ze#z!?YG*|{0+~b>>dpSlinYn+;HW+1m z$>F3uP4#Qlz4WPoLqtT%vP3!U>HS$7OI+@oVn-C4GeH(YEh&^FftGYPMD9~D$|w_a z@fLl1ZZc63+T*$bg7p17A_e&BU~hoIK4XzV&!QKx*>z&~6wwxty0S#3I^7Mjc)*BJ zvt>@JPCjmC=IO3}v-N^Evwu%20tL8Dic@@gd^)Xi=YdA?<91~aPpWsr!^3r-gEcfX z$|g2s<7IH%(nHW0)xLKBs#MNj=;%=4=l4ro`)I^%g81G1Ap2-v4dEujMS5qcEH@f5 zXdvOT%W)}|K(}t)f)gn{B6mBY0CoC2Xx#rf<*Lh5oX zn8Dl6MR6#8f3cYB-)gz_X6nk>x*5Y$*9X}bT41rrTY$JUNW#`9&Vy zlR2}jOZwKW?N1je1G>w8o~sy+>JWvK=id5u^W(YVG0v3{uXmT0(582-<4 ziCnhiqKK3Lf>39r?OumEym>|`}>sf%N3zVQ}6Oa^$mBj(6)ExCMztSV>~Ek>AT zK2LdQzg`Y}H&lCTZ1Yl{!Ehs*(;%j@Y}U&R(el%Q5s8N7q|*_D?VI#!L@s9KpOxmB zX}F@|&DDRzZRRc#hu;ahn}fAHw|C^Q^Wl}!#U{`A{?R2q?h&r0NUzoft8F-gc78kv zz@&R<$VRm1boRADPf7|afbWDBk}+7MmyTG{TN+Jw&F9iO+}oQ+2$B`A)e3u$-m<)C zJ+$8URW6!o#v*yk_xMF}$e#lW&H!p!>kP0?d+WFeaEd&jwb74b^7_$KQ=Ud;7)`v3 z)qO@9!uK_OP4888am21mYEVl^ zkl~M7qq5f^?K(X74AVMCpR2HKGM^M0=u5eNU0pEoC1&N9D&3P{yyQ7}X3L=$$Bj|$ z7?<+MO=tF#A5||-9cRaevnL(fD=QPo@Euc4;61l9Pc8dgYTGUp^8pE7X8irhGTVb}Mp38)`(yVU>7EoP8gYhvPq58vIlylqSs`;$y$_j40M( z_>STHR@TeB--i8^FRw?-D_72cs#e@;@#~?*KX=lfTbGrWkw!@Uv!5T96Yd2_Pm>ks zRm-jP5~PxUzW)A-X4Wd`_FMDAyk~6kS6;DPLudNPRC7s211+`U{2;S*ddAkGzv$~% zVjEG&x~a3z$jC4pzBTSTBGH&blJ@v6b~PDC!}7PKh45v^V?DN!k+;9{L=0KpBNg&S zU$y)xS(x7{j$1uvvQcNASUo=$YcHpG0NkGo-{-b*| zwo2oay`Vy6v8n6E%Ej|tG?&Bb%|fGJ^l)#_cywa)W_)`|yz#@i?4M&)S6#dzFp?}B zxA4_C@N|-jiuiCk38lytysp0t1pg5rj?1pOwdYQt@Z+bR@?{nKmVIZ)!0jQg5oL1A z_~na=+eCcmW&zUgiQ~Lf&m{7}5G(+oJ?WPF>)Ml*EN|YNEvJl{gVC>RRHo1QisGGb zuJUa5R_ihRny5Tmrn}-`y`Ex2O=At-_15d(hx~VMh^Iavk6oLG%VoEvAgq4o_C4lT z9XSN^JLP)il+C{_Dkl*8dG1)_!~0IWP@tqHB)tAHClbXZ6T?|=-NQ@p_(WP%6o{k^ z^h!zYydH_@YQMeTdkF`p2NSKVt?@UWvZrIIN7zyrTm7<-J$P~VdkZVCLL2S>SulY( z;qn!)#)oPSo)8few{>?@)6jUQrc&1+7MRfX!PAV~AofYcLN`@ffN*l;LH$Fn#=HH~ zCF_sP1uZJSW1Txvk&NKHB|4%w+nQ;7aPkv#sXAmQG%tbW?x$*mmDTw<2fAI2*v`;7 zdDNQ8g&r=5JiD-$E8{8)Zi^T$FY^huu{D{WyOQy2mPbVQ^gK_yS*@`j>CSX9sD_yS z*!c9Uz5Cvq<=yxTM1Dii;PQ+`@W)$5k}m3F9|sB7p0FEqlK|xbW6#Kq2P(mcX#*}d z1#!hs`KT|=Gj2JV91f3ITA@u%>}xPOE#9vwo|Aa$j8Gk2UpFvH50bt2Uw4VL=pxRM z@)zf$8ShJFGhW0;ze;QHhYz8pg3QEp+t1Rn9MZ6tYFeO<5p7d1R6+2OfJe9dQ=GQq z#-$IXn;$eTh%3zZZ@pP4eU>;~&`LH3iOlDVgIOQiwa{2OoyF9?%1oA+(uO1oyx%&e zovH}62W?~d5bhG-!)x@RpTo4yqPpGBaVM&9W;x8MH!`zcHh**y5W634=NsI57&l)~ z{lsQuym1d_p0-jo!EBMZR9z5ygrWgEcUS|~bKb1BVhyWLK64IHYEHE-JQ^Rr$8xu%fdv>94z6FjxX;H>yCX03cL*EuFl)5Wj7T^t2; z?no!A-qrp0WdTxYupAw<5Sl4dt zD^Gw7cYc0aTe|98C3l9d%B-r+vWkPOZ!A}|%Xh{JpZ(Ws7PmBv5&B$487O&oJ#=fp zAkq5k4R>v_Fk|wWi#WGO)8!$dq0OLe5`mi>k`~fxYTQ2IUJC}{1YkfU#BqcZ4(b8X z*HbT3L>d+v5C=Rq<`W*aL#O8~HP4&e4_I{(YX)iCx>WsXo2XwuaadtO;0IJ`_~a?i%hKY-blb2T zmBHLXq08~k?EuTt;@3{wR28LW^{$2Ny>V7Oyp$KMu{vZQbe(n^FqkZU_fkmmksie8 zRoOOp_2bg>`G=}8{a7#{2H#GIDL9AI`qE1|xd!-VI5u{@AMoaqks*mUEYq992QLX{<`=4a|W{>9O3`N=cGv1ji^bCikzJ~GxKtoSaDd#p$F!V938om;+0)XjUS4s$%0UD3$w zx0?y#BeoF~-8FK%6F={-|cYfEBm_6Z(KlR3_@rRQe0p6obE`i;xWUZ`1VzHDg zZ%y`m?^%k|@K2R}otV<-8XCe2!hTXS$X((lN3QaM@xg-!y^{k2Y`ME$tOcVgHAQNT-tc^r{ebN@n8)*Kf`a=gRzzkq@>#WUt(JiM4-a zQ@HI?CxtVKBnF)`2YR&WUPfHo1wRx(-ZkYVpO`dadN!vXF2RKyKZ_}-W9vj`&RoPv zb1C`fC+W1tWgF1DV+#lsk9-$MOrXs04ElysQ*o3va4=9M z$zhrd1k169J>fF`!LBb;|09iKhm7%t2oi_a0 zDor4nL$~+gOGmb?tCqaxFr}y{!j9EO|j%H8nY9s8orJNCo|5@@X=O&H| z<|}*x{M49|GSn?Y4GxT=*_T-%>4Qcw(li2}J`sdp3&xFZT*ok5I=EWeuWvlJUQw$0 z#8~kqdGuIIvIu$evhB{;mD&`}#0YpumDr-FB+n$meWzH)|M!E|+&VD&j_4Pset$Lk zp4pu^nmobv-TwIK~S|1V-@dLWWoJ%p*pTMOPY=cZj z3a?V2(n!a=-khw?24BJP#;&qWzAvl4egen}v!D8?dAp&1T(VWrJ#j9v7W=ym-F7s` z8}(YU1-86^xQuiKdKW%&vr-Y3L3eU^Qu<#Jh&d2|aF#CEd!$q0* za?{gM67uscK8xJozTsqjAc?pGQw+w~D@sk8ZD)nHq(6FiaM&lbwMXTziglCR;yZeM z?{u9(>sb8dxJoQp7y=<>!noN|Qs zPa{oEJgQ4nRQ4J5Z&Lr8dn>&UpQjD^NI&7O{Vw|AKQFq@y|R!Ub$&zcVM*<^_3z!kY&Ow9LZth_;d6e}%75SU84!j&yocDNq`c?vZ++-7 zN?F?H=+*s(JHdmtCwtLlu06R;-6ElMyVQ}DnLzWCwpVJI7Y|&LOM}WhcRjKZS3CkX5o%H zk|Guy<<-zonH;}Cc59LJ&tt8G;LWm|+=D~37~GdeBPewYAfXOz8ynP+ z9Fkv4+x6pC?!%dy^^|13CFD>3p^hj6rFJ`W{QS?8X}qYl9!_Y_cYa&3V;EdlibK@J zKT-_3-LZq9RKlYy(6zfST&=$*V?~twk~SiqE&f<+_jr^`E4g_Thw|a2D*20>b^)=6 zD^qjTCxbfAjU;9A|zUd^0!i?c%LH9Zzzag^(~xJ`KcysFKu?& zsjHKvRND8ZMr!HLzE7_bTOJ@T%z-~l4huL+UwCkCaB=Z6NCoFHD$^&XPbvWn|G9Bl z`sd_6OWgMQ`8H;SLiBz_YO32*$lBW4bnIszRbJz1M`B`Pn)w)=7d*yyM%+(uEt0+0w(m6I%5!_rb!#0v`9tmIK&!=wcNlO=maJnyExJ zVo6wI3%vYk^hPFcxHzxk16k`9p(|qm(>?L$T+G~0B$g;oy6Sg>BcsiPGQM;^jID24 zo-O0PFv{(&C8})go@}W`);X2?o7z@UD7{&G55=@_Y}6~OW9MR9RikR~{EVE!N!tI5 z50O(35(R!^-44xdXjM?zIf@qaAa{JJEV?pq!cSh(;@$1$#%NSE2t1hB?A>i4jwmr{ zmVG6@R$=?xwOXH5ivNhhk~o)2e?c5CYWeJqJ-6l07X(m@K9`rrH7kv7C3lZ8&Mo%> zYkSWS*%zw=54*+g9;hQ%5i%@b!Rit+8G$NX-Kw1#P#AnkcqxSCrp+W(eDyztah)@{ zhc!mJ&!ePC%%<{N)ndpd>5Uh1ST0uVZ^%Zy<2r&9W06us#6)cP8K%({Kwd@A)F6xZ z4tT|<4Og8@gZFdNHOGCYu2sZJqt=^&yQ-1ZL2!MSJzf<^Vw06;bNbs<2#WMZo)|Ev zaT$`Yd72*t3f;fLKXBs0r;vGL=ks>OO!ZXVc-B2VMn0_zZ}SwEDsgxo*(UShX9zDW zhssJ>;j8ZN)<xq^fuw;wT=2l?m@l4Idbl5{B#oW$EYKf6ia^^{(`P!x=gC&yS^- z8Uut_UV$`%Il2o4{BQ!JqPU+M+}Zl&3V(fSdfL4zo}@7U^ILn|PIgM*ODvVZ>|6a3 zW0z~4$Htajj>v8J5VtT5;-wG<>^fCzRtn60s4j)bmKYF}OBrc#=nkHtp`o%$O2Tj6 zP=Jl#16I~xm`@!rdcqeUP*TDU#1!<1QmU#*C{CK=VB48hEK`FQ2aT=^_xH~!);Ox% zG=b*1SED-`&!ZR*6uuY1QTxD;y6L+AoD?;h8o$|VAVp9*#r!w$qk~k!!iD6%maEuib_u4$C+Wz1gm7w*>OYs0s2+$3bo!m#&#aj1J>DdX5899$fsXndfy4@#SaGm=8?w$;c2cAM4=CPP*ioOdt!6?lcYGW-f_|#@z3L! z%ewWk{@F~f28*v@kfEum9)R^fOt*o9&N`n0JwCX<7{Jj7I-vOXxBVrvM(QUsZyqSv z<)>A|R!E;_+*)~&qLBZX!5{zXIZK7LHEyplb$ZB?PG&BtDzytn9+?eYnK?2NO|+qT zt;QZEgyYNF9DFsd5o_eOR{2_mvVfZ7ME4H>(g>XGm}4Zkd{Y{E_qoIV>HTkpVtki9(zSpZs! zoEV4@q1guPkF19QH``NErFrR2KDF>t`(i{MpR3Slt;=*45Iq7b|gfK^S*R| zYZfH@`zj5 z-eplc!db5lJLWOt6YIetozaua%vp>r`EO-=YoemfS`_64$Da&g*E-@>G&Il=Ep)!F z=m{6NPOtXpedzB4Mf=`4PFp)A@}IA2FZw@w=!PxO$Oe)5$`RIF@Zd_p>igc4M`{%> zYdk-@=8NoNEkt(o47XeN^TJTB&%gK8|3yBrL$!xwP_H0r*tCus2!3>Tla-d~o(n&@ z^>hp6ruUV1AON#|?sh+<`m)|w;!NG|?$C%KCN@2$OAC9nSFDuc6HY!ec#Q7(-nm`t z$XcMR?%)14CMi%X6+<(AT1Dqpr88^vLHzHgjf1$8I!&A;HYD&?-4wS041Rn4#v(4C ze+juB>=|fDe@ji%|Ka8o8fUzdHzU&FW}}Zr`_D4FDpXZA(A!%Fqn-;-+0l3!pQQ~to)+ID%z9IDcY$43{zk`aHr;rE zJQ@I}v+SnSMs1F?A%R-$l!W|@0Rb|r6<73Z;s@+z4*bu<;>ofO^8~`ABVFetD&F*(B|fUZ%75Lc$a(GVK;T)_-)38+BRtz4(2L<(7bipDB3ooM zNDrnV>FMc6y|d8udU+Y%W{5;_A6*K7jrRRJ{_D%(pTb223dYu-g3F7_!}HK=99-OT z@VgR|lq4y<5VbTc9dm>>$Q8qH&=kHn3b?rU=#lBsB~Hng1&vH>ifbt0jz=}FlDL8v ztpz#&y*@98DsM$fQk*_1(nkW3*Xl+3=)wf5oyEs}i#rwxsaZpL8L^F6)Ga$VU)#yy zbvP*}Ro-LrnZG+Rk7f7Su!OznAA&~Y5&3rSsCU`DIgdIw3e=%02Q22OyT9gh-`I8= z?ac19*h=N~bTpFlV%Cu^O;B_-Q@v@FjAo$fzk+PDQp(M;%r>WhZ*dS%o$&Z112x7& zj7WK&q$H-4l+VUgD#DK)fGfSW6W&AWj(@)idwj`8d!#&g#P8kfTxd0xUkgyO1IUG8YqXXxm_dFt9U2f>9`hrSm3 zMu7w+ZoOnY1J%r*?FlCg;r48wD5%fkU^MvyIN4f5J3aOn7f5=7M7t?%ZEc?(&j)kH zo-rta0cQc|JU~AAcpk!>!;uq-MG%X)va)h{9q23Lyy`Hx99Q?y=69brid6GS5Z`cKN04Ozi8q zMrpbId@DMk=qe$kn_51M{E@r*GzmkJw`z;}-@wjw+dB_{X z{@|IBxOGxxGFcKyFOQFMY zDmB}OFb>nxIdVhy>E|eWyGyvGj<}Y}8XxJ|?dZMee6UxzO)n)H$|$0l>MtSh!7Mo| zX|tyt&KSnAzviemq$!Z(lceC5*;QN;HF@BeuO z7W^3K)vQk~5B~bK1DG-hN}Wu(C;%&1<)qMh_HKNxrlyv#3BDU|O-wMK<^tmkNNcOC zygV{9GqY~a-~T2(J^j|P3o_CVe!8qm3i1|c`eZ9HO!rK~0gzBxNxbFEsi2^M>Usno z)2SI5bZ;@Q6!AtyMWyHF-du49PT_>D5x5iN@25y8Wrf1;!&QkQofZr~NmfrX0>Fcx zxVcZl-{oayCY6_S{eD8u>DAIAEFHsn+hP3IFX0z2UQAm;cMl&Qzih(oiar!-FJ4@= ztGrONgr|TXd3kwJ9I7WNT6_EYv^H#Z=EYW4S3k7$E{$9{cN%)H^LuR#ljy^U37Hk9 ze$j>6+kjR<_$_GHyth_RQo6SCw!U^iS5Q!J+7kZwS_eHH98t*9y%x|~tKa}NA^eZu z5B^-U<3av_!(2Kh1-}kT8TfU0cE2|kUw#b@d4GA6es|yLj6oEOAo)Y{$7e@xm3M`Y9irOjQbXZ-7hB4Tp#O&A+EFg6o;Nk1lk7 z+?As92tDkz8N`i2bgo(~Y~FLn65tXzmNs~@c8m)`%h}Qw`Rf-md?)CkdLox~I4ACs z9=Wq{3XANz%c&}UthOI4tGT=W);#U>Bf*s=OA=o34jFgh=5}A&i=BxWmt#Gl%^0|{ zuy@>9RK9+dp-^ed?|5oDv50(j=E*JVQ^Z&#KJLSyJW;LNc*($d z1Xc&U9Gni|f+Lyy7J_;H38Md;e+IXv?k=@6BRRb=TI~`c{CR==^fw|qoCy)`eTQy3 zTNiE9jy7c%!5L8{lvNBaKUX} zeZwGgaR(Ofvly>2^_x2GT6{{vZuWN<%O=dss`r(E#jS-N&j;nOs6=Q%pn7QD=aPr2 zH4O4Sz?QND^mmtW$DT2z@PRusi1I+d?fDUdK)ZD819f#<192!i2ID&_t#8RSME1*B z-|XEzwNV8vKIV|}xnXv-1_Y+DPsdu{csPFjI9&R~LBv-eL-&{9t-fkK3=O85>%8Yn z_hY21%xnGD*Gn@_&d!=lM?KAtS?;#jxKj5WQa24V42yY*B)&HUCebC@sP5cZnp3&-eCGw-jq#rSn-f6AMFEup(J;q_AYmeW}pHkXz(2|%F_ z`x9%qd*QvVa$_IIcuqrzt(?PgUKEHbmAhf0~Ie@SIQR#RC{PVLMR}F_|;C(2Uf-EgYy{4BiftkCB z0k@bnYbt?OlJlz4c=d9xnASN?hSr7COMV$;0;6NoVqK4d%Br=zWnDP#owG){#$PvZ zc-a$|i^*QH-qJf3qil_t6wjaeqjY(>ec74#aCWC$dPK{KNep>G-RBDKyldG8v@HP0 zKVge=ejGbTa{)4$WZa8+FWmCQ!uGga&}oRu3RE6`u;*J1dnZDaAn3Y2BG`DvN4J7K*+BZ0Kitc6u| zOzg4o;iB1Ys8hv;0s%$dQe*5twTZdZtX@OtNJ?OuczhCc#vH|VfB1EUs!9ckiFAkl zqfhe*DT}#1+xP6vGE$P(MiU?NI6ehrnpkrX9IZ)z=@!!53&g*wxSYEhpG0VH< zDD5g~LyxDmK@h#Dj!ASjA7G8Ca=4y+P%?HU0G|kpIXsubtzREHdV;rS`0U`9`$O1^ zf~yBf*Onh8XRR0eov5T-zN6oIsLxJiscClq+WMaFV4~6|V*i@86y+?f1)jowXv8A2QT|gq;2jAVxd8Wy!M?=~QEIdv*}I zz>Qn}%64GO-0EK3{s+7C|5nmn?$A6r6RBN_7rEfl)`}GukrlYDrGZckGhmE=_fuS- z`~N-!Xn2|UkK1IB3=C|!ldlbZ5E}Lu;~@SnX4WsQ%RRdE&R5$tuddQ6)nGgjw3xAa z<94VasaB%xxLJ*jYw<39w})nbY^o*y^*P>`mn{82U=HfPffbG9c9kc5+BV4cw`E`9 zOX$-SCY654rF?KF)M~}G_ZmxBb_$=HHO!OycIbfOSDZ(j>zCt~l?|gR?cakz=QSIf zG4Yme(*je29j2zy1MfILOk(X3Vs7`*(7H|L{_v2-yrM zTX=GIMLw75mX2L|eD}V63TfM(PD@it&(fhjJdL>4`|h85RGz*RWtA!T{Uza#O-DXw zrBc7un#>yexv(JBRz~ehh#THdk|*SL!U84m~@7Cupl&=B>O|r@<3}iIE27Z~V*<|gED`_&$3<2qz3 z&_?3&(4>C@{kHm-(dBhqib#k5IAoyfN*xVb;(kiEJdSGTk21W=qi94P?rarv4RzS+ z*WaJb1uRb%hQ^TMt8 zR3xoR*q_A!zc2|7l4g=|z&U1(E?+jO!dfz_Ief0v!Kiq2VtDkS|HAI`}staGU3lG0S!*j;)Q?bOl ztlrZcbJN5MCmEqF{~S7BL88nn)TY^K(ccGJUyev5r!Zyzi zo;JIhFCUg=%%>)lXt^I5&)ROk4_yhuYD?QEq3plKrTkB?9qKmdiu_T9fWRG#lT^^c z6ccZc&?7HaPS7H+S-klL0fk9DgG+|Iyw6x!AT+;zE-p&{{7uP98a&bEO~X9`EHv;` z{-qJEHZGP?fqK;pKj9!0ih0iZMrRDEFLT`FN!#GOTwC*dD7#C8XQSZAGr-9jxSIgk zQCXrbOBODvr*6k{F|kf|33-ik(;jJz*LUp2nZG?|GSp1SHq-2T+LzRqT0H;qugLu< z@Go`Mwtf&DXT*kksIPLLkcov+>B~*&VTAOqXRIU)u955) z5QTa)bKS`Py>HCTz(bbfv_@ql_{P@BVYiN%#Ekr-V>wflJKR3Na>TyiY?B9>O{}FNl*hit1 zmj9q3rh9*Ghb;nIaEmG-CgN62n!xq&oZ&G}4KgnQ-M%r1+LEL|)0Mp$Wtqlq~d z5YBarD@;7TeVM~H(s$z3*rSt*w|32b+u5My$SIG?isyg_a`;5YVp7+0*Sv8js_%=V zbvhqLpIHi6z7*5YbRi>J*gH?*wQ;JJ}^j>>F7=V zj{kx8#x7EpK19ek>BDFke>Ebc)hP7UoixV9&D9IONWs6#Z7cL|@BrUHRwkN?)jy0q z4onD}+fwEG*31yySzrXpq@sT=bP>+{=ughc@k1JDK-2N5po1Wm@L!>B*dsOJSFc7r z14LM=EgW4)GB@@A-ZsbIN{GXUI9BH5zaVj4!F~Qn08yRdYXpo)vEXS*xc`Yde+>2I z{@3Gt93o=@*Yr3QZIx$kCtd_BrN(yfGo2c|9NS&!*)?2Ed#||i8TYeKpXenO~=&8ohW~FhDsV+V{?kVQqBe3ueBO?TD^HEpy z>VT1w9_P(!?;eigy9o;P-Vw!EyvSKF*Tvz!*6TEIMPQI?SK z4kqLRA$|y=2!Q?o2}OuFm7kwqNLZMhrY4yUGTNPE!Uk7(Ztvn5Q>25W9S3>bZ%hYv zQgddRm3_17d*Sh>DvvXUBIx0@+zhyq%zorDNvpv!)294eHoU4s#uZOb=GcDif~_9+^I(5} z|7U8Jmdwz0BMvRs29n;q=9n5@5mqr3C8Y%biQB{stPdz3^y}Ld=2bkH9%`v9hu9j7wumrikbNqw6c9s_NRc0cj~|Nl_Z4ySp0%Bt^PYx+JAL zrE7yogM>6lcQ;a!N_Ty8`#kS^&iV0;v47|f$8gOx*PQo#U!~D4KSl**Vjz0xwX7Zq zfq5O1ikeouwwRp8KpIKwU$N382Dm73plC|`C+Z>e4j)(kU!?pyE7f?5NaXLD57?>? zAAMBF7a#83o4&J_>jaD8G<`{&-QOaAypn!lEL*1O3~ab-9M(FYDJF%k0~!<*UuppT zDsTmC1TB+kScZuLU=+*&D1F6Xv`DkYd1?FZ{@mR0IsomAXc|7!;f5k8XGQJs(t+p& z&#-)Yuedci>fUW=%wzj1CK7QF4RJi3EaUN0rLa?4S$c|Be8xl0L4W`AapC{6U^zKC zVW8?Vxn7DmG>IP5RHMTS;*TjU>)eQ#?BWhu`lOw|QCP3a@7GDS-43Oa5+(kF6a)mc zKLF89C`gAUIC?5yzvT23WC(N_Y->2aH8dRE08v1LTX64yjVB1(a3vX4U`u!gEh4wV zS~%~V5BnW6_U>{{JhHV7aJmodmV5>_ zHe4LeODM=12acKspo?a&ar`4~f72Dr_PA{WQ)k$vypRDR36Nu^_f zcjM+#Yy73=*SHFUW}Ydz^h=Rvls~3T@YDzd|8!gDt&GpadrbSgZrj_ATOC=IlO^>^qvdF7tXWlPg zULi|d)&SK^V4+c8>!HCTQ`pVJ#5_(B&Yj7jGF;IbD0qjWKS$~Co*GF1e|Emp!d0`% zsx`8~1*(f?%%6s@&bL14@e$NNKoQ1e{FlmV)A3dlzj_riuJSoa@`EoK9f5wgdBt}O zZ=o+0E%;hnv{A!Ixv%?Z>=(HV+>a$O$c50_0b}Lrz+9NWeEVhE{QvOEp10V!27K(C zB1D~x{FseU!rDFb;*lCAVRDL@>m20iRU~i=ol2>+rIZ@{x;^w0E>^6VP*2pxrR2fu zCdg3@PTrM+wytb{LAki~c(BLNM361`SL~5ju8w!2<>_|_(yK>4izXyYJV0H?;>dCd={sHXy5IsR9HYVSz zb)&2~E&g8{VC6r>OsK59?=W#w?$c)5IC}yQS&4+y623oYmu7MF(6@NAkn`ndp@6x+@ApV%?D3@N?;#IZ5YNQM%pBF`-{%VnMKc3ky5Vk#Y&!%V)vRe{Y+jHsG_nemo34X~0lt}0Ml|1(W>Jg2X5JP{f`bgWtre9=8!-5#DZ)%b1AcP-8sn>m|T*Jl} zj7B&FS&Pv#lAQ3(3p4O6oIUh4T(uBUl(zJ@h7>c1U2II;H$48>cB1FH;1LhG!uZWcWTaZBa8=OQs+5nXkAD5r3T# z1rQjr+KC^50ksV)D~WD{zkKTn9E3(f!Q)aFc;9mM({i;}yS89=g>w0kS#+O>sT2&zcb5R43;L}n#V!+@9 z&ekBCAc@UAxV3sj#gOM?4(ROCvxpvi)>l7XEN-uk2yQR4djw{|{0}1=XvPYmRT*Xc zq;!60-iL?b%iHni0k=yk=gEGH57`@B@P`1n0n|r;dj(DIZeDL^L#nH>{e#N?ux1|a zbRskek{NjY=8Ps69)ZdN04xGz4Jm_o4OtRw2H;_PB_-(uS}!Seh0A;S+Bf-iAnzyF zZS3@uiTkvPFj2;Jl1F4sb~CzjToZ;BO+Ont(AytJC?}QSrz=FWX0|P_=Pgh4x9*Mn z+?TRML{_>FS*MfIm-wUv2+#iL2*lbMO+2(+yV~2VLrl&xt6x*U#(v{BXnO(*YFRbe zb^!rSdd`;y%+L>7f@#9JBt_;}&&tvg7K%ImTm3j8Iw7KW_+8Xwubp0x?Yekx5U>6` z?j^@$M0m<#J;SI8#;K=XtG(uIQSMBpu3WcvjcMr#G*3ofy%x*_<;CgU^{8KuRUO<> zKKVWJ7G-LZycv@wB-E#n2AEHpKxotx%_u!lwVd`H8%k{I&Tjb5Q|35b7HKx`91#^O z!`v(C)pcx4z$1YV|D7c=_Gd5@xt`n;uA((n&B-3N-2fe<2-<+T{1gph(o_;3!e}%49wzLPjm7tg^=zR@+JMt+M0)k=A$!W)Q|Num1!#!`l?b3PW4${uJL{oH|L)w1V~DV=5RzGtWZ5`GQuZ3 zeKIE^O?q}A=Z~2{WxJTF_>J{*U86N@+fR+>ibq2ZpzVSC8wqTDAr3KApd-0p&%SF`Z-6K?$)E*cXvASl%yQ5e5L+ zgkMpM;GBzgiN?wNsBdzk^KF!?a-CBK8&h;%ix-^K3Lqwt+|Ja~F+ROee&)|4Oy1QQ zyhFg$Ug<#5)aa~g^=fbobmmXFQbPB*7`C27b~ES>XB^bBK0D;ducJqWE1mfP@10q_ zj!v62spUcxj^Tv_J&|0KoxXNuuW0~{+OFHi67luJ^(v;-ZO5%}&Wawnh8j1G9fsA` zT$}R6zmvWMzmcJsLG%4OUDFHb-6_HIzqmTs{kSYT;zu9R*x*-hehGnVoMdV%oT2<( ze>~Mu1mjT?+;2fn9U|9W43CAcPrW`3|2~Y68;r{@Rq^2nj~9vxnO7G2Xf&Ruewid= zhJ{g|@`bDIqq||Ux@1IfItD{4iaV0kSaE=6**;EIHBO$2WbBTKQ##LzN3)^B9^+QT z)7HK^gjvM%?>2*`9NGa|wNoW7xHPy(@DL!9KwW=2_C*T6+mQd5Xw~;{QPj4r@rs+x zOBQrjFG>ELB=PaxmDU4msLVWw1Q4}4(mlr$I^03)Kr#wr`1M2`)pT=G>7~c@^%98U ze85i6E`$%0??jreDEzrM4xcGbyia|2^6ee7V-o%BYaGdPXIP_76QNO~rmW#L53A%028|9*#sCK|MFVx3$4bZ;kBi3565qiaGo zzOMN!$ch-x`RBFH*&_=f2V<+@r&`Y8G{4szfIE$in4(LPOE&c3^s}}rGScLNaq~Wo zp!~iJ*jF9CS1i$8(nq|cqMg|O_di7s{?d5!MLyFDk==I&6XG+8w6#B~XU4N7Ysq4F zfQM9rXtHt(CO7VOqy+(ZBpt)rFIl~!#Qno}gnGBLyoAY(4GUvTX=1N{1wc{)Y?zOX zkURtN>TbY&pA} z!@px*Ufk?8Y;o;TJ{Rz68Tt4G32r#W(3cl@8KDbp``h2$THG9xPu?HYKuC)CUIm_! zKXUiHDOI}9pnMjU!4TT}``od*OUl&#t-eR8@J30~aQNUp|2c}%N2$VhBQWD7prbI- zL90o~8~*~S-`TuqDmf@eqc71-u8IVpx|K>>#laow;dGnZz^SQ}d9`g9mrEC8^^3R+ zluhq{+!;)N#lxpQd}It$08mknq8>hD%VZlm2YkgGtlj=tueNTe4OvDDhu#c!JHXnZ zP75I+A+f&VNlk@5hu^zx=lnPNj1EF$e)6~=&-N!!+zTiw7H@0>E_l201GkL`-v_v} zrvy37bfP7*n1V90-7mHW*2atPOzl7kuJ zR#k};&&W}#e~gsxFIaOuIz(!f*O~PVFQ!IZEXKeQ+WB(gcO*+&iB89aQW&yI)EYR9 z?>zz-o~=MBoH66jLX8uhFI`106mWk;K?2rdcPHm6{|W7f`?oOPVuaT=6}ivJi#78Z z=({W5wWEGOK%kvE;m?y18%e?mouw&0tUgbkJtq#+`leI)y=6nFO$`>jQaYKaT-xF6 zEsahcz4_D^IJd819EaeBHrMK1g27EdXD8Y}sxLyrJAp0AelnK^_Ls zDYK2_y~y>~E{k;RzluEtQ#fc339b(q9i2OS#3C;d`Ik?@G*nhRe_LIbi0KDkBC_23 zGLv+n_e@EY?};v0g0ZomO5^7>3mJ-NhsVV9zOVXu><)K+1_`8Rir(283^QmI82h!m zhl-sHLlTOKK)!SQQ$REPRGaQBd4cZC^9+nut{O+5dClKUk|j`j$2xCy=c>{7DTY&X z)1}LA7a}b92#Jc!>$n=K$vRI~3uj{d)-G-H~A)@pi7Xge;gIm_s z!73oyx<@F<1;?L^oT#J9W9_wQn3q4@om|_==^F0<`Zdwt!n()oT(~yKR$fI1&nNOG z`=a})2UCMZZF%l#UZy?ky9V|*Q0dINS`aIsE&y?m1a<&;@h0oXSNADXI;7Tiw|p@I zC&>Jso-(oGW0Xx{G}QU5%>R-vcq36;!@#hNgK zQiJ>Zc`!(Od^eG!jeD^IHH&dDkS=0`d+K>>hYB~={Bnkek1JGClJTnzfz42((}^lQ z-jH16d(HQ_DMO0rwNEK5*mw~WXV1;~(=zjq-1mRb

    #DDTYRI%il^81;bV2p!qUG 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 P;p@1vx>Bmb>8t+>OfOh3 literal 0 HcmV?d00001 diff --git a/example/ck_tile/50_sparse_attn/sparge_blockmap_inst.cpp b/example/ck_tile/50_sparse_attn/sparge_blockmap_inst.cpp index a2df5bac569..3cc674f181f 100644 --- a/example/ck_tile/50_sparse_attn/sparge_blockmap_inst.cpp +++ b/example/ck_tile/50_sparse_attn/sparge_blockmap_inst.cpp @@ -5,6 +5,9 @@ #include "sparge_blockmap_trek.hpp" #include "ck_tile/ops/fmha/block/variants.hpp" +#include +#include +#include #include // ============================================================================ @@ -61,6 +64,9 @@ using bmap_fp16_problem = ck_tile::BlockFmhaPipelineProblem; using bmap_fp16_kernel = ck_tile::SpargeBlockMapKernel; +using kstats_fp16_pipeline = ck_tile::SpargeKStatsPipeline; +using kstats_fp16_kernel = ck_tile::SpargeKStatsKernel; + // ============================================================================ // bf16: D=128, kM0=64, kN0=128 // ============================================================================ @@ -112,6 +118,78 @@ using bmap_bf16_problem = ck_tile::BlockFmhaPipelineProblem; using bmap_bf16_kernel = ck_tile::SpargeBlockMapKernel; +using kstats_bf16_pipeline = ck_tile::SpargeKStatsPipeline; +using kstats_bf16_kernel = ck_tile::SpargeKStatsKernel; + +// ============================================================================ +// Internal K-stat workspace (R20): process-lifetime lazy hipMalloc, sized +// to the largest (batch, nhead_k, N_k, D) seen so far. Caller API unchanged. +// ============================================================================ + +namespace { + +struct KStatsWorkspace +{ + void* pooled_k_dev = nullptr; // [batch, nhead_k, N_k, D] fp32 + void* sim_k_dev = nullptr; // [batch, nhead_k, N_k] uint8 + size_t pooled_k_bytes = 0; + size_t sim_k_bytes = 0; + + void ensure(int batch, int nhead_k, int N_k, int D) + { + const size_t need_p = static_cast(batch) * nhead_k * N_k * D * sizeof(float); + const size_t need_s = static_cast(batch) * nhead_k * N_k * sizeof(uint8_t); + if(need_p > pooled_k_bytes) + { + if(pooled_k_dev != nullptr) (void)hipFree(pooled_k_dev); + (void)hipMalloc(&pooled_k_dev, need_p); + pooled_k_bytes = need_p; + } + if(need_s > sim_k_bytes) + { + if(sim_k_dev != nullptr) (void)hipFree(sim_k_dev); + (void)hipMalloc(&sim_k_dev, need_s); + sim_k_bytes = need_s; + } + } +}; + +KStatsWorkspace& g_kstats_ws() +{ + static KStatsWorkspace ws; + return ws; +} + +template +void launch_kstats_then_blockmap(sparge_blockmap_args args, const ck_tile::stream_config& s) +{ + const int N_k = ck_tile::integer_divide_ceil(args.seqlen_k, BlockMapKernel::kN0); + const int D = BlockMapKernel::D; + auto& ws = g_kstats_ws(); + ws.ensure(args.batch, args.nhead_k, N_k, D); + + // Stage 1: K stats + { + auto [kargs, grids] = + sparge_kstats_create_kargs_and_grids(args, ws.pooled_k_dev, ws.sim_k_dev); + const dim3 blocks = KStatsKernel::BlockSize(); + constexpr ck_tile::index_t kBlockPerCu = KStatsKernel::kBlockPerCu; + ck_tile::make_kernel(KStatsKernel{}, grids, blocks, 0, kargs)( + ck_tile::stream_config{s.stream_id_}); + } + // Stage 2: block_map (reads ws) + { + auto [kargs, grids] = sparge_blockmap_create_kargs_and_grids( + args, ws.pooled_k_dev, ws.sim_k_dev); + const dim3 blocks = BlockMapKernel::BlockSize(); + constexpr ck_tile::index_t kBlockPerCu = BlockMapKernel::kBlockPerCu; + ck_tile::make_kernel(BlockMapKernel{}, grids, blocks, 0, kargs)( + ck_tile::stream_config{s.stream_id_}); + } +} + +} // namespace + // ============================================================================ // Dispatch // ============================================================================ @@ -122,26 +200,20 @@ float sparge_blockmap_fwd(sparge_blockmap_traits traits, { if(traits.data_type == "fp16" && traits.hdim_q == 128) { - using k_ = bmap_fp16_kernel; if(s.log_level_ > 0) std::cout << ", sparge_blockmap_fp16_d128" << std::flush; - auto [kargs, grids] = sparge_blockmap_create_kargs_and_grids(args); - const dim3 blocks = k_::BlockSize(); - constexpr ck_tile::index_t kBlockPerCu = k_::kBlockPerCu; - return ck_tile::launch_kernel( - s, ck_tile::make_kernel(k_{}, grids, blocks, 0, kargs)); + return ck_tile::launch_kernel(s, [=](const ck_tile::stream_config& s_) { + launch_kstats_then_blockmap(args, s_); + }); } if(traits.data_type == "bf16" && traits.hdim_q == 128) { - using k_ = bmap_bf16_kernel; if(s.log_level_ > 0) std::cout << ", sparge_blockmap_bf16_d128" << std::flush; - auto [kargs, grids] = sparge_blockmap_create_kargs_and_grids(args); - const dim3 blocks = k_::BlockSize(); - constexpr ck_tile::index_t kBlockPerCu = k_::kBlockPerCu; - return ck_tile::launch_kernel( - s, ck_tile::make_kernel(k_{}, grids, blocks, 0, kargs)); + return ck_tile::launch_kernel(s, [=](const ck_tile::stream_config& s_) { + launch_kstats_then_blockmap(args, s_); + }); } if(s.log_level_ > 0) @@ -160,23 +232,13 @@ void sparge_blockmap_fwd_oneshot(sparge_blockmap_traits traits, { if(traits.data_type == "fp16" && traits.hdim_q == 128) { - using k_ = bmap_fp16_kernel; - auto [kargs, grids] = sparge_blockmap_create_kargs_and_grids(args); - const dim3 blocks = k_::BlockSize(); - constexpr ck_tile::index_t kBlockPerCu = k_::kBlockPerCu; - ck_tile::make_kernel(k_{}, grids, blocks, 0, kargs)( - ck_tile::stream_config{s.stream_id_}); + launch_kstats_then_blockmap(args, s); return; } if(traits.data_type == "bf16" && traits.hdim_q == 128) { - using k_ = bmap_bf16_kernel; - auto [kargs, grids] = sparge_blockmap_create_kargs_and_grids(args); - const dim3 blocks = k_::BlockSize(); - constexpr ck_tile::index_t kBlockPerCu = k_::kBlockPerCu; - ck_tile::make_kernel(k_{}, grids, blocks, 0, kargs)( - ck_tile::stream_config{s.stream_id_}); + launch_kstats_then_blockmap(args, s); return; } diff --git a/example/ck_tile/50_sparse_attn/sparge_blockmap_trek.hpp b/example/ck_tile/50_sparse_attn/sparge_blockmap_trek.hpp index 6eaeb9ea77b..92c32d29e85 100644 --- a/example/ck_tile/50_sparse_attn/sparge_blockmap_trek.hpp +++ b/example/ck_tile/50_sparse_attn/sparge_blockmap_trek.hpp @@ -8,7 +8,9 @@ #include "ck_tile/ops/fmha/pipeline/block_fmha_pipeline_problem.hpp" #include "ck_tile/ops/fmha/pipeline/tile_fmha_shape.hpp" #include "ck_tile/ops/sparse_attn/pipeline/sparge_blockmap_pipeline.hpp" +#include "ck_tile/ops/sparse_attn/pipeline/sparge_kstats_pipeline.hpp" #include "ck_tile/ops/sparse_attn/kernel/sparge_blockmap_kernel.hpp" +#include "ck_tile/ops/sparse_attn/kernel/sparge_kstats_kernel.hpp" #include "fmha_fwd_trek.hpp" @@ -45,6 +47,15 @@ struct sparge_blockmap_args void* block_map_ptr; void* lut_ptr; void* valid_block_num_ptr; + + // R21A Phase 4 + R21B fix: optional per-head superparams. nullptr => use scalar. + // Buffer sizes match SpargeAttn upstream contract (utils.py:324-328: all sized + // by Headnum=q.size(1)=nhead_q). K-side kernel still indexes [hk] into the + // first nhead_k entries — for MHA equivalent to old [nhead_k] sizing, for + // MQA/GQA aligns to upstream tuned ckpt layout. + const float* simthreshd1_per_head_ptr = nullptr; // size = nhead_q floats (kernel reads [0..nhead_k-1]) + const float* cdfthreshd_per_head_ptr = nullptr; // size = nhead_q floats + const float* topk_per_head_ptr = nullptr; // size = nhead_q floats }; struct sparge_blockmap_traits @@ -57,7 +68,9 @@ struct sparge_blockmap_traits // Create kernel args and grid dimensions // ============================================================================ template -auto sparge_blockmap_create_kargs_and_grids(sparge_blockmap_args args) +auto sparge_blockmap_create_kargs_and_grids(sparge_blockmap_args args, + const void* pooled_k_ws_ptr, + const void* sim_k_ws_ptr) { assert(args.nhead_q % args.nhead_k == 0); auto kargs = BlockMapKernel::MakeKargs(args.q_ptr, @@ -79,12 +92,38 @@ auto sparge_blockmap_create_kargs_and_grids(sparge_blockmap_args args) args.scale, args.block_map_ptr, args.lut_ptr, - args.valid_block_num_ptr); + args.valid_block_num_ptr, + pooled_k_ws_ptr, + sim_k_ws_ptr, + args.topk_per_head_ptr, + args.cdfthreshd_per_head_ptr); dim3 grids = BlockMapKernel::GridSize(args.batch, args.nhead_q, args.seqlen_q); return ck_tile::make_tuple(kargs, grids); } +template +auto sparge_kstats_create_kargs_and_grids(sparge_blockmap_args args, + void* pooled_k_ws_ptr, + void* sim_k_ws_ptr) +{ + assert(args.nhead_q % args.nhead_k == 0); + auto kargs = KStatsKernel::MakeKargs(args.k_ptr, + args.seqlen_k, + args.hdim_q, + args.nhead_k, + args.stride_k, + args.nhead_stride_k, + args.batch_stride_k, + args.simthreshd1, + pooled_k_ws_ptr, + sim_k_ws_ptr, + args.simthreshd1_per_head_ptr); + + dim3 grids = KStatsKernel::GridSize(args.batch, args.nhead_k, args.seqlen_k); + return ck_tile::make_tuple(kargs, grids); +} + // ============================================================================ // Hand-written template instantiation dispatch // ============================================================================ diff --git a/example/ck_tile/50_sparse_attn/test_sparge.cpp b/example/ck_tile/50_sparse_attn/test_sparge.cpp index 81a49ca006b..4c97a10d0f0 100644 --- a/example/ck_tile/50_sparse_attn/test_sparge.cpp +++ b/example/ck_tile/50_sparse_attn/test_sparge.cpp @@ -105,7 +105,10 @@ auto create_args(int argc, char* argv[]) .insert("seed", "42", "random seed") .insert("warmup", "5", "warmup iterations") .insert("repeat", "20", "benchmark iterations") - .insert("kname", "0", "print kernel name"); + .insert("kname", "0", "print kernel name") + .insert("perhead", "0", + "R21A Phase 4: 0=scalar (default), 1=per-head [H] superparam test " + "(varies topk[h] = topk * (1 + 0.5*(h - H/2)/H), simthreshd1 unchanged)"); bool result = arg_parser.parse(argc, argv); return std::make_tuple(result, arg_parser); @@ -135,6 +138,7 @@ bool run_test(const ck_tile::ArgParser& arg_parser) int warmup = arg_parser.get_int("warmup"); int repeat = arg_parser.get_int("repeat"); int kname = arg_parser.get_int("kname"); + int perhead = arg_parser.get_int("perhead"); if(nhead_k < 0) nhead_k = nhead; if(seqlen_k < 0) seqlen_k = seqlen_q; @@ -231,6 +235,33 @@ bool run_test(const ck_tile::ArgParser& arg_parser) bmap_args.lut_ptr = (pipeline == "vsa") ? lut_dev.GetDeviceBuffer() : nullptr; bmap_args.valid_block_num_ptr = (pipeline == "vsa") ? valid_bn_dev.GetDeviceBuffer() : nullptr; + // R21A Phase 4 + R21B fix: per-head superparam buffers, all sized [nhead_q] + // to match SpargeAttn upstream contract (utils.py:324-328, Headnum=q.size(1)). + // K-side kernel reads only the first nhead_k entries via [hk]. + ck_tile::DeviceMem topk_per_head_dev(static_cast(nhead) * sizeof(float)); + ck_tile::DeviceMem sim1_per_head_dev(static_cast(nhead) * sizeof(float)); + ck_tile::DeviceMem cdf_per_head_dev (static_cast(nhead) * sizeof(float)); + if(perhead != 0) + { + std::vector topk_h(nhead); + std::vector sim1_h(nhead); + std::vector cdf_h (nhead); + for(int h = 0; h < nhead; ++h) + { + // small per-head jitter around scalar topk so sparsity differs by head + const float jitter = 0.5f * (static_cast(h - nhead / 2) / nhead); + topk_h[h] = topk * (1.0f + jitter); + sim1_h[h] = simthreshd1; // bit-identical to scalar (kernel reads [0..nhead_k-1]) + cdf_h[h] = cdfthreshd; + } + topk_per_head_dev.ToDevice(topk_h.data()); + sim1_per_head_dev.ToDevice(sim1_h.data()); + cdf_per_head_dev .ToDevice(cdf_h.data()); + bmap_args.topk_per_head_ptr = static_cast(topk_per_head_dev.GetDeviceBuffer()); + bmap_args.simthreshd1_per_head_ptr = static_cast(sim1_per_head_dev.GetDeviceBuffer()); + bmap_args.cdfthreshd_per_head_ptr = static_cast(cdf_per_head_dev.GetDeviceBuffer()); + } + // ---- build attention args ---- ck_tile::stream_config stream_cfg; stream_cfg.stream_id_ = nullptr; diff --git a/include/ck_tile/ops/sparse_attn/kernel/sparge_blockmap_kernel.hpp b/include/ck_tile/ops/sparse_attn/kernel/sparge_blockmap_kernel.hpp index ca177abf23a..62b5b3591c0 100644 --- a/include/ck_tile/ops/sparse_attn/kernel/sparge_blockmap_kernel.hpp +++ b/include/ck_tile/ops/sparse_attn/kernel/sparge_blockmap_kernel.hpp @@ -52,7 +52,20 @@ struct SpargeBlockMapKernel void* lut_ptr; void* valid_block_num_ptr; + // R20 K-stat workspace from Kernel A + const void* pooled_k_ws_ptr; // [batch, nhead_k, N_k, D] fp32 + const void* sim_k_ws_ptr; // [batch, nhead_k, N_k] uint8 + index_t N_k; + + // R21A Phase 4: optional per-head topk (size = nhead_q floats). + // nullptr => use scalar `topk` for all heads. + const float* topk_per_head; + + // R21B: optional per-head cdfthreshd (size = nhead_q floats). + // nullptr => use scalar `cdfthreshd` for all heads. + // Only consulted on topk<=0 path; bench currently always uses topk path. + const float* cdfthreshd_per_head; }; CK_TILE_HOST static constexpr auto MakeKargs(const void* q_ptr, @@ -74,7 +87,11 @@ struct SpargeBlockMapKernel float scale, void* block_map_ptr, void* lut_ptr, - void* valid_block_num_ptr) + void* valid_block_num_ptr, + const void* pooled_k_ws_ptr, + const void* sim_k_ws_ptr, + const float* topk_per_head = nullptr, + const float* cdfthreshd_per_head = nullptr) { const index_t N_k = integer_divide_ceil(seqlen_k, kN0); return Kargs{q_ptr, @@ -97,7 +114,11 @@ struct SpargeBlockMapKernel block_map_ptr, lut_ptr, valid_block_num_ptr, - N_k}; + pooled_k_ws_ptr, + sim_k_ws_ptr, + N_k, + topk_per_head, + cdfthreshd_per_head}; } CK_TILE_HOST static constexpr auto GridSize(index_t batch, index_t nhead_q, index_t seqlen_q) @@ -174,6 +195,21 @@ struct SpargeBlockMapKernel // Shared memory __shared__ char smem[Pipeline::GetSmemSize()]; + // R20 K-stat workspace: pre-offset for this (b, hk). + const index_t nhead_k = kargs.nhead_q / kargs.nhead_ratio_qk; + const index_t khead_off = (b * nhead_k + hk) * N_k; + const auto* pooled_k_ws = + reinterpret_cast(kargs.pooled_k_ws_ptr) + khead_off * D; + const auto* sim_k_ws = + reinterpret_cast(kargs.sim_k_ws_ptr) + khead_off; + + // R21A Phase 4: per-head topk if provided, else scalar broadcast. + const float topk_eff = + (kargs.topk_per_head != nullptr) ? kargs.topk_per_head[hq] : kargs.topk; + // R21B: per-head cdfthreshd if provided, else scalar broadcast. + const float cdfthreshd_eff = + (kargs.cdfthreshd_per_head != nullptr) ? kargs.cdfthreshd_per_head[hq] : kargs.cdfthreshd; + Pipeline{}(q_window, k_window, kargs.seqlen_q, @@ -182,12 +218,14 @@ struct SpargeBlockMapKernel N_k, kargs.nhead_ratio_qk, kargs.simthreshd1, - kargs.cdfthreshd, - kargs.topk, + cdfthreshd_eff, + topk_eff, kargs.scale, bmap_ptr, lut_out, valid_out, + pooled_k_ws, + sim_k_ws, static_cast(smem)); } }; diff --git a/include/ck_tile/ops/sparse_attn/kernel/sparge_kstats_kernel.hpp b/include/ck_tile/ops/sparse_attn/kernel/sparge_kstats_kernel.hpp new file mode 100644 index 00000000000..3ce494f8702 --- /dev/null +++ b/include/ck_tile/ops/sparse_attn/kernel/sparge_kstats_kernel.hpp @@ -0,0 +1,136 @@ +// Copyright (c) Advanced Micro Devices, Inc., or its affiliates. +// SPDX-License-Identifier: MIT +#pragma once + +#include "ck_tile/core.hpp" +#include + +namespace ck_tile { + +// Kernel A wrapper: grid (N_k, nhead_k, batch). Each work-group precomputes +// K-block stats (pooled_k_mean[D], sim_k) for one (b, hk, kb) into a workspace +// that Kernel B (block_map) reads instead of recomputing per Q-block. +template +struct SpargeKStatsKernel +{ + using Pipeline = remove_cvref_t; + + static constexpr index_t kBlockSize = Pipeline::kBlockSize; + static constexpr index_t kBlockPerCu = Pipeline::kBlockPerCu; + + using QDataType = typename Pipeline::QDataType; + using KDataType = typename Pipeline::KDataType; + + static constexpr index_t kN0 = Pipeline::kN0; + static constexpr index_t D = Pipeline::D; + + static constexpr index_t kAlignment = 16 / sizeof(KDataType); + + struct Kargs + { + const void* k_ptr; + + index_t seqlen_k; + index_t hdim_q; + index_t nhead_k; + + index_t stride_k; + index_t nhead_stride_k; + index_t batch_stride_k; + + float simthreshd1; + + void* pooled_k_ptr; // [batch, nhead_k, N_k, D] fp32 + void* sim_k_ptr; // [batch, nhead_k, N_k] uint8 + + index_t N_k; + + // R21A Phase 4 + R21B fix: optional per-head simthreshd1. + // Buffer is sized [nhead_q] floats to match SpargeAttn upstream contract + // (utils.py:324, Headnum=q.size(1)). Kernel only indexes the first + // nhead_k entries via [hk]. nullptr => use scalar `simthreshd1`. + const float* simthreshd1_per_head; + }; + + CK_TILE_HOST static constexpr auto MakeKargs(const void* k_ptr, + index_t seqlen_k, + index_t hdim_q, + index_t nhead_k, + index_t stride_k, + index_t nhead_stride_k, + index_t batch_stride_k, + float simthreshd1, + void* pooled_k_ptr, + void* sim_k_ptr, + const float* simthreshd1_per_head = nullptr) + { + const index_t N_k = integer_divide_ceil(seqlen_k, kN0); + return Kargs{k_ptr, + seqlen_k, + hdim_q, + nhead_k, + stride_k, + nhead_stride_k, + batch_stride_k, + simthreshd1, + pooled_k_ptr, + sim_k_ptr, + N_k, + simthreshd1_per_head}; + } + + CK_TILE_HOST static constexpr auto GridSize(index_t batch, index_t nhead_k, index_t seqlen_k) + { + const index_t N_k = integer_divide_ceil(seqlen_k, kN0); + return dim3(N_k, nhead_k, batch); + } + + CK_TILE_HOST static constexpr auto BlockSize() { return dim3(kBlockSize); } + + CK_TILE_DEVICE void operator()(Kargs kargs) const + { + const index_t kb = static_cast(blockIdx.x); + const index_t hk = static_cast(blockIdx.y); + const index_t b = static_cast(blockIdx.z); + + const auto* k_base = reinterpret_cast(kargs.k_ptr) + + b * kargs.batch_stride_k + hk * kargs.nhead_stride_k + + kb * kN0 * kargs.stride_k; + + const auto k_dram_naive = make_naive_tensor_view( + k_base, + make_tuple(kargs.seqlen_k - kb * kN0, D), + make_tuple(kargs.stride_k, 1), + number{}, + number<1>{}); + const auto k_dram = pad_tensor_view( + k_dram_naive, make_tuple(number{}, number{}), sequence{}); + + auto k_window = make_tile_window(k_dram, + make_tuple(number{}, number{}), + {0, 0}, + Pipeline::MakeKBlockDistribution()); + + const index_t N_k = kargs.N_k; + const index_t khead_off = (b * kargs.nhead_k + hk) * N_k; + auto* pooled_k_out = reinterpret_cast(kargs.pooled_k_ptr) + (khead_off + kb) * D; + auto* sim_k_out = reinterpret_cast(kargs.sim_k_ptr) + (khead_off + kb); + + __shared__ char smem[Pipeline::GetSmemSize()]; + + // R21A Phase 4: per-head simthreshd1 if provided, else scalar broadcast. + const float simthreshd1_eff = (kargs.simthreshd1_per_head != nullptr) + ? kargs.simthreshd1_per_head[hk] + : kargs.simthreshd1; + + Pipeline{}(k_window, + kargs.seqlen_k, + kb, + simthreshd1_eff, + pooled_k_out, + sim_k_out, + static_cast(smem)); + } +}; + +} // namespace ck_tile diff --git a/include/ck_tile/ops/sparse_attn/pipeline/sparge_blockmap_pipeline.hpp b/include/ck_tile/ops/sparse_attn/pipeline/sparge_blockmap_pipeline.hpp index 222e73c60e2..25e3b964e93 100644 --- a/include/ck_tile/ops/sparse_attn/pipeline/sparge_blockmap_pipeline.hpp +++ b/include/ck_tile/ops/sparse_attn/pipeline/sparge_blockmap_pipeline.hpp @@ -32,14 +32,22 @@ struct SpargeBlockMapPipeline static constexpr index_t kMaxKBlocks = 1024; // LDS layout (non-overlapping, all used simultaneously in Phase 2): - // [0 .. kReduceBytes) cross-warp reduction scratch - // [kScoreOffset ..) scores[N_k] - // [kBmapOffset ..) block_map[N_k] - // [kSmallOffset ..) Phase 3 argmax scratch (2*NumWarps floats) - static constexpr index_t kReduceBytes = NumWarps * D * sizeof(float); - static constexpr index_t kScoreOffset = kReduceBytes; - static constexpr index_t kBmapOffset = kScoreOffset + kMaxKBlocks * sizeof(float); - static constexpr index_t kSmallOffset = kBmapOffset + kMaxKBlocks * sizeof(uint8_t); + // [0 .. kReduceBytes) cross-warp reduction scratch slab 0 + // [kReduceBytes .. 2*kReduceBytes) cross-warp reduction scratch slab 1 + // (Round 8 b1: ping-pong for K-loop double buffer) + // [kScoreOffset ..) scores[N_k] + // [kBmapOffset ..) block_map[N_k] + // [kSmallOffset ..) Phase 3 argmax scratch (2*NumWarps floats) + // B2.v3 column-stride pad: replace k_idx*KPerThread with k_idx*(KPerThread+1) + // to break the 4-way intra-warp bank conflict. New per-warp slab size: + // KThreads * (KPerThread + 1) floats. + static constexpr index_t kColPaddedStride = KPerThread + 1; + static constexpr index_t kPerWarpFloats = KThreads * kColPaddedStride; + static constexpr index_t kReduceBytes = NumWarps * kPerWarpFloats * sizeof(float); + static constexpr index_t kReduceTotalBytes = 2 * kReduceBytes; // Round 8 b1: 2 slabs + static constexpr index_t kScoreOffset = kReduceTotalBytes; + static constexpr index_t kBmapOffset = kScoreOffset + kMaxKBlocks * sizeof(float); + static constexpr index_t kSmallOffset = kBmapOffset + kMaxKBlocks * sizeof(uint8_t); CK_TILE_HOST_DEVICE static constexpr index_t GetSmemSize() { @@ -98,6 +106,12 @@ struct SpargeBlockMapPipeline } // Cross-warp LDS reduction for column sums. + // Round 13f: templated TrailingSync flag. When false, the trailing __syncthreads() + // is dropped — only safe when the next access targets a *different* slab and the + // intervening work does not read smem_reduce. Used at the slab_b call in Phase 2 + // K-loop, where the next iter's first cross-warp reduce writes to slab_a (different + // address) and is preceded by its own leading sync. + template CK_TILE_DEVICE static void column_reduce_cross_warp(float (&col_acc)[KPerThread], float* __restrict__ smem_reduce) { @@ -107,17 +121,21 @@ struct SpargeBlockMapPipeline const index_t k_idx = lane_id % KThreads; const index_t m_idx = lane_id / KThreads; + // B2.v3 column-stride pad: stride k_idx by (KPerThread+1)=9 instead of 8, + // changing per-lane bank from (k_idx*8+k)%32 to (k_idx*9+k)%32. For k=0, + // lanes (k_idx={0,4,8,12}) now hit banks {0,4,8,12} instead of all 0. if(m_idx == 0) for(index_t k = 0; k < KPerThread; ++k) - smem_reduce[warp_id * D + k_idx * KPerThread + k] = col_acc[k]; + smem_reduce[warp_id * kPerWarpFloats + k_idx * kColPaddedStride + k] = col_acc[k]; __syncthreads(); for(index_t k = 0; k < KPerThread; ++k) col_acc[k] = 0.f; for(index_t w = 0; w < NumWarps; ++w) for(index_t k = 0; k < KPerThread; ++k) - col_acc[k] += smem_reduce[w * D + k_idx * KPerThread + k]; - __syncthreads(); + col_acc[k] += smem_reduce[w * kPerWarpFloats + k_idx * kColPaddedStride + k]; + if constexpr(TrailingSync) + __syncthreads(); } // Compute ||v||^2 per row: sum along KPerThread then xor-shuffle across k_idx. @@ -162,7 +180,8 @@ struct SpargeBlockMapPipeline for(index_t m = 0; m < SeqPerThread; ++m) { - float inv_norm = (row_norms[m] > 0.f) ? (1.0f / __builtin_sqrtf(row_norms[m])) : 0.f; + // Round 12: hardware fast rsqrt (v_rsq_f32, ~1 ULP) replaces sw sqrt+rcp. + float inv_norm = (row_norms[m] > 0.f) ? rsqrtf(row_norms[m]) : 0.f; index_t gsq = m * (SeqThreadPerWarp * NumWarps) + warp_id * SeqThreadPerWarp + m_idx; if(gsq < actual_seq) for(index_t k = 0; k < KPerThread; ++k) @@ -230,9 +249,9 @@ struct SpargeBlockMapPipeline // ====================================================================== template CK_TILE_DEVICE void operator()(const QWindowType& q_window_in, - const KWindowType& k_window_in, + const KWindowType& /*k_window_in*/, index_t seqlen_q, - index_t seqlen_k, + index_t /*seqlen_k*/, index_t qb, index_t N_k, index_t /*nhead_ratio_qk*/, @@ -243,11 +262,15 @@ struct SpargeBlockMapPipeline uint8_t* block_map_ptr, int32_t* lut_ptr, int32_t* valid_block_num_ptr, + const float* __restrict__ pooled_k_ws_ptr, + const uint8_t* __restrict__ sim_k_ws_ptr, void* smem_ptr) const { const index_t tid = static_cast(threadIdx.x); - auto* smem_float = reinterpret_cast(smem_ptr); + // R20: K-loop no longer reduces, only Phase 1 uses smem_float0. + // smem_float1 slab is allocated for layout compat but unused. + auto* smem_float0 = reinterpret_cast(smem_ptr); auto* smem_scores = reinterpret_cast(reinterpret_cast(smem_ptr) + kScoreOffset); auto* smem_bmap = @@ -271,16 +294,22 @@ struct SpargeBlockMapPipeline row_reduce_sq_norm(q_data, psq, bs_q); // 1b. Column sum -> mean + // Track F (re-apply R8 b2): drop trailing sync. Next reduce reuses same slab + // (smem_float0) and has its own leading __syncthreads() before reading. + // pooled_q_mean is register-only between reduces. float pooled_q_mean[KPerThread]; column_reduce_thread_and_warp(q_data, pooled_q_mean); - column_reduce_cross_warp(pooled_q_mean, smem_float); + column_reduce_cross_warp(pooled_q_mean, smem_float0); for(index_t k = 0; k < KPerThread; ++k) pooled_q_mean[k] *= inv_bs_q; // 1c. Normalised sum_hat + // Track F (re-apply R8 b2): drop trailing sync. Next cross-warp reduce in + // K-loop iter 0 writes slab_a=smem_float0 (kb=0 even). Although same slab, + // its leading __syncthreads() covers the WAR. sum_hat register-only here. float sum_hat[KPerThread]; column_reduce_normalised(q_data, psq, sum_hat, bs_q); - column_reduce_cross_warp(sum_hat, smem_float); + column_reduce_cross_warp(sum_hat, smem_float0); // 1d. sim_q = ||sum_hat||^2 / bs_q^2 float sh_sq = 0.f; @@ -319,49 +348,34 @@ struct SpargeBlockMapPipeline smem_bmap[i] = 0; __syncthreads(); - auto k_window = k_window_in; + // R20: K-stats precomputed by Kernel A. Each thread loads its own + // KPerThread-slice of pooled_k_mean from DRAM workspace; sim_k is a single + // byte. No K-tile load, no cross-warp reduce in the K-loop. + const index_t lane_id_kb = tid % WarpSize; + const index_t k_idx_kb = lane_id_kb % KThreads; for(index_t kb = 0; kb < N_k; ++kb) { - const index_t bs_k = min(static_cast(kN0), seqlen_k - kb * kN0); - const float inv_bs_k = (bs_k > 0) ? (1.0f / static_cast(bs_k)) : 0.f; - - auto k_tile = load_tile(k_window); - - float k_data[NPerThread * KPerThread]; - tile_to_float(k_tile, k_data); - - // K mean + const float* p_kb = pooled_k_ws_ptr + kb * D + k_idx_kb * KPerThread; float pooled_k_mean[KPerThread]; - column_reduce_thread_and_warp(k_data, pooled_k_mean); - column_reduce_cross_warp(pooled_k_mean, smem_float); for(index_t k = 0; k < KPerThread; ++k) - pooled_k_mean[k] *= inv_bs_k; + pooled_k_mean[k] = p_kb[k]; - // dot(pooled_q_mean, pooled_k_mean) float dot = 0.f; for(index_t k = 0; k < KPerThread; ++k) dot += pooled_q_mean[k] * pooled_k_mean[k]; dot = reduce_across_k(dot); - // K L2 norms + normalised sum_hat - float k_psq[NPerThread]; - row_reduce_sq_norm(k_data, k_psq, bs_k); - - float k_sum_hat[KPerThread]; - column_reduce_normalised(k_data, k_psq, k_sum_hat, bs_k); - column_reduce_cross_warp(k_sum_hat, smem_float); - - // sim_k - float ksh_sq = 0.f; - for(index_t k = 0; k < KPerThread; ++k) - ksh_sq += k_sum_hat[k] * k_sum_hat[k]; - ksh_sq = reduce_across_k(ksh_sq); - const float denom_k = static_cast(bs_k) * static_cast(bs_k); - const bool sim_k = (denom_k > 0.f) && ((ksh_sq / denom_k) > simthreshd1); + const bool sim_k = (sim_k_ws_ptr[kb] != 0); if(tid == 0) { + // INVARIANT (mirrors SpargeAttn ref utils.py:175-180): + // ~sim_k blocks are forced ON in the bitmap (final_map[~sim_k]=1) + // AND have score = -inf so Phase 3 selection (topk / cdf) does NOT + // pick them again (would double-count toward topk budget). + // Both writes MUST stay together. Any Phase 3 selection rewrite + // (e.g. iterative argmax → bitonic sort) must keep the -inf write. if(!sim_k) { smem_bmap[kb] = 1; @@ -372,10 +386,8 @@ struct SpargeBlockMapPipeline smem_scores[kb] = dot * scale; } } - __syncthreads(); - - move_tile_window(k_window, {kN0, 0}); } + __syncthreads(); // guard Phase 3's reads of smem_bmap / smem_scores // ================================================================== // Phase 3: Softmax + Selection @@ -399,15 +411,24 @@ struct SpargeBlockMapPipeline } const float sum_exp = block_reduce_sum(lsum, smem_small); - // normalise - const float inv_sum = (sum_exp > 0.f) ? (1.0f / sum_exp) : 0.f; - for(index_t i = tid; i < N_k; i += kBlockSize) - smem_scores[i] *= inv_sum; - __syncthreads(); + // Round 13i: argmax is invariant under positive scaling (inv_sum > 0). When + // topk > 0 we never read normalised values for cdfthreshd, so skip the + // normalise pass entirely (saves N_k LDS writes + 1 __syncthreads). The + // cdfthreshd path (topk <= 0) still requires normalised scores so the + // accumulator `cumulative_prob` matches probabilities. + const bool topk_active = (topk > 0.f); + const float inv_sum = + (!topk_active && sum_exp > 0.f) ? (1.0f / sum_exp) : 0.f; + if(!topk_active) + { + for(index_t i = tid; i < N_k; i += kBlockSize) + smem_scores[i] *= inv_sum; + __syncthreads(); + } // Selection: iterative argmax index_t num_to_select = - (topk > 0.f) + topk_active ? max(static_cast(1), static_cast(topk * static_cast(N_k))) : N_k; @@ -448,6 +469,11 @@ struct SpargeBlockMapPipeline } __syncthreads(); + // Round 13g: collapse 2 syncs/round into 1. tid==0 computes the global + // winner AND writes the sentinel (smem_bmap=1, smem_scores=-1) in the same + // critical section, gated by bv>0. All threads then read smem_small[0] for + // the early break / cumulative_prob accumulation. Saves 1 __syncthreads per + // round (~32 syncs @ N_k=64 topk=0.5). if(tid == 0) { float bv = smem_small[0]; @@ -462,24 +488,22 @@ struct SpargeBlockMapPipeline bi = wi; } } + // Write sentinel into bmap/scores in the same critical section. + // Guarded by bv > 0 so we never poison a valid score with -1. + if(bv > 0.f) + { + smem_bmap[bi] = 1; + smem_scores[bi] = -1.f; + } smem_small[0] = bv; - smem_small[1] = bit_cast(static_cast(bi)); } __syncthreads(); - float g_val = smem_small[0]; - index_t g_idx = bit_cast(smem_small[1]); + float g_val = smem_small[0]; if(g_val <= 0.f) break; - if(tid == 0) - { - smem_bmap[g_idx] = 1; - smem_scores[g_idx] = -1.f; - } - __syncthreads(); - if(topk > 0.f) { if(round + 1 >= num_to_select) diff --git a/include/ck_tile/ops/sparse_attn/pipeline/sparge_kstats_pipeline.hpp b/include/ck_tile/ops/sparse_attn/pipeline/sparge_kstats_pipeline.hpp new file mode 100644 index 00000000000..1cb96d716a3 --- /dev/null +++ b/include/ck_tile/ops/sparse_attn/pipeline/sparge_kstats_pipeline.hpp @@ -0,0 +1,110 @@ +// Copyright (c) Advanced Micro Devices, Inc., or its affiliates. +// SPDX-License-Identifier: MIT +#pragma once + +#include "ck_tile/core.hpp" +#include "ck_tile/ops/sparse_attn/pipeline/sparge_blockmap_pipeline.hpp" + +namespace ck_tile { + +// Kernel A of the K-stat precompute split: one work-group per (b, hk, kb) +// computes pooled_k_mean and sim_k for that K-block once. Kernel B then reads +// from the workspace instead of recomputing per Q-block. +template +struct SpargeKStatsPipeline +{ + using Problem = remove_cvref_t; + using Base = SpargeBlockMapPipeline; + using QDataType = typename Base::QDataType; + using KDataType = typename Base::KDataType; + + static constexpr index_t kBlockSize = Base::kBlockSize; + static constexpr index_t kM0 = Base::kM0; + static constexpr index_t kN0 = Base::kN0; + static constexpr index_t D = Base::D; + static constexpr index_t NumWarps = Base::NumWarps; + static constexpr index_t WarpSize = Base::WarpSize; + + static constexpr index_t KPerThread = Base::KPerThread; + static constexpr index_t KThreads = Base::KThreads; + static constexpr index_t SeqThreadPerWarp = Base::SeqThreadPerWarp; + static constexpr index_t NPerThread = Base::NPerThread; + + static constexpr index_t kBlockPerCu = 1; + + static constexpr index_t kColPaddedStride = Base::kColPaddedStride; + static constexpr index_t kPerWarpFloats = Base::kPerWarpFloats; + static constexpr index_t kReduceBytes = NumWarps * kPerWarpFloats * sizeof(float); + + CK_TILE_HOST_DEVICE static constexpr index_t GetSmemSize() { return kReduceBytes; } + + CK_TILE_HOST_DEVICE static constexpr auto MakeKBlockDistribution() + { + return Base::MakeKBlockDistribution(); + } + + // operator(): one work-group, one K-block. Writes D fp32 + 1 uint8 to workspace. + template + CK_TILE_DEVICE void operator()(const KWindowType& k_window, + index_t seqlen_k, + index_t kb, + float simthreshd1, + float* __restrict__ pooled_k_out, // D floats + uint8_t* __restrict__ sim_k_out, // 1 byte + void* smem_ptr) const + { + const index_t tid = static_cast(threadIdx.x); + auto* smem_reduce = reinterpret_cast(smem_ptr); + + const index_t bs_k = min(static_cast(kN0), seqlen_k - kb * kN0); + const float inv_bs_k = (bs_k > 0) ? (1.0f / static_cast(bs_k)) : 0.f; + + auto k_tile = load_tile(k_window); + + float k_data[NPerThread * KPerThread]; + Base::template tile_to_float(k_tile, k_data); + + const index_t warp_id = tid / WarpSize; + const index_t lane_id = tid % WarpSize; + const index_t k_idx = lane_id % KThreads; + const index_t m_idx = lane_id / KThreads; + + // pooled_k_mean: column sum then cross-warp reduce. + // R21A: drop trailing sync (next cross_warp_reduce has its own leading sync). + float pooled_k_mean[KPerThread]; + Base::template column_reduce_thread_and_warp(k_data, pooled_k_mean); + Base::template column_reduce_cross_warp(pooled_k_mean, smem_reduce); + for(index_t k = 0; k < KPerThread; ++k) + pooled_k_mean[k] *= inv_bs_k; + + // R21A: write pooled_k_mean to global early so its register liveness ends here, + // freeing VGPR before k_sum_hat becomes live. + if(warp_id == 0 && m_idx == 0) + { + for(index_t k = 0; k < KPerThread; ++k) + pooled_k_out[k_idx * KPerThread + k] = pooled_k_mean[k]; + } + + // K row L2 norms + normalised column sum (k_sum_hat) + float k_psq[NPerThread]; + Base::template row_reduce_sq_norm(k_data, k_psq, bs_k); + + float k_sum_hat[KPerThread]; + Base::template column_reduce_normalised(k_data, k_psq, k_sum_hat, bs_k); + // R21A: drop trailing sync (no further smem read; only intra-warp shuffle + global write). + Base::template column_reduce_cross_warp(k_sum_hat, smem_reduce); + + // sim_k = (||k_sum_hat||^2 / bs_k^2) > simthreshd1 + float ksh_sq = 0.f; + for(index_t k = 0; k < KPerThread; ++k) + ksh_sq += k_sum_hat[k] * k_sum_hat[k]; + ksh_sq = Base::reduce_across_k(ksh_sq); + const float denom_k = static_cast(bs_k) * static_cast(bs_k); + const bool sim_k = (denom_k > 0.f) && ((ksh_sq / denom_k) > simthreshd1); + + if(tid == 0) + *sim_k_out = sim_k ? static_cast(1) : static_cast(0); + } +}; + +} // namespace ck_tile From 668e10728293e0ccf4884ce98b5e21b4a4191e68 Mon Sep 17 00:00:00 2001 From: Gino Lu Date: Sun, 17 May 2026 02:30:48 -0400 Subject: [PATCH 08/16] fix(sparse_attn): backport PR #4742 LDS s_barrier Add s_barrier after sched_barrier when K-tail and V share LDS buffer, mirroring upstream PR #4742. Applies to both async_vsa and async_jenga pipelines. Co-Authored-By: Claude Opus 4 --- .../pipeline/block_fmha_pipeline_qr_ks_vs_async_jenga.hpp | 6 ++++++ .../pipeline/block_fmha_pipeline_qr_ks_vs_async_vsa.hpp | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/include/ck_tile/ops/sparse_attn/pipeline/block_fmha_pipeline_qr_ks_vs_async_jenga.hpp b/include/ck_tile/ops/sparse_attn/pipeline/block_fmha_pipeline_qr_ks_vs_async_jenga.hpp index 9fe8b365b00..717d82aca78 100644 --- a/include/ck_tile/ops/sparse_attn/pipeline/block_fmha_pipeline_qr_ks_vs_async_jenga.hpp +++ b/include/ck_tile/ops/sparse_attn/pipeline/block_fmha_pipeline_qr_ks_vs_async_jenga.hpp @@ -430,6 +430,12 @@ struct BlockFmhaPipelineQRKSVSAsyncJenga s.get_tile_distribution()); // Pcompute{j} __builtin_amdgcn_sched_barrier(0x7F); + // Ensure gemm_0's LDS reads (K tile) from all threads are completed before V store + // Only needed when K tail and V use the same LDS buffer + if constexpr(LdsSeq.at(number{}) == LdsSeq.at(number{})) + { + __builtin_amdgcn_s_barrier(); + } // store & prefetch next v, after the max reduction auto v_shuffle_tmp = make_static_distributed_tensor( Policy::template MakeShuffledVRegBlockDescriptor()); diff --git a/include/ck_tile/ops/sparse_attn/pipeline/block_fmha_pipeline_qr_ks_vs_async_vsa.hpp b/include/ck_tile/ops/sparse_attn/pipeline/block_fmha_pipeline_qr_ks_vs_async_vsa.hpp index 578ad7e6039..507c91a585e 100644 --- a/include/ck_tile/ops/sparse_attn/pipeline/block_fmha_pipeline_qr_ks_vs_async_vsa.hpp +++ b/include/ck_tile/ops/sparse_attn/pipeline/block_fmha_pipeline_qr_ks_vs_async_vsa.hpp @@ -387,6 +387,12 @@ struct BlockFmhaPipelineQRKSVSAsyncVSA s.get_tile_distribution()); // Pcompute{j} __builtin_amdgcn_sched_barrier(0x7F); + // Ensure gemm_0's LDS reads (K tile) from all threads are completed before V store + // Only needed when K tail and V use the same LDS buffer + if constexpr(LdsSeq.at(number{}) == LdsSeq.at(number{})) + { + __builtin_amdgcn_s_barrier(); + } // store & prefetch next v, after the max reduction if constexpr(std::is_same_v) { From 7103eacc99829af14680e20ab82ab7c755aa6e07 Mon Sep 17 00:00:00 2001 From: Gino Lu Date: Sun, 17 May 2026 02:34:23 -0400 Subject: [PATCH 09/16] refactor(sparse_attn): caller-owned workspace + dtype-aware sizing Replace process-lifetime lazy hipMalloc K-stats workspace with a caller-owned buffer; expose sparge_blockmap_get_workspace_size() / compute_workspace_layout() host helpers. Split the combined sparge_blockmap_fwd into stage launchers (sparge_kstats_fwd_oneshot + sparge_blockmap_only_fwd_oneshot) so the chained launch is timed end-to-end. Make pooled_k storage dtype follow KDataType (fp16/bf16) instead of fp32 to halve workspace footprint and match dense-FMHA precision. Tighten per-head superparam pointers to required (non-null) and assert N_k <= 256 in jenga MakeKargs to document the 256-bool LDS staging cap. Drop the obsolete VSA extra-LDS staging. Co-Authored-By: Claude Opus 4 --- .../50_sparse_attn/sparge_blockmap_inst.cpp | 267 +++++++--------- .../50_sparse_attn/sparge_blockmap_trek.hpp | 64 ++-- .../ck_tile/50_sparse_attn/test_sparge.cpp | 300 +++++++++--------- .../kernel/fmha_fwd_jenga_kernel.hpp | 68 ++-- .../kernel/fmha_fwd_vsa_kernel.hpp | 3 +- .../kernel/sparge_blockmap_kernel.hpp | 40 +-- .../kernel/sparge_kstats_kernel.hpp | 24 +- .../pipeline/sparge_blockmap_pipeline.hpp | 9 +- .../pipeline/sparge_kstats_pipeline.hpp | 12 +- 9 files changed, 395 insertions(+), 392 deletions(-) diff --git a/example/ck_tile/50_sparse_attn/sparge_blockmap_inst.cpp b/example/ck_tile/50_sparse_attn/sparge_blockmap_inst.cpp index 3cc674f181f..0442f1de857 100644 --- a/example/ck_tile/50_sparse_attn/sparge_blockmap_inst.cpp +++ b/example/ck_tile/50_sparse_attn/sparge_blockmap_inst.cpp @@ -67,47 +67,24 @@ using bmap_fp16_kernel = ck_tile::SpargeBlockMapKernel; using kstats_fp16_pipeline = ck_tile::SpargeKStatsPipeline; using kstats_fp16_kernel = ck_tile::SpargeKStatsKernel; -// ============================================================================ -// bf16: D=128, kM0=64, kN0=128 -// ============================================================================ - -using bmap_bf16_block_tile = ck_tile::sequence<64, 128, 128, 128, 128, 128>; - -using bmap_bf16_shape = - ck_tile::TileFmhaShape, - ck_tile::sequence<16, 16, 16>, - ck_tile::sequence<4, 1, 1>, - ck_tile::sequence<16, 16, 16>, - true>; - -using bmap_bf16_trait = ck_tile::TileFmhaTraits; - -using bmap_bf16_variant = ck_tile::ComposedAttention<0, CK_TILE_FMHA_FWD_FAST_EXP2>; -using bmap_bf16_mask = ck_tile::GenericAttentionMask; - -using bmap_bf16_problem = ck_tile::BlockFmhaPipelineProblem; using kstats_bf16_kernel = ck_tile::SpargeKStatsKernel; // ============================================================================ -// Internal K-stat workspace (R20): process-lifetime lazy hipMalloc, sized -// to the largest (batch, nhead_k, N_k, D) seen so far. Caller API unchanged. +// Workspace layout: caller owns the buffer; we just compute size + offsets. +// Layout = [pooled_k (KDataType) | sim_k (uint8)]. sim_k follows pooled_k with +// no padding (uint8 has alignment 1). // ============================================================================ namespace { -struct KStatsWorkspace +constexpr int sparge_kN0_for(int hdim_q) { - void* pooled_k_dev = nullptr; // [batch, nhead_k, N_k, D] fp32 - void* sim_k_dev = nullptr; // [batch, nhead_k, N_k] uint8 - size_t pooled_k_bytes = 0; - size_t sim_k_bytes = 0; + // d=128 instances use kN0=128 (see bmap_fp16_block_tile). + return (hdim_q == 128) ? 128 : 0; +} - void ensure(int batch, int nhead_k, int N_k, int D) - { - const size_t need_p = static_cast(batch) * nhead_k * N_k * D * sizeof(float); - const size_t need_s = static_cast(batch) * nhead_k * N_k * sizeof(uint8_t); - if(need_p > pooled_k_bytes) - { - if(pooled_k_dev != nullptr) (void)hipFree(pooled_k_dev); - (void)hipMalloc(&pooled_k_dev, need_p); - pooled_k_bytes = need_p; - } - if(need_s > sim_k_bytes) - { - if(sim_k_dev != nullptr) (void)hipFree(sim_k_dev); - (void)hipMalloc(&sim_k_dev, need_s); - sim_k_bytes = need_s; - } - } -}; +size_t dtype_bytes(const std::string& dt) +{ + if(dt == "fp16" || dt == "bf16") + return 2; + return 0; +} -KStatsWorkspace& g_kstats_ws() +} // namespace + +sparge_blockmap_workspace_layout +sparge_blockmap_compute_workspace_layout(sparge_blockmap_traits traits, sparge_blockmap_args args) { - static KStatsWorkspace ws; - return ws; + const int kN0 = sparge_kN0_for(traits.hdim_q); + const int N_k = (kN0 > 0) ? ck_tile::integer_divide_ceil(args.seqlen_k, kN0) : 0; + const int D = traits.hdim_q; + const size_t element_bytes = dtype_bytes(traits.data_type); + + sparge_blockmap_workspace_layout layout{}; + layout.pooled_k_offset = 0; + layout.pooled_k_bytes = + static_cast(args.batch) * args.nhead_k * N_k * D * element_bytes; + layout.sim_k_offset = layout.pooled_k_bytes; + layout.sim_k_bytes = static_cast(args.batch) * args.nhead_k * N_k * sizeof(uint8_t); + layout.total_bytes = layout.sim_k_offset + layout.sim_k_bytes; + return layout; } -template -void launch_kstats_then_blockmap(sparge_blockmap_args args, const ck_tile::stream_config& s) +// ============================================================================ +// Stage launchers: read args.workspace_ptr split per layout, run one kernel. +// ============================================================================ + +namespace { + +template +void launch_kstats_only(sparge_blockmap_traits traits, + sparge_blockmap_args args, + const ck_tile::stream_config& s) { - const int N_k = ck_tile::integer_divide_ceil(args.seqlen_k, BlockMapKernel::kN0); - const int D = BlockMapKernel::D; - auto& ws = g_kstats_ws(); - ws.ensure(args.batch, args.nhead_k, N_k, D); + const auto layout = sparge_blockmap_compute_workspace_layout(traits, args); + auto* ws_base = static_cast(args.workspace_ptr); + void* pooled_k_ptr = ws_base + layout.pooled_k_offset; + void* sim_k_ptr = ws_base + layout.sim_k_offset; + + auto [kargs, grids] = + sparge_kstats_create_kargs_and_grids(args, pooled_k_ptr, sim_k_ptr); + const dim3 blocks = KStatsKernel::BlockSize(); + constexpr ck_tile::index_t kBlockPerCu = KStatsKernel::kBlockPerCu; + ck_tile::make_kernel(KStatsKernel{}, grids, blocks, 0, kargs)(s); +} - // Stage 1: K stats - { - auto [kargs, grids] = - sparge_kstats_create_kargs_and_grids(args, ws.pooled_k_dev, ws.sim_k_dev); - const dim3 blocks = KStatsKernel::BlockSize(); - constexpr ck_tile::index_t kBlockPerCu = KStatsKernel::kBlockPerCu; - ck_tile::make_kernel(KStatsKernel{}, grids, blocks, 0, kargs)( - ck_tile::stream_config{s.stream_id_}); - } - // Stage 2: block_map (reads ws) - { - auto [kargs, grids] = sparge_blockmap_create_kargs_and_grids( - args, ws.pooled_k_dev, ws.sim_k_dev); - const dim3 blocks = BlockMapKernel::BlockSize(); - constexpr ck_tile::index_t kBlockPerCu = BlockMapKernel::kBlockPerCu; - ck_tile::make_kernel(BlockMapKernel{}, grids, blocks, 0, kargs)( - ck_tile::stream_config{s.stream_id_}); - } +template +void launch_blockmap_only(sparge_blockmap_traits traits, + sparge_blockmap_args args, + const ck_tile::stream_config& s) +{ + const auto layout = sparge_blockmap_compute_workspace_layout(traits, args); + auto* ws_base = static_cast(args.workspace_ptr); + void* pooled_k_ptr = ws_base + layout.pooled_k_offset; + void* sim_k_ptr = ws_base + layout.sim_k_offset; + + auto [kargs, grids] = + sparge_blockmap_create_kargs_and_grids(args, pooled_k_ptr, sim_k_ptr); + const dim3 blocks = BlockMapKernel::BlockSize(); + constexpr ck_tile::index_t kBlockPerCu = BlockMapKernel::kBlockPerCu; + ck_tile::make_kernel(BlockMapKernel{}, grids, blocks, 0, kargs)(s); } } // namespace // ============================================================================ -// Dispatch +// Oneshot stages (no timing): caller chains them via launch_kernel. // ============================================================================ -float sparge_blockmap_fwd(sparge_blockmap_traits traits, - sparge_blockmap_args args, - const ck_tile::stream_config& s) +void sparge_kstats_fwd_oneshot(sparge_blockmap_traits traits, + sparge_blockmap_args args, + const ck_tile::stream_config& s) { if(traits.data_type == "fp16" && traits.hdim_q == 128) { - if(s.log_level_ > 0) - std::cout << ", sparge_blockmap_fp16_d128" << std::flush; - return ck_tile::launch_kernel(s, [=](const ck_tile::stream_config& s_) { - launch_kstats_then_blockmap(args, s_); - }); + launch_kstats_only(traits, args, s); + return; } - if(traits.data_type == "bf16" && traits.hdim_q == 128) { - if(s.log_level_ > 0) - std::cout << ", sparge_blockmap_bf16_d128" << std::flush; - return ck_tile::launch_kernel(s, [=](const ck_tile::stream_config& s_) { - launch_kstats_then_blockmap(args, s_); - }); + launch_kstats_only(traits, args, s); + return; } - - if(s.log_level_ > 0) - std::cerr << "sparge_blockmap_fwd: unsupported config (data_type=" << traits.data_type - << ", hdim_q=" << traits.hdim_q << ")" << std::endl; - return -1.f; + std::cerr << "sparge_kstats_fwd_oneshot: unsupported config (data_type=" << traits.data_type + << ", hdim_q=" << traits.hdim_q << ")" << std::endl; } -// ============================================================================ -// Oneshot version: launches kernel without timing wrapper -// ============================================================================ - -void sparge_blockmap_fwd_oneshot(sparge_blockmap_traits traits, - sparge_blockmap_args args, - const ck_tile::stream_config& s) +void sparge_blockmap_only_fwd_oneshot(sparge_blockmap_traits traits, + sparge_blockmap_args args, + const ck_tile::stream_config& s) { if(traits.data_type == "fp16" && traits.hdim_q == 128) { - launch_kstats_then_blockmap(args, s); + launch_blockmap_only(traits, args, s); return; } - if(traits.data_type == "bf16" && traits.hdim_q == 128) { - launch_kstats_then_blockmap(args, s); + launch_blockmap_only(traits, args, s); return; } - - std::cerr << "sparge_blockmap_fwd_oneshot: unsupported config (data_type=" << traits.data_type - << ", hdim_q=" << traits.hdim_q << ")" << std::endl; + std::cerr << "sparge_blockmap_only_fwd_oneshot: unsupported config (data_type=" + << traits.data_type << ", hdim_q=" << traits.hdim_q << ")" << std::endl; } // ============================================================================ -// Combined functions: blockmap + attention timed together via launch_kernel +// Combined functions: kstats + blockmap + attention timed together. // ============================================================================ -float sparge_jenga_fwd(sparge_blockmap_traits bmap_t, sparge_blockmap_args bmap_a, - fmha_jenga_fwd_traits attn_t, fmha_jenga_fwd_args attn_a, +float sparge_jenga_fwd(sparge_blockmap_traits bmap_t, + sparge_blockmap_args bmap_a, + fmha_jenga_fwd_traits attn_t, + fmha_jenga_fwd_args attn_a, const ck_tile::stream_config& s) { if(s.log_level_ > 0) - std::cout << ", sparge_blockmap_" << bmap_t.data_type << "_d" << bmap_t.hdim_q - << ", fmha_jenga_fwd_" << attn_t.data_type << "_d" << attn_t.hdim_q - << std::flush; + std::cout << ", sparge_kstats_" << bmap_t.data_type << "_d" << bmap_t.hdim_q + << ", sparge_blockmap_" << bmap_t.data_type << "_d" << bmap_t.hdim_q + << ", fmha_jenga_fwd_" << attn_t.data_type << "_d" << attn_t.hdim_q << std::flush; return ck_tile::launch_kernel( s, + [=](const ck_tile::stream_config& s_) { sparge_kstats_fwd_oneshot(bmap_t, bmap_a, s_); }, [=](const ck_tile::stream_config& s_) { - sparge_blockmap_fwd_oneshot(bmap_t, bmap_a, s_); + sparge_blockmap_only_fwd_oneshot(bmap_t, bmap_a, s_); }, - [=](const ck_tile::stream_config& s_) { - fmha_jenga_fwd_oneshot(attn_t, attn_a, s_); - }); + [=](const ck_tile::stream_config& s_) { fmha_jenga_fwd_oneshot(attn_t, attn_a, s_); }); } -float sparge_vsa_fwd_combined(sparge_blockmap_traits bmap_t, sparge_blockmap_args bmap_a, - fmha_vsa_fwd_traits attn_t, fmha_vsa_fwd_args attn_a, +float sparge_vsa_fwd_combined(sparge_blockmap_traits bmap_t, + sparge_blockmap_args bmap_a, + fmha_vsa_fwd_traits attn_t, + fmha_vsa_fwd_args attn_a, const ck_tile::stream_config& s) { if(s.log_level_ > 0) - std::cout << ", sparge_blockmap_" << bmap_t.data_type << "_d" << bmap_t.hdim_q - << ", fmha_vsa_fwd_" << attn_t.data_type << "_d" << attn_t.hdim_q - << std::flush; + std::cout << ", sparge_kstats_" << bmap_t.data_type << "_d" << bmap_t.hdim_q + << ", sparge_blockmap_" << bmap_t.data_type << "_d" << bmap_t.hdim_q + << ", fmha_vsa_fwd_" << attn_t.data_type << "_d" << attn_t.hdim_q << std::flush; return ck_tile::launch_kernel( s, + [=](const ck_tile::stream_config& s_) { sparge_kstats_fwd_oneshot(bmap_t, bmap_a, s_); }, [=](const ck_tile::stream_config& s_) { - sparge_blockmap_fwd_oneshot(bmap_t, bmap_a, s_); + sparge_blockmap_only_fwd_oneshot(bmap_t, bmap_a, s_); }, - [=](const ck_tile::stream_config& s_) { - fmha_vsa_fwd_oneshot(attn_t, attn_a, s_); - }); + [=](const ck_tile::stream_config& s_) { fmha_vsa_fwd_oneshot(attn_t, attn_a, s_); }); } diff --git a/example/ck_tile/50_sparse_attn/sparge_blockmap_trek.hpp b/example/ck_tile/50_sparse_attn/sparge_blockmap_trek.hpp index 92c32d29e85..4d0e935fc94 100644 --- a/example/ck_tile/50_sparse_attn/sparge_blockmap_trek.hpp +++ b/example/ck_tile/50_sparse_attn/sparge_blockmap_trek.hpp @@ -48,14 +48,23 @@ struct sparge_blockmap_args void* lut_ptr; void* valid_block_num_ptr; - // R21A Phase 4 + R21B fix: optional per-head superparams. nullptr => use scalar. - // Buffer sizes match SpargeAttn upstream contract (utils.py:324-328: all sized - // by Headnum=q.size(1)=nhead_q). K-side kernel still indexes [hk] into the - // first nhead_k entries — for MHA equivalent to old [nhead_k] sizing, for - // MQA/GQA aligns to upstream tuned ckpt layout. - const float* simthreshd1_per_head_ptr = nullptr; // size = nhead_q floats (kernel reads [0..nhead_k-1]) - const float* cdfthreshd_per_head_ptr = nullptr; // size = nhead_q floats - const float* topk_per_head_ptr = nullptr; // size = nhead_q floats + // Caller-owned K-stats workspace; size from sparge_blockmap_get_workspace_size. + // Internal layout (pooled_k then sim_k) given by sparge_blockmap_workspace_layout. + void* workspace_ptr = nullptr; + + // size = nhead_q to match SpargeAttn upstream hyperparameter_check + const float* simthreshd1_per_head_ptr = nullptr; + const float* cdfthreshd_per_head_ptr = nullptr; + const float* topk_per_head_ptr = nullptr; +}; + +struct sparge_blockmap_workspace_layout +{ + size_t pooled_k_offset; // bytes from workspace_ptr + size_t pooled_k_bytes; + size_t sim_k_offset; // bytes from workspace_ptr + size_t sim_k_bytes; + size_t total_bytes; }; struct sparge_blockmap_traits @@ -127,19 +136,36 @@ auto sparge_kstats_create_kargs_and_grids(sparge_blockmap_args args, // ============================================================================ // Hand-written template instantiation dispatch // ============================================================================ -float sparge_blockmap_fwd(sparge_blockmap_traits traits, - sparge_blockmap_args args, - const ck_tile::stream_config& stream_config); -void sparge_blockmap_fwd_oneshot(sparge_blockmap_traits traits, - sparge_blockmap_args args, - const ck_tile::stream_config& stream_config); +// Workspace sizing helpers (host, no template instantiation needed). +sparge_blockmap_workspace_layout +sparge_blockmap_compute_workspace_layout(sparge_blockmap_traits traits, sparge_blockmap_args args); + +inline size_t sparge_blockmap_get_workspace_size(sparge_blockmap_traits traits, + sparge_blockmap_args args) +{ + return sparge_blockmap_compute_workspace_layout(traits, args).total_bytes; +} -// Combined functions: blockmap + attention with unified timing -float sparge_jenga_fwd(sparge_blockmap_traits, sparge_blockmap_args, - fmha_jenga_fwd_traits, fmha_jenga_fwd_args, +// Stage 1: K-stats only. Writes pooled_k + sim_k into args.workspace_ptr. +void sparge_kstats_fwd_oneshot(sparge_blockmap_traits traits, + sparge_blockmap_args args, + const ck_tile::stream_config& stream_config); + +// Stage 2: block_map only. Reads pooled_k + sim_k from args.workspace_ptr. +void sparge_blockmap_only_fwd_oneshot(sparge_blockmap_traits traits, + sparge_blockmap_args args, + const ck_tile::stream_config& stream_config); + +// Combined functions: kstats + blockmap + attention with unified timing. +float sparge_jenga_fwd(sparge_blockmap_traits, + sparge_blockmap_args, + fmha_jenga_fwd_traits, + fmha_jenga_fwd_args, const ck_tile::stream_config&); -float sparge_vsa_fwd_combined(sparge_blockmap_traits, sparge_blockmap_args, - fmha_vsa_fwd_traits, fmha_vsa_fwd_args, +float sparge_vsa_fwd_combined(sparge_blockmap_traits, + sparge_blockmap_args, + fmha_vsa_fwd_traits, + fmha_vsa_fwd_args, const ck_tile::stream_config&); diff --git a/example/ck_tile/50_sparse_attn/test_sparge.cpp b/example/ck_tile/50_sparse_attn/test_sparge.cpp index 4c97a10d0f0..a2cf101cf1f 100644 --- a/example/ck_tile/50_sparse_attn/test_sparge.cpp +++ b/example/ck_tile/50_sparse_attn/test_sparge.cpp @@ -25,8 +25,11 @@ // ============================================================================ template -ck_tile::HostTensor -make_qkv_tensor(ck_tile::index_t batch, ck_tile::index_t nhead, ck_tile::index_t seqlen, ck_tile::index_t hdim, bool i_perm) +ck_tile::HostTensor make_qkv_tensor(ck_tile::index_t batch, + ck_tile::index_t nhead, + ck_tile::index_t seqlen, + ck_tile::index_t hdim, + bool i_perm) { if(i_perm) return ck_tile::HostTensor({batch, nhead, seqlen, hdim}); @@ -86,8 +89,7 @@ float to_float_for_compare(ck_tile::bf16_t value) auto create_args(int argc, char* argv[]) { ck_tile::ArgParser arg_parser; - arg_parser - .insert("v", "1", "0:no validation, 1:cpu validation") + arg_parser.insert("v", "1", "0:no validation, 1:cpu validation") .insert("pipeline", "jenga", "attention pipeline: jenga / vsa") .insert("b", "1", "batch size") .insert("h", "4", "num of head for q") @@ -105,10 +107,7 @@ auto create_args(int argc, char* argv[]) .insert("seed", "42", "random seed") .insert("warmup", "5", "warmup iterations") .insert("repeat", "20", "benchmark iterations") - .insert("kname", "0", "print kernel name") - .insert("perhead", "0", - "R21A Phase 4: 0=scalar (default), 1=per-head [H] superparam test " - "(varies topk[h] = topk * (1 + 0.5*(h - H/2)/H), simthreshd1 unchanged)"); + .insert("kname", "0", "print kernel name"); bool result = arg_parser.parse(argc, argv); return std::make_tuple(result, arg_parser); @@ -120,29 +119,31 @@ auto create_args(int argc, char* argv[]) template bool run_test(const ck_tile::ArgParser& arg_parser) { - int do_validation = arg_parser.get_int("v"); - std::string pipeline = arg_parser.get_str("pipeline"); - ck_tile::index_t batch = arg_parser.get_int("b"); - ck_tile::index_t nhead = arg_parser.get_int("h"); - ck_tile::index_t nhead_k = arg_parser.get_int("h_k"); - ck_tile::index_t seqlen_q = arg_parser.get_int("s"); - ck_tile::index_t seqlen_k = arg_parser.get_int("s_k"); - ck_tile::index_t hdim_q = arg_parser.get_int("d"); - ck_tile::index_t hdim_v = arg_parser.get_int("d_v"); - float topk = arg_parser.get_float("topk"); - float cdfthreshd = arg_parser.get_float("cdfthreshd"); - float simthreshd1 = arg_parser.get_float("simthreshd1"); - bool i_perm = arg_parser.get_bool("iperm"); - bool o_perm = arg_parser.get_bool("operm"); - uint32_t seed = arg_parser.get_uint32("seed"); - int warmup = arg_parser.get_int("warmup"); - int repeat = arg_parser.get_int("repeat"); - int kname = arg_parser.get_int("kname"); - int perhead = arg_parser.get_int("perhead"); - - if(nhead_k < 0) nhead_k = nhead; - if(seqlen_k < 0) seqlen_k = seqlen_q; - if(hdim_v < 0) hdim_v = hdim_q; + int do_validation = arg_parser.get_int("v"); + std::string pipeline = arg_parser.get_str("pipeline"); + ck_tile::index_t batch = arg_parser.get_int("b"); + ck_tile::index_t nhead = arg_parser.get_int("h"); + ck_tile::index_t nhead_k = arg_parser.get_int("h_k"); + ck_tile::index_t seqlen_q = arg_parser.get_int("s"); + ck_tile::index_t seqlen_k = arg_parser.get_int("s_k"); + ck_tile::index_t hdim_q = arg_parser.get_int("d"); + ck_tile::index_t hdim_v = arg_parser.get_int("d_v"); + float topk = arg_parser.get_float("topk"); + float cdfthreshd = arg_parser.get_float("cdfthreshd"); + float simthreshd1 = arg_parser.get_float("simthreshd1"); + bool i_perm = arg_parser.get_bool("iperm"); + bool o_perm = arg_parser.get_bool("operm"); + uint32_t seed = arg_parser.get_uint32("seed"); + int warmup = arg_parser.get_int("warmup"); + int repeat = arg_parser.get_int("repeat"); + int kname = arg_parser.get_int("kname"); + + if(nhead_k < 0) + nhead_k = nhead; + if(seqlen_k < 0) + seqlen_k = seqlen_q; + if(hdim_v < 0) + hdim_v = hdim_q; // If cdfthreshd >= 0, use CDF mode; otherwise use topk mode if(cdfthreshd >= 0.0f) @@ -162,15 +163,14 @@ bool run_test(const ck_tile::ArgParser& arg_parser) ck_tile::index_t num_k_blocks = (seqlen_k + BLKK - 1) / BLKK; std::string prec_str = std::is_same_v ? "fp16" : "bf16"; - std::cout << "[" << pipeline << "|" << prec_str - << "] b=" << batch << " h=" << nhead << " s=" << seqlen_q - << " d=" << hdim_q << " topk=" << topk - << " sim1=" << simthreshd1 << std::flush; + std::cout << "[" << pipeline << "|" << prec_str << "] b=" << batch << " h=" << nhead + << " s=" << seqlen_q << " d=" << hdim_q << " topk=" << topk << " sim1=" << simthreshd1 + << std::flush; // ---- allocate host tensors ---- - auto q_host = make_qkv_tensor(batch, nhead, seqlen_q, hdim_q, i_perm); - auto k_host = make_qkv_tensor(batch, nhead_k, seqlen_k, hdim_q, i_perm); - auto v_host = make_qkv_tensor(batch, nhead_k, seqlen_k, hdim_v, i_perm); + auto q_host = make_qkv_tensor(batch, nhead, seqlen_q, hdim_q, i_perm); + auto k_host = make_qkv_tensor(batch, nhead_k, seqlen_k, hdim_q, i_perm); + auto v_host = make_qkv_tensor(batch, nhead_k, seqlen_k, hdim_v, i_perm); auto output_host = o_perm ? ck_tile::HostTensor({batch, nhead, seqlen_q, hdim_v}) : ck_tile::HostTensor({batch, seqlen_q, nhead, hdim_v}); @@ -213,62 +213,61 @@ bool run_test(const ck_tile::ArgParser& arg_parser) bmap_traits.hdim_q = hdim_q; sparge_blockmap_args bmap_args; - bmap_args.q_ptr = q_dev.GetDeviceBuffer(); - bmap_args.k_ptr = k_dev.GetDeviceBuffer(); - bmap_args.batch = batch; - bmap_args.seqlen_q = seqlen_q; - bmap_args.seqlen_k = seqlen_k; - bmap_args.hdim_q = hdim_q; - bmap_args.nhead_q = nhead; - bmap_args.nhead_k = nhead_k; - bmap_args.stride_q = q_strides[i_perm ? 2 : 1]; - bmap_args.stride_k = k_strides[i_perm ? 2 : 1]; - bmap_args.nhead_stride_q = q_strides[i_perm ? 1 : 2]; - bmap_args.nhead_stride_k = k_strides[i_perm ? 1 : 2]; - bmap_args.batch_stride_q = q_strides[0]; - bmap_args.batch_stride_k = k_strides[0]; - bmap_args.simthreshd1 = simthreshd1; - bmap_args.cdfthreshd = (topk < 0.0f) ? cdfthreshd : -1.0f; - bmap_args.topk = topk; - bmap_args.scale = scale_s; - bmap_args.block_map_ptr = block_map_dev.GetDeviceBuffer(); - bmap_args.lut_ptr = (pipeline == "vsa") ? lut_dev.GetDeviceBuffer() : nullptr; + bmap_args.q_ptr = q_dev.GetDeviceBuffer(); + bmap_args.k_ptr = k_dev.GetDeviceBuffer(); + bmap_args.batch = batch; + bmap_args.seqlen_q = seqlen_q; + bmap_args.seqlen_k = seqlen_k; + bmap_args.hdim_q = hdim_q; + bmap_args.nhead_q = nhead; + bmap_args.nhead_k = nhead_k; + bmap_args.stride_q = q_strides[i_perm ? 2 : 1]; + bmap_args.stride_k = k_strides[i_perm ? 2 : 1]; + bmap_args.nhead_stride_q = q_strides[i_perm ? 1 : 2]; + bmap_args.nhead_stride_k = k_strides[i_perm ? 1 : 2]; + bmap_args.batch_stride_q = q_strides[0]; + bmap_args.batch_stride_k = k_strides[0]; + bmap_args.simthreshd1 = simthreshd1; + bmap_args.cdfthreshd = (topk < 0.0f) ? cdfthreshd : -1.0f; + bmap_args.topk = topk; + bmap_args.scale = scale_s; + bmap_args.block_map_ptr = block_map_dev.GetDeviceBuffer(); + bmap_args.lut_ptr = (pipeline == "vsa") ? lut_dev.GetDeviceBuffer() : nullptr; bmap_args.valid_block_num_ptr = (pipeline == "vsa") ? valid_bn_dev.GetDeviceBuffer() : nullptr; - // R21A Phase 4 + R21B fix: per-head superparam buffers, all sized [nhead_q] - // to match SpargeAttn upstream contract (utils.py:324-328, Headnum=q.size(1)). + // K-stats workspace: caller-owned, sized via host helper, allocated once outside any timing. + const size_t ws_bytes = sparge_blockmap_get_workspace_size(bmap_traits, bmap_args); + ck_tile::DeviceMem kstats_ws_dev(ws_bytes); + bmap_args.workspace_ptr = kstats_ws_dev.GetDeviceBuffer(); + + // Per-head superparam buffers, all sized [nhead_q] to match SpargeAttn upstream contract. // K-side kernel reads only the first nhead_k entries via [hk]. + // Filled with scalar broadcast; per-head index correctness verified by separate unit test. ck_tile::DeviceMem topk_per_head_dev(static_cast(nhead) * sizeof(float)); ck_tile::DeviceMem sim1_per_head_dev(static_cast(nhead) * sizeof(float)); - ck_tile::DeviceMem cdf_per_head_dev (static_cast(nhead) * sizeof(float)); - if(perhead != 0) + ck_tile::DeviceMem cdf_per_head_dev(static_cast(nhead) * sizeof(float)); { - std::vector topk_h(nhead); - std::vector sim1_h(nhead); - std::vector cdf_h (nhead); - for(int h = 0; h < nhead; ++h) - { - // small per-head jitter around scalar topk so sparsity differs by head - const float jitter = 0.5f * (static_cast(h - nhead / 2) / nhead); - topk_h[h] = topk * (1.0f + jitter); - sim1_h[h] = simthreshd1; // bit-identical to scalar (kernel reads [0..nhead_k-1]) - cdf_h[h] = cdfthreshd; - } + std::vector topk_h(nhead, topk); + std::vector sim1_h(nhead, simthreshd1); + std::vector cdf_h(nhead, cdfthreshd); topk_per_head_dev.ToDevice(topk_h.data()); sim1_per_head_dev.ToDevice(sim1_h.data()); - cdf_per_head_dev .ToDevice(cdf_h.data()); - bmap_args.topk_per_head_ptr = static_cast(topk_per_head_dev.GetDeviceBuffer()); - bmap_args.simthreshd1_per_head_ptr = static_cast(sim1_per_head_dev.GetDeviceBuffer()); - bmap_args.cdfthreshd_per_head_ptr = static_cast(cdf_per_head_dev.GetDeviceBuffer()); + cdf_per_head_dev.ToDevice(cdf_h.data()); + bmap_args.topk_per_head_ptr = + static_cast(topk_per_head_dev.GetDeviceBuffer()); + bmap_args.simthreshd1_per_head_ptr = + static_cast(sim1_per_head_dev.GetDeviceBuffer()); + bmap_args.cdfthreshd_per_head_ptr = + static_cast(cdf_per_head_dev.GetDeviceBuffer()); } // ---- build attention args ---- ck_tile::stream_config stream_cfg; - stream_cfg.stream_id_ = nullptr; + stream_cfg.stream_id_ = nullptr; stream_cfg.time_kernel_ = true; - stream_cfg.log_level_ = kname; + stream_cfg.log_level_ = kname; stream_cfg.cold_niters_ = warmup; - stream_cfg.nrepeat_ = repeat; + stream_cfg.nrepeat_ = repeat; float avg_ms = -1.0f; @@ -283,35 +282,35 @@ bool run_test(const ck_tile::ArgParser& arg_parser) attn_traits.bm0 = BLKQ; fmha_jenga_fwd_args attn_args; - attn_args.q_ptr = q_dev.GetDeviceBuffer(); - attn_args.k_ptr = k_dev.GetDeviceBuffer(); - attn_args.v_ptr = v_dev.GetDeviceBuffer(); + attn_args.q_ptr = q_dev.GetDeviceBuffer(); + attn_args.k_ptr = k_dev.GetDeviceBuffer(); + attn_args.v_ptr = v_dev.GetDeviceBuffer(); attn_args.block_relation_onehot_ptr = block_map_dev.GetDeviceBuffer(); - attn_args.o_ptr = o_dev.GetDeviceBuffer(); - attn_args.seqlen_q = seqlen_q; - attn_args.seqlen_k = seqlen_k; - attn_args.batch = batch; - attn_args.max_seqlen_q = seqlen_q; - attn_args.hdim_q = hdim_q; - attn_args.hdim_v = hdim_v; - attn_args.nhead_q = nhead; - attn_args.nhead_k = nhead_k; - attn_args.scale_s = scale_s; - attn_args.stride_q = q_strides[i_perm ? 2 : 1]; - attn_args.stride_k = k_strides[i_perm ? 2 : 1]; - attn_args.stride_v = v_strides[i_perm ? 2 : 1]; - attn_args.stride_o = o_strides[o_perm ? 2 : 1]; - attn_args.nhead_stride_q = q_strides[i_perm ? 1 : 2]; - attn_args.nhead_stride_k = k_strides[i_perm ? 1 : 2]; - attn_args.nhead_stride_v = v_strides[i_perm ? 1 : 2]; - attn_args.nhead_stride_o = o_strides[o_perm ? 1 : 2]; - attn_args.batch_stride_q = q_strides[0]; - attn_args.batch_stride_k = k_strides[0]; - attn_args.batch_stride_v = v_strides[0]; - attn_args.batch_stride_o = o_strides[0]; - attn_args.window_size_left = -1; - attn_args.window_size_right = -1; - attn_args.mask_type = 0; + attn_args.o_ptr = o_dev.GetDeviceBuffer(); + attn_args.seqlen_q = seqlen_q; + attn_args.seqlen_k = seqlen_k; + attn_args.batch = batch; + attn_args.max_seqlen_q = seqlen_q; + attn_args.hdim_q = hdim_q; + attn_args.hdim_v = hdim_v; + attn_args.nhead_q = nhead; + attn_args.nhead_k = nhead_k; + attn_args.scale_s = scale_s; + attn_args.stride_q = q_strides[i_perm ? 2 : 1]; + attn_args.stride_k = k_strides[i_perm ? 2 : 1]; + attn_args.stride_v = v_strides[i_perm ? 2 : 1]; + attn_args.stride_o = o_strides[o_perm ? 2 : 1]; + attn_args.nhead_stride_q = q_strides[i_perm ? 1 : 2]; + attn_args.nhead_stride_k = k_strides[i_perm ? 1 : 2]; + attn_args.nhead_stride_v = v_strides[i_perm ? 1 : 2]; + attn_args.nhead_stride_o = o_strides[o_perm ? 1 : 2]; + attn_args.batch_stride_q = q_strides[0]; + attn_args.batch_stride_k = k_strides[0]; + attn_args.batch_stride_v = v_strides[0]; + attn_args.batch_stride_o = o_strides[0]; + attn_args.window_size_left = -1; + attn_args.window_size_right = -1; + attn_args.mask_type = 0; avg_ms = sparge_jenga_fwd(bmap_traits, bmap_args, attn_traits, attn_args, stream_cfg); } @@ -326,38 +325,39 @@ bool run_test(const ck_tile::ArgParser& arg_parser) attn_traits.bm0 = BLKQ; fmha_vsa_fwd_args attn_args; - attn_args.q_ptr = q_dev.GetDeviceBuffer(); - attn_args.k_ptr = k_dev.GetDeviceBuffer(); - attn_args.v_ptr = v_dev.GetDeviceBuffer(); - attn_args.lut_ptr = lut_dev.GetDeviceBuffer(); + attn_args.q_ptr = q_dev.GetDeviceBuffer(); + attn_args.k_ptr = k_dev.GetDeviceBuffer(); + attn_args.v_ptr = v_dev.GetDeviceBuffer(); + attn_args.lut_ptr = lut_dev.GetDeviceBuffer(); attn_args.valid_block_num_ptr = valid_bn_dev.GetDeviceBuffer(); - attn_args.o_ptr = o_dev.GetDeviceBuffer(); - attn_args.seqlen_q = seqlen_q; - attn_args.seqlen_k = seqlen_k; - attn_args.batch = batch; - attn_args.max_seqlen_q = seqlen_q; - attn_args.hdim_q = hdim_q; - attn_args.hdim_v = hdim_v; - attn_args.nhead_q = nhead; - attn_args.nhead_k = nhead_k; - attn_args.scale_s = scale_s; - attn_args.stride_q = q_strides[i_perm ? 2 : 1]; - attn_args.stride_k = k_strides[i_perm ? 2 : 1]; - attn_args.stride_v = v_strides[i_perm ? 2 : 1]; - attn_args.stride_o = o_strides[o_perm ? 2 : 1]; - attn_args.nhead_stride_q = q_strides[i_perm ? 1 : 2]; - attn_args.nhead_stride_k = k_strides[i_perm ? 1 : 2]; - attn_args.nhead_stride_v = v_strides[i_perm ? 1 : 2]; - attn_args.nhead_stride_o = o_strides[o_perm ? 1 : 2]; - attn_args.batch_stride_q = q_strides[0]; - attn_args.batch_stride_k = k_strides[0]; - attn_args.batch_stride_v = v_strides[0]; - attn_args.batch_stride_o = o_strides[0]; - attn_args.window_size_left = -1; - attn_args.window_size_right = -1; - attn_args.mask_type = 0; - - avg_ms = sparge_vsa_fwd_combined(bmap_traits, bmap_args, attn_traits, attn_args, stream_cfg); + attn_args.o_ptr = o_dev.GetDeviceBuffer(); + attn_args.seqlen_q = seqlen_q; + attn_args.seqlen_k = seqlen_k; + attn_args.batch = batch; + attn_args.max_seqlen_q = seqlen_q; + attn_args.hdim_q = hdim_q; + attn_args.hdim_v = hdim_v; + attn_args.nhead_q = nhead; + attn_args.nhead_k = nhead_k; + attn_args.scale_s = scale_s; + attn_args.stride_q = q_strides[i_perm ? 2 : 1]; + attn_args.stride_k = k_strides[i_perm ? 2 : 1]; + attn_args.stride_v = v_strides[i_perm ? 2 : 1]; + attn_args.stride_o = o_strides[o_perm ? 2 : 1]; + attn_args.nhead_stride_q = q_strides[i_perm ? 1 : 2]; + attn_args.nhead_stride_k = k_strides[i_perm ? 1 : 2]; + attn_args.nhead_stride_v = v_strides[i_perm ? 1 : 2]; + attn_args.nhead_stride_o = o_strides[o_perm ? 1 : 2]; + attn_args.batch_stride_q = q_strides[0]; + attn_args.batch_stride_k = k_strides[0]; + attn_args.batch_stride_v = v_strides[0]; + attn_args.batch_stride_o = o_strides[0]; + attn_args.window_size_left = -1; + attn_args.window_size_right = -1; + attn_args.mask_type = 0; + + avg_ms = + sparge_vsa_fwd_combined(bmap_traits, bmap_args, attn_traits, attn_args, stream_cfg); } else { @@ -367,8 +367,8 @@ bool run_test(const ck_tile::ArgParser& arg_parser) // ---- TFLOPS calculation (dense FMHA formula, so sparsity gains show as higher TFLOPS) ---- std::size_t flop = static_cast(batch) * nhead * - (static_cast(2) * seqlen_q * seqlen_k * hdim_q + - static_cast(2) * seqlen_q * seqlen_k * hdim_v); + (static_cast(2) * seqlen_q * seqlen_k * hdim_q + + static_cast(2) * seqlen_q * seqlen_k * hdim_v); float tflops = (avg_ms > 0.f) ? static_cast(flop) / 1.E9f / avg_ms : 0.f; if(avg_ms > 0.f) @@ -382,14 +382,15 @@ bool run_test(const ck_tile::ArgParser& arg_parser) block_map_dev.FromDevice(block_map_host.data()); // ---- count active blocks ---- - ck_tile::index_t total_blocks = batch * nhead * num_q_blocks * num_k_blocks; + ck_tile::index_t total_blocks = batch * nhead * num_q_blocks * num_k_blocks; ck_tile::index_t active_blocks = 0; for(size_t i = 0; i < block_map_host.mData.size(); ++i) if(block_map_host.mData[i]) active_blocks++; - float actual_sparsity = 1.0f - static_cast(active_blocks) / static_cast(total_blocks); - std::cout << ", sparsity=" << std::setprecision(2) << actual_sparsity - << "(" << active_blocks << "/" << total_blocks << ")" << std::flush; + float actual_sparsity = + 1.0f - static_cast(active_blocks) / static_cast(total_blocks); + std::cout << ", sparsity=" << std::setprecision(2) << actual_sparsity << "(" << active_blocks + << "/" << total_blocks << ")" << std::flush; // ---- validation ---- bool pass = true; @@ -405,8 +406,8 @@ bool run_test(const ck_tile::ArgParser& arg_parser) auto [rtol, atol] = get_error_tolerance(); - float max_diff = 0.0f; - size_t num_errors = 0; + float max_diff = 0.0f; + size_t num_errors = 0; auto output_host_bhsd = to_bhsd(output_host, o_perm); for(size_t i = 0; i < output_host_bhsd.mData.size(); ++i) @@ -423,9 +424,8 @@ bool run_test(const ck_tile::ArgParser& arg_parser) } pass = (num_errors == 0); - std::cout << ", " << (pass ? "PASS" : "FAIL") - << "(err=" << num_errors << "/" << output_host_bhsd.mData.size() - << " maxdiff=" << max_diff << ")"; + std::cout << ", " << (pass ? "PASS" : "FAIL") << "(err=" << num_errors << "/" + << output_host_bhsd.mData.size() << " maxdiff=" << max_diff << ")"; } std::cout << std::endl; diff --git a/include/ck_tile/ops/sparse_attn/kernel/fmha_fwd_jenga_kernel.hpp b/include/ck_tile/ops/sparse_attn/kernel/fmha_fwd_jenga_kernel.hpp index cd3513530d4..e461f7d7435 100644 --- a/include/ck_tile/ops/sparse_attn/kernel/fmha_fwd_jenga_kernel.hpp +++ b/include/ck_tile/ops/sparse_attn/kernel/fmha_fwd_jenga_kernel.hpp @@ -8,6 +8,7 @@ #include "ck_tile/ops/fmha/block/block_attention_bias_enum.hpp" #include "ck_tile/ops/fmha/block/variants.hpp" +#include #include #include #include @@ -133,34 +134,41 @@ struct FmhaFwdJengaKernel }; // std::variant<> can't take in a list initializer, overload for backward compatibility - CK_TILE_HOST static constexpr Kargs MakeKargs(const void* q_ptr, - const void* k_ptr, - const void* v_ptr, - const void* block_relation_onehot_ptr, - void* o_ptr, - ck_tile::index_t seqlen_q, - ck_tile::index_t seqlen_k, - ck_tile::index_t hdim_q, - ck_tile::index_t hdim_v, - ck_tile::index_t num_head_q, - ck_tile::index_t nhead_ratio_qk, - float scale_s, - ck_tile::index_t stride_q, - ck_tile::index_t stride_k, - ck_tile::index_t stride_v, - ck_tile::index_t stride_o, - ck_tile::index_t nhead_stride_q, - ck_tile::index_t nhead_stride_k, - ck_tile::index_t nhead_stride_v, - ck_tile::index_t nhead_stride_o, - ck_tile::index_t batch_stride_q, - ck_tile::index_t batch_stride_k, - ck_tile::index_t batch_stride_v, - ck_tile::index_t batch_stride_o, - ck_tile::index_t window_size_left, - ck_tile::index_t window_size_right, - ck_tile::index_t mask_type) + // 256-bool LDS staging caps N_k <= 256 (for kN0=64 -> seqlen_k <= 16384). + // Not constexpr because the assert needs runtime evaluation. + CK_TILE_HOST static Kargs MakeKargs(const void* q_ptr, + const void* k_ptr, + const void* v_ptr, + const void* block_relation_onehot_ptr, + void* o_ptr, + ck_tile::index_t seqlen_q, + ck_tile::index_t seqlen_k, + ck_tile::index_t hdim_q, + ck_tile::index_t hdim_v, + ck_tile::index_t num_head_q, + ck_tile::index_t nhead_ratio_qk, + float scale_s, + ck_tile::index_t stride_q, + ck_tile::index_t stride_k, + ck_tile::index_t stride_v, + ck_tile::index_t stride_o, + ck_tile::index_t nhead_stride_q, + ck_tile::index_t nhead_stride_k, + ck_tile::index_t nhead_stride_v, + ck_tile::index_t nhead_stride_o, + ck_tile::index_t batch_stride_q, + ck_tile::index_t batch_stride_k, + ck_tile::index_t batch_stride_v, + ck_tile::index_t batch_stride_o, + ck_tile::index_t window_size_left, + ck_tile::index_t window_size_right, + ck_tile::index_t mask_type) { + // 256-bool LDS staging caps N_k <= 256 per Q-tile. + // For kN0=64 this means seqlen_k <= 16384. + assert(ck_tile::integer_divide_ceil(seqlen_k, FmhaPipeline::kN0) <= 256 && + "256-bool LDS staging caps N_k <= 256 (for kN0=64: seqlen_k <= 16384)"); + Kargs kargs{{q_ptr, k_ptr, v_ptr, @@ -248,7 +256,11 @@ struct FmhaFwdJengaKernel CK_TILE_DEVICE void operator()(Kargs kargs) const { // allocate LDS - // Extra LDS for staging block_relation_onehot (256 bools); keep 4B alignment for LDS loads. + // Extra LDS stages 256 bools (4B-aligned for LDS loads) — caps N_k <= 256 per Q-tile, + // i.e. seqlen_k <= 256 * kN0 (for kN0=64 -> seqlen_k <= 16384). MakeKargs asserts this. + // The extra 1024B is jenga-specific: pipeline (block_fmha_pipeline_qr_ks_vs_async_jenga + // .hpp:261) stages block_relation_onehot here. Do NOT copy this `+ 256*sizeof(int)` to + // other sparse kernels (e.g. VSA) without first wiring a real reader. __shared__ char smem_ptr[GetSmemSize() + 256 * sizeof(int)]; // if (threadIdx.x==0 && blockIdx.x==0 && blockIdx.z ==0) printf("smem size: %d", diff --git a/include/ck_tile/ops/sparse_attn/kernel/fmha_fwd_vsa_kernel.hpp b/include/ck_tile/ops/sparse_attn/kernel/fmha_fwd_vsa_kernel.hpp index 5caf27756ff..14fd86e8d14 100644 --- a/include/ck_tile/ops/sparse_attn/kernel/fmha_fwd_vsa_kernel.hpp +++ b/include/ck_tile/ops/sparse_attn/kernel/fmha_fwd_vsa_kernel.hpp @@ -251,8 +251,7 @@ struct FmhaFwdVSAKernel CK_TILE_DEVICE void operator()(Kargs kargs) const { // allocate LDS - // Extra LDS for staging block_relation_onehot (256 bools); keep 4B alignment for LDS loads. - __shared__ char smem_ptr[GetSmemSize() + 256 * sizeof(int)]; + __shared__ char smem_ptr[GetSmemSize()]; // divide problem const auto [i_tile_m, i_tile_n, i_nhead, i_batch] = GetTileIndex(kargs); diff --git a/include/ck_tile/ops/sparse_attn/kernel/sparge_blockmap_kernel.hpp b/include/ck_tile/ops/sparse_attn/kernel/sparge_blockmap_kernel.hpp index 62b5b3591c0..9006ee76966 100644 --- a/include/ck_tile/ops/sparse_attn/kernel/sparge_blockmap_kernel.hpp +++ b/include/ck_tile/ops/sparse_attn/kernel/sparge_blockmap_kernel.hpp @@ -22,7 +22,7 @@ struct SpargeBlockMapKernel static constexpr index_t kN0 = Pipeline::kN0; static constexpr index_t D = Pipeline::D; - static constexpr index_t kAlignment = 16 / sizeof(QDataType); + static constexpr index_t kAlignment = 16 / sizeof(QDataType); // 16B = dwordx4 load width struct Kargs { @@ -52,19 +52,18 @@ struct SpargeBlockMapKernel void* lut_ptr; void* valid_block_num_ptr; - // R20 K-stat workspace from Kernel A - const void* pooled_k_ws_ptr; // [batch, nhead_k, N_k, D] fp32 - const void* sim_k_ws_ptr; // [batch, nhead_k, N_k] uint8 + // K-block stats workspace produced by SpargeKStatsKernel + const void* + pooled_k_ws_ptr; // [batch, nhead_k, N_k, D] KDataType (fp16/bf16, matches K dtype) + const void* sim_k_ws_ptr; // [batch, nhead_k, N_k] uint8 index_t N_k; - // R21A Phase 4: optional per-head topk (size = nhead_q floats). - // nullptr => use scalar `topk` for all heads. + // Per-head topk (size = nhead_q floats). Required (non-null). const float* topk_per_head; - // R21B: optional per-head cdfthreshd (size = nhead_q floats). - // nullptr => use scalar `cdfthreshd` for all heads. - // Only consulted on topk<=0 path; bench currently always uses topk path. + // Per-head cdfthreshd (size = nhead_q floats). Required (non-null); + // only consulted on topk<=0 path. const float* cdfthreshd_per_head; }; @@ -90,8 +89,8 @@ struct SpargeBlockMapKernel void* valid_block_num_ptr, const void* pooled_k_ws_ptr, const void* sim_k_ws_ptr, - const float* topk_per_head = nullptr, - const float* cdfthreshd_per_head = nullptr) + const float* topk_per_head, + const float* cdfthreshd_per_head) { const index_t N_k = integer_divide_ceil(seqlen_k, kN0); return Kargs{q_ptr, @@ -195,20 +194,15 @@ struct SpargeBlockMapKernel // Shared memory __shared__ char smem[Pipeline::GetSmemSize()]; - // R20 K-stat workspace: pre-offset for this (b, hk). - const index_t nhead_k = kargs.nhead_q / kargs.nhead_ratio_qk; + // K-stat workspace: pre-offset for this (b, hk). + const index_t nhead_k = kargs.nhead_q / kargs.nhead_ratio_qk; const index_t khead_off = (b * nhead_k + hk) * N_k; const auto* pooled_k_ws = - reinterpret_cast(kargs.pooled_k_ws_ptr) + khead_off * D; - const auto* sim_k_ws = - reinterpret_cast(kargs.sim_k_ws_ptr) + khead_off; - - // R21A Phase 4: per-head topk if provided, else scalar broadcast. - const float topk_eff = - (kargs.topk_per_head != nullptr) ? kargs.topk_per_head[hq] : kargs.topk; - // R21B: per-head cdfthreshd if provided, else scalar broadcast. - const float cdfthreshd_eff = - (kargs.cdfthreshd_per_head != nullptr) ? kargs.cdfthreshd_per_head[hq] : kargs.cdfthreshd; + reinterpret_cast(kargs.pooled_k_ws_ptr) + khead_off * D; + const auto* sim_k_ws = reinterpret_cast(kargs.sim_k_ws_ptr) + khead_off; + + const float topk_eff = kargs.topk_per_head[hq]; + const float cdfthreshd_eff = kargs.cdfthreshd_per_head[hq]; Pipeline{}(q_window, k_window, diff --git a/include/ck_tile/ops/sparse_attn/kernel/sparge_kstats_kernel.hpp b/include/ck_tile/ops/sparse_attn/kernel/sparge_kstats_kernel.hpp index 3ce494f8702..893e9a232e4 100644 --- a/include/ck_tile/ops/sparse_attn/kernel/sparge_kstats_kernel.hpp +++ b/include/ck_tile/ops/sparse_attn/kernel/sparge_kstats_kernel.hpp @@ -40,15 +40,13 @@ struct SpargeKStatsKernel float simthreshd1; - void* pooled_k_ptr; // [batch, nhead_k, N_k, D] fp32 + void* pooled_k_ptr; // [batch, nhead_k, N_k, D] KDataType (fp16/bf16, matches K dtype) void* sim_k_ptr; // [batch, nhead_k, N_k] uint8 index_t N_k; - // R21A Phase 4 + R21B fix: optional per-head simthreshd1. - // Buffer is sized [nhead_q] floats to match SpargeAttn upstream contract - // (utils.py:324, Headnum=q.size(1)). Kernel only indexes the first - // nhead_k entries via [hk]. nullptr => use scalar `simthreshd1`. + // Per-head simthreshd1 pointer (size = nhead_q floats; kernel indexes [hk] only). + // Required (non-null); matches SpargeAttn upstream contract. const float* simthreshd1_per_head; }; @@ -62,7 +60,7 @@ struct SpargeKStatsKernel float simthreshd1, void* pooled_k_ptr, void* sim_k_ptr, - const float* simthreshd1_per_head = nullptr) + const float* simthreshd1_per_head) { const index_t N_k = integer_divide_ceil(seqlen_k, kN0); return Kargs{k_ptr, @@ -111,17 +109,15 @@ struct SpargeKStatsKernel {0, 0}, Pipeline::MakeKBlockDistribution()); - const index_t N_k = kargs.N_k; - const index_t khead_off = (b * kargs.nhead_k + hk) * N_k; - auto* pooled_k_out = reinterpret_cast(kargs.pooled_k_ptr) + (khead_off + kb) * D; - auto* sim_k_out = reinterpret_cast(kargs.sim_k_ptr) + (khead_off + kb); + const index_t N_k = kargs.N_k; + const index_t khead_off = (b * kargs.nhead_k + hk) * N_k; + auto* pooled_k_out = + reinterpret_cast(kargs.pooled_k_ptr) + (khead_off + kb) * D; + auto* sim_k_out = reinterpret_cast(kargs.sim_k_ptr) + (khead_off + kb); __shared__ char smem[Pipeline::GetSmemSize()]; - // R21A Phase 4: per-head simthreshd1 if provided, else scalar broadcast. - const float simthreshd1_eff = (kargs.simthreshd1_per_head != nullptr) - ? kargs.simthreshd1_per_head[hk] - : kargs.simthreshd1; + const float simthreshd1_eff = kargs.simthreshd1_per_head[hk]; Pipeline{}(k_window, kargs.seqlen_k, diff --git a/include/ck_tile/ops/sparse_attn/pipeline/sparge_blockmap_pipeline.hpp b/include/ck_tile/ops/sparse_attn/pipeline/sparge_blockmap_pipeline.hpp index 25e3b964e93..8d813aa5782 100644 --- a/include/ck_tile/ops/sparse_attn/pipeline/sparge_blockmap_pipeline.hpp +++ b/include/ck_tile/ops/sparse_attn/pipeline/sparge_blockmap_pipeline.hpp @@ -262,7 +262,7 @@ struct SpargeBlockMapPipeline uint8_t* block_map_ptr, int32_t* lut_ptr, int32_t* valid_block_num_ptr, - const float* __restrict__ pooled_k_ws_ptr, + const KDataType* __restrict__ pooled_k_ws_ptr, const uint8_t* __restrict__ sim_k_ws_ptr, void* smem_ptr) const { @@ -356,10 +356,10 @@ struct SpargeBlockMapPipeline for(index_t kb = 0; kb < N_k; ++kb) { - const float* p_kb = pooled_k_ws_ptr + kb * D + k_idx_kb * KPerThread; + const KDataType* p_kb = pooled_k_ws_ptr + kb * D + k_idx_kb * KPerThread; float pooled_k_mean[KPerThread]; for(index_t k = 0; k < KPerThread; ++k) - pooled_k_mean[k] = p_kb[k]; + pooled_k_mean[k] = type_convert(p_kb[k]); float dot = 0.f; for(index_t k = 0; k < KPerThread; ++k) @@ -417,8 +417,7 @@ struct SpargeBlockMapPipeline // cdfthreshd path (topk <= 0) still requires normalised scores so the // accumulator `cumulative_prob` matches probabilities. const bool topk_active = (topk > 0.f); - const float inv_sum = - (!topk_active && sum_exp > 0.f) ? (1.0f / sum_exp) : 0.f; + const float inv_sum = (!topk_active && sum_exp > 0.f) ? (1.0f / sum_exp) : 0.f; if(!topk_active) { for(index_t i = tid; i < N_k; i += kBlockSize) diff --git a/include/ck_tile/ops/sparse_attn/pipeline/sparge_kstats_pipeline.hpp b/include/ck_tile/ops/sparse_attn/pipeline/sparge_kstats_pipeline.hpp index 1cb96d716a3..9c122d8dea6 100644 --- a/include/ck_tile/ops/sparse_attn/pipeline/sparge_kstats_pipeline.hpp +++ b/include/ck_tile/ops/sparse_attn/pipeline/sparge_kstats_pipeline.hpp @@ -49,8 +49,8 @@ struct SpargeKStatsPipeline index_t seqlen_k, index_t kb, float simthreshd1, - float* __restrict__ pooled_k_out, // D floats - uint8_t* __restrict__ sim_k_out, // 1 byte + KDataType* __restrict__ pooled_k_out, // D KDataType (fp16/bf16) + uint8_t* __restrict__ sim_k_out, // 1 byte void* smem_ptr) const { const index_t tid = static_cast(threadIdx.x); @@ -70,19 +70,19 @@ struct SpargeKStatsPipeline const index_t m_idx = lane_id / KThreads; // pooled_k_mean: column sum then cross-warp reduce. - // R21A: drop trailing sync (next cross_warp_reduce has its own leading sync). + // Drop trailing sync (next cross_warp_reduce has its own leading sync). float pooled_k_mean[KPerThread]; Base::template column_reduce_thread_and_warp(k_data, pooled_k_mean); Base::template column_reduce_cross_warp(pooled_k_mean, smem_reduce); for(index_t k = 0; k < KPerThread; ++k) pooled_k_mean[k] *= inv_bs_k; - // R21A: write pooled_k_mean to global early so its register liveness ends here, + // Write pooled_k_mean to global early so its register liveness ends here, // freeing VGPR before k_sum_hat becomes live. if(warp_id == 0 && m_idx == 0) { for(index_t k = 0; k < KPerThread; ++k) - pooled_k_out[k_idx * KPerThread + k] = pooled_k_mean[k]; + pooled_k_out[k_idx * KPerThread + k] = type_convert(pooled_k_mean[k]); } // K row L2 norms + normalised column sum (k_sum_hat) @@ -91,7 +91,7 @@ struct SpargeKStatsPipeline float k_sum_hat[KPerThread]; Base::template column_reduce_normalised(k_data, k_psq, k_sum_hat, bs_k); - // R21A: drop trailing sync (no further smem read; only intra-warp shuffle + global write). + // Drop trailing sync (no further smem read; only intra-warp shuffle + global write). Base::template column_reduce_cross_warp(k_sum_hat, smem_reduce); // sim_k = (||k_sum_hat||^2 / bs_k^2) > simthreshd1 From 879d50836e72d54f0f101f988d02953b35ef330b Mon Sep 17 00:00:00 2001 From: Gino Lu Date: Sun, 17 May 2026 02:35:07 -0400 Subject: [PATCH 10/16] cleanup(sparse_attn): R-tag rename + clang-format sweep MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Strip internal R-tag / phase labels (R20, R21A/B, Round 8/13f, Track F, B2.v3, Phase 1/2/3) from comments — replace with descriptive names so future readers don't need the change-log. Reflow long signature in fmha_fwd_trek.hpp. Co-Authored-By: Claude Opus 4 --- .../ck_tile/50_sparse_attn/fmha_fwd_trek.hpp | 4 +- .../pipeline/sparge_blockmap_pipeline.hpp | 62 +++++++++---------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/example/ck_tile/50_sparse_attn/fmha_fwd_trek.hpp b/example/ck_tile/50_sparse_attn/fmha_fwd_trek.hpp index 62d40ffbe02..384f7bb56d1 100644 --- a/example/ck_tile/50_sparse_attn/fmha_fwd_trek.hpp +++ b/example/ck_tile/50_sparse_attn/fmha_fwd_trek.hpp @@ -283,7 +283,9 @@ float fmha_jenga_fwd_(const ck_tile::stream_config&, fmha_jenga_fwd_args); template void fmha_jenga_fwd_oneshot_(const ck_tile::stream_config&, fmha_jenga_fwd_args); -void fmha_jenga_fwd_oneshot(fmha_jenga_fwd_traits, fmha_jenga_fwd_args, const ck_tile::stream_config&); +void fmha_jenga_fwd_oneshot(fmha_jenga_fwd_traits, + fmha_jenga_fwd_args, + const ck_tile::stream_config&); // VSA uses the same traits structure as Jenga; aliases for clarity template CK_TILE_DEVICE static void column_reduce_cross_warp(float (&col_acc)[KPerThread], float* __restrict__ smem_reduce) @@ -121,9 +120,9 @@ struct SpargeBlockMapPipeline const index_t k_idx = lane_id % KThreads; const index_t m_idx = lane_id / KThreads; - // B2.v3 column-stride pad: stride k_idx by (KPerThread+1)=9 instead of 8, - // changing per-lane bank from (k_idx*8+k)%32 to (k_idx*9+k)%32. For k=0, - // lanes (k_idx={0,4,8,12}) now hit banks {0,4,8,12} instead of all 0. + // Column-stride pad: stride k_idx by (KPerThread+1)=9 instead of 8, changing + // per-lane bank from (k_idx*8+k)%32 to (k_idx*9+k)%32. For k=0, lanes + // (k_idx={0,4,8,12}) hit banks {0,4,8,12} instead of all 0. if(m_idx == 0) for(index_t k = 0; k < KPerThread; ++k) smem_reduce[warp_id * kPerWarpFloats + k_idx * kColPaddedStride + k] = col_acc[k]; @@ -268,7 +267,7 @@ struct SpargeBlockMapPipeline { const index_t tid = static_cast(threadIdx.x); - // R20: K-loop no longer reduces, only Phase 1 uses smem_float0. + // K-loop no longer reduces; only Q-stats uses smem_float0. // smem_float1 slab is allocated for layout compat but unused. auto* smem_float0 = reinterpret_cast(smem_ptr); auto* smem_scores = @@ -282,7 +281,7 @@ struct SpargeBlockMapPipeline const float inv_bs_q = (bs_q > 0) ? (1.0f / static_cast(bs_q)) : 0.f; // ================================================================== - // Phase 1: Q Block Statistics + // Q Block Statistics // ================================================================== auto q_tile = load_tile(q_window_in); @@ -294,9 +293,8 @@ struct SpargeBlockMapPipeline row_reduce_sq_norm(q_data, psq, bs_q); // 1b. Column sum -> mean - // Track F (re-apply R8 b2): drop trailing sync. Next reduce reuses same slab - // (smem_float0) and has its own leading __syncthreads() before reading. - // pooled_q_mean is register-only between reduces. + // Drop trailing sync: next reduce reuses same slab (smem_float0) with its own + // leading __syncthreads() before reading. pooled_q_mean is register-only between reduces. float pooled_q_mean[KPerThread]; column_reduce_thread_and_warp(q_data, pooled_q_mean); column_reduce_cross_warp(pooled_q_mean, smem_float0); @@ -304,9 +302,9 @@ struct SpargeBlockMapPipeline pooled_q_mean[k] *= inv_bs_q; // 1c. Normalised sum_hat - // Track F (re-apply R8 b2): drop trailing sync. Next cross-warp reduce in - // K-loop iter 0 writes slab_a=smem_float0 (kb=0 even). Although same slab, - // its leading __syncthreads() covers the WAR. sum_hat register-only here. + // Drop trailing sync: next cross-warp reduce in K-loop iter 0 writes + // slab_a=smem_float0 (kb=0 even); its leading __syncthreads() covers the WAR. + // sum_hat is register-only here. float sum_hat[KPerThread]; column_reduce_normalised(q_data, psq, sum_hat, bs_q); column_reduce_cross_warp(sum_hat, smem_float0); @@ -342,15 +340,15 @@ struct SpargeBlockMapPipeline } // ================================================================== - // Phase 2: K Block Loop + // K Block Loop // ================================================================== for(index_t i = tid; i < N_k; i += kBlockSize) smem_bmap[i] = 0; __syncthreads(); - // R20: K-stats precomputed by Kernel A. Each thread loads its own - // KPerThread-slice of pooled_k_mean from DRAM workspace; sim_k is a single - // byte. No K-tile load, no cross-warp reduce in the K-loop. + // K-stats precomputed by SpargeKStatsKernel. Each thread loads its own + // KPerThread-slice of pooled_k_mean from DRAM workspace; sim_k is a single byte. + // No K-tile load, no cross-warp reduce in the K-loop. const index_t lane_id_kb = tid % WarpSize; const index_t k_idx_kb = lane_id_kb % KThreads; @@ -372,10 +370,10 @@ struct SpargeBlockMapPipeline { // INVARIANT (mirrors SpargeAttn ref utils.py:175-180): // ~sim_k blocks are forced ON in the bitmap (final_map[~sim_k]=1) - // AND have score = -inf so Phase 3 selection (topk / cdf) does NOT + // AND have score = -inf so the selection step (topk / cdf) does NOT // pick them again (would double-count toward topk budget). - // Both writes MUST stay together. Any Phase 3 selection rewrite - // (e.g. iterative argmax → bitonic sort) must keep the -inf write. + // Both writes MUST stay together. Any selection rewrite + // (e.g. iterative argmax -> bitonic sort) must keep the -inf write. if(!sim_k) { smem_bmap[kb] = 1; @@ -387,10 +385,10 @@ struct SpargeBlockMapPipeline } } } - __syncthreads(); // guard Phase 3's reads of smem_bmap / smem_scores + __syncthreads(); // guard selection's reads of smem_bmap / smem_scores // ================================================================== - // Phase 3: Softmax + Selection + // Softmax + Selection // ================================================================== // max From 840b8a37d97e2285fe2252a47118dce0068cce55 Mon Sep 17 00:00:00 2001 From: Gino Lu Date: Sun, 17 May 2026 02:35:51 -0400 Subject: [PATCH 11/16] test(sparse_attn): CPU-ref cross-check + BLKQ cite Wire SpargeAttn CPU reference into test_sparge: build the block_map on host via sparge::build_block_map_meansim and cross-check against the GPU-produced map; self-check the VSA delta-LUT (valid count + reachable kb indices); split PASS/FAIL into separate block_map / LUT / attention-output lines for clearer diagnosis. Set sparge_tool::SpargeParams::BLKQ default to 64 to match SpargeAttn SM90 convention (cite upstream qk_int_sv_f8_cuda_sm90.cu:143-144); tighten bf16 tolerance back to the dense FMHA baseline (4e-2 atol, 1e-2 rtol). Co-Authored-By: Claude Opus 4 --- .../ck_tile/50_sparse_attn/sparge_tool.hpp | 5 +- .../ck_tile/50_sparse_attn/test_sparge.cpp | 111 ++++++++++++++++-- 2 files changed, 102 insertions(+), 14 deletions(-) diff --git a/example/ck_tile/50_sparse_attn/sparge_tool.hpp b/example/ck_tile/50_sparse_attn/sparge_tool.hpp index 49c69cc6f74..94426c6fd8a 100644 --- a/example/ck_tile/50_sparse_attn/sparge_tool.hpp +++ b/example/ck_tile/50_sparse_attn/sparge_tool.hpp @@ -16,7 +16,10 @@ namespace sparge { struct SpargeParams { - int BLKQ = 128; + // BLKQ=64, BLKK=128 align with SpargeAttn SM90 (Hopper) convention; + // cf. upstream csrc/qattn/qk_int_sv_f8_cuda_sm90.cu:143-144. + // SM80/SM89 path uses the inverse 128/64 (cf. qk_int_sv_f16_cuda_sm80.cu:137-138). + int BLKQ = 64; int BLKK = 128; // Similarity gate threshold (TODO: per-head support). diff --git a/example/ck_tile/50_sparse_attn/test_sparge.cpp b/example/ck_tile/50_sparse_attn/test_sparge.cpp index a2cf101cf1f..73e58f0c249 100644 --- a/example/ck_tile/50_sparse_attn/test_sparge.cpp +++ b/example/ck_tile/50_sparse_attn/test_sparge.cpp @@ -57,13 +57,10 @@ ck_tile::HostTensor to_bhsd(const ck_tile::HostTensor& tensor, bool is_bhs template auto get_error_tolerance() { + // Matches dense FMHA fp16/bf16 bounds; validated on (b=1,h=2,d=128, + // s in {512, 2048, 4096, 8192}) with maxdiff = 0.00 across both dtypes. double rtol = 1e-2; double atol = 4e-2; - if constexpr(std::is_same_v) - { - atol = 2e-1; - rtol = 2e-1; - } return ck_tile::make_tuple(rtol, atol); } @@ -76,11 +73,7 @@ float to_float_for_compare(T value) template <> float to_float_for_compare(ck_tile::bf16_t value) { -#if CK_TILE_USE_CUSTOM_DATA_TYPE - return static_cast(value); -#else - return ck_tile::bf16_to_float_raw(ck_tile::bit_cast(value)); -#endif + return ck_tile::type_convert(value); } // ============================================================================ @@ -400,6 +393,97 @@ bool run_test(const ck_tile::ArgParser& arg_parser) auto k_ref = to_bhsd(k_host, i_perm); auto v_ref = to_bhsd(v_host, i_perm); + sparge::SpargeParams sp; + sp.BLKQ = BLKQ; + sp.BLKK = BLKK; + sp.simthreshd1 = simthreshd1; + sp.cdfthreshd = cdfthreshd; + sp.topk = topk; + sp.i_perm = i_perm; + + auto block_map_cpu = sparge::build_block_map_meansim(q_host, k_host, sp); + + size_t bm_total = block_map_host.mData.size(); + size_t bm_mismatch = 0; + size_t shown = 0; + constexpr size_t MAXSHOW = 10; + std::cout << "\n [block_map cross-check] total=" << bm_total; + for(size_t i = 0; i < bm_total; ++i) + { + uint8_t g = block_map_host.mData[i]; + uint8_t c = block_map_cpu.mData[i]; + if(g != c) + { + if(shown < MAXSHOW) + { + size_t k_idx = i % num_k_blocks; + size_t q_idx = (i / num_k_blocks) % num_q_blocks; + size_t h_idx = (i / (num_k_blocks * num_q_blocks)) % nhead; + size_t b_idx = i / (num_k_blocks * num_q_blocks * nhead); + std::cout << "\n miss[" << shown << "] (b=" << b_idx << ",h=" << h_idx + << ",qb=" << q_idx << ",kb=" << k_idx << ") gpu=" << int(g) + << " cpu=" << int(c); + ++shown; + } + ++bm_mismatch; + } + } + bool bm_pass = (bm_mismatch == 0); + float bm_ratio = bm_total ? 100.0f * float(bm_mismatch) / float(bm_total) : 0.0f; + std::cout << "\n [block_map cross-check] mismatch=" << bm_mismatch << "/" << bm_total + << " (" << std::setprecision(4) << bm_ratio << "%) " + << (bm_pass ? "PASS" : "FAIL"); + + auto cpu_lut = sparge::block_map_to_vsa_lut_delta(block_map_cpu); + bool lut_pass = true; + size_t lut_fails = 0; + for(ck_tile::index_t b = 0; b < batch && lut_fails < MAXSHOW; ++b) + { + for(ck_tile::index_t h = 0; h < nhead && lut_fails < MAXSHOW; ++h) + { + for(ck_tile::index_t qb = 0; qb < num_q_blocks && lut_fails < MAXSHOW; ++qb) + { + int32_t valid = cpu_lut.valid_block_num(b, h, qb); + int32_t active_count = 0; + for(ck_tile::index_t kb = 0; kb < num_k_blocks; ++kb) + if(block_map_cpu(b, h, qb, kb)) + ++active_count; + int32_t recon_kb = 0; + bool delta_ok = true; + for(int32_t i = 0; i < valid; ++i) + { + int32_t d = cpu_lut.lut(b, h, qb, i); + if(d < 0) + { + delta_ok = false; + break; + } + recon_kb += d; + if(recon_kb >= num_k_blocks) + { + delta_ok = false; + break; + } + if(!block_map_cpu(b, h, qb, recon_kb)) + { + delta_ok = false; + break; + } + } + if(valid != active_count || !delta_ok) + { + lut_pass = false; + if(lut_fails < MAXSHOW) + std::cout << "\n lut_fail (b=" << b << ",h=" << h << ",qb=" << qb + << ") valid=" << valid << " active=" << active_count + << " delta_ok=" << delta_ok; + ++lut_fails; + } + } + } + } + std::cout << "\n [VSA LUT self-consistency] " << (lut_pass ? "PASS" : "FAIL"); + ck_tile::HostTensor output_ref({batch, nhead, seqlen_q, hdim_v}); ck_tile::reference_blocked_attention( q_ref, k_ref, v_ref, block_map_host, output_ref, BLKQ, BLKK, scale_s); @@ -423,9 +507,10 @@ bool run_test(const ck_tile::ArgParser& arg_parser) num_errors++; } - pass = (num_errors == 0); - std::cout << ", " << (pass ? "PASS" : "FAIL") << "(err=" << num_errors << "/" - << output_host_bhsd.mData.size() << " maxdiff=" << max_diff << ")"; + pass = (num_errors == 0) && bm_pass && lut_pass; + std::cout << "\n [attention output] " << ((num_errors == 0) ? "PASS" : "FAIL") + << "(err=" << num_errors << "/" << output_host_bhsd.mData.size() + << " maxdiff=" << max_diff << ")"; } std::cout << std::endl; From 0f8b58ac886f56191139f544b11a44028a4be970 Mon Sep 17 00:00:00 2001 From: Gino Lu Date: Mon, 18 May 2026 06:13:38 -0400 Subject: [PATCH 12/16] =?UTF-8?q?sparse=5Fattn:=20R25=20Step=201=20A1=20?= =?UTF-8?q?=E2=80=94=20per-warp=20PV-skip=20(paper=20Algorithm=201)=20+=20?= =?UTF-8?q?V0=20instantiation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Preserve the R25 Step 1 "A1 / redesign D" state before redesigning toward "B" (per-CTA PV-skip matching upstream shipped reference). This snapshot lets us restore A1 if the B redesign fails. A1 redesign D pipeline (per-warp, arithmetic-only PV-skip, wrapped in `if constexpr (kEnablePVSkip)`): - include/ck_tile/ops/sparse_attn/pipeline/block_fmha_pipeline_qr_ks_vs_async_sparge.hpp - include/ck_tile/ops/sparse_attn/kernel/fmha_fwd_sparge_kernel.hpp V0 instantiation wiring (per gino_tmp/R25/programmer/v0_instance/REPORT.md): - example/ck_tile/50_sparse_attn/codegen/ops/fmha_fwd_sparge.py - example/ck_tile/50_sparse_attn/fmha_fwd_trek.hpp - example/ck_tile/50_sparse_attn/sparge_blockmap_trek.hpp - example/ck_tile/50_sparse_attn/sparge_blockmap_inst.cpp - example/ck_tile/50_sparse_attn/codegen/cpp_symbol_map.py - example/ck_tile/50_sparse_attn/CMakeLists.txt - example/ck_tile/01_fmha/CMakeLists.txt - example/ck_tile/50_sparse_attn/test_sparge.cpp (-pv_skip_compile=0|1 CLI) This commit excludes all *_REVIEW.{hpp,cpp} mirror files (left untracked) and all build artefacts. _vsa.hpp / _jenga.hpp are not modified. Tag: R25-step1-A1-paper-aligned points at this commit. --- example/ck_tile/01_fmha/CMakeLists.txt | 2 + example/ck_tile/50_sparse_attn/CMakeLists.txt | 56 + .../50_sparse_attn/codegen/cpp_symbol_map.py | 2 + .../codegen/ops/fmha_fwd_sparge.py | 1041 +++++++++++++++++ .../ck_tile/50_sparse_attn/fmha_fwd_trek.hpp | 138 +++ .../50_sparse_attn/sparge_blockmap_inst.cpp | 21 + .../50_sparse_attn/sparge_blockmap_trek.hpp | 6 + .../ck_tile/50_sparse_attn/test_sparge.cpp | 46 +- .../kernel/fmha_fwd_sparge_kernel.hpp | 442 +++++++ ...ck_fmha_pipeline_qr_ks_vs_async_sparge.hpp | 698 +++++++++++ 10 files changed, 2448 insertions(+), 4 deletions(-) create mode 100644 example/ck_tile/50_sparse_attn/codegen/ops/fmha_fwd_sparge.py create mode 100644 include/ck_tile/ops/sparse_attn/kernel/fmha_fwd_sparge_kernel.hpp create mode 100644 include/ck_tile/ops/sparse_attn/pipeline/block_fmha_pipeline_qr_ks_vs_async_sparge.hpp diff --git a/example/ck_tile/01_fmha/CMakeLists.txt b/example/ck_tile/01_fmha/CMakeLists.txt index 35afb1181e0..791ce21c868 100644 --- a/example/ck_tile/01_fmha/CMakeLists.txt +++ b/example/ck_tile/01_fmha/CMakeLists.txt @@ -200,6 +200,7 @@ message(DEBUG "adding example ${EXAMPLE_FMHA_FWD}") add_executable(${EXAMPLE_FMHA_FWD} EXCLUDE_FROM_ALL example_fmha_fwd.cpp) target_link_libraries(${EXAMPLE_FMHA_FWD} ${FMHA_FWD_INSTANCES}) target_include_directories(${EXAMPLE_FMHA_FWD} PRIVATE ${CMAKE_CURRENT_LIST_DIR}) +set_property(TARGET ${EXAMPLE_FMHA_FWD} PROPERTY HIP_ARCHITECTURES ${INST_TARGETS}) message(DEBUG "adding example ${EXAMPLE_FMHA_BWD}") # not using add_example_executable() to add this target, since we don't want this to be included in @@ -207,6 +208,7 @@ message(DEBUG "adding example ${EXAMPLE_FMHA_BWD}") add_executable(${EXAMPLE_FMHA_BWD} EXCLUDE_FROM_ALL example_fmha_bwd.cpp) target_link_libraries(${EXAMPLE_FMHA_BWD} ${FMHA_BWD_INSTANCES}) target_include_directories(${EXAMPLE_FMHA_BWD} PRIVATE ${CMAKE_CURRENT_LIST_DIR}) +set_property(TARGET ${EXAMPLE_FMHA_BWD} PROPERTY HIP_ARCHITECTURES ${INST_TARGETS}) # TODO: we have to turn off this global prop, otherwise the progress bar generated # by cmake will print too many files, execvp: /bin/sh: Argument list too long diff --git a/example/ck_tile/50_sparse_attn/CMakeLists.txt b/example/ck_tile/50_sparse_attn/CMakeLists.txt index b20a661805f..532285ce5a6 100644 --- a/example/ck_tile/50_sparse_attn/CMakeLists.txt +++ b/example/ck_tile/50_sparse_attn/CMakeLists.txt @@ -83,6 +83,7 @@ message(DEBUG "adding example ${EXAMPLE_JENGA_SPARSE_ATTN}") add_executable(${EXAMPLE_JENGA_SPARSE_ATTN} EXCLUDE_FROM_ALL test_jenga_sparse_attn.cpp) target_link_libraries(${EXAMPLE_JENGA_SPARSE_ATTN} ${SPARSE_ATTN_JENGA_INSTANCES}) target_include_directories(${EXAMPLE_JENGA_SPARSE_ATTN} PRIVATE ${CMAKE_CURRENT_LIST_DIR}) +set_property(TARGET ${EXAMPLE_JENGA_SPARSE_ATTN} PROPERTY HIP_ARCHITECTURES ${INST_TARGETS}) target_compile_options(${EXAMPLE_JENGA_SPARSE_ATTN} PRIVATE -Wno-undefined-func-template -Wno-float-equal @@ -148,11 +149,64 @@ message(DEBUG "adding example ${EXAMPLE_VSA_SPARSE_ATTN}") add_executable(${EXAMPLE_VSA_SPARSE_ATTN} EXCLUDE_FROM_ALL test_vsa_sparse_attn.cpp) target_link_libraries(${EXAMPLE_VSA_SPARSE_ATTN} ${SPARSE_ATTN_VSA_INSTANCES}) target_include_directories(${EXAMPLE_VSA_SPARSE_ATTN} PRIVATE ${CMAKE_CURRENT_LIST_DIR}) +set_property(TARGET ${EXAMPLE_VSA_SPARSE_ATTN} PROPERTY HIP_ARCHITECTURES ${INST_TARGETS}) target_compile_options(${EXAMPLE_VSA_SPARSE_ATTN} PRIVATE -Wno-undefined-func-template -Wno-float-equal ) +# ============================================================================ +# Sparge Sparse Attention (PV-skip enabled, derived from VSA) +# ============================================================================ +set(SPARSE_ATTN_SPARGE_CODE_GEN_ARGS + ${CMAKE_CURRENT_LIST_DIR}/generate.py + --api fwd_sparge + --receipt 600 +) + +# Generate list of Sparge kernels (at configure time, only list) +execute_process( + COMMAND ${Python3_EXECUTABLE} ${SPARSE_ATTN_SPARGE_CODE_GEN_ARGS} + --list_blobs ${CMAKE_CURRENT_BINARY_DIR}/sparge_blob_list.txt + RESULT_VARIABLE ret +) +if(ret AND NOT ret EQUAL 0) + message(FATAL_ERROR "Failed to generate Sparge kernel list") +endif() + +file(STRINGS ${CMAKE_CURRENT_BINARY_DIR}/sparge_blob_list.txt SPARSE_ATTN_SPARGE_GEN_BLOBS) + +# Generate Sparge kernel source files at build time +add_custom_command( + OUTPUT ${SPARSE_ATTN_SPARGE_GEN_BLOBS} + COMMAND ${Python3_EXECUTABLE} ${SPARSE_ATTN_SPARGE_CODE_GEN_ARGS} + --output_dir ${CMAKE_CURRENT_BINARY_DIR} + DEPENDS ${CODE_GEN_SCRIPTS} + COMMENT "Generate CK Tile Sparge Sparse Attention kernels" +) + +message(STATUS "Sparge kernel files to be generated: ${SPARSE_ATTN_SPARGE_GEN_BLOBS}") + +# Sparge Instances +set(SPARSE_ATTN_SPARGE_INSTANCES "tile_sparse_attn_sparge_instances") + +add_library(${SPARSE_ATTN_SPARGE_INSTANCES} OBJECT EXCLUDE_FROM_ALL + ${SPARSE_ATTN_SPARGE_GEN_BLOBS} +) +target_include_directories(${SPARSE_ATTN_SPARGE_INSTANCES} PRIVATE + ${CMAKE_CURRENT_LIST_DIR} + ${PROJECT_SOURCE_DIR}/include/ck_tile/ops/sparse_attn +) +set_source_files_properties(${SPARSE_ATTN_SPARGE_GEN_BLOBS} PROPERTIES LANGUAGE HIP) +set_property(TARGET ${SPARSE_ATTN_SPARGE_INSTANCES} PROPERTY HIP_ARCHITECTURES ${INST_TARGETS}) + +target_compile_options(${SPARSE_ATTN_SPARGE_INSTANCES} PRIVATE + -DCK_TILE_USE_BUFFER_ADDRESSING_BUILTIN + -DCK_TILE_FMHA_FWD_FAST_EXP2 + -Wno-undefined-func-template + -Wno-float-equal +) + # ============================================================================ # Sparge BlockMap GPU Kernel (hand-written instantiation, no codegen) # ============================================================================ @@ -188,9 +242,11 @@ add_executable(${EXAMPLE_SPARGE} EXCLUDE_FROM_ALL test_sparge.cpp) target_link_libraries(${EXAMPLE_SPARGE} ${SPARSE_ATTN_JENGA_INSTANCES} ${SPARSE_ATTN_VSA_INSTANCES} + ${SPARSE_ATTN_SPARGE_INSTANCES} ${SPARGE_BLOCKMAP_INSTANCES} ) target_include_directories(${EXAMPLE_SPARGE} PRIVATE ${CMAKE_CURRENT_LIST_DIR}) +set_property(TARGET ${EXAMPLE_SPARGE} PROPERTY HIP_ARCHITECTURES ${INST_TARGETS}) target_compile_options(${EXAMPLE_SPARGE} PRIVATE -Wno-undefined-func-template -Wno-float-equal diff --git a/example/ck_tile/50_sparse_attn/codegen/cpp_symbol_map.py b/example/ck_tile/50_sparse_attn/codegen/cpp_symbol_map.py index 8614a1ff3ba..0f2866cbf42 100644 --- a/example/ck_tile/50_sparse_attn/codegen/cpp_symbol_map.py +++ b/example/ck_tile/50_sparse_attn/codegen/cpp_symbol_map.py @@ -58,11 +58,13 @@ def get_mask_check_map(mask: str): PIPELINE_MAP = { "qr_async": "ck_tile::BlockFmhaPipelineQRKSVSAsyncJenga", "qr_async_vsa": "ck_tile::BlockFmhaPipelineQRKSVSAsyncVSA", + "qr_async_sparge": "ck_tile::BlockFmhaPipelineQRKSVSAsyncSparge", } PIPELINE_ENUM_MAP = { "qr_async": "ck_tile::BlockFmhaPipelineEnum::QRKSVS_ASYNC", "qr_async_vsa": "ck_tile::BlockFmhaPipelineEnum::QRKSVS_ASYNC", + "qr_async_sparge": "ck_tile::BlockFmhaPipelineEnum::QRKSVS_ASYNC", } BOOL_MAP = { diff --git a/example/ck_tile/50_sparse_attn/codegen/ops/fmha_fwd_sparge.py b/example/ck_tile/50_sparse_attn/codegen/ops/fmha_fwd_sparge.py new file mode 100644 index 00000000000..9489d3758fd --- /dev/null +++ b/example/ck_tile/50_sparse_attn/codegen/ops/fmha_fwd_sparge.py @@ -0,0 +1,1041 @@ +# Copyright (c) Advanced Micro Devices, Inc., or its affiliates. +# SPDX-License-Identifier: MIT +# generate kernel instances to speed up compilation + +import copy +from dataclasses import dataclass, field +import fnmatch +import itertools +import os +import os.path as path +from pathlib import Path +from typing import List, Optional, Tuple + +from codegen.cpp_symbol_map import ( + BOOL_MAP, + FWD_DTYPE_MAP, + LAYOUT_MAP, + MODE_MAP, + PIPELINE_ENUM_MAP, + PIPELINE_MAP, + get_mask_check_map, + get_mask_map, +) + +GEN_DIR = "" + + +def update_file(file_path, content): + """Update the file at file_path with the given content if it differs from the existing content. + + It avoids unnecessary touching of the file which triggers rebuilds + """ + + existing_content = "" + if path.exists(file_path): + with open(file_path, "r") as file: + existing_content = file.read() + if existing_content == content: + return + with open(file_path, "w") as file: + file.write(content) + + +DTYPE_BITS = {"fp32": 32, "fp16": 16, "bf16": 16} + +K0_MAX_SUBMAX_MAP = {32: 32, 64: 64, 96: 128, 128: 128, 192: 192, 256: 256} + +FMHA_FWD_KERNEL_HEADER = """// SPDX-License-Identifier: MIT +// Copyright (c) Advanced Micro Devices, Inc., or its affiliates.\n +// auto generated by generate.py +#include "ck_tile/ops/fmha/block/variants.hpp" +#include "fmha_fwd_trek.hpp" +#include "pipeline/block_fmha_pipeline_qr_ks_vs_async_sparge.hpp" +#include "kernel/fmha_fwd_sparge_kernel.hpp" + +""" + +# NOTE: Sparge sparse attention kernel has the following restrictions enforced by static_assert: +# - Group mode: NOT supported (batch mode only) +# - Bias: NOT supported (NO_BIAS only) +# - LSE output: NOT supported (false only) +# - Dropout: NOT supported (false only) +# - Logits soft-cap: NOT supported (false only) +# - FP8 static quantization: NOT supported (NO_SCALE only) +# The template below hardcodes these unsupported features accordingly. + +FMHA_FWD_KERNEL_BODY = """ +using fmha_dtype_{F_idx} = {F_dtype}; + +using fmha_block_tile_{F_idx} = ck_tile::sequence<{F_bm0}, {F_bn0}, {F_bk0}, {F_bn1}, {F_bk1}, {F_bk0max}>; + +using fmha_shape_{F_idx} = ck_tile::TileFmhaShape, + ck_tile::sequence<{F_wm0}, {F_wn0}, {F_wk0}>, + ck_tile::sequence<{F_rm1}, {F_rn1}, {F_rk1}>, + ck_tile::sequence<{F_wm1}, {F_wn1}, {F_wk1}>, + {F_vlayout}>; + +// TileFmhaTraits: spad, skpad, dpad, dvpad, has_logits_soft_cap, bias_enum, +// store_lse, has_dropout, has_randval, quant_scale_enum, occupancy, is_v_rowmajor_skip +using fmha_trait_{F_idx} = ck_tile::TileFmhaTraits<{F_spad}, + {F_skpad}, + {F_dpad}, + {F_dvpad}, + false, // has_logits_soft_cap - NOT supported + ck_tile::BlockAttentionBiasEnum::NO_BIAS, // bias - NOT supported + false, // store_lse - NOT supported + false, // has_dropout - NOT supported + false, // has_randval - NOT supported + ck_tile::BlockAttentionQuantScaleEnum::NO_SCALE, // FP8 quant - NOT supported + {F_occupancy}, + false>; + +using fmha_variant_{F_idx} = ck_tile::ComposedAttention<0, CK_TILE_FMHA_FWD_FAST_EXP2>; // logits_soft_cap=0 (NOT supported) + +using fmha_mask_{F_idx} = {F_mask}; + +using fmha_pipeline_problem_{F_idx} = ck_tile::BlockFmhaPipelineProblem< + typename FmhaSparseFwdTypeConfig::QDataType, + typename FmhaSparseFwdTypeConfig::KDataType, + typename FmhaSparseFwdTypeConfig::VDataType, + typename FmhaSparseFwdTypeConfig::SaccDataType, + typename FmhaSparseFwdTypeConfig::SMPLComputeDataType, + typename FmhaSparseFwdTypeConfig::BiasDataType, + typename FmhaSparseFwdTypeConfig::RandValOutputDataType, + typename FmhaSparseFwdTypeConfig::LSEDataType, + typename FmhaSparseFwdTypeConfig::PDataType, + typename FmhaSparseFwdTypeConfig::OaccDataType, + typename FmhaSparseFwdTypeConfig::ODataType, + fmha_shape_{F_idx}, + {F_mode}, + fmha_variant_{F_idx}, + fmha_mask_{F_idx}, + {F_trload}, + fmha_trait_{F_idx}>; + +// R25 V0: instantiate the Sparge pipeline with kEnablePVSkip = true (existing path) +// AND kEnablePVSkip = false (PV-skip AST removed at compile time, source-equivalent +// to the frozen VSA reference). Both kernels live in the same TU; the host dispatch +// in fmha_sparge_fwd_api.cpp picks one based on fmha_sparge_fwd_args::pv_skip_compile. +using fmha_pipeline_{F_idx}_pvst = ck_tile::BlockFmhaPipelineQRKSVSAsyncSparge< + fmha_pipeline_problem_{F_idx}, + ck_tile::BlockFmhaPipelineQRKSVSAsyncDefaultPolicy, + true>; +using fmha_pipeline_{F_idx}_pvsf = ck_tile::BlockFmhaPipelineQRKSVSAsyncSparge< + fmha_pipeline_problem_{F_idx}, + ck_tile::BlockFmhaPipelineQRKSVSAsyncDefaultPolicy, + false>; + +using fmha_epilogue_{F_idx} = + ck_tile::Default2DEpilogue::OaccDataType, + typename FmhaSparseFwdTypeConfig<{F_dtype}>::ODataType, + {F_spad}, {F_dvpad}>>; + +using fmha_kernel_{F_idx}_pvst = + ck_tile::FmhaFwdSpargeKernel; +using fmha_kernel_{F_idx}_pvsf = + ck_tile::FmhaFwdSpargeKernel; + +using trait_{F_idx} = fmha_sparge_fwd_traits_<{F_hdim}, {F_dtype}, {F_bm0}, {F_bn0}, {F_bk0}, {F_bn1}, {F_bk1}, {F_bk0max}, {F_vlayout}, + {F_pipeline_enum}, false/*logits*/, fmha_mask_{F_idx}, {F_spad}, {F_skpad}, {F_dpad}, {F_dvpad}, {F_trload}>; + +#include + +template<> +float fmha_sparge_fwd_(const ck_tile::stream_config& s, fmha_sparge_fwd_args a) +{{ + using k_ = fmha_kernel_{F_idx}_pvst; + if(s.log_level_ > 0) + std::cout << ", " << "{F_kernel_name}_pvst" << std::flush; + auto [kargs, grids] = fmha_fwd_create_kargs_and_grids(a); + const dim3 blocks = k_::BlockSize(); + constexpr ck_tile::index_t kBlockPerCu = k_::kBlockPerCu; + return ck_tile::launch_kernel(s, ck_tile::make_kernel(k_{{}}, grids, blocks, 0, kargs)); +}} + +template<> +float fmha_sparge_fwd_(const ck_tile::stream_config& s, fmha_sparge_fwd_args a) +{{ + using k_ = fmha_kernel_{F_idx}_pvsf; + if(s.log_level_ > 0) + std::cout << ", " << "{F_kernel_name}_pvsf" << std::flush; + auto [kargs, grids] = fmha_fwd_create_kargs_and_grids(a); + const dim3 blocks = k_::BlockSize(); + constexpr ck_tile::index_t kBlockPerCu = k_::kBlockPerCu; + return ck_tile::launch_kernel(s, ck_tile::make_kernel(k_{{}}, grids, blocks, 0, kargs)); +}} + +template<> +void fmha_sparge_fwd_oneshot_(const ck_tile::stream_config& s, fmha_sparge_fwd_args a) +{{ + using k_ = fmha_kernel_{F_idx}_pvst; + auto [kargs, grids] = fmha_fwd_create_kargs_and_grids(a); + const dim3 blocks = k_::BlockSize(); + constexpr ck_tile::index_t kBlockPerCu = k_::kBlockPerCu; + ck_tile::make_kernel(k_{{}}, grids, blocks, 0, kargs)( + ck_tile::stream_config{{s.stream_id_}}); +}} + +template<> +void fmha_sparge_fwd_oneshot_(const ck_tile::stream_config& s, fmha_sparge_fwd_args a) +{{ + using k_ = fmha_kernel_{F_idx}_pvsf; + auto [kargs, grids] = fmha_fwd_create_kargs_and_grids(a); + const dim3 blocks = k_::BlockSize(); + constexpr ck_tile::index_t kBlockPerCu = k_::kBlockPerCu; + ck_tile::make_kernel(k_{{}}, grids, blocks, 0, kargs)( + ck_tile::stream_config{{s.stream_id_}}); +}} +""" + +FMHA_FWD_API_FILENAME = "fmha_sparge_fwd_api.cpp" +FMHA_FWD_API = """ +#include + +#include + +namespace {{ +bool get_num_cus(unsigned& num_cus) {{ + int device; + auto status = hipGetDevice(&device); + if(status != hipSuccess) {{ + fprintf(stderr, "failed to get device"); + return false; + }} + + hipDeviceProp_t props{{}}; + status = hipGetDeviceProperties(&props, device); + if(status != hipSuccess) {{ + fprintf(stderr, "failed to get device properties"); + return false; + }} + + num_cus = props.multiProcessorCount; + return true; +}} + +unsigned get_num_thread_blocks(unsigned batch, unsigned nheads, unsigned max_seqlen_q, unsigned kM0) {{ + const unsigned num_m_blocks = (max_seqlen_q + kM0 - 1) / kM0; + const unsigned num_n_blocks = 1; // we assume that num_n_blocks is always 1 + + return batch * nheads * num_m_blocks * num_n_blocks; +}} +}} // namespace + +float fmha_sparge_fwd(fmha_sparge_fwd_traits t, fmha_sparge_fwd_args a, const ck_tile::stream_config& s){{ + float r = -1; + + [[maybe_unused]] const float min_cu_util_rate = 0.8; // minimum CU utilization rate + + unsigned num_cus; + if (!get_num_cus(num_cus)) {{ + return r; + }} + + [[maybe_unused]] auto get_num_blocks = [&](unsigned kM0) {{ + return get_num_thread_blocks(a.batch, a.nhead_q, a.max_seqlen_q, kM0); + }}; + + const bool has_load_tr = ck_tile::is_load_tr_supported(); + +{F_dispatch} + return r; +}} +""" + +FMHA_FWD_API_PER_TRLOAD = """ {F_if}({F_trload_cond}){{ +{F_dtype_case} + }} +""" + +FMHA_FWD_API_PER_DTYPE = """ {F_if}(t.data_type.compare(\"{F_dtype}\") == 0){{ +{F_hdim_case} + }} +""" +FMHA_FWD_API_PER_HDIM_CASE = """ {F_if} (t.hdim_q <= {F_hdim} && t.hdim_v <= {F_hdim_v}) {{ +{F_inner_dispatch} + }} +""" + +FMHA_FWD_API_INNER_DISPATCH = """ {F_if}((t.is_v_rowmajor == {F_vlayout}) && ({F_mask_check}) && + ({F_scheck}) && ({F_seqtune}) && ({F_skcheck}) && ({F_dcheck}) && ({F_dvcheck}) && ({F_constraint})) {{ + using trait_ = fmha_sparge_fwd_traits_<{F_hdim}, {F_dtype}, {F_bm0}, {F_bn0}, {F_bk0}, {F_bn1}, {F_bk1}, {F_bk0max}, {F_vlayout}, {F_pipeline_enum}, false/*logits*/, {F_mask}, {F_spad}, {F_skpad}, {F_dpad}, {F_dvpad}, {F_trload}>; + if(a.pv_skip_compile) + return fmha_sparge_fwd_(s, a); + else + return fmha_sparge_fwd_(s, a); + }} +""" + +FMHA_FWD_ONESHOT_API_FILENAME = "fmha_sparge_fwd_oneshot_api.cpp" +FMHA_FWD_ONESHOT_API = """ +#include "fmha_fwd_trek.hpp" +#include + +void fmha_sparge_fwd_oneshot(fmha_sparge_fwd_traits t, fmha_sparge_fwd_args a, const ck_tile::stream_config& s){{ + + const bool has_load_tr = ck_tile::is_load_tr_supported(); + +{F_dispatch} + std::cerr << "fmha_sparge_fwd_oneshot: no matching dispatch (dtype=" << t.data_type + << " hdim_q=" << t.hdim_q << " hdim_v=" << t.hdim_v + << " seqlen_q=" << a.seqlen_q << " seqlen_k=" << a.seqlen_k + << " mask=" << static_cast(t.mask_type) << ")" << std::endl; +}} +""" + +FMHA_FWD_ONESHOT_API_PER_TRLOAD = """ {F_if}({F_trload_cond}){{ +{F_dtype_case} + }} +""" + +FMHA_FWD_ONESHOT_API_PER_DTYPE = """ {F_if}(t.data_type.compare(\"{F_dtype}\") == 0){{ +{F_hdim_case} + }} +""" +FMHA_FWD_ONESHOT_API_PER_HDIM_CASE = """ {F_if} (t.hdim_q <= {F_hdim} && t.hdim_v <= {F_hdim_v}) {{ +{F_inner_dispatch} + }} +""" + +FMHA_FWD_ONESHOT_API_INNER_DISPATCH = """ {F_if}((t.is_v_rowmajor == {F_vlayout}) && ({F_mask_check}) && + ({F_scheck}) && ({F_seqtune}) && ({F_skcheck}) && ({F_dcheck}) && ({F_dvcheck}) && ({F_constraint})) {{ + using trait_ = fmha_sparge_fwd_traits_<{F_hdim}, {F_dtype}, {F_bm0}, {F_bn0}, {F_bk0}, {F_bn1}, {F_bk1}, {F_bk0max}, {F_vlayout}, {F_pipeline_enum}, false/*logits*/, {F_mask}, {F_spad}, {F_skpad}, {F_dpad}, {F_dvpad}, {F_trload}>; + if(a.pv_skip_compile) + fmha_sparge_fwd_oneshot_(s, a); + else + fmha_sparge_fwd_oneshot_(s, a); + return; + }} +""" + + +@dataclass +class CppConstraint: + bool_expr: str = None + + def __str__(self): + if self.bool_expr is None: + return "true" + else: + return f"{self.bool_expr}" + + def __and__(self, other): + return CppConstraint(f"({str(self)}) && ({str(other)})") + + +@dataclass +class FmhaFwdApiTrait: + pipeline_tag: str + # sync with fmha_fwd_traits<>, to generate fallback calls + hdim: str + dtype: str # data type + mode: str # value from MODE_MAP + bm0: int # tile size along q seqlen (block size) + bn0: int # tile size along qk seqlen + bk0: int # tile size along qk gemm unroll + bn1: int # tile size along v head_dim + bk1: int # tile size along kv gemm unroll + bk0max: int + vlayout: str + logits: str + mask: str + spad: str + skpad: str + dpad: str + dvpad: str + tr_load: str + constraint: CppConstraint + + @property + def name(self) -> str: + return ( + f"{self.hdim}-{self.dtype}-{self.mode}-{self.bm0}-{self.bn0}-{self.bk0}-{self.bn0}-{self.bk1}-{self.bk0max}-" + + f"{self.vlayout}-{self.logits}-{self.mask}-{self.spad}-{self.skpad}-{self.dpad}-{self.dvpad}" + ) + + @property + def scheck(self) -> str: + if self.mode == "group": + return "true/*group mode spad always true*/" # group mode only generate spad/skpad == true + if self.spad == "t": + return "true" # always support + return "true" + + @property + def seqtune(self) -> str: + return "true" + + @property + def skcheck(self) -> str: + if self.mode == "group": + return "true/*group mode skpad always true*/" # group mode only generate spad/skpad == true + if self.skpad == "t": + return f"a.seqlen_k == 0 || a.seqlen_k % {self.bn0} != 0" + return f"a.seqlen_k != 0 && a.seqlen_k % {self.bn0} == 0" + + @property + def dcheck(self) -> str: + vec = int((32 * 4) / DTYPE_BITS[self.dtype]) + if self.dpad == "t": + return f"a.hdim_q % {vec} == 0" + assert False + + @property + def dvcheck(self) -> str: + vec = int((32 * 4) / DTYPE_BITS[self.dtype]) + if self.dvpad == "t": + return f"a.hdim_v % {vec} == 0" + assert False + + +@dataclass +class FmhaFwdPipeline: + tag: str + + F_vlayout: str # row/col + F_spad: str # true/false + F_skpad: str # + F_dpad: str # + F_dvpad: str # + F_logits: str # t/f + F_mask: str # value from MASK_MAP + F_trload: str # true/false + F_constraint: CppConstraint = field(default_factory=CppConstraint) + + @property + def name(self) -> str: + def pad_name() -> str: + n = "" + if self.F_spad == "t": + n += "s" + if self.F_skpad == "t": + n += "sk" + if self.F_dpad == "t": + n += "d" + if self.F_dvpad == "t": + n += "dv" + if n != "": + n = "p" + n + return n + + pn = pad_name() + n = f"{self.tag}_v{self.F_vlayout[0]}" + if pn != "": + n += f"_{pn}" + else: + n += "_npad" + + if self.F_logits == "t": + n += "_logits" + else: + n += "_nlogits" + + n += "_nbias" + + if self.F_mask[0:2] == "s_": + if self.F_mask == "s_mask": + n += "_mask" + else: + n += "_nmask" + else: + if self.F_mask != "no": + n += f"_m{self.F_mask[0]}" + else: + n += "_nmask" + + n += "_nskip" + + n += "_nsquant" + + if self.F_trload == "t": + n += "_trload" + else: + n += "_ntrload" + + return n + + +class FmhaFwdApiPool: + def __init__(self, mask_impl): + self.pool = dict() + self.mask_impl = mask_impl + + def register_traits(self, trait: FmhaFwdApiTrait) -> None: + # TODO: do we need to check duplication? + if trait.dtype not in self.pool.keys(): + self.pool[trait.dtype] = dict() + hdim = trait.hdim, trait.bn1 + if hdim not in self.pool[trait.dtype].keys(): + self.pool[trait.dtype][hdim] = list() + + self.pool[trait.dtype][hdim].append(copy.copy(trait)) + + @property + def api(self) -> str: + tr_load_cond_map = {"t": "has_load_tr", "f": "true"} + + per_tr_load = str() + for tr_load in ["t", "f"]: + per_dtypes = str() + for i, dtype in enumerate(self.pool.keys()): + per_hdim_case = str() + for j, (hdim, hdim_v) in enumerate(self.pool[dtype].keys()): + traits = [ + t + for t in self.pool[dtype][(hdim, hdim_v)] + if tr_load == t.tr_load + ] + inners = str() + for k, trait in enumerate(traits): + if_k = "if" if k == 0 else "else if" + inners = inners + FMHA_FWD_API_INNER_DISPATCH.format( + F_if=if_k, + F_vlayout=LAYOUT_MAP[trait.vlayout], + F_pipeline_enum=PIPELINE_ENUM_MAP[trait.pipeline_tag], + # F_logits removed - hardcoded to false (NOT supported) + F_mask=get_mask_map(self.mask_impl)[trait.mask], + F_mask_check=get_mask_check_map(self.mask_impl)[trait.mask], + F_trload=BOOL_MAP[trait.tr_load], + F_scheck=trait.scheck, + F_seqtune=trait.seqtune, + F_skcheck=trait.skcheck, + F_dcheck=trait.dcheck, + F_dvcheck=trait.dvcheck, + F_constraint=trait.constraint, + F_spad=BOOL_MAP[trait.spad], + F_skpad=BOOL_MAP[trait.skpad], + F_dpad=BOOL_MAP[trait.dpad], + F_dvpad=BOOL_MAP[trait.dvpad], + F_bm0=trait.bm0, + F_bn0=trait.bn0, + F_bk0=trait.bk0, + F_bn1=trait.bn1, + F_bk1=trait.bk1, + F_bk0max=trait.bk0max, + F_hdim=hdim, + F_dtype=FWD_DTYPE_MAP[dtype], + ) + if_j = "if" if j == 0 else "else if" + per_hdim_case = per_hdim_case + FMHA_FWD_API_PER_HDIM_CASE.format( + F_if=if_j, F_hdim=hdim, F_hdim_v=hdim_v, F_inner_dispatch=inners + ) + if_i = "if" if i == 0 else "else if" + per_dtypes = per_dtypes + FMHA_FWD_API_PER_DTYPE.format( + F_if=if_i, F_dtype=dtype, F_hdim_case=per_hdim_case + ) + per_tr_load += FMHA_FWD_API_PER_TRLOAD.format( + F_if="if", + F_trload_cond=tr_load_cond_map[tr_load], + F_dtype_case=per_dtypes, + ) + if not per_tr_load: + # empty string we add some ignore to suppress warning in api + per_tr_load += " (void)t ; (void)s ; (void)a;" + return FMHA_FWD_KERNEL_HEADER + FMHA_FWD_API.format(F_dispatch=per_tr_load) + + @property + def oneshot_api(self) -> str: + tr_load_cond_map = {"t": "has_load_tr", "f": "true"} + + per_tr_load = str() + for tr_load in ["t", "f"]: + per_dtypes = str() + for i, dtype in enumerate(self.pool.keys()): + per_hdim_case = str() + for j, (hdim, hdim_v) in enumerate(self.pool[dtype].keys()): + traits = [ + t + for t in self.pool[dtype][(hdim, hdim_v)] + if tr_load == t.tr_load + ] + inners = str() + for k, trait in enumerate(traits): + if_k = "if" if k == 0 else "else if" + inners = inners + FMHA_FWD_ONESHOT_API_INNER_DISPATCH.format( + F_if=if_k, + F_vlayout=LAYOUT_MAP[trait.vlayout], + F_pipeline_enum=PIPELINE_ENUM_MAP[trait.pipeline_tag], + F_mask=get_mask_map(self.mask_impl)[trait.mask], + F_mask_check=get_mask_check_map(self.mask_impl)[trait.mask], + F_trload=BOOL_MAP[trait.tr_load], + F_scheck=trait.scheck, + F_seqtune=trait.seqtune, + F_skcheck=trait.skcheck, + F_dcheck=trait.dcheck, + F_dvcheck=trait.dvcheck, + F_constraint=trait.constraint, + F_spad=BOOL_MAP[trait.spad], + F_skpad=BOOL_MAP[trait.skpad], + F_dpad=BOOL_MAP[trait.dpad], + F_dvpad=BOOL_MAP[trait.dvpad], + F_bm0=trait.bm0, + F_bn0=trait.bn0, + F_bk0=trait.bk0, + F_bn1=trait.bn1, + F_bk1=trait.bk1, + F_bk0max=trait.bk0max, + F_hdim=hdim, + F_dtype=FWD_DTYPE_MAP[dtype], + ) + if_j = "if" if j == 0 else "else if" + per_hdim_case = per_hdim_case + FMHA_FWD_ONESHOT_API_PER_HDIM_CASE.format( + F_if=if_j, F_hdim=hdim, F_hdim_v=hdim_v, F_inner_dispatch=inners + ) + if_i = "if" if i == 0 else "else if" + per_dtypes = per_dtypes + FMHA_FWD_ONESHOT_API_PER_DTYPE.format( + F_if=if_i, F_dtype=dtype, F_hdim_case=per_hdim_case + ) + per_tr_load += FMHA_FWD_ONESHOT_API_PER_TRLOAD.format( + F_if="if", + F_trload_cond=tr_load_cond_map[tr_load], + F_dtype_case=per_dtypes, + ) + if not per_tr_load: + per_tr_load += " (void)t ; (void)s ; (void)a;" + return FMHA_FWD_KERNEL_HEADER + FMHA_FWD_ONESHOT_API.format(F_dispatch=per_tr_load) + + +@dataclass +class FmhaFwdTileSize: + F_bm0: int # tile size along q seqlen (block size) + F_bn0: int # tile size along k seqlen + F_bk0: int # tile size along qk gemm unroll + F_bn1: int # tile size along v head_dim + F_bk1: int # tile size along kv gemm unroll + F_bk0max: int # total length of K0, used for pipeline that need load Q at once (or repeately load Q as a whole tile) + F_rm0: int # number of warps for gemm0 along q seqlen + F_rn0: int # number of warps for gemm0 along k seqlen + F_rk0: int # number of warps for gemm0 along head dim q (not used) + F_rm1: int # number of warps for gemm1 along q seqlen + F_rn1: int # number of warps for gemm1 along head dim v + F_rk1: int # number of warps for gemm1 along k seqlen (not used) + F_wm0: int # gemm0 warp size along m + F_wn0: int # gemm0 warp size along n + F_wk0: int # gemm0 warp size along k + F_wm1: int # gemm1 warp size along m + F_wn1: int # gemm1 warp size along n + F_wk1: int # gemm1 warp size along k + F_occupancy: int # occupancy, -1 will let pipeline decide the occupancy, other value will overwrite occupancy + F_constraint: CppConstraint = field(default_factory=CppConstraint) + + @property + def name(self) -> str: + return ( + f"b{self.F_bm0}x{self.F_bn0}x{self.F_bk0}x{self.F_bn1}x{self.F_bk1}x{self.F_bk0max}" + + f"_r{self.F_rm0}x{self.F_rn0}x{self.F_rk0}_r{self.F_rm1}x{self.F_rn1}x{self.F_rk1}" + + f"_w{self.F_wm0}x{self.F_wn0}x{self.F_wk0}_w{self.F_wm1}x{self.F_wn1}x{self.F_wk1}" + + ("" if self.F_occupancy == -1 else f"_o{self.F_occupancy}") + ) + + +@dataclass +class FmhaFwdKernel: + F_idx: int # this is not a tunable, but a counter to differentiate symbol + F_hdim: int # hdim + F_dtype: str # data type + F_mode: str # value from MODE_MAP + F_tile: FmhaFwdTileSize + F_pipeline: FmhaFwdPipeline + mask_impl: str + + @property + def template(self) -> str: + # kernel_body removed - unused + return FMHA_FWD_KERNEL_HEADER + FMHA_FWD_KERNEL_BODY.format( + F_idx=self.F_idx, + F_hdim=self.F_hdim, + F_dtype=FWD_DTYPE_MAP[self.F_dtype], + F_bm0=self.F_tile.F_bm0, + F_bn0=self.F_tile.F_bn0, + F_bk0=self.F_tile.F_bk0, + F_bn1=self.F_tile.F_bn1, + F_bk1=self.F_tile.F_bk1, + F_bk0max=self.F_tile.F_bk0max, + F_rm0=self.F_tile.F_rm0, + F_rn0=self.F_tile.F_rn0, + F_rk0=self.F_tile.F_rk0, + F_rm1=self.F_tile.F_rm1, + F_rn1=self.F_tile.F_rn1, + F_rk1=self.F_tile.F_rk1, + F_wm0=self.F_tile.F_wm0, + F_wn0=self.F_tile.F_wn0, + F_wk0=self.F_tile.F_wk0, + F_wm1=self.F_tile.F_wm1, + F_wn1=self.F_tile.F_wn1, + F_wk1=self.F_tile.F_wk1, + F_vlayout=LAYOUT_MAP[self.F_pipeline.F_vlayout], + F_spad=BOOL_MAP[self.F_pipeline.F_spad], + F_skpad=BOOL_MAP[self.F_pipeline.F_skpad], + F_dpad=BOOL_MAP[self.F_pipeline.F_dpad], + F_dvpad=BOOL_MAP[self.F_pipeline.F_dvpad], + # F_logits removed - hardcoded to false in template (NOT supported) + F_occupancy=self.F_tile.F_occupancy, + F_pipeline_enum=PIPELINE_ENUM_MAP[self.F_pipeline.tag], + F_mask=get_mask_map(self.mask_impl)[self.F_pipeline.F_mask], + F_mode=MODE_MAP[self.F_mode], + F_pipeline=PIPELINE_MAP[self.F_pipeline.tag], + F_trload=BOOL_MAP[self.F_pipeline.F_trload], + F_kernel_name=self.name, + ) + + @property + def name(self) -> str: + # TODO: we don't encode idx here + return ( + f"fmha_sparge_fwd_d{self.F_hdim}_{self.F_dtype}_{self.F_mode}_" + + self.F_tile.name + + "_" + + self.F_pipeline.name + ) + + @property + def filename(self) -> str: + return self.name + ".cpp" + + def api_trait(self) -> FmhaFwdApiTrait: + return FmhaFwdApiTrait( + pipeline_tag=self.F_pipeline.tag, + hdim=str(self.F_hdim), + dtype=self.F_dtype, + mode=self.F_mode, + bm0=self.F_tile.F_bm0, + bn0=self.F_tile.F_bn0, + bk0=self.F_tile.F_bk0, + bn1=self.F_tile.F_bn1, + bk1=self.F_tile.F_bk1, + bk0max=self.F_tile.F_bk0max, + vlayout=self.F_pipeline.F_vlayout, + mask=self.F_pipeline.F_mask, + logits=self.F_pipeline.F_logits, + spad=self.F_pipeline.F_spad, + skpad=self.F_pipeline.F_skpad, + dpad=self.F_pipeline.F_dpad, + dvpad=self.F_pipeline.F_dvpad, + tr_load=self.F_pipeline.F_trload, + constraint=self.F_tile.F_constraint & self.F_pipeline.F_constraint, + ) + + +class KernelComponentFactory: + # TODO: design a more practical way to do it + # this is current supported tile size per hdim + @staticmethod + def get_hdim_tile_size_dict(dtype: str) -> Optional[dict]: + if dtype == "fp16" or dtype == "bf16": + return { + # (32, 32) : [FmhaFwdTileSize(128, 64, 16, 32, 32, 32, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, -1)], + # (64, 64) : [FmhaFwdTileSize(16, 32, 64, 64, 32, 64, 1, 1, 1, 1, 1, 1, 16, 16, 32, 16, 16, 32, -1), + # FmhaFwdTileSize(32, 32, 64, 64, 32, 64, 1, 1, 1, 1, 1, 1, 32, 32, 16, 32, 32, 16, -1), + # FmhaFwdTileSize(128, 64, 32, 64, 32, 64, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, -1)], + # (96, 128) : [FmhaFwdTileSize(128, 128, 32, 128, 32, 96, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, -1)], + (128, 128): [ + FmhaFwdTileSize( # fmt: skip -- 128x128 tile (original, for old sparse attn test) + 128, + 128, + 32, + 128, + 32, + 128, + 4, + 1, + 1, + 4, + 1, + 1, + 32, + 32, + 16, + 32, + 32, + 16, + -1, + CppConstraint("t.bm0 == 0 || t.bm0 == 128"), + ), + FmhaFwdTileSize( # fmt: skip -- 64x128 tile (for sparge blockmap kM0=64) + 64, + 128, + 32, + 128, + 32, + 128, + 2, + 1, + 1, + 2, + 1, + 1, + 32, + 32, + 16, + 32, + 32, + 16, + -1, + CppConstraint("t.bm0 == 64"), + ), + FmhaFwdTileSize( # fmt: skip + 16, + 32, + 64, + 128, + 32, + 128, + 1, + 1, + 1, + 1, + 1, + 1, + 16, + 16, + 32, + 16, + 16, + 32, + -1, + ), + FmhaFwdTileSize( # fmt: skip + 32, + 32, + 128, + 128, + 32, + 128, + 1, + 1, + 1, + 1, + 1, + 1, + 32, + 32, + 16, + 32, + 32, + 16, + -1, + ), + FmhaFwdTileSize( # fmt: skip + 128, + 64, + 32, + 128, + 16, + 128, + 4, + 1, + 1, + 4, + 1, + 1, + 32, + 32, + 16, + 32, + 32, + 16, + -1, + ), + ], + # (160,160) : [FmhaFwdTileSize(128, 128, 32, 160, 32, 160, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, 1)], + # (192,128) : [FmhaFwdTileSize(128, 128, 32, 128, 32, 192, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, -1)], + # (192,192) : [FmhaFwdTileSize(128, 128, 32, 192, 32, 192, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, 1)], + # (256,256) : [FmhaFwdTileSize(128, 128, 32, 256, 32, 256, 4, 1, 1, 4, 1, 1, 32, 32, 16, 32, 32, 16, -1)], + } + else: + return None + + # TODO: we don't support tuning yet, so pick up one value for vlayout/pipeline/pad + # support this in future + @staticmethod + def get_pipelines(dtype, hdim, hdim_v, receipt, mask_impl) -> List[FmhaFwdPipeline]: + # this function will populate a list possible pipelines + # TODO: the order of List matters! the later in this list will be also be checked later + # NOTE: logits soft-cap is NOT supported by Sparge sparse attention (enforced by static_assert) + pipelines = [] + if dtype in ["fp16", "bf16"]: + for logits, mask in itertools.product( + ["f"], # logits soft-cap NOT supported, always false + get_mask_map(mask_impl).keys(), + ): + if hdim == 256 and hdim_v == 256: + # sparge fmha only supports dim <= 192 for now. + continue + pipelines.append( + FmhaFwdPipeline( + "qr_async_sparge", + "row", + "t", + "f", + "t", + "t", + logits, + mask, + "f", + ) + ) + pipelines.append( + FmhaFwdPipeline( + "qr_async_sparge", + "row", + "t", + "t", + "t", + "t", + logits, + mask, + "f", + ) + ) + else: + assert False + return pipelines + + +class CustomFactory(KernelComponentFactory): + @staticmethod + def get_hdim_tile_size_dict(dtype: str) -> Optional[dict]: + result = KernelComponentFactory.get_hdim_tile_size_dict(dtype) + if dtype == "fp16" or dtype == "bf16": + if (128, 128) in result.keys(): + result[(128, 128)].insert( + 0, + FmhaFwdTileSize( + 64, + 128, + 64, + 128, + 64, + 128, + 4, + 1, + 1, + 4, + 1, + 1, + 16, + 16, + 16, + 16, + 16, + 16, + -1, + CppConstraint( + "get_num_blocks(128) < num_cus * min_cu_util_rate" + ), + ), + ) + return result + + +def get_fwd_blobs( + kernel_filter: Optional[str], receipt, optdim_list, mask_impl +) -> Tuple[FmhaFwdApiPool, List[FmhaFwdKernel]]: + gen = list() + api_pool = FmhaFwdApiPool(mask_impl) + + factory = ( + CustomFactory + if os.environ.get("CK_TILE_FMHA_FWD_CUSTOM_FACTORY", "0") == "1" + else KernelComponentFactory + ) + + # Only generate fp16/bf16 kernels for now. + # NOTE: Sparge sparse attention only supports batch mode (group mode NOT supported, enforced by static_assert) + for dtype in ["fp16", "bf16"]: + d = factory.get_hdim_tile_size_dict(dtype) + if d is None: + continue + for ((hdim, hdim_v), tiles), mode in itertools.product(d.items(), ["batch"]): + for tile, pipeline in itertools.product( + tiles, factory.get_pipelines(dtype, hdim, hdim_v, receipt, mask_impl) + ): + if tile.F_bm0 not in (64, 128) or tile.F_bn0 != 128: + continue + if pipeline.tag != "qr_async_sparge": + continue + k = FmhaFwdKernel( + F_idx=1, + F_hdim=hdim, + F_dtype=dtype, + F_mode=mode, + F_tile=tile, + F_pipeline=pipeline, + mask_impl=mask_impl, + ) + if kernel_filter != "": + if not fnmatch.fnmatch(k.name, kernel_filter): + continue + if optdim_list != [-1]: + if hdim not in optdim_list: + continue + # 2 - Flash attention integration + if receipt in (2, 3): + cond = dtype in ["fp16", "bf16"] + cond &= pipeline.F_vlayout == "row" + if not cond: + continue + # PyTorch integration + elif receipt == 4: + cond = dtype in ["fp16", "bf16"] + cond &= pipeline.F_vlayout == "row" + cond &= mode == "batch" + cond &= pipeline.F_logits == "f" + if not cond: + continue + # Aiter(mha_fwd) integration + elif receipt == 100: + cond = dtype in ["fp16", "bf16"] + cond &= mode == "batch" + cond &= pipeline.F_vlayout == "row" + if not cond: + continue + # Aiter(mha_varlen_fwd) integration + elif receipt == 200: + cond = dtype in ["fp16", "bf16"] + cond &= mode == "group" + cond &= pipeline.F_vlayout == "row" + if not cond: + continue + # aiter::mha_fwd C++ api integration + elif receipt == 600: + cond = dtype in ["fp16", "bf16"] + cond &= pipeline.F_vlayout == "row" + if not cond: + continue + + api_pool.register_traits(k.api_trait()) + gen.append(k) + + return (api_pool, gen) + + +def write_single_fwd_kernel(kernel: FmhaFwdKernel, autogen_dir: Path) -> None: + update_file(autogen_dir / kernel.filename, kernel.template) + + +def write_fwd_api(api_pool: FmhaFwdApiPool, autogen_dir: Path) -> None: + update_file(autogen_dir / FMHA_FWD_API_FILENAME, api_pool.api) + update_file(autogen_dir / FMHA_FWD_ONESHOT_API_FILENAME, api_pool.oneshot_api) + + +def write_blobs( + output_dir: Path, kernel_filter: str, receipt, optdim_list, mask_impl +) -> None: + api_pool, kernels = get_fwd_blobs(kernel_filter, receipt, optdim_list, mask_impl) + for kernel in kernels: + write_single_fwd_kernel(kernel, output_dir) + write_fwd_api(api_pool, output_dir) + + +def list_blobs( + file_path: Path, kernel_filter: str, receipt, optdim_list, mask_impl +) -> None: + with file_path.open("a") as f: + _, kernels = get_fwd_blobs(kernel_filter, receipt, optdim_list, mask_impl) + for kernel in kernels: + f.write((file_path.parent / GEN_DIR / kernel.filename).as_posix() + "\n") + f.write((file_path.parent / GEN_DIR / FMHA_FWD_API_FILENAME).as_posix() + "\n") + f.write((file_path.parent / GEN_DIR / FMHA_FWD_ONESHOT_API_FILENAME).as_posix() + "\n") diff --git a/example/ck_tile/50_sparse_attn/fmha_fwd_trek.hpp b/example/ck_tile/50_sparse_attn/fmha_fwd_trek.hpp index 384f7bb56d1..8339b503893 100644 --- a/example/ck_tile/50_sparse_attn/fmha_fwd_trek.hpp +++ b/example/ck_tile/50_sparse_attn/fmha_fwd_trek.hpp @@ -334,3 +334,141 @@ template void fmha_vsa_fwd_oneshot_(const ck_tile::stream_config&, fmha_vsa_fwd_args); void fmha_vsa_fwd_oneshot(fmha_vsa_fwd_traits, fmha_vsa_fwd_args, const ck_tile::stream_config&); + +// sparge: same args as vsa plus a scalar PV-skip threshold (Step 1). +struct fmha_sparge_fwd_args +{ + const void* q_ptr; + const void* k_ptr; + const void* v_ptr; + const void* lut_ptr; // delta-encoded K-block indices per Q-block, int32 [B,H,Q_blk,K_blk] + const void* valid_block_num_ptr; // valid K-block count per Q-block, int32 [B,H,Q_blk] + void* o_ptr; + + ck_tile::index_t seqlen_q; + ck_tile::index_t seqlen_k; + ck_tile::index_t batch; + ck_tile::index_t max_seqlen_q; + ck_tile::index_t hdim_q; + ck_tile::index_t hdim_v; + ck_tile::index_t nhead_q; + ck_tile::index_t nhead_k; + + float scale_s; + float pv_threshold; // SpargeAttn §4.4 PV-skip per-Q-tile threshold + + ck_tile::index_t stride_q; + ck_tile::index_t stride_k; + ck_tile::index_t stride_v; + ck_tile::index_t stride_o; + ck_tile::index_t nhead_stride_q; + ck_tile::index_t nhead_stride_k; + ck_tile::index_t nhead_stride_v; + ck_tile::index_t nhead_stride_o; + ck_tile::index_t batch_stride_q; + ck_tile::index_t batch_stride_k; + ck_tile::index_t batch_stride_v; + ck_tile::index_t batch_stride_o; + + ck_tile::index_t window_size_left; + ck_tile::index_t window_size_right; + ck_tile::index_t mask_type; + + // R25 V0: select between kEnablePVSkip=true / =false template instantiations + // at host dispatch time. Default true preserves existing behaviour (binary + // shipped pre-R25-V0 only had the true instance). Profiler can flip this to + // false to measure the source-equivalent-to-VSA baseline (`if constexpr` + // removes the entire PV-skip AST). + bool pv_skip_compile = true; +}; + +template +auto fmha_fwd_create_kargs_and_grids(fmha_sparge_fwd_args args) +{ + assert(args.nhead_q % args.nhead_k == 0); + auto kargs = FmhaKernel::MakeKargs(args.q_ptr, + args.k_ptr, + args.v_ptr, + args.lut_ptr, + args.valid_block_num_ptr, + args.o_ptr, + args.seqlen_q, + args.seqlen_k, + args.hdim_q, + args.hdim_v, + args.nhead_q, + args.nhead_q / args.nhead_k, + args.scale_s, + args.pv_threshold, + args.stride_q, + args.stride_k, + args.stride_v, + args.stride_o, + args.nhead_stride_q, + args.nhead_stride_k, + args.nhead_stride_v, + args.nhead_stride_o, + args.batch_stride_q, + args.batch_stride_k, + args.batch_stride_v, + args.batch_stride_o, + args.window_size_left, + args.window_size_right, + args.mask_type); + + dim3 grids = FmhaKernel::GridSize(args.batch, args.nhead_q, args.max_seqlen_q, args.hdim_v); + return ck_tile::make_tuple(kargs, grids); +} + +template +using fmha_sparge_fwd_traits_ = fmha_jenga_fwd_traits_; + +using fmha_sparge_fwd_traits = fmha_jenga_fwd_traits; + +float fmha_sparge_fwd(fmha_sparge_fwd_traits, fmha_sparge_fwd_args, const ck_tile::stream_config&); + +// R25 V0: kEnablePVSkip is now a template non-type param so the codegen can +// emit both true / false instantiations from the same source tree. The host +// dispatch (fmha_sparge_fwd_api.cpp) selects the right specialization based +// on fmha_sparge_fwd_args::pv_skip_compile at runtime. +template +float fmha_sparge_fwd_(const ck_tile::stream_config&, fmha_sparge_fwd_args); + +template +void fmha_sparge_fwd_oneshot_(const ck_tile::stream_config&, fmha_sparge_fwd_args); + +void fmha_sparge_fwd_oneshot(fmha_sparge_fwd_traits, + fmha_sparge_fwd_args, + const ck_tile::stream_config&); diff --git a/example/ck_tile/50_sparse_attn/sparge_blockmap_inst.cpp b/example/ck_tile/50_sparse_attn/sparge_blockmap_inst.cpp index 0442f1de857..06be6215bc6 100644 --- a/example/ck_tile/50_sparse_attn/sparge_blockmap_inst.cpp +++ b/example/ck_tile/50_sparse_attn/sparge_blockmap_inst.cpp @@ -264,3 +264,24 @@ float sparge_vsa_fwd_combined(sparge_blockmap_traits bmap_t, }, [=](const ck_tile::stream_config& s_) { fmha_vsa_fwd_oneshot(attn_t, attn_a, s_); }); } + +float sparge_sparge_fwd_combined(sparge_blockmap_traits bmap_t, + sparge_blockmap_args bmap_a, + fmha_sparge_fwd_traits attn_t, + fmha_sparge_fwd_args attn_a, + const ck_tile::stream_config& s) +{ + if(s.log_level_ > 0) + std::cout << ", sparge_kstats_" << bmap_t.data_type << "_d" << bmap_t.hdim_q + << ", sparge_blockmap_" << bmap_t.data_type << "_d" << bmap_t.hdim_q + << ", fmha_sparge_fwd_" << attn_t.data_type << "_d" << attn_t.hdim_q + << std::flush; + + return ck_tile::launch_kernel( + s, + [=](const ck_tile::stream_config& s_) { sparge_kstats_fwd_oneshot(bmap_t, bmap_a, s_); }, + [=](const ck_tile::stream_config& s_) { + sparge_blockmap_only_fwd_oneshot(bmap_t, bmap_a, s_); + }, + [=](const ck_tile::stream_config& s_) { fmha_sparge_fwd_oneshot(attn_t, attn_a, s_); }); +} diff --git a/example/ck_tile/50_sparse_attn/sparge_blockmap_trek.hpp b/example/ck_tile/50_sparse_attn/sparge_blockmap_trek.hpp index 4d0e935fc94..c0178f70def 100644 --- a/example/ck_tile/50_sparse_attn/sparge_blockmap_trek.hpp +++ b/example/ck_tile/50_sparse_attn/sparge_blockmap_trek.hpp @@ -169,3 +169,9 @@ float sparge_vsa_fwd_combined(sparge_blockmap_traits, fmha_vsa_fwd_traits, fmha_vsa_fwd_args, const ck_tile::stream_config&); + +float sparge_sparge_fwd_combined(sparge_blockmap_traits, + sparge_blockmap_args, + fmha_sparge_fwd_traits, + fmha_sparge_fwd_args, + const ck_tile::stream_config&); diff --git a/example/ck_tile/50_sparse_attn/test_sparge.cpp b/example/ck_tile/50_sparse_attn/test_sparge.cpp index 73e58f0c249..ae0952cc419 100644 --- a/example/ck_tile/50_sparse_attn/test_sparge.cpp +++ b/example/ck_tile/50_sparse_attn/test_sparge.cpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include #include #include @@ -100,7 +102,19 @@ auto create_args(int argc, char* argv[]) .insert("seed", "42", "random seed") .insert("warmup", "5", "warmup iterations") .insert("repeat", "20", "benchmark iterations") - .insert("kname", "0", "print kernel name"); + .insert("kname", "0", "print kernel name") + .insert("dump_o", + "", + "if non-empty, dump raw output buffer bytes to this path (for bit-identical " + "baseline comparison)") + .insert("pv_threshold", + "1e30", + "SpargeAttn PV-skip per-Q-tile threshold; default +1e30 disables skip") + .insert("pv_skip_compile", + "1", + "R25 V0: 1=use kEnablePVSkip=true template instance (existing path); 0=use " + "kEnablePVSkip=false instance (PV-skip AST removed at compile time, equivalent to " + "VSA baseline)"); bool result = arg_parser.parse(argc, argv); return std::make_tuple(result, arg_parser); @@ -130,6 +144,9 @@ bool run_test(const ck_tile::ArgParser& arg_parser) int warmup = arg_parser.get_int("warmup"); int repeat = arg_parser.get_int("repeat"); int kname = arg_parser.get_int("kname"); + std::string dump_o_path = arg_parser.get_str("dump_o"); + float pv_threshold = arg_parser.get_float("pv_threshold"); + int pv_skip_compile = arg_parser.get_int("pv_skip_compile"); if(nhead_k < 0) nhead_k = nhead; @@ -309,7 +326,9 @@ bool run_test(const ck_tile::ArgParser& arg_parser) } else if(pipeline == "vsa") { - fmha_vsa_fwd_traits attn_traits; + // R25: -pipeline=vsa now dispatches to the sparge pipeline family that adds + // SpargeAttn §4.4 PV-skip; pass pv_threshold (+1e30 disables skip, matches old vsa). + fmha_sparge_fwd_traits attn_traits; attn_traits.hdim_q = hdim_q; attn_traits.hdim_v = hdim_v; attn_traits.data_type = std::is_same_v ? "fp16" : "bf16"; @@ -317,7 +336,7 @@ bool run_test(const ck_tile::ArgParser& arg_parser) attn_traits.mask_type = mask_enum::no_mask; attn_traits.bm0 = BLKQ; - fmha_vsa_fwd_args attn_args; + fmha_sparge_fwd_args attn_args; attn_args.q_ptr = q_dev.GetDeviceBuffer(); attn_args.k_ptr = k_dev.GetDeviceBuffer(); attn_args.v_ptr = v_dev.GetDeviceBuffer(); @@ -333,6 +352,8 @@ bool run_test(const ck_tile::ArgParser& arg_parser) attn_args.nhead_q = nhead; attn_args.nhead_k = nhead_k; attn_args.scale_s = scale_s; + attn_args.pv_threshold = pv_threshold; + attn_args.pv_skip_compile = (pv_skip_compile != 0); attn_args.stride_q = q_strides[i_perm ? 2 : 1]; attn_args.stride_k = k_strides[i_perm ? 2 : 1]; attn_args.stride_v = v_strides[i_perm ? 2 : 1]; @@ -350,7 +371,7 @@ bool run_test(const ck_tile::ArgParser& arg_parser) attn_args.mask_type = 0; avg_ms = - sparge_vsa_fwd_combined(bmap_traits, bmap_args, attn_traits, attn_args, stream_cfg); + sparge_sparge_fwd_combined(bmap_traits, bmap_args, attn_traits, attn_args, stream_cfg); } else { @@ -374,6 +395,23 @@ bool run_test(const ck_tile::ArgParser& arg_parser) o_dev.FromDevice(output_host.data()); block_map_dev.FromDevice(block_map_host.data()); + // ---- optional raw output dump (for bit-identical baseline comparison) ---- + if(!dump_o_path.empty()) + { + std::ofstream ofs(dump_o_path, std::ios::binary | std::ios::trunc); + if(!ofs) + { + std::cerr << "\n [dump_o] failed to open " << dump_o_path << std::endl; + } + else + { + ofs.write(reinterpret_cast(output_host.data()), + static_cast(output_host.get_element_space_size_in_bytes())); + std::cout << "\n [dump_o] wrote " << output_host.get_element_space_size_in_bytes() + << " bytes to " << dump_o_path; + } + } + // ---- count active blocks ---- ck_tile::index_t total_blocks = batch * nhead * num_q_blocks * num_k_blocks; ck_tile::index_t active_blocks = 0; diff --git a/include/ck_tile/ops/sparse_attn/kernel/fmha_fwd_sparge_kernel.hpp b/include/ck_tile/ops/sparse_attn/kernel/fmha_fwd_sparge_kernel.hpp new file mode 100644 index 00000000000..d600ff70754 --- /dev/null +++ b/include/ck_tile/ops/sparse_attn/kernel/fmha_fwd_sparge_kernel.hpp @@ -0,0 +1,442 @@ +// Copyright (c) Advanced Micro Devices, Inc., or its affiliates. +// SPDX-License-Identifier: MIT +#pragma once + +#include "ck_tile/core.hpp" +#include "ck_tile/ops/fmha.hpp" +#include "ck_tile/ops/common.hpp" +#include "ck_tile/ops/fmha/block/block_attention_bias_enum.hpp" +#include "ck_tile/ops/fmha/block/variants.hpp" + +#include +#include +#include +#include + +// S[seqlen_q, seqlen_k] = Q[seqlen_q, hdim_q] @ K[seqlen_k, hdim_q] +// S'[seqlen_q, seqlen_k] = S[seqlen_q, seqlen_k] * Scale[1] +// S''[seqlen_q, seqlen_k] = S'[seqlen_q, seqlen_k] + Bias[seqlen_q, seqlen_k] +// P[seqlen_q, seqlen_k] = Softmax(S''[seqlen_q, seqlen_k]) +// O[seqlen_q, hdim_v] = P[seqlen_q, seqlen_k] @ V^T[hdim_v, seqlen_k] + +namespace ck_tile { + +template +struct FmhaFwdSpargeKernel +{ + using FmhaPipeline = ck_tile::remove_cvref_t; + using EpiloguePipeline = ck_tile::remove_cvref_t; + static constexpr ck_tile::index_t kBlockSize = FmhaPipeline::kBlockSize; + static constexpr ck_tile::index_t kBlockPerCu = FmhaPipeline::kBlockPerCu; + static_assert(kBlockPerCu > 0); + static constexpr ck_tile::index_t kBlockPerCuInput = FmhaPipeline::Problem::kBlockPerCu; + static constexpr bool kEnablePVSkip = kEnablePVSkip_; + + using QDataType = ck_tile::remove_cvref_t; + using KDataType = ck_tile::remove_cvref_t; + using VDataType = ck_tile::remove_cvref_t; + using BiasDataType = ck_tile::remove_cvref_t; + using RandValOutputDataType = + ck_tile::remove_cvref_t; + using LSEDataType = ck_tile::remove_cvref_t; + using ODataType = ck_tile::remove_cvref_t; + using SaccDataType = ck_tile::remove_cvref_t; + + using VLayout = ck_tile::remove_cvref_t; + + static constexpr bool kPadSeqLenQ = FmhaPipeline::kPadSeqLenQ; + static constexpr bool kPadSeqLenK = FmhaPipeline::kPadSeqLenK; + static constexpr bool kPadHeadDimQ = FmhaPipeline::kPadHeadDimQ; + static constexpr bool kPadHeadDimV = FmhaPipeline::kPadHeadDimV; + static constexpr bool kHasLogitsSoftCap = FmhaPipeline::kHasLogitsSoftCap; + static constexpr auto BiasEnum = FmhaPipeline::BiasEnum; + static constexpr bool kStoreLSE = FmhaPipeline::kStoreLSE; + static constexpr bool kHasDropout = FmhaPipeline::kHasDropout; + static constexpr auto QScaleEnum = FmhaPipeline::Problem::QScaleEnum; + static constexpr bool kDoFp8StaticQuant = + (QScaleEnum != ck_tile::BlockAttentionQuantScaleEnum::NO_SCALE); + static_assert(!FmhaPipeline::kIsGroupMode, "Sparge sparse attention supports batch mode only."); + static_assert(BiasEnum == BlockAttentionBiasEnum::NO_BIAS, + "Sparge sparse attention does not support bias."); + static_assert(!kStoreLSE, "Sparge sparse attention does not support LSE output."); + static_assert(!kHasDropout, "Sparge sparse attention does not support dropout."); + static_assert(!kHasLogitsSoftCap, "Sparge sparse attention does not support logits soft-cap."); + static_assert(!kDoFp8StaticQuant, + "Sparge sparse attention does not support FP8 static quantization yet."); + + using AttentionVariant = ck_tile::remove_cvref_t; + using FmhaMask = ck_tile::remove_cvref_t; + static constexpr bool kHasMask = FmhaMask::IsMasking; + + static constexpr bool kUseAsyncCopy = FmhaPipeline::Policy::AsyncCopy; + + template // to avoid duplicated base class prblem, introduce an template + // arg + struct FmhaFwdEmptyKargs + { + }; + + // kargs use aggregate initializer, so no constructor will provided + // use inheritance to minimize karg size + // user need to use MakeKargs() function to create kargs. + struct FmhaFwdCommonKargs + { + const void* q_ptr; + const void* k_ptr; + const void* v_ptr; + const void* lut_ptr; + const void* valid_block_num_ptr; + void* o_ptr; + + ck_tile::index_t seqlen_q; + ck_tile::index_t seqlen_k; + ck_tile::index_t hdim_q; + ck_tile::index_t hdim_v; + + ck_tile::index_t num_head_q; + // for MQA/GQA, nhead could be different. This parameter is nhead_q / nhead_k + // if this param is larger than 1, indicate MQA/GQA case + ck_tile::index_t nhead_ratio_qk; + float scale_s; + float pv_threshold; + + ck_tile::index_t stride_q; + ck_tile::index_t stride_k; + ck_tile::index_t stride_v; + ck_tile::index_t stride_o; + + ck_tile::index_t nhead_stride_q; + ck_tile::index_t nhead_stride_k; + ck_tile::index_t nhead_stride_v; + ck_tile::index_t nhead_stride_o; + }; + + struct FmhaFwdMaskKargs + { + ck_tile::index_t window_size_left, window_size_right; + ck_tile::GenericAttentionMaskEnum mask_type; + }; + + struct FmhaFwdBatchModeKargs + : FmhaFwdCommonKargs, + std::conditional_t> + { + ck_tile::index_t batch_stride_q; + ck_tile::index_t batch_stride_k; + ck_tile::index_t batch_stride_v; + ck_tile::index_t batch_stride_o; + }; + + using Kargs = FmhaFwdBatchModeKargs; + + struct BlockIndices + { + ck_tile::index_t batch_idx; + ck_tile::index_t qo_head_idx; + ck_tile::index_t kv_head_idx; + }; + + // std::variant<> can't take in a list initializer, overload for backward compatibility + CK_TILE_HOST static constexpr Kargs MakeKargs(const void* q_ptr, + const void* k_ptr, + const void* v_ptr, + const void* lut_ptr, + const void* valid_block_num_ptr, + void* o_ptr, + ck_tile::index_t seqlen_q, + ck_tile::index_t seqlen_k, + ck_tile::index_t hdim_q, + ck_tile::index_t hdim_v, + ck_tile::index_t num_head_q, + ck_tile::index_t nhead_ratio_qk, + float scale_s, + float pv_threshold, + ck_tile::index_t stride_q, + ck_tile::index_t stride_k, + ck_tile::index_t stride_v, + ck_tile::index_t stride_o, + ck_tile::index_t nhead_stride_q, + ck_tile::index_t nhead_stride_k, + ck_tile::index_t nhead_stride_v, + ck_tile::index_t nhead_stride_o, + ck_tile::index_t batch_stride_q, + ck_tile::index_t batch_stride_k, + ck_tile::index_t batch_stride_v, + ck_tile::index_t batch_stride_o, + ck_tile::index_t window_size_left, + ck_tile::index_t window_size_right, + ck_tile::index_t mask_type) + { + Kargs kargs{{q_ptr, + k_ptr, + v_ptr, + lut_ptr, + valid_block_num_ptr, + o_ptr, + seqlen_q, + seqlen_k, + hdim_q, + hdim_v, + num_head_q, + nhead_ratio_qk, +#if CK_TILE_FMHA_FWD_FAST_EXP2 + static_cast(scale_s * ck_tile::log2e_v<>), +#else + scale_s, +#endif + pv_threshold, + stride_q, + stride_k, + stride_v, + stride_o, + nhead_stride_q, + nhead_stride_k, + nhead_stride_v, + nhead_stride_o}, // FmhaFwdCommonKargs + {}, // FmhaFwdMaskKargs or FmhaFwdEmptyKargs<1> + batch_stride_q, + batch_stride_k, + batch_stride_v, + batch_stride_o}; + + if constexpr(kHasMask) + { + kargs.window_size_left = window_size_left; + kargs.window_size_right = window_size_right; + kargs.mask_type = static_cast(mask_type); + } + return kargs; + } + + CK_TILE_HOST static constexpr auto GridSize(ck_tile::index_t batch_size_, + ck_tile::index_t nhead_, + ck_tile::index_t seqlen_q_, + ck_tile::index_t hdim_v_) + { + return dim3(ck_tile::integer_divide_ceil(seqlen_q_, FmhaPipeline::kM0) * + ck_tile::integer_divide_ceil(hdim_v_, FmhaPipeline::kN1), + nhead_, + batch_size_); + } + + CK_TILE_DEVICE static constexpr auto GetTileIndex(const Kargs& kargs) + { + const index_t num_tile_n1 = ck_tile::integer_divide_ceil(kargs.hdim_v, FmhaPipeline::kN1); + + const index_t i_block = blockIdx.x; + const index_t i_nhead = blockIdx.y; + const index_t i_batch = blockIdx.z; + + const auto f = [](index_t dividend, index_t divisor) { + index_t quotient = dividend / divisor; + index_t modulus = dividend - quotient * divisor; + return ck_tile::make_tuple(quotient, modulus); + }; + + const auto [i_tile_m, i_tile_n] = f(i_block, num_tile_n1); + + if constexpr(kHasMask) + { + return ck_tile::make_tuple(gridDim.x - 1 - i_tile_m, i_tile_n, i_nhead, i_batch); + } + else + { + return ck_tile::make_tuple(i_tile_m, i_tile_n, i_nhead, i_batch); + } + } + + CK_TILE_HOST static constexpr auto BlockSize() { return dim3(kBlockSize); } + + CK_TILE_HOST_DEVICE static constexpr ck_tile::index_t GetSmemSize() + { + return ck_tile::max(FmhaPipeline::GetSmemSize(), EpiloguePipeline::GetSmemSize()); + } + + CK_TILE_DEVICE void operator()(Kargs kargs) const + { + // allocate LDS + __shared__ char smem_ptr[GetSmemSize()]; + + // divide problem + const auto [i_tile_m, i_tile_n, i_nhead, i_batch] = GetTileIndex(kargs); + + const index_t i_m0 = __builtin_amdgcn_readfirstlane(i_tile_m * FmhaPipeline::kM0); + const index_t i_n1 = __builtin_amdgcn_readfirstlane(i_tile_n * FmhaPipeline::kN1); + + long_index_t batch_offset_q = 0; + long_index_t batch_offset_k = 0; + long_index_t batch_offset_v = 0; + long_index_t batch_offset_o = 0; + + batch_offset_q = static_cast(i_batch) * kargs.batch_stride_q; + batch_offset_k = static_cast(i_batch) * kargs.batch_stride_k; + batch_offset_v = static_cast(i_batch) * kargs.batch_stride_v; + batch_offset_o = static_cast(i_batch) * kargs.batch_stride_o; + + // for simplicity, batch stride we just modify the pointer + const QDataType* q_ptr = reinterpret_cast(kargs.q_ptr) + + static_cast(i_nhead) * kargs.nhead_stride_q + + batch_offset_q; + const KDataType* k_ptr = + reinterpret_cast(kargs.k_ptr) + + static_cast(i_nhead / kargs.nhead_ratio_qk) * kargs.nhead_stride_k + + batch_offset_k; + const VDataType* v_ptr = + reinterpret_cast(kargs.v_ptr) + + static_cast(i_nhead / kargs.nhead_ratio_qk) * kargs.nhead_stride_v + + batch_offset_v; + + // sparse mask + const int* lut_ptr = + reinterpret_cast(kargs.lut_ptr) + + static_cast(i_batch * kargs.num_head_q + i_nhead) * + ck_tile::integer_divide_ceil(kargs.seqlen_q, FmhaPipeline::kM0) * + ck_tile::integer_divide_ceil(kargs.seqlen_k, FmhaPipeline::kN0) + + i_tile_m * ck_tile::integer_divide_ceil(kargs.seqlen_k, FmhaPipeline::kN0); + const int* valid_block_num_ptr = + reinterpret_cast(kargs.valid_block_num_ptr) + + static_cast(i_batch * kargs.num_head_q + i_nhead) * + ck_tile::integer_divide_ceil(kargs.seqlen_q, FmhaPipeline::kM0) + + i_tile_m; + const int valid_block_num_value = valid_block_num_ptr[0]; + + ODataType* o_ptr = reinterpret_cast(kargs.o_ptr) + + static_cast(i_nhead) * kargs.nhead_stride_o + + batch_offset_o; + + // Q/K/V DRAM and DRAM window + const auto q_dram = [&]() { + const auto q_dram_naive = make_naive_tensor_view( + q_ptr, + make_tuple(kargs.seqlen_q, kargs.hdim_q), + make_tuple(kargs.stride_q, 1), + number{}, + number<1>{}); + if constexpr(FmhaPipeline::kQLoadOnce) + { + return pad_tensor_view( + q_dram_naive, + make_tuple(number{}, number{}), + sequence{}); + } + else + { + return pad_tensor_view( + q_dram_naive, + make_tuple(number{}, number{}), + sequence{}); + } + }(); + const auto k_dram = [&]() { + const auto k_dram_naive = make_naive_tensor_view( + k_ptr, + make_tuple(kargs.seqlen_k, kargs.hdim_q), + make_tuple(kargs.stride_k, 1), + number{}, + number<1>{}); + + constexpr bool kPadSeqLenK_ = kUseAsyncCopy ? kPadSeqLenK : false; + return pad_tensor_view( + k_dram_naive, + make_tuple(number{}, number{}), + sequence{}); + }(); + const auto v_dram = [&]() { + if constexpr(std::is_same_v) + { + const auto v_dram_naive = make_naive_tensor_view( + v_ptr, + make_tuple(kargs.seqlen_k, kargs.hdim_v), + make_tuple(kargs.stride_v, 1), + number{}, + number<1>{}); + + const auto v_dram_transposed = + transform_tensor_view(v_dram_naive, + make_tuple(make_pass_through_transform(kargs.hdim_v), + make_pass_through_transform(kargs.seqlen_k)), + make_tuple(sequence<1>{}, sequence<0>{}), + make_tuple(sequence<0>{}, sequence<1>{})); + + constexpr bool kPadSeqLenK_ = kUseAsyncCopy ? kPadSeqLenK : false; + return pad_tensor_view( + v_dram_transposed, + make_tuple(number{}, number{}), + sequence{}); + } + }(); + + auto q_dram_window = make_tile_window( + q_dram, + [&]() { + if constexpr(FmhaPipeline::kQLoadOnce) + return make_tuple(number{}, + number{}); + else + return make_tuple(number{}, number{}); + }(), + {i_m0, 0}); + + auto k_dram_window = make_tile_window( + k_dram, make_tuple(number{}, number{}), {0, 0}); + + auto v_dram_window = + make_tile_window(v_dram, + make_tuple(number{}, number{}), + {i_n1, 0}); + + FmhaMask mask = [&]() { + if constexpr(kHasMask) + return ck_tile::make_generic_attention_mask_from_lr_window( + kargs.window_size_left, + kargs.window_size_right, + kargs.seqlen_q, + kargs.seqlen_k, + kargs.mask_type == GenericAttentionMaskEnum::MASK_FROM_TOP_LEFT); + else + return FmhaMask{kargs.seqlen_q, kargs.seqlen_k}; + }(); + + AttentionVariant variant; + const auto variant_params = ck_tile::StandardAttentionParams{mask, kargs.scale_s}; + + BlockIndices block_indices{i_batch, i_nhead, i_nhead / kargs.nhead_ratio_qk}; + + auto o_acc_tile = FmhaPipeline{}(q_dram_window, + k_dram_window, + v_dram_window, + lut_ptr, + valid_block_num_value, + mask, + kargs.scale_s, + kargs.pv_threshold, + variant, + variant_params, + block_indices, + smem_ptr); + + // O DRAM and O DRAM window + auto o_dram = [&]() { + const auto o_dram_naive = make_naive_tensor_view( + o_ptr, + make_tuple(kargs.seqlen_q, kargs.hdim_v), + make_tuple(kargs.stride_o, 1), + number{}, + number<1>{}); + + return pad_tensor_view( + o_dram_naive, + make_tuple(number{}, number{}), + sequence{}); + }(); + + auto o_dram_window = + make_tile_window(o_dram, + make_tuple(number{}, number{}), + {i_m0, i_n1}); + + EpiloguePipeline{}(o_dram_window, o_acc_tile, nullptr); + } +}; + +} // namespace ck_tile diff --git a/include/ck_tile/ops/sparse_attn/pipeline/block_fmha_pipeline_qr_ks_vs_async_sparge.hpp b/include/ck_tile/ops/sparse_attn/pipeline/block_fmha_pipeline_qr_ks_vs_async_sparge.hpp new file mode 100644 index 00000000000..4bf3c9d296c --- /dev/null +++ b/include/ck_tile/ops/sparse_attn/pipeline/block_fmha_pipeline_qr_ks_vs_async_sparge.hpp @@ -0,0 +1,698 @@ +// Copyright (c) Advanced Micro Devices, Inc., or its affiliates. +// SPDX-License-Identifier: MIT +#pragma once + +#include "ck_tile/core.hpp" +#include "ck_tile/ops/common/tensor_layout.hpp" +#include "ck_tile/ops/fmha/block/block_attention_bias_enum.hpp" +#include "ck_tile/ops/fmha/pipeline/block_fmha_pipeline_qr_ks_vs_async_default_policy.hpp" +#include "ck_tile/ops/fmha/block/block_dropout.hpp" +#include "ck_tile/ops/reduce/block/block_reduce.hpp" + +namespace ck_tile { + +// Sparge variant of qr/ks/vs/async pipeline. Cloned from BlockFmhaPipelineQRKSVSAsyncVSA; +// adds PV-skip per Q-tile (SpargeAttn paper 4.4). Kept as a separate file so the original +// _vsa.hpp can remain frozen as an A/B baseline. +// +// QUANT-HOOK: future int8/sage variant will add QScaleEnum template arg + per-tile descale Kargs; +// _sparge_sage.hpp will live alongside this file and reuse the PV-skip path verbatim. +template +struct BlockFmhaPipelineQRKSVSAsyncSparge +{ + static constexpr bool kEnablePVSkip = kEnablePVSkip_; + + using Problem = remove_cvref_t; + using Policy = remove_cvref_t; + using QDataType = remove_cvref_t; + using KDataType = remove_cvref_t; + using VDataType = remove_cvref_t; + using SaccDataType = remove_cvref_t; + using SMPLComputeDataType = remove_cvref_t; + using BiasDataType = remove_cvref_t; + using RandValOutputDataType = remove_cvref_t; + using LSEDataType = remove_cvref_t; + using PDataType = remove_cvref_t; + using OaccDataType = remove_cvref_t; + using ODataType = remove_cvref_t; + using AttentionVariant = remove_cvref_t; + using FmhaMask = remove_cvref_t; + + using BlockFmhaShape = remove_cvref_t; + using VLayout = remove_cvref_t; + static constexpr bool kQLoadOnce = true; // if q_tile load whole block length (hdim) at once + static_assert(kQLoadOnce == Policy::QLoadOnce); + + static constexpr index_t kBlockSize = Problem::kBlockSize; + + static constexpr index_t kM0 = BlockFmhaShape::kM0; + static constexpr index_t kN0 = BlockFmhaShape::kN0; + static constexpr index_t kK0 = BlockFmhaShape::kK0; + static constexpr index_t kN1 = BlockFmhaShape::kN1; + static constexpr index_t kK1 = BlockFmhaShape::kK1; + static constexpr index_t kQKHeaddim = BlockFmhaShape::kQKHeaddim; + static constexpr index_t kSubQKHeaddim = BlockFmhaShape::kSubQKHeaddim; + + static_assert(kSubQKHeaddim <= 256, "hdim bigger than 256 is not suitable for this pipeline!"); + + static constexpr bool kIsGroupMode = Problem::kIsGroupMode; + // TODO: seq_q always support padding, hdim_q/v support multiple of vector(like 8x) + // only need special care about seq_k padding (oob need set -INF of p instead of zero) + static_assert(Problem::kPadSeqLenQ == true && Problem::kPadHeadDimQ == true && + Problem::kPadHeadDimV == true); + static constexpr bool kPadSeqLenQ = true; + static constexpr bool kPadSeqLenK = Problem::kPadSeqLenK; + static constexpr bool kPadHeadDimQ = true; // support multiple of vector(like 8x) + static constexpr bool kPadHeadDimV = true; // support multiple of vector(like 8x) + static constexpr bool kHasLogitsSoftCap = Problem::kHasLogitsSoftCap; + static constexpr auto BiasEnum = Problem::BiasEnum; + static constexpr bool kStoreLSE = Problem::kStoreLSE; + static constexpr bool kHasDropout = Problem::kHasDropout; + + static_assert(BiasEnum == BlockAttentionBiasEnum::NO_BIAS, + "VSA sparse attention does not support bias."); + static_assert(!kHasDropout, "VSA sparse attention does not support dropout."); + static_assert(!kStoreLSE, "VSA sparse attention does not support LSE output."); + static_assert(!kHasLogitsSoftCap, "VSA sparse attention does not support logits soft-cap."); + + // last dimension vector length used to create tensor view(and decide buffer_load vector length) + // ... together with tensor distribution. tensor dist should able to overwrite this + static constexpr index_t kAlignmentQ = Policy::template GetAlignmentQ(); + static constexpr index_t kAlignmentK = Policy::template GetAlignmentK(); + static constexpr index_t kAlignmentV = []() { + if constexpr(std::is_same_v) + return Policy::template GetAlignmentV(); + else + return kPadSeqLenK ? 1 : Policy::template GetAlignmentV(); + }(); + static constexpr index_t kAlignmentO = Policy::template GetAlignmentO(); + +#if CK_TILE_FMHA_FWD_FAST_EXP2 + static constexpr auto R_LOG2E = 1.0 / log2e_v; +#endif + + static constexpr index_t kBlockPerCu = []() { + if constexpr(Problem::kBlockPerCu != -1) + return Problem::kBlockPerCu; + else + { + // minimize occupancy + if constexpr(kQKHeaddim <= 32) + { + if constexpr(kPadSeqLenK && FmhaMask::IsMasking) + return 1; + else + return 2; + } + else if constexpr(kQKHeaddim <= 64) + { + if constexpr(kPadSeqLenK) + return 2; + else + return 3; + } + else if constexpr(kQKHeaddim <= 128) + { + if constexpr(kPadSeqLenK) + return 1; + else + return 2; + } + else if constexpr(kQKHeaddim <= 192) + { + if constexpr(kPadSeqLenK) + return 1; + else + return 2; + } + else if constexpr(kQKHeaddim <= 256) + { + return 1; + } + else + { + return 1; + }; + } + }(); + + static constexpr const char* name = "qr_async"; + + CK_TILE_HOST_DEVICE static constexpr ck_tile::index_t GetSmemSize() + { + return Policy::template GetSmemSize(); + } + + template + CK_TILE_HOST_DEVICE auto + operator()(const QDramBlockWindowTmp& q_dram_block_window_tmp, // M0*K0 tile + const KDramBlockWindowTmp& k_dram_block_window_tmp, // N0*K0 tile + const VDramBlockWindowTmp& v_dram_block_window_tmp, // N1*K1 tile + const int* kv_block_idx_ptr, + int kv_blocks, + FmhaMask mask, + float scale_s, + float pv_threshold, // SpargeAttn PV-skip threshold; see §2 of pv_skip plan + const AttentionVariant& variant, + const AttentionVariantParams& variant_params, + const BlockIndices& block_indices, + void* smem_ptr) const + { + if constexpr(!kEnablePVSkip) + { + (void)pv_threshold; // silence unused-param when PV-skip is compiled out + } + // R25 Step 1 redesign D: PV-skip control is a compile-time gate + // (kEnablePVSkip). The entire PV-skip logic block below is wrapped in + // `if constexpr (kEnablePVSkip)`, so when this template parameter is + // false the AST contains no vote, no scalar gate, no extra LDS, and + // codegen converges with _vsa.hpp's FmhaFwdVSAKernel. + // + // Runtime fast-path (C3-lite): pv_threshold == +1e30 sentinel disables + // the skip at runtime via one scalar branch (sgpr); kept inside the + // `if constexpr` so the OFF instantiation pays zero cost. + static_assert( + std::is_same_v> && + std::is_same_v> && + std::is_same_v>, + "wrong!"); + + static_assert(kM0 == QDramBlockWindowTmp{}.get_window_lengths()[number<0>{}] && + kN0 == KDramBlockWindowTmp{}.get_window_lengths()[number<0>{}] && + kK0 == KDramBlockWindowTmp{}.get_window_lengths()[number<1>{}] && + kN1 == VDramBlockWindowTmp{}.get_window_lengths()[number<0>{}] && + kK1 == VDramBlockWindowTmp{}.get_window_lengths()[number<1>{}], + "wrong!"); + + constexpr auto LdsSeq = Policy::template GetLdsBufferSequence(); + + // K tile in LDS + auto k_lds_ptr = reinterpret_cast(smem_ptr); + auto k_lds_store = generate_tuple( + [&](auto i_buf) { + return make_tile_window( + make_tensor_view( + k_lds_ptr, Policy::template MakeKLdsStoreBlockDescriptor(i_buf)), + Policy::template MakeKLdsStoreBlockDescriptor(i_buf).get_lengths(), + {0, 0, 0}); + }, + number{}); + + auto k_lds_Load_view = make_tensor_view( + k_lds_ptr, Policy::template MakeKLdsLoadBlockDescriptor()); + + auto k_lds_load = + make_tile_window(k_lds_Load_view, + Policy::template MakeKLdsLoadBlockDescriptor().get_lengths(), + {0, 0}); + + // V tile in LDS + auto v_lds = make_tensor_view( + reinterpret_cast(smem_ptr), + Policy::template MakeVLdsBlockDescriptor()); + auto v_lds_window = make_tile_window( + v_lds, Policy::template MakeVLdsBlockDescriptor().get_lengths(), {0, 0}); + + // Block GEMM + constexpr auto gemm_0 = Policy::template GetQKBlockGemm(); + constexpr auto gemm_1 = Policy::template GetKVBlockGemm(); + + int seqlen_k_start = kv_block_idx_ptr[0] * kN0; + auto q_dram_window = make_tile_window(q_dram_block_window_tmp.get_bottom_tensor_view(), + q_dram_block_window_tmp.get_window_lengths(), + q_dram_block_window_tmp.get_window_origin(), + Policy::template MakeQRegTileDistribution()); + q_dram_window.init_raw(); + + // TODO: we use async Copy for K, which is inline asm + // a side effect is we have to use inline asm for q as well + auto q = decltype(load_tile(q_dram_window)){}; + // TODO: start from rocm-6.2, compiler will have problem if manually set clear of q. + // however, q would be cleared in the constructor of static distributed tensor + // set_tile(q, number<0>{}); // use per-dword clear to avoid scratch + load_tile_raw(q, q_dram_window); + __builtin_amdgcn_sched_barrier(0); + + using SaccBlockTileType = decltype(gemm_0.MakeCBlockTile()); + auto s_acc = SaccBlockTileType{}; + + // reduction function for softmax + const auto f_max = [](auto e0, auto e1) { return max(e0, e1); }; + const auto f_sum = [](auto e0, auto e1) { return e0 + e1; }; + + // infer Sacc, S, P, M, L, Oacc type + using SBlockTileType = decltype(cast_tile(s_acc)); + + using MLBlockTileType = decltype(block_tile_reduce( + SBlockTileType{}, sequence<1>{}, f_max, SMPLComputeDataType{0})); + + using OaccBlockTileType = decltype(gemm_1.MakeCBlockTile()); + + // init Oacc, M, L + auto o_acc = OaccBlockTileType{}; + auto m = MLBlockTileType{}; + auto l = MLBlockTileType{}; + + clear_tile(o_acc); + set_tile(m, -numeric::infinity()); + clear_tile(l); + + __builtin_amdgcn_sched_barrier(0); + const auto q_origin = q_dram_window.get_window_origin(); + const auto num_total_loop = kv_blocks; + + // check early exit if no work to do + if constexpr(FmhaMask::IsMasking || kPadSeqLenK) + { + if(num_total_loop <= 0) + { + buffer_load_fence(0); // rocm-6.1, if whole tile is masked out, need to fence(0) + // otherwise will have compute error(maybe compiler bug?) + + // Note: here occ are all cleard, return it + return o_acc; + } + __builtin_amdgcn_sched_barrier(0); // make sure sched_barrier(0) for this check + } + + auto k_dram_block_window = + make_tile_window(k_dram_block_window_tmp.get_bottom_tensor_view(), + k_dram_block_window_tmp.get_window_lengths(), + {seqlen_k_start, 0}); + + auto k_dram_window = make_tile_window( + k_dram_block_window.get_bottom_tensor_view(), + k_dram_block_window.get_window_lengths(), + k_dram_block_window.get_window_origin(), + Policy::template MakeKDramTileDistribution()); // K DRAM tile window for + // load + k_dram_window.init_raw(); + constexpr auto k_oob_ck = bool_constant{}; + constexpr auto k_pre_np = bool_constant{}; + auto v_dram_window = + make_tile_window(v_dram_block_window_tmp.get_bottom_tensor_view(), + v_dram_block_window_tmp.get_window_lengths(), + {0, seqlen_k_start}, // TODO: hdim split? + Policy::template MakeVDramTileDistribution()); + + // prefetch K tile + async_load_tile_raw( + k_lds_store(LdsSeq.at(number<0>{})), k_dram_window, number<-1>{}, k_oob_ck, k_pre_np); + move_tile_window(k_dram_window, {0, kK0}); + __builtin_amdgcn_sched_barrier(0); + + // buffer_load_fence(k_dram_window.get_num_of_access(), q.get_thread_buffer()); + buffer_load_fence(k_dram_window.get_num_of_access()); + + index_t i_total_loops = 0; + constexpr index_t k0_loops = kQKHeaddim / kK0; + constexpr index_t k1_loops = kN0 / kK1; + + static_assert(1 <= k0_loops); + static_assert(1 <= k1_loops); + // main loop + do + { + // STAGE 1, QK gemm + clear_tile(s_acc); // initialize C + if constexpr(k0_loops > 1) + { + static_for<0, k0_loops - 1, 1>{}([&](auto i_k0) { + async_load_tile_raw(k_lds_store(number{})>{}), + k_dram_window, + number<-1>{}, + k_oob_ck, + k_pre_np); + if constexpr(i_k0 < k0_loops - 1) + move_tile_window(k_dram_window, {0, kK0}); + + async_load_fence(k_dram_window.get_num_of_access()); + __builtin_amdgcn_s_barrier(); + __builtin_amdgcn_sched_barrier(0); + gemm_0(s_acc, + get_slice_tile( + q, sequence<0, i_k0 * kK0>{}, sequence{}), + get_slice_tile(k_lds_load, + sequence<(LdsSeq.at(number{})) * kN0, 0>{}, + sequence<(LdsSeq.at(number{}) + 1) * kN0, kK0>{})); + }); + } + + // TODO: this to fix a bug when loop smaller than 2, + // the following fence/barrier will be scheduled inside 1st loop + if constexpr(k0_loops <= 2) + __builtin_amdgcn_sched_barrier(0); + + async_load_fence(); + __builtin_amdgcn_s_barrier(); + + int block_idx = kv_block_idx_ptr[i_total_loops + 1]; + auto v_buf = load_tile(v_dram_window, number<-1>{}, bool_constant{}); + __builtin_amdgcn_sched_barrier(0); + { // tail + gemm_0( + s_acc, + get_slice_tile( + q, sequence<0, (k0_loops - 1) * kK0>{}, sequence{}), + get_slice_tile(k_lds_load, + sequence<(LdsSeq.at(number{})) * kN0, 0>{}, + sequence<(LdsSeq.at(number{}) + 1) * kN0, kK0>{})); + } + __builtin_amdgcn_sched_barrier(1); + + // STAGE 2, scale_s, mask, softmax (no bias/soft-cap) +#if !CK_TILE_FMHA_FWD_FAST_EXP2 + tile_elementwise_inout([&scale_s](auto& x) { x = x * scale_s; }, s_acc); +#endif + if constexpr(kPadSeqLenK || FmhaMask::IsMasking) + { + const auto k_origin = k_dram_block_window.get_window_origin(); + bool need_perpixel_check = mask.IsEdgeTile(q_origin.at(number<0>{}), + k_origin.at(number<0>{}), + number{}, + number{}); + + if(need_perpixel_check) + { + set_tile_if( + s_acc, -numeric::infinity(), [&](auto tile_idx) { + const auto row = q_origin.at(number<0>{}) + tile_idx.at(number<0>{}); + const auto col = k_origin.at(number<0>{}) + tile_idx.at(number<1>{}); + return !variant.LogitsMask(variant_params, + block_indices.batch_idx, + row, + col, + block_indices.qo_head_idx, + block_indices.kv_head_idx); + }); + } + } + + const auto s = cast_tile(s_acc); // S{j} + auto m_local = block_tile_reduce( + s, + sequence<1>{}, + f_max, + -numeric::infinity()); // m_local = rowmax(S{j}) + block_tile_reduce_sync(m_local, f_max, bool_constant{}); + + const auto m_old = m; // m{j-1} + tile_elementwise_inout( + [](auto& e0, auto e1, auto e2) { e0 = max(e1, e2); }, m, m_old, m_local); // m{j} + + auto p_compute = make_static_distributed_tensor( + s.get_tile_distribution()); // Pcompute{j} + + __builtin_amdgcn_sched_barrier(0x7F); + // Ensure gemm_0's LDS reads (K tile) from all threads are completed before V store + // Only needed when K tail and V use the same LDS buffer + if constexpr(LdsSeq.at(number{}) == LdsSeq.at(number{})) + { + __builtin_amdgcn_s_barrier(); + } + // store & prefetch next v, after the max reduction. + // R25 Step 1 redesign D: V→LDS store and the next-V DRAM load are + // UNCONDITIONAL — per-warp PV-skip cannot gate them (cross-warp + // shared LDS state; see Researcher audit A.3/A.4/A.5). + if constexpr(std::is_same_v) + { + auto v_shuffle_tmp = make_static_distributed_tensor( + Policy::template MakeShuffledVRegBlockDescriptor()); + shuffle_tile(v_shuffle_tmp, v_buf); + + auto v_lds_window_tmp = + get_slice_tile(v_lds_window, + sequence<(LdsSeq.at(number{})) * kN1, 0>{}, + sequence<(LdsSeq.at(number{}) + 1) * kN1, kK1>{}); + + store_tile(v_lds_window_tmp, v_shuffle_tmp); + } + else + { + auto v_lds_window_tmp = + get_slice_tile(v_lds_window, + sequence<(LdsSeq.at(number{})) * kN1, 0>{}, + sequence<(LdsSeq.at(number{}) + 1) * kN1, kK1>{}); + store_tile(v_lds_window_tmp, v_buf); + } + + if constexpr(k1_loops > 1) + { + move_tile_window( + v_dram_window, + {0, kK1}); // will have scratch if move this right after load_tile(v_dram)... + v_buf = load_tile( + v_dram_window, number<-1>{}, bool_constant{}); // load next v_buf + } + __builtin_amdgcn_sched_barrier(0); + + // ================================================================ + // PV-SKIP per Q-tile (SpargeAttn paper §4.4) + // R25 Step 1 redesign D — per-warp arithmetic-only: + // Compile-time `if constexpr (kEnablePVSkip)` wraps the entire + // block. When kEnablePVSkip=false the AST has zero PV-skip + // artifacts → codegen converges with _vsa.hpp. + // + // When enabled, a per-warp predicate gates ONLY the per-row, + // VGPR-private work (exp2 → p_compute, rowsum, `l += rowsum_p`). + // V load / V→LDS store / gemm_1 / every `s_barrier` / + // `block_sync_lds` stay unconditional (cross-warp LDS dep — see + // Researcher audit A.7). + // + // On warp_skip, this warp's owned rows of p_compute are zeroed + // so the unconditional gemm_1 contributes 0 to o_acc (audit + // A.7 "simplest realisation"). The alpha-rescale `l *= tmp` and + // `o *= tmp` still apply. + // + // pv_threshold semantics shift: now per-warp max diff (slightly + // more aggressive than per-block at the same threshold; matches + // upstream SpargeAttn `kPerWarp` mode default). + // + // Skip iff: scale_s * (m_local - m_old) + pv_threshold <= 0 + // (where m_local/m_old are warp-uniform after block_tile_reduce_sync) + // ================================================================ + // Per-warp PV-skip predicate. Only declared when kEnablePVSkip; + // wrapped in a lambda so the false instantiation contains nothing. + auto compute_warp_skip = [&]() { + if constexpr(kEnablePVSkip) + { + // C3-lite scalar fast-path: pv_threshold == +1e30 sentinel + // disables skip; runtime cost is a single sgpr branch. + if(pv_threshold >= 1e29f) + return false; + // Per-row predicate: warp-AND over rows this warp owns. + int warp_skip_int = 1; + constexpr auto m_spans = decltype(m_local)::get_distributed_spans(); + sweep_tile_span(m_spans[number<0>{}], [&](auto idx0) { + constexpr auto i_idx = make_tuple(idx0); + const float diff = scale_s * (static_cast(m_local[i_idx]) - + static_cast(m_old[i_idx])); + if(!(diff + pv_threshold <= 0.0f)) + warp_skip_int = 0; + }); + // Warp-level AND reduce (wave=64 on gfx942; xor butterfly). + // No LDS, no s_barrier, no cross-warp dependency. + warp_skip_int &= __shfl_xor(warp_skip_int, 32); + warp_skip_int &= __shfl_xor(warp_skip_int, 16); + warp_skip_int &= __shfl_xor(warp_skip_int, 8); + warp_skip_int &= __shfl_xor(warp_skip_int, 4); + warp_skip_int &= __shfl_xor(warp_skip_int, 2); + warp_skip_int &= __shfl_xor(warp_skip_int, 1); + return warp_skip_int != 0; + } + else + { + return false; + } + }; + const bool warp_skip = compute_warp_skip(); + + static const auto get_validated_m = [](SMPLComputeDataType raw_m) { + if constexpr(FmhaMask::IsMasking) + { + return raw_m == -numeric::infinity() + ? type_convert(0.f) + : raw_m; + } + else + { + return raw_m; + } + }; + + // exp2 → p_compute and rowsum_p. + // R25 redesign D: when kEnablePVSkip + warp_skip, we zero this + // warp's owned rows of p_compute so the unconditional gemm_1 + // contributes zero to o_acc, and skip the rowsum. + constexpr auto p_spans = decltype(p_compute)::get_distributed_spans(); + sweep_tile_span(p_spans[number<0>{}], [&](auto idx0) { + constexpr auto i_idx = make_tuple(idx0); +#if CK_TILE_FMHA_FWD_FAST_EXP2 + auto row_max = scale_s * get_validated_m(m[i_idx]); +#endif + sweep_tile_span(p_spans[number<1>{}], [&](auto idx1) { + constexpr auto i_j_idx = make_tuple(idx0, idx1); + if constexpr(kEnablePVSkip) + { + if(warp_skip) + { + p_compute(i_j_idx) = SMPLComputeDataType{0}; + return; + } + } +#if CK_TILE_FMHA_FWD_FAST_EXP2 + p_compute(i_j_idx) = exp2(scale_s * s[i_j_idx] - row_max); +#else + p_compute(i_j_idx) = exp(s[i_j_idx] - get_validated_m(m[i_idx])); +#endif + }); + }); + + auto rowsum_p = block_tile_reduce( + p_compute, sequence<1>{}, f_sum, SMPLComputeDataType{0}); // rowsum(Pcompute{j}) + + block_tile_reduce_sync(rowsum_p, f_sum, bool_constant{}); + + // l{j}, Oacc{j}: alpha rescale of l / o always runs. + // When warp_skip, rowsum_p is already 0 for this + // warp's owned rows (p_compute zeroed above), so + // `l += rowsum_p` is a no-op — no extra branch needed. + constexpr auto o_spans = decltype(o_acc)::get_distributed_spans(); + sweep_tile_span(o_spans[number<0>{}], [&](auto idx0) { + constexpr auto i_idx = make_tuple(idx0); +#if CK_TILE_FMHA_FWD_FAST_EXP2 + const auto tmp = [&]() { + auto row_max = scale_s * get_validated_m(m[i_idx]); + return exp2(scale_s * m_old[i_idx] - row_max); + }(); +#else + const auto tmp = exp(m_old[i_idx] - get_validated_m(m[i_idx])); +#endif + l(i_idx) = tmp * l[i_idx] + rowsum_p[i_idx]; + sweep_tile_span(o_spans[number<1>{}], [&](auto idx1) { + constexpr auto i_j_idx = make_tuple(idx0, idx1); + // FIXME: this use different equation from FA v2 paper, + // but produce correc result. + // Is the equation wrong? + o_acc(i_j_idx) *= tmp; + }); + }); + + const auto p = [&]() { + if constexpr(std::is_same_v) + return impl::cast_tile_pkrtz_fp16_fp32(p_compute); + else + return cast_tile(p_compute); + }(); + + // STAGE 3, KV gemm — always runs (block-wide LDS dep; per-warp + // skipping has been absorbed by zeroing p_compute rows above). + { + if constexpr(k1_loops > 1) + { + static_for<0, k1_loops - 1, 1>{}([&](auto i_k1) { + if constexpr(i_k1 != 0 && i_k1 < k1_loops - 1) + { + v_buf = load_tile(v_dram_window, + number<-1>{}, + bool_constant{}); // load next v_buf + } + block_sync_lds(); + gemm_1( + o_acc, + get_slice_tile( + p, sequence<0, i_k1 * kK1>{}, sequence{}), + get_slice_tile( + v_lds_window, + sequence<(LdsSeq.at(number{})) * kN1, 0>{}, + sequence<(LdsSeq.at(number{}) + 1) * kN1, kK1>{})); + + if constexpr(std::is_same_v) + { + auto v_shuffle_tmp = make_static_distributed_tensor( + Policy::template MakeShuffledVRegBlockDescriptor()); + shuffle_tile(v_shuffle_tmp, v_buf); + auto v_lds_window_tmp = get_slice_tile( + v_lds_window, + sequence<(LdsSeq.at(number{})) * kN1, 0>{}, + sequence<(LdsSeq.at(number{}) + 1) * kN1, + kK1>{}); + store_tile(v_lds_window_tmp, v_shuffle_tmp); + } + else + { + auto v_lds_window_tmp = get_slice_tile( + v_lds_window, + sequence<(LdsSeq.at(number{})) * kN1, 0>{}, + sequence<(LdsSeq.at(number{}) + 1) * kN1, + kK1>{}); + store_tile(v_lds_window_tmp, v_buf); + } + if constexpr(i_k1 < k1_loops - 1) + move_tile_window(v_dram_window, {0, kK1}); + }); + } + } + i_total_loops++; + if(i_total_loops < num_total_loop) + { + // V load runs unconditionally under redesign D, so no skip + // compensation needed (same offset arithmetic as _vsa.hpp). + move_tile_window(v_dram_window, {0, kN0 * (block_idx - 1)}); + move_tile_window(k_dram_block_window, {kN0 * block_idx, 0}); + k_dram_window.set_window_origin(k_dram_block_window.get_window_origin()); + + if constexpr(k1_loops >= 2 && + LdsSeq.at(number<0>{}) == LdsSeq.at(number{})) + __builtin_amdgcn_s_barrier(); + async_load_tile_raw(k_lds_store(LdsSeq.at(number<0>{})), + k_dram_window, + number<-1>{}, + k_oob_ck, + k_pre_np); + move_tile_window(k_dram_window, {0, kK0}); + } + // tail — gemm_1 runs unconditionally under redesign D. + { + block_sync_lds(); + gemm_1( + o_acc, + get_slice_tile(p, sequence<0, (k1_loops - 1) * kK1>{}, sequence{}), + get_slice_tile( + v_lds_window, + sequence<(LdsSeq.at(number{})) * kN1, 0>{}, + sequence<(LdsSeq.at(number{}) + 1) * kN1, kK1>{})); + } + } while(i_total_loops < num_total_loop); + + // finally, O + constexpr auto o_spans = decltype(o_acc)::get_distributed_spans(); + + sweep_tile_span(o_spans[number<0>{}], [&](auto idx0) { + constexpr auto i_idx = make_tuple(idx0); + const auto tmp = [&]() { + if constexpr(FmhaMask::IsMasking) + { + return l[i_idx] == 0.f ? 0.f : 1 / l[i_idx]; + } + else + return 1 / l[i_idx]; + }(); + sweep_tile_span(o_spans[number<1>{}], [&](auto idx1) { + constexpr auto i_j_idx = make_tuple(idx0, idx1); + o_acc(i_j_idx) *= tmp; + }); + }); + + return o_acc; + } +}; + +} // namespace ck_tile From d939c3b4fcbc6d220874ccf3b81a2a6c469cb680 Mon Sep 17 00:00:00 2001 From: Gino Lu Date: Tue, 19 May 2026 21:45:23 -0400 Subject: [PATCH 13/16] sparse_attn: split-launch dispatch + 3-mode PV-skip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Per-head pv_threshold via head_remap LUT (CLI: -pv_threshold_per_head); sentinel 1e30 routes to kEnablePVSkip=false bucket - kEnablePVSkip bool → PVSkipMode enum {kNone, kPerWarp, kPerBlock}; new kPerBlock matches upstream sm80 (LDS vote, V loads unconditional). CLI: -pv_mode={none,warp,block}, default warp - README: PV-skip modes section + MI300X 3-curve sparsity chart Co-Authored-By: Claude Opus 4 --- example/ck_tile/50_sparse_attn/README.md | 17 +- .../codegen/ops/fmha_fwd_sparge.py | 99 ++++++--- .../docs/pv_skip_mode_comparison.png | Bin 0 -> 113868 bytes .../ck_tile/50_sparse_attn/fmha_fwd_trek.hpp | 48 ++++- .../50_sparse_attn/sparge_blockmap_inst.cpp | 136 +++++++++++- .../ck_tile/50_sparse_attn/test_sparge.cpp | 101 +++++++-- .../kernel/fmha_fwd_sparge_kernel.hpp | 62 +++++- ...ck_fmha_pipeline_qr_ks_vs_async_sparge.hpp | 197 ++++++++++++++++-- 8 files changed, 575 insertions(+), 85 deletions(-) create mode 100644 example/ck_tile/50_sparse_attn/docs/pv_skip_mode_comparison.png diff --git a/example/ck_tile/50_sparse_attn/README.md b/example/ck_tile/50_sparse_attn/README.md index c7191c8e828..9fdad906de6 100644 --- a/example/ck_tile/50_sparse_attn/README.md +++ b/example/ck_tile/50_sparse_attn/README.md @@ -14,10 +14,23 @@ Not yet ported (upstream pinned to commit [`ae5b629`](https://github.com/thu-ml/ - **K smoothing** — pre-pool `k -= km`; required for diffusion / video checkpoints (CogVideoX, Mochi-1, Flux, OpenSora, SD 3.5) ([spas_sage_attn/core.py:L53](https://github.com/thu-ml/SpargeAttn/blob/ae5b629ebb41e41f86b3ea2ab5a3283f13ac151a/spas_sage_attn/core.py#L53)) - **is_causal mask in pooled score** — required for causal-LM prefill (Llama, Qwen) ([spas_sage_attn/utils.py:L338](https://github.com/thu-ml/SpargeAttn/blob/ae5b629ebb41e41f86b3ea2ab5a3283f13ac151a/spas_sage_attn/utils.py#L338)) - **attention_sink** — column 0 forced ON; upstream is hard-wired to `True` at inference ([spas_sage_attn/autotune.py:L355](https://github.com/thu-ml/SpargeAttn/blob/ae5b629ebb41e41f86b3ea2ab5a3283f13ac151a/spas_sage_attn/autotune.py#L355)) -- **pv_threshold per-Q-tile skip in attn kernel** — pure perf, ~5–15% on the dominant attention slice ([spas_sage_attn/core.py:L265](https://github.com/thu-ml/SpargeAttn/blob/ae5b629ebb41e41f86b3ea2ab5a3283f13ac151a/spas_sage_attn/core.py#L265)) - **Sort-based top-k selection** — replaces our O(N_k^2) iterative argmax; matters at long seqlen (s ≥ 16k) ([spas_sage_attn/utils.py:L345](https://github.com/thu-ml/SpargeAttn/blob/ae5b629ebb41e41f86b3ea2ab5a3283f13ac151a/spas_sage_attn/utils.py#L345)) - **Q/K int8 quant fusion in pool kernel** — enables a downstream int8 GEMM0 in the attn kernel ([spas_sage_attn/utils.py:L371](https://github.com/thu-ml/SpargeAttn/blob/ae5b629ebb41e41f86b3ea2ab5a3283f13ac151a/spas_sage_attn/utils.py#L371)) +## PV-skip modes + +`pv_threshold` per-Q-tile skip in the attention kernel is implemented in three variants, selectable at runtime via `-pv_mode={none|warp|block}`: + +- **`none`** — skip disabled; baseline matching the no-PV-skip codegen instance. +- **`warp`** (per-wavefront) — each wavefront votes locally via `__shfl_xor` butterfly AND; SGPR-resident flag. CK-tile-specific variant, not in upstream. +- **`block`** (per-block) — block-wide consensus vote via LDS broadcast; aligned with upstream sm80 ([`qk_int_sv_f16_cuda_sm80.cuh:L334`](https://github.com/thu-ml/SpargeAttn/blob/ae5b629ebb41e41f86b3ea2ab5a3283f13ac151a/csrc/qattn/qk_int_sv_f16_cuda_sm80.cuh#L334)). V loads stay unconditional in all modes — the guard wraps the PV MMA only, matching upstream and paper Algorithm 1. + +![PV-skip mode comparison](docs/pv_skip_mode_comparison.png) + +*MI300X, b=2 h=16 s=8192 d=128 fp16, 5 seeds × 9 sparsity points. All three modes dispatch to the `kM0=64 padK=0` tile bucket at this shape.* + +On the canonical recipe shape, `none > warp > block` at every measured sparsity, with no crossover. The per-block guard adds +33..+35 VGPR (6..9 spills) on this tile configuration, depressing occupancy. `warp` is +0..+4 VGPR. The default is `-pv_mode=warp` (preserves R25 A1 behaviour); switch to `none` for the no-skip baseline or `block` to exercise the upstream-aligned variant. A shape sweep is needed before recommending `block` as default — the `kM0=128` path has Δ ≈ 0 VGPR for per-block and is a candidate. + ## Performance At b=2 h=32 s=16384 fp16, sparge (vsa backend) reaches **1.78× FMHA throughput at topk=0.4** and **5.04× at topk=0.1**, and stays above 1.0× across the full topk range. @@ -37,6 +50,8 @@ ninja tile_example_sparge ./bin/tile_example_sparge -pipeline=vsa -b=2 -h=32 -s=16384 -d=128 -topk=0.4 -simthreshd1=0.001 ``` +Select a PV-skip variant with `-pv_mode={none|warp|block}` (default `warp`); finite `-pv_threshold=20` lets the per-Q-tile skip predicate fire. + Add `-v=1` for CPU validation; use a small shape (`-b=1 -h=2 -s=512`), since full-shape CPU reference scales O(s²) and runs 30+ minutes at s=8k, hours at s=16k. ## References diff --git a/example/ck_tile/50_sparse_attn/codegen/ops/fmha_fwd_sparge.py b/example/ck_tile/50_sparse_attn/codegen/ops/fmha_fwd_sparge.py index 9489d3758fd..e5182c3dc89 100644 --- a/example/ck_tile/50_sparse_attn/codegen/ops/fmha_fwd_sparge.py +++ b/example/ck_tile/50_sparse_attn/codegen/ops/fmha_fwd_sparge.py @@ -114,36 +114,67 @@ def update_file(file_path, content): {F_trload}, fmha_trait_{F_idx}>; -// R25 V0: instantiate the Sparge pipeline with kEnablePVSkip = true (existing path) -// AND kEnablePVSkip = false (PV-skip AST removed at compile time, source-equivalent -// to the frozen VSA reference). Both kernels live in the same TU; the host dispatch -// in fmha_sparge_fwd_api.cpp picks one based on fmha_sparge_fwd_args::pv_skip_compile. +// R30: emit 3 pipeline / kernel instances per traits combo — kNone (PV-skip +// AST removed; source-equivalent to VSA), kPerWave (R25 A1 shipped path), +// kPerBlock (R30 added: block-wide AND vote gates gemm_1). The host dispatch +// in fmha_sparge_fwd_api.cpp picks one based on +// fmha_sparge_fwd_args::pv_mode_compile (0/1/2). +// R26 split-launch: fmha_fwd_create_kargs_and_grids(a) forwards the new +// fmha_sparge_fwd_args fields (pv_threshold_per_head_ptr, head_remap_ptr, +// nhead_in_launch) to MakeKargs. When head_remap_ptr is non-null the wrapper +// also shrinks grids.y to nhead_in_launch so each bucket fires its own kernel. +// Suffixes: +// _pvsf = PV-Skip OFF (kNone) +// _pvst = PV-Skip per-WAVE (kPerWave; preserved R25 A1 binary name) +// _pvsb = PV-Skip per-BLOCK (kPerBlock; R30 new) +using fmha_pipeline_{F_idx}_pvsf = ck_tile::BlockFmhaPipelineQRKSVSAsyncSparge< + fmha_pipeline_problem_{F_idx}, + ck_tile::BlockFmhaPipelineQRKSVSAsyncDefaultPolicy, + ck_tile::PVSkipMode::kNone>; using fmha_pipeline_{F_idx}_pvst = ck_tile::BlockFmhaPipelineQRKSVSAsyncSparge< fmha_pipeline_problem_{F_idx}, ck_tile::BlockFmhaPipelineQRKSVSAsyncDefaultPolicy, - true>; -using fmha_pipeline_{F_idx}_pvsf = ck_tile::BlockFmhaPipelineQRKSVSAsyncSparge< + ck_tile::PVSkipMode::kPerWave>; +using fmha_pipeline_{F_idx}_pvsb = ck_tile::BlockFmhaPipelineQRKSVSAsyncSparge< fmha_pipeline_problem_{F_idx}, ck_tile::BlockFmhaPipelineQRKSVSAsyncDefaultPolicy, - false>; + ck_tile::PVSkipMode::kPerBlock>; using fmha_epilogue_{F_idx} = ck_tile::Default2DEpilogue::OaccDataType, typename FmhaSparseFwdTypeConfig<{F_dtype}>::ODataType, {F_spad}, {F_dvpad}>>; -using fmha_kernel_{F_idx}_pvst = - ck_tile::FmhaFwdSpargeKernel; using fmha_kernel_{F_idx}_pvsf = - ck_tile::FmhaFwdSpargeKernel; + ck_tile::FmhaFwdSpargeKernel; +using fmha_kernel_{F_idx}_pvst = + ck_tile::FmhaFwdSpargeKernel; +using fmha_kernel_{F_idx}_pvsb = + ck_tile::FmhaFwdSpargeKernel; using trait_{F_idx} = fmha_sparge_fwd_traits_<{F_hdim}, {F_dtype}, {F_bm0}, {F_bn0}, {F_bk0}, {F_bn1}, {F_bk1}, {F_bk0max}, {F_vlayout}, {F_pipeline_enum}, false/*logits*/, fmha_mask_{F_idx}, {F_spad}, {F_skpad}, {F_dpad}, {F_dvpad}, {F_trload}>; #include +// R30: 3 specializations per traits combo — int kPVMode values: +// 0 = kNone (pvsf binary) +// 1 = kPerWave (pvst binary; R25 A1 path) +// 2 = kPerBlock (pvsb binary; R30 new) +template<> +float fmha_sparge_fwd_(const ck_tile::stream_config& s, fmha_sparge_fwd_args a) +{{ + using k_ = fmha_kernel_{F_idx}_pvsf; + if(s.log_level_ > 0) + std::cout << ", " << "{F_kernel_name}_pvsf" << std::flush; + auto [kargs, grids] = fmha_fwd_create_kargs_and_grids(a); + const dim3 blocks = k_::BlockSize(); + constexpr ck_tile::index_t kBlockPerCu = k_::kBlockPerCu; + return ck_tile::launch_kernel(s, ck_tile::make_kernel(k_{{}}, grids, blocks, 0, kargs)); +}} + template<> -float fmha_sparge_fwd_(const ck_tile::stream_config& s, fmha_sparge_fwd_args a) +float fmha_sparge_fwd_(const ck_tile::stream_config& s, fmha_sparge_fwd_args a) {{ using k_ = fmha_kernel_{F_idx}_pvst; if(s.log_level_ > 0) @@ -155,11 +186,11 @@ def update_file(file_path, content): }} template<> -float fmha_sparge_fwd_(const ck_tile::stream_config& s, fmha_sparge_fwd_args a) +float fmha_sparge_fwd_(const ck_tile::stream_config& s, fmha_sparge_fwd_args a) {{ - using k_ = fmha_kernel_{F_idx}_pvsf; + using k_ = fmha_kernel_{F_idx}_pvsb; if(s.log_level_ > 0) - std::cout << ", " << "{F_kernel_name}_pvsf" << std::flush; + std::cout << ", " << "{F_kernel_name}_pvsb" << std::flush; auto [kargs, grids] = fmha_fwd_create_kargs_and_grids(a); const dim3 blocks = k_::BlockSize(); constexpr ck_tile::index_t kBlockPerCu = k_::kBlockPerCu; @@ -167,7 +198,18 @@ def update_file(file_path, content): }} template<> -void fmha_sparge_fwd_oneshot_(const ck_tile::stream_config& s, fmha_sparge_fwd_args a) +void fmha_sparge_fwd_oneshot_(const ck_tile::stream_config& s, fmha_sparge_fwd_args a) +{{ + using k_ = fmha_kernel_{F_idx}_pvsf; + auto [kargs, grids] = fmha_fwd_create_kargs_and_grids(a); + const dim3 blocks = k_::BlockSize(); + constexpr ck_tile::index_t kBlockPerCu = k_::kBlockPerCu; + ck_tile::make_kernel(k_{{}}, grids, blocks, 0, kargs)( + ck_tile::stream_config{{s.stream_id_}}); +}} + +template<> +void fmha_sparge_fwd_oneshot_(const ck_tile::stream_config& s, fmha_sparge_fwd_args a) {{ using k_ = fmha_kernel_{F_idx}_pvst; auto [kargs, grids] = fmha_fwd_create_kargs_and_grids(a); @@ -178,9 +220,9 @@ def update_file(file_path, content): }} template<> -void fmha_sparge_fwd_oneshot_(const ck_tile::stream_config& s, fmha_sparge_fwd_args a) +void fmha_sparge_fwd_oneshot_(const ck_tile::stream_config& s, fmha_sparge_fwd_args a) {{ - using k_ = fmha_kernel_{F_idx}_pvsf; + using k_ = fmha_kernel_{F_idx}_pvsb; auto [kargs, grids] = fmha_fwd_create_kargs_and_grids(a); const dim3 blocks = k_::BlockSize(); constexpr ck_tile::index_t kBlockPerCu = k_::kBlockPerCu; @@ -261,10 +303,13 @@ def update_file(file_path, content): FMHA_FWD_API_INNER_DISPATCH = """ {F_if}((t.is_v_rowmajor == {F_vlayout}) && ({F_mask_check}) && ({F_scheck}) && ({F_seqtune}) && ({F_skcheck}) && ({F_dcheck}) && ({F_dvcheck}) && ({F_constraint})) {{ using trait_ = fmha_sparge_fwd_traits_<{F_hdim}, {F_dtype}, {F_bm0}, {F_bn0}, {F_bk0}, {F_bn1}, {F_bk1}, {F_bk0max}, {F_vlayout}, {F_pipeline_enum}, false/*logits*/, {F_mask}, {F_spad}, {F_skpad}, {F_dpad}, {F_dvpad}, {F_trload}>; - if(a.pv_skip_compile) - return fmha_sparge_fwd_(s, a); - else - return fmha_sparge_fwd_(s, a); + // R30: pv_mode_compile selects 0=kNone / 1=kPerWave / 2=kPerBlock. + switch(a.pv_mode_compile) {{ + case 0: return fmha_sparge_fwd_(s, a); + case 1: return fmha_sparge_fwd_(s, a); + case 2: return fmha_sparge_fwd_(s, a); + default: return fmha_sparge_fwd_(s, a); // legacy default = per-wave + }} }} """ @@ -302,11 +347,13 @@ def update_file(file_path, content): FMHA_FWD_ONESHOT_API_INNER_DISPATCH = """ {F_if}((t.is_v_rowmajor == {F_vlayout}) && ({F_mask_check}) && ({F_scheck}) && ({F_seqtune}) && ({F_skcheck}) && ({F_dcheck}) && ({F_dvcheck}) && ({F_constraint})) {{ using trait_ = fmha_sparge_fwd_traits_<{F_hdim}, {F_dtype}, {F_bm0}, {F_bn0}, {F_bk0}, {F_bn1}, {F_bk1}, {F_bk0max}, {F_vlayout}, {F_pipeline_enum}, false/*logits*/, {F_mask}, {F_spad}, {F_skpad}, {F_dpad}, {F_dvpad}, {F_trload}>; - if(a.pv_skip_compile) - fmha_sparge_fwd_oneshot_(s, a); - else - fmha_sparge_fwd_oneshot_(s, a); - return; + // R30: pv_mode_compile selects 0=kNone / 1=kPerWave / 2=kPerBlock. + switch(a.pv_mode_compile) {{ + case 0: fmha_sparge_fwd_oneshot_(s, a); return; + case 1: fmha_sparge_fwd_oneshot_(s, a); return; + case 2: fmha_sparge_fwd_oneshot_(s, a); return; + default: fmha_sparge_fwd_oneshot_(s, a); return; + }} }} """ diff --git a/example/ck_tile/50_sparse_attn/docs/pv_skip_mode_comparison.png b/example/ck_tile/50_sparse_attn/docs/pv_skip_mode_comparison.png new file mode 100644 index 0000000000000000000000000000000000000000..b35c20a679ad9dd16548170b39c8836fc8caefea GIT binary patch literal 113868 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�KEnX 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 N@-j-&? @@ -414,9 +433,17 @@ auto fmha_fwd_create_kargs_and_grids(fmha_sparge_fwd_args args) args.batch_stride_o, args.window_size_left, args.window_size_right, - args.mask_type); - - dim3 grids = FmhaKernel::GridSize(args.batch, args.nhead_q, args.max_seqlen_q, args.hdim_v); + args.mask_type, + // R26 split-launch extras + args.pv_threshold_per_head_ptr, + args.head_remap_ptr, + args.nhead_in_launch); + + // R26 split-launch: when head_remap is active, gridDim.y shrinks to bucket size. + const ck_tile::index_t grid_nhead = (args.head_remap_ptr != nullptr && args.nhead_in_launch > 0) + ? args.nhead_in_launch + : args.nhead_q; + dim3 grids = FmhaKernel::GridSize(args.batch, grid_nhead, args.max_seqlen_q, args.hdim_v); return ck_tile::make_tuple(kargs, grids); } @@ -459,14 +486,15 @@ using fmha_sparge_fwd_traits = fmha_jenga_fwd_traits; float fmha_sparge_fwd(fmha_sparge_fwd_traits, fmha_sparge_fwd_args, const ck_tile::stream_config&); -// R25 V0: kEnablePVSkip is now a template non-type param so the codegen can -// emit both true / false instantiations from the same source tree. The host -// dispatch (fmha_sparge_fwd_api.cpp) selects the right specialization based -// on fmha_sparge_fwd_args::pv_skip_compile at runtime. -template +// R25 V0 / R30: PV-skip mode is a template non-type param so codegen can emit +// all 3 instantiations from the same source tree. The host dispatch +// (fmha_sparge_fwd_api.cpp) selects the right specialization based on +// fmha_sparge_fwd_args::pv_mode_compile at runtime. +// 0 = kNone, 1 = kPerWave, 2 = kPerBlock (matches ck_tile::PVSkipMode). +template float fmha_sparge_fwd_(const ck_tile::stream_config&, fmha_sparge_fwd_args); -template +template void fmha_sparge_fwd_oneshot_(const ck_tile::stream_config&, fmha_sparge_fwd_args); void fmha_sparge_fwd_oneshot(fmha_sparge_fwd_traits, diff --git a/example/ck_tile/50_sparse_attn/sparge_blockmap_inst.cpp b/example/ck_tile/50_sparse_attn/sparge_blockmap_inst.cpp index 06be6215bc6..10a58ae05f6 100644 --- a/example/ck_tile/50_sparse_attn/sparge_blockmap_inst.cpp +++ b/example/ck_tile/50_sparse_attn/sparge_blockmap_inst.cpp @@ -4,11 +4,14 @@ #include "sparge_blockmap_trek.hpp" #include "ck_tile/ops/fmha/block/variants.hpp" +#include "ck_tile/host/device_memory.hpp" #include +#include #include #include #include +#include // ============================================================================ // Type configuration for block map kernel (reuses FmhaSparseFwdTypeConfig) @@ -265,6 +268,21 @@ float sparge_vsa_fwd_combined(sparge_blockmap_traits bmap_t, [=](const ck_tile::stream_config& s_) { fmha_vsa_fwd_oneshot(attn_t, attn_a, s_); }); } +// R26 split-launch: partition heads into two buckets by per-head pv_threshold +// (sentinel >= 1e29f vs finite), materialise device-side remap LUTs, then issue +// one fmha launch per non-empty bucket. Bucket selection happens entirely on +// the host; the kernel just reads head_remap_ptr[blockIdx.y] to recover the +// original head index. +// +// R30: the "finite" bucket binary is selected by attn_a.pv_mode_compile (1 = +// per-wave (R25 A1 default); 2 = per-block (R30)). The "sentinel" bucket is +// always kNone (mode 0) — sentinel heads requested PV-skip OFF so the per-head +// per-mode choice is degenerate. Per-head per-mode bucket (3+ buckets) is a +// future R31 extension; this commit keeps the 2-bucket scheme and routes the +// active mode through attn_true.pv_mode_compile. +// +// Backward compat: if pv_threshold_per_head_ptr is null, fall back to the +// original single-launch path using attn_a.pv_threshold scalar. float sparge_sparge_fwd_combined(sparge_blockmap_traits bmap_t, sparge_blockmap_args bmap_a, fmha_sparge_fwd_traits attn_t, @@ -277,11 +295,115 @@ float sparge_sparge_fwd_combined(sparge_blockmap_traits bmap_t, << ", fmha_sparge_fwd_" << attn_t.data_type << "_d" << attn_t.hdim_q << std::flush; - return ck_tile::launch_kernel( - s, - [=](const ck_tile::stream_config& s_) { sparge_kstats_fwd_oneshot(bmap_t, bmap_a, s_); }, - [=](const ck_tile::stream_config& s_) { - sparge_blockmap_only_fwd_oneshot(bmap_t, bmap_a, s_); - }, - [=](const ck_tile::stream_config& s_) { fmha_sparge_fwd_oneshot(attn_t, attn_a, s_); }); + // Decide bucket plan. Pull per-head thresholds from device buffer when set, + // else broadcast the scalar across all heads to a single bucket. + const int nhead_q = attn_a.nhead_q; + std::vector false_heads; // pv_threshold >= 1e29f -> kNone binary (mode 0) + std::vector true_heads; // finite -> kPerWave or kPerBlock binary + false_heads.reserve(nhead_q); + true_heads.reserve(nhead_q); + + if(attn_a.pv_threshold_per_head_ptr != nullptr) + { + std::vector pv_host(nhead_q); + auto err = hipMemcpy(pv_host.data(), + attn_a.pv_threshold_per_head_ptr, + static_cast(nhead_q) * sizeof(float), + hipMemcpyDeviceToHost); + if(err != hipSuccess) + { + std::cerr << "sparge_sparge_fwd_combined: hipMemcpy pv_threshold_per_head failed: " + << hipGetErrorString(err) << std::endl; + return -1.f; + } + for(int h = 0; h < nhead_q; ++h) + { + if(pv_host[h] >= 1e29f) + false_heads.push_back(h); + else + true_heads.push_back(h); + } + } + else + { + // Scalar mode: identity remap, single binary picked by pv_mode_compile + // (R30) or the legacy pv_skip_compile bool (R25 A1). When the scalar + // pv_threshold is the sentinel, force the kNone binary regardless of + // mode_compile — the mode is then irrelevant because no skip happens. + if(attn_a.pv_threshold >= 1e29f) + for(int h = 0; h < nhead_q; ++h) + false_heads.push_back(h); + else + for(int h = 0; h < nhead_q; ++h) + true_heads.push_back(h); + } + + // R26-R3 gate: skip empty buckets so we never schedule a zero-grid launch. + const bool need_false = !false_heads.empty(); + const bool need_true = !true_heads.empty(); + + // Materialise per-bucket head-remap device buffers (one int32 each, freed at + // end of this function -- before that we keep them alive across the launch). + ck_tile::DeviceMem false_remap_dev(std::max(1, false_heads.size() * sizeof(int32_t))); + ck_tile::DeviceMem true_remap_dev(std::max(1, true_heads.size() * sizeof(int32_t))); + if(need_false) + false_remap_dev.ToDevice(false_heads.data()); + if(need_true) + true_remap_dev.ToDevice(true_heads.data()); + + // Build per-bucket attn args. Scalar pv_threshold field is left as-is so the + // device fallback (when pv_threshold_per_head is null and remap is null) + // remains correct; per-head buffer takes priority when remap is active. + fmha_sparge_fwd_args attn_false = attn_a; + fmha_sparge_fwd_args attn_true = attn_a; + // R30: derive the effective per-bucket mode. The "true" (finite-threshold) + // bucket inherits attn_a.pv_mode_compile so the CLI --pv_mode picks per-wave + // (1) or per-block (2). The "false" (sentinel) bucket is always mode 0 + // (kNone). If a caller still sets only the legacy pv_skip_compile bool + // (R25-A1-era) and leaves pv_mode_compile at its default 1, the behaviour + // is unchanged. + if(need_false) + { + attn_false.head_remap_ptr = static_cast(false_remap_dev.GetDeviceBuffer()); + attn_false.nhead_in_launch = static_cast(false_heads.size()); + attn_false.pv_skip_compile = false; // legacy bool — kept consistent + attn_false.pv_mode_compile = 0; // route to kNone binary (R30) + } + if(need_true) + { + attn_true.head_remap_ptr = static_cast(true_remap_dev.GetDeviceBuffer()); + attn_true.nhead_in_launch = static_cast(true_heads.size()); + attn_true.pv_skip_compile = true; // legacy bool — kept consistent + // R30: pv_mode_compile carries through unchanged from attn_a (CLI choice). + // attn_true is a copy of attn_a, so attn_true.pv_mode_compile already + // holds the user's selection (0 = kNone, 1 = per-wave, 2 = per-block). + // We deliberately do NOT override mode 0 here: if the user passes + // --pv_mode=none together with a finite pv_threshold, that is an + // explicit "build the bucket but don't skip" request (useful as a + // control measurement). Routing it to kNone keeps the CLI honest. + } + + // Chain callables: kstats -> blockmap -> [fmha_false?] -> [fmha_true?]. + // Empty buckets are skipped by emitting an empty lambda; the wrapped path + // never issues a kernel launch in that branch. + auto cb_kstats = [=](const ck_tile::stream_config& s_) { + sparge_kstats_fwd_oneshot(bmap_t, bmap_a, s_); + }; + auto cb_bmap = [=](const ck_tile::stream_config& s_) { + sparge_blockmap_only_fwd_oneshot(bmap_t, bmap_a, s_); + }; + auto cb_fmha_false = [=](const ck_tile::stream_config& s_) { + if(need_false) + fmha_sparge_fwd_oneshot(attn_t, attn_false, s_); + }; + auto cb_fmha_true = [=](const ck_tile::stream_config& s_) { + if(need_true) + fmha_sparge_fwd_oneshot(attn_t, attn_true, s_); + }; + + // launch_kernel returns elapsed ms for the whole chain when timing is on. + // We always pass 4 callables and gate execution inside the lambda; this + // keeps the timing contract stable, while a no-op lambda has negligible + // (~ns) cost compared to the saved 5-15us host launch. + return ck_tile::launch_kernel(s, cb_kstats, cb_bmap, cb_fmha_false, cb_fmha_true); } diff --git a/example/ck_tile/50_sparse_attn/test_sparge.cpp b/example/ck_tile/50_sparse_attn/test_sparge.cpp index ae0952cc419..8ba1b97e846 100644 --- a/example/ck_tile/50_sparse_attn/test_sparge.cpp +++ b/example/ck_tile/50_sparse_attn/test_sparge.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -110,11 +111,22 @@ auto create_args(int argc, char* argv[]) .insert("pv_threshold", "1e30", "SpargeAttn PV-skip per-Q-tile threshold; default +1e30 disables skip") + .insert("pv_threshold_per_head", + "", + "R26 split-launch: comma-separated per-head pv_threshold list " + "(length must == h). Empty = scalar mode using -pv_threshold.") .insert("pv_skip_compile", "1", "R25 V0: 1=use kEnablePVSkip=true template instance (existing path); 0=use " "kEnablePVSkip=false instance (PV-skip AST removed at compile time, equivalent to " - "VSA baseline)"); + "VSA baseline). Deprecated by -pv_mode; kept for back-compat scripts.") + .insert("pv_mode", + "warp", + "R30: PV-skip mode select. one of {none, warp, block}. " + "none = no skip (kNone binary; matches VSA baseline). " + "warp = per-wavefront butterfly vote (R25 A1; default). " + "block = per-block AND vote via 1 LDS slot + block_sync_lds (R30). " + "Overrides -pv_skip_compile when set explicitly."); bool result = arg_parser.parse(argc, argv); return std::make_tuple(result, arg_parser); @@ -147,6 +159,31 @@ bool run_test(const ck_tile::ArgParser& arg_parser) std::string dump_o_path = arg_parser.get_str("dump_o"); float pv_threshold = arg_parser.get_float("pv_threshold"); int pv_skip_compile = arg_parser.get_int("pv_skip_compile"); + std::string pv_per_head_s = arg_parser.get_str("pv_threshold_per_head"); + std::string pv_mode_str = arg_parser.get_str("pv_mode"); + + // R30: --pv_mode maps to the int dispatched at host. + // none -> 0 (kNone), warp -> 1 (kPerWave), block -> 2 (kPerBlock). + // Back-compat: if the user explicitly passed -pv_skip_compile=0 but left + // -pv_mode at default ("warp"), honour the legacy intent (mode=0). The CLI + // doesn't expose "was this passed explicitly", so we mirror the rule used + // pre-R30: bool 0 => kNone, bool 1 => kPerWave. + int pv_mode_compile; + if(pv_mode_str == "none") + pv_mode_compile = 0; + else if(pv_mode_str == "warp") + pv_mode_compile = 1; + else if(pv_mode_str == "block") + pv_mode_compile = 2; + else + { + std::cerr << "Unknown -pv_mode value: '" << pv_mode_str + << "' (expected one of: none, warp, block)" << std::endl; + return false; + } + // Legacy bool wins iff user explicitly disabled and pv_mode stayed warp. + if(pv_skip_compile == 0 && pv_mode_str == "warp") + pv_mode_compile = 0; if(nhead_k < 0) nhead_k = nhead; @@ -271,6 +308,31 @@ bool run_test(const ck_tile::ArgParser& arg_parser) static_cast(cdf_per_head_dev.GetDeviceBuffer()); } + // R26 split-launch: optional per-head pv_threshold buffer. Parse the CLI + // comma list (length must match nhead); empty list -> scalar broadcast + // (legacy path, single launch via host). + ck_tile::DeviceMem pv_per_head_dev(static_cast(nhead) * sizeof(float)); + std::vector pv_per_head_host; + bool use_pv_per_head = false; + if(!pv_per_head_s.empty()) + { + std::stringstream ss(pv_per_head_s); + std::string item; + while(std::getline(ss, item, ',')) + { + if(!item.empty()) + pv_per_head_host.push_back(std::stof(item)); + } + if(static_cast(pv_per_head_host.size()) != nhead) + { + std::cerr << "\n[pv_threshold_per_head] length " << pv_per_head_host.size() + << " != h=" << nhead << std::endl; + return false; + } + pv_per_head_dev.ToDevice(pv_per_head_host.data()); + use_pv_per_head = true; + } + // ---- build attention args ---- ck_tile::stream_config stream_cfg; stream_cfg.stream_id_ = nullptr; @@ -354,21 +416,28 @@ bool run_test(const ck_tile::ArgParser& arg_parser) attn_args.scale_s = scale_s; attn_args.pv_threshold = pv_threshold; attn_args.pv_skip_compile = (pv_skip_compile != 0); - attn_args.stride_q = q_strides[i_perm ? 2 : 1]; - attn_args.stride_k = k_strides[i_perm ? 2 : 1]; - attn_args.stride_v = v_strides[i_perm ? 2 : 1]; - attn_args.stride_o = o_strides[o_perm ? 2 : 1]; - attn_args.nhead_stride_q = q_strides[i_perm ? 1 : 2]; - attn_args.nhead_stride_k = k_strides[i_perm ? 1 : 2]; - attn_args.nhead_stride_v = v_strides[i_perm ? 1 : 2]; - attn_args.nhead_stride_o = o_strides[o_perm ? 1 : 2]; - attn_args.batch_stride_q = q_strides[0]; - attn_args.batch_stride_k = k_strides[0]; - attn_args.batch_stride_v = v_strides[0]; - attn_args.batch_stride_o = o_strides[0]; - attn_args.window_size_left = -1; - attn_args.window_size_right = -1; - attn_args.mask_type = 0; + attn_args.pv_mode_compile = pv_mode_compile; // R30: 0=none,1=warp,2=block + // R26 split-launch: when CLI provided per-head list, hand the device + // buffer to the combined wrapper; host code there will partition heads + // into 2 buckets and issue per-bucket launches. + attn_args.pv_threshold_per_head_ptr = + use_pv_per_head ? static_cast(pv_per_head_dev.GetDeviceBuffer()) + : nullptr; + attn_args.stride_q = q_strides[i_perm ? 2 : 1]; + attn_args.stride_k = k_strides[i_perm ? 2 : 1]; + attn_args.stride_v = v_strides[i_perm ? 2 : 1]; + attn_args.stride_o = o_strides[o_perm ? 2 : 1]; + attn_args.nhead_stride_q = q_strides[i_perm ? 1 : 2]; + attn_args.nhead_stride_k = k_strides[i_perm ? 1 : 2]; + attn_args.nhead_stride_v = v_strides[i_perm ? 1 : 2]; + attn_args.nhead_stride_o = o_strides[o_perm ? 1 : 2]; + attn_args.batch_stride_q = q_strides[0]; + attn_args.batch_stride_k = k_strides[0]; + attn_args.batch_stride_v = v_strides[0]; + attn_args.batch_stride_o = o_strides[0]; + attn_args.window_size_left = -1; + attn_args.window_size_right = -1; + attn_args.mask_type = 0; avg_ms = sparge_sparge_fwd_combined(bmap_traits, bmap_args, attn_traits, attn_args, stream_cfg); diff --git a/include/ck_tile/ops/sparse_attn/kernel/fmha_fwd_sparge_kernel.hpp b/include/ck_tile/ops/sparse_attn/kernel/fmha_fwd_sparge_kernel.hpp index d600ff70754..cbca128ca6f 100644 --- a/include/ck_tile/ops/sparse_attn/kernel/fmha_fwd_sparge_kernel.hpp +++ b/include/ck_tile/ops/sparse_attn/kernel/fmha_fwd_sparge_kernel.hpp @@ -7,6 +7,9 @@ #include "ck_tile/ops/common.hpp" #include "ck_tile/ops/fmha/block/block_attention_bias_enum.hpp" #include "ck_tile/ops/fmha/block/variants.hpp" +// PVSkipMode enum lives in the sparge pipeline header; pull it in so the +// kernel template arg can name it (R30: promote bool kEnablePVSkip_ to 3-way enum). +#include "ck_tile/ops/sparse_attn/pipeline/block_fmha_pipeline_qr_ks_vs_async_sparge.hpp" #include #include @@ -21,7 +24,9 @@ namespace ck_tile { -template +template struct FmhaFwdSpargeKernel { using FmhaPipeline = ck_tile::remove_cvref_t; @@ -30,7 +35,9 @@ struct FmhaFwdSpargeKernel static constexpr ck_tile::index_t kBlockPerCu = FmhaPipeline::kBlockPerCu; static_assert(kBlockPerCu > 0); static constexpr ck_tile::index_t kBlockPerCuInput = FmhaPipeline::Problem::kBlockPerCu; - static constexpr bool kEnablePVSkip = kEnablePVSkip_; + static constexpr PVSkipMode kPVSkipMode = kPVSkipMode_; + // Legacy alias preserved: any non-kNone mode is "PV-skip enabled". + static constexpr bool kEnablePVSkip = (kPVSkipMode_ != PVSkipMode::kNone); using QDataType = ck_tile::remove_cvref_t; using KDataType = ck_tile::remove_cvref_t; @@ -99,6 +106,15 @@ struct FmhaFwdSpargeKernel ck_tile::index_t nhead_ratio_qk; float scale_s; float pv_threshold; + // R26 split-launch: when non-null, indexed by remapped i_nhead (post head_remap), + // overrides scalar pv_threshold. Buffer length = num_head_q. + const float* pv_threshold_per_head; + // R26 split-launch: when non-null, i_nhead = head_remap_ptr[blockIdx.y]. + // Buffer length = nhead_in_launch. Null = identity (blockIdx.y directly). + const int* head_remap_ptr; + // R26 split-launch: gridDim.y when head_remap_ptr is active (== bucket size). + // Kept for future host-side asserts / debug; kernel reads via blockIdx.y. + ck_tile::index_t nhead_in_launch; ck_tile::index_t stride_q; ck_tile::index_t stride_k; @@ -165,7 +181,12 @@ struct FmhaFwdSpargeKernel ck_tile::index_t batch_stride_o, ck_tile::index_t window_size_left, ck_tile::index_t window_size_right, - ck_tile::index_t mask_type) + ck_tile::index_t mask_type, + // R26 split-launch (default-null preserves + // backward compat = scalar mode). + const float* pv_threshold_per_head = nullptr, + const int* head_remap_ptr = nullptr, + ck_tile::index_t nhead_in_launch = 0) { Kargs kargs{{q_ptr, k_ptr, @@ -185,6 +206,9 @@ struct FmhaFwdSpargeKernel scale_s, #endif pv_threshold, + pv_threshold_per_head, + head_remap_ptr, + nhead_in_launch, stride_q, stride_k, stride_v, @@ -224,7 +248,18 @@ struct FmhaFwdSpargeKernel const index_t num_tile_n1 = ck_tile::integer_divide_ceil(kargs.hdim_v, FmhaPipeline::kN1); const index_t i_block = blockIdx.x; - const index_t i_nhead = blockIdx.y; + // R26 split-launch: if head_remap_ptr is set, translate the launch-local + // head index to the original num_head_q-space index. Null pointer -> + // identity (single-launch backward compat). The remap LUT load is uniform + // across the wavefront (same blockIdx.y for all lanes), but the compiler + // can't infer scalar-uniformity through a global ptr indirection, so we + // broadcast via readfirstlane. Without this, dependent offset/buffer- + // descriptor computations spill to VGPRs and buffer_load_dwordx4 inline + // asm rejects the VGPR operand. + const index_t i_nhead = + (kargs.head_remap_ptr != nullptr) + ? __builtin_amdgcn_readfirstlane(kargs.head_remap_ptr[blockIdx.y]) + : static_cast(blockIdx.y); const index_t i_batch = blockIdx.z; const auto f = [](index_t dividend, index_t divisor) { @@ -402,6 +437,23 @@ struct FmhaFwdSpargeKernel BlockIndices block_indices{i_batch, i_nhead, i_nhead / kargs.nhead_ratio_qk}; + // R26 split-launch: per-head pv_threshold override (null = scalar mode). + // i_nhead is already scalar-broadcast in GetTileIndex; the load is uniform + // and the resulting float lands in SGPRs naturally. We additionally route + // via readfirstlane on the int representation as a defensive hint to keep + // it scalar even when the compiler is conservative about float traffic. + float pv_threshold_resolved; + if(kargs.pv_threshold_per_head != nullptr) + { + const int raw = __builtin_amdgcn_readfirstlane( + __builtin_bit_cast(int, kargs.pv_threshold_per_head[i_nhead])); + pv_threshold_resolved = __builtin_bit_cast(float, raw); + } + else + { + pv_threshold_resolved = kargs.pv_threshold; + } + auto o_acc_tile = FmhaPipeline{}(q_dram_window, k_dram_window, v_dram_window, @@ -409,7 +461,7 @@ struct FmhaFwdSpargeKernel valid_block_num_value, mask, kargs.scale_s, - kargs.pv_threshold, + pv_threshold_resolved, variant, variant_params, block_indices, diff --git a/include/ck_tile/ops/sparse_attn/pipeline/block_fmha_pipeline_qr_ks_vs_async_sparge.hpp b/include/ck_tile/ops/sparse_attn/pipeline/block_fmha_pipeline_qr_ks_vs_async_sparge.hpp index 4bf3c9d296c..0a8baa4e623 100644 --- a/include/ck_tile/ops/sparse_attn/pipeline/block_fmha_pipeline_qr_ks_vs_async_sparge.hpp +++ b/include/ck_tile/ops/sparse_attn/pipeline/block_fmha_pipeline_qr_ks_vs_async_sparge.hpp @@ -11,18 +11,40 @@ namespace ck_tile { +// R30: PV-skip mode enum. R25 A1 shipped a per-wavefront vote; R30 adds a +// per-block consensus vote (matches upstream SpargeAttn kPerBlock semantics; +// see R29 researcher report per_block_vload_guard.md). kNone disables the +// skip path entirely (AST removed). The legacy bool `kEnablePVSkip_=true` +// maps to kPerWave; `false` maps to kNone — preserved via codegen. +enum class PVSkipMode : int +{ + kNone = 0, + kPerWave = 1, + kPerBlock = 2, +}; + // Sparge variant of qr/ks/vs/async pipeline. Cloned from BlockFmhaPipelineQRKSVSAsyncVSA; // adds PV-skip per Q-tile (SpargeAttn paper 4.4). Kept as a separate file so the original // _vsa.hpp can remain frozen as an A/B baseline. // +// R30: kPVSkipMode_ promoted from bool to 3-value enum {kNone, kPerWave, kPerBlock}. +// kPerWave is the R25 A1 shipped path; kPerBlock adds a block-wide consensus AND vote +// (1 LDS slot + 1 block_sync_lds) so all waves in a block agree before skipping the +// PV mma. Per R29 audit, the V load / V->LDS store / cp_async pipeline stay +// unconditional in BOTH per-wave and per-block modes (only the gemm_1 is gated). +// // QUANT-HOOK: future int8/sage variant will add QScaleEnum template arg + per-tile descale Kargs; // _sparge_sage.hpp will live alongside this file and reuse the PV-skip path verbatim. template + typename Policy_ = BlockFmhaPipelineQRKSVSAsyncDefaultPolicy, + PVSkipMode kPVSkipMode_ = PVSkipMode::kPerWave> struct BlockFmhaPipelineQRKSVSAsyncSparge { - static constexpr bool kEnablePVSkip = kEnablePVSkip_; + static constexpr PVSkipMode kPVSkipMode = kPVSkipMode_; + // Legacy alias: true iff any PV-skip mode (per-wave or per-block) is active. + // Kept so existing `if constexpr (kEnablePVSkip)` reads still compile. + static constexpr bool kEnablePVSkip = (kPVSkipMode_ != PVSkipMode::kNone); + static constexpr bool kPerBlockPVSkip = (kPVSkipMode_ == PVSkipMode::kPerBlock); using Problem = remove_cvref_t; using Policy = remove_cvref_t; @@ -140,7 +162,22 @@ struct BlockFmhaPipelineQRKSVSAsyncSparge static constexpr const char* name = "qr_async"; + // R30: per-block PV-skip needs one int32 LDS slot to broadcast the AND-vote + // result across waves. Reserved at the TAIL of the pipeline's LDS budget + // (after the existing K + V allocations), 4 bytes, aligned. When mode is + // kNone or kPerWave the byte is unused; the sentinel cost is negligible + // (4 bytes vs the multi-kB K/V tiles) so we always reserve it to keep the + // smem layout uniform across modes — simpler than per-mode policy plumbing. + static constexpr ck_tile::index_t kPerBlockVoteSlotBytes = 4; + CK_TILE_HOST_DEVICE static constexpr ck_tile::index_t GetSmemSize() + { + return Policy::template GetSmemSize() + kPerBlockVoteSlotBytes; + } + + // R30: byte offset of the per-block vote flag from `smem_ptr`. Lives just + // past the policy's K+V smem footprint. + CK_TILE_HOST_DEVICE static constexpr ck_tile::index_t GetPerBlockVoteSlotOffset() { return Policy::template GetSmemSize(); } @@ -513,6 +550,69 @@ struct BlockFmhaPipelineQRKSVSAsyncSparge }; const bool warp_skip = compute_warp_skip(); + // ================================================================ + // R30: per-block PV-skip — block-wide AND vote over warp_skip. + // Hand-rolled (no `block_and` primitive in CK-tile, no + // `__syncthreads_and` analog — see R30 idiom catalog §7.5). + // + // Protocol: + // 1. Lane 0 of each wave atomicAnd's its warp_skip int into a + // shared LDS sentinel (initialised to 1 by lane 0 of wave 0 + // before the vote). + // 2. block_sync_lds() — all stores visible, all waves rendezvous + // (uses the same s_waitcnt+s_barrier discipline as the K/V + // LDS chain; lgkmcnt accounting stays consistent — idiom + // §3.1 / §4.2). + // 3. All lanes read the sentinel back into a register. The + // result is wave-uniform (and effectively SGPR after + // readfirstlane) — used to gate gemm_1 at :607 / :665 below. + // + // Cost: 1 LDS init + 1 atomicAnd + 1 block_sync_lds + 1 LDS load. + // The vote slot lives at `smem_ptr + GetPerBlockVoteSlotOffset()`, + // 4 bytes past the policy K+V budget (see GetSmemSize override). + // No interaction with LdsSeq rotation slots. + // + // V load / V->LDS store / cp_async pipeline stay UNCONDITIONAL in + // both per-wave and per-block modes — matches upstream SpargeAttn + // (R29 audit) and CK-tile LDS-rotation discipline. + // ================================================================ + bool block_skip = false; + if constexpr(kPerBlockPVSkip) + { + // Carve a 4-byte uint32 slot at the LDS tail. The cast is safe: + // GetSmemSize() bumped the smem_ptr allocation by 4 bytes (see + // pipeline override above), so the slot is dedicated to this + // pipeline instance and never reused by K/V tiles. + auto* vote_slot = reinterpret_cast(static_cast(smem_ptr) + + GetPerBlockVoteSlotOffset()); + + const int lane_id = threadIdx.x % warpSize; + const int warp_id = threadIdx.x / warpSize; + + // Initialise the sentinel to 1 (skip-everything) before any + // wave votes. Only one thread does the init; the subsequent + // block_sync_lds() makes it visible to all waves. + if(warp_id == 0 && lane_id == 0) + { + *vote_slot = 1u; + } + block_sync_lds(); + + // Each wave contributes its warp_skip (already wave-uniform + // after the butterfly in compute_warp_skip). Lane 0 of each + // wave issues the atomicAnd; other lanes are idle. The atomic + // is on LDS (s_or_b32 / ds_and_b32), much cheaper than global. + if(lane_id == 0) + { + atomicAnd(vote_slot, warp_skip ? 1u : 0u); + } + block_sync_lds(); + + // Broadcast the consensus back to every lane. + const uint32_t consensus = *vote_slot; + block_skip = (consensus != 0u); + } + static const auto get_validated_m = [](SMPLComputeDataType raw_m) { if constexpr(FmhaMask::IsMasking) { @@ -530,6 +630,10 @@ struct BlockFmhaPipelineQRKSVSAsyncSparge // R25 redesign D: when kEnablePVSkip + warp_skip, we zero this // warp's owned rows of p_compute so the unconditional gemm_1 // contributes zero to o_acc, and skip the rowsum. + // R30: per-block mode uses block_skip (uniform across waves) and + // additionally skips gemm_1 itself (see guard at the gemm_1 site + // below). The p_compute zeroing remains so rowsum_p -> 0 and + // `l += rowsum_p` is a no-op for skipped iters. constexpr auto p_spans = decltype(p_compute)::get_distributed_spans(); sweep_tile_span(p_spans[number<0>{}], [&](auto idx0) { constexpr auto i_idx = make_tuple(idx0); @@ -538,7 +642,15 @@ struct BlockFmhaPipelineQRKSVSAsyncSparge #endif sweep_tile_span(p_spans[number<1>{}], [&](auto idx1) { constexpr auto i_j_idx = make_tuple(idx0, idx1); - if constexpr(kEnablePVSkip) + if constexpr(kPerBlockPVSkip) + { + if(block_skip) + { + p_compute(i_j_idx) = SMPLComputeDataType{0}; + return; + } + } + else if constexpr(kEnablePVSkip) { if(warp_skip) { @@ -603,15 +715,39 @@ struct BlockFmhaPipelineQRKSVSAsyncSparge number<-1>{}, bool_constant{}); // load next v_buf } + // block_sync_lds() stays UNCONDITIONAL — it is the + // workgroup barrier the V->LDS rotation chain requires + // (idiom catalog §3.1 / §4.1). Only the gemm_1 MFMA is + // gated on block_skip when in per-block mode. block_sync_lds(); - gemm_1( - o_acc, - get_slice_tile( - p, sequence<0, i_k1 * kK1>{}, sequence{}), - get_slice_tile( - v_lds_window, - sequence<(LdsSeq.at(number{})) * kN1, 0>{}, - sequence<(LdsSeq.at(number{}) + 1) * kN1, kK1>{})); + if constexpr(kPerBlockPVSkip) + { + if(!block_skip) + { + gemm_1( + o_acc, + get_slice_tile(p, + sequence<0, i_k1 * kK1>{}, + sequence{}), + get_slice_tile( + v_lds_window, + sequence<(LdsSeq.at(number{})) * kN1, 0>{}, + sequence<(LdsSeq.at(number{}) + 1) * kN1, + kK1>{})); + } + } + else + { + gemm_1(o_acc, + get_slice_tile(p, + sequence<0, i_k1 * kK1>{}, + sequence{}), + get_slice_tile( + v_lds_window, + sequence<(LdsSeq.at(number{})) * kN1, 0>{}, + sequence<(LdsSeq.at(number{}) + 1) * kN1, + kK1>{})); + } if constexpr(std::is_same_v) @@ -659,16 +795,37 @@ struct BlockFmhaPipelineQRKSVSAsyncSparge k_pre_np); move_tile_window(k_dram_window, {0, kK0}); } - // tail — gemm_1 runs unconditionally under redesign D. + // tail — gemm_1 runs unconditionally under redesign D (per-wave). + // R30: per-block mode gates the MFMA on block_skip; block_sync_lds + // still runs unconditionally (workgroup barrier for LDS rotation). { block_sync_lds(); - gemm_1( - o_acc, - get_slice_tile(p, sequence<0, (k1_loops - 1) * kK1>{}, sequence{}), - get_slice_tile( - v_lds_window, - sequence<(LdsSeq.at(number{})) * kN1, 0>{}, - sequence<(LdsSeq.at(number{}) + 1) * kN1, kK1>{})); + if constexpr(kPerBlockPVSkip) + { + if(!block_skip) + { + gemm_1( + o_acc, + get_slice_tile( + p, sequence<0, (k1_loops - 1) * kK1>{}, sequence{}), + get_slice_tile( + v_lds_window, + sequence<(LdsSeq.at(number{})) * kN1, 0>{}, + sequence<(LdsSeq.at(number{}) + 1) * kN1, + kK1>{})); + } + } + else + { + gemm_1(o_acc, + get_slice_tile( + p, sequence<0, (k1_loops - 1) * kK1>{}, sequence{}), + get_slice_tile( + v_lds_window, + sequence<(LdsSeq.at(number{})) * kN1, 0>{}, + sequence<(LdsSeq.at(number{}) + 1) * kN1, + kK1>{})); + } } } while(i_total_loops < num_total_loop); From 9e3f8838dee377af95ba30d86a42f7427196d974 Mon Sep 17 00:00:00 2001 From: Gino Lu Date: Tue, 19 May 2026 22:37:55 -0400 Subject: [PATCH 14/16] sparse_attn: drop stale FMHA-vs-sparge perf section from README --- example/ck_tile/50_sparse_attn/README.md | 14 +------------- .../50_sparse_attn/docs/kernel_breakdown.png | Bin 85047 -> 0 bytes .../docs/speedup_vs_sparsity.png | Bin 127494 -> 0 bytes 3 files changed, 1 insertion(+), 13 deletions(-) delete mode 100644 example/ck_tile/50_sparse_attn/docs/kernel_breakdown.png delete mode 100644 example/ck_tile/50_sparse_attn/docs/speedup_vs_sparsity.png diff --git a/example/ck_tile/50_sparse_attn/README.md b/example/ck_tile/50_sparse_attn/README.md index 9fdad906de6..0a7b513748b 100644 --- a/example/ck_tile/50_sparse_attn/README.md +++ b/example/ck_tile/50_sparse_attn/README.md @@ -29,19 +29,7 @@ Not yet ported (upstream pinned to commit [`ae5b629`](https://github.com/thu-ml/ *MI300X, b=2 h=16 s=8192 d=128 fp16, 5 seeds × 9 sparsity points. All three modes dispatch to the `kM0=64 padK=0` tile bucket at this shape.* -On the canonical recipe shape, `none > warp > block` at every measured sparsity, with no crossover. The per-block guard adds +33..+35 VGPR (6..9 spills) on this tile configuration, depressing occupancy. `warp` is +0..+4 VGPR. The default is `-pv_mode=warp` (preserves R25 A1 behaviour); switch to `none` for the no-skip baseline or `block` to exercise the upstream-aligned variant. A shape sweep is needed before recommending `block` as default — the `kM0=128` path has Δ ≈ 0 VGPR for per-block and is a candidate. - -## Performance - -At b=2 h=32 s=16384 fp16, sparge (vsa backend) reaches **1.78× FMHA throughput at topk=0.4** and **5.04× at topk=0.1**, and stays above 1.0× across the full topk range. - -![Speedup vs sparsity](docs/speedup_vs_sparsity.png) - -*Speedup vs FMHA, b=2 h=32 s=16384 d=128 fp16. Shape chosen to match Fig. 10 of the SpargeAttn paper ([arXiv:2502.18137](https://arxiv.org/abs/2502.18137); Mochi-1, 22K context, head_dim=128); s=16384 is the closest grid point. Gray-outlined points have >30% inter-rep spread.* - -![Kernel breakdown](docs/kernel_breakdown.png) - -*BlockMap (`_pre`) stacked on attention (`_attn`), b=2 h=32 d=128 fp16 topk=0.4. BlockMap is roughly 17% of total at s=16384.* +On the canonical recipe shape, `none > warp > block` at every measured sparsity, with no crossover. The per-block guard adds +33..+35 VGPR (6..9 spills) on this tile configuration, depressing occupancy. `warp` is +0..+4 VGPR. The default is `-pv_mode=warp`; switch to `none` for the no-skip baseline or `block` to exercise the upstream-aligned variant. A shape sweep is needed before recommending `block` as default — the `kM0=128` path has Δ ≈ 0 VGPR for per-block and is a candidate. ## Usage diff --git a/example/ck_tile/50_sparse_attn/docs/kernel_breakdown.png b/example/ck_tile/50_sparse_attn/docs/kernel_breakdown.png deleted file mode 100644 index 8704334155cbb0b5351293e9406d594de2ebfc82..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 85047 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

      !*laDTdS?US#enL#ct3Jd|Tf0u3 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 diff --git a/example/ck_tile/50_sparse_attn/docs/speedup_vs_sparsity.png b/example/ck_tile/50_sparse_attn/docs/speedup_vs_sparsity.png deleted file mode 100644 index 9a2f053b0b46fab1620939cda92d4fc3efc067b3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 127494 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%0DQf>!uT)X=EFrCat&W2-3 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%0DM8@)^74_XO<|N^3Sn#o#m2xhv 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

      #DDTYRI%il^81;bV2p!qUG zR`3o`y$0{{KCB(PF72P5fD=n<`7u|S{Js7kzC={2LH3k<%`ZS5t!iHmSy5B|-s01@ zVfH+5V`;tGVHN2Tp{|fn?WQK{ef5*JQMQqn{VeQJPE;NLSsS#N?`bBTWAe%vvV_1c zh%F6Z*FZdBP>}*8H&BPZEcbPk1wOp1AZ#j#7F<3|{O-&7MTFZJ=QsA>F%iOr+feD8 z?{%$E3UI~MC@iR{XE-fOpbeL!mH~?Yk-LNDo7?-(V4WPn$$#Q>ja>^%_^dXt`0|ECxLAe4>{7#Km2rX^I=jkY3zWGMI{iNdMSllE}YEnkzg=0=9 z0=0H(8*MqXiHf;rZzXBu1{-K}oyb%_eG<40e11r!SWsjO!ZNRE~RyvLV(=z zPg@S4XQbcOhj^Lvy7zhNh9&yne{dFAbEJ`g-@zATCT4lgJz6>Tn_x(-?=hW_oY%sr z`QL@OFyJF)GiJNqTdzQ@d%HX%;<4O51Oc2w@o4hYYf7xzIUS3tx+B#=X$qgq)n@XR zCchQiZ(Qor`jVjRAKFpipy%CIB=~#T0;3-}N4idsVVerXO_UUWcL*?6ArbV%^#=q) zbu~5YIqq`%;x{^1Hxw2O8$0~E4pTmIsH7&viWUMhUZl9b95i6C{Vk7A4T#0WA zemXvq74<%#xrj2iuaS*SCVD)MB;WY0ZzZ=1WEzhJyMm-LK)-aJx9GG!J+n4Fv)S-D zBOg`oj^};G`vC9E`1evwIQ+{{@O8j|Pd|@y9G3=PJww}jpcQJ!g)$lDPdNSi04M^! zP;km zNmqpmL8r+0?$yPEE4aYSF3ougF@{`-ywdCV=ukGkydl&oJ2BKmUP6xol_ww^$LF;E zNyZnjeV8Cwb6VqZNAB$qcRBx~)zh7m4egZ>BwoK28~Q;I;~@~g7Uyhgl0aBy3M99n z;YYLLo?r6KwTz8(caAYu*tW0dv8?Br*Ij<*v>Y%+2nJjgx$dGmXmqU5qsxA}MY60B zKT0idl;-G4stq|nrzB@aZxDFFKmpgHV3g);F+Mn$W=g$=ky4a*OFeo>63u|dy(!(9TD;}1-101*(}?jRkLV1KqE zyYjT{@!|F(VHUeN23<*uSzC`wUt2+2KGr)+6#UN%(YKPc(w95TwBBQHf3Jq%W+Hm< zMX9=|(c)Uv)Qp*#2md*{mTB0fK9G+7$@p5bA+W{R8~^DyOriFR4kVs|LGf-XEk*Q! zIqj*i4)QFjeDRSa_Jjsn^zC7#&F1-zM{Yq3ZZJEc_d@Sx(0L7pNESQaFrABXcWavf zut!6lg?zyp9y4)PuAMTXwQWNoSGw+{iy33-qlj>H!_AyWJIqtWsz@zW76-aFxp+CH z`Fh&iPoM|Cf1lV$RrrD^*`3NjH#mJ}^llK#^i z|9S1VZ@}=jLPbl9`^y7>*PxNn(2xN=hYkb~Qi@uqiHY3!C~8- z)n9+rBwyclOqg%VW?-pYAF}H*xw^F{;(GdOkoFp0FvkJ>M=c6E z94$_qJoV#OB9P8tioWj8?O6*{Wti4WccweX_uI{!7HlRktfn~gyCt~w?LZnP&(SRI zkL*V~cHi?N$3uFv^Lqpx~SO6#s+(3Zw0C1J~G;H>y zeg7QnH#_JwvnPtUe`ttgj}5y&h<);K1C@gM)Ro;w`GSG-eD#$9UEFS>JV;u z-+a?4-mPRq^eR*ZHGa6Nw9Z#Q&Xy@{QcEd8%c>UJ7Ga$HT3Sd6GAAJg{PM^j3ay4x z?%C5|1jC;pjok30&hyEGU=wDXEQQ@jxca)%Seg1M`S)9P{`wY2ltkqCRv9p}p+1K| z=8Uia2(7>+lMH09GXcMhE;6k*Z+?W5`^uP_n&#*Okn;X~R$WDfWfydaFM}tir3xCR zB5+f{%9z|tPE2$PKVB=7V6;6e7t4LOHIVdGuhB`P*!RH^G&shEyv|w4c!}1_k7sz zd)Ockh_-&XW(I7o0N;nZoOnJ(&Ez46co}Ng(8a+pR|dQ?{1tlJwI9mFqdVu*atL%% zq$O3J6L9Y}Dq)iFOKB+Na2%ceu>u~wFNI5XOLABAVwTyxWsN;u`1pltUKRgNt2S>q z47?qqdrGbYdtJJ1#DWrhMmR4qEml=B@`eWCo88 zW2|kL!PNI zD2YJ} zI2uBL{?jB|t%Q<}4&~^UEmffiOBso$#a_M(Lmv8FeT!Bn_mcCJgmh#n(f!EB&%D7B zb2teht|I4!6Tf>Mz+9Sh)$W@(em{#PrZ9+^6bL=)zf|%GGG1SQp3{R|Jrcu3$#qOe zu>)=GKV5v$%Hmbh9c zV{jvXdA*>pca0!p2t&t{r1pZxlJyp$IGGGdpX*Wr-EV4#Q?6~tWGaCzh+HEZ3orkv z-!nO6(eEvS@92dbK70p6I@=2_SN*mKcOUPcse!ggja$>YN*;f3#Zie-xsK|bTk)st zKIn$2Nh@Ai*L>L5gaX+juC83bs+J#!%z&`F+J03CoakiQ@A8?xL}RMmm!4#u)S}39SUG$$APv8EOsxz z$t^4_egYp}4lb^VrKQN;4U{wW5)G)wJqU0H?yP<3C}hIp{Ve8%e#Cw6#!E&gEx+ce zyOsHsx(hb6Fi%=mLewjE3OIn}*EUJs2rrGpr&*Iu-%XkJHd%g#Gx3l2V-$KcN zBFTAgLDd3aLqxVL=(4BY=V^RyG|XU|r%?)WYVgT8s07nduJ>)UW%P4MR>;D|8u`%{ z%TvPutqe#k##PHxgCc7tZOg{e6%$Bxjm=EJZ!x!H$$yaBJLK)>7W9}=oQa2|Ov5$p z-YSuq$)9+Cz#bnf>7hQ*WyA+M7b15h5L){h2*7|sP_Pd`_%CRq4oBsfqL`@`lcVz7 zeruIZ4}jP*9*3VWT6&HMpcvT(Zvh&m00Ux?4a4QJ`za|_jU)+y78K@g&`F+h4LWSn zNJvVO)YQcPX{ZW7H>dS(6re43-X1|O7#nq$!p;nk+xPrIW4$;xLXMTWf@~Kvxcj<{ zcxQK$AXCH9vHPpG=ukLb7a$VkQP+-dba;Hl-L6t=%Jx8n3GQgm*v=MD$rk?Rm`wpl zuW52nKL@DG+#;Edk^qTtLv>JI?IbhlOvxJ)8i}|}mTJuCuHV0hJ@MZ>cS>rh+M;rW>@ceR<5IkZ$mAlIo_|azZ!y_sTPjp&UcQg5;(B$lXxf=h=?SME)di%&FLgE*L~`sgMJs z2-*+0R)wB@U#XAG@$2SvR2akuQL}j;(ERSMVm?MfgxVn0du)tWPsg(?ehsB{0m%oE z_Br0W-HAd_#9)gjejHvD6HLgmch4%DwKE06_9-B@JM??}4D9}3Fi81=y4HXTR4h<^ zf%zY3b%1e<-%v%GgcCiqZ1-e3e+fWZaHzaYd-0XDMNHiibiIp?mEV+xt~bokS+Ear zvcg4XvqdQc$THr%@vSP+a#7oH8S=d;$x;7scbZ-Mus17eyEmoLHaDfKWUgHa^G0u} zA$0llhM2>5Uw4B~`j@?*{K3tK4YCLcHtem}W@M{g@nz(*#-#5~NXyi#EFHPPnL(=6 zGofMZ=;=9lBzB*3*LHh3ziiCSL(Y7xnhsm&e&4CC^EDdRcvYE{?|jOKb^YAKvaSx} zd#8SV&1YaPv30P97SBPg~(A&k0ut;AFp3~ezO@s!LM6(RFm|XTSEVzit$wMU5i>FFH zVE4dj$j3Z^^t$!*rVy4U3g zJ0U`ZJYv?S$MKBzqAT=C(OOndt`@DaX=jHsqY+UoO>MZFG{15&^GPbR<>)*>?!{6DxP&;u$g;ro%M6TwGL{@EJt5 z95`X5Yf-9R3)O6jX9F9jWACj|O!$&GQ>t$J?7mtt;MXD457@6fOcXN5rA!;TUjOVs z^7uK4q~!5H>4QCsXV|-JzKY%sQWi%*1mMn?YmUgrl~NjJ!IYrb!yfsoWB@ET-eP{MK;s*;vh%i z)kd#dUQ|S{;+)SmVx1+)l{>Z%lMA>|Tzk#A`uXZ}ZkOsh40)x^uvo{L@T4R+*{Y&C$KL zkZ8*A)@T6R?S1x*=D^d$opT<`8lK}DYNa46_S+w2ug=gGl?ye%!k`}qqPk0*3n@iT zF%OL>Dg-3q9PMOD-}2J}%7-i@KVd_^ zH!wO%P5QWFm>j;Y1sFjoCkaY^p-3_jXDd-Hfu*45q)(z0$0pk9ds`Zx2IT9^|1g_C zC+f)>zcXb(wztb+HeS-v_U1SI9fNTR?E4zT#F=3#J6Wdm><47&w^ZQDa!5|1So)Kr ze$9w)b9)iz&~^Ti%lO+9+pol0qdP*87RJkj=DaY!>2BpH<9DOfN?y=ONvlOhdvs$g zj_%{Q=;*gRSLwPHt<%j=h(!#ZrSTlyhgayjP-nm^$mqnRf32x3mKXw{AV9BaH8{Te zS~DWX`(kkt08xW2ekAvVnRG6k%t4L!wm*LSFlk8${$bluUcE8XRAo$la+O$|82fGD zkDM#_0oidA=-z?g#rt&0-pNSwXy8leB1VFV%***$l1&t14w&R|(zKWSx+e)GIA z43*dXbJ_32+24afCt*L))Pa$YV#NxsJ4r>vA#(?@Ms$ktP%clE`_$+gY~P`?Yam{E z7BX#>`5zDgx*FzKAwkb1ASWt+50+n2EZljUuhINrWj?`anyxRZ1a-xQH(4UN%fp2h zpXI-H#Nkoci@ih8vVHw!PCL=jae`Qatpwnap80C@chAMEJn^Gnh&8qdExaD_C)bdF z@#KqE%d&a~2osU!=So8`X z_`mPEx~h-AdFGS}{8g?87uw+K1mE6J0*VGg(mMJb9eZwP1n`aci=&K=X8-RcpzGc^#lWlu1B4dW!pXe*nSi1BYwMU7r6%7h-!M^B^m58UX z>=v@C`0VP%-ygD><4OS!)Z=4z){x;9DM-HaoO1!8nEVNFxZKEWatjfpRw5wi3%L0) zU)&(p9~o^NS~r$(^A1NMWKvs_MucFW%K9z$H1+V& ziaaVT9EWOFju{2~AE`%9A1>mNaMTRE+|__5#q8{~-uq=aT)@3@B^smcDlw7r2Ms7r ztZJ>O-X0@XuS?_SlGxe5JO7?6TDpNyo$pC<#v{;`(GkjJx@{Swo_R*CEBqGb?3x?> zYRnMU@9uCG&NP@#$`8c=4D#e6nTiGJbRu*LN%pWsEi{PO-m{4#-h zcH!Z(sHq#bvM*eo`SWOLznwAVEEX;NMm7t~?>0G9=k>!33NaVl8h*1FdT&I)bXysR z>XS16h!wN5`9p`m5w@uz1Aw%hogJ7Lbh1hq@kgTn=8ZgO4{--B65voUB3hU%ff#j9 z(k1!|7p`eW59-VWvo6B=2+D8xWXB%%1zF(ibHN>#G-RSk}ekD*qq$b`?+oB`9>0!pl?$Zi|)yB;= zxItRN|HK;Mgb3Nek6Xq<)ZZL&AA@Lw*?KX(zU{;;&qTrV&5H$T6ms_i`X$ZBU)5h6 z_|G1O^CU{GK9WBE<|d69hA~F`lYY{^8#EcV-;Dygd$j8*Co-SJozvUwlu^6hyI-GS zDdaNaU;cm~L&ekml$@+&`4hp<2%wbO(%H!p1UrKZm{l+9E4FI(k62@Nj^lQYsxvlZ zRTY|LX#L)Dq?A&HgdZq-cgC!xXwrujGX|Hs8@v9v-7dMz@w4zMQgkLO73Nb`1#x*1 zlU`uJclkYwvywr#^lH7k_D`957pY?`Vv`QqO>%>yuAF5;XGHm(+Uo4h}_QZqiMc(3u|75&Vkmslcxq0`lNg(^li zN*$NmYis9OxSz)!f5)7H=z3K4N=q3z#>)nmm4H__Z`yOfrf&PCsB7g}_2aQ|%&A}Y z137DY>X=%ysHw;E@#oKs6#krh%pcBUinR7gm!x_aHbXx%l>vK9Dx?2KM=_y|80-z| zq%+3<(veNy=dQ(r6TmQiaoTVD=JL@_J=fQrwY%_^Jv(x@!5Yb@&+$8e=-123&>NLH z6}jUcR)tZm*jvu9J88yDt|e(d=n$dSdGiyEKmiq~&pd*r^Wfm1c!`FZs_I9nHRI^K zC^oSgD<#Q~)tS6|b^u5v=C<2xxm?NDpT%h3w!0?*USU0Zd*{J>jzDlQVMbPo?!IK_ z7_H&stpiT2Gae`l9o*My^Z@3@&<*FV6x89q)BB?NMK($f{q3-wHtrE*+Kb-@jdE=W~?Hg^B72?fDtFNulY6EdmB@Po^@7)}Q7fep00ma{1k7nvO*M~!Al3o58h#QP2j*WrFtlI#|?wn7GBRYT9|C_6BM`Q z4+K!D7<6FbJYAOPaoZg+Zo+c?CZ};vGg9gmbixH@j2EtLl_nfS{lmkOxhh!s>Nxr8 z>@*%G9i4wGLW(@~Y@_RT*V9VGEePPOd7_VJLJo)v-=OP;Z~J|v;dFJO-Xbatc~heq z#lln}j5DypWfBr?P)IW!D;JvoLb)V$)Amc$t1vR1DF0d03>Gtf z8UFgk$6J}V@-tO^1q=ue1E$`9|L4Wb4BNSLRpeHU{R#%S| zK=Q|w9-YD+q@V{|5tv&6YxQ}XqL!A-SJKfAz`&l>enkL4DLoU(oe2{JMVB+ovnnJXe;CQF6&%kX+Z%cWb2 z+xx*28zyENewz%EU|Em%iCmPYblnW}Qp#VAEE1fO)V4-x!%>HI;I3xk?T^c-Ry+4k zC$L1CoYrWZQo*S%=0^$zJ zV96~m|%`7rf&Qb>Qk|O$u=fD7^*)%;zi7|6wToClGHGwAo0GQIg`4|zV zqUC}JM4E#Cl*8q+TTOX&{OzO9C#}ynnzY1>Z0EwQ*p-@echAk(>*}62R=UaV8le^`9!L?xf+g-xXbxy_97chKm$cSmYl$@%!H$TlT z_?DDIE3#IsZW5uaFuxFaLKO*`&^}huxnh^>3k)c3eD#^?7%- zT54yz`XYJl97Xxb;4xz2wukfsOo7U?BhNi?g!!c7GS zklNvVwxdc!MC8DiDR6%_2?Z;qD`wR}bkP$@Ns_X|P5VQV?Yapik|*?KXjW8OuVuEP z-$>#@dJz}Ei9Fv};}<*v{0eb!aGa^LXDXpnh5XuXX-hf<_UO6U-{<2-y9|TQSL}I+ zDLdz8a`VftQgWj3Qrv8)x9;;2R=+lnrh4ZPCb^>~oJ$=+nF~)4L{iA^*Jj}&EI`G zXVWq$BsKiil$>fU>R%g1Hmb=K07aL!3qq0t6Vub ziG)m2?Omq5yVJ0@jVDtTgJ`1Q9AN*bRksj)$NAE1^k=z<74frX*01Dn_#o(7@#dFvP);ad%+@ARCb0-x3i(}K zeKgA6S9V(-wYg{eRWuq|{XQ7$eL$C$XSGe*YOXKvKUEiqKL+$GSP>(mHQ+`5a##A_ zO^m{ACZXV|_s;S;r2zU!kQLl)9|J9VH*udgkRPax?iszK3TQM?zP}C2=QCUnSD3KV zUNfYqn2l_{X4XAAn;SP7-Fw}(|GHyvNDL3BwUj^+p8X{gl@Zk447fG6!R5tDwR{5ZvNf0 zcL^s|W9Di?+Cg$rzq#p@S@&eZ$MblDWv?xG=3HYjKMRpKB?;6sH=NxvTR(P2ATeCI z?b}NWK=L1`Tb;+xm#Wz8mp1s-fe#%(yA6PGLAB1koTH12N!xgw0`q^*Me&|Z7!pD8 zub*ab8z63`0`lxvQkS{C8!GENQ$LARd}r%DGCPDQEF%;t@sa#a4X)+r?FPqC9={L@ zrq`OZ?IeDd-n#@|T9F|g>Lbk6A+N1T-bgK95>p#%#6OnYXdRmu2g?;;44bOdHf?^y^(u1-JUBBm!|a?u>( z6j2Wrcr#K;S%<#_2>z0mFVAj+bHZz%>gR_D!Q4Ld3>^iwf+LRZBE@NTNk{e~Ey>6M>;OD}&}&z0`l-eUvKF?~zf&fvwUEeu+0Nfb=Dg@y0kbqp9wg#qOz;gu zPuQ|-zPxzi6r(R|Xb?j&-+v&_9}sPSKP4kQe?(55$VZh9|0F%pJzn@bYFB5?lP&j` z^avWpkI6oa$RM?I%h@qaX*B!|@ivA1;qh)t85N3G5Ej&U`2ggy&CJBrh~KEtCk=h( zjsawYczUf!G+o5ciJ17gI$qSH;7tdT_ zP2R02^Ilt^{>mL-5;KxH`ktSSh?*3hh`Bxv63|Aq6$Z+DsJEW5W$Pfhi=Efi>*~4F z%oQ#j9KCaOFD5flE9WBXjuqrHLJ=vnmb4YSXre0dob8>cu^^;Q9 z)g=a$rN6e}*Ku6&VKXJ*mzF6|O`cmZTOcr%A)t{+lr7tcX*)2&N?xewfn$9R2mJY= zmh`>nT?E(Jght)Hy~_W$Y6GVehe%<_oWf%=ra)RqB3Clc9r z)D+2JIr0p=tq-)K6maX4D##VsDJade*cVE#(`hl&Etu1JIMXR#rB6w4 zq&>GV+sYXk5YIh|t8Q25${H^#o|OhduU%>dV7(nBmK#fDACHv?ZmGYz;6#(irP;NM z3A^V_xb_ZVS%WwN%%xGG()t+Ov8@MGrOxxIF_}z%r#dxb>2k(0VJ0m9phoht4HU*~ z0b^3)YOQ|67!>U@VVz5#dX^&1DvzcnWfWX+oF_jY%_WIuDT|h_%xnBq&k4i0r(Usr zL(zO~3KOiB6DIp^*x}h`$Aa@?s!W z5f~rykYEI1fw&!T;-mq(3e?Y?hDLfMH(ot&wzyB}(@TB|t8QO#d4CC{7P=LKOz z4OiC2O zKZ8-me@w3J`cf0o!gQV<*W4{)(ZZnnPm)jy(0LN|3%5NtvTC24`1HL5!A+OjE+TLB zu>aSn|HOr-;PV>N{1F4TSxKYvQ@?#-`NeP$1wFTYN?YvBiGVPLssi7w3A(*xl$5gm&(s(k@Us04W6vpmAk#^T z4*RSpAhvHm(e;YGOkm+$_qug^XdS3BWj@cHKtBn0qCet$R91u$`l?tT zNYUft#Q?zvm>7Yr20UeHl(PzecP)rz7BK_?-@uDo8eH_;OAdjINeK$HP!A@W%jsx3 z4_VlyCPugF5L|o(2FKknnyh!T<@(-doTK?)NNz_B-Ou&=Nk|o5Mvig!L~mvibgfq8rj#3_LQNwE%pe@*NrMuSO8Gc|U)%Gr|9yyl0fQJ2s7FEvazM2fMb8ADRp3jvghp6DEzYEh1SZp#{Ly21 zOoFMGx`eYcJG4qW_Pl*80xd7N!<;%02+$hYmQJLPan{p;20eic7|8K|51q?^eNILH+Pfa*AoJZmycdn9E{(Ij zMc3j=WPLuKy*d91km*q4tdKo5C?&0!4xTOSj@?9U44LIbbhJ{LNn9+-gSb}8R=^Ck z=<4UZF6kEAd;2RVD|2hyeVJZk+*6}!{DHCL3cPsP;&^pc>ViDva!`WrqV`gWk7`QvGrz?DaT((}el#r>-8L{+D8G;aQXvRCV zgdt|5rdsQCn3un2=tdXIM^dEjHbYFuy%M%SyEOuLd??)u8KZsVyR4RCSY=&?m2!;c zW$I=@vt}~}g9y@!C(TR<1`zCQz)Zurs~XuSDPt)V$wj@FnS1k`^QQ;@$8RfT1cj^o za%HEi+tuH4;AtR&7`j@FU*-rkjvb4!v7Z!r4a_CJb3QGmY@;oVN`9->BJgmv;Vy7r zN(N|CT(Lik6DJg=2pok)+(j{ep zq;!dhA{~N+gbIR$EvSToA|g^sN|%TzigZZ{NFyzI$MXAq=brD}bDrni^Ur-Qe;BaY zd#!i9@0??fIp&-sNPLKK?3SSAcsz~*u*!32IGjcD5VQ?nubaoe=ZL$nqAYuN7Z z?q6$V@?bxlh^X%KilH{Mlv&jFw6_S$Mo#s*@@b7AGo$|KaKBEs+k$=w+ppY*z);f($8obgokw!LTWD!zTb>ubmI zH9Fa+Sy_iC7rydQ6eFBbEPyG3HYSLxbE2QAEw zZ*MR8 z7}^-XN#oofS5@7d+1%W0De{OId?c3B^}7)K8}v$t*l3iMSN6Lb@JIM-G<2Z3LDI4q z%dM*NUwu_%4`sYXG5G3=B{Y;btNOTPWMVKGPJXHS9&d&LKX4NSc}&)FmluSXLjxNc zPEAcuuN&*^?8Hb6l91o(vV1<2P_JH5`7bYDo>^RtszqHA*@urGcgf1iel^q84J_~B zZqwr&jlgdaa2vTJpXlY){Afdw4cVEs3=u6 zwKd}aD6Bdh#1PjVNgkdl+~Mr(eD?hLjcvw8Mws+;G_hVwQ z;V$7h%F4>*^Fgs@H?7%Q;*}>7M1IUZL|u}W)=Dg`Pufux6^a`-R=UdO3U@%-fA-v5 zAkXUi74T{RADB7`ZPyl_>Jv}ApB`;y`^~`!Hn%%oX`CP88a|8JJqPk+$1q*4wcYZ7%kn+IN?w#k!+&tyqW||I*z10Mk;1iNhrA zGgZxN0zLhe-_CoA1UE`VYBZUc85^q-UF`EtHqZAvOk)db$-iM!= z8*S~yWV*7E*$~)*y6@iwPVIX@syO&Xnj05$OgS^e_zud1@MA{)%A zh}Yz~4u)_@x#M*&U%zIAsltp|Lvnh*-!{2%f|Q38S3gvdKT)(_Qc{w< zdEwJ}S?*fdB|D<)F%?$;Y(v1yv@{lU#9;DYs1)?&vazutUnyQ&nFz&{4ZYWfZ&h%! zM~?;(AB8?ePd5B{^2C$tbYQ`gtCgDgbHkrLsnvb`%4s4HNxTv`ioBR1!Q*#-q_V22 zYBw8Zk-f$C*<6w4JG&oy1{N#+#S0aTxjI7XjrAVif>VBFb!}R1jhna)W&xAExZ(VK zW8#J}GKESW<}CQl8xaf9DBK|NU1A9f3Z$L8OUTfFNXgSP&)VLe3DeZc={2N(1rT#7 zO!{znjF=C1W1+3NXb;{oDd{-7xRX4_N6Rtz=MKGyB7P4uv9OsS+PBDX?o+w*;KA+y z`m!?_!Pp7ER+lC=%d>{75K&yn`1p9qD)G7KN)~G0o{e4o^3|)Yu<`Nf>6)Zo4Zp>E zFH=%z!iI=`a=FV>HI?UOr?=sLQ&Li}6QqS+=qBrJsH{>IEaV~X69zEg^En3EAVuPi zaC>BwSw;uGU!FdREx051J=$UM64RJ};T$UCL7Y4pj^WM>VBa!G6-dMGhv!+hLIC(b z%fYl?54#BiD;SA`hAU?F-@A7&J^oX6?kz4BHD+`bC`zlub5+vSec+Ved8Js{{4!<= z>K+mk+nD+*?~9K*CW9#~qc((Dfl|xdTo98qxEu<8sgMO@8yOQbD;B$O{D@-{pPOrd ztJ2W8MLcv=*Rc2>Q&VFMqT9)gsTvBDYnL;zpjrk7d)>#{n1UYxCftdRW`Fo>I~yA| zhm6K zz{=H?r45|%@$nVoo%zb=&!6vRLvotE(d4_>lk0sZzFj`^H}I@J=3T!2xgR5+{D~*i zo>%bd)rk(D`A!#sCr4a8ZCu_gOzc*z7{T*;xpT6ZP2JUVXOBpve&kMuvIb72dW&BR z8WW6buI(czb@dMs!o5Y0W@^dsbYjjQrlqeSn*;nw)o+RC!U5!-;F4CBE5K<^5{ z)MHHMCa<@ob*<^vP4#S6$Yf_I>)2H^*O*^*C~^&Wo)OL3({k}gc5S+n*I_3bijJXGS8PG+Usfp=jCB3|<8%Mfo9tVX^-`$v^De)h zs5vMi^2cVs5BUUcL}!ZYF-{ceg1@x@ra`J`tPwWlH*2KMfQcX{M?&5G^*fpC!(Q2<&2Hl(O+xr`50JQ z`_^yy$s>!?%-1=T=ry;PeCiSHd$9na1L-j9)?p40H zP;~s&o|A`cw5JcUX+4Y2-F`Se-a>Z^%bdOcwsYfMJNwjF&$V19Q}3SkEgVSbIG&_-FBX5`3vxZNYb1*@M-@K@xeo6J%F?mx{4m28kHZhwnPMh#E zN|CQ+r72#}y_kuex%s3bpT~j0tmx*c7ZV%P`k4zNrj2N4zGf}g{us~~h&f{ECcIqu ziBhvvH0adqXL8mP4o3`)X>Li%s6Eh)*i<`9v2e6m;rsK@D@95-Mvg99hSXkHHeuMW_P5t54GUmB6gI)gd)eNgMm9e>Q?>ABj9{dqYzT>-@riU%RMn`h> z6_X2PDQP|yukFpBP-^VtrucGzg7L%$U;Q(-YZy~u-!)%aTIvD1o@~;n);4cLTL0Wc z)xxj7xKsO_k!HQPK2-G*4-XGXKB!ARDPxYdhvEFO6AvSKls~E7WgFl*zGeDNt_THN6#5b&td~%8#c6mL1)gSIZLH!=<^fv6D~bcL$%^(BtK`TGi6_k)qdW=7Nt_ z3f1ZbJ}7CI7Vz+$F}vF!#CtsCVdVNecFiS2GxZm8cgZw6JX@I(!rauoj~+gt%||{R zM^i|yagFRF1-CSH_?$+Y?d{UDx63uh>c-=oYY#m+Per0`r26UTeb|^00IZj7?p&uv60>3SH7VlHONB)4##qq%XE54dERQCBoR`&WfEuq8SuW)6> z`jrW=adcqwNd5pK#!+tHURP8o5nP81(~d2FzR3KwX%?;I2vTLoY+k36r`q9^pv$tDUXJj-~FSjMQt2)Na9%=jjr|_YO~=HSMO`lzAZI+dAC%tb99q8$6mGS2R92}u#Nn% zZt#Ap)zubpj&zunXX@#sZDtnRKAGE3n3qk3UtRc;w8x(6!PZxrOo!IFxo_C}R&Dd6 zCOVOD$99<{=~JW1jY_(04vZ_t&2u3sl-qN*%O>$Tgd8&}!t;Z+!E$ABR)*vH^9Pvh z&@D*fIq)BD^7x*6N1oNmr{$b7MBUkSG(~S7diRcOc5+5oa)xzkMp&Ww?1#1(j&+LX zT8ldJ2PxRjMkM;5+%85VTwOise=n*>W!+aznnS1BsC2Af+D!SW=D$g$5`Omabn0vW zq<3rskJ%o+niU!uv3O;htMl+~soiZwrqdV2BWvd#Z#}n7jfUzWn^DV8+Tx!^)&-6& zSxn<}N@E8EKgf(TCP*JxGxE|jjBPv5G0Y-Vu*2o#y|Viz8}EIU@9Eq4sCGhqg3s3C zRrOlo#W{u-HB4i&=6v;^A8$&iU9Xs6YsIM1rSkLK7aQ-&iKGc#=NoNJGr|%7@t`#{ zVuY@}PAuB@WE+^+B+YA2ei+u1YKCktF0Lnk-=|#XJ|nZ%a)!y=de>J>BAG7S_M+>q zKW5&HyM6DU^nhNsUr1N4#6*`&NB2R3yl!&N%+qgTB^{@)SMLoft6%LIKX;GXh3?>| zaC?@QjOtz|AB9DT)pYC)%G+Z7+BR>JK|J%lh*Cx>HA&OEb8}$Oc!8LR#r9g>c{`Cz z4L;8KLy9bBboRwkjER?6G=7 zJ$5i>dF6o4LoQprOCsAJEAgKd+Q!e2`mD*>JK_C|sdKgFI#wH}*|p6_o|(~gH^o>U z8WL`N&hkLpUZFTc*L~?=lV_=QgJEp%;Fvw1`tOA!FKmuld~6#}dV5(m=4t4ml7sab z8%>R+eZsyfzZ2K5pNG#XRD<4tRPyCZ+R;8vn-I_A+7$&nNfzpae@IFmx+ocN{dzA5 zY?6PW_xzb3AMQAbzCDSRu%9(*%fBRuSis)=OSD9NOx&ddbb=*PEmJP>LpqB_XR!@z z^0|EPgdV8er)9oL%aqnt`4M)2+xcK~HyX9WS@hSv#tQwSPyhPZNXx7Wv;Xk=7#5C-RTitqW?F zXRtH>ko4}=8^-iQ`#!iCeJFI({CSykdhW_6$>$nyguf+eM{KpzZERQ5A~GK&7m^Wo zjkfxvRdV@nW&dz24IHRT_ebE!pg@T+UIk0z`3rdpLz#AQp(5NBr}4GC-|n4g z&@ClAsWP7$Q@@bDe#t4r4X!5-G?cE8vcBszY^6D@xpCWR5rYuzCcnVy4Bk5(^%+7| zBH|;?Jl9&SQC}l^VKy{-;)IoG>2cwgZButye4AZQT(~azA^5BF19#iuP#KGtdL57W zIW)b*6oxt}El)0e$5ya&kJ~iI60(CFqBIs{j3YvGwiwU%z>uv=r&>Wn92S;+Dgu^X?+so}Mx^ z`_45NI%ztQludD?`qLgI$xn*98?+0@4{cW9vOMc#~f~z)h+co_Xn;R}w z^4-&-rCbtu9&ebIq{pl2C?fH0%T0!t>*TG9*qA)qL~GNneBzq7mh@_;^|h95h69spbk@mKQiE@VK0}K7&F=w%By%p3WlZPUq4=V$@vj(Pyy=xvT>jY2 z^n*;^?|jiKo2LcjzTB#(QxBG%>Y{YLw(Z=r4!x%~k`nLg&2%_C-kH1Ff8N{_m7d#B z)6it}j%1USs;_sWBm6%s@By;|)}1~xTZptj>D=3G7>-?rPD4WOqXudi8AboFv9_-z6I9rx&(3fpO5q0t4=>#y%mYpoj^C)3R*rsGRT!* zh2T{_P7^(~+q^_TlSy<&=H!TBl;Bl#PI3r+QpC%+!@sR-N}>JUv!&XcnP_s-G8e0m z;dLvjgST_`=T`Do%_f+slCTriJ#we3Ot;SR{D)88bFW&+@;!AJ%J30ZD;+<6qtgYl z>U-4l*k?4Z6YLGp7={IDFnVN~CAuWwO1ZMQ$9T1TJN&0M5ZqWuTnzUy^DUY-=| zIj{|VnTXUecnSZ~(@yVF)%IM&rSDOD=#LhC9v+jTk?(>WB8-iRf$@RN7{u7T7 z&u7Q8$&1!^`TV=J+;V+ev`wgrl4(Wgs%E3@+yCsug~i3}!mV0jmj52(|AB6^57&yu zGY!v+#C5$m8L=j4w0@@4;xWI#tOzzGNqpcTPvAdp^RMG35=icQX@KJ1Y+b=<)cMZP z{(*sAVq!Uo<(zNI|6x4;ggWs>v5}*B4K?f6uh0C?uj06t`4?5}!AB(}C9$+uK9K#x z@&1WRLKeNuTnyO%0?}Kx;dBNKH=Ma4`yU;(3;-7Q;GHozHjgretl9CUzCrOYhdSwQ2bM5-;YuGon zNkyFhX7InhOy2iu3+kAdgdyn`OMC4D?SJt&Y7?ZUrly`YkJy`*6r~}7O-9=H;1%9? z@jv?wFIBTxIlmoj;V;VDGX}qLtgfyK6kgH3ha^;Vbo77qIruG`Lu?C6OP4&P)PHu? zIGu6%6LS1->-29+fA-7nf8Ee7mu;b{f3G0%?14_5pFe{Z2XH3lYszkXNbuM7X~ zQP2ImA7teI{YFZ77B~KWC#=!dzuyU)@xS%S$2*XbaE6i-*oqE?P-y7#9yqGK&0Jn#S1`k=M}c~~D7CMz?^o+XtfF?Tdan(xI2NyDZ{|+?{`#$Fs|kqa zvBBTb60RMre6zb_KGz`RQQUJcYeCYfTfTXNMqxyLYGjUM*L%zZa7A8EK0%vHNQfS3 zH4k)O%jdKi9+QlUM2g1_*~@#?PCviK2}oZX@2jgP)2x}B=(>fxDN>F6Cb{&IF|<#7 z7x@VMWZRtD6Ubs_?Ya}QYRe#ZrVdoN(o-JPhx_lOmzX_=3qi*kf^?mnnkt(as=xF2 zameDL7m`IiV#U8{P)~6kA1eQ>?qdBmEywPgBmGEbI*+ypt35un2~AT1wUd<4OGVVK z#s8M`wIhLU_(Vse!a3aB_P1bcl4J!o=U6m=9&d%=Cg3Nsa0`Ji-D&yjib_arJ zQ+naq4db<{yDs6b&C`^5$sjp5HT}VUB2o{XEUVMKW zk&OF={*17s-GJ@7FC03z6PHX23xLwp9O3u)84TID^?tvDR z=t{dT%P4$u(g}HAMkq88I~ZW#E!+0(-jZs=imwaN=$Rqbckx02hT>6f+H}OjL);hQ zw?0gCErs$e-EE|4$IbhT?}*I=%i{6l&e86^zWVx}T&7UftyU?wvro~ayn1Dm5rU}j z-7Z0m7|Fo<_ZdI*yTAl;2?(UtJ8O4+(`{D84kAJXY=iQ2i}Fcy(L??#S0_WetLJ7x zN4mwjqU9;o0!*Z)q@r^14r`ddI6DyuDG5?yN?20ZK$Guuvj@gM)V!$K>CsIV)Zdh? zlc=7@wr5t@)U9wdMX?w008fo%c73aOZIqZJGNkW^7Bu2he~cGMyA3m9C^ya9)-ZIi zQ+p^>YL5@DYiw4fqod>D<$ac&9Z`H=J0Io@M`|63xJ}M`)plt|u2GPYFq9tu8I3)o z*zkCKVda|Og#fdSKDt^s*T!bzB3{Dhb$Kg=lH<@W^ zaX~N~n3$OO;mentEEXoF8?RsU1G?1>`xX`yC?VW#UVpMMHh*fWAxWp~%NGp~Z|_iO z)f_@%z4l+~?}uHD8TW&hQz7XTm7JWMmX1z6Q=)vKrv3^$i$XATM`ZHPU%YTX@}nUt zZ+g(f6@v~M?lOB0fm1McE*Es6uwVHLgiD^6LgOdcKPf1xD6t?!SAFSvNUa`!OxB~!r( zv`qh1zH?P`nMfuQ*F`v0QiVU+Iwc1O2eH^KvlQ@P%D*HRUL&&OW3(riC_tWW{pmq z8xee(QutWUTm?(r$c9;}+ zWJJ|o^@^$;NI1wZpmp9&RM6a`XM?cSN5;A|b2(yh^^%#;^1L@8(rhbAA385GH?-%D zco8mJ*mv0@uFg6DuefpJ#`3x?d7n%@B`%GdkjX>7@ivruTtb8^z-juJ)Zfr`m|Hwf z?Ee;t{{8xYV&MN7)a5z8k(S+GZBO~*!?qo7ilIk1vgck89|{L8;xE-&;1JruQPGxHkSO}M#z4?eCOaW4Ua#LK)6 zT^~62`Eek8e*t`|&?7Y&4qALjS`XkVJub>E3z}Sd!s7yIwARPv#+05e0Tv{tWf5{} z84mS)JR*eg7nhvPFaJ`Oo-A3{l6ipvB*(Y0D&AeYhQnK2iKxJ<5F(uIcuR=O$A>gK z#(``hyrJB@d1Oda@+{Coa9G&rTwm>@gBy<=Ia2uM>oFyz2A#iRkd2r@k->BZ%|)#~ zoE2$Le)_Z|+YA6xFU2Sjmb0{`W-}KTmj^D$d3D({$Vb4uh#iOodOVN*2m17IP7h8H zFYEzGkO@<<%s-R-`n8g__EzMUno>>M2NnmvJgEXZMyMFw>FRBUSV7kqphFW5?Nhzv zp25K&pr^BE&x((hOY*-iD%w%J*qBDYZQCZiF>tyi{4z4VO{PVcC3bj^3X@7KZ^@jS z_su!CI5PyfA2|scn$qmggs|H{_UI!u0<50~?ZGPL&VPt4+&=Z$$B!qL`vv`u;HseK z>mC}qk)AHJ{PU=sf`db9y(JY_I`+R>j3m=v!3z*msg8zBAVHI)e@eU`8|$#-H=X-> ztaiY}Wp^T^nZE#I&tP2h3&XJc?37Z$;+GAI8Q`>Z9(O|`0GO{ z_61aw{wNoKQnNahvU_lFBME9-6(|Tm#tHi@UL-;nG-zmf=e8en8m_?4@$A_nIqKc` zN>?1xP@=9AR@)LNnOHzXAwf_EWwSLyZfXhjCeadFoHYVy98wcl?>=HF`f z9@h`hq6eRB30iCN*Dvf^GIYTUgJ5izh{#qvnD;o0&ix;m2n(O>V?5YzqrTG1QY?>& zxoC(i3rBSZFonQL2p(1k{Z^|iRzR;6AWPbkWtxU*F?vpWqoe_?H-7cSJ}Cv(6o4LfINPhkx`fZslL8`ZViH9BtI=a z+~(p~r3O%tIPB0N9yFV>;ZsNmRghKFE(5UE+jqfN?E1q*y)1`3l$6=A{X<)5#XB>d z+H5p#r=+Gy!lgtX*K0B;FE7vMd0+eE z{;gZL6r+NI`kPJeRsTqTotjEZie}Eh5q#PA$05bjzWx5G>z!N8&=dlIwv^@u92KF9 zKQ;43?^OpNP)-aCJpaUp8J)sC)>h^-Jbs-ayk&OT5pOK^x^%{1qkSfsZqy^5L^$Mu z@z3o(b|o%FckbMwwI_{UuW?I=h?UGa_n8e;(PgYEZQ&hJ`?j<5S%;f2d*agZu$bzk zn{Svmr~K>jOAto<1K$WkW%TSa!E2#YE@Hb)g=Q1!%Xl)RXgQ8i99CL;%)(ZN{{4N} zw!U;Vfyh4etFHu@ow%fr1}K#P(HvB&Vh$bhW@fRbQCL!yFxD%8;O#;sO&4b_OWxDru=>2={In*# zzE;&==t~Xn>hK2@0QOe!BcQ16Ut(fOqAv94iGGc?(yUM5YLfmfY{dUKe zlh5shSSO>pmMKW-Kr@LMcvuCPiS0qu0wJI!qS?+3FJPHSCEyi|P1CY0iTrEb(Vdl` z8z#ZFFey+5josQQH=en7DoAd*)M3Hc%4+v2q>~DJ?pdInYxZ2z_leZ4OiY1LJ?4K` zfGpM^&t-R*%CqvtFVfT0|B77OzrK0VZs@(9j6?28?T=GO4P8qgZzl|n#9szS9Zr+l z!oq@pKPa>kI$c7-#Y&^S28=-^xCa(AjqbbUx6DnXhK}Fc6ZiBfiHN#Yo|QsA%o!=Q z%3(Y8qZ-bs3eE+dC&OpS-K6O9HaeLZdbql-GtvMxU@JudTfX})*;8_*U#Nc}8ua0F zv;+g#MB=au`u+;lYqlR_CHjUt{hqQSfZf7mIUVHSBt%Rm`R+7{>M=xFokK@%kI*d* z)Dx)OWWOMlgQ$QJ6$nI3L*VRoFOZrrMZ~EFLFBOgP2+O)h4C4XkUdarmp>G?oW%P+ z%gES*557WA4$MwaXQNH#SiNuc^%;0RU`V^s{M`T6ExJ^*!VrLk0!&RI95fl};JHsF zO)16@yI=kkBgL}s0TtdM7e6)*t7?oMeu=>Wpv-n+Aos{j5%xEwKXOI8Y)VnvNO%Pd za3%LAaRTYs*Mx?KR)X)<*}}xM`~Hapg!WqjT+=c7;bKcO#*HBSi;CL6x;!Ws%B6Un z5_^w$C826XvyaiHuD>0teY1mTR3)J&Cj|%X@H)myeO>g6CB_*j#AyaY#H!ArnARMs zZ|12Swp~meVO^YFa^=$_*DAo#?3G>UUT1NL&3@_Fi4$8%5QoQj{HkP8h6g2LAk0`` zLWiLj0KK%%<1HDAU-_0E(R1C`AuO3=9mn@6^2~!N47DO$XV+dn;MY8DUV$VAZ%( zNV@>%EY?zgoT`tu8|j#yo+kV@$Xb`hSO9&ks_FozZ)s7475N+?XnvCbfO2v>fl^1C zEP)xefqbh#1Y@T-`L2zipJQXkFH&;MMic&x;GV#n&~tEf*7xAuW~rk8h56BPp%(*o ztDsW94#pyuHpuH3>P3LLLqYnyNyqW|q4L*@7Xt{s43Jn6IO@sPrlb9XgMElPBtX69 z#nJ}C`088yNiZx1z~w?4{UWja>5fB435Rh=Wh1$UV8cy(@ZY5!T>pz?#{Vpk!?ogS z|6AFT|9?JUhYW|jD_G_fWW|3tiCZN2w8-OiiBmhVf-6(2jg63`Zz11Q%| zm-v$*>Pt5BTXCb_CbAx_hMtta0XV`oTPL^NLr0c**$m0i^R>~XK*CNVO$VUM$4Mdh zB%E*LZmmO?>}&vh_ww@-6%p;@3)53mNR)7O6!~nYxJCxmEW*NacqW2SBoQNqZX>*D zS3$BE3ZnW%jiaDkI`a(q9-JW33!feeKNNS~^L21DF{*Zv9H5Sml{#V&%!IJj&_WoS&G8iO!dV2qsmX=uB ze(%1eDbEQg0boio6#RllkOHxBw(@>{RW%n)^QeXe}nd6=3#KU9}Fe!mCt?#j~y2%mx@Zm#&;1ll% zRUnKoBp_sfoJdq`Xs~g^y}p7=>_MmoYOo3>)k~6{;Bf8=F-J;r@$zmQ8X9^i>9!BJ z+akwqVY26;h!qu5K}MY@=d;Y2x7(^vVkcS-s&PstqQeRktUuJ$l;7R03aeyqdn+VF z0ZqCQ!S+C&u)9hYV_T5zAzY7fJF1Hae@Ds1-sHIW2mxIm>aGbkVAP8(Fd(3%DfH$| z71Wv6#>$Lnn%EQ-H$_WbqJ_V;$$Hn=t1U0@-zf`S(+zAS;*sQEns8VB5*K?B$u~q= z^ho4#d~d3%s*0=9*Z6s3)QXFdzq4zNf5Mky+Y z_EEC(w0?+W;NVfbIJ{pP%%8j8SkuVtLh;&?91(aTC#T+*PWy{Z#_JG#wU(aVb>&dz zQk-H7>pCPL?e^b~5qBmBf698unw|((;bhunlTkzII;f63U0HoSgXR0MD~XA`NC5^R z8?`QdhPkcSPp?o^7r6$=>|S!(ewjs}cfg0~_F3HQK+7e+Rhg5JH81R9ny zg@(nbvk55(tOqI{52SM6tNscq5YVT!pVijZ_73b5Hph|V#pTRDtclsM3rU;r?I+ReJf z^sjg=6sgxz>qBMj?b|alG9F4@atLjQ!7_nj-*w>A1?MNolqy+Ra9#HCxpV)1fYvjW z)(ENX%=+5eS5Xy4m`t{C3IL_Oe0)dL)oH;>z^58GUZ>&Fhf27Ca46~NcmVbAP~ zrkjv-kd&SQubVEF?H1g0PeYcdho$9M!k#+{Tw1icXiFe|lE3HlfCZ2@y#gmU_qs%l zCo1T?0UJhWpl~{~AI&$-I|C39^XNiCLWC}S;|as)6QUIyu;ML%-?pYN5Gd=L`&i}A z83dgaFnqHfG}lpNfdNmQnoTcRAPP7zP=`mC0d5KAp+l6harnvFGB2`wzdmr^LZtOR zq8wD%fNN!fJk%u>9aV#Bq$!z~dJfm}lwfUo8we@M(c4M@F3#QbfD> z`Kb}85s8+C*BDw6WSZ~ZgUvz&b0-?`e?BL2<-spfXT_>q)38 zCMLuo$9xrr5(zMLgP><&IiK@n3%5!)7VZe@teyu=BD&b3_2rCnwKCj}eyYh46BCml z2M_EU0zVUs8BQSB2b7vAA@X^l@q{)-FSM=A2r3Uo&=uvdQ0tIx$1%E$NKE|FIQyQH zkI%MuA#O%aV9dcOCeL$v4-N^zqrj69LLGzj1ynWrYbj&riJ#X_% z<@{Eqp)m)^V6|T$26hTXJ~o$LD_*BQf(K7*6a>AABljaBdI2D>sV6GpNnl%PVnc(1 zY?p-jBEtSV4<819_;9SFz>|UCuR(YrSJBS#1@&DZ;GGcFBEgMR5P5T&xuaVY(vENu z?KB=bAUitWF=15aQSd10$kMF=b^q)tE;c25ngq{65Ym&AlO>p})7{f^1+X{{yX?Z1 zen0_&sUs2$#OcFzsK5zERcf4XX2P8gi;j!WAugH(p0orA$&PhslI|$<79)z)5ip)c zVw!LrgdNb=4*~}v+9w?KnIH}Ks7D9GgyuKAb%GxvJS3sHfKO$Yyhw@7!7AnuT|djc zYZutgAXFyg_iDp9Ae(Bnq}fl(>0v$%?K^pGC)9!{Q>53`)g|!c2;KrVHO$m`2+x>R_)I{jc}+}Ga&kGsjZrH415A+(HiwDAOXCh~HgWe+)&P3s zDnO(i!A^}3Ftu5-^@MpKDkAe&Op1Lu&_w`{EI!2^j@o_Xke{EC>*CCXXjz}Op*Kwq z{wZRoJ_qg3nl*$nu9wbl;aa_q`*Zn2VE+i+A$&KOPC2wHfRvu_%!$g>w!$dkq{5!K zPEXFQ66R!3WheR!VgOK>l>nc=5Qs{R)|?fQkO>-)kt0awr%yR&fTc*peuo(U3_Ru7 zDr)2$Q3w}0f>0c)o}MbX)r>Kf+zYotQALWnI(8tlZ0ViS9zqDQ3Act~%6!x524v!4 z(zp*FZghyBgHz6V*NIPs7O5xmd`zpIii?W~3PC7JM9%(d)Hkoh1WdwKBrZvL&+&uJ znuNNh&1KKNeN<@D`Pj6y#HJ(HIa{KQf|eb`s~xs`+~?lAZ%0nRrY5)N_O+}m%?ByO zrJoFQOlTf)#^fWYc&hd2!*1TZUS|z|!h8GS!=E$DQCk&2({BQ-&dzS~nv^&Q3ab6r zMhH;wa7~bDtL(k2wnx##gdGXIYlx$&=7O$agM(FSzib4G_}IylS@tYjwvfSRLJUKT zSp@DfOxw3_hwS8=Xs9LF#VCpm-_2aVj4ZCc;!1YrlkFNmh@nv6MU>%M|I(s0 zfc@(jh2q6sV>;yK&1~=9hmr6i7Ms$S-}s?i3>K?xk1hqjjLd7XL#S#k2WO&r>Qn%- zBqoJkOyH5m9{24L7tif<{Xo4#x+V1~Jv%#E3XrKYDZnN}MiHc4{Yn3g~S>aWuy@H{z;C zi(*CXA1jhkrC$~R>wAl%EowmF!UaC8)93)r_QN)e$THuApe?gybJFe2%T!0hDD#LgqLCE&^p61 z>iIG{EW@0vhn9$>+B1TEOE^trX2_n^S+^HuW;UeeGBPnWrkd$XNJwP!o&X4JA4}s) z(X*ZJzEV;mPcXCRb&ZTdaEA1}e<A9`aY4m3x3vp(|ISmG7h<>IG{NzD~se`yfWsPvg6qSd=^>;R3d&(Cq!Az z&D>Z$B+_UL2Orzmf3pr$=((Lk6<9XzotX!A#@#2)Kw{~;t^Jzb_`}VKS(bkX%f{c^ zH1%wE9KKbv=!Vi%66(`sOTgdQ)uc=xHoVoPck5Gj?v(G`NHZOtjW`ep5IX9~iPC%` zA)M_-_-H~916U5%fE@{IUWSv#G1nD)BOgO9iA)UvDp3JWDkv<(g!V~LxDumjzrA=G3GGM-j& za`VJ%n`fu%pgi zM#9y;I2m;vUj_hm zuzl=CK>PQg83e&*>!q5sj$7F2VT3u!A0n|T3q!!Laqy?^G70;|#8|NWqM5Z|D(Y=C z*lmToGpcsDdVQLkk$po&NlA!r)vc&*Pr8l24ff00_oH&76UG=MvUREDLrWk57C%`fFu`26BlGMVOY3vxCx_#$P zI@}8~C1|eE`uXTLQwnNU#B0IuRmM@(XUiZEXzvv*Dk=i(1OGvZhCun;YTKclA_5yC zwFNOt&W`QYp0x4Ev4Ekuh&dpLKLP1)_V%7ZmdVY!9cDPLpn#oFtbzC)HG@Y-?~J4F z?(R`bzvtQ=2@KV)v(8uq1P17lq)PIeZZEA)o}_^@U^lb>T;CUFB5)M)G>{xj_{d{O1y~-*OL`fA0;zUgv9x`nz(652` z2HVydsqFkYKs?1bO-^ZP_L57=-{yRa5a|-XR6qtHRS@I*79+PnaGr`s|w5Qviy2O=nhj0Zh=$go9o-9{qH`{H6S81p?inMnW_3y2SM zUHYjI!B1Q?g|q>b1`%b$4mmd8-h64h$XQA((pwPjZ#s9ETo@NG^I=H=xj#LO+Z&P49%8VDdLr{hvnYxSIL95vqFT!b`( z2Kfm>Z3xN$kE{x(^G-}mID(O!WPMHJ-o8HP$!Cc7zpJMx@0RkTZ30adE=8LB*4Tk1ag8bwinI~ zn#Ytm>bJDU1u$kogO6R#A$%9V_>-O(j0^E!dn|KvTY>sGZsZ!W8SK&?^vHQ&@*;u~ z*S#SZ_25O(9qD#dlx@_=iDz}Ezdb)rfNdmf>5-g5yb=vY1|*7m(lJT{FS2k}VR9m0 zjnEhY9Ffz_nKwZ?L{Uj;J={ttqLH9PXO*Kz`=B-=TE!uU9fYxa?KBf3FMs~DCh#uM zLlu7N3>^}?l>4WIvbVsChzPUoTCYN|aRzsQc` zjku5C`oP?{(>;V#1PJxw;v^XnlI+?K@{SK_Xt+Eg_Czlw?I6%Rdz1lus5v)E3F#AUyAUL*b)c?%B&By#kyo!*X?Rw zRvnK(m<#5V=rDvvLDzBev7yih%vRreggVdUCRo5T@Hhx{GER~lL>^6o6P9|t6F?Sh zSx;HuW+?64ByZg}5y&}N(vg{XXEx~)^5@`$;^N{6PmVymahT_eztzh&r2Z@khG7Af z2)IyC#;3hcY$Ks%4ks!Ih@_l~%Js1!d+<bzLv5|Qs&h4QgK+XV!Req8Ku-{Ye-t zkj@#6KxhmC zCFqGEqK<5Z-30j4Qau|lH9lIMQnb*!850n?&&w?tM$Y^G3?@1b;D{kyfzB-0#2s`D zEXZkyl}x8r8bL}DU6ep^YUV$=f2=|0yXPPWq>8&FB=Qm!{}cdiviy=Tafk50$W@9X zo;z0?-TEvr0ci*nuyH|>K|@1BkVdKI#W!(7a|UWk<3iY+M5|HoZI@VS`$MprU=WCG zCf0^HvoJ?sC4OGFzVS{SEvaC{N4=Q;q^=~&d%w+pfK-ya(CikJ%R7pNXF-=B$Dj$| z3Xz)P#r)Gz%MHWP^Z!b4E1;BDuM_%jafNfrW|O zeu}K_tz%RmmIx75V)?m0#X66B)5YV}fkb_h8GZ{O1$n@A9w|KH>o}CxN--dY$gh=t z?VSQ1$H8^8B(fRAS0^CXpF@s6TntCP&=3laKVLQ*{tD@cSN%Usn63J$xqaj86XFvg P;p@1vx>Bmb>8t+>OfOh3 From b3ea819ff7aa6d1cd7b26d4d4acb26692162199c Mon Sep 17 00:00:00 2001 From: Gino Lu Date: Tue, 19 May 2026 22:42:49 -0400 Subject: [PATCH 15/16] sparse_attn: annotate PV-skip chart with speedup vs dense baseline --- .../docs/pv_skip_mode_comparison.png | Bin 113868 -> 132070 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/example/ck_tile/50_sparse_attn/docs/pv_skip_mode_comparison.png b/example/ck_tile/50_sparse_attn/docs/pv_skip_mode_comparison.png index b35c20a679ad9dd16548170b39c8836fc8caefea..28eec99e79aa59409c4667b1b446eeeee49aaa11 100644 GIT binary patch literal 132070 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

      f6Vp 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 literal 113868 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�KEnX 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 N@-j-&? Date: Tue, 19 May 2026 23:22:00 -0400 Subject: [PATCH 16/16] sparse_attn: wire -mask and -attention_sink (block-map prune + attn mask) --- example/ck_tile/50_sparse_attn/README.md | 10 +- .../50_sparse_attn/sparge_blockmap_trek.hpp | 9 +- .../ck_tile/50_sparse_attn/test_sparge.cpp | 211 +++++++++++------- .../kernel/sparge_blockmap_kernel.hpp | 18 +- .../pipeline/sparge_blockmap_pipeline.hpp | 45 +++- 5 files changed, 195 insertions(+), 98 deletions(-) diff --git a/example/ck_tile/50_sparse_attn/README.md b/example/ck_tile/50_sparse_attn/README.md index 0a7b513748b..593f4a85ef5 100644 --- a/example/ck_tile/50_sparse_attn/README.md +++ b/example/ck_tile/50_sparse_attn/README.md @@ -9,11 +9,11 @@ Implemented: - top-k / `cdfthreshd` block selection, BlockMap LUT - sparse FMHA (both `vsa` and `jenga` backends) - per-head `topk` / `simthreshd1` / `cdfthreshd` +- **is_causal mask in pooled score** (top-left only at block-map grain) ([spas_sage_attn/utils.py:L338](https://github.com/thu-ml/SpargeAttn/blob/ae5b629ebb41e41f86b3ea2ab5a3283f13ac151a/spas_sage_attn/utils.py#L338)) +- **attention_sink** — block-map column 0 force-on ([spas_sage_attn/autotune.py:L355](https://github.com/thu-ml/SpargeAttn/blob/ae5b629ebb41e41f86b3ea2ab5a3283f13ac151a/spas_sage_attn/autotune.py#L355)) Not yet ported (upstream pinned to commit [`ae5b629`](https://github.com/thu-ml/SpargeAttn/tree/ae5b629ebb41e41f86b3ea2ab5a3283f13ac151a)): - **K smoothing** — pre-pool `k -= km`; required for diffusion / video checkpoints (CogVideoX, Mochi-1, Flux, OpenSora, SD 3.5) ([spas_sage_attn/core.py:L53](https://github.com/thu-ml/SpargeAttn/blob/ae5b629ebb41e41f86b3ea2ab5a3283f13ac151a/spas_sage_attn/core.py#L53)) -- **is_causal mask in pooled score** — required for causal-LM prefill (Llama, Qwen) ([spas_sage_attn/utils.py:L338](https://github.com/thu-ml/SpargeAttn/blob/ae5b629ebb41e41f86b3ea2ab5a3283f13ac151a/spas_sage_attn/utils.py#L338)) -- **attention_sink** — column 0 forced ON; upstream is hard-wired to `True` at inference ([spas_sage_attn/autotune.py:L355](https://github.com/thu-ml/SpargeAttn/blob/ae5b629ebb41e41f86b3ea2ab5a3283f13ac151a/spas_sage_attn/autotune.py#L355)) - **Sort-based top-k selection** — replaces our O(N_k^2) iterative argmax; matters at long seqlen (s ≥ 16k) ([spas_sage_attn/utils.py:L345](https://github.com/thu-ml/SpargeAttn/blob/ae5b629ebb41e41f86b3ea2ab5a3283f13ac151a/spas_sage_attn/utils.py#L345)) - **Q/K int8 quant fusion in pool kernel** — enables a downstream int8 GEMM0 in the attn kernel ([spas_sage_attn/utils.py:L371](https://github.com/thu-ml/SpargeAttn/blob/ae5b629ebb41e41f86b3ea2ab5a3283f13ac151a/spas_sage_attn/utils.py#L371)) @@ -40,7 +40,11 @@ ninja tile_example_sparge Select a PV-skip variant with `-pv_mode={none|warp|block}` (default `warp`); finite `-pv_threshold=20` lets the per-Q-tile skip predicate fire. -Add `-v=1` for CPU validation; use a small shape (`-b=1 -h=2 -s=512`), since full-shape CPU reference scales O(s²) and runs 30+ minutes at s=8k, hours at s=16k. +Mask + attention sink: +- `-mask` accepts the `01_fmha` grammar (`0` / `t` / `b` / `t:l,r` / `xt:N` / `g:y,x`, default `0`). The block-map selection prunes past-diagonal blocks only under `mask_top_left` (`t`); `b` / SWA / generic are forwarded to the attention kernel and emit a stderr WARN that the block-map selection is unchanged. +- `-attention_sink {0,1}` forces block-map column `kb=0` ON for every Q-block (default `0`). Under `-mask t` this is degenerate since `kb=0` is always causal-valid. + +Add `-v=1` for CPU validation; use a small shape (`-b=1 -h=2 -s=512`), since full-shape CPU reference scales O(s²) and runs 30+ minutes at s=8k, hours at s=16k. When `-mask != 0` or `-attention_sink == 1`, the `[block_map cross-check]` and `[VSA LUT self-consistency]` cells are SKIPPED (the CPU reference does not model causal mask or sink); the `[attention output]` cell still runs but the dense reference applies no mask, so it will report FAIL on the kernel-correct output. Treat `-v=1` correctness as **block-map level only** in those configurations. ## References diff --git a/example/ck_tile/50_sparse_attn/sparge_blockmap_trek.hpp b/example/ck_tile/50_sparse_attn/sparge_blockmap_trek.hpp index c0178f70def..7591b94e54d 100644 --- a/example/ck_tile/50_sparse_attn/sparge_blockmap_trek.hpp +++ b/example/ck_tile/50_sparse_attn/sparge_blockmap_trek.hpp @@ -56,6 +56,11 @@ struct sparge_blockmap_args const float* simthreshd1_per_head_ptr = nullptr; const float* cdfthreshd_per_head_ptr = nullptr; const float* topk_per_head_ptr = nullptr; + + // R32 Items 2+3. Pipeline only honours mask_enum::mask_top_left; CLI warns + // on other types. Defaults preserve back-compat for callers not yet setting. + mask_enum mask_type = mask_enum::no_mask; + bool attention_sink = false; }; struct sparge_blockmap_workspace_layout @@ -105,7 +110,9 @@ auto sparge_blockmap_create_kargs_and_grids(sparge_blockmap_args args, pooled_k_ws_ptr, sim_k_ws_ptr, args.topk_per_head_ptr, - args.cdfthreshd_per_head_ptr); + args.cdfthreshd_per_head_ptr, + static_cast(args.mask_type), + args.attention_sink); dim3 grids = BlockMapKernel::GridSize(args.batch, args.nhead_q, args.seqlen_q); return ck_tile::make_tuple(kargs, grids); diff --git a/example/ck_tile/50_sparse_attn/test_sparge.cpp b/example/ck_tile/50_sparse_attn/test_sparge.cpp index 8ba1b97e846..c368cb0304c 100644 --- a/example/ck_tile/50_sparse_attn/test_sparge.cpp +++ b/example/ck_tile/50_sparse_attn/test_sparge.cpp @@ -19,6 +19,7 @@ #include "ck_tile/host/reference/reference_blocked_attention.hpp" #include "ck_tile/core/utility/bit_cast.hpp" +#include "01_fmha/mask.hpp" // R32: mask_info::decode, mask_enum #include "fmha_fwd_trek.hpp" #include "sparge_blockmap_trek.hpp" #include "sparge_tool.hpp" @@ -126,7 +127,23 @@ auto create_args(int argc, char* argv[]) "none = no skip (kNone binary; matches VSA baseline). " "warp = per-wavefront butterfly vote (R25 A1; default). " "block = per-block AND vote via 1 LDS slot + block_sync_lds (R30). " - "Overrides -pv_skip_compile when set explicitly."); + "Overrides -pv_skip_compile when set explicitly.") + .insert("mask", + "0", + "0: no mask, 1: top-left(same as 't'), 2:bottom-right(same as 'b')\n" + "'t', top-left causal mask, 'b', bottom-r causal mask\n" + "'t:l,r', top-left sliding window attn(swa) with FA style left right size\n" + "'b:l,r', bottom-r sliding window attn(swa) with FA style left right size\n" + "'xt:window_size', xformer style masking from top-left, " + "window_size negative is causal, positive is swa\n" + "'xb:window_size', xformer style masking from bottom-r, " + "window_size negative is causal, positive is swa\n" + "'g:y,x', generic attention mask coordinate with y/x size " + "(only debug purpose for now)") + .insert("attention_sink", + "0", + "SpargeAttn: force block-map column 0 ON (kb=0 always selected). " + "0=off, 1=on. Block-map level only; independent of -mask sink prefix."); bool result = arg_parser.parse(argc, argv); return std::make_tuple(result, arg_parser); @@ -161,6 +178,8 @@ bool run_test(const ck_tile::ArgParser& arg_parser) int pv_skip_compile = arg_parser.get_int("pv_skip_compile"); std::string pv_per_head_s = arg_parser.get_str("pv_threshold_per_head"); std::string pv_mode_str = arg_parser.get_str("pv_mode"); + std::string mask_str = arg_parser.get_str("mask"); + bool attention_sink = arg_parser.get_bool("attention_sink"); // R30: --pv_mode maps to the int dispatched at host. // none -> 0 (kNone), warp -> 1 (kPerWave), block -> 2 (kPerBlock). @@ -192,6 +211,15 @@ bool run_test(const ck_tile::ArgParser& arg_parser) if(hdim_v < 0) hdim_v = hdim_q; + mask_info mask = mask_info::decode(mask_str, seqlen_q, seqlen_k); + if(mask.type != mask_enum::no_mask && mask.type != mask_enum::mask_top_left) + std::fprintf(stderr, + "[test_sparge] WARN: -mask='%s' (type=%d) - block-map only " + "filters mask_top_left; selection will not prune past-diagonal " + "blocks. attention kernel still applies the mask.\n", + mask_str.c_str(), + static_cast(mask.type)); + // If cdfthreshd >= 0, use CDF mode; otherwise use topk mode if(cdfthreshd >= 0.0f) topk = -1.0f; @@ -281,6 +309,8 @@ bool run_test(const ck_tile::ArgParser& arg_parser) bmap_args.block_map_ptr = block_map_dev.GetDeviceBuffer(); bmap_args.lut_ptr = (pipeline == "vsa") ? lut_dev.GetDeviceBuffer() : nullptr; bmap_args.valid_block_num_ptr = (pipeline == "vsa") ? valid_bn_dev.GetDeviceBuffer() : nullptr; + bmap_args.mask_type = mask.type; // R32 Item 2 + bmap_args.attention_sink = attention_sink; // R32 Item 3 // K-stats workspace: caller-owned, sized via host helper, allocated once outside any timing. const size_t ws_bytes = sparge_blockmap_get_workspace_size(bmap_traits, bmap_args); @@ -350,7 +380,7 @@ bool run_test(const ck_tile::ArgParser& arg_parser) attn_traits.hdim_v = hdim_v; attn_traits.data_type = std::is_same_v ? "fp16" : "bf16"; attn_traits.is_v_rowmajor = true; - attn_traits.mask_type = mask_enum::no_mask; + attn_traits.mask_type = mask.type; attn_traits.bm0 = BLKQ; fmha_jenga_fwd_args attn_args; @@ -380,9 +410,9 @@ bool run_test(const ck_tile::ArgParser& arg_parser) attn_args.batch_stride_k = k_strides[0]; attn_args.batch_stride_v = v_strides[0]; attn_args.batch_stride_o = o_strides[0]; - attn_args.window_size_left = -1; - attn_args.window_size_right = -1; - attn_args.mask_type = 0; + attn_args.window_size_left = mask.left; + attn_args.window_size_right = mask.right; + attn_args.mask_type = static_cast(mask.type); avg_ms = sparge_jenga_fwd(bmap_traits, bmap_args, attn_traits, attn_args, stream_cfg); } @@ -395,7 +425,7 @@ bool run_test(const ck_tile::ArgParser& arg_parser) attn_traits.hdim_v = hdim_v; attn_traits.data_type = std::is_same_v ? "fp16" : "bf16"; attn_traits.is_v_rowmajor = true; - attn_traits.mask_type = mask_enum::no_mask; + attn_traits.mask_type = mask.type; attn_traits.bm0 = BLKQ; fmha_sparge_fwd_args attn_args; @@ -435,9 +465,9 @@ bool run_test(const ck_tile::ArgParser& arg_parser) attn_args.batch_stride_k = k_strides[0]; attn_args.batch_stride_v = v_strides[0]; attn_args.batch_stride_o = o_strides[0]; - attn_args.window_size_left = -1; - attn_args.window_size_right = -1; - attn_args.mask_type = 0; + attn_args.window_size_left = mask.left; + attn_args.window_size_right = mask.right; + attn_args.mask_type = static_cast(mask.type); avg_ms = sparge_sparge_fwd_combined(bmap_traits, bmap_args, attn_traits, attn_args, stream_cfg); @@ -500,96 +530,111 @@ bool run_test(const ck_tile::ArgParser& arg_parser) auto k_ref = to_bhsd(k_host, i_perm); auto v_ref = to_bhsd(v_host, i_perm); - sparge::SpargeParams sp; - sp.BLKQ = BLKQ; - sp.BLKK = BLKK; - sp.simthreshd1 = simthreshd1; - sp.cdfthreshd = cdfthreshd; - sp.topk = topk; - sp.i_perm = i_perm; - - auto block_map_cpu = sparge::build_block_map_meansim(q_host, k_host, sp); - - size_t bm_total = block_map_host.mData.size(); - size_t bm_mismatch = 0; - size_t shown = 0; - constexpr size_t MAXSHOW = 10; - std::cout << "\n [block_map cross-check] total=" << bm_total; - for(size_t i = 0; i < bm_total; ++i) + // R32: CPU reference lacks causal mask + attention_sink; skip block_map + // cross-check + VSA LUT self-consistency when either is in effect. The + // attention-output check below still runs (consumes GPU bmap). + const bool skip_cpu_bm_check = (mask.type != mask_enum::no_mask) || attention_sink; + + bool bm_pass = true; + bool lut_pass = true; + if(!skip_cpu_bm_check) { - uint8_t g = block_map_host.mData[i]; - uint8_t c = block_map_cpu.mData[i]; - if(g != c) + + sparge::SpargeParams sp; + sp.BLKQ = BLKQ; + sp.BLKK = BLKK; + sp.simthreshd1 = simthreshd1; + sp.cdfthreshd = cdfthreshd; + sp.topk = topk; + sp.i_perm = i_perm; + + auto block_map_cpu = sparge::build_block_map_meansim(q_host, k_host, sp); + + size_t bm_total = block_map_host.mData.size(); + size_t bm_mismatch = 0; + size_t shown = 0; + constexpr size_t MAXSHOW = 10; + std::cout << "\n [block_map cross-check] total=" << bm_total; + for(size_t i = 0; i < bm_total; ++i) { - if(shown < MAXSHOW) + uint8_t g = block_map_host.mData[i]; + uint8_t c = block_map_cpu.mData[i]; + if(g != c) { - size_t k_idx = i % num_k_blocks; - size_t q_idx = (i / num_k_blocks) % num_q_blocks; - size_t h_idx = (i / (num_k_blocks * num_q_blocks)) % nhead; - size_t b_idx = i / (num_k_blocks * num_q_blocks * nhead); - std::cout << "\n miss[" << shown << "] (b=" << b_idx << ",h=" << h_idx - << ",qb=" << q_idx << ",kb=" << k_idx << ") gpu=" << int(g) - << " cpu=" << int(c); - ++shown; + if(shown < MAXSHOW) + { + size_t k_idx = i % num_k_blocks; + size_t q_idx = (i / num_k_blocks) % num_q_blocks; + size_t h_idx = (i / (num_k_blocks * num_q_blocks)) % nhead; + size_t b_idx = i / (num_k_blocks * num_q_blocks * nhead); + std::cout << "\n miss[" << shown << "] (b=" << b_idx << ",h=" << h_idx + << ",qb=" << q_idx << ",kb=" << k_idx << ") gpu=" << int(g) + << " cpu=" << int(c); + ++shown; + } + ++bm_mismatch; } - ++bm_mismatch; } - } - bool bm_pass = (bm_mismatch == 0); - float bm_ratio = bm_total ? 100.0f * float(bm_mismatch) / float(bm_total) : 0.0f; - std::cout << "\n [block_map cross-check] mismatch=" << bm_mismatch << "/" << bm_total - << " (" << std::setprecision(4) << bm_ratio << "%) " - << (bm_pass ? "PASS" : "FAIL"); - - auto cpu_lut = sparge::block_map_to_vsa_lut_delta(block_map_cpu); - bool lut_pass = true; - size_t lut_fails = 0; - for(ck_tile::index_t b = 0; b < batch && lut_fails < MAXSHOW; ++b) - { - for(ck_tile::index_t h = 0; h < nhead && lut_fails < MAXSHOW; ++h) + bm_pass = (bm_mismatch == 0); + float bm_ratio = bm_total ? 100.0f * float(bm_mismatch) / float(bm_total) : 0.0f; + std::cout << "\n [block_map cross-check] mismatch=" << bm_mismatch << "/" << bm_total + << " (" << std::setprecision(4) << bm_ratio << "%) " + << (bm_pass ? "PASS" : "FAIL"); + + auto cpu_lut = sparge::block_map_to_vsa_lut_delta(block_map_cpu); + size_t lut_fails = 0; + for(ck_tile::index_t b = 0; b < batch && lut_fails < MAXSHOW; ++b) { - for(ck_tile::index_t qb = 0; qb < num_q_blocks && lut_fails < MAXSHOW; ++qb) + for(ck_tile::index_t h = 0; h < nhead && lut_fails < MAXSHOW; ++h) { - int32_t valid = cpu_lut.valid_block_num(b, h, qb); - int32_t active_count = 0; - for(ck_tile::index_t kb = 0; kb < num_k_blocks; ++kb) - if(block_map_cpu(b, h, qb, kb)) - ++active_count; - int32_t recon_kb = 0; - bool delta_ok = true; - for(int32_t i = 0; i < valid; ++i) + for(ck_tile::index_t qb = 0; qb < num_q_blocks && lut_fails < MAXSHOW; ++qb) { - int32_t d = cpu_lut.lut(b, h, qb, i); - if(d < 0) + int32_t valid = cpu_lut.valid_block_num(b, h, qb); + int32_t active_count = 0; + for(ck_tile::index_t kb = 0; kb < num_k_blocks; ++kb) + if(block_map_cpu(b, h, qb, kb)) + ++active_count; + int32_t recon_kb = 0; + bool delta_ok = true; + for(int32_t i = 0; i < valid; ++i) { - delta_ok = false; - break; + int32_t d = cpu_lut.lut(b, h, qb, i); + if(d < 0) + { + delta_ok = false; + break; + } + recon_kb += d; + if(recon_kb >= num_k_blocks) + { + delta_ok = false; + break; + } + if(!block_map_cpu(b, h, qb, recon_kb)) + { + delta_ok = false; + break; + } } - recon_kb += d; - if(recon_kb >= num_k_blocks) + if(valid != active_count || !delta_ok) { - delta_ok = false; - break; + lut_pass = false; + if(lut_fails < MAXSHOW) + std::cout << "\n lut_fail (b=" << b << ",h=" << h << ",qb=" << qb + << ") valid=" << valid << " active=" << active_count + << " delta_ok=" << delta_ok; + ++lut_fails; } - if(!block_map_cpu(b, h, qb, recon_kb)) - { - delta_ok = false; - break; - } - } - if(valid != active_count || !delta_ok) - { - lut_pass = false; - if(lut_fails < MAXSHOW) - std::cout << "\n lut_fail (b=" << b << ",h=" << h << ",qb=" << qb - << ") valid=" << valid << " active=" << active_count - << " delta_ok=" << delta_ok; - ++lut_fails; } } } + std::cout << "\n [VSA LUT self-consistency] " << (lut_pass ? "PASS" : "FAIL"); + } // end if(!skip_cpu_bm_check) + else + { + std::cout << "\n [block_map cross-check] SKIPPED (mask/sink active; CPU ref lacks)"; + std::cout << "\n [VSA LUT self-consistency] SKIPPED"; } - std::cout << "\n [VSA LUT self-consistency] " << (lut_pass ? "PASS" : "FAIL"); ck_tile::HostTensor output_ref({batch, nhead, seqlen_q, hdim_v}); ck_tile::reference_blocked_attention( diff --git a/include/ck_tile/ops/sparse_attn/kernel/sparge_blockmap_kernel.hpp b/include/ck_tile/ops/sparse_attn/kernel/sparge_blockmap_kernel.hpp index 9006ee76966..bb1cdbfec46 100644 --- a/include/ck_tile/ops/sparse_attn/kernel/sparge_blockmap_kernel.hpp +++ b/include/ck_tile/ops/sparse_attn/kernel/sparge_blockmap_kernel.hpp @@ -65,6 +65,12 @@ struct SpargeBlockMapKernel // Per-head cdfthreshd (size = nhead_q floats). Required (non-null); // only consulted on topk<=0 path. const float* cdfthreshd_per_head; + + // R32 Items 2+3. mask_type stored as index_t (not mask_enum) to keep this + // include-tree header independent of example/01_fmha/mask.hpp. Magic + // constant 1 == mask_enum::mask_top_left (01_fmha/mask.hpp:13-19). + index_t mask_type; + bool attention_sink; }; CK_TILE_HOST static constexpr auto MakeKargs(const void* q_ptr, @@ -90,7 +96,9 @@ struct SpargeBlockMapKernel const void* pooled_k_ws_ptr, const void* sim_k_ws_ptr, const float* topk_per_head, - const float* cdfthreshd_per_head) + const float* cdfthreshd_per_head, + index_t mask_type, + bool attention_sink) { const index_t N_k = integer_divide_ceil(seqlen_k, kN0); return Kargs{q_ptr, @@ -117,7 +125,9 @@ struct SpargeBlockMapKernel sim_k_ws_ptr, N_k, topk_per_head, - cdfthreshd_per_head}; + cdfthreshd_per_head, + mask_type, + attention_sink}; } CK_TILE_HOST static constexpr auto GridSize(index_t batch, index_t nhead_q, index_t seqlen_q) @@ -220,7 +230,9 @@ struct SpargeBlockMapKernel valid_out, pooled_k_ws, sim_k_ws, - static_cast(smem)); + static_cast(smem), + kargs.mask_type, + kargs.attention_sink); } }; diff --git a/include/ck_tile/ops/sparse_attn/pipeline/sparge_blockmap_pipeline.hpp b/include/ck_tile/ops/sparse_attn/pipeline/sparge_blockmap_pipeline.hpp index e04c6e2d4f8..176063cee1d 100644 --- a/include/ck_tile/ops/sparse_attn/pipeline/sparge_blockmap_pipeline.hpp +++ b/include/ck_tile/ops/sparse_attn/pipeline/sparge_blockmap_pipeline.hpp @@ -263,10 +263,16 @@ struct SpargeBlockMapPipeline int32_t* valid_block_num_ptr, const KDataType* __restrict__ pooled_k_ws_ptr, const uint8_t* __restrict__ sim_k_ws_ptr, - void* smem_ptr) const + void* smem_ptr, + index_t mask_type, + bool attention_sink) const { const index_t tid = static_cast(threadIdx.x); + // mask_enum::mask_top_left == 1 (01_fmha/mask.hpp:16). Multiplicative + // form handles BLKQ=64,BLKK=128 (kM0=kN0 case. + const bool is_causal_tl = (mask_type == 1); + // K-loop no longer reduces; only Q-stats uses smem_float0. // smem_float1 slab is allocated for layout compat but unused. auto* smem_float0 = reinterpret_cast(smem_ptr); @@ -320,13 +326,23 @@ struct SpargeBlockMapPipeline // Not similar → force all K blocks ON, early exit if(!sim_q) { + // R32 Item 2: only fill causal-valid prefix when active. + const index_t causal_kb_end = + is_causal_tl ? min(N_k, integer_divide_ceil((qb + 1) * kM0, kN0)) : N_k; + for(index_t i = tid; i < N_k; i += kBlockSize) - block_map_ptr[i] = 1; + block_map_ptr[i] = (i < causal_kb_end) ? 1 : 0; + + // R32 Item 3: sink force. Under top-left causal, kb=0 always + // causal-valid for qb>=0 -> no-op; meaningful for mask=no + sink=1. + if(attention_sink && tid == 0) + block_map_ptr[0] = 1; + __syncthreads(); // sink visible to LUT-build below if(lut_ptr != nullptr && tid == 0) { int32_t valid = 0, prev = 0; - for(index_t kb = 0; kb < N_k; ++kb) + for(index_t kb = 0; kb < causal_kb_end; ++kb) { lut_ptr[valid] = static_cast(kb) - prev; prev = static_cast(kb); @@ -354,6 +370,10 @@ struct SpargeBlockMapPipeline for(index_t kb = 0; kb < N_k; ++kb) { + // R32 Item 2: top-left causal at block grain. + // (qb,kb) past-diagonal iff kb*kN0 >= (qb+1)*kM0. + const bool causal_killed = is_causal_tl && (kb * kN0 >= (qb + 1) * kM0); + const KDataType* p_kb = pooled_k_ws_ptr + kb * D + k_idx_kb * KPerThread; float pooled_k_mean[KPerThread]; for(index_t k = 0; k < KPerThread; ++k) @@ -372,17 +392,17 @@ struct SpargeBlockMapPipeline // ~sim_k blocks are forced ON in the bitmap (final_map[~sim_k]=1) // AND have score = -inf so the selection step (topk / cdf) does NOT // pick them again (would double-count toward topk budget). - // Both writes MUST stay together. Any selection rewrite - // (e.g. iterative argmax -> bitonic sort) must keep the -inf write. - if(!sim_k) + // R32: causal_killed gates the force-on so past-diagonal blocks are + // NOT forced ON; bmap stays 0, scores -inf so selection excludes them. + if(causal_killed) + smem_scores[kb] = -numeric::infinity(); // bmap stays 0 + else if(!sim_k) { smem_bmap[kb] = 1; smem_scores[kb] = -numeric::infinity(); } else - { smem_scores[kb] = dot * scale; - } } } __syncthreads(); // guard selection's reads of smem_bmap / smem_scores @@ -517,6 +537,15 @@ struct SpargeBlockMapPipeline // ================================================================== // Write outputs to global memory // ================================================================== + // R32 Item 3: force smem_bmap[0]=1 BEFORE LUT collation reads it. + // Reuses existing LUT-build loop (R31 §4: don't manually insert into + // delta stream). Causal post-multiply unnecessary: D.2 sets killed + // scores to -inf; selection gate L490 `bv > 0` excludes them, so + // smem_bmap[bi]=1 never fires for killed blocks. + if(attention_sink && tid == 0) + smem_bmap[0] = 1; + __syncthreads(); + for(index_t i = tid; i < N_k; i += kBlockSize) block_map_ptr[i] = smem_bmap[i];