Files
lmcache--lmcache/csrc/sycl/mem_kernels_sycl.cpp
T
2026-07-13 12:24:33 +08:00

1024 lines
44 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// SPDX-License-Identifier: Apache-2.0
//
// SYCL implementation of LMCache memory kernels for Intel XPU
// (PVC / Arc / Battlemage).
//
// Performance-critical design choices:
//
// 1. Work-group size 256 -- keeps Intel EU ALUs fed and hides
// global-memory latency.
// 2. [[sycl::reqd_sub_group_size(16)]] -- native SIMD width on all
// Intel discrete GPUs; prevents IGC from falling back to width 32
// on PVC (which would halve occupancy).
// 3. Compile-time template parameters (DIRECTION, USE_MLA) eliminate
// run-time branches in the innermost loop.
// 4. Hoisted loop-invariant base offsets -- integer division/modulo
// (flash_infer block indexing) is computed once per token+layer
// rather than per inner-loop iteration.
// 5. 64-bit (int64_t) bulk transfers pack two fp32 / four fp16 /
// eight int8 values per move.
// 6. Fused K+V work-groups (non-MLA multi-layer kernels) -- one
// work-group handles both K and V, halving dispatch count and
// avoiding redundant slot/pointer/division work.
// sycl/accessor.hpp references the deprecated 'host_buffer' internally
// even when user code only uses USM pointers; suppress the noise.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
#include <sycl/sycl.hpp>
#pragma GCC diagnostic pop
#include <torch/all.h>
#include <ATen/ATen.h>
#include <c10/core/DeviceGuard.h>
#include <c10/xpu/XPUStream.h>
#include "mem_kernels_sycl.h"
#include <algorithm>
#include <cstdint>
#include <stdexcept>
#include <string>
// ---------------------------------------------------------------------------
// Tuning constants
// ---------------------------------------------------------------------------
// Native SIMD width on PVC, DG2, and BMG.
constexpr int INTEL_SUB_GROUP_SIZE = 16;
// Max work-group size; must be a multiple of INTEL_SUB_GROUP_SIZE.
constexpr int MAX_WG_SIZE = 256;
// ---------------------------------------------------------------------------
// Round up so the work-group divides evenly into sub-groups.
// ---------------------------------------------------------------------------
inline int round_up_to_sg(int n) {
return ((n + INTEL_SUB_GROUP_SIZE - 1) / INTEL_SUB_GROUP_SIZE) *
INTEL_SUB_GROUP_SIZE;
}
// ---------------------------------------------------------------------------
// Namespace lmc -- device-side helper functions
// ---------------------------------------------------------------------------
namespace lmc {
template <EngineKVFormat format>
inline int64_t page_buffer_offset(const int k_or_v, const int token_idx,
const int scalar_offset,
const int scalars_per_token,
const int page_buffer_size,
const int block_size) {
// vLLM cross layer
if constexpr (format == EngineKVFormat::NB_NL_TWO_BS_NH_HS) {
return k_or_v * page_buffer_size * scalars_per_token +
token_idx * scalars_per_token + scalar_offset;
}
// vLLM flash attention
else if constexpr (format == EngineKVFormat::NL_X_TWO_NB_BS_NH_HS) {
return k_or_v * page_buffer_size * scalars_per_token +
token_idx * scalars_per_token + scalar_offset;
}
// vLLM flash infer
else if constexpr (format == EngineKVFormat::NL_X_NB_TWO_BS_NH_HS) {
const int block_idx = token_idx / block_size;
const int block_offset = token_idx % block_size;
return block_idx * 2 * block_size * scalars_per_token +
k_or_v * block_size * scalars_per_token +
block_offset * scalars_per_token + scalar_offset;
}
// MLA formats: vLLM (NL_X_NB_BS_HS) and SGLang (NL_X_NBBS_ONE_HS)
else if constexpr (format == EngineKVFormat::NL_X_NB_BS_HS ||
format == EngineKVFormat::NL_X_NBBS_ONE_HS) {
return token_idx * scalars_per_token + scalar_offset;
}
}
/// Loop-invariant base offset for the paged buffer.
/// page_buffer_offset(k_or_v, slot, i, ...) == base_offset(...) + i.
/// Hoisting this out of the inner loop avoids re-computing the
/// integer division / modulo (flash_infer) on every iteration.
template <EngineKVFormat format>
inline int64_t page_buffer_base_offset(const int k_or_v, const int token_idx,
const int scalars_per_token,
const int page_buffer_size,
const int block_size) {
if constexpr (format == EngineKVFormat::NB_NL_TWO_BS_NH_HS) {
return k_or_v * page_buffer_size * scalars_per_token +
token_idx * scalars_per_token;
} else if constexpr (format == EngineKVFormat::NL_X_TWO_NB_BS_NH_HS) {
return k_or_v * page_buffer_size * scalars_per_token +
token_idx * scalars_per_token;
} else if constexpr (format == EngineKVFormat::NL_X_NB_TWO_BS_NH_HS) {
const int block_idx = token_idx / block_size;
const int block_offset = token_idx % block_size;
return block_idx * 2 * block_size * scalars_per_token +
k_or_v * block_size * scalars_per_token +
block_offset * scalars_per_token;
} else if constexpr (format == EngineKVFormat::NL_X_NB_BS_HS ||
format == EngineKVFormat::NL_X_NBBS_ONE_HS) {
return token_idx * scalars_per_token;
}
}
inline int64_t page_buffer_offset_unilateral(const int token_idx,
const int scalar_offset,
const int scalars_per_token) {
return token_idx * scalars_per_token + scalar_offset;
}
inline int64_t key_value_offset(const int k_or_v, const int layer_idx,
const int token_idx, const int scalar_offset,
const int scalars_per_token,
const int num_tokens, const int num_layers) {
return k_or_v * num_layers * num_tokens * scalars_per_token +
layer_idx * num_tokens * scalars_per_token +
token_idx * scalars_per_token + scalar_offset;
}
/// Loop-invariant base offset for the LMCache key_value buffer.
/// key_value_offset(k_or_v, layer, token, i, ...) == base(...) + i.
inline int64_t key_value_base_offset(const int k_or_v, const int layer_idx,
const int token_idx,
const int scalars_per_token,
const int num_tokens,
const int num_layers) {
return k_or_v * num_layers * num_tokens * scalars_per_token +
layer_idx * num_tokens * scalars_per_token +
token_idx * scalars_per_token;
}
} // namespace lmc
// ---------------------------------------------------------------------------
// Pointer helper -- returns a kernel-accessible pointer of the given type.
// XPU tensors expose their USM device pointer; CPU tensors must be backed
// by USM host memory (e.g. sycl::malloc_host) to be device-accessible.
// ---------------------------------------------------------------------------
template <typename T, typename TENSOR_TYPE>
T* get_kernel_ptr(TENSOR_TYPE& tensor) {
torch::Device device = tensor.device();
if (device.is_xpu()) {
return static_cast<T*>(tensor.data_ptr());
} else if (device.is_cpu()) {
// USM host pointers are device-accessible.
return static_cast<T*>(tensor.data_ptr());
} else {
TORCH_CHECK(false,
"Invalid device. Device must be xpu or cpu (USM pinned).");
}
}
// ---------------------------------------------------------------------------
// Kernel-launch helpers -- multi-layer kernels
// ---------------------------------------------------------------------------
/**
* Submit the multi-layer KV transfer kernel for MLA formats
* (k_or_v_size == 1).
*
* nd_range layout: group(0)=k_or_v, group(1)=layer, group(2)=token;
* local_id(2)=tid, local_range(2)=num_threads.
*
* Optimizations:
* - DIRECTION is a compile-time bool (no branch in hot loop)
* - Loop-invariant base offsets (including flash_infer's integer
* division) are computed once before the inner loop
* - Work-group size rounded to a sub-group multiple for full SIMD
* utilisation
*/
template <typename scalar_t, bool DIRECTION, EngineKVFormat format>
void submit_multi_layer_kernel(sycl::queue& queue, scalar_t* key_value_ptr,
scalar_t** page_buffer_ptrs,
const int64_t* slot_mapping_ptr,
int scalars_per_token, int num_tokens,
int num_layers, int page_buffer_size,
int block_size, int skip_prefix_n_tokens,
int k_or_v_size, int wg_size) {
int num_transfer_tokens = num_tokens - skip_prefix_n_tokens;
if (num_transfer_tokens <= 0 || num_layers <= 0) return;
sycl::range<3> global_range(
static_cast<size_t>(k_or_v_size), static_cast<size_t>(num_layers),
static_cast<size_t>(num_transfer_tokens) * wg_size);
sycl::range<3> local_range(1, 1, static_cast<size_t>(wg_size));
queue.parallel_for(
sycl::nd_range<3>(global_range, local_range),
[=](sycl::nd_item<3> item) [[sycl::reqd_sub_group_size(16)]] {
const int token_id = static_cast<int>(item.get_group(2));
const int layer_id = static_cast<int>(item.get_group(1));
const int k_or_v = static_cast<int>(item.get_group(0));
const int tid = static_cast<int>(item.get_local_id(2));
const int num_threads = static_cast<int>(item.get_local_range(2));
const int kv_token_id = token_id + skip_prefix_n_tokens;
const int64_t slot_idx = slot_mapping_ptr[kv_token_id];
scalar_t* paged_buffer_ptr = page_buffer_ptrs[layer_id];
if (slot_idx < 0) return;
// Hoist loop-invariant base offsets (flash_infer's integer
// division happens here, once, not per loop iteration).
const int64_t lmc_base = lmc::key_value_base_offset(
k_or_v, layer_id, kv_token_id, scalars_per_token, num_tokens,
num_layers);
const int64_t vllm_base = lmc::page_buffer_base_offset<format>(
k_or_v, slot_idx, scalars_per_token, page_buffer_size, block_size);
for (int i = tid; i < scalars_per_token; i += num_threads) {
if constexpr (DIRECTION) {
key_value_ptr[lmc_base + i] = paged_buffer_ptr[vllm_base + i];
} else {
paged_buffer_ptr[vllm_base + i] = key_value_ptr[lmc_base + i];
}
}
});
}
/**
* Submit a fused K+V multi-layer kernel for non-MLA formats.
*
* Processes both key (k_or_v=0) and value (k_or_v=1) within the same
* work-group, halving work-group count compared to dispatching K and
* V separately. Slot mapping, pointer-array lookup, and flash_infer's
* block-index division are each performed once and reused for both.
*/
template <typename scalar_t, bool DIRECTION, EngineKVFormat format>
void submit_multi_layer_kernel_fused_kv(
sycl::queue& queue, scalar_t* key_value_ptr, scalar_t** page_buffer_ptrs,
const int64_t* slot_mapping_ptr, int scalars_per_token, int num_tokens,
int num_layers, int page_buffer_size, int block_size,
int skip_prefix_n_tokens, int wg_size) {
int num_transfer_tokens = num_tokens - skip_prefix_n_tokens;
if (num_transfer_tokens <= 0 || num_layers <= 0) return;
// Grid: (1, num_layers, num_transfer_tokens * wg_size);
// no k_or_v dimension — K and V share one work-group.
sycl::range<3> global_range(
1, static_cast<size_t>(num_layers),
static_cast<size_t>(num_transfer_tokens) * wg_size);
sycl::range<3> local_range(1, 1, static_cast<size_t>(wg_size));
queue.parallel_for(
sycl::nd_range<3>(global_range, local_range),
[=](sycl::nd_item<3> item) [[sycl::reqd_sub_group_size(16)]] {
const int token_id = static_cast<int>(item.get_group(2));
const int layer_id = static_cast<int>(item.get_group(1));
const int tid = static_cast<int>(item.get_local_id(2));
const int num_threads = static_cast<int>(item.get_local_range(2));
const int kv_token_id = token_id + skip_prefix_n_tokens;
const int64_t slot_idx = slot_mapping_ptr[kv_token_id];
scalar_t* paged_buffer_ptr = page_buffer_ptrs[layer_id];
if (slot_idx < 0) return;
// Base offsets for K (k_or_v=0) and V (k_or_v=1); the
// flash_infer division/modulo runs once per token+layer.
const int64_t lmc_base_k = lmc::key_value_base_offset(
0, layer_id, kv_token_id, scalars_per_token, num_tokens,
num_layers);
const int64_t lmc_base_v = lmc::key_value_base_offset(
1, layer_id, kv_token_id, scalars_per_token, num_tokens,
num_layers);
const int64_t vllm_base_k = lmc::page_buffer_base_offset<format>(
0, slot_idx, scalars_per_token, page_buffer_size, block_size);
const int64_t vllm_base_v = lmc::page_buffer_base_offset<format>(
1, slot_idx, scalars_per_token, page_buffer_size, block_size);
for (int i = tid; i < scalars_per_token; i += num_threads) {
if constexpr (DIRECTION) {
// paged buffer → LMCache
key_value_ptr[lmc_base_k + i] = paged_buffer_ptr[vllm_base_k + i];
key_value_ptr[lmc_base_v + i] = paged_buffer_ptr[vllm_base_v + i];
} else {
// LMCache → paged buffer
paged_buffer_ptr[vllm_base_k + i] = key_value_ptr[lmc_base_k + i];
paged_buffer_ptr[vllm_base_v + i] = key_value_ptr[lmc_base_v + i];
}
}
});
}
/**
* Submit the multi-layer unilateral kernel (SGLang MHA).
*
* DIRECTION = true → paged buffer → LMCache (D2H)
* DIRECTION = false → LMCache → paged buffer (H2D)
*
* Uses `if constexpr (DIRECTION)` so the compiler can
* dead-strip the unused branch entirely.
*/
template <typename scalar_t, bool DIRECTION>
void submit_multi_layer_unilateral_kernel(
sycl::queue& queue, scalar_t* key_value_ptr, scalar_t** page_buffer_ptrs,
const int64_t* slot_mapping_ptr, int scalars_per_token, int num_tokens,
int num_layers, int page_buffer_size, int k_or_v_size, int kv_num_tokens,
int wg_size) {
if (kv_num_tokens <= 0 || num_layers <= 0) return;
sycl::range<3> global_range(static_cast<size_t>(k_or_v_size),
static_cast<size_t>(num_layers),
static_cast<size_t>(kv_num_tokens) * wg_size);
sycl::range<3> local_range(1, 1, static_cast<size_t>(wg_size));
queue.parallel_for(
sycl::nd_range<3>(global_range, local_range),
[=](sycl::nd_item<3> item) [[sycl::reqd_sub_group_size(16)]] {
const int token_id = static_cast<int>(item.get_group(2));
const int layer_id = static_cast<int>(item.get_group(1));
const int k_or_v = static_cast<int>(item.get_group(0));
const int tid = static_cast<int>(item.get_local_id(2));
const int num_threads = static_cast<int>(item.get_local_range(2));
const int64_t slot_idx = slot_mapping_ptr[token_id];
scalar_t* key_ptr = page_buffer_ptrs[layer_id];
scalar_t* value_ptr = page_buffer_ptrs[layer_id + num_layers];
if (slot_idx < 0) return;
const int64_t lmc_base = lmc::key_value_base_offset(
k_or_v, layer_id, token_id, scalars_per_token, num_tokens,
num_layers);
const int64_t sgl_base = slot_idx * scalars_per_token;
for (int i = tid; i < scalars_per_token; i += num_threads) {
if (k_or_v == 0) {
if constexpr (DIRECTION)
key_value_ptr[lmc_base + i] = key_ptr[sgl_base + i];
else
key_ptr[sgl_base + i] = key_value_ptr[lmc_base + i];
} else {
if constexpr (DIRECTION)
key_value_ptr[lmc_base + i] = value_ptr[sgl_base + i];
else
value_ptr[sgl_base + i] = key_value_ptr[lmc_base + i];
}
}
});
}
// ---------------------------------------------------------------------------
// Macros to dispatch multi-layer kernels with a specific EngineKVFormat.
// MLA formats (k_or_v_size==1) use the per-component kernel.
// Non-MLA formats (k_or_v_size==2) use the fused K+V kernel.
// ---------------------------------------------------------------------------
#define LAUNCH_KERNEL_WITH_FORMAT(T, DIRECTION, FORMAT) \
submit_multi_layer_kernel<T, DIRECTION, FORMAT>( \
queue, key_value_ptr, page_buffer_ptrs, slot_mapping_ptr, num_xwords, \
num_tokens, num_layers, page_buffer_size, block_size, \
skip_prefix_n_tokens, k_or_v_size, wg_size);
#define LAUNCH_FUSED_KV_KERNEL_WITH_FORMAT(T, DIRECTION, FORMAT) \
submit_multi_layer_kernel_fused_kv<T, DIRECTION, FORMAT>( \
queue, key_value_ptr, page_buffer_ptrs, slot_mapping_ptr, num_xwords, \
num_tokens, num_layers, page_buffer_size, block_size, \
skip_prefix_n_tokens, wg_size);
// ---------------------------------------------------------------------------
// multi_layer_kv_transfer -- templated implementation
// ---------------------------------------------------------------------------
template <typename T>
void multi_layer_kv_transfer_templated(
torch::Tensor& key_value, const torch::Tensor& key_value_ptrs,
const torch::Tensor& slot_mapping, const torch::Device& paged_memory_device,
const int page_buffer_size, const TransferDirection direction,
const EngineKVFormat engine_kv_format, const int block_size,
const int skip_prefix_n_tokens) {
T* key_value_ptr = get_kernel_ptr<T, torch::Tensor>(key_value);
T** page_buffer_ptrs =
get_kernel_ptr<T*, const torch::Tensor>(key_value_ptrs);
const int64_t* slot_mapping_ptr =
get_kernel_ptr<const int64_t, const torch::Tensor>(slot_mapping);
int num_layers = key_value.size(1);
int num_tokens = key_value.size(2);
int num_origin_elements = key_value.size(3);
int elements_per_xword = sizeof(T) / key_value.element_size();
int num_xwords = num_origin_elements / elements_per_xword;
int k_or_v_size = ::is_mla(engine_kv_format) ? 1 : 2;
// Round up to a sub-group multiple so every sub-group is full.
int wg_size = round_up_to_sg(std::min(num_xwords, MAX_WG_SIZE));
const c10::OptionalDeviceGuard device_guard(paged_memory_device);
sycl::queue& queue =
c10::xpu::getCurrentXPUStream(paged_memory_device.index()).queue();
// Non-MLA formats use the fused K+V kernel; MLA formats
// (k_or_v_size==1) use the per-component kernel.
if (k_or_v_size == 2) {
if (direction == TransferDirection::H2D) {
switch (engine_kv_format) {
case EngineKVFormat::NB_NL_TWO_BS_NH_HS:
LAUNCH_FUSED_KV_KERNEL_WITH_FORMAT(
T, false, EngineKVFormat::NB_NL_TWO_BS_NH_HS);
break;
case EngineKVFormat::NL_X_TWO_NB_BS_NH_HS:
LAUNCH_FUSED_KV_KERNEL_WITH_FORMAT(
T, false, EngineKVFormat::NL_X_TWO_NB_BS_NH_HS);
break;
case EngineKVFormat::NL_X_NB_TWO_BS_NH_HS:
LAUNCH_FUSED_KV_KERNEL_WITH_FORMAT(
T, false, EngineKVFormat::NL_X_NB_TWO_BS_NH_HS);
break;
default:
throw std::runtime_error("Unsupported non-MLA EngineKVFormat");
}
} else {
switch (engine_kv_format) {
case EngineKVFormat::NB_NL_TWO_BS_NH_HS:
LAUNCH_FUSED_KV_KERNEL_WITH_FORMAT(
T, true, EngineKVFormat::NB_NL_TWO_BS_NH_HS);
break;
case EngineKVFormat::NL_X_TWO_NB_BS_NH_HS:
LAUNCH_FUSED_KV_KERNEL_WITH_FORMAT(
T, true, EngineKVFormat::NL_X_TWO_NB_BS_NH_HS);
break;
case EngineKVFormat::NL_X_NB_TWO_BS_NH_HS:
LAUNCH_FUSED_KV_KERNEL_WITH_FORMAT(
T, true, EngineKVFormat::NL_X_NB_TWO_BS_NH_HS);
break;
default:
throw std::runtime_error("Unsupported non-MLA EngineKVFormat");
}
}
} else {
// MLA path (k_or_v_size == 1)
if (direction == TransferDirection::H2D) {
switch (engine_kv_format) {
case EngineKVFormat::NL_X_NB_BS_HS:
LAUNCH_KERNEL_WITH_FORMAT(T, false, EngineKVFormat::NL_X_NB_BS_HS);
break;
case EngineKVFormat::NL_X_NBBS_ONE_HS:
LAUNCH_KERNEL_WITH_FORMAT(T, false, EngineKVFormat::NL_X_NBBS_ONE_HS);
break;
default:
throw std::runtime_error("Unsupported MLA EngineKVFormat");
}
} else {
switch (engine_kv_format) {
case EngineKVFormat::NL_X_NB_BS_HS:
LAUNCH_KERNEL_WITH_FORMAT(T, true, EngineKVFormat::NL_X_NB_BS_HS);
break;
case EngineKVFormat::NL_X_NBBS_ONE_HS:
LAUNCH_KERNEL_WITH_FORMAT(T, true, EngineKVFormat::NL_X_NBBS_ONE_HS);
break;
default:
throw std::runtime_error("Unsupported MLA EngineKVFormat");
}
}
}
}
#undef LAUNCH_KERNEL_WITH_FORMAT
#undef LAUNCH_FUSED_KV_KERNEL_WITH_FORMAT
// ---------------------------------------------------------------------------
// Public API: multi_layer_kv_transfer
// ---------------------------------------------------------------------------
void multi_layer_kv_transfer(
torch::Tensor& key_value, const torch::Tensor& key_value_ptrs,
const torch::Tensor& slot_mapping, const torch::Device& paged_memory_device,
const int page_buffer_size, const TransferDirection direction,
const EngineKVFormat engine_kv_format, const int block_size,
const int head_size, const int skip_prefix_n_tokens) {
// head_size is currently unused in the SYCL implementation; accepted to
// keep ABI parity with the CUDA c_ops binding so callers can pass the
// same kwargs to either backend.
(void)head_size;
int num_origin_elements = key_value.size(3);
int copy_size = num_origin_elements * key_value.element_size();
#define LAUNCH_MULTI_LAYER_KV_TRANSFER(type) \
do { \
multi_layer_kv_transfer_templated<type>( \
key_value, key_value_ptrs, slot_mapping, paged_memory_device, \
page_buffer_size, direction, engine_kv_format, block_size, \
skip_prefix_n_tokens); \
} while (0)
if (copy_size % 8 == 0) {
LAUNCH_MULTI_LAYER_KV_TRANSFER(int64_t);
} else if (copy_size % 4 == 0) {
LAUNCH_MULTI_LAYER_KV_TRANSFER(int32_t);
} else if (copy_size % 2 == 0) {
LAUNCH_MULTI_LAYER_KV_TRANSFER(int16_t);
} else {
LAUNCH_MULTI_LAYER_KV_TRANSFER(int8_t);
}
#undef LAUNCH_MULTI_LAYER_KV_TRANSFER
}
// ---------------------------------------------------------------------------
// Public API: multi_layer_kv_transfer_unilateral
// ---------------------------------------------------------------------------
void multi_layer_kv_transfer_unilateral(
torch::Tensor& key_value, const torch::Tensor& key_value_ptrs,
const torch::Tensor& slot_mapping, const torch::Device& paged_memory_device,
const int page_buffer_size, const TransferDirection direction,
const EngineKVFormat engine_kv_format) {
const bool use_mla = ::is_mla(engine_kv_format);
// MLA case collapses back to multi_layer_kv_transfer
if (use_mla) {
return multi_layer_kv_transfer(key_value, key_value_ptrs, slot_mapping,
paged_memory_device, page_buffer_size,
direction, engine_kv_format);
}
int64_t* key_value_ptr = get_kernel_ptr<int64_t, torch::Tensor>(key_value);
int64_t** page_buffer_ptrs =
get_kernel_ptr<int64_t*, const torch::Tensor>(key_value_ptrs);
const int64_t* slot_mapping_ptr =
get_kernel_ptr<const int64_t, const torch::Tensor>(slot_mapping);
int num_layers = key_value.size(1);
int num_tokens = slot_mapping.size(0);
int num_origin_elements = key_value.size(3);
int elements_per_qword = 8 / key_value.element_size();
int num_qwords = num_origin_elements / elements_per_qword;
int k_or_v_size = 2;
int kv_num_tokens = key_value.size(2);
int wg_size = round_up_to_sg(std::min(num_qwords, MAX_WG_SIZE));
const c10::OptionalDeviceGuard device_guard(paged_memory_device);
sycl::queue& queue =
c10::xpu::getCurrentXPUStream(paged_memory_device.index()).queue();
if (direction == TransferDirection::H2D) {
submit_multi_layer_unilateral_kernel<int64_t, false>(
queue, key_value_ptr, page_buffer_ptrs, slot_mapping_ptr, num_qwords,
num_tokens, num_layers, page_buffer_size, k_or_v_size, kv_num_tokens,
wg_size);
} else {
submit_multi_layer_unilateral_kernel<int64_t, true>(
queue, key_value_ptr, page_buffer_ptrs, slot_mapping_ptr, num_qwords,
num_tokens, num_layers, page_buffer_size, k_or_v_size, kv_num_tokens,
wg_size);
}
}
// ---------------------------------------------------------------------------
// single_layer_kv_transfer — helper template
// ---------------------------------------------------------------------------
// USE_MLA and IS_D2H are template parameters so the compiler can
// dead-strip the unused branch.
template <bool USE_MLA, bool IS_D2H>
void single_layer_kv_transfer_impl(sycl::queue& queue, int64_t* lmc_ptr,
int64_t* vllm_ptr, const int64_t* slot_ptr,
int num_tokens, int n, int lmc_stride,
int lmc_value_offset, int block_size,
int vllm_block_key_stride_in_64bit,
int vllm_value_offset, int num_heads,
int head_size_in_64bit, int wg_size) {
if (num_tokens <= 0) return;
sycl::range<1> global_range(static_cast<size_t>(num_tokens) * wg_size);
sycl::range<1> local_range(static_cast<size_t>(wg_size));
queue.parallel_for(
sycl::nd_range<1>(global_range, local_range),
[=](sycl::nd_item<1> item) [[sycl::reqd_sub_group_size(16)]] {
const int64_t token_idx = static_cast<int64_t>(item.get_group(0));
const int64_t slot_idx = slot_ptr[token_idx];
if (slot_idx < 0) return;
const int64_t block_idx = slot_idx / block_size;
const int64_t block_offset = slot_idx % block_size;
const int tid = static_cast<int>(item.get_local_id(0));
const int nthreads = static_cast<int>(item.get_local_range(0));
for (int i = tid; i < n; i += nthreads) {
const int64_t lmc_key_idx = token_idx * lmc_stride + i;
const int head_idx = i / head_size_in_64bit;
const int head_offset = i % head_size_in_64bit;
const int64_t vllm_key_idx =
block_idx * vllm_block_key_stride_in_64bit +
block_offset * num_heads * head_size_in_64bit +
head_idx * head_size_in_64bit + head_offset;
if constexpr (IS_D2H) {
lmc_ptr[lmc_key_idx] = vllm_ptr[vllm_key_idx];
if constexpr (!USE_MLA) {
const int64_t lmc_value_idx = lmc_key_idx + lmc_value_offset;
const int64_t vllm_value_idx = vllm_key_idx + vllm_value_offset;
lmc_ptr[lmc_value_idx] = vllm_ptr[vllm_value_idx];
}
} else {
vllm_ptr[vllm_key_idx] = lmc_ptr[lmc_key_idx];
if constexpr (!USE_MLA) {
const int64_t lmc_value_idx = lmc_key_idx + lmc_value_offset;
const int64_t vllm_value_idx = vllm_key_idx + vllm_value_offset;
vllm_ptr[vllm_value_idx] = lmc_ptr[lmc_value_idx];
}
}
}
});
}
// ---------------------------------------------------------------------------
// Public API: single_layer_kv_transfer
// ---------------------------------------------------------------------------
void single_layer_kv_transfer(torch::Tensor& lmc_key_value_cache,
torch::Tensor& vllm_key_value_cache,
torch::Tensor& slot_mapping,
const TransferDirection direction,
const EngineKVFormat engine_kv_format,
const bool token_major) {
int64_t* lmc_key_value_cache_ptr =
get_kernel_ptr<int64_t, torch::Tensor>(lmc_key_value_cache);
int64_t* vllm_key_value_cache_ptr =
get_kernel_ptr<int64_t, torch::Tensor>(vllm_key_value_cache);
const int64_t* slot_mapping_ptr =
get_kernel_ptr<const int64_t, const torch::Tensor>(slot_mapping);
int elements_per_entry = 8 / vllm_key_value_cache.element_size();
int num_tokens = slot_mapping.size(0);
int num_heads;
int head_size_in_64bit;
int block_size;
const bool use_mla = ::is_mla(engine_kv_format);
if (use_mla) {
num_heads = 1;
block_size = vllm_key_value_cache.size(1);
head_size_in_64bit = vllm_key_value_cache.size(2) / elements_per_entry;
} else {
num_heads = vllm_key_value_cache.size(3);
head_size_in_64bit = vllm_key_value_cache.size(4) / elements_per_entry;
block_size = vllm_key_value_cache.size(2);
}
int lmc_stride;
int lmc_value_offset;
if (use_mla) {
lmc_stride = lmc_key_value_cache.stride(0) / elements_per_entry;
lmc_value_offset = 0;
} else if (token_major) {
lmc_stride = lmc_key_value_cache.stride(0) / elements_per_entry;
lmc_value_offset = lmc_key_value_cache.stride(1) / elements_per_entry;
} else {
lmc_stride = lmc_key_value_cache.stride(1) / elements_per_entry;
lmc_value_offset = lmc_key_value_cache.stride(0) / elements_per_entry;
}
int vllm_block_key_stride_in_64bit;
int vllm_value_offset;
if (use_mla) {
vllm_block_key_stride_in_64bit =
vllm_key_value_cache.stride(0) / elements_per_entry;
vllm_value_offset = 0;
} else if (engine_kv_format == EngineKVFormat::NL_X_TWO_NB_BS_NH_HS) {
vllm_block_key_stride_in_64bit =
vllm_key_value_cache.stride(1) / elements_per_entry;
vllm_value_offset = vllm_key_value_cache.stride(0) / elements_per_entry;
} else if (engine_kv_format == EngineKVFormat::NL_X_NB_TWO_BS_NH_HS) {
vllm_block_key_stride_in_64bit =
vllm_key_value_cache.stride(0) / elements_per_entry;
vllm_value_offset = vllm_key_value_cache.stride(1) / elements_per_entry;
} else {
throw std::runtime_error(
"Unsupported non-MLA EngineKVFormat in single_layer_kv_transfer: " +
std::to_string(static_cast<int>(engine_kv_format)));
}
int n = num_heads * head_size_in_64bit;
int wg_size = round_up_to_sg(std::min(n, MAX_WG_SIZE));
if (num_tokens <= 0) return;
const c10::OptionalDeviceGuard device_guard(device_of(vllm_key_value_cache));
sycl::queue& queue =
c10::xpu::getCurrentXPUStream(vllm_key_value_cache.device().index())
.queue();
auto lmc_ptr = lmc_key_value_cache_ptr;
auto vllm_ptr = vllm_key_value_cache_ptr;
auto slot_ptr = slot_mapping_ptr;
// Dispatch to 4 compile-time specialisations (USE_MLA × IS_D2H)
// so the inner loop is branch-free.
if (use_mla) {
if (direction == TransferDirection::D2H)
single_layer_kv_transfer_impl<true, true>(
queue, lmc_ptr, vllm_ptr, slot_ptr, num_tokens, n, lmc_stride,
lmc_value_offset, block_size, vllm_block_key_stride_in_64bit,
vllm_value_offset, num_heads, head_size_in_64bit, wg_size);
else
single_layer_kv_transfer_impl<true, false>(
queue, lmc_ptr, vllm_ptr, slot_ptr, num_tokens, n, lmc_stride,
lmc_value_offset, block_size, vllm_block_key_stride_in_64bit,
vllm_value_offset, num_heads, head_size_in_64bit, wg_size);
} else {
if (direction == TransferDirection::D2H)
single_layer_kv_transfer_impl<false, true>(
queue, lmc_ptr, vllm_ptr, slot_ptr, num_tokens, n, lmc_stride,
lmc_value_offset, block_size, vllm_block_key_stride_in_64bit,
vllm_value_offset, num_heads, head_size_in_64bit, wg_size);
else
single_layer_kv_transfer_impl<false, false>(
queue, lmc_ptr, vllm_ptr, slot_ptr, num_tokens, n, lmc_stride,
lmc_value_offset, block_size, vllm_block_key_stride_in_64bit,
vllm_value_offset, num_heads, head_size_in_64bit, wg_size);
}
}
// ---------------------------------------------------------------------------
// single_layer_kv_transfer_sgl — helper template
// ---------------------------------------------------------------------------
template <bool IS_D2H>
void single_layer_kv_transfer_sgl_impl(
sycl::queue& queue, int64_t* lmc_ptr, int64_t* sgl_k_ptr,
int64_t* sgl_v_ptr, const int64_t* slot_ptr, int num_tokens, int n,
int lmc_stride, int lmc_value_offset, int block_stride_in_64bit,
int block_size, int num_heads, int head_size_in_64bit, int wg_size) {
if (num_tokens <= 0) return;
sycl::range<1> global_range(static_cast<size_t>(num_tokens) * wg_size);
sycl::range<1> local_range(static_cast<size_t>(wg_size));
queue.parallel_for(
sycl::nd_range<1>(global_range, local_range),
[=](sycl::nd_item<1> item) [[sycl::reqd_sub_group_size(16)]] {
const int64_t token_idx = static_cast<int64_t>(item.get_group(0));
const int64_t slot_idx = slot_ptr[token_idx];
if (slot_idx < 0) return;
const int64_t block_idx = slot_idx / block_size;
const int64_t block_offset = slot_idx % block_size;
const int tid = static_cast<int>(item.get_local_id(0));
const int nthreads = static_cast<int>(item.get_local_range(0));
for (int i = tid; i < n; i += nthreads) {
const int64_t lmc_key_idx = token_idx * lmc_stride + i;
const int64_t lmc_value_idx = lmc_key_idx + lmc_value_offset;
const int head_idx = i / head_size_in_64bit;
const int head_offset = i % head_size_in_64bit;
const int64_t sgl_kv_idx =
block_idx * block_stride_in_64bit +
block_offset * num_heads * head_size_in_64bit +
head_idx * head_size_in_64bit + head_offset;
if constexpr (IS_D2H) {
lmc_ptr[lmc_key_idx] = sgl_k_ptr[sgl_kv_idx];
lmc_ptr[lmc_value_idx] = sgl_v_ptr[sgl_kv_idx];
} else {
sgl_k_ptr[sgl_kv_idx] = lmc_ptr[lmc_key_idx];
sgl_v_ptr[sgl_kv_idx] = lmc_ptr[lmc_value_idx];
}
}
});
}
// ---------------------------------------------------------------------------
// Public API: single_layer_kv_transfer_sgl (SGLang)
// ---------------------------------------------------------------------------
void single_layer_kv_transfer_sgl(torch::Tensor& lmc_key_value_cache,
torch::Tensor& sgl_key_cache,
torch::Tensor& sgl_value_cache,
torch::Tensor& slot_mapping,
const TransferDirection direction,
const bool token_major) {
int64_t* lmc_key_value_cache_ptr =
get_kernel_ptr<int64_t, torch::Tensor>(lmc_key_value_cache);
int64_t* sgl_key_cache_ptr =
get_kernel_ptr<int64_t, torch::Tensor>(sgl_key_cache);
int64_t* sgl_value_cache_ptr =
get_kernel_ptr<int64_t, torch::Tensor>(sgl_value_cache);
const int64_t* slot_mapping_ptr =
get_kernel_ptr<const int64_t, const torch::Tensor>(slot_mapping);
int elements_per_entry = 8 / sgl_key_cache.element_size();
int num_tokens = slot_mapping.size(0);
int num_heads = sgl_key_cache.size(2);
int head_size_in_64bit = sgl_key_cache.size(3) / elements_per_entry;
int block_size = sgl_key_cache.size(1);
int lmc_stride;
int lmc_value_offset;
if (token_major) {
lmc_stride = lmc_key_value_cache.stride(0) / elements_per_entry;
lmc_value_offset = lmc_key_value_cache.stride(1) / elements_per_entry;
} else {
lmc_stride = lmc_key_value_cache.stride(1) / elements_per_entry;
lmc_value_offset = lmc_key_value_cache.stride(0) / elements_per_entry;
}
int block_stride_in_64bit = sgl_key_cache.stride(0) / elements_per_entry;
TORCH_CHECK(sgl_key_cache.stride(0) == sgl_value_cache.stride(0));
int n = num_heads * head_size_in_64bit;
int wg_size = round_up_to_sg(std::min(n, MAX_WG_SIZE));
if (num_tokens <= 0) return;
const c10::OptionalDeviceGuard device_guard(device_of(sgl_key_cache));
sycl::queue& queue =
c10::xpu::getCurrentXPUStream(sgl_key_cache.device().index()).queue();
if (direction == TransferDirection::D2H)
single_layer_kv_transfer_sgl_impl<true>(
queue, lmc_key_value_cache_ptr, sgl_key_cache_ptr, sgl_value_cache_ptr,
slot_mapping_ptr, num_tokens, n, lmc_stride, lmc_value_offset,
block_stride_in_64bit, block_size, num_heads, head_size_in_64bit,
wg_size);
else
single_layer_kv_transfer_sgl_impl<false>(
queue, lmc_key_value_cache_ptr, sgl_key_cache_ptr, sgl_value_cache_ptr,
slot_mapping_ptr, num_tokens, n, lmc_stride, lmc_value_offset,
block_stride_in_64bit, block_size, num_heads, head_size_in_64bit,
wg_size);
}
// ---------------------------------------------------------------------------
// Public API: load_and_reshape_flash (deprecated -- unit tests only)
// ---------------------------------------------------------------------------
void load_and_reshape_flash(torch::Tensor& key_value, torch::Tensor& key_cache,
torch::Tensor& value_cache,
torch::Tensor& slot_mapping, const int layer_idx) {
int64_t* key_value_ptr = get_kernel_ptr<int64_t, torch::Tensor>(key_value);
int64_t* key_cache_ptr = get_kernel_ptr<int64_t, torch::Tensor>(key_cache);
int64_t* value_cache_ptr =
get_kernel_ptr<int64_t, torch::Tensor>(value_cache);
const int64_t* slot_mapping_ptr =
get_kernel_ptr<const int64_t, const torch::Tensor>(slot_mapping);
int elements_per_entry = 8 / key_cache.element_size();
int num_tokens = slot_mapping.size(0);
int num_heads = key_cache.size(2);
int head_size_in_64bit = key_cache.size(3) / elements_per_entry;
int block_size = key_cache.size(1);
int key_value_stride = key_value.stride(2) / elements_per_entry;
int num_layers = key_value.size(1);
int key_layer_offset = layer_idx * key_value.stride(1) / elements_per_entry;
int value_layer_offset =
(layer_idx + num_layers) * key_value.stride(1) / elements_per_entry;
int block_stride_in_64bit = key_cache.stride(0) / elements_per_entry;
TORCH_CHECK(key_cache.stride(0) == value_cache.stride(0));
int n = num_heads * head_size_in_64bit;
int wg_size = round_up_to_sg(std::min(n, MAX_WG_SIZE));
if (num_tokens <= 0) return;
sycl::range<1> global_range(static_cast<size_t>(num_tokens) * wg_size);
sycl::range<1> local_range(static_cast<size_t>(wg_size));
const c10::OptionalDeviceGuard device_guard(device_of(key_cache));
sycl::queue& queue =
c10::xpu::getCurrentXPUStream(key_cache.device().index()).queue();
auto kv_ptr = key_value_ptr;
auto k_ptr = key_cache_ptr;
auto v_ptr = value_cache_ptr;
auto slot_ptr = slot_mapping_ptr;
queue.parallel_for(
sycl::nd_range<1>(global_range, local_range),
[=](sycl::nd_item<1> item) [[sycl::reqd_sub_group_size(16)]] {
const int64_t token_idx = static_cast<int64_t>(item.get_group(0));
const int64_t slot_idx = slot_ptr[token_idx];
if (slot_idx < 0) return;
const int64_t blk_idx = slot_idx / block_size;
const int64_t blk_off = slot_idx % block_size;
const int tid = static_cast<int>(item.get_local_id(0));
const int nthreads = static_cast<int>(item.get_local_range(0));
for (int i = tid; i < n; i += nthreads) {
const int64_t tgt_key_idx =
key_layer_offset + token_idx * key_value_stride + i;
const int64_t tgt_value_idx =
value_layer_offset + token_idx * key_value_stride + i;
const int head_idx = i / head_size_in_64bit;
const int head_offset = i % head_size_in_64bit;
const int64_t src_kv_idx = blk_idx * block_stride_in_64bit +
blk_off * num_heads * head_size_in_64bit +
head_idx * head_size_in_64bit +
head_offset;
kv_ptr[tgt_key_idx] = k_ptr[src_kv_idx];
kv_ptr[tgt_value_idx] = v_ptr[src_kv_idx];
}
});
}
// ---------------------------------------------------------------------------
// Public API: reshape_and_cache_back_flash (deprecated -- unit
// tests only)
// ---------------------------------------------------------------------------
void reshape_and_cache_back_flash(torch::Tensor& key_value,
torch::Tensor& key_cache,
torch::Tensor& value_cache,
torch::Tensor& slot_mapping,
const int layer_idx) {
int64_t* key_cache_ptr = get_kernel_ptr<int64_t, torch::Tensor>(key_cache);
int64_t* value_cache_ptr =
get_kernel_ptr<int64_t, torch::Tensor>(value_cache);
int64_t* key_value_ptr = get_kernel_ptr<int64_t, torch::Tensor>(key_value);
const int64_t* slot_mapping_ptr =
get_kernel_ptr<const int64_t, const torch::Tensor>(slot_mapping);
int elements_per_entry = 8 / key_cache.element_size();
int num_tokens = slot_mapping.size(0);
int num_heads = key_cache.size(2);
int head_size_in_64bit = key_cache.size(3) / elements_per_entry;
int block_size = key_cache.size(1);
int key_value_stride = key_value.stride(2) / elements_per_entry;
int num_layers = key_value.size(1);
int key_layer_offset = layer_idx * key_value.stride(1) / elements_per_entry;
int value_layer_offset =
(layer_idx + num_layers) * key_value.stride(1) / elements_per_entry;
int block_stride_in_64bit = key_cache.stride(0) / elements_per_entry;
TORCH_CHECK(key_cache.stride(0) == value_cache.stride(0));
int n = num_heads * head_size_in_64bit;
int wg_size = round_up_to_sg(std::min(n, MAX_WG_SIZE));
if (num_tokens <= 0) return;
sycl::range<1> global_range(static_cast<size_t>(num_tokens) * wg_size);
sycl::range<1> local_range(static_cast<size_t>(wg_size));
const c10::OptionalDeviceGuard device_guard(device_of(key_cache));
sycl::queue& queue =
c10::xpu::getCurrentXPUStream(key_cache.device().index()).queue();
auto kv_ptr = key_value_ptr;
auto k_ptr = key_cache_ptr;
auto v_ptr = value_cache_ptr;
auto slot_ptr = slot_mapping_ptr;
queue.parallel_for(
sycl::nd_range<1>(global_range, local_range),
[=](sycl::nd_item<1> item) [[sycl::reqd_sub_group_size(16)]] {
const int64_t token_idx = static_cast<int64_t>(item.get_group(0));
const int64_t slot_idx = slot_ptr[token_idx];
if (slot_idx < 0) return;
const int64_t blk_idx = slot_idx / block_size;
const int64_t blk_off = slot_idx % block_size;
const int tid = static_cast<int>(item.get_local_id(0));
const int nthreads = static_cast<int>(item.get_local_range(0));
for (int i = tid; i < n; i += nthreads) {
const int64_t tgt_key_idx =
key_layer_offset + token_idx * key_value_stride + i;
const int64_t tgt_value_idx =
value_layer_offset + token_idx * key_value_stride + i;
const int head_idx = i / head_size_in_64bit;
const int head_offset = i % head_size_in_64bit;
const int64_t src_kv_idx = blk_idx * block_stride_in_64bit +
blk_off * num_heads * head_size_in_64bit +
head_idx * head_size_in_64bit +
head_offset;
k_ptr[src_kv_idx] = kv_ptr[tgt_key_idx];
v_ptr[src_kv_idx] = kv_ptr[tgt_value_idx];
}
});
}
// ---------------------------------------------------------------------------
// Public API: lmcache_memcpy_async
// ---------------------------------------------------------------------------
void lmcache_memcpy_async(uintptr_t dest, uintptr_t src, size_t nbytes,
TransferDirection direction,
size_t host_buffer_offset,
size_t host_buffer_alignments) {
TORCH_CHECK((host_buffer_alignments & (host_buffer_alignments - 1)) == 0,
"host_buffer_alignments must be power of two");
// SYCL USM memcpy infers direction from pointer allocation types;
// the `direction` parameter is retained only for API compatibility.
(void)direction;
sycl::queue& queue = c10::xpu::getCurrentXPUStream().queue();
size_t offset = 0;
const size_t mask = host_buffer_alignments - 1;
while (offset < nbytes) {
size_t current_src = src + offset;
size_t current_dest = dest + offset;
size_t aligned_area_end =
((offset + host_buffer_offset) & ~mask) + host_buffer_alignments;
size_t real_end = std::min(host_buffer_offset + nbytes, aligned_area_end);
size_t max_nbytes = real_end - offset - host_buffer_offset;
// USM memcpy is direction-agnostic.
queue.memcpy(reinterpret_cast<void*>(current_dest),
reinterpret_cast<const void*>(current_src), max_nbytes);
offset += max_nbytes;
}
}