// // MetalKVCacheManager.mm // MNN // // Created by MNN on 2025/12/04. // Copyright © 2018, Alibaba Group Holding Limited // #ifdef MNN_SUPPORT_TRANSFORMER_FUSE #import "backend/metal/MetalBackend.hpp" #import "backend/metal/MNNMetalContext.h" #import "MetalKVCacheManager.hpp" namespace MNN { void MetalKVCacheManager::onResize(int kv_num_head, int head_dim) { mKvNumHead = kv_num_head; mHeadDim = head_dim; auto mtbn = static_cast(mBackend); // Record bytes for K cache element. When mQuantKey is enabled, key is stored as int8. mBytes = mQuantKey ? 1 : (mtbn->useFp16InsteadFp32() ? 2 : 4); } void MetalKVCacheManager::onAlloc(KVMeta* meta, int seq_len) { mMeta = meta; auto mtbn = static_cast(mBackend); auto context = (__bridge MNNMetalContext *)mtbn->context(); auto kv_seq_len = mMeta != nullptr ? mMeta->add : seq_len; int keyByte = mQuantKey ? 1 : (mtbn->useFp16InsteadFp32() ? 2 : 4); int valueByte = mQuantValue ? 1 : (mtbn->useFp16InsteadFp32() ? 2 : 4); // load disk prefix kvcache if(mMeta != nullptr && mMeta->file_name.size() > 0 && mMeta->file_flag == KVMeta::PendingRead) { // create new files std::string pathk = MNNFilePathConcat(mConfig.mPrefixCacheDir, mMeta->file_name) + "_" + std::to_string(mMeta->layer_index) + ".k"; std::string pathv = MNNFilePathConcat(mConfig.mPrefixCacheDir, mMeta->file_name) + "_" + std::to_string(mMeta->layer_index++) + ".v"; mMeta->layer_index = mMeta->layer_index % mMeta->layer_nums; auto old_key_fd = MNNOpenFile(pathk.c_str(), MNN_FILE_READ | MNN_FILE_WRITE); auto old_value_fd = MNNOpenFile(pathv.c_str(), MNN_FILE_READ | MNN_FILE_WRITE); if (old_key_fd == INVALID_FILE) { MNN_PRINT("Failed to open the file: %s\n", pathk.c_str()); } if (old_value_fd == INVALID_FILE) { MNN_PRINT("Failed to open the file: %s\n", pathv.c_str()); } // get kv cache file info auto oldKeySize = MNNGetFileSize(old_key_fd); auto oldValueSize = MNNGetFileSize(old_value_fd); auto oldTotalSize = ALIMIN(oldKeySize, oldValueSize); if(oldKeySize != oldValueSize) { MNN_ERROR("[Error]: Kvcache in disk size of key and value should equal with metal backend\n"); } size_t oldKeyMaxLength = oldKeySize / (mKvNumHead * mHeadDim * (mtbn->useFp16InsteadFp32() ? 2 : 4)); size_t oldValueMaxLength = oldValueSize / (mKvNumHead * mHeadDim * (mtbn->useFp16InsteadFp32() ? 2 : 4)); size_t oldMaxLength = ALIMIN(oldKeyMaxLength, oldValueMaxLength); if(oldMaxLength < meta->seqlen_in_disk) { MNN_ERROR("[Error]: Kvcache in disk size smaller than saved lengthInDiskToload:%d\n", (int)meta->seqlen_in_disk); } int kv_seq_len = ROUND_UP(meta->add + meta->seqlen_in_disk, mConfig.mKvAlignNum); mMaxLength = kv_seq_len > oldMaxLength ? ROUND_UP(meta->add + meta->seqlen_in_disk + mConfig.mExpandChunk, mConfig.mKvAlignNum) : oldMaxLength; size_t totalSize = mKvNumHead * mMaxLength * mHeadDim * (mtbn->useFp16InsteadFp32() ? 2 : 4); mCurrentTotalSize = totalSize; size_t old_piece_size = meta->seqlen_in_disk * (mtbn->useFp16InsteadFp32() ? 2 : 4); size_t old_piece_stride = oldMaxLength * (mtbn->useFp16InsteadFp32() ? 2 : 4); size_t new_piece_stride = mMaxLength * (mtbn->useFp16InsteadFp32() ? 2 : 4); mCurrentTotalSize = ALIMAX(mCurrentTotalSize, oldKeySize); mCurrentTotalSize = ALIMAX(mCurrentTotalSize, oldValueSize); createKVCacheFile(); resetKVCacheFileSize(mCurrentTotalSize, mCurrentTotalSize); expandKVCacheInDisk(oldTotalSize, mCurrentTotalSize, old_piece_stride, old_piece_size, new_piece_stride, true, old_key_fd, old_value_fd); mPastLength = meta->seqlen_in_disk; mKVCacheInDisk = true; return; } // align max kv_seq_len to mKvAlignNum, for simd/tensor matrix load alignment mMaxLength = ROUND_UP(kv_seq_len + mConfig.mExpandChunk, mConfig.mKvAlignNum); size_t totalSize = mKvNumHead * mMaxLength * mHeadDim * keyByte; mCurrentTotalSize = totalSize; bool storeKvInDisk = !mConfig.mKVCacheDir.empty(); bool sharePrefixKv = mMeta != nullptr && mMeta->file_name.size() > 0 && mMeta->file_flag == KVMeta::PendingWrite; if (sharePrefixKv) { mSaveShareKvPrefix = true; if(!MNNCreateDir(mConfig.mPrefixCacheDir.c_str())) { MNN_PRINT("Failed to create prefix cache file dir: %s\n", mConfig.mPrefixCacheDir.c_str()); } } if(storeKvInDisk || sharePrefixKv) { std::string keyStoredDst = ""; std::string valueStoredDst = ""; if(mMeta != nullptr) { mBasePrefixFileName = MNNFilePathConcat(mConfig.mPrefixCacheDir, mMeta->file_name) + "_" + std::to_string(mMeta->layer_index); keyStoredDst = sharePrefixKv ? mBasePrefixFileName + ".k" : ""; valueStoredDst = sharePrefixKv ? mBasePrefixFileName + ".v" : ""; mMeta->layer_index++; mMeta->layer_index = mMeta->layer_index % mMeta->layer_nums; } createKVCacheFile(keyStoredDst, valueStoredDst); resetKVCacheFileSize(totalSize, totalSize); mmapKVCache(totalSize, totalSize); mKVCacheInDisk = true; mKeyBuffer = [[context device] newBufferWithBytesNoCopy:mMapKeyAddr length:totalSize options:MTLResourceStorageModeShared deallocator:nil]; mValueBuffer = [[context device] newBufferWithBytesNoCopy:mMapValueAddr length:totalSize options:MTLResourceStorageModeShared deallocator:nil]; if (useDynamicScaleBuffer()) { int scaleByte = mtbn->useFp16InsteadFp32() ? 2 : 4; mKScaleBuffer = [[context device] newBufferWithLength:mMaxLength * scaleByte * 2 options:MTLResourceStorageModeShared]; mVScaleBuffer = [[context device] newBufferWithLength:mMaxLength * scaleByte * 2 options:MTLResourceStorageModeShared]; } else { mKScaleBuffer = nil; mVScaleBuffer = nil; } auto new_key_ptr = (uint8_t*)[mKeyBuffer contents]; ::memset(new_key_ptr, 0, mMaxLength * mKvNumHead * mHeadDim * keyByte); auto new_value_ptr = (uint8_t*)[mValueBuffer contents]; ::memset(new_value_ptr, 0, mMaxLength * mKvNumHead * mHeadDim * (mtbn->useFp16InsteadFp32() ? 2 : 4)); } else { // past_key: [maxlen, kvNumhead, headdim] Tensor* new_key = Tensor::createDevice({mMaxLength, mKvNumHead, mHeadDim * keyByte}); // past_value: [kvNumhead, headdim, maxlen] Tensor* new_value = Tensor::createDevice({mKvNumHead, mHeadDim, mMaxLength * valueByte}); auto res = mBackend->onAcquireBuffer(new_key, Backend::STATIC); res = res && mBackend->onAcquireBuffer(new_value, Backend::STATIC); if(!res) { MNN_ERROR("attition kv cache alloc memory error:%d\n", res); } // memset for qkv matmul mad, in case dirty data auto newKeyBuf = MetalBackend::getBuffer(new_key); auto new_key_ptr = (uint8_t*)[newKeyBuf.first contents] + newKeyBuf.second; ::memset(new_key_ptr, 0, mMaxLength * mKvNumHead * mHeadDim * keyByte); auto newValueBuf = MetalBackend::getBuffer(new_value); auto new_value_ptr = (uint8_t*)[newValueBuf.first contents] + newValueBuf.second; ::memset(new_value_ptr, 0, mMaxLength * mKvNumHead * mHeadDim * valueByte); mPastKey.reset(new_key); mPastValue.reset(new_value); if (useDynamicScaleBuffer()) { int scaleByte = mtbn->useFp16InsteadFp32() ? 2 : 4; mKScaleBuffer = [[context device] newBufferWithLength:mMaxLength * scaleByte * 2 options:MTLResourceStorageModeShared]; mVScaleBuffer = [[context device] newBufferWithLength:mMaxLength * scaleByte * 2 options:MTLResourceStorageModeShared]; } else { mKScaleBuffer = nil; mVScaleBuffer = nil; } } } void MetalKVCacheManager::onRealloc(KVMeta* meta) { mMeta = meta; auto kv_seq_len = mMeta->previous + mMeta->add - mMeta->remove + mMeta->computeReverseSize(); auto mtbn = static_cast(mBackend); int keyByte = mQuantKey ? 1 : (mtbn->useFp16InsteadFp32() ? 2 : 4); int valueByte = mQuantValue ? 1 : (mtbn->useFp16InsteadFp32() ? 2 : 4); auto start = mMeta->previous - mMeta->remove; // latest length larger than maxLen if (kv_seq_len > mMaxLength) { // copy mPastLength including all remove/reverse to new buffer first auto copy_len = mPastLength; bool needCopy = mPastLength > 0; size_t old_size = (size_t)mKvNumHead * copy_len * mHeadDim * keyByte; size_t old_piece_size = (size_t)copy_len * valueByte; size_t old_piece_stride = (size_t)mMaxLength * valueByte; // align max kv_seq_len to mKvAlignNum, for simd/tensor matrix load alignment mMaxLength = ROUND_UP(kv_seq_len + mConfig.mExpandChunk, mConfig.mKvAlignNum); auto oldTotalSize = mCurrentTotalSize; size_t size = (size_t)mKvNumHead * mMaxLength * mHeadDim * keyByte; mCurrentTotalSize = size; size_t new_piece_stride = (size_t)mMaxLength * valueByte; mPastLength = (int)start; if(mKVCacheInDisk) { expandKVCacheInDisk(oldTotalSize, mCurrentTotalSize, old_piece_stride, old_piece_size, new_piece_stride, needCopy); } else { expandKVCacheInMem(old_size, old_piece_stride, old_piece_size, new_piece_stride, needCopy); } } // Remove { if (0 == mMeta->n_reserve) { mPastLength = start; return; } int8_t *key_ptr = nullptr; int8_t *value_ptr = nullptr; if(mKVCacheInDisk) { key_ptr = mMapKeyAddr; value_ptr = mMapValueAddr; } else { auto keyBuf = MetalBackend::getBuffer(mPastKey.get()); key_ptr = (int8_t*)[keyBuf.first contents] + keyBuf.second; auto valueBuf = MetalBackend::getBuffer(mPastValue.get()); value_ptr = (int8_t*)[valueBuf.first contents] + valueBuf.second; } auto src_start = start; // TODO: need to ensure reserve info is sorted for (int n = 0; n < mMeta->n_reserve; ++n) { auto begin = mMeta->reserve[2 * n]; auto length = mMeta->reserve[2 * n + 1]; // past_key : [mCache->mPastLength, mKvNumHead, mHeadDim] // past_value : [mKvNumHead, mHeadDim, mCache->mMaxLength] auto copy_src_index = src_start + begin; auto copy_dst_index = start; for(int i = 0; i < length; i++) { ::memcpy(key_ptr + (copy_dst_index + i) * mKvNumHead * mHeadDim * keyByte, key_ptr + (copy_src_index + i) * mKvNumHead * mHeadDim * keyByte, mKvNumHead * mHeadDim * keyByte); } for(int j = 0; j < mKvNumHead * mHeadDim; j++) { for(int i = 0; i < length; i++) { ::memcpy(value_ptr + (j * mMaxLength + copy_dst_index + i) * valueByte, value_ptr + (j * mMaxLength + copy_src_index + i) * valueByte, valueByte); } } if (mKScaleBuffer != nil) { int scaleByte = mtbn->useFp16InsteadFp32() ? 2 : 4; if (scaleByte == 2) { int16_t* k_scale_ptr = (int16_t*)[mKScaleBuffer contents]; int16_t* v_scale_ptr = (int16_t*)[mVScaleBuffer contents]; for(int i = 0; i < length; i++) { k_scale_ptr[(copy_dst_index + i) * 2 + 0] = k_scale_ptr[(copy_src_index + i) * 2 + 0]; k_scale_ptr[(copy_dst_index + i) * 2 + 1] = k_scale_ptr[(copy_src_index + i) * 2 + 1]; v_scale_ptr[(copy_dst_index + i) * 2 + 0] = v_scale_ptr[(copy_src_index + i) * 2 + 0]; v_scale_ptr[(copy_dst_index + i) * 2 + 1] = v_scale_ptr[(copy_src_index + i) * 2 + 1]; } } else { float* k_scale_ptr = (float*)[mKScaleBuffer contents]; float* v_scale_ptr = (float*)[mVScaleBuffer contents]; for(int i = 0; i < length; i++) { k_scale_ptr[(copy_dst_index + i) * 2 + 0] = k_scale_ptr[(copy_src_index + i) * 2 + 0]; k_scale_ptr[(copy_dst_index + i) * 2 + 1] = k_scale_ptr[(copy_src_index + i) * 2 + 1]; v_scale_ptr[(copy_dst_index + i) * 2 + 0] = v_scale_ptr[(copy_src_index + i) * 2 + 0]; v_scale_ptr[(copy_dst_index + i) * 2 + 1] = v_scale_ptr[(copy_src_index + i) * 2 + 1]; } } } start += length; } mPastLength = (int)start; } } void MetalKVCacheManager::expandKVCacheInMem(size_t oldSize, size_t old_piece_stride, size_t old_piece_size, size_t new_piece_stride, bool need_copy) { auto mtbn = static_cast(mBackend); int keyByte = mQuantKey ? 1 : (mtbn->useFp16InsteadFp32() ? 2 : 4); int valueByte = mQuantValue ? 1 : (mtbn->useFp16InsteadFp32() ? 2 : 4); // past_key: [maxlen, kvNumhead, headdim] Tensor* new_key = Tensor::createDevice({mMaxLength, mKvNumHead, mHeadDim * keyByte}); // past_value: [kvNumhead, headdim, maxlen] Tensor* new_value = Tensor::createDevice({mKvNumHead, mHeadDim, mMaxLength * valueByte}); auto res = mBackend->onAcquireBuffer(new_key, Backend::STATIC); res = res && mBackend->onAcquireBuffer(new_value, Backend::STATIC); if(!res) { MNN_ERROR("attition kv cache realloc memory error:%d\n", res); } // memset for qkv matmul mad, in case dirty data auto newKeyBuf = MetalBackend::getBuffer(new_key); auto new_key_ptr = (uint8_t*)[newKeyBuf.first contents] + newKeyBuf.second; ::memset(new_key_ptr, 0, (size_t)mMaxLength * mKvNumHead * mHeadDim * keyByte); auto newValueBuf = MetalBackend::getBuffer(new_value); auto new_value_ptr = (uint8_t*)[newValueBuf.first contents] + newValueBuf.second; ::memset(new_value_ptr, 0, (size_t)mMaxLength * mKvNumHead * mHeadDim * valueByte); if (need_copy) { auto keyBuf = MetalBackend::getBuffer(mPastKey.get()); auto key_ptr = (uint8_t*)[keyBuf.first contents] + keyBuf.second;; ::memcpy(new_key_ptr, key_ptr, oldSize); auto valueBuf = MetalBackend::getBuffer(mPastValue.get()); auto value_ptr = (uint8_t*)[valueBuf.first contents] + valueBuf.second; for(int i = 0; i < mKvNumHead * mHeadDim; i++) { ::memcpy(new_value_ptr + i * new_piece_stride, value_ptr + i * old_piece_stride, old_piece_size); } } mPastKey.reset(new_key); mPastValue.reset(new_value); auto context = (__bridge MNNMetalContext *)mtbn->context(); if (useDynamicScaleBuffer()) { int scaleByte = mtbn->useFp16InsteadFp32() ? 2 : 4; id newKScale = [[context device] newBufferWithLength:mMaxLength * scaleByte * 2 options:MTLResourceStorageModeShared]; id newVScale = [[context device] newBufferWithLength:mMaxLength * scaleByte * 2 options:MTLResourceStorageModeShared]; if (need_copy && mKScaleBuffer != nil) { ::memcpy([newKScale contents], [mKScaleBuffer contents], (old_piece_size / valueByte) * scaleByte * 2); ::memcpy([newVScale contents], [mVScaleBuffer contents], (old_piece_size / valueByte) * scaleByte * 2); } mKScaleBuffer = newKScale; mVScaleBuffer = newVScale; } else { mKScaleBuffer = nil; mVScaleBuffer = nil; } } void MetalKVCacheManager::expandKVCacheInDisk(size_t oldSize, size_t curSize, size_t old_piece_stride, size_t old_piece_size, size_t new_piece_stride, bool need_copy, file_t specKeyFile, file_t specValueFile) { auto mtbn = static_cast(mBackend); auto context = (__bridge MNNMetalContext *)mtbn->context(); mmapKVCache(oldSize, oldSize, specKeyFile, specValueFile); std::vector prevKey, prevValue; prevKey.resize(oldSize); prevValue.resize(oldSize); memcpy(prevKey.data(), mMapKeyAddr, oldSize); memcpy(prevValue.data(), mMapValueAddr, oldSize); unmapKVCache(oldSize, oldSize); resetKVCacheFileSize(curSize, curSize); mmapKVCache(curSize, curSize); // reset id mKeyBuffer = [[context device] newBufferWithBytesNoCopy:mMapKeyAddr length:curSize options:MTLResourceStorageModeShared deallocator:nil]; mValueBuffer = [[context device] newBufferWithBytesNoCopy:mMapValueAddr length:curSize options:MTLResourceStorageModeShared deallocator:nil]; int valueByte = mQuantValue ? 1 : (mtbn->useFp16InsteadFp32() ? 2 : 4); if (useDynamicScaleBuffer()) { int scaleByte = mtbn->useFp16InsteadFp32() ? 2 : 4; id newKScale = [[context device] newBufferWithLength:mMaxLength * scaleByte * 2 options:MTLResourceStorageModeShared]; id newVScale = [[context device] newBufferWithLength:mMaxLength * scaleByte * 2 options:MTLResourceStorageModeShared]; if (need_copy && mKScaleBuffer != nil) { ::memcpy([newKScale contents], [mKScaleBuffer contents], (old_piece_size / valueByte) * scaleByte * 2); ::memcpy([newVScale contents], [mVScaleBuffer contents], (old_piece_size / valueByte) * scaleByte * 2); } mKScaleBuffer = newKScale; mVScaleBuffer = newVScale; } else { mKScaleBuffer = nil; mVScaleBuffer = nil; } // Step 3: Move the kvcache from temporary buffers in memory to disk memset(mMapKeyAddr, 0, curSize); memset(mMapValueAddr, 0, curSize); if (need_copy) { ::memcpy(mMapKeyAddr, prevKey.data(), oldSize); for(int i = 0; i < mKvNumHead * mHeadDim; i++) { ::memcpy(mMapValueAddr + i * new_piece_stride, prevValue.data() + i * old_piece_stride, old_piece_size); } } } void MetalKVCacheManager::onClear() { if (mKVCacheInDisk) { mKeyBuffer = nil; mValueBuffer = nil; // mSaveShareKvPrefix also need unmap file unmapKVCache(mCurrentTotalSize, mCurrentTotalSize); if(mSaveShareKvPrefix) { // set prefix cachefile validation auto k_file = mBasePrefixFileName + ".k"; if(MNNFileExist(k_file.c_str())) { auto k_sync_file = mBasePrefixFileName + "_sync.k"; MNNCreateFile(k_sync_file.c_str()); } auto v_file = mBasePrefixFileName + ".v"; if(MNNFileExist(v_file.c_str())) { auto v_sync_file = mBasePrefixFileName + "_sync.v"; MNNCreateFile(v_sync_file.c_str()); } } else { // delete temp kvcache file removeKVCacheFile(); } mKVCacheInDisk = false; } mPastKey.reset(); mPastValue.reset(); mKScaleBuffer = nil; mVScaleBuffer = nil; mMaxLength = 0; mPastLength = 0; } } // namespace MNN #endif // MNN_SUPPORT_TRANSFORMER_FUSE