975 lines
45 KiB
C++
975 lines
45 KiB
C++
#ifdef MNN_SUPPORT_TRANSFORMER_FUSE
|
|
|
|
#include "VulkanAttention.hpp"
|
|
#include "core/Macro.h"
|
|
#include "core/TensorUtils.hpp"
|
|
#include "backend/vulkan/vulkan/vulkan_wrapper.h"
|
|
#include <climits>
|
|
|
|
namespace MNN {
|
|
|
|
static inline float _invSqrt(float x) {
|
|
return 1.0f / ::sqrtf(x);
|
|
}
|
|
|
|
static uint32_t _selectSoftmaxLocalSize(int totalLen, uint32_t maxSizeX, uint32_t maxInvocations) {
|
|
if (totalLen <= 1) {
|
|
return 1;
|
|
}
|
|
uint32_t cap = 128;
|
|
cap = ALIMIN(cap, maxSizeX);
|
|
cap = ALIMIN(cap, maxInvocations);
|
|
cap = ALIMIN(cap, (uint32_t)totalLen);
|
|
uint32_t localSize = 1;
|
|
while ((localSize << 1) <= cap) {
|
|
localSize <<= 1;
|
|
}
|
|
return localSize;
|
|
}
|
|
|
|
static constexpr int kAttentionPrefillKBlock = 512;
|
|
|
|
static bool _supportDecodeQ1Subgroup(const VulkanDevice& device) {
|
|
const auto& subgroup = device.getSubgroupInfo();
|
|
if (0 == subgroup.size) {
|
|
return false;
|
|
}
|
|
if (0 == (subgroup.stages & VK_SHADER_STAGE_COMPUTE_BIT)) {
|
|
return false;
|
|
}
|
|
const VkSubgroupFeatureFlags required = VK_SUBGROUP_FEATURE_BASIC_BIT | VK_SUBGROUP_FEATURE_ARITHMETIC_BIT;
|
|
if ((subgroup.ops & required) != required) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void VulkanAttention::KVCache::reset() {
|
|
maxLen = 0;
|
|
kvHeadNum = 0;
|
|
headDim = 0;
|
|
fp16 = false;
|
|
key = nullptr;
|
|
value = nullptr;
|
|
}
|
|
|
|
void VulkanAttention::KVCache::ensureCapacity(VulkanBackend* vkBn, int requiredLen, int kvH, int d, bool useFP16) {
|
|
MNN_ASSERT(requiredLen >= 0);
|
|
MNN_ASSERT(kvH > 0);
|
|
MNN_ASSERT(d > 0);
|
|
if (kvHeadNum != kvH || headDim != d || fp16 != useFP16 || nullptr == key || nullptr == value) {
|
|
reset();
|
|
kvHeadNum = kvH;
|
|
headDim = d;
|
|
fp16 = useFP16;
|
|
maxLen = requiredLen + expandChunk;
|
|
maxLen = ALIMAX(maxLen, expandChunk);
|
|
const size_t bytes = fp16 ? sizeof(uint16_t) : sizeof(float);
|
|
const size_t bufSize = (size_t)maxLen * (size_t)kvHeadNum * (size_t)headDim * bytes;
|
|
key.reset(new VulkanBuffer(vkBn->getMemoryPool(), false, bufSize, nullptr,
|
|
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT |
|
|
VK_BUFFER_USAGE_TRANSFER_DST_BIT));
|
|
value.reset(new VulkanBuffer(vkBn->getMemoryPool(), false, bufSize, nullptr,
|
|
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT |
|
|
VK_BUFFER_USAGE_TRANSFER_DST_BIT));
|
|
return;
|
|
}
|
|
if (requiredLen <= maxLen) {
|
|
return;
|
|
}
|
|
const int oldMaxLen = maxLen;
|
|
maxLen = requiredLen + expandChunk;
|
|
const size_t bytes = fp16 ? sizeof(uint16_t) : sizeof(float);
|
|
const size_t newSize = (size_t)maxLen * (size_t)kvHeadNum * (size_t)headDim * bytes;
|
|
std::shared_ptr<VulkanBuffer> newKey(new VulkanBuffer(vkBn->getMemoryPool(), false, newSize, nullptr,
|
|
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT |
|
|
VK_BUFFER_USAGE_TRANSFER_SRC_BIT |
|
|
VK_BUFFER_USAGE_TRANSFER_DST_BIT));
|
|
std::shared_ptr<VulkanBuffer> newValue(new VulkanBuffer(vkBn->getMemoryPool(), false, newSize, nullptr,
|
|
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT |
|
|
VK_BUFFER_USAGE_TRANSFER_SRC_BIT |
|
|
VK_BUFFER_USAGE_TRANSFER_DST_BIT));
|
|
// Preserve old content.
|
|
//
|
|
// cacheKey is packed as [kvHeadNum, headDim/4, maxLen, 4], so changing maxLen changes the row stride and we must repack.
|
|
// cacheValue is kvh-major as [kvHeadNum, maxLen, headDim], so changing maxLen changes the kvh stride and we must repack too.
|
|
const size_t oldSize = key->size();
|
|
if (oldSize > 0) {
|
|
// Value: repack kvh blocks with new stride.
|
|
{
|
|
const VkDeviceSize rowBytes = (VkDeviceSize)oldMaxLen * (VkDeviceSize)headDim * (VkDeviceSize)bytes;
|
|
const VkDeviceSize srcStride = rowBytes;
|
|
const VkDeviceSize dstStride = (VkDeviceSize)maxLen * (VkDeviceSize)headDim * (VkDeviceSize)bytes;
|
|
std::vector<VkBufferCopy> regions;
|
|
regions.reserve((size_t)kvHeadNum);
|
|
for (int kvh = 0; kvh < kvHeadNum; ++kvh) {
|
|
VkBufferCopy c;
|
|
c.srcOffset = (VkDeviceSize)kvh * srcStride;
|
|
c.dstOffset = (VkDeviceSize)kvh * dstStride;
|
|
c.size = rowBytes;
|
|
regions.emplace_back(c);
|
|
}
|
|
vkBn->copyGPUToGPUBufferRegions(value->buffer(), newValue->buffer(), regions.data(), (uint32_t)regions.size());
|
|
}
|
|
|
|
// Key: repack rows with new stride.
|
|
const int d4Size = headDim / 4;
|
|
MNN_ASSERT(d4Size > 0);
|
|
const uint32_t rowCount = (uint32_t)kvHeadNum * (uint32_t)d4Size;
|
|
const VkDeviceSize vec4Bytes = (VkDeviceSize)(4 * bytes);
|
|
const VkDeviceSize srcRowStride = (VkDeviceSize)oldMaxLen * vec4Bytes;
|
|
const VkDeviceSize dstRowStride = (VkDeviceSize)maxLen * vec4Bytes;
|
|
std::vector<VkBufferCopy> regions;
|
|
regions.reserve(rowCount);
|
|
for (uint32_t r = 0; r < rowCount; ++r) {
|
|
VkBufferCopy c;
|
|
c.srcOffset = (VkDeviceSize)r * srcRowStride;
|
|
c.dstOffset = (VkDeviceSize)r * dstRowStride;
|
|
c.size = srcRowStride;
|
|
regions.emplace_back(c);
|
|
}
|
|
vkBn->copyGPUToGPUBufferRegions(key->buffer(), newKey->buffer(), regions.data(), (uint32_t)regions.size());
|
|
}
|
|
key = newKey;
|
|
value = newValue;
|
|
}
|
|
|
|
VulkanAttention::VulkanAttention(const Op* op, Backend* bn) : VulkanBasicExecution(bn), mOp(op) {
|
|
auto vkBn = static_cast<VulkanBackend*>(bn);
|
|
mUseFP16 = vkBn->useFP16();
|
|
mMeta = reinterpret_cast<KVMeta*>(vkBn->getMetaPtr());
|
|
if (nullptr != op && nullptr != op->main_as_AttentionParam()) {
|
|
mNeedKvCache = op->main_as_AttentionParam()->kv_cache();
|
|
}
|
|
mKVCache.reset(new KVCache);
|
|
mParam = vkBn->allocUniform(nullptr, sizeof(GpuParam));
|
|
if (!mNeedKvCache) {
|
|
std::vector<VkDescriptorType> typesAttn{
|
|
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, // output
|
|
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, // query
|
|
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, // keyIn
|
|
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, // valueIn
|
|
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, // cacheKey
|
|
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, // cacheValue
|
|
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, // mask
|
|
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER // param
|
|
};
|
|
std::string attnName = "glsl_attention_fused_";
|
|
if (mUseFP16) {
|
|
attnName += "FP16_";
|
|
}
|
|
attnName += "comp";
|
|
mAttentionLegacyPipeline = vkBn->getPipeline(attnName, typesAttn);
|
|
MNN_ASSERT(nullptr != mAttentionLegacyPipeline);
|
|
mAttentionLegacySet.reset(mAttentionLegacyPipeline->createSet());
|
|
return;
|
|
}
|
|
|
|
// kv_cache=true path: pre-create update/prefill/decode pipelines to avoid resize cold-start.
|
|
{
|
|
std::vector<VkDescriptorType> typesUpdate{
|
|
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, // keyIn
|
|
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, // valueIn
|
|
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, // cacheKey
|
|
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, // cacheValue
|
|
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER // param
|
|
};
|
|
std::string updateName = "glsl_attention_kvcache_update_";
|
|
if (mUseFP16) {
|
|
updateName += "FP16_";
|
|
}
|
|
updateName += "comp";
|
|
mUpdatePipeline = vkBn->getPipeline(updateName, typesUpdate);
|
|
MNN_ASSERT(nullptr != mUpdatePipeline);
|
|
mUpdateSet.reset(mUpdatePipeline->createSet());
|
|
}
|
|
|
|
{
|
|
std::vector<VkDescriptorType> typesRearrange{
|
|
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, // queryOut
|
|
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, // queryIn
|
|
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER // param
|
|
};
|
|
std::string rqName = "glsl_attention_prefill_rearrange_q_";
|
|
if (mUseFP16) {
|
|
rqName += "FP16_";
|
|
}
|
|
rqName += "comp";
|
|
mRearrangeQPipeline = vkBn->getPipeline(rqName, typesRearrange);
|
|
MNN_ASSERT(nullptr != mRearrangeQPipeline);
|
|
mRearrangeQSet.reset(mRearrangeQPipeline->createSet());
|
|
}
|
|
|
|
{
|
|
std::vector<VkDescriptorType> typesInit{
|
|
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, // m
|
|
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, // l
|
|
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, // alpha
|
|
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, // oAcc
|
|
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER // param
|
|
};
|
|
std::string initName = "glsl_attention_prefill_kblock_init_state_";
|
|
if (mUseFP16) {
|
|
initName += "FP16_";
|
|
}
|
|
initName += "comp";
|
|
mInitStatePipeline = vkBn->getPipeline(initName, typesInit);
|
|
MNN_ASSERT(nullptr != mInitStatePipeline);
|
|
mInitStateSet.reset(mInitStatePipeline->createSet());
|
|
}
|
|
|
|
{
|
|
std::vector<VkDescriptorType> typesQK{
|
|
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, // qk
|
|
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, // query
|
|
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, // cacheKey
|
|
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, // mask
|
|
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER // param
|
|
};
|
|
|
|
std::string qkName = "glsl_attention_prefill_kblock_qk_";
|
|
if (mUseFP16) {
|
|
qkName += "FP16_";
|
|
}
|
|
qkName += "comp";
|
|
mQKBlockPipeline = vkBn->getPipeline(qkName, typesQK);
|
|
MNN_ASSERT(nullptr != mQKBlockPipeline);
|
|
mQKBlockSet.reset(mQKBlockPipeline->createSet());
|
|
|
|
std::string qkFullName = "glsl_attention_prefill_kblock_qk_full_";
|
|
if (mUseFP16) {
|
|
qkFullName += "FP16_";
|
|
}
|
|
qkFullName += "comp";
|
|
mQKBlockFullPipeline = vkBn->getPipeline(qkFullName, typesQK);
|
|
MNN_ASSERT(nullptr != mQKBlockFullPipeline);
|
|
mQKBlockFullSet.reset(mQKBlockFullPipeline->createSet());
|
|
}
|
|
|
|
{
|
|
std::vector<VkDescriptorType> typesSoftmax{
|
|
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, // w
|
|
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, // qk
|
|
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, // m
|
|
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, // l
|
|
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, // alpha
|
|
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER // param
|
|
};
|
|
std::string softmaxName = "glsl_attention_prefill_kblock_softmax_online_";
|
|
if (mUseFP16) {
|
|
softmaxName += "FP16_";
|
|
}
|
|
softmaxName += "comp";
|
|
const auto& limits = vkBn->getDevice().proty().limits;
|
|
const int kBlock4 = UP_DIV(kAttentionPrefillKBlock, 4) * 4;
|
|
const int maxK4 = UP_DIV(kBlock4, 4);
|
|
uint32_t localSize = _selectSoftmaxLocalSize(maxK4, (uint32_t)limits.maxComputeWorkGroupSize[0],
|
|
(uint32_t)limits.maxComputeWorkGroupInvocations);
|
|
mSoftmaxOnlinePipeline = vkBn->getPipeline(softmaxName, typesSoftmax, {localSize});
|
|
MNN_ASSERT(nullptr != mSoftmaxOnlinePipeline);
|
|
mSoftmaxOnlineSet.reset(mSoftmaxOnlinePipeline->createSet());
|
|
mSoftmaxOnlineLocalSize = localSize;
|
|
}
|
|
|
|
{
|
|
std::vector<VkDescriptorType> typesQKV{
|
|
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, // oAcc
|
|
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, // w
|
|
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, // cacheValue
|
|
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, // alpha
|
|
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER // param
|
|
};
|
|
std::string qkvName = "glsl_attention_prefill_kblock_qkv_acc_";
|
|
if (mUseFP16) {
|
|
qkvName += "FP16_";
|
|
}
|
|
qkvName += "comp";
|
|
mQKVAccPipeline = vkBn->getPipeline(qkvName, typesQKV);
|
|
MNN_ASSERT(nullptr != mQKVAccPipeline);
|
|
mQKVAccSet.reset(mQKVAccPipeline->createSet());
|
|
|
|
std::string qkvFullName = "glsl_attention_prefill_kblock_qkv_acc_full_";
|
|
if (mUseFP16) {
|
|
qkvFullName += "FP16_";
|
|
}
|
|
qkvFullName += "comp";
|
|
mQKVAccFullPipeline = vkBn->getPipeline(qkvFullName, typesQKV);
|
|
MNN_ASSERT(nullptr != mQKVAccFullPipeline);
|
|
mQKVAccFullSet.reset(mQKVAccFullPipeline->createSet());
|
|
}
|
|
|
|
{
|
|
std::vector<VkDescriptorType> typesFinal{
|
|
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, // output
|
|
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, // oAcc
|
|
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, // l
|
|
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER // param
|
|
};
|
|
std::string finalName = "glsl_attention_prefill_kblock_finalize_";
|
|
if (mUseFP16) {
|
|
finalName += "FP16_";
|
|
}
|
|
finalName += "comp";
|
|
mFinalizePipeline = vkBn->getPipeline(finalName, typesFinal);
|
|
MNN_ASSERT(nullptr != mFinalizePipeline);
|
|
mFinalizeSet.reset(mFinalizePipeline->createSet());
|
|
}
|
|
|
|
{
|
|
std::vector<VkDescriptorType> typesAttn{
|
|
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, // output
|
|
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, // query
|
|
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, // keyIn
|
|
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, // valueIn
|
|
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, // cacheKey
|
|
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, // cacheValue
|
|
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, // mask
|
|
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER // param
|
|
};
|
|
std::string attnName = "glsl_attention_fused_packed_";
|
|
if (mUseFP16) {
|
|
attnName += "FP16_";
|
|
}
|
|
attnName += "comp";
|
|
mAttentionPipeline = vkBn->getPipeline(attnName, typesAttn);
|
|
MNN_ASSERT(nullptr != mAttentionPipeline);
|
|
mAttentionSet.reset(mAttentionPipeline->createSet());
|
|
|
|
if (_supportDecodeQ1Subgroup(vkBn->getDevice())) {
|
|
mDecodeQ1SubgroupLocalSize = vkBn->getDevice().getSubgroupSize();
|
|
if (mDecodeQ1SubgroupLocalSize > 0) {
|
|
std::string decodeQ1Name = "glsl_attention_decode_q1_subgroup_";
|
|
if (mUseFP16) {
|
|
decodeQ1Name += "FP16_";
|
|
}
|
|
decodeQ1Name += "comp";
|
|
mDecodeQ1SubgroupPipeline = vkBn->getPipeline(decodeQ1Name, typesAttn, {mDecodeQ1SubgroupLocalSize});
|
|
if (nullptr != mDecodeQ1SubgroupPipeline) {
|
|
mDecodeQ1SubgroupSet.reset(mDecodeQ1SubgroupPipeline->createSet());
|
|
}
|
|
|
|
std::string decodeQ1HD128Name = "glsl_attention_decode_q1_subgroup_hd128_";
|
|
if (mUseFP16) {
|
|
decodeQ1HD128Name += "FP16_";
|
|
}
|
|
decodeQ1HD128Name += "comp";
|
|
mDecodeQ1SubgroupHD128Pipeline = vkBn->getPipeline(decodeQ1HD128Name, typesAttn, {mDecodeQ1SubgroupLocalSize});
|
|
if (nullptr != mDecodeQ1SubgroupHD128Pipeline) {
|
|
mDecodeQ1SubgroupHD128Set.reset(mDecodeQ1SubgroupHD128Pipeline->createSet());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
VulkanAttention::~VulkanAttention() {
|
|
auto vkBn = static_cast<VulkanBackend*>(backend());
|
|
if (mTempQuery) {
|
|
vkBn->onReleaseBuffer(mTempQuery.get(), Backend::DYNAMIC);
|
|
mTempQuery.reset();
|
|
}
|
|
if (mTempQKBlock) {
|
|
vkBn->onReleaseBuffer(mTempQKBlock.get(), Backend::DYNAMIC);
|
|
mTempQKBlock.reset();
|
|
}
|
|
if (mTempWBlock) {
|
|
vkBn->onReleaseBuffer(mTempWBlock.get(), Backend::DYNAMIC);
|
|
mTempWBlock.reset();
|
|
}
|
|
if (mTempM) {
|
|
vkBn->onReleaseBuffer(mTempM.get(), Backend::DYNAMIC);
|
|
mTempM.reset();
|
|
}
|
|
if (mTempL) {
|
|
vkBn->onReleaseBuffer(mTempL.get(), Backend::DYNAMIC);
|
|
mTempL.reset();
|
|
}
|
|
if (mTempAlpha) {
|
|
vkBn->onReleaseBuffer(mTempAlpha.get(), Backend::DYNAMIC);
|
|
mTempAlpha.reset();
|
|
}
|
|
if (mTempOAcc) {
|
|
vkBn->onReleaseBuffer(mTempOAcc.get(), Backend::DYNAMIC);
|
|
mTempOAcc.reset();
|
|
}
|
|
vkBn->recycleUniform(mParam);
|
|
}
|
|
|
|
bool VulkanAttention::onClone(Backend* bn, const Op* op, VulkanBasicExecution** dst) {
|
|
if (nullptr == dst) {
|
|
return true;
|
|
}
|
|
auto res = new VulkanAttention(op, bn);
|
|
res->mKVCache = mKVCache;
|
|
res->mMeta = mMeta;
|
|
*dst = res;
|
|
return true;
|
|
}
|
|
|
|
ErrorCode VulkanAttention::onEncode(const std::vector<Tensor*>& inputs, const std::vector<Tensor*>& outputs,
|
|
const VulkanCommandPool::Buffer* cmdBuffer) {
|
|
MNN_ASSERT(!inputs.empty());
|
|
MNN_ASSERT(!outputs.empty());
|
|
auto query = inputs[0];
|
|
auto key = inputs[1];
|
|
auto value = inputs[2];
|
|
MNN_ASSERT(nullptr != query && nullptr != key && nullptr != value);
|
|
MNN_ASSERT(query->dimensions() == 4);
|
|
MNN_ASSERT(key->dimensions() == 4);
|
|
MNN_ASSERT(value->dimensions() == 4);
|
|
MNN_ASSERT(query->length(0) == 1);
|
|
MNN_ASSERT(key->length(0) == 1);
|
|
MNN_ASSERT(value->length(0) == 1);
|
|
mQueryLen = query->length(1);
|
|
mKeyLen = key->length(1);
|
|
mHeadNum = query->length(2);
|
|
mHeadDim = query->length(3);
|
|
mKvHeadNum = key->length(2);
|
|
MNN_ASSERT(mHeadNum > 0 && mKvHeadNum > 0);
|
|
MNN_ASSERT(mHeadNum % mKvHeadNum == 0);
|
|
MNN_ASSERT(mHeadDim > 0);
|
|
MNN_ASSERT((mHeadDim & 3) == 0);
|
|
MNN_ASSERT(mHeadDim <= 256);
|
|
MNN_ASSERT(value->length(1) == mKeyLen);
|
|
MNN_ASSERT(value->length(2) == mKvHeadNum);
|
|
MNN_ASSERT(value->length(3) == mHeadDim);
|
|
|
|
auto vkBn = static_cast<VulkanBackend*>(backend());
|
|
auto cmd = cmdBuffer->get();
|
|
|
|
#ifdef ENABLE_VULKAN_TIME_PROFILE
|
|
auto dispatchWithProfile = [&](const char* name, const VulkanPipeline* pipeline,
|
|
const std::shared_ptr<VulkanLayout::DescriptorSet>& set, uint32_t x, uint32_t y,
|
|
uint32_t z) {
|
|
auto* profiler = vkBn->timeProfiler();
|
|
if (nullptr != profiler) {
|
|
VulkanTimeProfileScope scope(profiler, cmd, name, VulkanTimeProfiler::Kind::Shader);
|
|
pipeline->bind(cmd, set->get());
|
|
vkCmdDispatch(cmd, x, y, z);
|
|
return;
|
|
}
|
|
pipeline->bind(cmd, set->get());
|
|
vkCmdDispatch(cmd, x, y, z);
|
|
};
|
|
#else
|
|
auto dispatchWithProfile = [&](const char*, const VulkanPipeline* pipeline,
|
|
const std::shared_ptr<VulkanLayout::DescriptorSet>& set, uint32_t x, uint32_t y,
|
|
uint32_t z) {
|
|
pipeline->bind(cmd, set->get());
|
|
vkCmdDispatch(cmd, x, y, z);
|
|
};
|
|
#endif
|
|
|
|
const bool usePrefill = mNeedKvCache && mQueryLen > 1;
|
|
mUsePrefill = usePrefill;
|
|
|
|
if (mNeedKvCache) {
|
|
MNN_ASSERT(nullptr != mUpdatePipeline);
|
|
MNN_ASSERT(nullptr != mUpdateSet);
|
|
|
|
// Dispatch: KV update (x=dim/4, y=keyLen, z=kvHeadNum).
|
|
dispatchWithProfile(mUseFP16 ? "glsl_attention_kvcache_update_FP16_comp" : "glsl_attention_kvcache_update_comp",
|
|
mUpdatePipeline, mUpdateSet, UP_DIV(mHeadDim / 4, 8), mKeyLen, mKvHeadNum);
|
|
// NOTE: KV cache buffers may be reallocated in onBeforeExecute (descriptor set updated there), so we must not
|
|
// record a VkBufferMemoryBarrier with a stale VkBuffer handle here. Use a global memory barrier instead.
|
|
{
|
|
VkMemoryBarrier barrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
|
barrier.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT;
|
|
barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT;
|
|
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT,
|
|
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &barrier,
|
|
0, nullptr, 0, nullptr);
|
|
}
|
|
}
|
|
|
|
if (usePrefill) {
|
|
constexpr int K_BLOCK = kAttentionPrefillKBlock;
|
|
int pastLenForPrefill = 0;
|
|
if (mNeedKvCache) {
|
|
MNN_ASSERT(nullptr != mMeta);
|
|
MNN_ASSERT(mMeta->n_reserve == 0);
|
|
MNN_ASSERT(mMeta->computeReverseSize() == 0);
|
|
const int previous = (int)mMeta->previous;
|
|
const int remove = (int)mMeta->remove;
|
|
MNN_ASSERT(previous >= 0);
|
|
MNN_ASSERT(remove >= 0);
|
|
MNN_ASSERT(remove <= previous);
|
|
pastLenForPrefill = previous - remove;
|
|
}
|
|
mPrefillTotalLen = pastLenForPrefill + mKeyLen;
|
|
mQueryLen4 = UP_DIV(mQueryLen, 4) * 4;
|
|
MNN_ASSERT(mPrefillTotalLen > 0);
|
|
|
|
const int64_t queryElementsI64 = (int64_t)mHeadNum * (int64_t)mHeadDim * (int64_t)mQueryLen4;
|
|
MNN_ASSERT(queryElementsI64 > 0 && queryElementsI64 <= (int64_t)INT_MAX);
|
|
const int queryElements = (int)queryElementsI64;
|
|
|
|
const int kBlock4 = UP_DIV(K_BLOCK, 4) * 4;
|
|
const int64_t rowCountI64 = (int64_t)mQueryLen * (int64_t)mHeadNum;
|
|
MNN_ASSERT(rowCountI64 > 0 && rowCountI64 <= (int64_t)INT_MAX);
|
|
const int rowCount = (int)rowCountI64;
|
|
|
|
const int64_t qkElementsI64 = (int64_t)rowCount * (int64_t)kBlock4;
|
|
MNN_ASSERT(qkElementsI64 > 0 && qkElementsI64 <= (int64_t)INT_MAX);
|
|
const int qkElements = (int)qkElementsI64;
|
|
|
|
const int64_t oaccElementsI64 = (int64_t)rowCount * (int64_t)mHeadDim;
|
|
MNN_ASSERT(oaccElementsI64 > 0 && oaccElementsI64 <= (int64_t)INT_MAX);
|
|
const int oaccElements = (int)oaccElementsI64;
|
|
|
|
// Acquire workspace tensors fresh each onEncode (Backend::DYNAMIC). They are released at the end of this
|
|
// call so other ops within the same resize can reuse the pool chunks. Descriptor sets capture
|
|
// (VkBuffer, offset) below; the underlying GPU memory stays alive past release because the parent
|
|
// VkBuffer is owned by the pool, and command-buffer order + barriers serialize cross-op access.
|
|
// M / L / Alpha / OAcc must be FP32 even when the backend runs FP16 -> int tensor forces 4-byte storage.
|
|
auto acquireTemp = [&](std::shared_ptr<Tensor>& t, Tensor* dev) -> bool {
|
|
t.reset(dev);
|
|
return vkBn->onAcquireBuffer(t.get(), Backend::DYNAMIC);
|
|
};
|
|
if (!acquireTemp(mTempQuery, Tensor::createDevice<float>({queryElements}))) return OUT_OF_MEMORY;
|
|
if (!acquireTemp(mTempQKBlock, Tensor::createDevice<float>({qkElements}))) return OUT_OF_MEMORY;
|
|
if (!acquireTemp(mTempWBlock, Tensor::createDevice<float>({qkElements}))) return OUT_OF_MEMORY;
|
|
if (!acquireTemp(mTempM, Tensor::createDevice<int>({rowCount}))) return OUT_OF_MEMORY;
|
|
if (!acquireTemp(mTempL, Tensor::createDevice<int>({rowCount}))) return OUT_OF_MEMORY;
|
|
if (!acquireTemp(mTempAlpha, Tensor::createDevice<int>({rowCount}))) return OUT_OF_MEMORY;
|
|
if (!acquireTemp(mTempOAcc, Tensor::createDevice<int>({oaccElements}))) return OUT_OF_MEMORY;
|
|
|
|
MNN_ASSERT(nullptr != mRearrangeQPipeline);
|
|
MNN_ASSERT(nullptr != mRearrangeQSet);
|
|
MNN_ASSERT(nullptr != mInitStatePipeline);
|
|
MNN_ASSERT(nullptr != mInitStateSet);
|
|
MNN_ASSERT(nullptr != mQKBlockPipeline);
|
|
MNN_ASSERT(nullptr != mQKBlockSet);
|
|
MNN_ASSERT(nullptr != mQKBlockFullPipeline);
|
|
MNN_ASSERT(nullptr != mQKBlockFullSet);
|
|
MNN_ASSERT(nullptr != mSoftmaxOnlinePipeline);
|
|
MNN_ASSERT(nullptr != mSoftmaxOnlineSet);
|
|
MNN_ASSERT(nullptr != mQKVAccPipeline);
|
|
MNN_ASSERT(nullptr != mQKVAccSet);
|
|
MNN_ASSERT(nullptr != mQKVAccFullPipeline);
|
|
MNN_ASSERT(nullptr != mQKVAccFullSet);
|
|
MNN_ASSERT(nullptr != mFinalizePipeline);
|
|
MNN_ASSERT(nullptr != mFinalizeSet);
|
|
|
|
// Bind workspace + uniform descriptor slots here (their (VkBuffer, offset) is stable across executes).
|
|
// Cache (cacheKey/cacheValue) and external I/O (query/mask/output) are bound in onBeforeExecute because
|
|
// KV cache may be reallocated by ensureCapacity().
|
|
auto tqBuf = vkBn->getTensorBuffer(mTempQuery.get());
|
|
auto qkBuf = vkBn->getTensorBuffer(mTempQKBlock.get());
|
|
auto wBuf = vkBn->getTensorBuffer(mTempWBlock.get());
|
|
auto mBuf = vkBn->getTensorBuffer(mTempM.get());
|
|
auto lBuf = vkBn->getTensorBuffer(mTempL.get());
|
|
auto aBuf = vkBn->getTensorBuffer(mTempAlpha.get());
|
|
auto oBuf = vkBn->getTensorBuffer(mTempOAcc.get());
|
|
|
|
mRearrangeQSet->writeBuffer(tqBuf.first->buffer(), 0, vkBn->getTensorSize(mTempQuery.get()), tqBuf.second);
|
|
mRearrangeQSet->writeBuffer(mParam->buffer(), 2, mParam->size());
|
|
|
|
mInitStateSet->writeBuffer(mBuf.first->buffer(), 0, vkBn->getTensorSize(mTempM.get()), mBuf.second);
|
|
mInitStateSet->writeBuffer(lBuf.first->buffer(), 1, vkBn->getTensorSize(mTempL.get()), lBuf.second);
|
|
mInitStateSet->writeBuffer(aBuf.first->buffer(), 2, vkBn->getTensorSize(mTempAlpha.get()), aBuf.second);
|
|
mInitStateSet->writeBuffer(oBuf.first->buffer(), 3, vkBn->getTensorSize(mTempOAcc.get()), oBuf.second);
|
|
mInitStateSet->writeBuffer(mParam->buffer(), 4, mParam->size());
|
|
|
|
mQKBlockSet->writeBuffer(qkBuf.first->buffer(), 0, vkBn->getTensorSize(mTempQKBlock.get()), qkBuf.second);
|
|
mQKBlockSet->writeBuffer(tqBuf.first->buffer(), 1, vkBn->getTensorSize(mTempQuery.get()), tqBuf.second);
|
|
mQKBlockSet->writeBuffer(mParam->buffer(), 4, mParam->size());
|
|
|
|
mQKBlockFullSet->writeBuffer(qkBuf.first->buffer(), 0, vkBn->getTensorSize(mTempQKBlock.get()), qkBuf.second);
|
|
mQKBlockFullSet->writeBuffer(tqBuf.first->buffer(), 1, vkBn->getTensorSize(mTempQuery.get()), tqBuf.second);
|
|
mQKBlockFullSet->writeBuffer(mParam->buffer(), 4, mParam->size());
|
|
|
|
mSoftmaxOnlineSet->writeBuffer(wBuf.first->buffer(), 0, vkBn->getTensorSize(mTempWBlock.get()), wBuf.second);
|
|
mSoftmaxOnlineSet->writeBuffer(qkBuf.first->buffer(), 1, vkBn->getTensorSize(mTempQKBlock.get()), qkBuf.second);
|
|
mSoftmaxOnlineSet->writeBuffer(mBuf.first->buffer(), 2, vkBn->getTensorSize(mTempM.get()), mBuf.second);
|
|
mSoftmaxOnlineSet->writeBuffer(lBuf.first->buffer(), 3, vkBn->getTensorSize(mTempL.get()), lBuf.second);
|
|
mSoftmaxOnlineSet->writeBuffer(aBuf.first->buffer(), 4, vkBn->getTensorSize(mTempAlpha.get()), aBuf.second);
|
|
mSoftmaxOnlineSet->writeBuffer(mParam->buffer(), 5, mParam->size());
|
|
|
|
mQKVAccSet->writeBuffer(oBuf.first->buffer(), 0, vkBn->getTensorSize(mTempOAcc.get()), oBuf.second);
|
|
mQKVAccSet->writeBuffer(wBuf.first->buffer(), 1, vkBn->getTensorSize(mTempWBlock.get()), wBuf.second);
|
|
mQKVAccSet->writeBuffer(aBuf.first->buffer(), 3, vkBn->getTensorSize(mTempAlpha.get()), aBuf.second);
|
|
mQKVAccSet->writeBuffer(mParam->buffer(), 4, mParam->size());
|
|
|
|
mQKVAccFullSet->writeBuffer(oBuf.first->buffer(), 0, vkBn->getTensorSize(mTempOAcc.get()), oBuf.second);
|
|
mQKVAccFullSet->writeBuffer(wBuf.first->buffer(), 1, vkBn->getTensorSize(mTempWBlock.get()), wBuf.second);
|
|
mQKVAccFullSet->writeBuffer(aBuf.first->buffer(), 3, vkBn->getTensorSize(mTempAlpha.get()), aBuf.second);
|
|
mQKVAccFullSet->writeBuffer(mParam->buffer(), 4, mParam->size());
|
|
|
|
mFinalizeSet->writeBuffer(oBuf.first->buffer(), 1, vkBn->getTensorSize(mTempOAcc.get()), oBuf.second);
|
|
mFinalizeSet->writeBuffer(lBuf.first->buffer(), 2, vkBn->getTensorSize(mTempL.get()), lBuf.second);
|
|
mFinalizeSet->writeBuffer(mParam->buffer(), 3, mParam->size());
|
|
|
|
// 1) Rearrange Q to packed-D Qtmp: (x=qLen4, y=headDim/4, z=headNum)
|
|
dispatchWithProfile(mUseFP16 ? "glsl_attention_prefill_rearrange_q_FP16_comp" : "glsl_attention_prefill_rearrange_q_comp",
|
|
mRearrangeQPipeline, mRearrangeQSet, UP_DIV(mQueryLen4, 8), UP_DIV(mHeadDim / 4, 8), mHeadNum);
|
|
cmdBuffer->barrierSource(tqBuf.first->buffer(), tqBuf.second, vkBn->getTensorSize(mTempQuery.get()));
|
|
|
|
// K-block prefill: online softmax in K dimension to avoid O(qLen*totalLen) intermediates.
|
|
dispatchWithProfile(mUseFP16 ? "glsl_attention_prefill_kblock_init_state_FP16_comp" : "glsl_attention_prefill_kblock_init_state_comp",
|
|
mInitStatePipeline, mInitStateSet, UP_DIV((uint32_t)mQueryLen * (uint32_t)mHeadNum * (uint32_t)mHeadDim, 256),
|
|
1, 1);
|
|
cmdBuffer->barrierSource(mBuf.first->buffer(), mBuf.second, vkBn->getTensorSize(mTempM.get()));
|
|
cmdBuffer->barrierSource(lBuf.first->buffer(), lBuf.second, vkBn->getTensorSize(mTempL.get()));
|
|
cmdBuffer->barrierSource(aBuf.first->buffer(), aBuf.second, vkBn->getTensorSize(mTempAlpha.get()));
|
|
cmdBuffer->barrierSource(oBuf.first->buffer(), oBuf.second, vkBn->getTensorSize(mTempOAcc.get()));
|
|
|
|
struct QKPushConst {
|
|
uint32_t kStart;
|
|
uint32_t blockLen;
|
|
};
|
|
struct SoftmaxPushConst {
|
|
uint32_t blockLen;
|
|
};
|
|
|
|
auto dispatchWithPushConst = [&](const char* name, const VulkanPipeline* pipeline,
|
|
const std::shared_ptr<VulkanLayout::DescriptorSet>& set, uint32_t x, uint32_t y,
|
|
uint32_t z, const void* pcData, uint32_t pcSize) {
|
|
#ifdef ENABLE_VULKAN_TIME_PROFILE
|
|
auto* profiler = vkBn->timeProfiler();
|
|
if (nullptr != profiler) {
|
|
VulkanTimeProfileScope scope(profiler, cmd, name, VulkanTimeProfiler::Kind::Shader);
|
|
pipeline->bind(cmd, set->get());
|
|
vkCmdPushConstants(cmd, pipeline->layout(), VK_SHADER_STAGE_COMPUTE_BIT, 0, pcSize, pcData);
|
|
vkCmdDispatch(cmd, x, y, z);
|
|
return;
|
|
}
|
|
#endif
|
|
pipeline->bind(cmd, set->get());
|
|
vkCmdPushConstants(cmd, pipeline->layout(), VK_SHADER_STAGE_COMPUTE_BIT, 0, pcSize, pcData);
|
|
vkCmdDispatch(cmd, x, y, z);
|
|
};
|
|
|
|
const int totalLen = mPrefillTotalLen;
|
|
const int kBlock = K_BLOCK;
|
|
for (int kStart = 0; kStart < totalLen; kStart += kBlock) {
|
|
const int blockLen = ALIMIN(kBlock, totalLen - kStart);
|
|
const int blockLen4 = UP_DIV(blockLen, 4) * 4;
|
|
const int blockLen4_4 = UP_DIV(blockLen4, 4);
|
|
|
|
// 2) QK block: (x=blockLen4/4, y=qLen4/4, z=headNum)
|
|
QKPushConst pcQK{(uint32_t)kStart, (uint32_t)blockLen};
|
|
const bool fullBlock = (blockLen == kBlock) && (kStart + kBlock <= totalLen);
|
|
const VulkanPipeline* qkPipe = fullBlock ? mQKBlockFullPipeline : mQKBlockPipeline;
|
|
const std::shared_ptr<VulkanLayout::DescriptorSet>& qkSet = fullBlock ? mQKBlockFullSet : mQKBlockSet;
|
|
const char* qkName = nullptr;
|
|
if (fullBlock) {
|
|
qkName = mUseFP16 ? "glsl_attention_prefill_kblock_qk_full_FP16_comp" : "glsl_attention_prefill_kblock_qk_full_comp";
|
|
} else {
|
|
qkName = mUseFP16 ? "glsl_attention_prefill_kblock_qk_FP16_comp" : "glsl_attention_prefill_kblock_qk_comp";
|
|
}
|
|
dispatchWithPushConst(qkName, qkPipe, qkSet, UP_DIV((uint32_t)blockLen4_4, 8),
|
|
UP_DIV((uint32_t)UP_DIV(mQueryLen4, 4), 8), (uint32_t)mHeadNum, &pcQK, sizeof(pcQK));
|
|
cmdBuffer->barrierSource(qkBuf.first->buffer(), qkBuf.second, vkBn->getTensorSize(mTempQKBlock.get()));
|
|
|
|
// 3) Softmax online: updates m/l and writes unnormalized w (x=headNum, y=qLen)
|
|
SoftmaxPushConst pcSM{(uint32_t)blockLen};
|
|
dispatchWithPushConst(mUseFP16 ? "glsl_attention_prefill_kblock_softmax_online_FP16_comp"
|
|
: "glsl_attention_prefill_kblock_softmax_online_comp",
|
|
mSoftmaxOnlinePipeline, mSoftmaxOnlineSet, (uint32_t)mHeadNum, (uint32_t)mQueryLen, 1, &pcSM,
|
|
sizeof(pcSM));
|
|
cmdBuffer->barrierSource(wBuf.first->buffer(), wBuf.second, vkBn->getTensorSize(mTempWBlock.get()));
|
|
cmdBuffer->barrierSource(mBuf.first->buffer(), mBuf.second, vkBn->getTensorSize(mTempM.get()));
|
|
cmdBuffer->barrierSource(lBuf.first->buffer(), lBuf.second, vkBn->getTensorSize(mTempL.get()));
|
|
cmdBuffer->barrierSource(aBuf.first->buffer(), aBuf.second, vkBn->getTensorSize(mTempAlpha.get()));
|
|
|
|
// 4) QKV accumulate: (x=headDim/4, y=qLen/2, z=headNum)
|
|
const VulkanPipeline* qkvPipe = fullBlock ? mQKVAccFullPipeline : mQKVAccPipeline;
|
|
const std::shared_ptr<VulkanLayout::DescriptorSet>& qkvSet = fullBlock ? mQKVAccFullSet : mQKVAccSet;
|
|
const char* qkvName = nullptr;
|
|
if (fullBlock) {
|
|
qkvName = mUseFP16 ? "glsl_attention_prefill_kblock_qkv_acc_full_FP16_comp"
|
|
: "glsl_attention_prefill_kblock_qkv_acc_full_comp";
|
|
} else {
|
|
qkvName =
|
|
mUseFP16 ? "glsl_attention_prefill_kblock_qkv_acc_FP16_comp" : "glsl_attention_prefill_kblock_qkv_acc_comp";
|
|
}
|
|
dispatchWithPushConst(qkvName, qkvPipe, qkvSet, UP_DIV((uint32_t)(mHeadDim / 4), 8),
|
|
UP_DIV((uint32_t)UP_DIV(mQueryLen, 2), 8), (uint32_t)mHeadNum, &pcQK, sizeof(pcQK));
|
|
cmdBuffer->barrierSource(oBuf.first->buffer(), oBuf.second, vkBn->getTensorSize(mTempOAcc.get()));
|
|
}
|
|
|
|
// 5) Finalize: output = oAcc / l
|
|
dispatchWithProfile(mUseFP16 ? "glsl_attention_prefill_kblock_finalize_FP16_comp" : "glsl_attention_prefill_kblock_finalize_comp",
|
|
mFinalizePipeline, mFinalizeSet, UP_DIV((uint32_t)(mHeadDim / 4), 8),
|
|
UP_DIV((uint32_t)UP_DIV(mQueryLen, 2), 8), (uint32_t)mHeadNum);
|
|
|
|
auto releaseTemp = [&](std::shared_ptr<Tensor>& t) {
|
|
if (t) {
|
|
vkBn->onReleaseBuffer(t.get(), Backend::DYNAMIC);
|
|
t.reset();
|
|
}
|
|
};
|
|
releaseTemp(mTempQuery);
|
|
releaseTemp(mTempQKBlock);
|
|
releaseTemp(mTempWBlock);
|
|
releaseTemp(mTempM);
|
|
releaseTemp(mTempL);
|
|
releaseTemp(mTempAlpha);
|
|
releaseTemp(mTempOAcc);
|
|
return NO_ERROR;
|
|
}
|
|
|
|
// Decode (or kv_cache disabled): keep fused shader.
|
|
mQueryLen4 = 0;
|
|
if (mTempQuery) {
|
|
vkBn->onReleaseBuffer(mTempQuery.get(), Backend::DYNAMIC);
|
|
mTempQuery.reset();
|
|
}
|
|
if (mTempQKBlock) {
|
|
vkBn->onReleaseBuffer(mTempQKBlock.get(), Backend::DYNAMIC);
|
|
mTempQKBlock.reset();
|
|
}
|
|
if (mTempWBlock) {
|
|
vkBn->onReleaseBuffer(mTempWBlock.get(), Backend::DYNAMIC);
|
|
mTempWBlock.reset();
|
|
}
|
|
if (mTempM) {
|
|
vkBn->onReleaseBuffer(mTempM.get(), Backend::DYNAMIC);
|
|
mTempM.reset();
|
|
}
|
|
if (mTempL) {
|
|
vkBn->onReleaseBuffer(mTempL.get(), Backend::DYNAMIC);
|
|
mTempL.reset();
|
|
}
|
|
if (mTempAlpha) {
|
|
vkBn->onReleaseBuffer(mTempAlpha.get(), Backend::DYNAMIC);
|
|
mTempAlpha.reset();
|
|
}
|
|
if (mTempOAcc) {
|
|
vkBn->onReleaseBuffer(mTempOAcc.get(), Backend::DYNAMIC);
|
|
mTempOAcc.reset();
|
|
}
|
|
mPrefillTotalLen = 0;
|
|
|
|
if (mNeedKvCache) {
|
|
const bool useDecodeQ1Subgroup =
|
|
(mQueryLen == 1) && (nullptr != mDecodeQ1SubgroupPipeline) && (nullptr != mDecodeQ1SubgroupSet);
|
|
if (useDecodeQ1Subgroup) {
|
|
const bool useHD128 = (mHeadDim == 128) && (nullptr != mDecodeQ1SubgroupHD128Pipeline) &&
|
|
(nullptr != mDecodeQ1SubgroupHD128Set);
|
|
if (useHD128) {
|
|
dispatchWithProfile(mUseFP16 ? "glsl_attention_decode_q1_subgroup_hd128_FP16_comp"
|
|
: "glsl_attention_decode_q1_subgroup_hd128_comp",
|
|
mDecodeQ1SubgroupHD128Pipeline, mDecodeQ1SubgroupHD128Set, (uint32_t)mHeadNum, 1, 1);
|
|
} else {
|
|
dispatchWithProfile(mUseFP16 ? "glsl_attention_decode_q1_subgroup_FP16_comp"
|
|
: "glsl_attention_decode_q1_subgroup_comp",
|
|
mDecodeQ1SubgroupPipeline, mDecodeQ1SubgroupSet, (uint32_t)mHeadNum, 1, 1);
|
|
}
|
|
} else {
|
|
MNN_ASSERT(nullptr != mAttentionPipeline);
|
|
MNN_ASSERT(nullptr != mAttentionSet);
|
|
dispatchWithProfile(mUseFP16 ? "glsl_attention_fused_packed_FP16_comp" : "glsl_attention_fused_packed_comp",
|
|
mAttentionPipeline, mAttentionSet, UP_DIV(mHeadNum, 8), UP_DIV(mQueryLen, 8), 1);
|
|
}
|
|
} else {
|
|
MNN_ASSERT(nullptr != mAttentionLegacyPipeline);
|
|
MNN_ASSERT(nullptr != mAttentionLegacySet);
|
|
dispatchWithProfile(mUseFP16 ? "glsl_attention_fused_FP16_comp" : "glsl_attention_fused_comp",
|
|
mAttentionLegacyPipeline, mAttentionLegacySet, UP_DIV(mHeadNum, 8), UP_DIV(mQueryLen, 8), 1);
|
|
}
|
|
|
|
return NO_ERROR;
|
|
}
|
|
|
|
ErrorCode VulkanAttention::onBeforeExecute(const std::vector<Tensor*>& inputs, const std::vector<Tensor*>& outputs) {
|
|
MNN_ASSERT(!inputs.empty());
|
|
MNN_ASSERT(!outputs.empty());
|
|
auto query = inputs[0];
|
|
auto key = inputs[1];
|
|
auto value = inputs[2];
|
|
auto output = outputs[0];
|
|
MNN_ASSERT(nullptr != query && nullptr != key && nullptr != value && nullptr != output);
|
|
MNN_ASSERT(query->length(1) == mQueryLen);
|
|
MNN_ASSERT(key->length(1) == mKeyLen);
|
|
MNN_ASSERT(query->length(2) == mHeadNum);
|
|
MNN_ASSERT(key->length(2) == mKvHeadNum);
|
|
MNN_ASSERT(query->length(3) == mHeadDim);
|
|
MNN_ASSERT(key->length(3) == mHeadDim);
|
|
MNN_ASSERT(value->length(1) == mKeyLen);
|
|
MNN_ASSERT(value->length(2) == mKvHeadNum);
|
|
MNN_ASSERT(value->length(3) == mHeadDim);
|
|
MNN_ASSERT(query->length(0) == 1);
|
|
|
|
auto vkBn = static_cast<VulkanBackend*>(backend());
|
|
|
|
int pastLenForCompute = 0;
|
|
if (mNeedKvCache) {
|
|
MNN_ASSERT(nullptr != mMeta);
|
|
MNN_ASSERT(mMeta->n_reserve == 0);
|
|
MNN_ASSERT(mMeta->computeReverseSize() == 0);
|
|
const int previous = (int)mMeta->previous;
|
|
const int remove = (int)mMeta->remove;
|
|
const int add = (int)mMeta->add;
|
|
MNN_ASSERT(previous >= 0);
|
|
MNN_ASSERT(remove >= 0);
|
|
MNN_ASSERT(add >= 0);
|
|
MNN_ASSERT(add <= mKeyLen);
|
|
MNN_ASSERT(remove <= previous);
|
|
pastLenForCompute = previous - remove;
|
|
// Ensure capacity for compute window (pastLen + keyLen), because shaders read only from KV cache.
|
|
mKVCache->ensureCapacity(vkBn, pastLenForCompute + mKeyLen, mKvHeadNum, mHeadDim, mUseFP16);
|
|
}
|
|
|
|
const int group = mHeadNum / mKvHeadNum;
|
|
const int totalLenForCompute = pastLenForCompute + mKeyLen;
|
|
|
|
int maskMode = 0;
|
|
int maskQlen = 0;
|
|
int maskKvlen = 0;
|
|
const Tensor* mask = nullptr;
|
|
if (inputs.size() > 3 && nullptr != inputs[3]) {
|
|
mask = inputs[3];
|
|
MNN_ASSERT(mask->getType() == halide_type_of<float>());
|
|
if (mask->shape().empty()) {
|
|
// Match the transformer-fuse attention convention used by the tests:
|
|
// a shape-empty scalar mask is the lower-triangular causal sentinel.
|
|
maskMode = 2;
|
|
} else {
|
|
const int md = mask->dimensions();
|
|
MNN_ASSERT(md >= 2);
|
|
maskQlen = mask->length(md - 2);
|
|
maskKvlen = mask->length(md - 1);
|
|
MNN_ASSERT(maskQlen == mQueryLen);
|
|
MNN_ASSERT(maskKvlen > 0);
|
|
maskMode = 1;
|
|
}
|
|
}
|
|
|
|
auto gpuParam = reinterpret_cast<GpuParam*>(mParam->map());
|
|
gpuParam->s0[0] = mQueryLen;
|
|
gpuParam->s0[1] = mKeyLen;
|
|
gpuParam->s0[2] = mHeadNum;
|
|
gpuParam->s0[3] = mKvHeadNum;
|
|
gpuParam->s1[0] = mHeadDim;
|
|
gpuParam->s1[1] = group;
|
|
gpuParam->s1[2] = pastLenForCompute;
|
|
gpuParam->s1[3] = totalLenForCompute;
|
|
gpuParam->s2[0] = maskQlen;
|
|
gpuParam->s2[1] = maskKvlen;
|
|
gpuParam->s2[2] = maskMode;
|
|
gpuParam->s2[3] = mNeedKvCache ? mKVCache->maxLen : 0;
|
|
gpuParam->f0[0] = _invSqrt((float)mHeadDim);
|
|
gpuParam->f0[1] = 0.0f;
|
|
gpuParam->f0[2] = 0.0f;
|
|
gpuParam->f0[3] = 0.0f;
|
|
mParam->unmap();
|
|
|
|
// Bind buffers (update + attention). Note: when hasMask == 0, bind query buffer as placeholder.
|
|
auto queryBuf = vkBn->getTensorBuffer(query);
|
|
auto keyBuf = vkBn->getTensorBuffer(key);
|
|
auto valueBuf = vkBn->getTensorBuffer(value);
|
|
auto outBuf = vkBn->getTensorBuffer(output);
|
|
const VkDeviceSize queryOffset = queryBuf.second;
|
|
|
|
const VulkanBuffer* cacheKeyBuf = nullptr;
|
|
const VulkanBuffer* cacheValueBuf = nullptr;
|
|
VkDeviceSize cacheKeyOffset = 0;
|
|
VkDeviceSize cacheValueOffset = 0;
|
|
size_t cacheKeySize = 0;
|
|
size_t cacheValueSize = 0;
|
|
|
|
if (mNeedKvCache) {
|
|
cacheKeyBuf = mKVCache->key.get();
|
|
cacheValueBuf = mKVCache->value.get();
|
|
MNN_ASSERT(nullptr != cacheKeyBuf && nullptr != cacheValueBuf);
|
|
cacheKeySize = cacheKeyBuf->size();
|
|
cacheValueSize = cacheValueBuf->size();
|
|
} else {
|
|
// KV cache disabled: alias cache buffers to current K/V (shaders read only from cache bindings).
|
|
cacheKeyBuf = keyBuf.first;
|
|
cacheValueBuf = valueBuf.first;
|
|
cacheKeyOffset = keyBuf.second;
|
|
cacheValueOffset = valueBuf.second;
|
|
cacheKeySize = vkBn->getTensorSize(key);
|
|
cacheValueSize = vkBn->getTensorSize(value);
|
|
}
|
|
|
|
// Update set (only when KV cache is enabled; kv_cache=false uses legacy fused shader directly on input K/V).
|
|
if (mNeedKvCache) {
|
|
mUpdateSet->writeBuffer(keyBuf.first->buffer(), 0, vkBn->getTensorSize(key), keyBuf.second);
|
|
mUpdateSet->writeBuffer(valueBuf.first->buffer(), 1, vkBn->getTensorSize(value), valueBuf.second);
|
|
mUpdateSet->writeBuffer(cacheKeyBuf->buffer(), 2, cacheKeySize, cacheKeyOffset);
|
|
mUpdateSet->writeBuffer(cacheValueBuf->buffer(), 3, cacheValueSize, cacheValueOffset);
|
|
mUpdateSet->writeBuffer(mParam->buffer(), 4, mParam->size());
|
|
}
|
|
|
|
if (mUsePrefill) {
|
|
MNN_ASSERT(totalLenForCompute == mPrefillTotalLen);
|
|
MNN_ASSERT(mQueryLen4 == UP_DIV(mQueryLen, 4) * 4);
|
|
|
|
// Only the bindings that may change between executes are rewritten below.
|
|
|
|
MNN_ASSERT(nullptr != mRearrangeQSet);
|
|
mRearrangeQSet->writeBuffer(queryBuf.first->buffer(), 1, vkBn->getTensorSize(query), queryBuf.second);
|
|
|
|
mQKBlockSet->writeBuffer(cacheKeyBuf->buffer(), 2, cacheKeySize, cacheKeyOffset);
|
|
mQKBlockFullSet->writeBuffer(cacheKeyBuf->buffer(), 2, cacheKeySize, cacheKeyOffset);
|
|
if (maskMode == 1) {
|
|
auto maskBuf = vkBn->getTensorBuffer(mask);
|
|
mQKBlockSet->writeBuffer(maskBuf.first->buffer(), 3, vkBn->getTensorSize(mask), maskBuf.second);
|
|
mQKBlockFullSet->writeBuffer(maskBuf.first->buffer(), 3, vkBn->getTensorSize(mask), maskBuf.second);
|
|
} else {
|
|
mQKBlockSet->writeBuffer(queryBuf.first->buffer(), 3, vkBn->getTensorSize(query), queryBuf.second);
|
|
mQKBlockFullSet->writeBuffer(queryBuf.first->buffer(), 3, vkBn->getTensorSize(query), queryBuf.second);
|
|
}
|
|
|
|
mQKVAccSet->writeBuffer(cacheValueBuf->buffer(), 2, cacheValueSize, cacheValueOffset);
|
|
mQKVAccFullSet->writeBuffer(cacheValueBuf->buffer(), 2, cacheValueSize, cacheValueOffset);
|
|
|
|
mFinalizeSet->writeBuffer(outBuf.first->buffer(), 0, vkBn->getTensorSize(output), outBuf.second);
|
|
return NO_ERROR;
|
|
}
|
|
|
|
// Attention set (fused). Keep packed fused set for fallback even when decode-q1 subgroup is available.
|
|
auto writeAttentionSet = [&](const std::shared_ptr<VulkanLayout::DescriptorSet>& set) {
|
|
MNN_ASSERT(nullptr != set);
|
|
set->writeBuffer(outBuf.first->buffer(), 0, vkBn->getTensorSize(output), outBuf.second);
|
|
set->writeBuffer(queryBuf.first->buffer(), 1, vkBn->getTensorSize(query), queryBuf.second);
|
|
set->writeBuffer(keyBuf.first->buffer(), 2, vkBn->getTensorSize(key), keyBuf.second);
|
|
set->writeBuffer(valueBuf.first->buffer(), 3, vkBn->getTensorSize(value), valueBuf.second);
|
|
set->writeBuffer(cacheKeyBuf->buffer(), 4, cacheKeySize, cacheKeyOffset);
|
|
set->writeBuffer(cacheValueBuf->buffer(), 5, cacheValueSize, cacheValueOffset);
|
|
if (maskMode == 1) {
|
|
auto maskBuf = vkBn->getTensorBuffer(mask);
|
|
set->writeBuffer(maskBuf.first->buffer(), 6, vkBn->getTensorSize(mask), maskBuf.second);
|
|
} else {
|
|
set->writeBuffer(queryBuf.first->buffer(), 6, vkBn->getTensorSize(query), queryBuf.second);
|
|
}
|
|
set->writeBuffer(mParam->buffer(), 7, mParam->size());
|
|
};
|
|
if (mNeedKvCache) {
|
|
MNN_ASSERT(nullptr != mAttentionSet);
|
|
writeAttentionSet(mAttentionSet);
|
|
if (mQueryLen == 1 && nullptr != mDecodeQ1SubgroupSet) {
|
|
writeAttentionSet(mDecodeQ1SubgroupSet);
|
|
}
|
|
if (mQueryLen == 1 && nullptr != mDecodeQ1SubgroupHD128Set) {
|
|
writeAttentionSet(mDecodeQ1SubgroupHD128Set);
|
|
}
|
|
} else {
|
|
MNN_ASSERT(nullptr != mAttentionLegacySet);
|
|
writeAttentionSet(mAttentionLegacySet);
|
|
}
|
|
|
|
return NO_ERROR;
|
|
}
|
|
|
|
class VulkanAttentionCreator : public VulkanBackend::Creator {
|
|
public:
|
|
VulkanBasicExecution* onCreate(const std::vector<Tensor*>& inputs, const std::vector<Tensor*>& outputs, const MNN::Op* op,
|
|
Backend* backend) const override {
|
|
return new VulkanAttention(op, backend);
|
|
}
|
|
};
|
|
|
|
static bool gResistor = []() {
|
|
VulkanBackend::addCreator(OpType_Attention, new VulkanAttentionCreator);
|
|
return true;
|
|
}();
|
|
|
|
} // namespace MNN
|
|
|
|
#endif // MNN_SUPPORT_TRANSFORMER_FUSE
|