Files
2026-07-13 13:33:03 +08:00

397 lines
19 KiB
Plaintext

//
// 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<MetalBackend *>(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<MetalBackend *>(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<int8_t>({mMaxLength, mKvNumHead, mHeadDim * keyByte});
// past_value: [kvNumhead, headdim, maxlen]
Tensor* new_value = Tensor::createDevice<int8_t>({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<MetalBackend *>(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<MetalBackend *>(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<int8_t>({mMaxLength, mKvNumHead, mHeadDim * keyByte});
// past_value: [kvNumhead, headdim, maxlen]
Tensor* new_value = Tensor::createDevice<int8_t>({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<MTLBuffer> newKScale = [[context device] newBufferWithLength:mMaxLength * scaleByte * 2 options:MTLResourceStorageModeShared];
id<MTLBuffer> 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<MetalBackend *>(mBackend);
auto context = (__bridge MNNMetalContext *)mtbn->context();
mmapKVCache(oldSize, oldSize, specKeyFile, specValueFile);
std::vector<int8_t> 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<MTLBuffer>
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<MTLBuffer> newKScale = [[context device] newBufferWithLength:mMaxLength * scaleByte * 2 options:MTLResourceStorageModeShared];
id<MTLBuffer> 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