// // CPUAttention.cpp // MNN // // Created by MNN on 2024/03/19. // Copyright © 2018, Alibaba Group Holding Limited // #ifdef MNN_SUPPORT_TRANSFORMER_FUSE #include #include "CPUAttention.hpp" #include "CPUBackend.hpp" #include "compute/CommonOptFunction.h" #include "compute/TurboQuant.hpp" #include "core/Macro.h" #include "core/Concurrency.h" #include "core/BufferAllocator.hpp" #include "core/TensorUtils.hpp" #include "core/OpCommonUtils.hpp" #include "core/BufferAllocator.hpp" #include "compute/ConvolutionTiledExecutor.hpp" #if defined(__aarch64__) #define FLOAT16_T __fp16 #else #define FLOAT16_T float #endif namespace MNN { template static void _maskQK(float* qkPacked, const float* scale, size_t seqLen, size_t processedKvSeq, int pack, int kvSeqLen, int kvoffset, int padKvSeqLen, const float* sinksPtr, const Tensor* mask, bool quantKey, bool isLowerTriangular) { /* * FIGURE 1: mask->elementSize() == seqLen * maskStride * Context: Cross Attention or Prefill stage (Full Context). * Logic: gapLen = 0. The mask tensor dimensions match the logical QK matrix exactly. * Direct access: mask[row * stride + col] * Row\Col 0 1 2 3 * * 0 0 X X X (Can only see Col 0) * * 1 0 0 X X (Can see Col 0, 1) * * 2 0 0 0 X (Can see Col 0, 1, 2) * * 3 0 0 0 0 (Fully visible) * * Legend: * '0' : Visible (Value = Scale * QK) * 'X' : Masked (Value = -inf) */ /* * FIGURE 2: mask->elementSize() != seqLen * maskStride * Context: Self-Attention Inference (Decoding stage). * Logic: gapLen = maskStride - seqLen (Right Alignment). * The "Gap" represents History KV Cache, which is implicitly visible. * The Mask Tensor only covers the current sequence window. * * Example: maskStride (Total KV) = 6 * seqLen (Current Q) = 4 * gapLen = 6 - 4 = 2 * * Structure: * - Cols [0, 1]: "Gap" / History region. Code logic: `if (col < gapLen) continue;`. * No mask is added, so they remain Visible ('0'). * - Cols [2-5]: "Current" region. Code logic: `mask[col - gapLen]`. * * Row\Col 0 1 | 2 3 4 5 * (Gap) | (Mask Tensor Region) * * 0 0 0 | 0 X X X <-- Mask row 0 applies to Col 2~5 * | * 1 0 0 | 0 0 X X <-- Mask row 1 applies to Col 2~5 * | * 2 0 0 | 0 0 0 X <-- Mask row 2 applies to Col 2~5 * | * 3 0 0 | 0 0 0 0 <-- Mask row 3 applies to Col 2~5 * * Legend: * '0' (Left) : History KV, implicitly visible (code skips mask addition). * '0' (Right) : Current KV, visible according to Mask Tensor. * 'X' : Masked by Mask Tensor (-inf). */ if (isLowerTriangular && quantKey) { return; } constexpr float NEG_INF = -std::numeric_limits::infinity(); auto source = (T*)qkPacked; float scaleVal = scale[0]; auto kvBlockCount = UP_DIV(processedKvSeq, pack); auto qkSize = ROUND_UP(processedKvSeq, pack) * seqLen; if (isLowerTriangular) { for (int i = 0; i < qkSize; ++i) { source[i] *= scaleVal; } return; } if (mask == nullptr) { return; } int gapLen = (mask->elementSize() == (seqLen + padKvSeqLen) * (kvSeqLen + padKvSeqLen)) ? 0 : static_cast(kvSeqLen - seqLen); auto maskPtr = mask->host(); auto maskCols = (mask->elementSize() == (seqLen + padKvSeqLen) * (kvSeqLen + padKvSeqLen)) ? kvSeqLen + padKvSeqLen : seqLen + padKvSeqLen; for (int i = 0; i < kvBlockCount; ++i) { T* blockDataPtr = source + (i * seqLen * pack); for (int j = 0; j < seqLen; ++j) { T* dataPtr = blockDataPtr + (j * pack); const T* currentMaskRow = maskPtr + j * maskCols; for (int k = 0; k < pack; ++k) { float val = (float)dataPtr[k]; if (!quantKey) { val *= scaleVal; dataPtr[k] = (T)val; } int currentKvSeqIndx = kvoffset + i * pack + k; // kvoffset=i*mBlockKv if (currentKvSeqIndx < gapLen) { continue; } if (currentKvSeqIndx - gapLen >= maskCols) { break; } val += (float)currentMaskRow[currentKvSeqIndx - gapLen]; dataPtr[k] = (T)val; } } } } ErrorCode CPUAttention::onResize(const std::vector& inputs, const std::vector& outputs) { auto gcore = static_cast(backend())->functions(); auto core = static_cast(backend())->int8Functions(); gcore->MNNGetMatMulPackMode(&eP, &lP, &hP); mThreadNum = ((CPUBackend*)backend())->threadNumber(); mPack = gcore->pack; mBytes = gcore->bytes; int attentionOption = static_cast(backend())->getRuntime()->hint().attentionOption; mUseFlashAttention = (attentionOption / 8 == 1); // attentionOption % 8: // 0: no quant, 1: K int8, 2: K+V int8, 3: K TQ3, 4: K+V TQ3, 5: K TQ4, 6: K+V TQ4 int quantMode = attentionOption % 8; mKeyQuantMode = KVQuantMode::None; mValueQuantMode = KVQuantMode::None; if (inputs.size() < 5) { switch (quantMode) { case 1: mKeyQuantMode = KVQuantMode::Int8; break; case 2: mKeyQuantMode = KVQuantMode::Int8; mValueQuantMode = KVQuantMode::Int8; break; case 3: mKeyQuantMode = KVQuantMode::TQ3; break; case 4: mKeyQuantMode = KVQuantMode::TQ3; mValueQuantMode = KVQuantMode::TQ3; break; case 5: mKeyQuantMode = KVQuantMode::TQ4; break; case 6: mKeyQuantMode = KVQuantMode::TQ4; mValueQuantMode = KVQuantMode::TQ4; break; default: break; } if (mValueQuantMode == KVQuantMode::Int8 && !mUseFlashAttention) { mValueQuantMode = KVQuantMode::None; } } static_cast(backend())->int8Functions()->MNNGetGemmUnit(&hP8, &lP8, &eP8); auto query = inputs[0]; auto key = inputs[1]; int seqLen = query->length(1); int mBlockNum = 1; mNumHead = query->length(2); mHeadDim = query->length(3); mKvNumHead = key->length(2); if (!mIsKVShared) { mKVCacheManager->setKVQuantMode(mUseFlashAttention, mKeyQuantMode, mValueQuantMode); mKVCacheManager->onResize(mKvNumHead, mHeadDim); } // Common buffer allocated auto bufferAlloc = static_cast(backend())->getBufferAllocator(); mPackQKV.reset(Tensor::createDevice({mThreadNum, UP_DIV(mHeadDim, mPack), seqLen, mPack * mBytes})); backend()->onAcquireBuffer(mPackQKV.get(), Backend::DYNAMIC); if (inputs.size() > 4 || mUseFlashAttention) { // needed by flash attention and sliding attention with sink mRunningMax.reset(Tensor::createDevice({mThreadNum, seqLen * 4})); mRunningSum.reset(Tensor::createDevice({mThreadNum, seqLen * 4})); backend()->onAcquireBuffer(mRunningMax.get(), Backend::DYNAMIC); backend()->onAcquireBuffer(mRunningSum.get(), Backend::DYNAMIC); } if (mUseFlashAttention) { // extra buffer need by flash attention mExpfDiffMax.reset(Tensor::createDevice({mThreadNum, seqLen * 4})); mTempOut.reset(Tensor::createDevice({mThreadNum, UP_DIV(mHeadDim, mPack), seqLen, mPack * mBytes})); backend()->onAcquireBuffer(mExpfDiffMax.get(), Backend::DYNAMIC); backend()->onAcquireBuffer(mTempOut.get(), Backend::DYNAMIC); } if (mKeyQuantMode == KVQuantMode::TQ3 || mKeyQuantMode == KVQuantMode::TQ4 || mValueQuantMode == KVQuantMode::TQ3 || mValueQuantMode == KVQuantMode::TQ4) { // Vec_dot fusion buffers (per thread, shared by TQ3/TQ4): // Q_rotated: seqLen * headDim floats (WHT_forward of scaled Q) // V_acc_rotated: headDim floats (accumulator in rotated domain) // weights: blockKV floats (extracted softmax weights for one query) int blockKV = mUseFlashAttention ? MNN_FLASH_ATTENTION_BLOCK_SIZE : (seqLen + 64); int qRotatedSize = seqLen * mHeadDim * sizeof(float); int vAccSize = mHeadDim * sizeof(float); int weightsSize = blockKV * sizeof(float); mTQ3DequantBuf.reset(Tensor::createDevice({mThreadNum, qRotatedSize + vAccSize + weightsSize})); backend()->onAcquireBuffer(mTQ3DequantBuf.get(), Backend::DYNAMIC); } if (mKeyQuantMode == KVQuantMode::Int8) { int outterSeqLen = UP_DIV(seqLen, eP8); int outterHeadDim = UP_DIV(mHeadDim, lP8); size_t packedQSize = 0; if (outterSeqLen > 0) { int fullSeqBlocks = (seqLen / eP8); packedQSize += (size_t)fullSeqBlocks * outterHeadDim * eP8 * lP8; int lastEUnit = seqLen % eP8; if (lastEUnit != 0) { packedQSize += (size_t)outterHeadDim * lastEUnit * lP8; } } mPackQ.reset(Tensor::createDevice({mNumHead, (int32_t)packedQSize})); backend()->onAcquireBuffer(mPackQ.get(), Backend::DYNAMIC); mSumQ = bufferAlloc->alloc(mThreadNum * ROUND_UP(seqLen, eP8) * mBlockNum * sizeof(int32_t)); mQueryScale = bufferAlloc->alloc(mNumHead * seqLen * mBlockNum * QUANT_INFO_BYTES); mQueryZeroPoint = bufferAlloc->alloc(mNumHead * seqLen * mBlockNum * QUANT_INFO_BYTES); mQueryQuantZero = bufferAlloc->alloc(mNumHead * seqLen * mBlockNum * QUANT_INFO_BYTES); mQueryQuantScale = bufferAlloc->alloc(mNumHead * seqLen * mBlockNum * QUANT_INFO_BYTES); mQuantQuery = bufferAlloc->alloc(seqLen * mNumHead * UP_DIV(mHeadDim, gcore->pack) * gcore->pack); if (mBlockNum > 1) { mAccumBuffer = bufferAlloc->alloc(eP8 * hP8 * mThreadNum * QUANT_INFO_BYTES); if (mAccumBuffer.invalid()) { return OUT_OF_MEMORY; } } if (mSumQ.invalid() || mQueryScale.invalid() || mQueryQuantZero.invalid() || mQueryZeroPoint.invalid() || mQueryQuantScale.invalid() || mQuantQuery.invalid()) { return OUT_OF_MEMORY; } // post parameters for int8 gemm mGemmRelu.reset(2 * sizeof(int32_t)); if (!mGemmRelu.get()) { MNN_ERROR("Allocate mGemmRelu buffer failed in CPU Attention"); return OUT_OF_MEMORY; } ((float*)mGemmRelu.get())[0] = -std::numeric_limits().max(); ((float*)mGemmRelu.get())[1] = std::numeric_limits().max(); if (mBytes == 2) { gcore->MNNFp32ToLowp((float*)mGemmRelu.get(), reinterpret_cast(mGemmRelu.get()), 2); } // GemmInt8 kernels if (mBytes == 4) { mInt8GemmKernel = core->Int8GemmKernel; } else { mInt8GemmKernel = core->MNNGemmInt8AddBiasScale_Unit_FP16; } if (mValueQuantMode == KVQuantMode::Int8) { mQuantQK = bufferAlloc->alloc(mThreadNum * eP8 * ROUND_UP(MNN_FLASH_ATTENTION_BLOCK_SIZE, mPack)); mQKScale = bufferAlloc->alloc(eP8 * QUANT_INFO_BYTES); mQKBias = bufferAlloc->alloc(eP8 * QUANT_INFO_BYTES); mSumQK = bufferAlloc->alloc(mThreadNum * eP8 * QUANT_INFO_BYTES); if (mQuantQK.invalid() || mQKScale.invalid() || mQKBias.invalid() || mSumQK.invalid()) { return OUT_OF_MEMORY; } } } else { mPackQ.reset( Tensor::createDevice({mThreadNum, UP_DIV(seqLen, eP), ROUND_UP(mHeadDim, lP), eP * mBytes})); backend()->onAcquireBuffer(mPackQ.get(), Backend::DYNAMIC); backend()->onAcquireBuffer(mPackQKV.get(), Backend::DYNAMIC); } // release tensor backend()->onReleaseBuffer(mPackQ.get(), Backend::DYNAMIC); backend()->onReleaseBuffer(mPackQKV.get(), Backend::DYNAMIC); if (inputs.size() > 4 || mUseFlashAttention) { backend()->onReleaseBuffer(mRunningMax.get(), Backend::DYNAMIC); backend()->onReleaseBuffer(mRunningSum.get(), Backend::DYNAMIC); } if (mUseFlashAttention) { backend()->onReleaseBuffer(mExpfDiffMax.get(), Backend::DYNAMIC); backend()->onReleaseBuffer(mTempOut.get(), Backend::DYNAMIC); } if (mKeyQuantMode == KVQuantMode::TQ3 || mKeyQuantMode == KVQuantMode::TQ4 || mValueQuantMode == KVQuantMode::TQ3 || mValueQuantMode == KVQuantMode::TQ4) { backend()->onReleaseBuffer(mTQ3DequantBuf.get(), Backend::DYNAMIC); } // release memchunk if (mKeyQuantMode == KVQuantMode::Int8) { bufferAlloc->free(mSumQ); bufferAlloc->free(mQueryScale); bufferAlloc->free(mQueryZeroPoint); bufferAlloc->free(mQueryQuantScale); bufferAlloc->free(mQueryQuantZero); bufferAlloc->free(mQuantQuery); if (mBlockNum > 1) { bufferAlloc->free(mAccumBuffer); } if (mValueQuantMode == KVQuantMode::Int8) { bufferAlloc->free(mQuantQK); bufferAlloc->free(mQKScale); bufferAlloc->free(mQKBias); bufferAlloc->free(mSumQK); } } // Only allocated for quantized Q&K if (mKeyQuantMode == KVQuantMode::Int8) { if (mBytes == 4) { mQuantFunc = core->MNNFloat2Int8; } else { mQuantFunc = core->DynamicQuanInput_ARM82; } } return NO_ERROR; } ErrorCode CPUAttention::onExecute(const std::vector& inputs, const std::vector& outputs) { auto gcore = static_cast(backend())->functions(); auto core = static_cast(backend())->int8Functions(); bool outputC4 = TensorUtils::getDescribe(outputs[0])->dimensionFormat == MNN_DATA_FORMAT_NC4HW4; auto query = inputs[0]; auto key = inputs[1]; auto value = inputs[2]; int seqLen = query->length(1); const Tensor* mask = nullptr; if (inputs.size() > 3) { mask = inputs[3]; } const Tensor* sinks = nullptr; if (inputs.size() > 4) { sinks = inputs[4]; MNN_ASSERT(sinks != nullptr); MNN_ASSERT(sinks->elementSize() == mNumHead) } int numHeadDiv = UP_DIV(mNumHead, mThreadNum); int group_size = mNumHead / mKvNumHead; // reduce the value of 'query' to avoid fp16 overflow float mScale = (mMeta && mMeta->attn_scale > 0) ? mMeta->attn_scale : (1.0 / sqrt(mHeadDim)); float q_scale = 1.0; if (mBytes == 2 && mKeyQuantMode != KVQuantMode::Int8) { // reduce the value of 'query' to 'query * FP16_QSCALE', avoid fp16 overflow FLOAT16_T minValue; FLOAT16_T maxValue; gcore->MNNCountMaxMinValue(query->host(), (float*)(&minValue), (float*)(&maxValue), query->elementSize()); float maxV = maxValue; float minV = minValue; float absMax = ALIMAX(fabsf(maxV), fabsf(minV)); if (absMax > 1.0f) { q_scale = 1.0f / absMax; } mScale /= q_scale; } int insertLen = seqLen; if (!mIsKVShared) { if (mKVCache && mMeta != nullptr) { if (mMeta->previous == mMeta->remove) { mKVCacheManager->onClear(); mKVCacheManager->onAlloc(mMeta, seqLen); } else { MNN_ASSERT(mMeta->previous == mKVCacheManager->kvLength()); mKVCacheManager->onRealloc(mMeta); } insertLen = (int)mMeta->add; } else { mKVCacheManager->onClear(); mKVCacheManager->onAlloc(mMeta, seqLen); } // Add the new kv to the kvcache mKVCacheManager->onUpdateKV(key, value, (int)insertLen); } else { // Shared layer: KV cache is shared via onClone, skip KV update insertLen = (int)mMeta->add; } if (mUseFlashAttention) { mBlockKV = ALIMIN(MNN_FLASH_ATTENTION_BLOCK_SIZE, mKVCacheManager->kvLength()); } else { mBlockKV = mKVCacheManager->kvLength(); } // Constant Initialization auto padSeqLength = seqLen - insertLen; seqLen = insertLen; int kvSeqLen = mKVCacheManager->kvLength(); int maxLen = mKVCacheManager->maxLength(); int32_t units[2] = {eP, lP}; const float* sinksPtr = sinks ? sinks->host() : nullptr; int kvValidOffset = kvSeqLen - seqLen; // reuse_kv=true or decode, kvValidOffset>0 // Temporary tensors for intermediate results std::shared_ptr unpackQK(Tensor::createDevice({mThreadNum, seqLen, mBlockKV})); std::shared_ptr softmMaxQ(Tensor::createDevice( {mThreadNum, seqLen, ROUND_UP(mBlockKV, mPack)})); // [mBlockKV/mPack, seqLen, mPack ] std::shared_ptr newPackQK; if (mValueQuantMode != KVQuantMode::Int8) { newPackQK.reset(Tensor::createDevice({mThreadNum, eP * ROUND_UP(mBlockKV, lP) * mBytes})); } else { newPackQK.reset( Tensor::createDevice({mThreadNum, eP8 * ROUND_UP(MNN_FLASH_ATTENTION_BLOCK_SIZE, lP8)})); } std::shared_ptr mTempQKBlock( Tensor::createDevice({mThreadNum, UP_DIV(mBlockKV, mPack), seqLen, mPack * mBytes})); backend()->onAcquireBuffer(unpackQK.get(), Backend::STATIC); backend()->onAcquireBuffer(softmMaxQ.get(), Backend::STATIC); backend()->onAcquireBuffer(newPackQK.get(), Backend::STATIC); backend()->onAcquireBuffer(mTempQKBlock.get(), Backend::STATIC); // Quantize Q and initialize bias 0 if (mKeyQuantMode == KVQuantMode::Int8) { mGemmBias.reset(ROUND_UP(ALIMAX(mBlockKV, mHeadDim), hP8) * QUANT_INFO_BYTES); if (!mGemmBias.get()) { MNN_ERROR("Allocate bias buffer failed in CPU Attention\n"); return OUT_OF_MEMORY; } memset(mGemmBias.get(), 0, ROUND_UP(ALIMAX(mBlockKV, mHeadDim), hP8) * QUANT_INFO_BYTES); // Q: [seqLen,numHead,headDim] // maxQ, minQ: [seqLen,numHead] // scaleQ, zeroQ: [numHead, seqLen] // quantQ: [seqLen,numHead,headDim] auto queryPtr = query->host(); int divPart = UP_DIV(seqLen * mNumHead, mThreadNum); MNN_CONCURRENCY_BEGIN(tId, mThreadNum) { size_t info[9] = {1, (size_t)mHeadDim, 1, 1, 1, 1, 1, 1, 0}; auto remainLu = seqLen * mNumHead - tId * divPart; if (remainLu > 0) { remainLu = ALIMIN(divPart, remainLu); for (int i = tId * divPart; i < tId * divPart + remainLu; ++i) { // address auto srcFloatPtr = (float*)(queryPtr + i * mHeadDim * mBytes); auto dstInt8Ptr = (int8_t*)(mQuantQuery.ptr() + i * mHeadDim); auto quantScalePtr = (float*)(mQueryQuantScale.ptr() + i * QUANT_INFO_BYTES); auto quantZeroPtr = (float*)(mQueryQuantZero.ptr() + i * QUANT_INFO_BYTES); // scaleQ, zeroQ, [seqLen,numHead]->[numHead,seqLen] int indexQ = (i / mNumHead) + (i % mNumHead) * seqLen; auto scalePtr = (float*)(mQueryScale.ptr() + indexQ * QUANT_INFO_BYTES); auto zeroPtr = (float*)(mQueryZeroPoint.ptr() + indexQ * QUANT_INFO_BYTES); // compute the quant/dequant scale/bias gcore->MNNAsyQuantInfo(scalePtr, zeroPtr, quantScalePtr, quantZeroPtr, nullptr, nullptr, srcFloatPtr, info); scalePtr[0] *= mScale; zeroPtr[0] *= mScale; // quantize the float query to int8_t query mQuantFunc(srcFloatPtr, dstInt8Ptr, UP_DIV(mHeadDim, gcore->pack), quantScalePtr, -128, 127, quantZeroPtr, 0); } } } MNN_CONCURRENCY_END(); // source int8_t query: [seqLen,numHead,headDim] // dest int8_t query: [numHead,seqLen/eP,headDim/lP,eP,lP] int outterSeqLen = UP_DIV(seqLen, eP8); int outterHeadDim = UP_DIV(mHeadDim, lP8); size_t outputOffset = 0; const int8_t* src_base_ptr = (const int8_t*)mQuantQuery.ptr(); int8_t* dst_base_ptr = mPackQ->host(); for (int h = 0; h < mNumHead; ++h) { for (int seqBlock = 0; seqBlock < outterSeqLen; ++seqBlock) { int seqBase = seqBlock * eP8; int eunit = std::min(eP8, seqLen - seqBase); size_t currentSeqBlockSize = (size_t)outterHeadDim * eunit * lP8; for (int dimBlock = 0; dimBlock < outterHeadDim; ++dimBlock) { int dimBase = dimBlock * lP8; int headDimRemain = mHeadDim - dimBase; int copyLen = std::min(lP8, headDimRemain); if (copyLen <= 0) { continue; } int8_t* dst_block_ptr = dst_base_ptr + outputOffset + (size_t)dimBlock * (eunit * lP8); const size_t src_row_stride = (size_t)mNumHead * mHeadDim; for (int seqLocal = 0; seqLocal < eunit; ++seqLocal) { int innerSeq = seqBase + seqLocal; const int8_t* src_row_ptr = src_base_ptr + (size_t)innerSeq * src_row_stride + (size_t)h * mHeadDim + dimBase; int8_t* dst_row_ptr = dst_block_ptr + seqLocal * lP8; std::memcpy(dst_row_ptr, src_row_ptr, copyLen); } if (copyLen < lP8) { for (int seqLocal = 0; seqLocal < eunit; ++seqLocal) { int8_t* dst_pad_ptr = dst_block_ptr + seqLocal * lP8 + copyLen; std::memset(dst_pad_ptr, 0, lP8 - copyLen); } } } outputOffset += currentSeqBlockSize; } } // Finish quantize Q if (mValueQuantMode == KVQuantMode::Int8) { auto scalePtr = (float*)(mQKScale.ptr()); auto zeroPtr = (float*)(mQKBias.ptr()); for (int k = 0; k < eP8; ++k) { scalePtr[k] = 1.f / 255.f; #ifdef MNN_USE_SSE zeroPtr[k] = 0; #else zeroPtr[k] = 128.f / 255.f; #endif } } } std::function mCompute = [=](int tId) { int8_t* qReordered = nullptr; auto qkPacked = mTempQKBlock->host() + tId * mTempQKBlock->stride(0); auto qkFlatten = unpackQK->host() + tId * unpackQK->stride(0); auto qkSoftmax = softmMaxQ->host() + tId * softmMaxQ->stride(0); auto qkReordered = newPackQK->host() + tId * newPackQK->stride(0); auto qkvPacked = mPackQKV->host() + tId * mPackQKV->stride(0); int headIndex = tId * numHeadDiv; int headsToCompute = ALIMIN(numHeadDiv, mNumHead - headIndex); // Flash Attention auto runningMax = mRunningMax ? (float*)(mRunningMax->host() + tId * mRunningMax->stride(0)) : nullptr; auto runningSum = mRunningSum ? (float*)(mRunningSum->host() + tId * mRunningSum->stride(0)) : nullptr; auto diffScale = mExpfDiffMax ? (float*)(mExpfDiffMax->host() + tId * mExpfDiffMax->stride(0)) : nullptr; auto outputPacked = mTempOut ? mTempOut->host() + tId * mTempOut->stride(0) : qkvPacked; int kvBlocks = UP_DIV(kvSeqLen, mBlockKV); bool isLowerTriangular = (mask == nullptr); if (mask != nullptr && mask->shape().empty()) { if (mBytes == 2) { auto maskPtr = mask->host(); if (maskPtr[0] < 1e-6) { isLowerTriangular = true; } } else { auto maskPtr = mask->host(); if (maskPtr[0] < 1e-6f) { isLowerTriangular = true; } } } bool useMaskInSoftmax = (isLowerTriangular && sinksPtr == nullptr); QuanPostTreatParameters gemmParam4QxK, gemmParam4QKxV; // used by int8 gemm, allocated per thread. SumByAxisParams sumParams4QxK, sumParams4QKxV = {}; float* qSumAddr = nullptr; float* qScale = nullptr; float* qBias = nullptr; float* accumbuff = nullptr; int32_t unitColBufferSize = 0; if (mKeyQuantMode == KVQuantMode::Int8) { // parameters shared by all mBlockKV gemmParam4QxK.blockNum = mBlockNum; gemmParam4QxK.biasFloat = reinterpret_cast(mGemmBias.get()); gemmParam4QxK.useInt8 = 0; gemmParam4QxK.fp32minmax = reinterpret_cast(mGemmRelu.get()); sumParams4QxK.oneScale = 0; sumParams4QxK.SRC_UNIT = lP8; sumParams4QxK.blockNum = mBlockNum; sumParams4QxK.DST_XUNIT = eP8; sumParams4QxK.inputBlock = 0; sumParams4QxK.kernelxy = 1; // fixed sumParams4QxK.LU = UP_DIV(mHeadDim, lP8); sumParams4QxK.unitColBufferSize = ROUND_UP(mHeadDim, lP8) * eP8; sumParams4QxK.kernelCountUnitDouble = UP_DIV(mHeadDim, lP8); sumParams4QxK.valid = mHeadDim % lP8; if (mBlockNum > 1) { accumbuff = (float*)(mAccumBuffer.ptr() + tId * eP8 * hP8 * QUANT_INFO_BYTES); } unitColBufferSize = eP8 * ROUND_UP(mHeadDim, lP8); if (mValueQuantMode == KVQuantMode::Int8) { gemmParam4QKxV.blockNum = mBlockNum; gemmParam4QKxV.biasFloat = reinterpret_cast(mGemmBias.get()); gemmParam4QKxV.useInt8 = 0; gemmParam4QKxV.fp32minmax = reinterpret_cast(mGemmRelu.get()); gemmParam4QKxV.inputScale = (float*)mQKScale.ptr(); gemmParam4QKxV.inputBias = (float*)mQKBias.ptr(); gemmParam4QKxV.srcKernelSum = (float*)(mSumQK.ptr() + tId * eP8 * QUANT_INFO_BYTES); sumParams4QKxV.oneScale = 0; sumParams4QKxV.SRC_UNIT = lP8; sumParams4QKxV.blockNum = mBlockNum; sumParams4QKxV.DST_XUNIT = eP8; sumParams4QKxV.inputBlock = 0; sumParams4QKxV.kernelxy = 1; sumParams4QKxV.unitColBufferSize = ROUND_UP(MNN_FLASH_ATTENTION_BLOCK_SIZE, lP8) * eP8; sumParams4QKxV.kernelCountUnitDouble = UP_DIV(MNN_FLASH_ATTENTION_BLOCK_SIZE, lP8); } } size_t vstride0 = ROUND_UP(mHeadDim, hP) * ROUND_UP(MNN_FLASH_ATTENTION_BLOCK_SIZE, lP); if (mValueQuantMode == KVQuantMode::Int8) { vstride0 = (ROUND_UP(mHeadDim, hP8) * ROUND_UP(mKVCacheManager->getFlashAttentionBlockKv(), lP8) + 2 * QUANT_INFO_BYTES * mBlockNum * ROUND_UP(mHeadDim, hP8)); } // use for V float const* srcPtr[1]; // only used for quantized V float vQuantScale[1] = {255.f}; float vQuantBias[1] = {-128.f}; int32_t infoInt8V[5]; infoInt8V[0] = 1; // number infoInt8V[2] = static_cast(sumParams4QKxV.unitColBufferSize); infoInt8V[3] = 1; // stride int32_t elInt8V[4] = {eP8, ROUND_UP(MNN_FLASH_ATTENTION_BLOCK_SIZE, lP8), 0, 0}; // only used for float V int32_t infoFloatV[4]; infoFloatV[0] = 1; // number infoFloatV[1] = seqLen; // eReal infoFloatV[3] = 1; // stride int32_t elFloatV[4] = {seqLen, ROUND_UP(kvSeqLen, lP), 0, 0}; int offset[2] = {seqLen, mNumHead * mHeadDim}; for (int h = headIndex; h < headIndex + headsToCompute; h++) { auto dstStep = mBytes * seqLen * mPack; if (outputC4) { outputPacked = outputs[0]->host() + h * mHeadDim * seqLen * mBytes; if (!mUseFlashAttention) { qkvPacked = outputPacked; } } // Prepare for flash attention if (runningSum && runningMax) { if (sinksPtr == nullptr) { memset(runningSum, 0, mRunningSum->stride(0)); for (int k = 0; k < seqLen; ++k) { runningMax[k] = std::numeric_limits::lowest(); } } else { for (int k = 0; k < seqLen; ++k) { runningSum[k] = 1.f; // exp(sink-sink) } float sinkVal; if (mBytes == 2) { sinkVal = ((FLOAT16_T*)sinksPtr)[h]; } else { sinkVal = sinksPtr[h]; } for (int k = 0; k < seqLen; ++k) { runningMax[k] = sinkVal; } } } // Compute the current addresses int kvHeadIndex = h / group_size; int8_t* keyAddr = mKVCacheManager->addrOfKey(kvHeadIndex); int8_t* keySum = mKVCacheManager->addrOfKeySum(kvHeadIndex); int8_t* valueAddr = mKVCacheManager->addrOfValue(kvHeadIndex); float* valueSum = (float*)mKVCacheManager->addrOfValueSum(kvHeadIndex); // Get packed Q if (mKeyQuantMode != KVQuantMode::Int8) { qReordered = mPackQ->host() + tId * mPackQ->stride(0); gcore->MNNAttenPackAndScaleSingleHead((float*)qReordered, (float*)(query->host() + h * mHeadDim * mBytes), mHeadDim * mNumHead, &q_scale, units, seqLen, mHeadDim); } else { qReordered = mPackQ->host() + h * mPackQ->stride(0); qSumAddr = (float*)(mSumQ.ptr() + tId * ROUND_UP(seqLen, eP8) * mBlockNum * QUANT_INFO_BYTES); qScale = (float*)(mQueryScale.ptr() + h * seqLen * mBlockNum * QUANT_INFO_BYTES); qBias = (float*)(mQueryZeroPoint.ptr() + h * seqLen * mBlockNum * QUANT_INFO_BYTES); gcore->MNNSumByAxisLForMatmul_A(qSumAddr, qReordered, qScale, seqLen, sumParams4QxK); } // Start computing for (int i = 0; i < kvBlocks; ++i) { int subKvSeqLen = ALIMIN(mBlockKV, kvSeqLen - i * mBlockKV); // 1. query @ key if (mKeyQuantMode == KVQuantMode::TQ3) { // Vec_dot fusion: Q_rotated · TQ3_compressed_K directly (no dequant buffer) // Q_rotated = WHT_forward(Q * q_scale) computed once per KV block iteration int tq3BytesPerSeq = (mHeadDim / TQ3_BLOCK_SIZE) * TQ3_BYTES_PER_BLOCK; int numBlocks = mHeadDim / TQ3_BLOCK_SIZE; auto tq3Buf = mTQ3DequantBuf->host() + tId * mTQ3DequantBuf->stride(0); auto qRotated = (float*)tq3Buf; // seqLen * headDim floats // Pre-rotate Q vectors (only on first KV block) if (i == 0) { float qScale = 1.0f / sqrtf((float)mHeadDim); auto queryBase = (float*)(query->host() + h * mHeadDim * mBytes); int qStride = mHeadDim * mNumHead; // stride between seq positions for (int q = 0; q < seqLen; q++) { for (int b = 0; b < numBlocks; b++) { float scaled[TQ3_BLOCK_SIZE]; if (mBytes == 2) { auto src16 = (FLOAT16_T*)(query->host() + h * mHeadDim * mBytes) + q * mHeadDim * mNumHead; for (int d = 0; d < TQ3_BLOCK_SIZE; d++) { scaled[d] = (float)src16[b * TQ3_BLOCK_SIZE + d] * qScale; } } else { auto srcF = queryBase + q * qStride; for (int d = 0; d < TQ3_BLOCK_SIZE; d++) { scaled[d] = srcF[b * TQ3_BLOCK_SIZE + d] * qScale; } } tq3_wht_forward_32(qRotated + q * mHeadDim + b * TQ3_BLOCK_SIZE, scaled); } } } // Compute QK scores directly: score[q][s] = Σ_b vec_dot_block(Q_rot, K_tq3) // Output format: qkPacked [kvSeq/mPack, seqLen, mPack] for (int s = 0; s < subKvSeqLen; s++) { int seqIdx = i * mBlockKV + s; auto kPtr = (uint8_t*)keyAddr + seqIdx * tq3BytesPerSeq; for (int q = 0; q < seqLen; q++) { float score = 0.0f; auto qr = qRotated + q * mHeadDim; for (int b = 0; b < numBlocks; b++) { score += tq3_vec_dot_block(qr + b * TQ3_BLOCK_SIZE, kPtr + b * TQ3_BYTES_PER_BLOCK); } // Write to [kvSeq/mPack, seqLen, mPack] format int packIdx = (s / mPack) * seqLen * mPack + q * mPack + s % mPack; if (mBytes == 2) { ((FLOAT16_T*)qkPacked)[packIdx] = (FLOAT16_T)score; } else { ((float*)qkPacked)[packIdx] = score; } } } } else if (mKeyQuantMode == KVQuantMode::TQ4) { // Vec_dot fusion for TQ4 (4-bit): same logic as TQ3, different bytesPerBlock + functions int tq4BytesPerSeq = (mHeadDim / TQ4_BLOCK_SIZE) * TQ4_BYTES_PER_BLOCK; int numBlocks = mHeadDim / TQ4_BLOCK_SIZE; auto tq4Buf = mTQ3DequantBuf->host() + tId * mTQ3DequantBuf->stride(0); auto qRotated = (float*)tq4Buf; if (i == 0) { float qScale = 1.0f / sqrtf((float)mHeadDim); for (int q = 0; q < seqLen; q++) { for (int b = 0; b < numBlocks; b++) { float scaled[TQ4_BLOCK_SIZE]; if (mBytes == 2) { auto src16 = (FLOAT16_T*)(query->host() + h * mHeadDim * mBytes) + q * mHeadDim * mNumHead; for (int d = 0; d < TQ4_BLOCK_SIZE; d++) scaled[d] = (float)src16[b * TQ4_BLOCK_SIZE + d] * qScale; } else { auto srcF = (float*)(query->host() + h * mHeadDim * mBytes) + q * mHeadDim * mNumHead; for (int d = 0; d < TQ4_BLOCK_SIZE; d++) scaled[d] = srcF[b * TQ4_BLOCK_SIZE + d] * qScale; } tq3_wht_forward_32(qRotated + q * mHeadDim + b * TQ4_BLOCK_SIZE, scaled); } } } for (int s = 0; s < subKvSeqLen; s++) { int seqIdx = i * mBlockKV + s; auto kPtr = (uint8_t*)keyAddr + seqIdx * tq4BytesPerSeq; for (int q = 0; q < seqLen; q++) { float score = 0.0f; auto qr = qRotated + q * mHeadDim; for (int b = 0; b < numBlocks; b++) { score += tq4_vec_dot_block(qr + b * TQ4_BLOCK_SIZE, kPtr + b * TQ4_BYTES_PER_BLOCK); } int packIdx = (s / mPack) * seqLen * mPack + q * mPack + s % mPack; if (mBytes == 2) { ((FLOAT16_T*)qkPacked)[packIdx] = (FLOAT16_T)score; } else { ((float*)qkPacked)[packIdx] = score; } } } } else if (mKeyQuantMode != KVQuantMode::Int8) { auto keyPtr = keyAddr + i * UP_DIV(mBlockKV, hP) * ROUND_UP(mHeadDim, lP) * hP * mBytes; int loop_e = seqLen / eP; int remain = seqLen % eP; auto qStride0 = ROUND_UP(mHeadDim, lP) * eP * mBytes; size_t shapeParameters[7] = {(size_t)eP * lP * mBytes, ROUND_UP((size_t)mHeadDim, lP), (size_t)subKvSeqLen, (size_t)seqLen * mPack * mBytes, 0, 0, 0}; for (int ei = 0; ei < loop_e; ei++) { gcore->MNNPackedMatMul((float*)(qkPacked + (ei * eP * mPack) * mBytes), (float*)(qReordered + ei * qStride0), (float*)keyPtr, shapeParameters, nullptr, nullptr, nullptr, nullptr); } if (remain > 0) { gcore->MNNPackedMatMulRemain((float*)(qkPacked + (loop_e * eP * mPack) * mBytes), (float*)(qReordered + loop_e * qStride0), (float*)keyPtr, remain, shapeParameters, nullptr, nullptr, nullptr, nullptr); } } else { auto eRemain = seqLen; auto srcInt8 = qReordered; auto dstInt8 = qkPacked; auto keyPtr = keyAddr + i * UP_DIV(mBlockKV, hP8) * (ROUND_UP(mHeadDim, lP8) * hP8 + 2 * hP8 * QUANT_INFO_BYTES); gemmParam4QxK.weightKernelSum = (float*)(keySum + i * mBlockKV * QUANT_INFO_BYTES); gemmParam4QxK.inputScale = qScale; gemmParam4QxK.inputBias = qBias; gemmParam4QxK.srcKernelSum = qSumAddr; while (eRemain > 0) { auto eSize = ALIMIN(eP8, eRemain); mInt8GemmKernel(dstInt8, srcInt8, keyPtr, UP_DIV(mHeadDim, lP8), mBytes * seqLen * mPack, UP_DIV(subKvSeqLen, mPack), &gemmParam4QxK, eSize); eRemain -= eP8; gemmParam4QxK.inputScale += eP8; gemmParam4QxK.inputBias += eP8; gemmParam4QxK.srcKernelSum += eP8; srcInt8 += unitColBufferSize; dstInt8 += eP8 * mPack * mBytes; if (mBlockNum > 1) { memset(accumbuff, 0, eP8 * hP8 * QUANT_INFO_BYTES); gemmParam4QxK.accumBuffer = accumbuff; } } } // 2. softmax scores, softmax src/dst shape: [kv_seq_len/mPack, seq_len, mPack] { if (mKeyQuantMode != KVQuantMode::Int8 || isLowerTriangular == false || sinksPtr != nullptr) { if (mBytes == 2) { _maskQK((float*)qkPacked, &mScale, seqLen, subKvSeqLen, mPack, kvSeqLen, i * mBlockKV, padSeqLength, sinksPtr, mask, (mKeyQuantMode == KVQuantMode::Int8), isLowerTriangular); } else { _maskQK((float*)qkPacked, &mScale, seqLen, subKvSeqLen, mPack, kvSeqLen, i * mBlockKV, padSeqLength, sinksPtr, mask, (mKeyQuantMode == KVQuantMode::Int8), isLowerTriangular); } } gcore->MNNSoftmax(qkSoftmax, (float*)qkPacked, runningMax, runningSum, diffScale, seqLen, subKvSeqLen, i * mBlockKV, kvValidOffset, mPack, useMaskInSoftmax); } // 3. qk @ v auto qkStride0 = ROUND_UP(subKvSeqLen, lP) * eP * mBytes; auto rowStart = (!isLowerTriangular || i * mBlockKV < kvValidOffset) ? 0 : (i * mBlockKV - kvValidOffset); if (mValueQuantMode == KVQuantMode::TQ3) { // Vec_dot Value fusion: accumulate in rotated domain, WHT_inverse once // qkSoftmax format: [kvSeq/mPack, seqLen, mPack], element (s,q) at (s/mPack)*seqLen*mPack + q*mPack // + s%mPack int tq3BytesPerSeq = (mHeadDim / TQ3_BLOCK_SIZE) * TQ3_BYTES_PER_BLOCK; int numBlocks = mHeadDim / TQ3_BLOCK_SIZE; auto tq3Buf = mTQ3DequantBuf->host() + tId * mTQ3DequantBuf->stride(0); auto vAccRotated = (float*)(tq3Buf + seqLen * mHeadDim * sizeof(float)); auto weightsPtr = (float*)(tq3Buf + seqLen * mHeadDim * sizeof(float) + mHeadDim * sizeof(float)); for (int q = rowStart; q < seqLen; q++) { // Extract softmax weights for this query position (float) float* weights = weightsPtr; for (int s = 0; s < subKvSeqLen; s++) { int packIdx = (s / mPack) * seqLen * mPack + q * mPack + s % mPack; if (mBytes == 2) { weights[s] = (float)((FLOAT16_T*)qkSoftmax)[packIdx]; } else { weights[s] = ((float*)qkSoftmax)[packIdx]; } } // For each dim block: accumulate weighted codebook values in rotated domain for (int b = 0; b < numBlocks; b++) { memset(vAccRotated, 0, TQ3_BLOCK_SIZE * sizeof(float)); for (int s = 0; s < subKvSeqLen; s++) { int seqIdx = i * mBlockKV + s; const uint8_t* block = (uint8_t*)valueAddr + seqIdx * tq3BytesPerSeq + b * TQ3_BYTES_PER_BLOCK; uint16_t scaleFp16; memcpy(&scaleFp16, block, 2); float w = weights[s] * tq3_fp16_to_float(scaleFp16); tq3_weighted_acc_block(vAccRotated, w, block + 2); } // WHT_inverse to get final output values float reconstructed[TQ3_BLOCK_SIZE]; tq3_wht_inverse_32(reconstructed, vAccRotated); // Write to qkvPacked: [headDim/mPack, seqLen, mPack] for (int d = 0; d < TQ3_BLOCK_SIZE; d++) { int dimIdx = b * TQ3_BLOCK_SIZE + d; int outIdx = (dimIdx / mPack) * seqLen * mPack + q * mPack + dimIdx % mPack; if (mBytes == 2) { ((FLOAT16_T*)qkvPacked)[outIdx] = (FLOAT16_T)reconstructed[d]; } else { ((float*)qkvPacked)[outIdx] = reconstructed[d]; } } } } } else if (mValueQuantMode == KVQuantMode::TQ4) { // Vec_dot Value fusion for TQ4: same structure as TQ3 int tq4BytesPerSeq = (mHeadDim / TQ4_BLOCK_SIZE) * TQ4_BYTES_PER_BLOCK; int numBlocks = mHeadDim / TQ4_BLOCK_SIZE; auto tqBuf = mTQ3DequantBuf->host() + tId * mTQ3DequantBuf->stride(0); auto vAccRotated = (float*)(tqBuf + seqLen * mHeadDim * sizeof(float)); auto weightsPtr = (float*)(tqBuf + seqLen * mHeadDim * sizeof(float) + mHeadDim * sizeof(float)); for (int q = rowStart; q < seqLen; q++) { float* weights = weightsPtr; for (int s = 0; s < subKvSeqLen; s++) { int packIdx = (s / mPack) * seqLen * mPack + q * mPack + s % mPack; if (mBytes == 2) { weights[s] = (float)((FLOAT16_T*)qkSoftmax)[packIdx]; } else { weights[s] = ((float*)qkSoftmax)[packIdx]; } } for (int b = 0; b < numBlocks; b++) { memset(vAccRotated, 0, TQ4_BLOCK_SIZE * sizeof(float)); for (int s = 0; s < subKvSeqLen; s++) { int seqIdx = i * mBlockKV + s; const uint8_t* block = (uint8_t*)valueAddr + seqIdx * tq4BytesPerSeq + b * TQ4_BYTES_PER_BLOCK; uint16_t scaleFp16; memcpy(&scaleFp16, block, 2); float w = weights[s] * tq3_fp16_to_float(scaleFp16); tq4_weighted_acc_block(vAccRotated, w, block + 2); } float reconstructed[TQ4_BLOCK_SIZE]; tq3_wht_inverse_32(reconstructed, vAccRotated); for (int d = 0; d < TQ4_BLOCK_SIZE; d++) { int dimIdx = b * TQ4_BLOCK_SIZE + d; int outIdx = (dimIdx / mPack) * seqLen * mPack + q * mPack + dimIdx % mPack; if (mBytes == 2) { ((FLOAT16_T*)qkvPacked)[outIdx] = (FLOAT16_T)reconstructed[d]; } else { ((float*)qkvPacked)[outIdx] = reconstructed[d]; } } } } } else if (mValueQuantMode != KVQuantMode::Int8) { auto valuePtr = valueAddr + i * vstride0 * mBytes; size_t shapeParameters[7] = {(size_t)eP * lP * mBytes, ROUND_UP((size_t)subKvSeqLen, lP), (size_t)mHeadDim, (size_t)dstStep, 0, 0, 0}; size_t bExtraStride = (i < kvBlocks - 1) ? 0 : (ROUND_UP(mKVCacheManager->getFlashAttentionBlockKv(), lP) - ROUND_UP(subKvSeqLen, lP)) * hP * mBytes; shapeParameters[5] = bExtraStride; int loop_e = (seqLen - rowStart) / eP; int remain = (seqLen - rowStart) % eP; int ei = 0; elFloatV[0] = eP; elFloatV[1] = ROUND_UP(subKvSeqLen, lP); infoFloatV[2] = eP; for (; ei < loop_e; ei++) { srcPtr[0] = (float const*)((int8_t*)qkSoftmax + (ei * eP + rowStart) * mPack * mBytes); gcore->MNNPackC4ForMatMul_A((float*)qkReordered, srcPtr, infoFloatV, elFloatV); gcore->MNNPackedMatMul((float*)(qkvPacked + (ei * eP + rowStart) * mPack * mBytes), (float*)qkReordered, (float*)valuePtr, shapeParameters, nullptr, nullptr, nullptr, nullptr); } if (remain > 0) { elFloatV[0] = remain; infoFloatV[2] = remain; srcPtr[0] = (float const*)((int8_t*)qkSoftmax + (loop_e * eP + rowStart) * mPack * mBytes); shapeParameters[0] = remain * lP * mBytes; gcore->MNNPackC4ForMatMul_A((float*)qkReordered, srcPtr, infoFloatV, elFloatV); gcore->MNNPackedMatMulRemain((float*)(qkvPacked + (loop_e * eP + rowStart) * mPack * mBytes), (float*)qkReordered, (float*)valuePtr, remain, shapeParameters, nullptr, nullptr, nullptr, nullptr); } } else { // use int8 kernel to compute qk@ v auto valuePtr = valueAddr + i * vstride0; auto eRemain = seqLen - rowStart; auto qkPtr = (int8_t*)(qkSoftmax) + rowStart * mPack * mBytes; // [UP_DIV(subKvSeqLen,pack),seqLen,pack] auto qkvFloat = qkvPacked + rowStart * mPack * mBytes; gemmParam4QKxV.weightKernelSum = valueSum + i * ROUND_UP(mHeadDim, hP8); sumParams4QKxV.valid = subKvSeqLen % lP8; sumParams4QKxV.LU = UP_DIV(subKvSeqLen, lP8); auto dstInt8Ptr = (int8_t*)mQuantQK.ptr() + tId * eP8 * ROUND_UP(MNN_FLASH_ATTENTION_BLOCK_SIZE, mPack); srcPtr[0] = (const float*)(dstInt8Ptr); while (eRemain > 0) { auto eSize = ALIMIN(eRemain, eP8); memset(dstInt8Ptr, 0, eP8 * ROUND_UP(MNN_FLASH_ATTENTION_BLOCK_SIZE, mPack)); infoInt8V[1] = eSize; // eReal infoInt8V[4] = eSize; // e to process elInt8V[0] = eSize; // e to process for (int qi = 0; qi < UP_DIV(subKvSeqLen, mPack); ++qi) { mQuantFunc((float*)(qkPtr + qi * seqLen * mPack * mBytes), dstInt8Ptr + qi * eSize * mPack, eSize, vQuantScale, -128, 127, vQuantBias, 0); } core->MNNPackC4Int8ForMatMul_A(qkReordered, (int8_t const**)srcPtr, infoInt8V, elInt8V); // mSumQK gcore->MNNSumByAxisLForMatmul_A(gemmParam4QKxV.srcKernelSum, qkReordered, (float*)mQKScale.ptr(), eSize, sumParams4QKxV); mInt8GemmKernel(qkvFloat, qkReordered, valuePtr, UP_DIV(MNN_FLASH_ATTENTION_BLOCK_SIZE, lP8), dstStep, UP_DIV(mHeadDim, mPack), &gemmParam4QKxV, eSize); eRemain -= eSize; qkPtr += (eSize * mPack * mBytes); qkvFloat += (eSize * mPack * mBytes); } } // 4. flash attention, update each sub kvSeq's final results if (runningMax != nullptr && runningSum != nullptr && diffScale != nullptr) { gcore->MNNFlashAttentionUpdateBlockOutput((float*)outputPacked, (float*)qkvPacked, diffScale, runningSum, UP_DIV(mHeadDim, mPack), seqLen, mPack, i, kvBlocks, mPackQKV->stride(0) / mBytes, mBytes, rowStart); } } // Final results writing: [head_dim/mPack, seq_len, mPack] -> [seq_len, num_head, head_dim] if (!outputC4) { auto dstPtr = outputs[0]->host() + h * mHeadDim * mBytes; // offset = {seqLen, mNumHead * mHeadDim}; gcore->MNNUnpackCUnitTranspose((float*)dstPtr, (float*)outputPacked, seqLen, mHeadDim, offset); } } }; MNN_CONCURRENCY_BEGIN(tId, mThreadNum) { mCompute((int)tId); } MNN_CONCURRENCY_END(); backend()->onReleaseBuffer(unpackQK.get(), Backend::STATIC); backend()->onReleaseBuffer(softmMaxQ.get(), Backend::STATIC); backend()->onReleaseBuffer(newPackQK.get(), Backend::STATIC); backend()->onReleaseBuffer(mTempQKBlock.get(), Backend::STATIC); if (!mKVCache) { mKVCacheManager->onClear(); } if (!outputC4) { auto ptr = outputs[0]->host(); if (seqLen < outputs[0]->length(1)) { ::memset(outputs[0]->host() + seqLen * mHeadDim * mNumHead * mBytes, 0, (outputs[0]->length(1) - seqLen) * mHeadDim * mNumHead * mBytes); } } return NO_ERROR; } bool CPUAttention::onClone(Backend* bn, const Op* op, Execution** dst) { if (nullptr == dst) { return true; } auto tmp = new CPUAttention(bn, mKVCache); // Share KV cache when cloning within the same session (same meta pointer) if (bn->getMetaPtr() == mMeta) { tmp->mKVCacheManager = mKVCacheManager; // Mark as KV-shared if the target op requests KV reuse auto param = op->main_as_AttentionParam(); if (param && param->kv_shared_layer_index() >= 0) { tmp->mIsKVShared = true; } } *dst = tmp; return true; } CPUAttention::CPUAttention(Backend* backend, bool kv_cache) : Execution(backend), mKVCache(kv_cache) { mMeta = (KVMeta*)(backend->getMetaPtr()); mPackQ.reset(Tensor::createDevice({1, 1, 1, 1})); mPackQKV.reset(Tensor::createDevice({1, 1, 1, 1})); MNN::KVCacheManager::KVCacheConfig kvconfig; // attentionOption % 8: // 0: Do not quantize // 1: Q,K: Int8, V: Float32 // 2: Q,K,V: Int8 // 3: K: TQ3, V: Float32 // 4: K,V: TQ3 // 5: K: TQ4, V: Float32 // 6: K,V: TQ4 // attentionOption / 8: // 0: do not use flash attention // 1: use flash attention kvconfig.mKVCacheDir = static_cast(backend)->getRuntime()->hint().kvcacheDirPath; kvconfig.mPrefixCacheDir = static_cast(backend)->getRuntime()->hint().prefixcacheDirPath; kvconfig.mExpandChunk = 64; kvconfig.mBlockNum = 1; mKVCacheManager.reset(new CPUKVCacheManager(backend, kvconfig)); } class CPUAttentionCreator : public CPUBackend::Creator { public: virtual Execution* onCreate(const std::vector& inputs, const std::vector& outputs, const MNN::Op* op, Backend* backend) const override { auto param = op->main_as_AttentionParam(); return new CPUAttention(backend, param->kv_cache()); } }; REGISTER_CPU_OP_CREATOR_TRANSFORMER(CPUAttentionCreator, OpType_Attention); } // namespace MNN #endif // MNN_SUPPORT_TRANSFORMER_FUSE