From 714be54b690c068b2413e05c871bb4f385c191b4 Mon Sep 17 00:00:00 2001 From: x00937880 Date: Wed, 30 Jul 2025 18:31:03 +0800 Subject: [PATCH] Adapt LMCache to Ascend fix date --- 0001-npu-picked-support.patch | 3380 ++++++++++++++++++++++++++ LMCache-0.1.4.alpha.tar.gz | Bin 817998 -> 0 bytes LMCache.tar.gz | Bin 0 -> 8053000 bytes fix-dependance-issue-on-ascend.patch | 13 - lmcache.spec | 29 +- 5 files changed, 3396 insertions(+), 26 deletions(-) create mode 100644 0001-npu-picked-support.patch delete mode 100644 LMCache-0.1.4.alpha.tar.gz create mode 100644 LMCache.tar.gz delete mode 100644 fix-dependance-issue-on-ascend.patch diff --git a/0001-npu-picked-support.patch b/0001-npu-picked-support.patch new file mode 100644 index 0000000..e22bb12 --- /dev/null +++ b/0001-npu-picked-support.patch @@ -0,0 +1,3380 @@ +diff --git a/csrc/ascend/CMakeLists.txt b/csrc/ascend/CMakeLists.txt +new file mode 100644 +index 0000000..b434723 +--- /dev/null ++++ b/csrc/ascend/CMakeLists.txt +@@ -0,0 +1,130 @@ ++# Copyright (c) Huawei Technologies Co., Ltd. 2020. All rights reserved. ++ ++# CMake lowest version requirement ++cmake_minimum_required(VERSION 3.16.0) ++# project information ++project(c_ops) ++ ++set(CMAKE_CXX_STANDARD 17) ++include(${CMAKE_CURRENT_LIST_DIR}/utils.cmake) ++ ++find_package(Python3 COMPONENTS Interpreter Development REQUIRED) ++set(PYTHON_SUPPORTED_VERSIONS "3.9" "3.10" "3.11") ++find_package(pybind11 REQUIRED) ++ ++append_cmake_prefix_path("torch" "torch.utils.cmake_prefix_path") ++set(LMC_INSTALL_PATH "${CMAKE_INSTALL_PREFIX}") ++ ++set(SOC_VERSION ${SOC_VERSION}) ++set(ARCH ${ARCH}) ++ ++if (NOT CMAKE_BUILD_TYPE) ++ set(CMAKE_BUILD_TYPE "Release" CACHE STRINGS "Build type Release/Debug (default Release)" FORCE) ++endif() ++ ++set(ASCEND_HOME_PATH ${ASCEND_CANN_PACKAGE_PATH}) ++if(EXISTS ${ASCEND_CANN_PACKAGE_PATH}/tools/tikcpp/ascendc_kernel_cmake) ++ set(ASCENDC_CMAKE_DIR ${ASCEND_CANN_PACKAGE_PATH}/tools/tikcpp/ascendc_kernel_cmake) ++elseif(EXISTS ${ASCEND_CANN_PACKAGE_PATH}/compiler/tikcpp/ascendc_kernel_cmake) ++ set(ASCENDC_CMAKE_DIR ${ASCEND_CANN_PACKAGE_PATH}/compiler/tikcpp/ascendc_kernel_cmake) ++elseif(EXISTS ${ASCEND_CANN_PACKAGE_PATH}/ascendc_devkit/tikcpp/samples/cmake) ++ set(ASCENDC_CMAKE_DIR ${ASCEND_CANN_PACKAGE_PATH}/ascendc_devkit/tikcpp/samples/cmake) ++else() ++ message(FATAL_ERROR "ascendc_kernel_cmake does not exist, please check whether the cann package is installed.") ++endif() ++ ++include(${ASCENDC_CMAKE_DIR}/ascendc.cmake) ++ ++# ${KERNEL_FILES} are used to compile library, push files written by ascendc in ${KERNEL_FILES}. ++# ref to cmake/npu.cmake ascendc_library, cmake/cpu.cmake add_library ++file(GLOB KERNEL_FILES ++${CMAKE_CURRENT_SOURCE_DIR}/kernels/*.cpp) ++ ++message(STATUS "kernel files: ${KERNEL_FILES}") ++ ++# ascendc_library use to add kernel file to generate ascendc library ++ascendc_library(ascend_kernels SHARED ++ ${KERNEL_FILES} ++) ++ ++message("TORCH_NPU_PATH is ${TORCH_NPU_PATH}") ++ ++file(GLOB SRC_FILES ++${CMAKE_CURRENT_SOURCE_DIR}/*.cpp) ++ ++set(PYBIND11_SOURCE_FILE ++${CMAKE_CURRENT_SOURCE_DIR}/mem_kernels.cpp ++${CMAKE_CURRENT_SOURCE_DIR}/managed_mem.cpp ++${CMAKE_CURRENT_SOURCE_DIR}/../pybind.cpp ++) ++ ++find_package(Torch REQUIRED) ++ ++include_directories( ++ ${CMAKE_CURRENT_SOURCE_DIR} ++ ${pybind11_INCLUDE_DIRS} ++ ${PYTHON_INCLUDE_PATH} ++ ${TORCH_INCLUDE_DIRS} ++ ${TORCH_NPU_PATH}/include ++ ${ASCEND_HOME_PATH}/include ++ ${ASCEND_HOME_PATH}/aarch64-linux/ascendc/include ++ ${ASCEND_HOME_PATH}/aarch64-linux/include/experiment/platform ++ ${ASCEND_HOME_PATH}/aarch64-linux/include/experiment/ascend_hal ++ ${ASCEND_HOME_PATH}/x86_64-linux/include/experiment/platform ++ ${ASCEND_HOME_PATH}/x86_64-linux/include/experiment/ascend_hal ++) ++ ++ ++set( ++ INCLUDES ++ ${TORCH_INCLUDE_DIRS} ++ ${TORCH_NPU_PATH}/include ++ ${ASCEND_HOME_PATH}/include ++ ${ASCEND_HOME_PATH}/aarch64-linux/ascendc/include ++ ${ASCEND_HOME_PATH}/aarch64-linux/include/experiment/platform ++ ${ASCEND_HOME_PATH}/aarch64-linux/include/experiment/ascend_hal ++) ++ ++set(PYMODULE_FILES ++ ${SRC_FILES} ++ ${PYBIND11_SOURCE_FILE} ++) ++ ++pybind11_add_module(c_ops ${PYMODULE_FILES}) ++ ++message(STATUS "CMake: Adding -DUSE_ASCEND compile definition.") ++target_compile_definitions(c_ops PRIVATE USE_ASCEND) ++ ++set(TORCH_NPU_LIBS_DIR "${TORCH_NPU_PATH}/lib") ++set(ASCEND_CANN_LIBS_DIR "${ASCEND_HOME_PATH}/lib64") ++set(TORCH_LIBS_DIR "${TORCH_PATH}/lib") ++ ++ ++target_link_options(c_ops PRIVATE ++ "-Wl,-rpath,$ORIGIN:$ORIGIN/lib" ++ "-Wl,-rpath,${LMC_INSTALL_PATH}" ++) ++ ++target_link_directories( ++ c_ops ++ PRIVATE ++ ${TORCH_LIBS_DIR} ++ ${TORCH_NPU_PATH}/lib/ ++ ${ASCEND_HOME_PATH}/lib64 ++ ${ASCEND_DRIVER_PATH}/lib64/driver ++) ++ ++target_link_libraries( ++ c_ops ++ PUBLIC ++ ${TORCH_LIBRARIES} ++ libtorch_npu.so ++ ascend_kernels ++ ascendcl ++ platform ++ ascend_hal ++ tiling_api ++) ++ ++ ++install(TARGETS c_ops ascend_kernels DESTINATION ${LMC_INSTALL_PATH}) +diff --git a/csrc/ascend/cachegen_kernels.cpp b/csrc/ascend/cachegen_kernels.cpp +new file mode 100644 +index 0000000..fdf865f +--- /dev/null ++++ b/csrc/ascend/cachegen_kernels.cpp +@@ -0,0 +1,32 @@ ++#include "cachegen_kernels.h" ++#include ++#include ++ ++namespace py = pybind11; ++ ++void encode_cuda_new(const at::Tensor& cdf, const at::Tensor& input_sym, ++ at::Tensor& output_buffer, at::Tensor& output_lengths) { ++ // TODO: ++ PyErr_SetString(PyExc_NotImplementedError, "Please contact LMCache Ascend."); ++ throw py::error_already_set(); ++}; ++ ++void decode_cuda_new(const at::Tensor& cdf, const at::Tensor& bytestreams, ++ const at::Tensor& lengths, at::Tensor& output) { ++ // TODO: ++ PyErr_SetString(PyExc_NotImplementedError, "Please contact LMCache Ascend."); ++ throw py::error_already_set(); ++}; ++ ++void decode_cuda_prefsum(const at::Tensor& cdf, const at::Tensor& bytestreams, ++ const at::Tensor& lengths, at::Tensor& output) { ++ // TODO: ++ PyErr_SetString(PyExc_NotImplementedError, "Please contact LMCache Ascend."); ++ throw py::error_already_set(); ++}; ++ ++at::Tensor calculate_cdf(const at::Tensor& input, const int max_bins) { ++ // TODO: ++ PyErr_SetString(PyExc_NotImplementedError, "Please contact LMCache Ascend."); ++ throw py::error_already_set(); ++}; +\ No newline at end of file +diff --git a/csrc/ascend/cachegen_kernels.h b/csrc/ascend/cachegen_kernels.h +new file mode 100644 +index 0000000..d1a1701 +--- /dev/null ++++ b/csrc/ascend/cachegen_kernels.h +@@ -0,0 +1,16 @@ ++#pragma once ++#include ++#include ++#include ++#include ++ ++void encode_cuda_new(const at::Tensor& cdf, const at::Tensor& input_sym, ++ at::Tensor& output_buffer, at::Tensor& output_lengths); ++ ++void decode_cuda_new(const at::Tensor& cdf, const at::Tensor& bytestreams, ++ const at::Tensor& lengths, at::Tensor& output); ++ ++void decode_cuda_prefsum(const at::Tensor& cdf, const at::Tensor& bytestreams, ++ const at::Tensor& lengths, at::Tensor& output); ++ ++at::Tensor calculate_cdf(const at::Tensor& input, const int max_bins); +\ No newline at end of file +diff --git a/csrc/ascend/kernels/load_and_reshape_flash.cpp b/csrc/ascend/kernels/load_and_reshape_flash.cpp +new file mode 100644 +index 0000000..7e04e6f +--- /dev/null ++++ b/csrc/ascend/kernels/load_and_reshape_flash.cpp +@@ -0,0 +1,264 @@ ++/* ++ * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. ++ * ++ * Licensed under the Apache License, Version 2.0 (the "License"); ++ * you may not use this file except in compliance with the License. ++ * You may obtain a copy of the License at ++ * ++ * http://www.apache.org/licenses/LICENSE-2.0 ++ * ++ * Unless required by applicable law or agreed to in writing, software ++ * distributed under the License is distributed on an "AS IS" BASIS, ++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++ * See the License for the specific language governing permissions and ++ * limitations under the License. ++ */ ++#include "kernel_operator.h" ++#include ++#include "types.h" ++#include "utils.h" ++ ++template class LoadAndReshapeFlashCopy { ++ using local_scalar_t = AscendC::LocalTensor; ++ ++public: ++ __aicore__ inline LoadAndReshapeFlashCopy() ++ { ++ } ++ ++ __aicore__ inline void init(GM_ADDR cacheTensor, GM_ADDR keyCachePtr, GM_ADDR valueCachePtr, GM_ADDR slotmappings, ++ const int64_t numPages, const int64_t hiddenDims, const int32_t pagedSize, ++ const int32_t numTokens, const int32_t numLayers, const int32_t layerIdx, ++ const bool page2L, AscendC::TPipe *pipe) ++ { ++ this->pipe_ = pipe; ++ this->numPages_ = numPages; ++ this->hiddenDims_ = hiddenDims; ++ this->numTokens_ = numTokens; ++ this->pagedSize_ = pagedSize; ++ this->numLayers_ = numLayers; ++ this->layerIdx_ = layerIdx; ++ this->valid_ = true; ++ this->page2L_ = page2L; ++ ++ // TODO: Not sure how many to allocate, but let's do 4 blocks of hiddenDims_ ++ // if it was fp16, 2048, we would get 16kb.? ++ // should check whether hiddenDims_ is > 192KB. ++ this->pipe_->InitBuffer(this->pagedTokenQue_, 4, this->hiddenDims_*sizeof(scalar_t)); ++ } ++ ++ __aicore__ inline void reset(){ ++ this->valid_ = true; ++ } ++ ++ __aicore__ inline void updateTensorMemOffsetAndProcess(__gm__ uint8_t *pagedKeyTensor, ++ __gm__ uint8_t *pagedValueTensor, ++ __gm__ uint8_t* nonPagedTensor, ++ __gm__ uint8_t *slotmappings, const int tokenIdx) ++ { ++ __gm__ slot_t *slotmappingPtr = reinterpret_cast<__gm__ slot_t*>(slotmappings); ++ int64_t slot = static_cast(slotmappingPtr[tokenIdx]); ++ ++ if (slot == -1) { ++ this->valid_ = false; ++ return; ++ } ++ ++ // for the page tensor ++ int64_t pagedIdxOffset = slot * this->hiddenDims_; ++ ++ // for the lmc tensor ++ int64_t nonPagedKeyOffset = this->layerIdx_ * this->numTokens_ * this->hiddenDims_ + ++ tokenIdx * this->hiddenDims_; ++ ++ // values are stored after keys in the non-paged tensor ++ int64_t nonPagedValueOffset = this->numLayers_ * this->numTokens_ * this->hiddenDims_ + ++ this->layerIdx_ * this->numTokens_ * this->hiddenDims_ + ++ tokenIdx * this->hiddenDims_; ++ ++ // keys ++ this->keyTokensGlobal_.SetGlobalBuffer(reinterpret_cast<__gm__ scalar_t*>(pagedKeyTensor) + pagedIdxOffset, ++ this->hiddenDims_); ++ this->lmcBufferKeyGlobal_.SetGlobalBuffer(reinterpret_cast<__gm__ scalar_t*>(nonPagedTensor) + nonPagedKeyOffset, ++ this->hiddenDims_); ++ // values ++ this->valueTokensGlobal_.SetGlobalBuffer(reinterpret_cast<__gm__ scalar_t*>(pagedValueTensor) + pagedIdxOffset, ++ this->hiddenDims_); ++ this->lmcBufferValueGlobal_.SetGlobalBuffer(reinterpret_cast<__gm__ scalar_t*>(nonPagedTensor) + nonPagedValueOffset, ++ this->hiddenDims_); ++ } ++ ++ __aicore__ inline void processFunc() { ++ if (!this->valid_) { ++ return; ++ } ++ // 1. Alloc Tensor for local page ++ local_scalar_t hiddenKeysDimTensor = this->pagedTokenQue_.template AllocTensor(); ++ local_scalar_t hiddenValuesDimTensor = this->pagedTokenQue_.template AllocTensor();; ++ ++ // 2. copy from global tensor into local (GM -> UB) ++ if (this->page2L_) { ++ AscendC::DataCopy(hiddenKeysDimTensor, this->keyTokensGlobal_, this->hiddenDims_); ++ AscendC::DataCopy(hiddenValuesDimTensor, this->valueTokensGlobal_, this->hiddenDims_); ++ } else { ++ AscendC::DataCopy(hiddenKeysDimTensor, this->lmcBufferKeyGlobal_, this->hiddenDims_); ++ AscendC::DataCopy(hiddenValuesDimTensor, this->lmcBufferValueGlobal_, this->hiddenDims_); ++ } ++ ++ // 3. enque vecin ++ pagedTokenQue_.EnQue(hiddenKeysDimTensor); ++ pagedTokenQue_.EnQue(hiddenValuesDimTensor); ++ ++ // 4. deque vecin, possible to reuse due to QueBind ++ hiddenKeysDimTensor = pagedTokenQue_.DeQue(); ++ hiddenValuesDimTensor = pagedTokenQue_.DeQue(); ++ ++ // 5. datacopy into GM ++ if (this->page2L_) { ++ AscendC::DataCopy(this->lmcBufferKeyGlobal_, hiddenKeysDimTensor, this->hiddenDims_); ++ AscendC::DataCopy(this->lmcBufferValueGlobal_, hiddenValuesDimTensor, this->hiddenDims_); ++ } else { ++ AscendC::DataCopy(this->keyTokensGlobal_, hiddenKeysDimTensor, this->hiddenDims_); ++ AscendC::DataCopy(this->valueTokensGlobal_, hiddenValuesDimTensor, this->hiddenDims_); ++ } ++ // 6. free alloced Tensor ++ pagedTokenQue_.FreeTensor(hiddenKeysDimTensor); ++ pagedTokenQue_.FreeTensor(hiddenValuesDimTensor); ++ } ++ ++private: ++ AscendC::TPipe *pipe_; ++ AscendC::TQueBind pagedTokenQue_; ++ ++ // [numPages, pagedSize, heads*headsize] ++ AscendC::GlobalTensor keyTokensGlobal_; ++ AscendC::GlobalTensor valueTokensGlobal_; ++ ++ // Depends on LMC setting whether we store in tokensMajor or not. ++ // the layout would be the followings: ++ // [tokens, kvs, heads*headsize] or [kvs, tokens, heads*headsize] ++ // TODO: check whether should combine the two and use a loop ++ AscendC::GlobalTensor lmcBufferKeyGlobal_; ++ AscendC::GlobalTensor lmcBufferValueGlobal_; ++ ++ int64_t numPages_; // num vllm npu blocks ++ int32_t pagedSize_; // per npu block tokens ++ int64_t hiddenDims_; // heads * headsize ++ int32_t numTokens_; // num tokens in the cache tensor chunk ++ int32_t numLayers_; // num layers in the cache tensor ++ int32_t layerIdx_; // layer idx in the cache tensor ++ bool valid_; ++ bool page2L_; // true, from pagedTensor to LMC, false otherwise ++}; ++ ++#define LOAD_AND_RESHAPE_FLASH_COPY_TYPE_DECLARE(TYPE, SLOTTYPE) \ ++ extern "C" __global__ __aicore__ void load_and_reshape_flash_copy_##TYPE##_##SLOTTYPE( \ ++ __gm__ uint8_t* dstCacheTensor, __gm__ uint8_t* keyCachePtr, __gm__ uint8_t* valueCachePtr, \ ++ __gm__ uint8_t* slotmappings, const int64_t hiddenDims, const int64_t numPages, const int32_t pagedSize, \ ++ const int32_t numTokens, const int32_t numLayers, const int32_t layerIdx, const bool page2L, \ ++ const int blockNum) \ ++ { \ ++ AscendC::TPipe pipe; \ ++ LoadAndReshapeFlashCopy op{}; \ ++ op.init(dstCacheTensor, keyCachePtr, valueCachePtr, slotmappings, numPages, hiddenDims, pagedSize, \ ++ numTokens, numLayers, layerIdx, page2L, &pipe); \ ++ int64_t bIdx = AscendC::GetBlockIdx(); \ ++ for (int64_t i = bIdx; i < numTokens; i+=blockNum) \ ++ { \ ++ op.reset(); \ ++ op.updateTensorMemOffsetAndProcess(keyCachePtr, valueCachePtr, dstCacheTensor, slotmappings, i); \ ++ op.processFunc(); \ ++ } \ ++ } ++ ++// Declare support kernel entry ++LOAD_AND_RESHAPE_FLASH_COPY_TYPE_DECLARE(half, int32_t); ++LOAD_AND_RESHAPE_FLASH_COPY_TYPE_DECLARE(half, int64_t); ++LOAD_AND_RESHAPE_FLASH_COPY_TYPE_DECLARE(bfloat16_t, int32_t); ++LOAD_AND_RESHAPE_FLASH_COPY_TYPE_DECLARE(bfloat16_t, int64_t); ++LOAD_AND_RESHAPE_FLASH_COPY_TYPE_DECLARE(int8_t, int32_t); ++LOAD_AND_RESHAPE_FLASH_COPY_TYPE_DECLARE(int8_t, int64_t); ++ ++namespace lmc { ++ ++#define LOAD_AND_RESHAPE_FLASH_COPY_KERNEL_CALL(TYPE, SLOTTYPE) \ ++ load_and_reshape_flash_copy_##TYPE##_##SLOTTYPE<<>>(dstCacheTensor, keyCachePtr, \ ++ valueCachePtr, slotmappings, hiddenDims, numPages, pagedSize, \ ++ numTokens, numLayers, layerIdx, page2L, blockDim); ++ ++template ++void load_and_reshape_kernel_call(uint32_t blockDim, void *stream, uint8_t *dstCacheTensor, uint8_t *keyCachePtr, ++ uint8_t *valueCachePtr, uint8_t *slotmappings, const int64_t hiddenDims, const int64_t numPages, ++ const int32_t pagedSize, const int32_t numTokens, const int32_t numLayers, ++ const int32_t layerIdx, const bool page2L); ++ ++ ++#define LOAD_AND_RESHAPE_KERNEL_CALL_TYPE_DECLARE(TYPE, SLOTTYPE) \ ++template<> \ ++void load_and_reshape_kernel_call(uint32_t blockDim, void *stream, uint8_t *dstCacheTensor, \ ++ uint8_t *keyCachePtr, uint8_t *valueCachePtr, uint8_t *slotmappings, \ ++ const int64_t hiddenDims, const int64_t numPages, \ ++ const int32_t pagedSize, const int32_t numTokens, \ ++ const int32_t numLayers, const int32_t layerIdx, \ ++ const bool page2L) { \ ++ LOAD_AND_RESHAPE_FLASH_COPY_KERNEL_CALL(TYPE, SLOTTYPE); \ ++} ++ ++LOAD_AND_RESHAPE_KERNEL_CALL_TYPE_DECLARE(half, int32_t); ++LOAD_AND_RESHAPE_KERNEL_CALL_TYPE_DECLARE(half, int64_t); ++LOAD_AND_RESHAPE_KERNEL_CALL_TYPE_DECLARE(bfloat16_t, int32_t); ++LOAD_AND_RESHAPE_KERNEL_CALL_TYPE_DECLARE(bfloat16_t, int64_t); ++LOAD_AND_RESHAPE_KERNEL_CALL_TYPE_DECLARE(int8_t, int32_t); ++LOAD_AND_RESHAPE_KERNEL_CALL_TYPE_DECLARE(int8_t, int64_t); ++ ++template ++void dispatch_on_slot_type(vllm_ascend::AscendType slotType, uint32_t blockDim, void *stream, ++ uint8_t *dstCacheTensor, uint8_t *keyCachePtr, uint8_t *valueCachePtr, ++ uint8_t *slotmappings, const int64_t hiddenDims, const int64_t numPages, ++ const int32_t pagedSize, const int32_t numTokens, const int32_t numLayers, ++ const int32_t layerIdx, const bool page2L) { ++ switch(slotType) { ++ case vllm_ascend::AscendType::INT32: ++ load_and_reshape_kernel_call(blockDim, stream, dstCacheTensor, keyCachePtr, valueCachePtr, ++ slotmappings, hiddenDims, numPages, pagedSize, numTokens, numLayers, layerIdx, ++ page2L); ++ break; ++ case vllm_ascend::AscendType::INT64: ++ load_and_reshape_kernel_call(blockDim, stream, dstCacheTensor, keyCachePtr, valueCachePtr, ++ slotmappings, hiddenDims, numPages, pagedSize, numTokens, numLayers, layerIdx, ++ page2L); ++ break; ++ default: ++ return; ++ } ++} ++ ++extern void load_and_reshape_flash_kernel(vllm_ascend::AscendType type, vllm_ascend::AscendType slotType, ++ uint32_t blockDim, void *stream, ++ uint8_t *dstCacheTensor, uint8_t *keyCachePtr, uint8_t *valueCachePtr, ++ uint8_t *slotmappings, const int64_t hiddenDims, const int64_t numPages, ++ const int32_t pagedSize, const int32_t numTokens, const int32_t numLayers, ++ const int32_t layerIdx, bool page2L) ++{ ++ switch(type) { ++ case vllm_ascend::AscendType::FP16: ++ dispatch_on_slot_type(slotType, blockDim, stream, dstCacheTensor, keyCachePtr, valueCachePtr, ++ slotmappings, hiddenDims, numPages, pagedSize, numTokens, numLayers, layerIdx, ++ page2L); ++ break; ++ case vllm_ascend::AscendType::BF16: ++ dispatch_on_slot_type(slotType, blockDim, stream, dstCacheTensor, keyCachePtr, valueCachePtr, ++ slotmappings, hiddenDims, numPages, pagedSize, numTokens, numLayers, layerIdx, ++ page2L); ++ break; ++ case vllm_ascend::AscendType::INT8: ++ dispatch_on_slot_type(slotType, blockDim, stream, dstCacheTensor, keyCachePtr, valueCachePtr, ++ slotmappings, hiddenDims, numPages, pagedSize, numTokens, numLayers, layerIdx, ++ page2L); ++ break; ++ default: ++ return; ++ } ++} ++ ++} // namespace lmc +diff --git a/csrc/ascend/kernels/multi_layer_mem_kernels.cpp b/csrc/ascend/kernels/multi_layer_mem_kernels.cpp +new file mode 100644 +index 0000000..3e94f63 +--- /dev/null ++++ b/csrc/ascend/kernels/multi_layer_mem_kernels.cpp +@@ -0,0 +1,234 @@ ++/* ++ * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. ++ * ++ * Licensed under the Apache License, Version 2.0 (the "License"); ++ * you may not use this file except in compliance with the License. ++ * You may obtain a copy of the License at ++ * ++ * http://www.apache.org/licenses/LICENSE-2.0 ++ * ++ * Unless required by applicable law or agreed to in writing, software ++ * distributed under the License is distributed on an "AS IS" BASIS, ++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++ * See the License for the specific language governing permissions and ++ * limitations under the License. ++ */ ++#include "kernel_operator.h" ++#include ++#include "types.h" ++#include "utils.h" ++ ++template class MultiLayerPagedKVCopy { ++ using local_scalar_t = AscendC::LocalTensor; ++ ++public: ++ __aicore__ inline MultiLayerPagedKVCopy() ++ { ++ } ++ ++ __aicore__ inline void init(GM_ADDR pagedKVCaches, GM_ADDR cacheTensor, GM_ADDR slotmappings, ++ const int64_t hiddenDims, const int32_t numLayers, const int64_t pageBuffSize, ++ const int32_t numTokensChunk, const bool page2L, ++ AscendC::TPipe *pipe) ++ { ++ this->pipe_ = pipe; ++ this->numLayers_ = numLayers; ++ this->hiddenDims_ = hiddenDims; ++ this->pageBuffSize_ = pageBuffSize; ++ this->numTokensChunk_ = numTokensChunk; ++ this->page2L_ = page2L; ++ this->valid_ = true; ++ ++ this->pipe_->InitBuffer(pagedTokenQue_, 4, this->hiddenDims_*sizeof(scalar_t)); ++ } ++ ++ __aicore__ inline void reset(){ ++ this->valid_ = true; ++ } ++ ++ __aicore__ inline void updateMemOffset(__gm__ uint8_t *pagedKVCaches, __gm__ uint8_t* cacheTensor, ++ __gm__ uint8_t *slotmappings, const int tokenIdx, ++ const int kvIdx, const int layerIdx) ++ { ++ __gm__ slot_t *slotmappingPtr = reinterpret_cast<__gm__ slot_t*>(slotmappings); ++ int64_t slot = static_cast(slotmappingPtr[tokenIdx]); ++ ++ if (slot == -1) { ++ this->valid_ = false; ++ return; ++ } ++ ++ // its a pointer within the GM addr space, that point to another GM addr space ++ __gm__ uint8_t * __gm__ *pagedKVCachesPtr = reinterpret_cast<__gm__ uint8_t* __gm__ *>(pagedKVCaches); ++ ++ // getting the right ptr to the paged kvcache layer ++ __gm__ uint8_t *pagedLayerKVCaches = pagedKVCachesPtr[layerIdx]; ++ ++ int64_t pagedIdxOffset = kvIdx * this->pageBuffSize_ * this->hiddenDims_ + ++ slot * this->hiddenDims_; ++ ++ int64_t dstTensorIdxOffset = kvIdx * this->numLayers_ * this->numTokensChunk_ * this->hiddenDims_ + ++ layerIdx * this->numTokensChunk_ * this->hiddenDims_ + ++ tokenIdx * this->hiddenDims_; ++ ++ this->pagedTokenGlobal_.SetGlobalBuffer(reinterpret_cast<__gm__ scalar_t*>(pagedLayerKVCaches) + pagedIdxOffset, ++ this->hiddenDims_); ++ this->lmcBufferGlobal_.SetGlobalBuffer(reinterpret_cast<__gm__ scalar_t*>(cacheTensor) + dstTensorIdxOffset, ++ this->hiddenDims_); ++ } ++ ++ __aicore__ inline void processFunc() { ++ if (!this->valid_) { ++ return; ++ } ++ // 1. Alloc Tensor for local page ++ local_scalar_t hiddenDimTensor = pagedTokenQue_.AllocTensor(); ++ ++ // 2. copy from global tensor into local ++ if (this->page2L_) { ++ AscendC::DataCopy(hiddenDimTensor, this->pagedTokenGlobal_, this->hiddenDims_); ++ } else { ++ AscendC::DataCopy(hiddenDimTensor, this->lmcBufferGlobal_, this->hiddenDims_); ++ } ++ ++ // 3. enque vecin ++ pagedTokenQue_.EnQue(hiddenDimTensor); ++ // 4. deque vecin, possible to reuse due to QueBind ++ hiddenDimTensor = pagedTokenQue_.DeQue(); ++ ++ // 5. datacopy into GM ++ if (this->page2L_) { ++ AscendC::DataCopy(this->lmcBufferGlobal_, hiddenDimTensor, this->hiddenDims_); ++ } else { ++ AscendC::DataCopy(this->pagedTokenGlobal_, hiddenDimTensor, this->hiddenDims_); ++ } ++ ++ // 6. free alloced Tensor ++ pagedTokenQue_.FreeTensor(hiddenDimTensor); ++ } ++ ++ ++private: ++ AscendC::TPipe *pipe_; ++ AscendC::TQueBind pagedTokenQue_; ++ ++ // [layers * [kvs, numPages * pagedSize, heads*headsize]] ++ AscendC::GlobalTensor pagedTokenGlobal_; ++ // [kvs, layers, numTokensChunk, heads*headsize] ++ AscendC::GlobalTensor lmcBufferGlobal_; ++ int32_t numLayers_; // num layers ++ int64_t pageBuffSize_; // pages * pageSize ++ int64_t hiddenDims_; // heads * headSize ++ int32_t numTokensChunk_; // num tokens in the cache tensor chunk ++ bool valid_; ++ bool page2L_; // true, from pagedTensor to LMC, false otherwise ++}; ++ ++// NOTE: there are potential micro optimizaiton here. ++#define MULTI_LAYER_PAGED_KV_COPY_TYPE_DECLARE(TYPE, SLOTTYPE) \ ++ extern "C" __global__ __aicore__ void multi_layer_paged_kv_copy_##TYPE##_##SLOTTYPE( \ ++ __gm__ uint8_t* pagedKVCaches, __gm__ uint8_t* dstCacheTensor, __gm__ uint8_t* slotmappings, \ ++ const int64_t hiddenDims, const int32_t kvs, const int32_t numLayers, \ ++ const int64_t pageBuffSize, const int32_t numTokensChunk, const int coreNum, const bool page2L) \ ++ { \ ++ AscendC::TPipe pipe; \ ++ MultiLayerPagedKVCopy op{}; \ ++ op.init(pagedKVCaches, dstCacheTensor, slotmappings, hiddenDims, \ ++ numLayers, pageBuffSize, numTokensChunk, page2L, &pipe); \ ++ int64_t bIdx = AscendC::GetBlockIdx(); \ ++ for (int64_t i = bIdx; i < numTokensChunk; i+=coreNum) { \ ++ for (int32_t kvIdx = 0; kvIdx < kvs; kvIdx ++) { \ ++ for (int32_t layerIdx = 0; layerIdx < numLayers; layerIdx++) { \ ++ op.reset(); \ ++ op.updateMemOffset(pagedKVCaches, dstCacheTensor, slotmappings, i, kvIdx, layerIdx); \ ++ op.processFunc(); \ ++ } \ ++ } \ ++ } \ ++ } ++ ++// Declare support kernel entry ++MULTI_LAYER_PAGED_KV_COPY_TYPE_DECLARE(half, int32_t); ++MULTI_LAYER_PAGED_KV_COPY_TYPE_DECLARE(half, int64_t); ++MULTI_LAYER_PAGED_KV_COPY_TYPE_DECLARE(bfloat16_t, int32_t); ++MULTI_LAYER_PAGED_KV_COPY_TYPE_DECLARE(bfloat16_t, int64_t); ++MULTI_LAYER_PAGED_KV_COPY_TYPE_DECLARE(int8_t, int32_t); ++MULTI_LAYER_PAGED_KV_COPY_TYPE_DECLARE(int8_t, int64_t); ++ ++namespace lmc { ++ ++#define MULTI_LAYER_PAGED_KV_COPY_KERNEL_CALL(TYPE, SLOTTYPE) \ ++ multi_layer_paged_kv_copy_##TYPE##_##SLOTTYPE<<>>(pagedKVCaches, dstCacheTensor, \ ++ slotmappings, hiddenDims, kvs, \ ++ numLayers, pageBuffSize, \ ++ numTokensChunk, blockDim, page2L); ++ ++template ++void multi_layer_paged_kernel(uint32_t blockDim, void *stream, uint8_t *pagedKVCaches, uint8_t *dstCacheTensor, ++ uint8_t *slotmappings, const int64_t hiddenDims, const int32_t kvs, const int32_t numLayers, ++ const int64_t pageBuffSize, const int32_t numTokensChunk, const bool page2L); ++ ++#define MULTI_LAYER_PAGED_KERNEL_CALL_TYPE_DECLARE(TYPE, SLOTTYPE) \ ++template<> \ ++void multi_layer_paged_kernel(uint32_t blockDim, void *stream, uint8_t *pagedKVCaches, \ ++ uint8_t *dstCacheTensor, uint8_t *slotmappings, \ ++ const int64_t hiddenDims, const int32_t kvs, const int32_t numLayers, \ ++ const int64_t pageBuffSize, const int32_t numTokensChunk, \ ++ const bool page2L){ \ ++ MULTI_LAYER_PAGED_KV_COPY_KERNEL_CALL(TYPE, SLOTTYPE); \ ++} ++ ++MULTI_LAYER_PAGED_KERNEL_CALL_TYPE_DECLARE(half, int32_t); ++MULTI_LAYER_PAGED_KERNEL_CALL_TYPE_DECLARE(half, int64_t); ++MULTI_LAYER_PAGED_KERNEL_CALL_TYPE_DECLARE(bfloat16_t, int32_t); ++MULTI_LAYER_PAGED_KERNEL_CALL_TYPE_DECLARE(bfloat16_t, int64_t); ++MULTI_LAYER_PAGED_KERNEL_CALL_TYPE_DECLARE(int8_t, int32_t); ++MULTI_LAYER_PAGED_KERNEL_CALL_TYPE_DECLARE(int8_t, int64_t); ++ ++template ++void dispatch_paged_kernel_on_slot_type(vllm_ascend::AscendType slotType, uint32_t blockDim, void *stream, ++ uint8_t *pagedKVCaches, uint8_t *dstCacheTensor, uint8_t *slotmappings, ++ const int64_t hiddenDims, const int32_t kvs, const int32_t numLayers, ++ const int64_t pageBuffSize, const int32_t numTokensChunk, const bool page2L) { ++ switch(slotType) { ++ case vllm_ascend::AscendType::INT32: ++ multi_layer_paged_kernel(blockDim, stream, pagedKVCaches, dstCacheTensor, slotmappings, ++ hiddenDims, kvs, numLayers, pageBuffSize, numTokensChunk, page2L); ++ break; ++ case vllm_ascend::AscendType::INT64: ++ multi_layer_paged_kernel(blockDim, stream, pagedKVCaches, dstCacheTensor, slotmappings, ++ hiddenDims, kvs, numLayers, pageBuffSize, numTokensChunk, page2L); ++ break; ++ default: ++ return; ++ } ++} ++ ++extern void multi_layer_kv_transfer_kernel(vllm_ascend::AscendType type, vllm_ascend::AscendType slotType, ++ uint32_t blockDim, void *stream, uint8_t *pagedKVCaches, ++ uint8_t *dstCacheTensor, uint8_t *slotmappings, ++ const int64_t hiddenDims, const int32_t kvs, const int32_t numLayers, ++ const int64_t pageBuffSize, const int32_t numTokensChunk, const bool page2L) ++{ ++ switch(type) { ++ case vllm_ascend::AscendType::FP16: ++ dispatch_paged_kernel_on_slot_type(slotType, blockDim, stream, pagedKVCaches, dstCacheTensor, ++ slotmappings, hiddenDims, kvs, numLayers, pageBuffSize, ++ numTokensChunk, page2L); ++ break; ++ case vllm_ascend::AscendType::BF16: ++ dispatch_paged_kernel_on_slot_type(slotType, blockDim, stream, pagedKVCaches, dstCacheTensor, ++ slotmappings, hiddenDims, kvs, numLayers, pageBuffSize, ++ numTokensChunk, page2L); ++ break; ++ case vllm_ascend::AscendType::INT8: ++ dispatch_paged_kernel_on_slot_type(slotType, blockDim, stream, pagedKVCaches, dstCacheTensor, ++ slotmappings, hiddenDims, kvs, numLayers, pageBuffSize, ++ numTokensChunk, page2L); ++ break; ++ default: ++ return; ++ } ++} ++ ++} // namespace lmc +diff --git a/csrc/ascend/kernels/single_layer_mem_kernels.cpp b/csrc/ascend/kernels/single_layer_mem_kernels.cpp +new file mode 100644 +index 0000000..a1cc30d +--- /dev/null ++++ b/csrc/ascend/kernels/single_layer_mem_kernels.cpp +@@ -0,0 +1,310 @@ ++/* ++ * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. ++ * ++ * Licensed under the Apache License, Version 2.0 (the "License"); ++ * you may not use this file except in compliance with the License. ++ * You may obtain a copy of the License at ++ * ++ * http://www.apache.org/licenses/LICENSE-2.0 ++ * ++ * Unless required by applicable law or agreed to in writing, software ++ * distributed under the License is distributed on an "AS IS" BASIS, ++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++ * See the License for the specific language governing permissions and ++ * limitations under the License. ++ */ ++#include "kernel_operator.h" ++#include ++#include "types.h" ++#include "utils.h" ++ ++template class SingleLayerPagedKVCopy { ++ using local_scalar_t = AscendC::LocalTensor; ++ ++public: ++ __aicore__ inline SingleLayerPagedKVCopy() ++ { ++ } ++ ++ __aicore__ inline void init(GM_ADDR cacheTensor, GM_ADDR keyCachePtr, GM_ADDR valueCachePtr, GM_ADDR slotmappings, ++ const int64_t hiddenDims, const int32_t numTokens, const bool page2L, ++ const bool tokenMajor, AscendC::TPipe *pipe) ++ { ++ this->pipe_ = pipe; ++ this->hiddenDims_ = hiddenDims; ++ this->numTokens_ = numTokens; ++ this->tokenMajor_ = tokenMajor; ++ this->valid_ = true; ++ this->page2L_ = page2L; ++ if constexpr (IsMLA) { ++ this->numKvs_ = 1; ++ } else { ++ this->numKvs_ = 2; ++ } ++ // TODO: Not sure how many to allocate, but let's do 4 blocks of hiddenDims_ ++ // if it was fp16, 2048, we would get 16kb ? ++ this->pipe_->InitBuffer(this->pagedTokenQue_, 4, this->hiddenDims_*sizeof(scalar_t)); ++ } ++ ++ __aicore__ inline void reset(){ ++ this->valid_ = true; ++ } ++ ++ __aicore__ inline void updateTensorMemOffsetAndProcess(__gm__ uint8_t *pagedTensor, __gm__ uint8_t* nonPagedTensor, ++ __gm__ uint8_t *slotmappings, const int tokenIdx, const int kvIdx) ++ { ++ __gm__ slot_t *slotmappingPtr = reinterpret_cast<__gm__ slot_t*>(slotmappings); ++ int64_t slot = slotmappingPtr[tokenIdx]; ++ ++ if (slot == -1) { ++ this->valid_ = false; ++ return; ++ } ++ ++ // for the page tensor ++ int64_t pagedIdxOffset = slot * this->hiddenDims_; ++ ++ // for the lmc tensor ++ int64_t nonPagedIdxOffset = -1; ++ if (this->tokenMajor_) { ++ nonPagedIdxOffset = tokenIdx * this->numKvs_ * this->hiddenDims_ + ++ kvIdx * this->hiddenDims_; ++ } else { ++ nonPagedIdxOffset = kvIdx * this->numTokens_ * this -> hiddenDims_ + ++ tokenIdx * this->hiddenDims_; ++ } ++ ++ if (kvIdx == 0) { ++ // keys ++ this->keyTokensGlobal_.SetGlobalBuffer(reinterpret_cast<__gm__ scalar_t*>(pagedTensor) + pagedIdxOffset, ++ this->hiddenDims_); ++ this->lmcBufferKeyGlobal_.SetGlobalBuffer(reinterpret_cast<__gm__ scalar_t*>(nonPagedTensor) + nonPagedIdxOffset, ++ this->hiddenDims_); ++ } else { ++ // values ++ this->valueTokensGlobal_.SetGlobalBuffer(reinterpret_cast<__gm__ scalar_t*>(pagedTensor) + pagedIdxOffset, ++ this->hiddenDims_); ++ this->lmcBufferValueGlobal_.SetGlobalBuffer(reinterpret_cast<__gm__ scalar_t*>(nonPagedTensor) + nonPagedIdxOffset, ++ this->hiddenDims_); ++ } ++ } ++ ++ __aicore__ inline void processFunc() { ++ if (!this->valid_) { ++ return; ++ } ++ // 1. Alloc Tensor for local page ++ local_scalar_t hiddenKeysDimTensor = this->pagedTokenQue_.template AllocTensor(); ++ local_scalar_t hiddenValuesDimTensor; ++ if constexpr(!IsMLA) { ++ hiddenValuesDimTensor = this->pagedTokenQue_.template AllocTensor(); ++ } ++ ++ // 2. copy from global tensor into local ++ if (this->page2L_) { ++ AscendC::DataCopy(hiddenKeysDimTensor, this->keyTokensGlobal_, this->hiddenDims_); ++ if constexpr (!IsMLA) { ++ AscendC::DataCopy(hiddenValuesDimTensor, this->valueTokensGlobal_, this->hiddenDims_); ++ } ++ } else { ++ AscendC::DataCopy(hiddenKeysDimTensor, this->lmcBufferKeyGlobal_, this->hiddenDims_); ++ if constexpr(!IsMLA) { ++ AscendC::DataCopy(hiddenValuesDimTensor, this->lmcBufferValueGlobal_, this->hiddenDims_); ++ } ++ } ++ ++ // 3. enque vecin ++ pagedTokenQue_.EnQue(hiddenKeysDimTensor); ++ if constexpr(!IsMLA) { ++ pagedTokenQue_.EnQue(hiddenValuesDimTensor); ++ } ++ ++ // 4. deque vecin, possible to reuse due to QueBind ++ hiddenKeysDimTensor = pagedTokenQue_.DeQue(); ++ if constexpr(!IsMLA) { ++ hiddenValuesDimTensor = pagedTokenQue_.DeQue(); ++ } ++ ++ // 5. datacopy into GM ++ if (this->page2L_) { ++ AscendC::DataCopy(this->lmcBufferKeyGlobal_, hiddenKeysDimTensor, this->hiddenDims_); ++ if constexpr(!IsMLA) { ++ AscendC::DataCopy(this->lmcBufferValueGlobal_, hiddenValuesDimTensor, this->hiddenDims_); ++ } ++ } else { ++ AscendC::DataCopy(this->keyTokensGlobal_, hiddenKeysDimTensor, this->hiddenDims_); ++ if constexpr(!IsMLA) { ++ AscendC::DataCopy(this->valueTokensGlobal_, hiddenValuesDimTensor, this->hiddenDims_); ++ } ++ } ++ ++ // 6. free alloced Tensor ++ pagedTokenQue_.FreeTensor(hiddenKeysDimTensor); ++ if constexpr(!IsMLA) { ++ pagedTokenQue_.FreeTensor(hiddenValuesDimTensor); ++ } ++ } ++ ++private: ++ AscendC::TPipe *pipe_; ++ // a depth of 2 ++ AscendC::TQueBind pagedTokenQue_; ++ ++ // [kvs, numPages * pagedSize, heads*headsize] ++ AscendC::GlobalTensor keyTokensGlobal_; ++ // iff !isMLA ++ AscendC::GlobalTensor valueTokensGlobal_; ++ ++ // Depends on LMC setting whether we store in tokensMajor or not. ++ // the layout would be the followings: ++ // [tokens, kvs, heads*headsize] or [kvs, tokens, heads*headsize] ++ // TODO: check whether should combine the two and use a loop ++ AscendC::GlobalTensor lmcBufferKeyGlobal_; ++ AscendC::GlobalTensor lmcBufferValueGlobal_; ++ ++ int64_t hiddenDims_; // heads * headsize ++ int32_t numTokens_; // num tokens in the cache tensor chunk ++ int16_t numKvs_; // 1 if MLA else 2 ++ bool page2L_; // whether the direction of copy is from page to lmc ++ bool tokenMajor_; // whether the lmc buffer is in token major. ++ bool valid_; ++}; ++ ++#define SINGLE_LAYER_PAGED_KV_COPY_TYPE_DECLARE(TYPE, SLOTTYPE, ISMLA) \ ++ extern "C" __global__ __aicore__ void single_layer_paged_kv_copy_##TYPE##_##SLOTTYPE##_##ISMLA( \ ++ __gm__ uint8_t* dstCacheTensor, __gm__ uint8_t* keyCachePtr, __gm__ uint8_t* valueCachePtr, \ ++ __gm__ uint8_t* slotmappings, const int64_t hiddenDims, const int32_t numTokens, const int coreNums, \ ++ const bool page2L, const bool tokenMajor) \ ++ { \ ++ AscendC::TPipe pipe; \ ++ SingleLayerPagedKVCopy op{}; \ ++ op.init(dstCacheTensor, keyCachePtr, valueCachePtr, slotmappings, hiddenDims, numTokens, \ ++ page2L, tokenMajor, &pipe); \ ++ int64_t bIdx = AscendC::GetBlockIdx(); \ ++ for (int64_t i = bIdx; i < numTokens; i+=coreNums) \ ++ { \ ++ op.reset(); \ ++ op.updateTensorMemOffsetAndProcess(keyCachePtr, dstCacheTensor, slotmappings, i, 0); \ ++ if constexpr(!ISMLA) { \ ++ op.updateTensorMemOffsetAndProcess(valueCachePtr, dstCacheTensor, slotmappings, i, 1); \ ++ } \ ++ op.processFunc(); \ ++ } \ ++ } ++ ++// Declare support kernel entry ++SINGLE_LAYER_PAGED_KV_COPY_TYPE_DECLARE(half, int32_t, false); ++SINGLE_LAYER_PAGED_KV_COPY_TYPE_DECLARE(half, int32_t, true); ++SINGLE_LAYER_PAGED_KV_COPY_TYPE_DECLARE(bfloat16_t, int32_t, false); ++SINGLE_LAYER_PAGED_KV_COPY_TYPE_DECLARE(bfloat16_t, int32_t, true); ++SINGLE_LAYER_PAGED_KV_COPY_TYPE_DECLARE(int8_t, int32_t, false); ++SINGLE_LAYER_PAGED_KV_COPY_TYPE_DECLARE(int8_t, int32_t, true); ++ ++SINGLE_LAYER_PAGED_KV_COPY_TYPE_DECLARE(half, int64_t, false); ++SINGLE_LAYER_PAGED_KV_COPY_TYPE_DECLARE(half, int64_t, true); ++SINGLE_LAYER_PAGED_KV_COPY_TYPE_DECLARE(bfloat16_t, int64_t, false); ++SINGLE_LAYER_PAGED_KV_COPY_TYPE_DECLARE(bfloat16_t, int64_t, true); ++SINGLE_LAYER_PAGED_KV_COPY_TYPE_DECLARE(int8_t, int64_t, false); ++SINGLE_LAYER_PAGED_KV_COPY_TYPE_DECLARE(int8_t, int64_t, true); ++ ++namespace lmc { ++ ++#define SINGLE_LAYER_PAGED_KV_COPY_KERNEL_CALL(TYPE, SLOTTYPE, ISMLA) \ ++ single_layer_paged_kv_copy_##TYPE##_##SLOTTYPE##_##ISMLA<<>>(dstCacheTensor, \ ++ keyCachePtr, valueCachePtr, slotmappings, hiddenDims, \ ++ numTokens, blockDim, page2L, tokenMajor); ++ ++template ++void single_layer_paged_kernel(uint32_t blockDim, void *stream, uint8_t *dstCacheTensor, uint8_t *keyCachePtr, ++ uint8_t *valueCachePtr, uint8_t *slotmappings, const int64_t hiddenDims, ++ const int32_t numTokens, const bool page2L, const bool tokenMajor); ++ ++#define SINGLE_LAYER_PAGED_KERNEL_CALL_TYPE_DECLARE(TYPE, SLOTTYPE, ISMLA) \ ++template<> \ ++void single_layer_paged_kernel(uint32_t blockDim, void *stream, uint8_t *dstCacheTensor, \ ++ uint8_t *keyCachePtr, uint8_t *valueCachePtr, uint8_t *slotmappings, \ ++ const int64_t hiddenDims, const int32_t numTokens, const bool page2L, \ ++ const bool tokenMajor){ \ ++ SINGLE_LAYER_PAGED_KV_COPY_KERNEL_CALL(TYPE, SLOTTYPE, ISMLA); \ ++} ++ ++ ++SINGLE_LAYER_PAGED_KERNEL_CALL_TYPE_DECLARE(half, int32_t, false); ++SINGLE_LAYER_PAGED_KERNEL_CALL_TYPE_DECLARE(half, int64_t, false); ++SINGLE_LAYER_PAGED_KERNEL_CALL_TYPE_DECLARE(bfloat16_t, int32_t, false); ++SINGLE_LAYER_PAGED_KERNEL_CALL_TYPE_DECLARE(bfloat16_t, int64_t, false); ++SINGLE_LAYER_PAGED_KERNEL_CALL_TYPE_DECLARE(int8_t, int32_t, false); ++SINGLE_LAYER_PAGED_KERNEL_CALL_TYPE_DECLARE(int8_t, int64_t, false); ++ ++SINGLE_LAYER_PAGED_KERNEL_CALL_TYPE_DECLARE(half, int32_t, true); ++SINGLE_LAYER_PAGED_KERNEL_CALL_TYPE_DECLARE(half, int64_t, true); ++SINGLE_LAYER_PAGED_KERNEL_CALL_TYPE_DECLARE(bfloat16_t, int32_t, true); ++SINGLE_LAYER_PAGED_KERNEL_CALL_TYPE_DECLARE(bfloat16_t, int64_t, true); ++SINGLE_LAYER_PAGED_KERNEL_CALL_TYPE_DECLARE(int8_t, int32_t, true); ++SINGLE_LAYER_PAGED_KERNEL_CALL_TYPE_DECLARE(int8_t, int64_t, true); ++ ++ ++ ++template ++void dispatch_single_layer_kernel_on_slot_type(vllm_ascend::AscendType slotType, uint32_t blockDim, void *stream, ++ uint8_t *dstCacheTensor, uint8_t *keyCachePtr, uint8_t *valueCachePtr, ++ uint8_t *slotmappings, const int64_t hiddenDims, const int32_t numTokens, ++ const bool page2L, const bool tokenMajor, const bool isMLA) { ++ if (isMLA) { ++ switch(slotType) { ++ case vllm_ascend::AscendType::INT32: ++ single_layer_paged_kernel(blockDim, stream, dstCacheTensor, keyCachePtr, valueCachePtr, ++ slotmappings, hiddenDims, numTokens, page2L, tokenMajor); ++ break; ++ case vllm_ascend::AscendType::INT64: ++ single_layer_paged_kernel(blockDim, stream, dstCacheTensor, keyCachePtr, valueCachePtr, ++ slotmappings, hiddenDims, numTokens, page2L, tokenMajor); ++ break; ++ default: ++ return; ++ } ++ } else { ++ switch(slotType) { ++ case vllm_ascend::AscendType::INT32: ++ single_layer_paged_kernel(blockDim, stream, dstCacheTensor, keyCachePtr, valueCachePtr, ++ slotmappings, hiddenDims, numTokens, page2L, tokenMajor); ++ break; ++ case vllm_ascend::AscendType::INT64: ++ single_layer_paged_kernel(blockDim, stream, dstCacheTensor, keyCachePtr, valueCachePtr, ++ slotmappings, hiddenDims, numTokens, page2L, tokenMajor); ++ break; ++ default: ++ return; ++ } ++ } ++ ++} ++ ++ ++extern void single_layer_kv_transfer_kernel(vllm_ascend::AscendType type, vllm_ascend::AscendType slotType, ++ uint32_t blockDim, void *stream, uint8_t *dstCacheTensor, ++ uint8_t *keyCachePtr, uint8_t *valueCachePtr, ++ uint8_t *slotmappings, const int64_t hiddenDims, const int32_t numTokens, ++ const bool page2L, const bool tokenMajor, const bool isMLA) ++{ ++ switch(type) { ++ case vllm_ascend::AscendType::FP16: ++ dispatch_single_layer_kernel_on_slot_type(slotType, blockDim, stream, dstCacheTensor, keyCachePtr, ++ valueCachePtr, slotmappings, hiddenDims, numTokens, page2L, ++ tokenMajor, isMLA); ++ break; ++ case vllm_ascend::AscendType::BF16: ++ dispatch_single_layer_kernel_on_slot_type(slotType, blockDim, stream, dstCacheTensor, keyCachePtr, ++ valueCachePtr, slotmappings, hiddenDims, numTokens, ++ page2L, tokenMajor, isMLA); ++ break; ++ case vllm_ascend::AscendType::INT8: ++ dispatch_single_layer_kernel_on_slot_type(slotType, blockDim, stream, dstCacheTensor, keyCachePtr, ++ valueCachePtr, slotmappings, hiddenDims, numTokens, page2L, ++ tokenMajor, isMLA); ++ default: ++ return; ++ } ++} ++ ++} // namespace lmc +diff --git a/csrc/ascend/kernels/types.h b/csrc/ascend/kernels/types.h +new file mode 100644 +index 0000000..7c6c46e +--- /dev/null ++++ b/csrc/ascend/kernels/types.h +@@ -0,0 +1,28 @@ ++/* ++ * Copyright (c) Huawei Technologies Co., Ltd. 2024. All rights reserved. ++ * ++ * Licensed under the Apache License, Version 2.0 (the "License"); ++ * you may not use this file except in compliance with the License. ++ * You may obtain a copy of the License at ++ * ++ * http://www.apache.org/licenses/LICENSE-2.0 ++ * ++ * Unless required by applicable law or agreed to in writing, software ++ * distributed under the License is distributed on an "AS IS" BASIS, ++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++ * See the License for the specific language governing permissions and ++ * limitations under the License. ++ */ ++ ++#pragma once ++ ++namespace vllm_ascend { ++enum struct AscendType { ++ FP16 = 0, ++ BF16 = 1, ++ FP32 = 2, ++ INT8 = 3, ++ INT32 = 4, ++ INT64 = 5, ++}; ++} +\ No newline at end of file +diff --git a/csrc/ascend/kernels/utils.h b/csrc/ascend/kernels/utils.h +new file mode 100644 +index 0000000..a6dd3f3 +--- /dev/null ++++ b/csrc/ascend/kernels/utils.h +@@ -0,0 +1,38 @@ ++/* ++ * Copyright (c) Huawei Technologies Co., Ltd. 2024. All rights reserved. ++ * ++ * Licensed under the Apache License, Version 2.0 (the "License"); ++ * you may not use this file except in compliance with the License. ++ * You may obtain a copy of the License at ++ * ++ * http://www.apache.org/licenses/LICENSE-2.0 ++ * ++ * Unless required by applicable law or agreed to in writing, software ++ * distributed under the License is distributed on an "AS IS" BASIS, ++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++ * See the License for the specific language governing permissions and ++ * limitations under the License. ++ */ ++ ++#pragma once ++#include "kernel_type.h" ++namespace vllm_ascend { ++ ++template struct AccType; ++ ++template <> struct AccType { ++ using type = float; ++}; ++ ++template <> struct AccType { ++ using type = half; ++}; ++ ++template <> struct AccType { ++ using type = float; ++}; ++ ++template <> struct AccType { ++ using type = int; ++}; ++}// namespace vllm_ascend +\ No newline at end of file +diff --git a/csrc/ascend/managed_mem.cpp b/csrc/ascend/managed_mem.cpp +new file mode 100644 +index 0000000..4c79f54 +--- /dev/null ++++ b/csrc/ascend/managed_mem.cpp +@@ -0,0 +1,326 @@ ++#include "managed_mem.h" ++#include ++// Only required for old driver version (look at registerHostPtr) ++#ifdef PROF_ERROR ++ // You can add a pragma message to see this in your build log if you want: ++ // #pragma message("Undefining PROF_ERROR from ascend_hal.h before NPU headers") ++ #undef PROF_ERROR ++#endif ++ ++#include ++#include ++#include "driver/ascend_hal_define.h" ++#include "driver/ascend_hal.h" ++#include ++#include "torch/torch.h" ++#include "torch/extension.h" ++ ++namespace lmc { ++constexpr int32_t PROT_FLAGS = static_cast(PROT_READ) | static_cast(PROT_WRITE); ++constexpr int32_t MAP_FLAGS = static_cast(MAP_PRIVATE) | static_cast(MAP_ANONYMOUS) | static_cast(MAP_POPULATE); ++ ++// Signatures for internal helper functions ++ ++// Get the version of the NPU driver as a string ++std::string get_driver_version(); ++// Checks whether the major version of the NPU is greater or equal 25 to support aclrtHostRegister ++bool is_version_at_least_25(const std::string& version_str); ++// Gets the current device offsetting on ASCEND_RT_VISIBLE_DEVICES when needed ++int get_device(); ++// Uregisters the malloced hostPtr ++void unregisterPtr(void* ptr); ++// Swaps the host memory allocated to a tensor with the given hostPtr ++void swap_tensor_ptr(void* hostPtr, torch::Tensor& original_tensor); ++ ++// Class implementations ++ ++HostRegisteredMemoryManager::HostRegisteredMemoryManager(){ ++}; ++ ++HostRegisteredMemoryManager::~HostRegisteredMemoryManager() { ++ this->unregisterAll(); ++}; ++ ++void HostRegisteredMemoryManager::unregisterAll(){ ++ const std::unique_lock guard(this->mux); ++ ++ // Iterate through each key-value pair in the map. ++ for (const auto& pair : this->allocatedMap) { ++ void* hostPtr = pair.first; ++ aclrtHostUnregister(hostPtr); ++ } ++ ++ // After unregistering all pointers, clear the map completely. ++ this->allocatedMap.clear(); ++}; ++ ++// Register a pointer through high level APIs (aclrt) return devPtr ++// Returns the created RegisteredMemoryRecord ++RegisteredMemoryRecord HostRegisteredMemoryManager::registerHostPtr(void* hostPtr, size_t bufferSize) { // torch::Tensor& tensor){ ++ TORCH_CHECK(!(hostPtr == nullptr || bufferSize == 0), "Error: hostPtr cannot be null and bufferSize must be greater than 0."); ++ const std::unique_lock guard(this->mux); ++ ++ // Check if the host pointer is already registered ++ if (this->allocatedMap.count(hostPtr)) { ++ return this->allocatedMap[hostPtr]; ++ } ++ ++ void* devPtr; ++ aclError err = aclrtHostRegister(hostPtr, static_cast(bufferSize), ++ ACL_HOST_REGISTER_MAPPED, (void**)&devPtr); ++ TORCH_CHECK(err == 0, "Unable to host register the host ptr: " + std::to_string(err)); ++ ++ this->allocatedMap.emplace(hostPtr, RegisteredMemoryRecord{reinterpret_cast(hostPtr), ++ reinterpret_cast(devPtr), bufferSize}); ++ ++ return this->allocatedMap[hostPtr]; ++}; ++ ++// Register a pointer through low level APIs (HAL). Allocates a new pinned host memory ++// This should be used for driver versions, where cannot rely on aclrtHostRegister() ++// Returns the created RegisteredMemoryRecord ++RegisteredMemoryRecord HostRegisteredMemoryManager::halRegisterHostPtr(size_t bufferSize){ ++ // We allocate a new chunk of memory, register it, and replace the tensor. ++ // Essentially, the halHostRegister function requires a ptr given by mmap. ++ TORCH_CHECK((bufferSize >= 0), "Error: bufferSize must be greater than 0."); ++ const std::unique_lock guard(this->mux); ++ ++ void* devPtr; ++ int device = get_device(); ++ void* hostPtr; ++ // Allocate and register ++ hostPtr = mmap(nullptr, bufferSize, PROT_FLAGS, MAP_FLAGS, -1, 0); ++ TORCH_CHECK(hostPtr != MAP_FAILED, "Unable to alloc memory with mmap."); ++ auto ret = madvise(reinterpret_cast(hostPtr), bufferSize, MADV_HUGEPAGE); ++ auto drvRet = halHostRegister((void*)hostPtr, static_cast(bufferSize), ++ HOST_MEM_MAP_DEV_PCIE_TH, (UINT32)device, (void**)&devPtr); ++ TORCH_CHECK(drvRet == 0, "Unable to register host memory with hal: " + std::to_string(drvRet)) ++ ++ // Lock the memory and fail if impossible to lock ++ auto lockErr = mlock(reinterpret_cast(hostPtr), bufferSize); ++ if (lockErr == -1) { ++ // This can happen in non-privileged mode or not enough rlimit, ++ // let's not proceed since we wanted to guarantee pinned ++ // because we already alloced, let's free ++ auto ret = halHostUnregisterEx(reinterpret_cast(hostPtr), ++ static_cast(device), HOST_MEM_MAP_DEV_PCIE_TH); ++ TORCH_CHECK(ret==0, "Unable to pin host memory, unable to unregister. Error code: " + std::to_string(ret)) ++ auto mret = munmap(reinterpret_cast(hostPtr), bufferSize); ++ TORCH_CHECK(false, "Unable to pin host memory with error code: " + std::to_string(lockErr)) ++ } ++ ++ this->allocatedMap.emplace(hostPtr, RegisteredMemoryRecord{reinterpret_cast(hostPtr), ++ reinterpret_cast(devPtr), bufferSize}); ++ ++ return this->allocatedMap[hostPtr]; ++}; ++ ++void HostRegisteredMemoryManager::unregisterMemory(void* hostPtr) { ++ TORCH_CHECK(hostPtr != nullptr, "Error: hostPtr cannot be null."); ++ ++ // we don't actually mind if it doesn't unregister, ++ // at context destroy it should be unregister anyway. ++ const std::unique_lock guard(this->mux); ++ aclError err = aclrtHostUnregister(hostPtr); ++ this->allocatedMap.erase(hostPtr); ++}; ++ ++/* ++* For now we only do a linear search as we probably won't have a long list of ptrs ++* we go through each record and check whether we are in range, if so ++* we calculate the offset from the host ptr and apply to the device ptr ++* finally we return the device ptr. ++*/ ++void* HostRegisteredMemoryManager::getDevicePtr(void* hostPtr) { ++ if (hostPtr == nullptr) { ++ return nullptr; ++ } ++ const std::shared_lock guard(this->mux); ++ ++ const uintptr_t hostAddrPtr = reinterpret_cast(hostPtr); ++ ++ for (const auto& pair: this->allocatedMap) { ++ const RegisteredMemoryRecord& record = pair.second; ++ ++ if (hostAddrPtr >= record.ptr && hostAddrPtr < (record.ptr + record.buffSize)) { ++ const size_t offset = hostAddrPtr - record.ptr; ++ ++ const uintptr_t deviceAddrPtr = record.devptr + offset; ++ ++ return reinterpret_cast(deviceAddrPtr); ++ } ++ } ++ ++ return nullptr; ++}; ++ ++ ++size_t HostRegisteredMemoryManager::getRecordSize(void* hostPtr){ ++ if (hostPtr == nullptr) { ++ return 0; ++ } ++ const std::shared_lock guard(this->mux); ++ ++ const uintptr_t hostAddrPtr = reinterpret_cast(hostPtr); ++ ++ for (const auto& pair: this->allocatedMap) { ++ const RegisteredMemoryRecord& record = pair.second; ++ ++ if (hostAddrPtr >= record.ptr && hostAddrPtr < (record.ptr + record.buffSize)) { ++ return record.buffSize; ++ } ++ } ++ return 0; ++}; ++ ++std::string get_driver_version() { ++ void* handle = nullptr; ++ int (*dsmi_get_version)(int, char*, unsigned int, unsigned int*) = nullptr; ++ std::string result; ++ ++ handle = dlopen("libdrvdsmi_host.so", RTLD_LAZY); ++ if (!handle) { ++ TORCH_CHECK(false, std::string("Error opening libdrvdsmi_host.so: ") + dlerror() ); ++ return result; ++ } ++ dlerror(); ++ ++ // Load the function ++ *(void**) (&dsmi_get_version) = dlsym(handle, "dsmi_get_version"); ++ const char* dlsym_error = dlerror(); ++ if (dlsym_error) { ++ dlclose(handle); ++ TORCH_CHECK(false, std::string("Error loading dsmi_get_version: ") + dlsym_error); ++ return result; ++ } ++ ++ // Call the function ++ int device_id = c10_npu::getCurrentNPUStream().device_index(); ++ const unsigned int buffer_size = 256; ++ std::vector version_buffer(buffer_size); ++ unsigned int ret_len = 0; ++ int ret = dsmi_get_version(device_id, version_buffer.data(), buffer_size, &ret_len); ++ if (ret == 0) { ++ if (ret_len > 0 && ret_len <= buffer_size) { ++ version_buffer[ret_len] = '\0'; // Ensure null-termination ++ result = version_buffer.data(); ++ } else { ++ TORCH_CHECK(false, "Error: Invalid length returned: " + std::to_string(ret_len)); ++ } ++ } else { ++ TORCH_CHECK(false, "Error: dsmi_get_version returned " + std::to_string(ret)); ++ } ++ ++ dlclose(handle); ++ ++ return result; ++} ++ ++// To be on the safe side, returns false in case of uncertainties ++bool is_version_at_least_25(const std::string& version_str) { ++ if (version_str.empty()) { ++ return false; ++ } ++ ++ size_t num_end = 0; ++ long major_version = 0; ++ ++ try { ++ major_version = std::stol(version_str, &num_end); ++ } catch (const std::invalid_argument&) { ++ // No valid number at start ++ return false; ++ } catch (const std::out_of_range&) { ++ // Should never happen, here for robustness ++ return false; ++ } ++ return major_version >= 25; ++} ++ ++int get_device(){ ++ int device = c10_npu::getCurrentNPUStream().device_index(); ++ const char* env_visible_devices_p = std::getenv("ASCEND_RT_VISIBLE_DEVICES"); ++ // If we are using a custom list of visible devices, the index refers to that ++ if (env_visible_devices_p != nullptr) { ++ std::string env_visible_devices = env_visible_devices_p; ++ std::vector list_visible_devices; ++ std::stringstream ss(env_visible_devices); ++ std::string item; ++ while (std::getline(ss, item, ',')) { ++ list_visible_devices.push_back(std::stoi(item)); ++ } ++ std::sort(list_visible_devices.begin(), list_visible_devices.end()); ++ // Here two cases are possible: ++ // 1. no hccl, we just use current_device, even though we have specify the ASCEND_RT_VISIBLE_DEVICES ++ // 2. hccl, and we use current_device that seems to be correct ++ // for case 2, since the current_device would have been correct anyway, obtaining from the list would be fine. ++ // for case 1, we have shifted the device to the RT_VISIBLE_DEVICES, so it should be corrected. ++ device = list_visible_devices[device]; ++ } ++ return device; ++} ++ ++void unregisterPtr(void* ptr) { ++ if (ptr){ ++ int device = get_device(); ++ auto& hmm = HostRegisteredMemoryManager::GetInstance(); ++ size_t bufferSize = hmm.getRecordSize(ptr); ++ auto ret = halHostUnregisterEx(reinterpret_cast(ptr), ++ static_cast(device), HOST_MEM_MAP_DEV_PCIE_TH); ++ if (ret != 0) { ++ std::cout << "Unable to hal host unregister: "<< ret << std::endl; ++ } ++ auto mret = munmap(reinterpret_cast(ptr), bufferSize); ++ if (mret != 0) { ++ std::cout << "Unable to unmap memory: "<< ret << std::endl; ++ } ++ } ++} ++ ++ ++void swap_tensor_ptr(void* hostPtr, torch::Tensor& original_tensor){ ++ torch::TensorOptions tensorOpsCpu = torch::TensorOptions() ++ .dtype(original_tensor.dtype()) ++ .device(original_tensor.device()) ++ .pinned_memory(true); ++ int64_t numel = static_cast(original_tensor.nbytes()); ++ std::vector dims = {numel}; ++ torch::Tensor new_tensor_from_myptr = torch::from_blob( ++ hostPtr, dims, unregisterPtr, tensorOpsCpu); ++ ++ original_tensor.set_(new_tensor_from_myptr.storage(), original_tensor.storage_offset(), ++ original_tensor.sizes(), original_tensor.strides()); ++} ++ ++} // namespace lmc ++ ++ ++void* register_memory(torch::Tensor& tensor) { ++ torch::Device device = tensor.device(); ++ if (!device.is_cpu() || !tensor.is_pinned()) { ++ TORCH_CHECK(false, "Invalid device. Device must be CPU and tensor must be pinned."); ++ } ++ auto& hmm = lmc::HostRegisteredMemoryManager::GetInstance(); ++ size_t tensorSize = tensor.nbytes(); ++ std::string verString = lmc::get_driver_version(); ++ if (lmc::is_version_at_least_25(verString)) { // New driver version, supports aclrtHostRegister() ++ void* hostPtr = static_cast(tensor.data_ptr()); ++ return (void*) hmm.registerHostPtr(hostPtr, tensorSize).devptr; ++ } else { // Old driver version, does not support aclrtHostRegister(), we have to use HAL. ++ // We ask for a new registerd memory and substitute with the previously allocated. ++ lmc::RegisteredMemoryRecord record = hmm.halRegisterHostPtr(tensorSize); ++ lmc::swap_tensor_ptr((void*) record.ptr, tensor); ++ return (void*) record.devptr; ++ } ++}; ++ ++void unregister_memory(torch::Tensor& tensor) { ++ void* hostPtr = static_cast(tensor.data_ptr()); ++ auto& hmm = lmc::HostRegisteredMemoryManager::GetInstance(); ++ hmm.unregisterMemory(hostPtr); ++}; ++ ++void* get_device_ptr(void* ptr) { ++ auto& hmm = lmc::HostRegisteredMemoryManager::GetInstance(); ++ return hmm.getDevicePtr(ptr); ++}; +diff --git a/csrc/ascend/managed_mem.h b/csrc/ascend/managed_mem.h +new file mode 100644 +index 0000000..ae42364 +--- /dev/null ++++ b/csrc/ascend/managed_mem.h +@@ -0,0 +1,69 @@ ++#pragma once ++#include ++#include ++#include ++#include ++ ++namespace lmc { ++ ++struct RegisteredMemoryRecord { ++ uintptr_t ptr; ++ uintptr_t devptr; ++ size_t buffSize; ++}; ++ ++/* ++* We are not responsible for acl init and ctx initialization, ++* we assume the user responsible for ctx initialization ++*/ ++class HostRegisteredMemoryManager { ++private: ++ HostRegisteredMemoryManager(); ++ ++ // Delete copy constructor and assignment operator ++ HostRegisteredMemoryManager(const HostRegisteredMemoryManager&) = delete; ++ HostRegisteredMemoryManager& operator=(const HostRegisteredMemoryManager&) = delete; ++ HostRegisteredMemoryManager(HostRegisteredMemoryManager&&) = delete; ++ HostRegisteredMemoryManager& operator=(HostRegisteredMemoryManager&&) = delete; ++ ++ std::map allocatedMap; ++ mutable std::shared_mutex mux; ++ ++public: ++ static HostRegisteredMemoryManager& GetInstance() ++ { ++ static HostRegisteredMemoryManager instance; ++ return instance; ++ } ++ ~HostRegisteredMemoryManager(); ++ ++ // Register a pointer through high level APIs (aclrt) return devPtr ++ // Returns an already existing RegisteredMemoryRecord or the newly created one ++ // Inputs: ++ // -hostPtr: host pointer of the allocated memory area to register on device ++ // -bufferSize: size of the allocated memory area to register on device ++ RegisteredMemoryRecord registerHostPtr(void* hostPtr, size_t bufferSize); //torch::Tensor& tensor); // ++ // Register a pointer through low level APIs (hal) ++ // This should be used for driver versions, where cannot rely on aclrtHostRegister() ++ // Returns the created RegisteredMemoryRecord ++ // Inputs: ++ // -bufferSize: size of the allocated memory area to register on device ++ RegisteredMemoryRecord halRegisterHostPtr(size_t bufferSize); ++ void unregisterMemory(void* hostPtr); ++ void* getDevicePtr(void* hostPtr); ++ size_t getRecordSize(void* hostPtr); ++ void unregisterAll(); ++}; ++} // namespace lmc ++ ++// Register a tensor on the current device ++// Inputs: ++// -tensor: The tensor to register on the device ++// Returns the device ptr for that tensor ++void* register_memory(torch::Tensor& tensor); ++// Reverse of register ++// Inputs: ++// -tensor: The tensor to register on the device ++void unregister_memory(torch::Tensor& tensor); ++// Takes in input a host pointer, returns the corresponding device pointer ++void* get_device_ptr(void* ptr); +diff --git a/csrc/ascend/mem_kernels.cpp b/csrc/ascend/mem_kernels.cpp +new file mode 100644 +index 0000000..6526836 +--- /dev/null ++++ b/csrc/ascend/mem_kernels.cpp +@@ -0,0 +1,245 @@ ++#include "mem_kernels.h" ++#include ++#include ++#include ++#include ++#include "utils.h" ++#include "tiling/platform/platform_ascendc.h" ++#include ++#include ++ ++template ++T* get_kernel_ptr(TENSOR_TYPE& tensor) { ++ torch::Device device = tensor.device(); ++ // NPU should be using PrivateUse1 ++ if (device.is_privateuseone() || device.is_cuda()) { ++ return static_cast(tensor.data_ptr()); ++ } else if (device.is_cpu()) { ++ // find device ptr based on the host pinned ptr ++ // because acl does not currently support HostGetDevicePointer API ++ void* devPtr = get_device_ptr(tensor.data_ptr()); ++ TORCH_CHECK(devPtr != nullptr, "Unable to retrieve device ptr, is this a host registered pointer ?"); ++ return reinterpret_cast(devPtr); ++ } else { ++ TORCH_CHECK(false, "Invalid device. Device must be ascend (PrivateUseOne) or pinned cpu."); ++ } ++} ++ ++/** ++ * Quickly offload KV cache from vLLM paged memory to the offloading buffer ++ * Processes all the layers at the same time ++ * ++ * Each layer in vLLM's KV buffer has a shape of ++ * [2, PAGE_BUFFER_SIZE, num_heads*head_size] ++ * ++ * Each AIV Core processes the copy for a token ++ * ++ * Therefore: ++ * AIV Core - token ++ * ++ * The function does: ++ * slot_id = slot_mapping[tokenId] ++ * ptrs[mem_offset(kv, layer, tokenId, hiddenDims)] = key_value[mem_offset(kv, layer, pages, pageSize, slot_id, hiddenDims)] ++ * ++ * Param: ++ * - direction: false means LMCache to PagedBuffer, true means PagedBuffer to ++ * LMCache ++ */ ++void multi_layer_kv_transfer(torch::Tensor& key_value, // [kv, num_layer, num_tokens, hidden] ++ const torch::Tensor& key_value_ptrs, // [num_layers] ++ const torch::Tensor& slot_mapping, // [num_tokens] ++ const torch::Device& paged_memory_device, ++ const int page_buffer_size, const bool direction, ++ const bool use_mla) { ++ uint8_t* key_value_ptr = get_kernel_ptr(key_value); ++ // it is actually a uint8_t**. we will reinterpret it inside the kernel ++ uint8_t* page_buffer_ptrs = get_kernel_ptr(key_value_ptrs); ++ uint8_t* slot_mapping_ptr = get_kernel_ptr(slot_mapping); ++ ++ int num_layers = key_value.size(1); ++ int num_tokens = slot_mapping.size(0); ++ int hidden_dims = key_value.size(-1); ++ int kv_size = 2; ++ if (use_mla) { ++ kv_size = 1; ++ } ++ ++ const c10::OptionalDeviceGuard device_guard(paged_memory_device); ++ // we require the kv ptr list to be on the device too ++ const c10::OptionalDeviceGuard kv_device_guard(device_of(key_value_ptrs)); ++ ++ const aclrtStream stream = c10_npu::getCurrentNPUStream().stream(); ++ at::ScalarType scalar_type = key_value.scalar_type(); ++ at::ScalarType slot_type = slot_mapping.scalar_type(); ++ const char* socName = aclrtGetSocName(); ++ ++ at_npu::native::OpCommand cmd; ++ cmd.Name("multi_layer_kv_transfer_kernel"); ++ cmd.SetCustomHandler([scalar_type, slot_type, socName, stream, page_buffer_ptrs, key_value_ptr, ++ slot_mapping_ptr, hidden_dims, kv_size, num_layers, page_buffer_size, ++ num_tokens, direction]()->int{ ++ auto slot_num = vllm_ascend::get_dtype_from_torch(slot_type); ++ auto dtype_num = vllm_ascend::get_dtype_from_torch(scalar_type); ++ auto ascendcPlatform = platform_ascendc::PlatformAscendCManager::GetInstance(socName); ++ uint32_t aiv_num = ascendcPlatform->GetCoreNumAiv(); ++ lmc::multi_layer_kv_transfer_kernel(dtype_num, slot_num, aiv_num, stream, page_buffer_ptrs, key_value_ptr, ++ slot_mapping_ptr, hidden_dims, kv_size, num_layers, page_buffer_size, ++ num_tokens, direction); ++ return 0; ++ }); ++ cmd.Run(); ++ return ; ++}; ++ ++ ++void multi_layer_kv_transfer_unilateral(torch::Tensor& key_value, ++ const torch::Tensor& key_ptrs, ++ const torch::Tensor& value_ptrs, ++ const torch::Tensor& slot_mapping, ++ const torch::Device& paged_memory_device, ++ const int page_buffer_size, ++ const bool direction){ ++ // TODO: ++ PyErr_SetString(PyExc_NotImplementedError, "Please contact LMCache Ascend."); ++ throw py::error_already_set(); ++}; ++ ++ ++void single_layer_kv_transfer(torch::Tensor& lmc_key_value_cache, // [num_tokens, 2, num_heads*head_size] ++ // or ++ // [2, num_tokens, num_heads*head_size] ++ torch::Tensor& vllm_key_cache, // [num_blocks, block_size, num_heads, head_size] ++ torch::Tensor& vllm_value_cache, // [....] ++ torch::Tensor& slot_mapping, // [num_tokens] ++ const bool direction, // false: LMCache to PagedBuffer, true: PagedBuffer to LMCache ++ const bool token_major // true: lmc_key_value_cache is [num_tokens, 2, num_heads*head_size] ++ // false: otherwise ++) { ++ uint8_t *lmc_key_value_cache_ptr = get_kernel_ptr(lmc_key_value_cache); ++ uint8_t *vllm_key_cache_ptr = get_kernel_ptr(vllm_key_cache); ++ uint8_t *vllm_value_cache_ptr = get_kernel_ptr(vllm_value_cache); ++ uint8_t *slot_mapping_ptr = get_kernel_ptr(slot_mapping); ++ ++ int num_tokens = slot_mapping.size(0); ++ int hidden_dims = lmc_key_value_cache.size(-1); ++ ++ const c10::OptionalDeviceGuard device_guard(device_of(vllm_key_cache)); ++ const c10::OptionalDeviceGuard slot_device_guard(device_of(slot_mapping)); ++ const aclrtStream stream = c10_npu::getCurrentNPUStream().stream(); ++ ++ at::ScalarType scalar_type = vllm_key_cache.scalar_type(); ++ at::ScalarType slot_type = slot_mapping.scalar_type(); ++ ++ const char* socName = aclrtGetSocName(); ++ ++ at_npu::native::OpCommand cmd; ++ cmd.Name("single_layer_kv_transfer_kernel"); ++ cmd.SetCustomHandler([scalar_type, slot_type, socName, stream, lmc_key_value_cache_ptr, ++ vllm_key_cache_ptr, vllm_value_cache_ptr, slot_mapping_ptr, ++ hidden_dims, num_tokens, direction, token_major]() -> int { ++ auto slot_num = vllm_ascend::get_dtype_from_torch(slot_type); ++ auto dtype_num = vllm_ascend::get_dtype_from_torch(scalar_type); ++ auto ascendcPlatform = platform_ascendc::PlatformAscendCManager::GetInstance(socName); ++ uint32_t aiv_num = ascendcPlatform->GetCoreNumAiv(); ++ // TODO: We will add the isMLA argument once the signature have support for the MLA. ++ lmc::single_layer_kv_transfer_kernel(dtype_num, slot_num, aiv_num, stream, lmc_key_value_cache_ptr, ++ vllm_key_cache_ptr, vllm_value_cache_ptr, slot_mapping_ptr, ++ hidden_dims, num_tokens, direction, token_major, false); ++ return 0; ++ }); ++ cmd.Run(); ++ return ; ++}; ++ ++void load_and_reshape_flash( ++ torch::Tensor& key_value, // [2, num_layer, num_tokens, num_heads*head_size] ++ // must be one gpu / pinned cpu ++ torch::Tensor& key_cache, // [num_blocks, block_size, num_heads, head_size] ++ torch::Tensor& value_cache, // [num_blocks, block_size, num_heads, head_size] ++ torch::Tensor& slot_mapping, // [num_tokens], ++ const int layer_idx) { ++ ++ uint8_t* key_value_ptr = get_kernel_ptr(key_value); ++ uint8_t* key_cache_ptr = get_kernel_ptr(key_cache); ++ uint8_t* value_cache_ptr = get_kernel_ptr(value_cache); ++ ++ uint8_t* slot_mapping_ptr = get_kernel_ptr(slot_mapping); ++ ++ int num_tokens = slot_mapping.size(0); ++ int num_layers = key_value.size(1); ++ int block_size = key_cache.size(1); ++ int num_blocks = key_cache.size(0); ++ int hidden_dims = key_value.size(-1); ++ const c10::OptionalDeviceGuard device_guard(device_of(key_cache)); ++ const aclrtStream stream = c10_npu::getCurrentNPUStream().stream(); ++ ++ at::ScalarType scalar_type = key_value.scalar_type(); ++ at::ScalarType slot_type = slot_mapping.scalar_type(); ++ const char* socName = aclrtGetSocName(); ++ ++ at_npu::native::OpCommand cmd; ++ cmd.Name("load_and_reshape_flash_kernel"); ++ cmd.SetCustomHandler([scalar_type, slot_type, socName, stream, key_value_ptr, ++ key_cache_ptr, value_cache_ptr, slot_mapping_ptr, ++ hidden_dims, num_blocks, block_size, ++ num_tokens, num_layers, layer_idx]()->int { ++ auto slot_num = vllm_ascend::get_dtype_from_torch(slot_type); ++ auto dtype_num = vllm_ascend::get_dtype_from_torch(scalar_type); ++ auto ascendcPlatform = platform_ascendc::PlatformAscendCManager::GetInstance(socName); ++ uint32_t aiv_num = ascendcPlatform->GetCoreNumAiv(); ++ lmc::load_and_reshape_flash_kernel(dtype_num, slot_num, aiv_num, stream, key_value_ptr, ++ key_cache_ptr, value_cache_ptr, slot_mapping_ptr, ++ hidden_dims, num_blocks, block_size, ++ num_tokens, num_layers, layer_idx, true); ++ return 0; ++ }); ++ cmd.Run(); ++ return; ++}; ++ ++void reshape_and_cache_back_flash( ++ torch::Tensor& key_value, // [2, num_layer, num_tokens, num_heads*head_size] ++ // must be one gpu / pinned cpu ++ torch::Tensor& key_cache, // [num_blocks, block_size, num_heads, head_size] ++ torch::Tensor& value_cache, // [num_blocks, block_size, num_heads, head_size] ++ torch::Tensor& slot_mapping, // [num_tokens], ++ const int layer_idx) { ++ ++ uint8_t* key_value_ptr = get_kernel_ptr(key_value); ++ uint8_t* key_cache_ptr = get_kernel_ptr(key_cache); ++ uint8_t* value_cache_ptr = get_kernel_ptr(value_cache); ++ ++ uint8_t* slot_mapping_ptr = get_kernel_ptr(slot_mapping); ++ ++ int num_tokens = slot_mapping.size(0); ++ int num_layers = key_value.size(1); ++ int block_size = key_cache.size(1); ++ int num_blocks = key_cache.size(0); ++ int hidden_dims = key_value.size(-1); ++ const c10::OptionalDeviceGuard device_guard(device_of(key_cache)); ++ const aclrtStream stream = c10_npu::getCurrentNPUStream().stream(); ++ ++ at::ScalarType scalar_type = key_value.scalar_type(); ++ at::ScalarType slot_type = slot_mapping.scalar_type(); ++ ++ const char* socName = aclrtGetSocName(); ++ ++ at_npu::native::OpCommand cmd; ++ cmd.Name("reshape_and_cache_back_flash"); ++ cmd.SetCustomHandler([scalar_type, slot_type, socName, stream, key_value_ptr, ++ key_cache_ptr, value_cache_ptr, slot_mapping_ptr, ++ hidden_dims, num_blocks, block_size, ++ num_tokens, num_layers, layer_idx]() -> int { ++ auto slot_num = vllm_ascend::get_dtype_from_torch(slot_type); ++ auto dtype_num = vllm_ascend::get_dtype_from_torch(scalar_type); ++ auto ascendcPlatform = platform_ascendc::PlatformAscendCManager::GetInstance(socName); ++ uint32_t aiv_num = ascendcPlatform->GetCoreNumAiv(); ++ lmc::load_and_reshape_flash_kernel(dtype_num, slot_num, aiv_num, stream, key_value_ptr, ++ key_cache_ptr, value_cache_ptr, slot_mapping_ptr, ++ hidden_dims, num_blocks, block_size, ++ num_tokens, num_layers, layer_idx, false); ++ return 0; ++ }); ++ cmd.Run(); ++ return; ++}; +diff --git a/csrc/ascend/mem_kernels.h b/csrc/ascend/mem_kernels.h +new file mode 100644 +index 0000000..01e0494 +--- /dev/null ++++ b/csrc/ascend/mem_kernels.h +@@ -0,0 +1,58 @@ ++#pragma once ++#include ++#include ++#include "managed_mem.h" ++#include "kernels/types.h" ++ ++namespace lmc { ++void multi_layer_kv_transfer_kernel(vllm_ascend::AscendType type, vllm_ascend::AscendType slotType, uint32_t blockDim, ++ void *stream, uint8_t *pagedKVCaches, uint8_t *dstCacheTensor, ++ uint8_t *slotmappings, const int64_t hiddenDims, const int32_t kvs, ++ const int32_t numLayers, const int64_t pageBuffSize, const int32_t numTokensChunk, ++ const bool page2L); ++ ++void single_layer_kv_transfer_kernel(vllm_ascend::AscendType type, vllm_ascend::AscendType slotType, ++ uint32_t blockDim, void *stream, uint8_t *dstCacheTensor, ++ uint8_t *keyCachePtr, uint8_t *valueCachePtr, ++ uint8_t *slotmappings, const int64_t hiddenDims, const int32_t numTokens, ++ const bool page2L, const bool tokenMajor, const bool isMLA); ++ ++void load_and_reshape_flash_kernel(vllm_ascend::AscendType type, vllm_ascend::AscendType slotType, ++ uint32_t blockDim, void *stream, uint8_t *dstCacheTensor, uint8_t *keyCachePtr, ++ uint8_t *valueCachePtr, uint8_t *slotmappings, const int64_t hiddenDims, ++ const int64_t numPages, const int32_t pagedSize, const int32_t numTokens, ++ const int32_t numLayers, const int32_t layerIdx, const bool page2L); ++} ++ ++ ++void multi_layer_kv_transfer(torch::Tensor& key_value, // [kv, num_layer, num_tokens, hidden] ++ const torch::Tensor& key_value_ptrs, // [num_layers] ++ const torch::Tensor& slot_mapping, // [num_tokens] ++ const torch::Device& paged_memory_device, ++ const int page_buffer_size, const bool direction, ++ const bool use_mla); ++ ++void multi_layer_kv_transfer_unilateral(torch::Tensor& key_value, ++ const torch::Tensor& key_ptrs, ++ const torch::Tensor& value_ptrs, ++ const torch::Tensor& slot_mapping, ++ const torch::Device& paged_memory_device, ++ const int page_buffer_size, ++ const bool direction); ++ ++void single_layer_kv_transfer(torch::Tensor& lmc_key_value_cache, ++ torch::Tensor& vllm_key_cache, ++ torch::Tensor& vllm_value_cache, ++ torch::Tensor& slot_mapping, ++ const bool direction, ++ const bool token_major = false); ++ ++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); ++ ++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); +\ No newline at end of file +diff --git a/csrc/ascend/pos_kernels.cpp b/csrc/ascend/pos_kernels.cpp +new file mode 100644 +index 0000000..01ee897 +--- /dev/null ++++ b/csrc/ascend/pos_kernels.cpp +@@ -0,0 +1,14 @@ ++#include "pos_kernels.h" ++#include ++#include ++ ++namespace py = pybind11; ++ ++void rotary_embedding_k_fused(const torch::Tensor& old_positions, ++ const torch::Tensor& new_positions, ++ torch::Tensor& key, int64_t head_size, ++ const torch::Tensor& cos_sin_cache, bool is_neox) { ++ // TODO: ++ PyErr_SetString(PyExc_NotImplementedError, "Please contact LMCache Ascend."); ++ throw py::error_already_set(); ++}; +\ No newline at end of file +diff --git a/csrc/ascend/pos_kernels.h b/csrc/ascend/pos_kernels.h +new file mode 100644 +index 0000000..b0bcaf1 +--- /dev/null ++++ b/csrc/ascend/pos_kernels.h +@@ -0,0 +1,10 @@ ++#pragma once ++#include ++#include ++#include ++#include ++ ++void rotary_embedding_k_fused(const torch::Tensor& old_positions, ++ const torch::Tensor& new_positions, ++ torch::Tensor& key, int64_t head_size, ++ const torch::Tensor& cos_sin_cache, bool is_neox); +\ No newline at end of file +diff --git a/csrc/ascend/torch_tensor.h b/csrc/ascend/torch_tensor.h +new file mode 100644 +index 0000000..f1842dd +--- /dev/null ++++ b/csrc/ascend/torch_tensor.h +@@ -0,0 +1,33 @@ ++#pragma once ++#include ++#include ++#include ++#include "managed_mem.h" ++ ++void unregisterPtr(void* ptr) { ++ if (ptr) { ++ auto& hmm = lmc::HostRegisteredMemoryManager::GetInstance(); ++ hmm.unregisterMemory(ptr); ++ } ++} ++ ++torch::Tensor create_pinned_host_registered_tensor(size_t bufferSize) { ++ torch::TensorOptions tensorOpsCpu = torch::TensorOptions() ++ .dtype(torch::kUInt8) ++ .device(torch::kCPU) ++ .pinned_memory(true); ++ TORCH_CHECK(bufferSize > 0, "Buffer size must be greater than zero. Got: " + std::to_string(bufferSize)); ++ ++ // unlikely this would be greater than int64_t ++ int64_t numel = static_cast(bufferSize); ++ ++ void* hostPtr; ++ aclError err = aclrtMallocHost((void**)&hostPtr, bufferSize); ++ TORCH_CHECK(err == 0, "Unable to malloc host buffer, error: " + std::to_string(err)); ++ ++ auto& hmm = lmc::HostRegisteredMemoryManager::GetInstance(); ++ hmm.registerHostPtr(hostPtr, bufferSize); ++ ++ std::vector dims = {numel}; ++ return torch::from_blob(hostPtr, dims, unregisterPtr, tensorOpsCpu); ++} +\ No newline at end of file +diff --git a/csrc/ascend/utils.cmake b/csrc/ascend/utils.cmake +new file mode 100644 +index 0000000..ebf06c6 +--- /dev/null ++++ b/csrc/ascend/utils.cmake +@@ -0,0 +1,28 @@ ++# ++# Run `EXPR` in python. The standard output of python is stored in `OUT` and ++# has trailing whitespace stripped. If an error is encountered when running ++# python, a fatal message `ERR_MSG` is issued. ++# ++function (run_python OUT EXPR ERR_MSG) ++ execute_process( ++ COMMAND ++ "${PYTHON_EXECUTABLE}" "-c" "${EXPR}" ++ OUTPUT_VARIABLE PYTHON_OUT ++ RESULT_VARIABLE PYTHON_ERROR_CODE ++ ERROR_VARIABLE PYTHON_STDERR ++ OUTPUT_STRIP_TRAILING_WHITESPACE) ++ ++ if(NOT PYTHON_ERROR_CODE EQUAL 0) ++ message(FATAL_ERROR "${ERR_MSG}: ${PYTHON_STDERR}") ++ endif() ++ set(${OUT} ${PYTHON_OUT} PARENT_SCOPE) ++endfunction() ++ ++# Run `EXPR` in python after importing `PKG`. Use the result of this to extend ++# `CMAKE_PREFIX_PATH` so the torch cmake configuration can be imported. ++macro (append_cmake_prefix_path PKG EXPR) ++ run_python(_PREFIX_PATH ++ "import ${PKG}; print(${EXPR})" "Failed to locate ${PKG} path") ++ list(APPEND CMAKE_PREFIX_PATH ${_PREFIX_PATH}) ++endmacro() ++ +diff --git a/csrc/ascend/utils.h b/csrc/ascend/utils.h +new file mode 100644 +index 0000000..c1c02cd +--- /dev/null ++++ b/csrc/ascend/utils.h +@@ -0,0 +1,39 @@ ++/* ++ * Copyright (c) Huawei Technologies Co., Ltd. 2024. All rights reserved. ++ * ++ * Licensed under the Apache License, Version 2.0 (the "License"); ++ * you may not use this file except in compliance with the License. ++ * You may obtain a copy of the License at ++ * ++ * http://www.apache.org/licenses/LICENSE-2.0 ++ * ++ * Unless required by applicable law or agreed to in writing, software ++ * distributed under the License is distributed on an "AS IS" BASIS, ++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++ * See the License for the specific language governing permissions and ++ * limitations under the License. ++ */ ++#pragma once ++ ++#include "kernels/types.h" ++#include ++#include ++ ++namespace vllm_ascend { ++AscendType get_dtype_from_torch(at::ScalarType scalarType) ++{ ++ if (scalarType == at::ScalarType::Float) { ++ return AscendType::FP32; ++ } else if (scalarType == at::ScalarType::BFloat16) { ++ return AscendType::BF16; ++ } else if (scalarType == at::ScalarType::Half) { ++ return AscendType::FP16; ++ } else if (scalarType == at::ScalarType::Long) { ++ return AscendType::INT64; ++ } else if (scalarType == at::ScalarType::Int) { ++ return AscendType::INT32; ++ } else { ++ TORCH_CHECK(false, "ScalarType not supported."); ++ } ++}; ++} // namespace vllm_ascend +diff --git a/csrc/pybind.cpp b/csrc/pybind.cpp +index 4b4b900..d7ad00d 100644 +--- a/csrc/pybind.cpp ++++ b/csrc/pybind.cpp +@@ -15,19 +15,29 @@ + */ + + #include +-#include "mem_kernels.cuh" +-#include "cachegen_kernels.cuh" +-#include "pos_kernels.cuh" ++#ifdef USE_ASCEND ++ #include "ascend/mem_kernels.h" ++ #include "ascend/managed_mem.h" ++ #include "ascend/cachegen_kernels.h" ++ #include "ascend/pos_kernels.h" ++#else ++ #include "mem_kernels.cuh" ++ #include "cachegen_kernels.cuh" ++ #include "pos_kernels.cuh" ++#endif + #include + #include + + namespace py = pybind11; + + PYBIND11_MODULE(c_ops, m) { ++#ifdef USE_ASCEND ++ m.def("host_register", ®ister_memory); ++#endif + m.def("multi_layer_kv_transfer", &multi_layer_kv_transfer); ++ m.def("single_layer_kv_transfer", &single_layer_kv_transfer); + m.def("multi_layer_kv_transfer_unilateral", + &multi_layer_kv_transfer_unilateral); +- m.def("single_layer_kv_transfer", &single_layer_kv_transfer); + m.def("load_and_reshape_flash", &load_and_reshape_flash); + m.def("reshape_and_cache_back_flash", &reshape_and_cache_back_flash); + m.def("encode_fast_new", &encode_cuda_new); +diff --git a/lmcache/integration/vllm/vllm_adapter.py b/lmcache/integration/vllm/vllm_adapter.py +index a316561..8527d02 100644 +--- a/lmcache/integration/vllm/vllm_adapter.py ++++ b/lmcache/integration/vllm/vllm_adapter.py +@@ -23,25 +23,35 @@ from torch.nn.utils.rnn import pad_sequence + import torch + import torch.distributed as dist + +-if TYPE_CHECKING: +- from vllm.worker.model_runner import ModelInputForGPUWithSamplingMetadata +- + # Third Party + from vllm.attention import AttentionMetadata + +-# from vllm.attention.backends.flash_attn import FlashAttentionMetadata +-try: ++from vllm.platforms import current_platform ++if current_platform.is_cuda() or current_platform.device_type == "cuda": ++ if TYPE_CHECKING: ++ from vllm.worker.model_runner import ModelInputForGPUWithSamplingMetadata ++ ++ # from vllm.attention.backends.flash_attn import FlashAttentionMetadata ++ try: ++ # Third Party ++ from vllm.attention.backends.flash_attn import FlashAttentionMetadata ++ except (ModuleNotFoundError, ImportError): ++ # vllm_flash_attn is not installed, try the ROCm FA metadata ++ from vllm.attention.backends.rocm_flash_attn import ( ++ ROCmFlashAttentionMetadata as FlashAttentionMetadata, ++ ) ++ + # Third Party +- from vllm.attention.backends.flash_attn import FlashAttentionMetadata +-except (ModuleNotFoundError, ImportError): +- # vllm_flash_attn is not installed, try the ROCm FA metadata +- from vllm.attention.backends.rocm_flash_attn import ( +- ROCmFlashAttentionMetadata as FlashAttentionMetadata, +- ) ++ from vllm.attention.backends.flashmla import FlashMLAMetadata ++ from vllm.attention.backends.mla.common import MLACommonMetadata ++elif current_platform.device_name == "npu": ++ if TYPE_CHECKING: ++ from vllm_ascend.worker.model_runner import ModelInputForNPUWithSamplingMetadata as ModelInputForGPUWithSamplingMetadata ++ from vllm_ascend.attention.attention_v1 import AscendMetadata as FlashAttentionMetadata ++ from vllm_ascend.attention.mla_v1 import AscendMLAMetadata as FlashMLAMetadata, AscendMLAMetadata as MLACommonMetadata ++ from torch_npu.contrib import transfer_to_npu ++ import lmcache.c_ops as lmc_ops + +-# Third Party +-from vllm.attention.backends.flashmla import FlashMLAMetadata +-from vllm.attention.backends.mla.common import MLACommonMetadata + from vllm.config import ( + CacheConfig, + ModelConfig, +@@ -224,9 +234,9 @@ def init_lmcache_engine( + use_mla=use_mla, + ) + engine = LMCacheEngineBuilder.get_or_create( +- ENGINE_NAME, config, metadata, vllm_gpu_connector ++ ENGINE_NAME, config, metadata, vllm_gpu_connector, device=current_platform.device_name + ) +- ++ + return engine + + +diff --git a/lmcache/v1/cache_engine.py b/lmcache/v1/cache_engine.py +index eaba752..4fb57a3 100644 +--- a/lmcache/v1/cache_engine.py ++++ b/lmcache/v1/cache_engine.py +@@ -769,6 +769,7 @@ class LMCacheEngineBuilder: + def _Create_memory_allocator( + config: LMCacheEngineConfig, + metadata: LMCacheEngineMetadata, ++ device: str = "cuda", + ) -> MemoryAllocatorInterface: + if config.enable_nixl: + assert config.nixl_buffer_device is not None +@@ -810,7 +811,7 @@ class LMCacheEngineBuilder: + return CuFileMemoryAllocator(config.cufile_buffer_size * 1024**2) + + max_local_cpu_size = config.max_local_cpu_size +- return MixedMemoryAllocator(int(max_local_cpu_size * 1024**3)) ++ return MixedMemoryAllocator(int(max_local_cpu_size * 1024**3), device=device) + + @staticmethod + def _Create_token_database( +@@ -828,6 +829,7 @@ class LMCacheEngineBuilder: + config: LMCacheEngineConfig, + metadata: LMCacheEngineMetadata, + gpu_connector: GPUConnectorInterface, ++ device: str = "cuda" + ) -> LMCacheEngine: + """ + Builds a new LMCacheEngine instance if it doesn't already exist for the +@@ -838,7 +840,7 @@ class LMCacheEngineBuilder: + """ + logger.info(f"Creating LMCacheEngine instance {instance_id}") + if instance_id not in cls._instances: +- memory_allocator = cls._Create_memory_allocator(config, metadata) ++ memory_allocator = cls._Create_memory_allocator(config, metadata, device=device) + token_database = cls._Create_token_database(config, metadata) + stat_logger = LMCacheStatsLogger(metadata, log_interval=10) + +@@ -885,4 +887,4 @@ class LMCacheEngineBuilder: + cls._cfgs.pop(instance_id, None) + cls._metadatas.pop(instance_id, None) + cls._stat_loggers.pop(instance_id, None) +- LMCStatsMonitor.DestroyInstance() ++ LMCStatsMonitor.DestroyInstance() +\ No newline at end of file +diff --git a/lmcache/v1/gpu_connector.py b/lmcache/v1/gpu_connector.py +index 62fe7d5..8b257c9 100644 +--- a/lmcache/v1/gpu_connector.py ++++ b/lmcache/v1/gpu_connector.py +@@ -18,6 +18,10 @@ import abc + + # Third Party + import torch ++try: ++ from torch_npu.contrib import transfer_to_npu ++except (ModuleNotFoundError, ImportError): ++ print("Not importing NPU packages. We are not running on Ascend") + + # First Party + from lmcache.integration.vllm.utils import ENGINE_NAME +@@ -153,7 +157,8 @@ class VLLMPagedMemGPUConnectorV2(GPUConnectorInterface): + def _initialize_pointers(self, kv_caches: List[torch.Tensor]) -> torch.Tensor: + self.kv_cache_pointers.numpy()[:] = [t.data_ptr() for t in kv_caches] + device = kv_caches[0].device +- assert device.type == "cuda", "The device should be CUDA." ++ from vllm.platforms import current_platform ++ assert device.type == current_platform.device_name # The device should be CUDA or Ascend based on vllm. + idx = device.index + if idx not in self.kv_cache_pointers_on_gpu: + self.kv_cache_pointers_on_gpu[idx] = torch.empty( +@@ -161,9 +166,9 @@ class VLLMPagedMemGPUConnectorV2(GPUConnectorInterface): + ) + self.kv_cache_pointers_on_gpu[idx].copy_(self.kv_cache_pointers) + if self.use_mla: +- # kv_caches[0].shape: [num_pages, page_size, head_size] +- assert kv_caches[0].dim() == 3 +- self.page_buffer_size = kv_caches[0].shape[0] * kv_caches[0].shape[1] ++ # kv_caches[0].shape: [num_pages, page_size, head_size] (vllm) or ++ # kv_caches[0].shape: [1, num_pages, page_size, head_size] (vllm-Ascend) ++ self.page_buffer_size = kv_caches[0].shape[-3] * kv_caches[0].shape[-2] + else: + # kv_caches[0].shape: [2, num_pages, page_size, num_heads, head_size] + assert kv_caches[0].dim() == 5 +@@ -798,7 +803,7 @@ class VLLMPagedMemLayerwiseGPUConnector(GPUConnectorInterface): + memory_obj.tensor, + self.kvcaches[layer_id][0], + self.kvcaches[layer_id][1], +- slot_mapping_full, ++ slot_mapping[start:end], + False, + True, + ) +@@ -819,7 +824,8 @@ class VLLMPagedMemLayerwiseGPUConnector(GPUConnectorInterface): + current_stream.wait_stream(self.load_stream) + + # free the buffer memory +- tmp_gpu_buffer_obj.ref_count_down() ++ if self.use_gpu: ++ tmp_gpu_buffer_obj.ref_count_down() + + logger.debug(f"Finished loading layer {layer_id}") + yield +@@ -932,7 +938,8 @@ class VLLMPagedMemLayerwiseGPUConnector(GPUConnectorInterface): + logger.debug(f"Finished offloading layer {layer_id}") + + # free the buffer memory +- tmp_gpu_buffer_obj.ref_count_down() ++ if self.use_gpu: ++ tmp_gpu_buffer_obj.ref_count_down() + yield + + def get_shape(self, num_tokens: int) -> torch.Size: +diff --git a/lmcache/v1/memory_management.py b/lmcache/v1/memory_management.py +index 68b6f12..a17cc8a 100644 +--- a/lmcache/v1/memory_management.py ++++ b/lmcache/v1/memory_management.py +@@ -31,6 +31,10 @@ import torch + from lmcache.logging import init_logger + from lmcache.observability import LMCStatsMonitor + from lmcache.utils import _lmcache_nvtx_annotate ++import lmcache.c_ops as lmc_ops ++ ++# Third Party ++from vllm.platforms import current_platform + + logger = init_logger(__name__) + +@@ -563,6 +567,19 @@ class MemoryAllocatorInterface(metaclass=abc.ABCMeta): + """ + raise NotImplementedError + ++ @abc.abstractmethod ++ def registerHostPtr( ++ self, ++ device: str = "cuda", ++ size: int = 0 ++ ): ++ """ ++ Register the host ptr with the device using device specific APIs. ++ ++ :param str device: The device type to register the host ptr with. ++ """ ++ raise NotImplementedError ++ + def close(self): + """ + Closes the memory allocator. +@@ -900,6 +917,10 @@ class TensorMemoryAllocator(MemoryAllocatorInterface): + clear = False + return clear + ++ def registerHostPtr(self, device: str = "cuda", size: int = 0): ++ # do nothing for tensor memory allocator ++ pass ++ + + class PagedTensorMemoryAllocator(MemoryAllocatorInterface): + """ +@@ -1151,6 +1172,10 @@ class PagedTensorMemoryAllocator(MemoryAllocatorInterface): + # FIXME: NIXL-related memory leak should be handled somewhere (else). + del self.buffer + ++ def registerHostPtr(self, device: str = "cuda", size: int = 0): ++ # do nothing for paged tensor memory allocator ++ pass ++ + + class BufferAllocator(MemoryAllocatorInterface): + """Allocates memory in the pre-allocated pinned memory.""" +@@ -1201,6 +1226,9 @@ class BufferAllocator(MemoryAllocatorInterface): + def memcheck(self): + return True + ++ def registerHostPtr(self, device: str = "cuda", size: int = 0): ++ pass ++ + + class HostMemoryAllocator(MemoryAllocatorInterface): + """Allocates memory in the pre-allocated Host memory.""" +@@ -1272,21 +1300,20 @@ class HostMemoryAllocator(MemoryAllocatorInterface): + with self.host_mem_lock: + return self.allocator.memcheck() + ++ def registerHostPtr(self, device: str = "cuda", size: int = 0): ++ pass + + class PinMemoryAllocator(MemoryAllocatorInterface): + """Allocates memory in the pre-allocated pinned memory.""" + +- def __init__(self, size: int, use_paging: bool = False, **kwargs): ++ def __init__(self, size: int, use_paging: bool = False, device: str = "cuda", **kwargs): + """ + :param int size: The size of the pinned memory in bytes. + """ + + self.buffer = torch.empty(size, dtype=torch.uint8) +- ptr = self.buffer.data_ptr() +- err = torch.cuda.cudart().cudaHostRegister(ptr, size, 0) +- assert err == 0, ( +- f"cudaHostRegister failed: {torch.cuda.cudart().cudaGetErrorString(err)}" +- ) ++ self._device = device ++ self.registerHostPtr(device, size) + self._unregistered = False + + if use_paging: +@@ -1353,9 +1380,22 @@ class PinMemoryAllocator(MemoryAllocatorInterface): + def close(self): + if not self._unregistered: + torch.cuda.synchronize() +- torch.cuda.cudart().cudaHostUnregister(self.buffer.data_ptr()) ++ if self._device == "cuda": ++ torch.cuda.cudart().cudaHostUnregister(self.buffer.data_ptr()) + self._unregistered = True + ++ def registerHostPtr(self, device: str = "cuda", size: int = 0): ++ ptr = self.buffer.data_ptr() ++ if device == "cuda": ++ assert size >= 0, ("size must be non-negative and greater than 0.") ++ err = torch.cuda.cudart().cudaHostRegister(ptr, size, 0) ++ assert err == 0, ( ++ f"cudaHostRegister failed: {torch.cuda.cudart().cudaGetErrorString(err)}" ++ ) ++ elif device == "npu": ++ # Ascend need to manually manage host register API and memory is pinned ++ self.buffer = self.buffer.pin_memory() ++ lmc_ops.host_register(self.buffer) + + class MixedMemoryAllocator(MemoryAllocatorInterface): + """ +@@ -1363,17 +1403,14 @@ class MixedMemoryAllocator(MemoryAllocatorInterface): + (2) byte_array buffer memory. + """ + +- def __init__(self, size: int, use_paging: bool = False, **kwargs): ++ def __init__(self, size: int, use_paging: bool = False, device: str = "cuda", **kwargs): + """ + :param int size: The size of the pinned memory in bytes. + """ + + self.buffer = torch.empty(size, dtype=torch.uint8) +- ptr = self.buffer.data_ptr() +- err = torch.cuda.cudart().cudaHostRegister(ptr, size, 0) +- assert err == 0, ( +- f"cudaHostRegister failed: {torch.cuda.cudart().cudaGetErrorString(err)}" +- ) ++ self._device = device ++ self.registerHostPtr(device, size) + self._unregistered = False + + if use_paging: +@@ -1487,8 +1524,22 @@ class MixedMemoryAllocator(MemoryAllocatorInterface): + def close(self): + if not self._unregistered: + torch.cuda.synchronize() +- torch.cuda.cudart().cudaHostUnregister(self.buffer.data_ptr()) ++ if self._device == "cuda": ++ torch.cuda.cudart().cudaHostUnregister(self.buffer.data_ptr()) + self._unregistered = True ++ ++ def registerHostPtr(self, device: str = "cuda", size: int = 0): ++ ptr = self.buffer.data_ptr() ++ if device == "cuda": ++ assert size >= 0, ("size must be non-negative and greater than 0.") ++ err = torch.cuda.cudart().cudaHostRegister(ptr, size, 0) ++ assert err == 0, ( ++ f"cudaHostRegister failed: {torch.cuda.cudart().cudaGetErrorString(err)}" ++ ) ++ elif device == "npu": ++ # Ascend need to manually manage host register API and memory is pinned ++ self.buffer = self.buffer.pin_memory() ++ lmc_ops.host_register(self.buffer) + + + class GPUMemoryAllocator(MemoryAllocatorInterface): +@@ -1570,6 +1621,8 @@ class GPUMemoryAllocator(MemoryAllocatorInterface): + with self.device_mem_lock: + return self.allocator.memcheck() + ++ def registerHostPtr(self, device: str = "cuda", size: int = 0): ++ pass + + class AdHocMemoryAllocator(MemoryAllocatorInterface): + """ +@@ -1650,6 +1703,9 @@ class AdHocMemoryAllocator(MemoryAllocatorInterface): + def memcheck(self): + return True + ++ def registerHostPtr(self, device: str = "cuda", size: int = 0): ++ pass ++ + + class CuFileMemoryAllocator(GPUMemoryAllocator): + def __init__(self, size: int, device=None): +@@ -1749,3 +1805,6 @@ class NixlCPUMemoryAllocator(MemoryAllocatorInterface): + self.cpu_allocator.batched_free(memory_objs, update_stats=update_stats) + else: + raise ValueError(f"Unsupported allocator type: {allocator_type}") ++ ++ def registerHostPtr(self, device: str = "cuda", size: int = 0): ++ pass +\ No newline at end of file +diff --git a/lmcache/v1/storage_backend/storage_manager.py b/lmcache/v1/storage_backend/storage_manager.py +index 82d6edc..ba0b103 100644 +--- a/lmcache/v1/storage_backend/storage_manager.py ++++ b/lmcache/v1/storage_backend/storage_manager.py +@@ -425,4 +425,4 @@ class StorageManager: + if self.thread.is_alive(): + self.thread.join() + +- logger.info("Storage manager closed.") ++ logger.info("Storage manager closed.") +\ No newline at end of file +diff --git a/pyproject.toml b/pyproject.toml +index f8ee543..8a362c1 100644 +--- a/pyproject.toml ++++ b/pyproject.toml +@@ -6,7 +6,7 @@ requires = [ + "packaging>=24.2", + "setuptools>=77.0.3,<81.0.0", + "setuptools_scm>=8", +- "torch==2.7.0", ++ "torch>=2.5.1", + "wheel", + ] + build-backend = "setuptools.build_meta" +@@ -14,8 +14,8 @@ + [project] + name = "lmcache" + authors = [{name = "LMCache Team", email = "lmcacheteam@gmail.com"}] +-license = "Apache-2.0" +-license-files = ["LICENSE"] ++license = { file = "LICENSE" } ++ + readme = "README.md" + description = "A LLM serving engine extension to reduce TTFT and increase throughput, especially under long-context scenarios." + classifiers = [ + +diff --git a/requirements/ascend.txt b/requirements/ascend.txt +new file mode 100644 +index 0000000..ad04d52 +--- /dev/null ++++ b/requirements/ascend.txt +@@ -0,0 +1,7 @@ ++torch>=2.5.1 ++# Common project dependencies ++-r common.txt ++ ++ ++# These must be updated alongside torch to correspond to vLLM-ascend versions ++torch-npu>=2.5.1.post1.dev20250619 +\ No newline at end of file +diff --git a/requirements/build.txt b/requirements/build.txt +index 85aacb4..0923a0b 100644 +--- a/requirements/build.txt ++++ b/requirements/build.txt +@@ -4,6 +4,6 @@ ninja + packaging>=24.2 + setuptools>=77.0.3,<81.0.0 + setuptools_scm>=8 +-torch==2.7.0 # Corresponds to the version used by vLLM main branch ++torch>=2.5.1 # Corresponds to the version used by vLLM main branch + wheel + +diff --git a/requirements/common.txt b/requirements/common.txt +index 34e30c2..35011ad 100644 +--- a/requirements/common.txt ++++ b/requirements/common.txt +@@ -1,8 +1,7 @@ + aiofile + aiofiles + aiohttp +-cufile-python +-infinistore ++infinistore; platform_machine == 'x86_64' + msgspec + numpy + nvtx +@@ -15,6 +14,6 @@ safetensors + setuptools>=77.0.3,<81.0.0 + setuptools_scm>=8 + sortedcontainers +-torch==2.7.0 # Should correspond to the version used by vLLM main branch ++torch>=2.5.1 # Should correspond to the version used by vLLM main branch + transformers >= 4.51.1 + xxhash==3.5.0 +diff --git a/requirements/cuda.txt b/requirements/cuda.txt +index 719261d..4912df9 100644 +--- a/requirements/cuda.txt ++++ b/requirements/cuda.txt +@@ -9,3 +9,4 @@ nvidia-ml-py # for pynvml package + torch == 2.7.0 + torchvision == 0.22.0 # Required for phi3v processor. See https://github.com/pytorch/vision?tab=readme-ov-file#installation for corresponding version + xformers == 0.0.30; platform_system == 'Linux' and platform_machine == 'x86_64' # Requires PyTorch 2.7.0 ++cufile-python +diff --git a/setup.py b/setup.py +index 00a4980..b964aff 100644 +--- a/setup.py ++++ b/setup.py +@@ -4,7 +4,17 @@ import os + import sys + + # Third Party +-from setuptools import find_packages, setup ++from setuptools import find_packages, setup, Extension ++from setuptools.command.build_ext import build_ext ++from setuptools.command.develop import develop ++from setuptools.command.install import install ++ ++import logging ++import sysconfig ++import subprocess ++import platform ++import shutil ++ + + ROOT_DIR = Path(__file__).parent + HIPIFY_DIR = os.path.join(ROOT_DIR, "csrc/") +@@ -14,10 +24,159 @@ HIPIFY_OUT_DIR = os.path.join(ROOT_DIR, "csrc_hip/") + # will run python setup.py sdist --dist-dir dist + BUILDING_SDIST = "sdist" in sys.argv or os.environ.get("NO_CUDA_EXT", "0") == "1" + +-# New environment variable to choose between CUDA and HIP +-BUILD_WITH_HIP = os.environ.get("BUILD_WITH_HIP", "0") == "1" ++# Environment variable to choose between CUDA, HIP, Ascend ++TARGET_DEVICE = os.environ.get("LMCACHE_TARGET_DEVICE", "CUDA") ++ ++logging.basicConfig(level=logging.INFO) ++logger = logging.getLogger(__name__) ++ ++def _get_ascend_home_path(): ++ # NOTE: standard Ascend CANN toolkit path ++ return os.environ.get("ASCEND_HOME_PATH", "/usr/local/Ascend/ascend-toolkit/latest") ++ ++def _get_ascend_driver_path(): ++ # NOTE: standard Ascend path ++ return os.environ.get("ASCEND_DRIVER_PATH", "/usr/local/Ascend/driver") ++ ++def _get_ascend_env_path(): ++ # NOTE: standard Ascend Environment variable setup path ++ env_script_path = os.path.realpath(os.path.join(_get_ascend_home_path(), "..", "set_env.sh")) ++ if not os.path.exists(env_script_path): ++ raise ValueError(f"The file '{env_script_path}' is not found, " ++ "please make sure environment variable 'ASCEND_HOME_PATH' is set correctly.") ++ return env_script_path ++ ++def _get_npu_soc(): ++ _soc_version = os.getenv("SOC_VERSION", None) ++ if _soc_version is None: ++ npu_smi_cmd = [ ++ "bash", ++ "-c", ++ "npu-smi info | grep OK | awk '{print $3}' | head -n 1", ++ ] ++ try: ++ _soc_version = subprocess.check_output(npu_smi_cmd, ++ text=True).strip() ++ _soc_version = _soc_version.split("-")[0] ++ _soc_version = "Ascend"+_soc_version ++ return _soc_version ++ except subprocess.CalledProcessError as e: ++ raise RuntimeError(f"Retrieve SoC version failed: {e}") ++ return _soc_version + ++class CMakeExtension(Extension): + ++ def __init__(self, ++ name: str, ++ cmake_lists_dir: str = ".", ++ **kwargs) -> None: ++ super().__init__(name, sources=[], py_limited_api=False, **kwargs) ++ self.cmake_lists_dir = os.path.abspath(cmake_lists_dir) ++ ++class custom_install(install): ++ def run(self): ++ self.run_command("build_ext") ++ install.run(self) ++ ++class CustomAscendCmakeBuildExt(build_ext): ++ ++ def build_extension(self, ext): ++ # build the so as c_ops ++ ext_name = ext.name.split(".")[-1] ++ so_name = ext_name + ".so" ++ logger.info(f"Building {so_name} ...") ++ OPS_DIR = os.path.join(ROOT_DIR, "csrc", "ascend") ++ BUILD_OPS_DIR = os.path.join(ROOT_DIR, "build", "ascend") ++ os.makedirs(BUILD_OPS_DIR, exist_ok=True) ++ ++ ascend_home_path = _get_ascend_home_path() ++ ascend_driver_path = _get_ascend_driver_path() ++ env_path = _get_ascend_env_path() ++ _soc_version = _get_npu_soc() ++ _cxx_compiler = os.getenv("CXX") ++ _cc_compiler = os.getenv("CC") ++ python_executable = sys.executable ++ ++ try: ++ # if pybind11 is installed via pip ++ pybind11_cmake_path = (subprocess.check_output( ++ [python_executable, "-m", "pybind11", ++ "--cmakedir"]).decode().strip()) ++ except subprocess.CalledProcessError as e: ++ # else specify pybind11 path installed from source code on CI container ++ raise RuntimeError(f"CMake configuration failed: {e}") ++ ++ import torch_npu ++ torch_npu_path = os.path.dirname(os.path.abspath(torch_npu.__file__)) ++ import torch ++ torch_path = os.path.dirname(os.path.abspath(torch.__file__)) ++ ++ # python include ++ python_include_path = sysconfig.get_path('include', scheme='posix_prefix') ++ ++ arch = platform.machine() ++ install_path = os.path.join(BUILD_OPS_DIR, 'install') ++ if isinstance(self.distribution.get_command_obj("develop"), develop): ++ install_path=BUILD_OPS_DIR ++ ++ cmake_cmd = [ ++ f"source {env_path} && " ++ f"cmake -S {OPS_DIR} -B {BUILD_OPS_DIR}" ++ f" -DSOC_VERSION={_soc_version}" ++ f" -DARCH={arch}" ++ " -DUSE_ASCEND=1" ++ f" -DPYTHON_EXECUTABLE={python_executable}" ++ f" -DCMAKE_PREFIX_PATH={pybind11_cmake_path}" ++ f" -DCMAKE_BUILD_TYPE=Release" ++ f" -DCMAKE_INSTALL_PREFIX={install_path}" ++ f" -DPYTHON_INCLUDE_PATH={python_include_path}" ++ f" -DTORCH_NPU_PATH={torch_npu_path}" ++ f" -DTORCH_PATH={torch_path}" ++ f" -DASCEND_CANN_PACKAGE_PATH={ascend_home_path}" ++ f" -DASCEND_DRIVER_PATH={ascend_driver_path}" ++ " -DCMAKE_VERBOSE_MAKEFILE=ON" ++ ] ++ ++ if _cxx_compiler is not None: ++ cmake_cmd += [f" -DCMAKE_CXX_COMPILER={_cxx_compiler}"] ++ ++ if _cc_compiler is not None: ++ cmake_cmd += [f" -DCMAKE_C_COMPILER={_cc_compiler}"] ++ ++ cmake_cmd += [f" && cmake --build {BUILD_OPS_DIR} -j --verbose"] ++ cmake_cmd += [f" && cmake --install {BUILD_OPS_DIR}"] ++ cmake_cmd = "".join(cmake_cmd) ++ ++ logger.info(f"Start running CMake commands:\n{cmake_cmd}") ++ try: ++ result = subprocess.run(cmake_cmd, cwd=ROOT_DIR, text=True, shell=True, check=True) ++ except subprocess.CalledProcessError as e: ++ raise RuntimeError(f"Failed to build {so_name}: {e}") ++ ++ build_lib_dir = self.get_ext_fullpath(ext.name) ++ os.makedirs(os.path.dirname(build_lib_dir), exist_ok=True) ++ ++ package_name = ext.name.split('.')[0] # e.g., 'lmcache' ++ src_dir = os.path.join(ROOT_DIR, package_name) ++ ++ for root, _, files in os.walk(install_path): ++ for file in files: ++ if file.endswith(".so"): ++ src_path = os.path.join(root, file) ++ dst_path = os.path.join(os.path.dirname(build_lib_dir), file) ++ if os.path.exists(dst_path): ++ os.remove(dst_path) ++ ++ if isinstance(self.distribution.get_command_obj("develop"), develop): ++ # For the ascend kernels ++ src_dir_file = os.path.join(src_dir, file) ++ shutil.copy(src_path, src_dir_file) ++ shutil.copy(src_path, dst_path) ++ ++ logger.info(f"Copied {file} to {dst_path}") ++ ++ ++ + def hipify_wrapper() -> None: + # Third Party + from torch.utils.hipify.hipify_python import hipify +@@ -134,6 +293,12 @@ def rocm_extension() -> tuple[list, dict]: + return ext_modules, cmdclass + + ++def ascend_extension(): ++ print("Building Ascend extensions") ++ return [CMakeExtension(name="lmcache.c_ops")], \ ++ {"build_ext": CustomAscendCmakeBuildExt} ++ ++ + def source_dist_extension() -> tuple[list, dict]: + print("Not building CUDA/HIP extensions for sdist") + return [], {} +@@ -142,11 +307,14 @@ def source_dist_extension() -> tuple[list, dict]: + if __name__ == "__main__": + if BUILDING_SDIST: + get_extension = source_dist_extension +- elif BUILD_WITH_HIP: ++ TARGET_DEVICE = "EMPTY" ++ elif TARGET_DEVICE == "HIP": + get_extension = rocm_extension +- else: ++ elif TARGET_DEVICE == "CUDA": + get_extension = cuda_extension +- ++ elif TARGET_DEVICE == "ASCEND": ++ get_extension = ascend_extension ++ + ext_modules, cmdclass = get_extension() + + setup( +diff --git a/tests/conftest.py b/tests/conftest.py +index e7aafc8..c6f9805 100644 +--- a/tests/conftest.py ++++ b/tests/conftest.py +@@ -247,3 +247,18 @@ def autorelease_v1(request): + # Cleanup all objects created by the factory + # for obj in objects: + # obj.close() ++ ++@pytest.fixture(scope="module") ++def has_npu(request): ++ try: ++ import torch_npu ++ from torch_npu.contrib import transfer_to_npu ++ return True ++ except ImportError as ie: ++ return False ++ ++@pytest.fixture(scope="module") ++def skip_if_npu(has_npu): ++ if has_npu: ++ pytest.skip("Skipped. NPU does not support current function.") ++ return has_npu +\ No newline at end of file +diff --git a/tests/v1/storage_backend/test_local_cpu_backend.py b/tests/v1/storage_backend/test_local_cpu_backend.py +index 6c7fbb1..061b277 100644 +--- a/tests/v1/storage_backend/test_local_cpu_backend.py ++++ b/tests/v1/storage_backend/test_local_cpu_backend.py +@@ -64,9 +64,10 @@ def create_test_memory_obj(shape=(2, 16, 8, 128), dtype=torch.bfloat16) -> Memor + + + @pytest.fixture +-def memory_allocator(): ++def memory_allocator(has_npu): + """Create a memory allocator for testing.""" +- return MixedMemoryAllocator(1024 * 1024 * 1024) # 1GB ++ device = "npu" if has_npu else "cuda" ++ return MixedMemoryAllocator(1024 * 1024 * 1024, device=device) # 1GB + + + @pytest.fixture +@@ -589,4 +590,4 @@ class TestLocalCPUBackend: + # The accessed key should be at the end (MRU) + assert retrieved_keys[-1] == keys[0] + +- local_cpu_backend.memory_allocator.close() ++ local_cpu_backend.memory_allocator.close() +\ No newline at end of file +diff --git a/tests/v1/storage_backend/test_local_disk_backend.py b/tests/v1/storage_backend/test_local_disk_backend.py +index 197c195..77474da 100644 +--- a/tests/v1/storage_backend/test_local_disk_backend.py ++++ b/tests/v1/storage_backend/test_local_disk_backend.py +@@ -619,4 +619,4 @@ class TestLocalDiskBackend: + # The backend should still be in a consistent state + assert local_disk_backend.contains(key) + +- local_disk_backend.local_cpu_backend.memory_allocator.close() ++ local_disk_backend.local_cpu_backend.memory_allocator.close() +\ No newline at end of file +diff --git a/tests/v1/test_cache_engine.py b/tests/v1/test_cache_engine.py +index ba9721f..0827e85 100644 +--- a/tests/v1/test_cache_engine.py ++++ b/tests/v1/test_cache_engine.py +@@ -19,10 +19,11 @@ import torch + # First Party + from lmcache.v1.cache_engine import LMCacheEngineBuilder + from lmcache.v1.config import LMCacheEngineConfig ++import lmcache.c_ops as lmc_ops + + +-def test_paged_same_retrieve_store(autorelease_v1): +- device = "cuda" ++def test_paged_same_retrieve_store(autorelease_v1, has_npu): ++ device = "npu" if has_npu else "cuda" + fmt = "vllm" + num_tokens = 2000 + num_blocks = 1000 +@@ -57,9 +58,10 @@ def test_paged_same_retrieve_store(autorelease_v1): + + engine = autorelease_v1( + LMCacheEngineBuilder.get_or_create( +- "test", cfg, dumb_metadata(fmt, kv_shape), connector ++ "test", cfg, dumb_metadata(fmt, kv_shape), connector, device=device + ) + ) ++ + """ test retrieve empty """ + ret_mask = engine.retrieve( + tokens, kvcaches=retrieved_cache, slot_mapping=slot_mapping +@@ -93,7 +95,7 @@ def test_paged_same_retrieve_store(autorelease_v1): + @pytest.mark.parametrize("backend", ["cpu", "local_disk", "remote", "remote_cachegen"]) + @pytest.mark.parametrize("lmserver_v1_process", ["cpu"], indirect=True) + def test_paged_retrieve_prefix( +- fmt, chunk_size, backend, lmserver_v1_process, autorelease_v1 ++ fmt, chunk_size, backend, lmserver_v1_process, autorelease_v1, has_npu + ): + url = None + remote_serde = None +@@ -101,12 +103,14 @@ def test_paged_retrieve_prefix( + if "remote" in backend: + url = lmserver_v1_process.server_url + if backend == "remote_cachegen": ++ if has_npu: ++ pytest.skip("NPU backend: Not implemented for cachegen") + backend = "remote" + remote_serde = "cachegen" + check_equality = False + else: + remote_serde = "naive" +- device = "cuda" ++ device = "npu" if has_npu else "cuda" + num_tokens = 2000 + new_num_tokens = 1000 + kv_shape = (32, 2, chunk_size, 8, 128) +@@ -139,9 +143,10 @@ def test_paged_retrieve_prefix( + + engine = autorelease_v1( + LMCacheEngineBuilder.get_or_create( +- "test", cfg, dumb_metadata(fmt, kv_shape), connector ++ "test", cfg, dumb_metadata(fmt, kv_shape), connector, device=device + ) + ) ++ + """ test store """ + t1 = time.perf_counter() + engine.store(tokens, kvcaches=kv_cache, slot_mapping=slot_mapping) +@@ -199,12 +204,12 @@ def test_paged_retrieve_prefix( + ) + @pytest.mark.parametrize("lmserver_v1_process", ["cpu"], indirect=True) + def test_paged_store_offset( +- fmt, chunk_size, backend, lmserver_v1_process, autorelease_v1 ++ fmt, chunk_size, backend, lmserver_v1_process, autorelease_v1, has_npu + ): + url = None + if backend == "remote": + url = lmserver_v1_process.server_url +- device = "cuda" ++ device = "npu" if has_npu else "cuda" + num_tokens = 2000 + num_suffix_tokens = 500 + num_total_tokens = 3000 +@@ -231,9 +236,10 @@ def test_paged_store_offset( + + engine = autorelease_v1( + LMCacheEngineBuilder.get_or_create( +- "test", cfg, dumb_metadata(fmt, kv_shape), connector ++ "test", cfg, dumb_metadata(fmt, kv_shape), connector, device=device + ) + ) ++ + """ test store """ + engine.store( + tokens[:num_tokens], +@@ -295,8 +301,8 @@ def test_paged_store_offset( + "local_disk" + ], + ) +-def test_paged_mixed_retrieve(fmt, chunk_size, backend, autorelease_v1): +- device = "cuda" ++def test_paged_mixed_retrieve(fmt, chunk_size, backend, autorelease_v1, has_npu): ++ device = "npu" if has_npu else "cuda" + num_tokens = 2000 + new_num_tokens = 1000 + num_blocks = 1000 +@@ -327,9 +333,10 @@ def test_paged_mixed_retrieve(fmt, chunk_size, backend, autorelease_v1): + + engine = autorelease_v1( + LMCacheEngineBuilder.get_or_create( +- "test", cfg, dumb_metadata(fmt, kv_shape), connector ++ "test", cfg, dumb_metadata(fmt, kv_shape), connector, device=device + ) + ) ++ + """ test store """ + engine.store(tokens, kvcaches=kv_cache, slot_mapping=slot_mapping) + engine.store(new_tokens, kvcaches=kv_cache, slot_mapping=new_slot_mapping) +@@ -421,8 +428,8 @@ def test_paged_mixed_retrieve(fmt, chunk_size, backend, autorelease_v1): + + + @pytest.mark.parametrize("fmt", ["vllm"]) +-def test_paged_store_kv_tensors_mask(fmt, autorelease_v1): +- device = "cuda" ++def test_paged_store_kv_tensors_mask(fmt, autorelease_v1, has_npu): ++ device = "npu" if has_npu else "cuda" + num_tokens = 1000 + new_num_tokens = 2000 + num_blocks = 1000 +@@ -452,9 +459,10 @@ def test_paged_store_kv_tensors_mask(fmt, autorelease_v1): + + engine = autorelease_v1( + LMCacheEngineBuilder.get_or_create( +- "test", cfg, dumb_metadata(fmt, kv_shape), connector ++ "test", cfg, dumb_metadata(fmt, kv_shape), connector, device=device + ) + ) ++ + """ Store some tokens with mask """ + engine.store(tokens, kvcaches=kv_cache, slot_mapping=slot_mapping) + """Wait until store finishes""" +@@ -570,12 +578,12 @@ def test_paged_store_kv_tensors_mask(fmt, autorelease_v1): + ) + @pytest.mark.parametrize("lmserver_v1_process", ["cpu"], indirect=True) + def test_paged_hierarchy_retrieve( +- fmt, chunk_size, backend, retrieve_from, lmserver_v1_process, autorelease_v1 ++ fmt, chunk_size, backend, retrieve_from, lmserver_v1_process, autorelease_v1, has_npu + ): + url = None + if backend == "local_cpu_disk_remote": + url = lmserver_v1_process.server_url +- device = "cuda" ++ device = "npu" if has_npu else "cuda" + num_tokens = 2000 + new_num_tokens = 1000 + kv_shape = (32, 2, chunk_size, 8, 128) +@@ -609,9 +617,10 @@ def test_paged_hierarchy_retrieve( + + engine = autorelease_v1( + LMCacheEngineBuilder.get_or_create( +- "test", cfg, dumb_metadata(fmt, kv_shape), connector ++ "test", cfg, dumb_metadata(fmt, kv_shape), connector, device=device + ) + ) ++ + """ test store """ + t1 = time.perf_counter() + engine.store(tokens, kvcaches=kv_cache, slot_mapping=slot_mapping) +@@ -692,8 +701,8 @@ def test_paged_hierarchy_retrieve( + "local_disk", + ], + ) +-def test_paged_prefetch_retrieve(backend, prefetch_from, autorelease_v1): +- device = "cuda" ++def test_paged_prefetch_retrieve(backend, prefetch_from, autorelease_v1, has_npu): ++ device = "npu" if has_npu else "cuda" + num_tokens = 2000 + new_num_tokens = 1000 + num_blocks = 1000 +@@ -726,7 +735,7 @@ def test_paged_prefetch_retrieve(backend, prefetch_from, autorelease_v1): + + engine = autorelease_v1( + LMCacheEngineBuilder.get_or_create( +- "test", cfg, dumb_metadata(fmt, kv_shape), connector ++ "test", cfg, dumb_metadata(fmt, kv_shape), connector, device=device + ) + ) + """ test store """ +@@ -804,12 +813,12 @@ def test_paged_prefetch_retrieve(backend, prefetch_from, autorelease_v1): + ], + ) + @pytest.mark.parametrize("lmserver_v1_process", ["cpu"], indirect=True) +-def test_paged_mem_leak(fmt, chunk_size, backend, lmserver_v1_process, autorelease_v1): ++def test_paged_mem_leak(fmt, chunk_size, backend, lmserver_v1_process, autorelease_v1, has_npu): + url = None + if "remote" in backend: + url = lmserver_v1_process.server_url + +- device = "cuda" ++ device = "npu" if has_npu else "cuda" + num_tokens = 2000 + kv_shape = (32, 2, chunk_size, 8, 128) + num_blocks = 1000 +@@ -830,7 +839,7 @@ def test_paged_mem_leak(fmt, chunk_size, backend, lmserver_v1_process, autorelea + + engine = autorelease_v1( + LMCacheEngineBuilder.get_or_create( +- "test", cfg, dumb_metadata(fmt, kv_shape), connector ++ "test", cfg, dumb_metadata(fmt, kv_shape), connector, device=device + ) + ) + +@@ -872,20 +881,20 @@ def test_paged_mem_leak(fmt, chunk_size, backend, lmserver_v1_process, autorelea + LMCacheEngineBuilder.destroy("test") + + +-def test_builder(autorelease_v1): ++def test_builder(autorelease_v1, has_npu): + instance_id = "test" + cfg = LMCacheEngineConfig.from_legacy(chunk_size=256) + cfg2 = LMCacheEngineConfig.from_legacy(chunk_size=512) + connector = None + should_be_none = LMCacheEngineBuilder.get(instance_id) + assert should_be_none is None +- ++ device = "npu" if has_npu else "cuda" + _engine = autorelease_v1( +- LMCacheEngineBuilder.get_or_create(instance_id, cfg, dumb_metadata(), connector) ++ LMCacheEngineBuilder.get_or_create(instance_id, cfg, dumb_metadata(), connector, device=device) + ) + _engine2 = autorelease_v1(LMCacheEngineBuilder.get(instance_id)) # noqa + + with pytest.raises(ValueError): + LMCacheEngineBuilder.get_or_create( +- instance_id, cfg2, dumb_metadata(), connector ++ instance_id, cfg2, dumb_metadata(), connector, device=device + ) +diff --git a/tests/v1/test_connector.py b/tests/v1/test_connector.py +index 9dbfe72..3e538e6 100644 +--- a/tests/v1/test_connector.py ++++ b/tests/v1/test_connector.py +@@ -16,7 +16,7 @@ import torch + # First Party + from lmcache.v1.memory_management import PinMemoryAllocator + from lmcache.v1.storage_backend.connector import CreateConnector +- ++import lmcache.c_ops as lmc_ops + + @pytest.mark.parametrize("lmserver_v1_process", ["cpu"], indirect=True) + @pytest.mark.parametrize( +@@ -25,12 +25,14 @@ from lmcache.v1.storage_backend.connector import CreateConnector + "lm://localhost:65000", + ], + ) +-def test_lm_connector(url, autorelease_v1, lmserver_v1_process): ++def test_lm_connector(url, autorelease_v1, lmserver_v1_process, has_npu): + if url.startswith("lm"): + url = lmserver_v1_process.server_url + + async_loop, async_thread = init_asyncio_loop() +- memory_allocator = PinMemoryAllocator(1024 * 1024 * 1024) ++ device = "npu" if has_npu else "cuda" ++ memory_allocator = PinMemoryAllocator(1024 * 1024 * 1024, device=device) ++ + connector = autorelease_v1(CreateConnector(url, async_loop, memory_allocator)) + + random_key = dumb_cache_engine_key() +@@ -70,14 +72,15 @@ def test_lm_connector(url, autorelease_v1, lmserver_v1_process): + + + @pytest.mark.parametrize("lmserver_v1_process", ["cpu"], indirect=True) +-def test_fs_connector(lmserver_v1_process, autorelease_v1): ++def test_fs_connector(lmserver_v1_process, autorelease_v1, has_npu): + """Test filesystem connector: exists, put, get, list, and file store.""" + + with tempfile.TemporaryDirectory() as temp_dir: + # Setup + url = f"fs://host:0/{temp_dir}/" + async_loop, async_thread = init_asyncio_loop() +- memory_allocator = PinMemoryAllocator(1024 * 1024 * 1024) ++ device = "npu" if has_npu else "cuda" ++ memory_allocator = PinMemoryAllocator(1024 * 1024 * 1024, device=device) + connector = autorelease_v1(CreateConnector(url, async_loop, memory_allocator)) + random_key = dumb_cache_engine_key() + +@@ -138,7 +141,7 @@ def test_fs_connector(lmserver_v1_process, autorelease_v1): + "unix:///tmp/redis.sock", + ], + ) +-def test_redis_connector(url, autorelease_v1): ++def test_redis_connector(url, autorelease_v1, has_npu): + """Test Redis connector: exists, put, get operations. + + This test uses the MockRedis from conftest.py to simulate +@@ -146,7 +149,8 @@ def test_redis_connector(url, autorelease_v1): + """ + + async_loop, async_thread = init_asyncio_loop() +- memory_allocator = PinMemoryAllocator(1024 * 1024 * 1024) ++ device = "npu" if has_npu else "cuda" ++ memory_allocator = PinMemoryAllocator(1024 * 1024 * 1024, device=device) + connector = autorelease_v1(CreateConnector(url, async_loop, memory_allocator)) + + random_key = dumb_cache_engine_key() +@@ -199,7 +203,7 @@ def test_redis_connector(url, autorelease_v1): + "redis-sentinel://localhost:26379", + ], + ) +-def test_redis_sentinel_connector(url, autorelease_v1): ++def test_redis_sentinel_connector(url, autorelease_v1, has_npu): + """Test Redis Sentinel connector: exists, put, get operations. + + This test uses the MockRedisSentinel from conftest.py to simulate +@@ -213,7 +217,8 @@ def test_redis_sentinel_connector(url, autorelease_v1): + os.environ["REDIS_TIMEOUT"] = "5" + + async_loop, async_thread = init_asyncio_loop() +- memory_allocator = PinMemoryAllocator(1024 * 1024 * 1024) ++ device = "npu" if has_npu else "cuda" ++ memory_allocator = PinMemoryAllocator(1024 * 1024 * 1024, device=device) + connector = autorelease_v1(CreateConnector(url, async_loop, memory_allocator)) + + random_key = dumb_cache_engine_key() +diff --git a/tests/v1/test_gds.py b/tests/v1/test_gds.py +index 5fda653..ee0c093 100644 +--- a/tests/v1/test_gds.py ++++ b/tests/v1/test_gds.py +@@ -20,7 +20,7 @@ from lmcache.v1.storage_backend import CreateStorageBackends + from lmcache.v1.storage_backend.gds_backend import pack_metadata, unpack_metadata + + +-def test_gds_backend_metadata(): ++def test_gds_backend_metadata(skip_if_npu): + # This is a sanity check that packing and unpacking works. We can add + # more tensor types to be sure. + for [tensor, expected_nbytes] in [(torch.randn(3, 10), 120)]: +@@ -46,7 +46,7 @@ def test_gds_backend_metadata(): + + + @pytest.mark.skip(reason="We need to add this test back after implementing prefetch") +-def test_gds_backend_sanity(): ++def test_gds_backend_sanity(skip_if_npu): + BASE_DIR = Path(__file__).parent + GDS_DIR = "/tmp/gds/test-cache" + TEST_KEY = CacheEngineKey( +diff --git a/tests/v1/test_gpu_connector.py b/tests/v1/test_gpu_connector.py +index 8fcac95..6ab3bfa 100644 +--- a/tests/v1/test_gpu_connector.py ++++ b/tests/v1/test_gpu_connector.py +@@ -25,10 +25,9 @@ from lmcache.v1.memory_management import ( + PinMemoryAllocator, + ) + +- + @pytest.mark.parametrize("use_gpu", [True, False]) + @pytest.mark.parametrize("use_mla", [True, False]) +-def test_vllm_paged_connector_v2_with_gpu_and_mla(use_gpu, use_mla): ++def test_vllm_paged_connector_v2_with_gpu_and_mla(use_gpu, use_mla, has_npu): + num_blocks = 100 + block_size = 16 + num_layers = 32 +@@ -39,8 +38,8 @@ def test_vllm_paged_connector_v2_with_gpu_and_mla(use_gpu, use_mla): + + num_tokens = 800 + chunk_size = 256 +- +- allocator = PinMemoryAllocator(1024 * 1024 * 1024) ++ device = "npu" if has_npu else "cuda" ++ allocator = PinMemoryAllocator(1024 * 1024 * 1024, device=device) + + gpu_kv_src = generate_kv_cache_paged_list_tensors( + num_blocks=num_blocks, device=device, block_size=block_size, use_mla=use_mla +@@ -122,7 +121,7 @@ def test_vllm_paged_connector_v2_with_gpu_and_mla(use_gpu, use_mla): + + + @pytest.mark.parametrize("use_gpu", [True]) +-def test_layerwise_vllm_paged_connector_with_gpu(use_gpu): ++def test_layerwise_vllm_paged_connector_with_gpu(use_gpu, has_npu): + num_blocks = 100 + block_size = 16 + num_layers = 32 +@@ -133,8 +132,8 @@ def test_layerwise_vllm_paged_connector_with_gpu(use_gpu): + + num_tokens = 800 + chunk_size = 256 +- +- allocator = PinMemoryAllocator(1024 * 1024 * 1024) ++ device = "npu" if has_npu else "cuda" ++ allocator = PinMemoryAllocator(1024 * 1024 * 1024, device=device) + + gpu_kv_src = generate_kv_cache_paged_list_tensors(num_blocks, device, block_size) + gpu_kv_dst = generate_kv_cache_paged_list_tensors(num_blocks, device, block_size) +@@ -222,7 +221,7 @@ def test_layerwise_vllm_paged_connector_with_gpu(use_gpu): + + + @pytest.mark.parametrize("use_gpu", [True]) +-def test_batched_layerwise_vllm_paged_connector_with_gpu(use_gpu): ++def test_batched_layerwise_vllm_paged_connector_with_gpu(use_gpu, has_npu): + num_blocks = 100 + block_size = 16 + num_layers = 32 +@@ -235,8 +234,8 @@ def test_batched_layerwise_vllm_paged_connector_with_gpu(use_gpu): + num_tokens_2 = 500 + num_tokens_total = num_tokens_1 + num_tokens_2 + chunk_size = 256 +- +- allocator = PinMemoryAllocator(1024 * 1024 * 1024) ++ device = "npu" if has_npu else "cuda" ++ allocator = PinMemoryAllocator(1024 * 1024 * 1024, device=device) + + gpu_kv_src = generate_kv_cache_paged_list_tensors(num_blocks, device, block_size) + gpu_kv_dst = generate_kv_cache_paged_list_tensors(num_blocks, device, block_size) +@@ -540,7 +539,7 @@ def test_vllm_paged_connector_v2_to_gpu_bench(benchmark): + + @pytest.mark.parametrize("use_gpu", [True, False]) + @pytest.mark.parametrize("use_mla", [True, False]) +-def test_sglang_connector_with_gpu_and_mla(use_gpu, use_mla): ++def test_sglang_connector_with_gpu_and_mla(use_gpu, use_mla, skip_if_npu): + num_blocks = 100 + block_size = 16 + num_layers = 32 +diff --git a/tests/v1/test_mem_kernels.py b/tests/v1/test_mem_kernels.py +index 2cabd67..94440df 100644 +--- a/tests/v1/test_mem_kernels.py ++++ b/tests/v1/test_mem_kernels.py +@@ -13,6 +13,7 @@ from utils import ( + import pytest + import torch + ++ + # First Party + from lmcache.v1.memory_management import PinMemoryAllocator + import lmcache.c_ops as lmc_ops +@@ -54,7 +55,7 @@ def _slice_kv_at( + + + @pytest.mark.parametrize("num_tokens", [256, 500, 1024, 8000]) +-def test_extract_and_load_back(num_tokens): ++def test_extract_and_load_back(num_tokens, has_npu): + device = "cuda" + + num_blocks = 1000 +@@ -68,7 +69,8 @@ def test_extract_and_load_back(num_tokens): + slot_mapping = torch.tensor(slot_mapping, device=device) + + pinned_cpu_size = 4 * 1024 * 1024 * 1024 # 4GB +- mem_allocator = PinMemoryAllocator(pinned_cpu_size) ++ device = "npu" if has_npu else "cuda" ++ mem_allocator = PinMemoryAllocator(pinned_cpu_size, device=device) + + # Old extract + kv_tuple_list = [] +@@ -154,7 +156,7 @@ def test_extract_and_load_back(num_tokens): + + + @pytest.mark.parametrize("num_tokens", [256, 500, 1024, 8000]) +-def test_multi_layer_kernel(num_tokens): ++def test_multi_layer_kernel(num_tokens, has_npu): + device = "cuda" + + num_blocks = 1000 +@@ -172,7 +174,8 @@ def test_multi_layer_kernel(num_tokens): + slot_mapping = torch.tensor(slot_mapping, device=device) + + pinned_cpu_size = 4 * 1024 * 1024 * 1024 # 4GB +- mem_allocator = PinMemoryAllocator(pinned_cpu_size) ++ device = "npu" if has_npu else "cuda" ++ mem_allocator = PinMemoryAllocator(pinned_cpu_size, device=device) + + # lmc_ops.multi_layer_kv_transfer(memory_obj_new.tensor, + # kv_cache_pointers, # TODO: initialize this +@@ -209,8 +212,12 @@ def test_multi_layer_kernel(num_tokens): + kv_cache_pointers = torch.empty( + 32, dtype=torch.int64, device="cpu", pin_memory=True + ) ++ + for i in range(32): + kv_cache_pointers[i] = kv_cache[i].data_ptr() ++ ++ if has_npu: ++ kv_cache_pointers = kv_cache_pointers.to(device) + + memory_obj_new_list = [] + start_event = torch.cuda.Event(enable_timing=True) +@@ -253,6 +260,9 @@ def test_multi_layer_kernel(num_tokens): + ) + for i in range(32): + kv_cache_pointers_new[i] = kv_cache_new[i].data_ptr() ++ ++ if has_npu: ++ kv_cache_pointers_new = kv_cache_pointers_new.to(device) + + for chunk_id, slot_mapping_temp in enumerate(slot_mapping_chunked): + memory_obj_new = memory_obj_new_list[chunk_id] +@@ -276,7 +286,7 @@ def test_multi_layer_kernel(num_tokens): + + + @pytest.mark.parametrize("num_tokens", [256, 500, 1024, 8000]) +-def test_multi_layer_kernel_use_mla(num_tokens): ++def test_multi_layer_kernel_use_mla(num_tokens, has_npu): + device = "cuda" + + num_blocks = 1000 +@@ -293,8 +303,9 @@ def test_multi_layer_kernel_use_mla(num_tokens): + slot_mapping = torch.tensor(slot_mapping, device=device) + + pinned_cpu_size = 4 * 1024 * 1024 * 1024 # 4GB +- mem_allocator = PinMemoryAllocator(pinned_cpu_size) +- ++ device = "npu" if has_npu else "cuda" ++ mem_allocator = PinMemoryAllocator(pinned_cpu_size, device=device) ++ + # layer by layer extract + memory_obj_old_list = [] + start_event = torch.cuda.Event(enable_timing=True) +@@ -329,6 +340,8 @@ def test_multi_layer_kernel_use_mla(num_tokens): + ) + for i in range(num_layers): + kv_cache_pointers[i] = kv_cache[i].data_ptr() ++ if has_npu: ++ kv_cache_pointers = kv_cache_pointers.to(device) + + memory_obj_new_list = [] + start_event = torch.cuda.Event(enable_timing=True) +@@ -377,6 +390,9 @@ def test_multi_layer_kernel_use_mla(num_tokens): + ) + for i in range(num_layers): + kv_cache_pointers_new[i] = kv_cache_new[i].data_ptr() ++ ++ if has_npu: ++ kv_cache_pointers_new = kv_cache_pointers_new.to(device) + + for chunk_id, slot_mapping_temp in enumerate(slot_mapping_chunked): + memory_obj_new = memory_obj_new_list[chunk_id] +diff --git a/tests/v1/test_memory_management.py b/tests/v1/test_memory_management.py +index 2f70517..660e74f 100644 +--- a/tests/v1/test_memory_management.py ++++ b/tests/v1/test_memory_management.py +@@ -142,15 +142,15 @@ def test_tensor_allocator(use_paging): + True, + ], + ) +-def test_device_allocators(alloc_cls, use_paging): ++def test_device_allocators(alloc_cls, use_paging, has_npu): + total_size = 1024 * 1024 * 128 # 128MB + + shape = torch.Size([2, 32, 16, 1024]) # 64 pages + dtype = torch.bfloat16 + fmt = MemoryFormat.KV_2LTD +- ++ device = "npu" if has_npu else "cuda" + allocator = alloc_cls( +- total_size, use_paging=use_paging, shape=shape, dtype=dtype, fmt=fmt ++ total_size, use_paging=use_paging, shape=shape, dtype=dtype, fmt=fmt, device=device + ) + + if use_paging: +@@ -171,9 +171,10 @@ def test_device_allocators(alloc_cls, use_paging): + MixedMemoryAllocator, + ], + ) +-def test_inplace_modification(alloc_cls): ++def test_inplace_modification(alloc_cls, has_npu): + total_size = 1024 +- allocator = alloc_cls(total_size) ++ device = "npu" if has_npu else "cuda" ++ allocator = alloc_cls(total_size, device=device) + + data = allocator.allocate([10], torch.float) + assert data is not None +@@ -198,9 +199,10 @@ def test_inplace_modification(alloc_cls): + MixedMemoryAllocator, + ], + ) +-def test_boundary_alloc(alloc_cls): ++def test_boundary_alloc(alloc_cls, has_npu): + total_size = 1 << 25 +- allocator = alloc_cls(total_size) ++ device = "npu" if has_npu else "cuda" ++ allocator = alloc_cls(total_size, device=device) + data1 = allocator.allocate([512, 10], torch.float) + allocator.allocate([512, 10], torch.float) + allocator.free(data1) +@@ -225,10 +227,11 @@ def test_boundary_alloc(alloc_cls): + MixedMemoryAllocator, + ], + ) +-def test_batched_alloc(alloc_cls): ++def test_batched_alloc(alloc_cls, has_npu): + total_size = 32 * 100 * 2 * 1024 * 2 + batch_size = 32 +- allocator = alloc_cls(total_size) ++ device = "npu" if has_npu else "cuda" ++ allocator = alloc_cls(total_size, device=device) + objs = allocator.batched_allocate( + [100, 2, 1024], torch.bfloat16, batch_size, MemoryFormat.KV_T2D + ) +@@ -255,9 +258,10 @@ def test_batched_alloc(alloc_cls): + MixedMemoryAllocator, + ], + ) +-def test_mixed_alloc(alloc_cls): ++def test_mixed_alloc(alloc_cls, has_npu): + total_size = 1 << 25 +- allocator = alloc_cls(total_size) ++ device = "npu" if has_npu else "cuda" ++ allocator = alloc_cls(total_size, device=device) + data1 = allocator.allocate([512, 0], None, MemoryFormat.BINARY_BUFFER) + allocator.allocate([512, 10], torch.float) + allocator.free(data1) +diff --git a/tests/v1/test_weka.py b/tests/v1/test_weka.py +index eab8e1b..7cf052b 100644 +--- a/tests/v1/test_weka.py ++++ b/tests/v1/test_weka.py +@@ -18,7 +18,7 @@ from lmcache.v1.storage_backend import CreateStorageBackends + + + @pytest.mark.skip(reason="We need to add this test back after implementing prefetch") +-def test_weka_backend_sanity(): ++def test_weka_backend_sanity(skip_if_npu): + BASE_DIR = Path(__file__).parent + WEKA_DIR = "/tmp/weka/test-cache" + TEST_KEY = CacheEngineKey( diff --git a/LMCache-0.1.4.alpha.tar.gz b/LMCache-0.1.4.alpha.tar.gz deleted file mode 100644 index c242010c5335191bfa5298c757768528425427ec..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 817998 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

uvYMtqk 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