#include #include #include #include #include #include #include #include #include namespace { struct FusedRopeParams { void* __restrict__ q_ptr; void* __restrict__ k_ptr; // NOTE: this k is pre-offset in host code to reduce computation in kernel const void* __restrict__ cos_sin_cache_ptr; const void* __restrict__ positions; int64_t q_stride_bytes; int64_t k_stride_bytes; int64_t head_stride_bytes; uint32_t num_qo_heads; uint32_t num_kv_heads; uint32_t num_tokens; }; struct FusedRopeStoreParams { FusedRopeParams base_params; void* v_ptr; void* __restrict__ k_cache; void* __restrict__ v_cache; const void* __restrict__ out_loc; int64_t v_stride_bytes; int64_t cache_stride_bytes; }; constexpr uint32_t kBlockSize = 128; [[maybe_unused]] constexpr auto next_pow2(uint32_t target, uint32_t factor = 1) { uint32_t power = 1; while (power * factor < target) power *= 2; return power; } template __global__ void fused_rope_kernel(const __grid_constant__ FusedRopeParams params) { using namespace device; constexpr int64_t kCosSinStrideBytes = kRopeDim * sizeof(float); constexpr int64_t kVecSize = next_pow2(kRopeDim, (2 * kWorkThreads * (1 + kIsNeox))); using DType2 = packed_t; using InputStorage = AlignedVector; constexpr int64_t kDimPerThread = kVecSize * 2 * (1 + kIsNeox); constexpr uint32_t kLaneCount = kRopeDim / kDimPerThread; static_assert(kRopeDim % kDimPerThread == 0 && kLaneCount <= kWorkThreads); const auto &[ q, k, cos_sin_cache_ptr, positions, // pointers q_stride_bytes, k_stride_bytes, head_stride_bytes, // strides num_qo_heads, num_kv_heads, num_tokens // dimensions ] = params; const auto num_blks = gridDim.x; constexpr auto kWorkersPerBlock = kBlockSize / kWorkThreads; const auto num_workers = num_blks * kWorkersPerBlock; const auto num_q_and_k_heads = num_qo_heads + num_kv_heads; const auto num_works = num_q_and_k_heads * num_tokens; const auto start_worker_id = (blockIdx.x * kBlockSize + threadIdx.x) / kWorkThreads; const auto cos_cache_ptr = cos_sin_cache_ptr; const auto sin_cache_ptr = pointer::offset(cos_sin_cache_ptr, kCosSinStrideBytes / 2); uint32_t lane_id = threadIdx.x % kWorkThreads; if constexpr (kLaneCount < kWorkThreads) { if (lane_id >= kLaneCount) return; } PDLWaitPrimary(); for (auto idx = start_worker_id; idx < num_works; idx += num_workers) { const int64_t token_id = idx / num_q_and_k_heads; const int64_t head_id = idx % num_q_and_k_heads; const auto pos = static_cast(positions)[token_id]; const auto load_q = head_id < num_qo_heads; const auto input_ = load_q ? pointer::offset(q, token_id * q_stride_bytes) // : pointer::offset(k, token_id * k_stride_bytes); const auto input = pointer::offset(input_, head_id * head_stride_bytes); const auto cos_ptr = pointer::offset(cos_cache_ptr, pos * kCosSinStrideBytes); const auto sin_ptr = pointer::offset(sin_cache_ptr, pos * kCosSinStrideBytes); if constexpr (kIsNeox) { using CacheStorage = AlignedVector; const auto input_x = input; const auto input_y = pointer::offset(input, (kRopeDim / 2) * sizeof(DType)); auto input_vec_x = load_as(input_x, lane_id); auto input_vec_y = load_as(input_y, lane_id); const auto cos_pair = load_as(cos_ptr, lane_id); const auto sin_pair = load_as(sin_ptr, lane_id); #pragma unroll for (int64_t j = 0; j < kVecSize; ++j) { const auto [x0, x1] = cast(input_vec_x[j]); const auto [y0, y1] = cast(input_vec_y[j]); const auto [cos_0, cos_1] = cos_pair[j]; const auto [sin_0, sin_1] = sin_pair[j]; const auto out_x0 = x0 * cos_0 - y0 * sin_0; const auto out_y0 = x0 * sin_0 + y0 * cos_0; const auto out_x1 = x1 * cos_1 - y1 * sin_1; const auto out_y1 = x1 * sin_1 + y1 * cos_1; input_vec_x[j] = cast({out_x0, out_x1}); input_vec_y[j] = cast({out_y0, out_y1}); } store_as(input_x, input_vec_x, lane_id); store_as(input_y, input_vec_y, lane_id); } else { using CacheStorage = AlignedVector; auto input_vec = load_as(input, lane_id); const auto cos_vec = load_as(cos_ptr, lane_id); const auto sin_vec = load_as(sin_ptr, lane_id); #pragma unroll for (int64_t j = 0; j < kVecSize; ++j) { const auto [x, y] = cast(input_vec[j]); const auto cos = cos_vec[j]; const auto sin = sin_vec[j]; const auto out_x = x * cos - y * sin; const auto out_y = x * sin + y * cos; input_vec[j] = cast({out_x, out_y}); } store_as(input, input_vec, lane_id); } } PDLTriggerSecondary(); } template __global__ void fused_rope_store_kernel(const __grid_constant__ FusedRopeStoreParams params) { using namespace device; constexpr int64_t kCosSinStrideBytes = kRopeDim * sizeof(float); constexpr int64_t kVecSize = kRopeDim / (2 * kWorkThreads * (1 + kIsNeox)); using DType2 = packed_t; using InputStorage = AlignedVector; constexpr int64_t kDimPerThread = kVecSize * 2 * (1 + kIsNeox); static_assert(kRopeDim == kDimPerThread * kWorkThreads); const auto& [base_params, v_ptr, k_cache, v_cache, out_loc, v_stride_bytes, cache_stride_bytes] = params; const auto &[ q, k, cos_sin_cache_ptr, positions, // pointers q_stride_bytes, k_stride_bytes, head_stride_bytes, // strides num_qo_heads, num_kv_heads, num_tokens // dimensions ] = base_params; const auto num_blks = gridDim.x; constexpr auto kWorkersPerBlock = kBlockSize / kWorkThreads; const auto num_workers = num_blks * kWorkersPerBlock; const auto num_q_and_k_heads = num_qo_heads + num_kv_heads; const auto num_works = num_q_and_k_heads * num_tokens; const auto num_extra_works = num_kv_heads * num_tokens; // rope works + v store works const auto start_worker_id = (blockIdx.x * kBlockSize + threadIdx.x) / kWorkThreads; const auto lane_id = threadIdx.x % kWorkThreads; const auto cos_cache_ptr = cos_sin_cache_ptr; const auto sin_cache_ptr = pointer::offset(cos_sin_cache_ptr, kCosSinStrideBytes / 2); auto idx = start_worker_id; PDLWaitPrimary(); // in this case, head_dim = rope_dim must be true __builtin_assume(head_stride_bytes == kRopeDim * sizeof(DType)); for (; idx < num_works; idx += num_workers) { const int64_t token_id = idx / num_q_and_k_heads; const int64_t head_id = idx % num_q_and_k_heads; const auto pos = static_cast(positions)[token_id]; const auto loc = static_cast(out_loc)[token_id]; const auto load_q = head_id < num_qo_heads; const auto input_ = load_q ? pointer::offset(q, token_id * q_stride_bytes) // : pointer::offset(k, token_id * k_stride_bytes); const auto input = pointer::offset(input_, head_id * head_stride_bytes); const auto cos_ptr = pointer::offset(cos_cache_ptr, pos * kCosSinStrideBytes); const auto sin_ptr = pointer::offset(sin_cache_ptr, pos * kCosSinStrideBytes); if constexpr (kIsNeox) { using CacheStorage = AlignedVector; const auto input_x = input; const auto input_y = pointer::offset(input, (kRopeDim / 2) * sizeof(DType)); auto input_vec_x = load_as(input_x, lane_id); auto input_vec_y = load_as(input_y, lane_id); const auto cos_pair = load_as(cos_ptr, lane_id); const auto sin_pair = load_as(sin_ptr, lane_id); #pragma unroll for (int64_t j = 0; j < kVecSize; ++j) { const auto [x0, x1] = cast(input_vec_x[j]); const auto [y0, y1] = cast(input_vec_y[j]); const auto [cos_0, cos_1] = cos_pair[j]; const auto [sin_0, sin_1] = sin_pair[j]; const auto out_x0 = x0 * cos_0 - y0 * sin_0; const auto out_y0 = x0 * sin_0 + y0 * cos_0; const auto out_x1 = x1 * cos_1 - y1 * sin_1; const auto out_y1 = x1 * sin_1 + y1 * cos_1; input_vec_x[j] = cast({out_x0, out_x1}); input_vec_y[j] = cast({out_y0, out_y1}); } store_as(input, input_vec_x, lane_id); const auto input_y_out = pointer::offset(input, (kRopeDim / 2) * sizeof(DType)); store_as(input_y_out, input_vec_y, lane_id); if (!load_q) { const auto k_out = pointer::offset(k_cache, loc * cache_stride_bytes, head_id * head_stride_bytes); store_as(k_out, input_vec_x, lane_id); const auto k_out_y = pointer::offset(k_out, (kRopeDim / 2) * sizeof(DType)); store_as(k_out_y, input_vec_y, lane_id); } } else { using CacheStorage = AlignedVector; auto input_vec = load_as(input, lane_id); const auto cos_vec = load_as(cos_ptr, lane_id); const auto sin_vec = load_as(sin_ptr, lane_id); #pragma unroll for (int64_t j = 0; j < kVecSize; ++j) { const auto [x, y] = cast(input_vec[j]); const auto cos = cos_vec[j]; const auto sin = sin_vec[j]; const auto out_x = x * cos - y * sin; const auto out_y = x * sin + y * cos; input_vec[j] = cast({out_x, out_y}); } store_as(input, input_vec, lane_id); if (!load_q) { const auto k_out = pointer::offset(k_cache, loc * cache_stride_bytes, head_id * head_stride_bytes); store_as(k_out, input_vec, lane_id); } } } __syncwarp(); // to avoid warp divergence idx -= num_works; for (; idx < num_extra_works; idx += num_workers) { using VStorage = AlignedVector; const int64_t token_id = idx / num_kv_heads; const int64_t head_id = idx % num_kv_heads; const auto loc = static_cast(out_loc)[token_id]; const auto input = pointer::offset(v_ptr, token_id * v_stride_bytes, head_id * head_stride_bytes); const auto input_vec = load_as(input, lane_id); const auto output = pointer::offset(v_cache, loc * cache_stride_bytes, head_id * head_stride_bytes); store_as(output, input_vec, lane_id); } PDLTriggerSecondary(); } template struct FusedRopeKernel { static constexpr uint32_t kDimPerThread = std::gcd(16 / sizeof(DType), kRopeDim); static constexpr uint32_t kWorkThreads = next_pow2(kRopeDim, kDimPerThread); static constexpr bool kSupportFused = kWorkThreads * kDimPerThread == kRopeDim; static_assert(kRopeDim % kDimPerThread == 0); static_assert(kBlockSize % kWorkThreads == 0); template static constexpr auto _kernel_0 = fused_rope_kernel; template static constexpr auto _kernel_1 = fused_rope_store_kernel; static auto get_num_sm(DLDevice device) { static const auto kNumSM = host::runtime::get_sm_count(device.device_id); return kNumSM; } static void run(const tvm::ffi::TensorView q, const tvm::ffi::TensorView k, const tvm::ffi::TensorView cos_sin_cache, const tvm::ffi::TensorView positions) { using namespace host; auto N = SymbolicSize{"num_tokens"}; auto Q = SymbolicSize{"num_qo_heads"}; auto K = SymbolicSize{"num_kv_heads"}; auto D = SymbolicSize{"rope_dim"}; auto Dq = SymbolicSize{"q_stride"}; auto Dk = SymbolicSize{"k_stride"}; auto Dd = SymbolicSize{"head_stride"}; auto device = SymbolicDevice{}; auto id_type = SymbolicDType{}; D.set_value(kRopeDim); device.set_options(); TensorMatcher({N, Q, D}) // q input .with_strides({Dq, Dd, 1}) .with_dtype() .with_device(device) .verify(q); TensorMatcher({N, K, D}) // k input .with_strides({Dk, Dd, 1}) .with_dtype() .with_device(device) .verify(k); TensorMatcher({-1, D}) // cos_sin_cache .with_dtype() .with_device(device) .verify(cos_sin_cache); TensorMatcher({N}) // positions .with_dtype(id_type) .with_device(device) .verify(positions); const auto num_tokens = static_cast(N.unwrap()); const auto num_qo_heads = static_cast(Q.unwrap()); const auto num_kv_heads = static_cast(K.unwrap()); const auto q_stride_bytes = static_cast(Dq.unwrap() * sizeof(DType)); const auto k_stride_bytes = static_cast(Dk.unwrap() * sizeof(DType)); const auto head_stride_bytes = static_cast(Dd.unwrap() * sizeof(DType)); // NOTE: we offset the k here to reduce computation cost in the kernel const int64_t k_offset = static_cast(num_qo_heads) * head_stride_bytes; const auto params = FusedRopeParams{ .q_ptr = q.data_ptr(), .k_ptr = pointer::offset(k.data_ptr(), -k_offset), .cos_sin_cache_ptr = cos_sin_cache.data_ptr(), .positions = positions.data_ptr(), .q_stride_bytes = q_stride_bytes, .k_stride_bytes = k_stride_bytes, .head_stride_bytes = head_stride_bytes, .num_qo_heads = num_qo_heads, .num_kv_heads = num_kv_heads, .num_tokens = num_tokens, }; const auto is_int32 = id_type.is_type(); const auto kernel = is_int32 ? _kernel_0 : _kernel_0; const uint32_t kNumSM = get_num_sm(device.unwrap()); static const uint32_t kOccupancyTable[2] = { runtime::get_blocks_per_sm(_kernel_0, kBlockSize), runtime::get_blocks_per_sm(_kernel_0, kBlockSize), }; const auto max_blocks = kOccupancyTable[is_int32 ? 0 : 1] * kNumSM; const auto num_works = (num_qo_heads + num_kv_heads) * num_tokens; const auto needed_blocks = div_ceil(num_works, (kBlockSize / kWorkThreads)); const auto num_blocks = std::min(max_blocks, needed_blocks); LaunchKernel(num_blocks, kBlockSize, device.unwrap()) // .enable_pdl(kUsePDL)(kernel, params); } static void run_fused( const tvm::ffi::TensorView q, const tvm::ffi::TensorView k, const tvm::ffi::TensorView v, const tvm::ffi::TensorView k_cache, const tvm::ffi::TensorView v_cache, const tvm::ffi::TensorView cos_sin_cache, const tvm::ffi::TensorView positions, const tvm::ffi::TensorView out_loc) { if constexpr (kSupportFused) { return _run_fused_impl(q, k, v, k_cache, v_cache, cos_sin_cache, positions, out_loc); } else { host::Panic("Fused rope + store is not supported for rope_dim ", kRopeDim); } } static void _run_fused_impl( const tvm::ffi::TensorView q, const tvm::ffi::TensorView k, const tvm::ffi::TensorView v, const tvm::ffi::TensorView k_cache, const tvm::ffi::TensorView v_cache, const tvm::ffi::TensorView cos_sin_cache, const tvm::ffi::TensorView positions, const tvm::ffi::TensorView out_loc) { using namespace host; auto N = SymbolicSize{"num_tokens"}; auto Q = SymbolicSize{"num_qo_heads"}; auto K = SymbolicSize{"num_kv_heads"}; auto D = SymbolicSize{"rope_dim"}; auto R = SymbolicSize{"row_size"}; auto Dq = SymbolicSize{"q_stride"}; auto Dk = SymbolicSize{"k_stride"}; auto Dv = SymbolicSize{"v_stride"}; auto Dd = SymbolicSize{"head_stride"}; auto Dc = SymbolicSize{"cache_stride"}; auto device = SymbolicDevice{}; auto id_type = SymbolicDType{}; D.set_value(kRopeDim); device.set_options(); TensorMatcher({N, Q, D}) // q input .with_strides({Dq, Dd, 1}) .with_dtype() .with_device(device) .verify(q); TensorMatcher({N, K, D}) // k input .with_strides({Dk, Dd, 1}) .with_dtype() .with_device(device) .verify(k); TensorMatcher({N, K, D}) // v input .with_strides({Dv, Dd, 1}) .with_dtype() .with_device(device) .verify(v); TensorMatcher({-1, D}) // cos_sin_cache .with_dtype() .with_device(device) .verify(cos_sin_cache); TensorMatcher({N}) // positions, out_loc .with_dtype(id_type) .with_device(device) .verify(positions) .verify(out_loc); TensorMatcher({-1, R}) // k_cache .with_strides({Dc, 1}) .with_dtype() .with_device(device) .verify(k_cache) .verify(v_cache); const auto num_tokens = static_cast(N.unwrap()); const auto num_qo_heads = static_cast(Q.unwrap()); const auto num_kv_heads = static_cast(K.unwrap()); const auto q_stride_bytes = static_cast(Dq.unwrap() * sizeof(DType)); const auto k_stride_bytes = static_cast(Dk.unwrap() * sizeof(DType)); const auto head_stride = Dd.unwrap(); const auto row_dim = R.unwrap(); const auto head_stride_bytes = static_cast(Dd.unwrap() * sizeof(DType)); RuntimeCheck(kRopeDim == head_stride, "rope_dim ", kRopeDim, " should = head_stride ", head_stride); RuntimeCheck(num_kv_heads * kRopeDim == row_dim, "invalid kvcache"); // NOTE: we offset the k here to reduce computation cost in the kernel const int64_t k_offset = static_cast(num_qo_heads) * head_stride_bytes; const auto params = FusedRopeParams{ .q_ptr = q.data_ptr(), .k_ptr = pointer::offset(k.data_ptr(), -k_offset), .cos_sin_cache_ptr = cos_sin_cache.data_ptr(), .positions = positions.data_ptr(), .q_stride_bytes = q_stride_bytes, .k_stride_bytes = k_stride_bytes, .head_stride_bytes = head_stride_bytes, .num_qo_heads = num_qo_heads, .num_kv_heads = num_kv_heads, .num_tokens = num_tokens, }; const auto v_stride_bytes = static_cast(Dv.unwrap() * sizeof(DType)); const auto cache_stride_bytes = static_cast(Dc.unwrap() * sizeof(DType)); const auto store_params = FusedRopeStoreParams{ .base_params = params, .v_ptr = v.data_ptr(), .k_cache = pointer::offset(k_cache.data_ptr(), -k_offset), .v_cache = v_cache.data_ptr(), .out_loc = out_loc.data_ptr(), .v_stride_bytes = v_stride_bytes, .cache_stride_bytes = cache_stride_bytes, }; const auto is_int32 = id_type.is_type(); const auto kernel = is_int32 ? _kernel_1 : _kernel_1; const uint32_t kNumSM = get_num_sm(device.unwrap()); static const uint32_t kOccupancyTable[2] = { runtime::get_blocks_per_sm(_kernel_1, kBlockSize), runtime::get_blocks_per_sm(_kernel_1, kBlockSize), }; const auto max_blocks = kOccupancyTable[is_int32 ? 0 : 1] * kNumSM; // rope works for q+k heads, plus v store works for kv heads const auto num_total_works = (num_qo_heads + 2 * num_kv_heads) * num_tokens; const auto needed_blocks = div_ceil(num_total_works, (kBlockSize / kWorkThreads)); const auto num_blocks = std::min(max_blocks, needed_blocks); LaunchKernel(num_blocks, kBlockSize, device.unwrap()) // .enable_pdl(kUsePDL)(kernel, store_params); } }; } // namespace