1160 lines
55 KiB
C++
1160 lines
55 KiB
C++
//
|
|
// CPUAttention.cpp
|
|
// MNN
|
|
//
|
|
// Created by MNN on 2024/03/19.
|
|
// Copyright © 2018, Alibaba Group Holding Limited
|
|
//
|
|
|
|
#ifdef MNN_SUPPORT_TRANSFORMER_FUSE
|
|
|
|
#include <limits>
|
|
#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 <typename T>
|
|
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<float>::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<int>(kvSeqLen - seqLen);
|
|
auto maskPtr = mask->host<T>();
|
|
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<Tensor*>& inputs, const std::vector<Tensor*>& outputs) {
|
|
auto gcore = static_cast<CPUBackend*>(backend())->functions();
|
|
auto core = static_cast<CPUBackend*>(backend())->int8Functions();
|
|
gcore->MNNGetMatMulPackMode(&eP, &lP, &hP);
|
|
mThreadNum = ((CPUBackend*)backend())->threadNumber();
|
|
mPack = gcore->pack;
|
|
mBytes = gcore->bytes;
|
|
int attentionOption = static_cast<CPUBackend*>(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<CPUBackend*>(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<CPUBackend*>(backend())->getBufferAllocator();
|
|
mPackQKV.reset(Tensor::createDevice<int8_t>({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<int8_t>({mThreadNum, seqLen * 4}));
|
|
mRunningSum.reset(Tensor::createDevice<int8_t>({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<int8_t>({mThreadNum, seqLen * 4}));
|
|
mTempOut.reset(Tensor::createDevice<int8_t>({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<int8_t>({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<int8_t>({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<float>().max();
|
|
((float*)mGemmRelu.get())[1] = std::numeric_limits<float>().max();
|
|
if (mBytes == 2) {
|
|
gcore->MNNFp32ToLowp((float*)mGemmRelu.get(), reinterpret_cast<int16_t*>(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<int8_t>({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<Tensor*>& inputs, const std::vector<Tensor*>& outputs) {
|
|
auto gcore = static_cast<CPUBackend*>(backend())->functions();
|
|
auto core = static_cast<CPUBackend*>(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>(), (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<float>() : nullptr;
|
|
int kvValidOffset = kvSeqLen - seqLen; // reuse_kv=true or decode, kvValidOffset>0
|
|
|
|
// Temporary tensors for intermediate results
|
|
std::shared_ptr<Tensor> unpackQK(Tensor::createDevice<int32_t>({mThreadNum, seqLen, mBlockKV}));
|
|
std::shared_ptr<Tensor> softmMaxQ(Tensor::createDevice<int32_t>(
|
|
{mThreadNum, seqLen, ROUND_UP(mBlockKV, mPack)})); // [mBlockKV/mPack, seqLen, mPack ]
|
|
std::shared_ptr<Tensor> newPackQK;
|
|
if (mValueQuantMode != KVQuantMode::Int8) {
|
|
newPackQK.reset(Tensor::createDevice<int8_t>({mThreadNum, eP * ROUND_UP(mBlockKV, lP) * mBytes}));
|
|
} else {
|
|
newPackQK.reset(
|
|
Tensor::createDevice<int8_t>({mThreadNum, eP8 * ROUND_UP(MNN_FLASH_ATTENTION_BLOCK_SIZE, lP8)}));
|
|
}
|
|
std::shared_ptr<Tensor> mTempQKBlock(
|
|
Tensor::createDevice<int8_t>({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<int8_t>();
|
|
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<int8_t>();
|
|
|
|
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<void(int)> mCompute = [=](int tId) {
|
|
int8_t* qReordered = nullptr;
|
|
auto qkPacked = mTempQKBlock->host<int8_t>() + tId * mTempQKBlock->stride(0);
|
|
auto qkFlatten = unpackQK->host<float>() + tId * unpackQK->stride(0);
|
|
auto qkSoftmax = softmMaxQ->host<float>() + tId * softmMaxQ->stride(0);
|
|
auto qkReordered = newPackQK->host<int8_t>() + tId * newPackQK->stride(0);
|
|
auto qkvPacked = mPackQKV->host<int8_t>() + tId * mPackQKV->stride(0);
|
|
int headIndex = tId * numHeadDiv;
|
|
int headsToCompute = ALIMIN(numHeadDiv, mNumHead - headIndex);
|
|
|
|
// Flash Attention
|
|
auto runningMax = mRunningMax ? (float*)(mRunningMax->host<int8_t>() + tId * mRunningMax->stride(0)) : nullptr;
|
|
auto runningSum = mRunningSum ? (float*)(mRunningSum->host<int8_t>() + tId * mRunningSum->stride(0)) : nullptr;
|
|
auto diffScale =
|
|
mExpfDiffMax ? (float*)(mExpfDiffMax->host<int8_t>() + tId * mExpfDiffMax->stride(0)) : nullptr;
|
|
auto outputPacked = mTempOut ? mTempOut->host<int8_t>() + 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<FLOAT16_T>();
|
|
if (maskPtr[0] < 1e-6) {
|
|
isLowerTriangular = true;
|
|
}
|
|
} else {
|
|
auto maskPtr = mask->host<float>();
|
|
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<float*>(mGemmBias.get());
|
|
gemmParam4QxK.useInt8 = 0;
|
|
gemmParam4QxK.fp32minmax = reinterpret_cast<float*>(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<float*>(mGemmBias.get());
|
|
gemmParam4QKxV.useInt8 = 0;
|
|
gemmParam4QKxV.fp32minmax = reinterpret_cast<float*>(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<int32_t>(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<int8_t>() + 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<float>::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<int8_t>() + tId * mPackQ->stride(0);
|
|
gcore->MNNAttenPackAndScaleSingleHead((float*)qReordered,
|
|
(float*)(query->host<int8_t>() + h * mHeadDim * mBytes),
|
|
mHeadDim * mNumHead, &q_scale, units, seqLen, mHeadDim);
|
|
} else {
|
|
qReordered = mPackQ->host<int8_t>() + 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<int8_t>() + 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<int8_t>() + 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<int8_t>() + 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<int8_t>() + 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<int8_t>() + 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<int8_t>() + 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<FLOAT16_T>((float*)qkPacked, &mScale, seqLen, subKvSeqLen, mPack, kvSeqLen,
|
|
i * mBlockKV, padSeqLength, sinksPtr, mask,
|
|
(mKeyQuantMode == KVQuantMode::Int8), isLowerTriangular);
|
|
} else {
|
|
_maskQK<float>((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<int8_t>() + 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<int8_t>() + 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<int8_t>() + 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<float>();
|
|
if (seqLen < outputs[0]->length(1)) {
|
|
::memset(outputs[0]->host<uint8_t>() + 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<float>({1, 1, 1, 1}));
|
|
mPackQKV.reset(Tensor::createDevice<float>({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<CPUBackend*>(backend)->getRuntime()->hint().kvcacheDirPath;
|
|
kvconfig.mPrefixCacheDir = static_cast<CPUBackend*>(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<Tensor*>& inputs, const std::vector<Tensor*>& 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
|