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

833 lines
47 KiB
C++

//
// LinearAttentionBufExecution.cpp
// MNN
//
// Created by MNN on 2026/02/12.
// Copyright © 2018, Alibaba Group Holding Limited
//
#ifdef MNN_SUPPORT_TRANSFORMER_FUSE
#include "LinearAttentionBufExecution.hpp"
#include "core/TensorUtils.hpp"
#include "core/OpCommonUtils.hpp"
namespace MNN {
namespace OpenCL {
LinearAttentionBufExecution::LinearAttentionBufExecution(const MNN::Op *op, Backend *backend)
: CommonExecution(backend, op) {
mOpenCLBackend = static_cast<OpenCLBackend *>(backend);
mMeta = (KVMeta*)(backend->getMetaPtr());
auto param = op->main_as_LinearAttentionParam();
mAttentionType = param->attn_type()->str();
mNumKHeads = param->num_k_heads();
mNumVHeads = param->num_v_heads();
mHeadKDim = param->head_k_dim();
mHeadVDim = param->head_v_dim();
mUseQKL2Norm = param->use_qk_l2norm();
mStateCache.reset(new OpenCLStateCache);
}
ErrorCode LinearAttentionBufExecution::onResize(const std::vector<Tensor *> &inputs, const std::vector<Tensor *> &outputs) {
auto qkv = inputs[0];
int batch = qkv->length(0);
int convDim = qkv->length(1);
int seqLen = qkv->length(2);
// ─── Chunked prefill: fully independent branch ───
mUseChunkedPrefill = (seqLen > 1);
if (mUseChunkedPrefill) {
return onResizeChunkedPrefill(inputs, outputs);
}
int H = mNumVHeads;
int dk = mHeadKDim;
int dv = mHeadVDim;
int K_conv = inputs[3]->length(2);
int convStateSize = K_conv - 1;
int key_dim = mNumKHeads * dk;
int val_dim = mNumVHeads * dv;
int gqa_factor = (mNumVHeads > mNumKHeads) ? (mNumVHeads / mNumKHeads) : 1;
float qScale = 1.0f / sqrt((float)dk);
auto runtime = mOpenCLBackend->getOpenCLRuntime();
// ─── Persistent state buffers (STATIC): allocate once, shared via onClone ───
int bytesPerElement = mOpenCLBackend->fpBytes();
if (mStateCache->mRecurrentState.get() == nullptr) {
// First time: allocate and zero-initialize
int rnnSize = batch * H * dk * dv;
mStateCache->mRecurrentState.reset(Tensor::createDevice<float>({rnnSize}));
bool success = backend()->onAcquireBuffer(mStateCache->mRecurrentState.get(), Backend::STATIC);
if (!success) return OUT_OF_MEMORY;
{
cl_int res;
int bufferBytes = rnnSize * bytesPerElement;
void* mapPtr = runtime->commandQueue().enqueueMapBuffer(
openCLBuffer(mStateCache->mRecurrentState.get()), true, CL_MAP_WRITE, 0, bufferBytes, nullptr, nullptr, &res);
if (mapPtr != nullptr && res == CL_SUCCESS) {
::memset(mapPtr, 0, bufferBytes);
runtime->commandQueue().enqueueUnmapMemObject(openCLBuffer(mStateCache->mRecurrentState.get()), mapPtr);
}
}
if (convStateSize > 0) {
int convStateTotal = batch * convDim * convStateSize;
mStateCache->mConvState.reset(Tensor::createDevice<float>({convStateTotal}));
success &= backend()->onAcquireBuffer(mStateCache->mConvState.get(), Backend::STATIC);
if (!success) return OUT_OF_MEMORY;
cl_int res;
int bufferBytes = convStateTotal * bytesPerElement;
void* mapPtr = runtime->commandQueue().enqueueMapBuffer(
openCLBuffer(mStateCache->mConvState.get()), true, CL_MAP_WRITE, 0, bufferBytes, nullptr, nullptr, &res);
if (mapPtr != nullptr && res == CL_SUCCESS) {
::memset(mapPtr, 0, bufferBytes);
runtime->commandQueue().enqueueUnmapMemObject(openCLBuffer(mStateCache->mConvState.get()), mapPtr);
}
}
} else if (seqLen > 1) {
// Prefill: reset state for new sequence, UNLESS:
// 1. Loading from prefix cache (PendingRead), or
// 2. Reusing KV from previous inference (reuse_kv=true, i.e. previous != remove)
bool loadingFromDisk = (mMeta != nullptr && mMeta->file_flag == KVMeta::PendingRead && mMeta->file_name.size() > 0);
bool reusingKV = (mMeta != nullptr && mMeta->previous != mMeta->remove);
if (!loadingFromDisk && !reusingKV) {
{
cl_int res;
int bufferBytes = mStateCache->mRecurrentState->elementSize() * bytesPerElement;
void* mapPtr = runtime->commandQueue().enqueueMapBuffer(
openCLBuffer(mStateCache->mRecurrentState.get()), true, CL_MAP_WRITE, 0, bufferBytes, nullptr, nullptr, &res);
if (mapPtr != nullptr && res == CL_SUCCESS) {
::memset(mapPtr, 0, bufferBytes);
runtime->commandQueue().enqueueUnmapMemObject(openCLBuffer(mStateCache->mRecurrentState.get()), mapPtr);
}
}
if (mStateCache->mConvState.get() != nullptr) {
cl_int res;
int bufferBytes = mStateCache->mConvState->elementSize() * bytesPerElement;
void* mapPtr = runtime->commandQueue().enqueueMapBuffer(
openCLBuffer(mStateCache->mConvState.get()), true, CL_MAP_WRITE, 0, bufferBytes, nullptr, nullptr, &res);
if (mapPtr != nullptr && res == CL_SUCCESS) {
::memset(mapPtr, 0, bufferBytes);
runtime->commandQueue().enqueueUnmapMemObject(openCLBuffer(mStateCache->mConvState.get()), mapPtr);
}
}
}
}
// Decode (seqLen == 1): keep existing state untouched
// Allocate temporary conv output buffer
mConvOut.reset(Tensor::createDevice<float>({batch * convDim * seqLen}));
mOpenCLBackend->onAcquireBuffer(mConvOut.get(), Backend::DYNAMIC);
mOpenCLBackend->onReleaseBuffer(mConvOut.get(), Backend::DYNAMIC);
// Build kernels
std::set<std::string> buildOptions;
int local_size = 16;
buildOptions.emplace("-DLOCAL_SIZE=" + std::to_string(local_size));
buildOptions.emplace("-DK_SIZE=" + std::to_string(dv));
// Kernel 1: Conv1D + SiLU
mKernelConvSilu = runtime->buildKernel("linear_attention_buf", "linear_attn_conv_silu", buildOptions, mOpenCLBackend->getPrecision());
int totalConvSilu = batch * convDim * seqLen;
mGWSConvSilu = {(uint32_t)totalConvSilu, 1, 1};
auto maxWorkGroupSize = static_cast<uint32_t>(runtime->getMaxWorkGroupSize(mKernelConvSilu));
uint32_t lwsConv = std::min(maxWorkGroupSize, (uint32_t)256);
lwsConv = std::min(lwsConv, (uint32_t)totalConvSilu);
mLWSConvSilu = {lwsConv, 1, 1};
// Kernel 2: Conv state update
if (convStateSize > 0) {
mKernelConvStateUpdate = runtime->buildKernel("linear_attention_buf", "linear_attn_conv_state_update", buildOptions, mOpenCLBackend->getPrecision());
int totalConvUpdate = batch * convDim * convStateSize;
mGWSConvStateUpdate = {(uint32_t)totalConvUpdate, 1, 1};
maxWorkGroupSize = static_cast<uint32_t>(runtime->getMaxWorkGroupSize(mKernelConvStateUpdate));
uint32_t lwsUpdate = std::min(maxWorkGroupSize, (uint32_t)256);
lwsUpdate = std::min(lwsUpdate, (uint32_t)totalConvUpdate);
mLWSConvStateUpdate = {lwsUpdate, 1, 1};
}
// Kernel 3: Gated Delta Rule
auto gateDeltaRuleBuildOptions = buildOptions;
if(seqLen == 1){
gateDeltaRuleBuildOptions.emplace("-DDECODE_PHASE");
}
mKernelGatedDeltaRule = runtime->buildKernel("linear_attention_buf", "linear_attn_gated_delta_rule", gateDeltaRuleBuildOptions, mOpenCLBackend->getPrecision());
// Set kernel arguments
// Kernel 1: conv_silu
{
uint32_t idx = 0;
cl_int ret = CL_SUCCESS;
ret |= mKernelConvSilu->get().setArg(idx++, totalConvSilu);
ret |= mKernelConvSilu->get().setArg(idx++, openCLBuffer(inputs[0])); // qkv
ret |= mKernelConvSilu->get().setArg(idx++, openCLBuffer(mStateCache->mConvState.get())); // conv_state
ret |= mKernelConvSilu->get().setArg(idx++, openCLBuffer(inputs[3])); // conv_weight
ret |= mKernelConvSilu->get().setArg(idx++, openCLBuffer(mConvOut.get())); // conv_out
ret |= mKernelConvSilu->get().setArg(idx++, batch);
ret |= mKernelConvSilu->get().setArg(idx++, convDim);
ret |= mKernelConvSilu->get().setArg(idx++, seqLen);
ret |= mKernelConvSilu->get().setArg(idx++, K_conv);
ret |= mKernelConvSilu->get().setArg(idx++, convStateSize);
MNN_CHECK_CL_SUCCESS(ret, "setArg linear_attn_conv_silu");
}
// Kernel 2: conv_state_update
if (convStateSize > 0) {
int totalConvUpdate = batch * convDim * convStateSize;
uint32_t idx = 0;
cl_int ret = CL_SUCCESS;
ret |= mKernelConvStateUpdate->get().setArg(idx++, totalConvUpdate);
ret |= mKernelConvStateUpdate->get().setArg(idx++, openCLBuffer(inputs[0])); // qkv
ret |= mKernelConvStateUpdate->get().setArg(idx++, openCLBuffer(mStateCache->mConvState.get())); // conv_state
ret |= mKernelConvStateUpdate->get().setArg(idx++, batch);
ret |= mKernelConvStateUpdate->get().setArg(idx++, convDim);
ret |= mKernelConvStateUpdate->get().setArg(idx++, seqLen);
ret |= mKernelConvStateUpdate->get().setArg(idx++, convStateSize);
MNN_CHECK_CL_SUCCESS(ret, "setArg linear_attn_conv_state_update");
}
// Kernel 2.5: l2
if(mUseQKL2Norm){
auto l2BuildOptions = buildOptions;
if(seqLen > 1){
l2BuildOptions.emplace("-DUSE_VEC");
}
mKernell2Norm = runtime->buildKernel("linear_attention_buf", "l2_norm", l2BuildOptions, mOpenCLBackend->getPrecision());
mGWSl2Norm = {128, (uint32_t)(mNumKHeads * UP_DIV(seqLen, 4)), (uint32_t)(batch * 2)};
mLWSl2Norm = {128, 1, 1};
uint32_t idx = 0;
cl_int ret = CL_SUCCESS;
ret |= mKernell2Norm->get().setArg(idx++, openCLBuffer(mConvOut.get())); // conv_out
ret |= mKernell2Norm->get().setArg(idx++, openCLBuffer(mConvOut.get())); // conv_out
ret |= mKernell2Norm->get().setArg(idx++, convDim);
ret |= mKernell2Norm->get().setArg(idx++, dk);
ret |= mKernell2Norm->get().setArg(idx++, 1);
ret |= mKernell2Norm->get().setArg(idx++, key_dim);
ret |= mKernell2Norm->get().setArg(idx++, seqLen);
MNN_CHECK_CL_SUCCESS(ret, "setArg l2 norm");
}
// Kernel 3: gated_delta_rule
{
mGWSGatedDeltaRule = {(uint32_t)local_size, (uint32_t)UP_DIV(dv, 4) * H * batch};
uint32_t idx = 0;
cl_int ret = CL_SUCCESS;
ret |= mKernelGatedDeltaRule->get().setArg(idx++, openCLBuffer(mConvOut.get())); // conv_out
ret |= mKernelGatedDeltaRule->get().setArg(idx++, openCLBuffer(inputs[1])); // gate
ret |= mKernelGatedDeltaRule->get().setArg(idx++, openCLBuffer(inputs[2])); // beta
ret |= mKernelGatedDeltaRule->get().setArg(idx++, openCLBuffer(mStateCache->mRecurrentState.get())); // recurrent_state id = 6
ret |= mKernelGatedDeltaRule->get().setArg(idx++, openCLBuffer(outputs[0])); // attn_out
ret |= mKernelGatedDeltaRule->get().setArg(idx++, batch);
ret |= mKernelGatedDeltaRule->get().setArg(idx++, convDim);
ret |= mKernelGatedDeltaRule->get().setArg(idx++, seqLen);
ret |= mKernelGatedDeltaRule->get().setArg(idx++, mNumKHeads);
ret |= mKernelGatedDeltaRule->get().setArg(idx++, mNumVHeads);
ret |= mKernelGatedDeltaRule->get().setArg(idx++, dk);
ret |= mKernelGatedDeltaRule->get().setArg(idx++, dv);
ret |= mKernelGatedDeltaRule->get().setArg(idx++, key_dim);
ret |= mKernelGatedDeltaRule->get().setArg(idx++, val_dim);
ret |= mKernelGatedDeltaRule->get().setArg(idx++, gqa_factor);
ret |= mKernelGatedDeltaRule->get().setArg(idx++, qScale);
MNN_CHECK_CL_SUCCESS(ret, "setArg linear_attn_gated_delta_rule");
maxWorkGroupSize = static_cast<uint32_t>(runtime->getMaxWorkGroupSize(mKernelGatedDeltaRule));
mLWSGatedDeltaRule = {(uint32_t)local_size, 1};
}
// Round up global work sizes to multiples of local work sizes
mGWSConvSilu[0] = ROUND_UP(mGWSConvSilu[0], mLWSConvSilu[0]);
if (convStateSize > 0) {
mGWSConvStateUpdate[0] = ROUND_UP(mGWSConvStateUpdate[0], mLWSConvStateUpdate[0]);
}
mGWSGatedDeltaRule[0] = ROUND_UP(mGWSGatedDeltaRule[0], mLWSGatedDeltaRule[0]);
// Record kernels for queue recording optimization
mOpenCLBackend->startRecord(mRecording);
mOpenCLBackend->recordKernel3d(mKernelConvSilu, mGWSConvSilu, mLWSConvSilu);
if (convStateSize > 0) {
mOpenCLBackend->recordKernel3d(mKernelConvStateUpdate, mGWSConvStateUpdate, mLWSConvStateUpdate);
}
if(mUseQKL2Norm){
mOpenCLBackend->recordKernel3d(mKernell2Norm, mGWSl2Norm, mLWSl2Norm);
}
mOpenCLBackend->recordKernel2d(mKernelGatedDeltaRule, mGWSGatedDeltaRule, mLWSGatedDeltaRule);
mOpenCLBackend->endRecord(mRecording);
return NO_ERROR;
}
ErrorCode LinearAttentionBufExecution::onResizeChunkedPrefill(
const std::vector<Tensor *> &inputs, const std::vector<Tensor *> &outputs) {
auto qkv = inputs[0];
int batch = qkv->length(0);
int convDim = qkv->length(1);
int seqLen = qkv->length(2);
int H = mNumVHeads;
int dk = mHeadKDim;
int dv = mHeadVDim;
int K_conv = inputs[3]->length(2);
int convStateSize = K_conv - 1;
int key_dim = mNumKHeads * dk;
int gqa_factor = (mNumVHeads > mNumKHeads) ? (mNumVHeads / mNumKHeads) : 1;
float qScale = 1.0f / sqrt((float)dk);
auto runtime = mOpenCLBackend->getOpenCLRuntime();
int bytesPerElement = mOpenCLBackend->fpBytes();
// ─── Persistent state buffers (STATIC): allocate once, shared via onClone ───
if (mStateCache->mRecurrentState.get() == nullptr) {
int rnnSize = batch * H * dk * dv;
mStateCache->mRecurrentState.reset(Tensor::createDevice<float>({rnnSize}));
mStateCache->mRecurrentStateTune.reset(Tensor::createDevice<float>({rnnSize}));
bool success = backend()->onAcquireBuffer(mStateCache->mRecurrentState.get(), Backend::STATIC);
if (!success) return OUT_OF_MEMORY;
{
cl_int res;
int bufferBytes = rnnSize * bytesPerElement;
void* mapPtr = runtime->commandQueue().enqueueMapBuffer(
openCLBuffer(mStateCache->mRecurrentState.get()), true, CL_MAP_WRITE, 0, bufferBytes, nullptr, nullptr, &res);
if (mapPtr != nullptr && res == CL_SUCCESS) {
::memset(mapPtr, 0, bufferBytes);
runtime->commandQueue().enqueueUnmapMemObject(openCLBuffer(mStateCache->mRecurrentState.get()), mapPtr);
}
}
if (convStateSize > 0) {
int convStateTotal = batch * convDim * convStateSize;
mStateCache->mConvState.reset(Tensor::createDevice<float>({convStateTotal}));
success &= backend()->onAcquireBuffer(mStateCache->mConvState.get(), Backend::STATIC);
if (!success) return OUT_OF_MEMORY;
cl_int res;
int bufferBytes = convStateTotal * bytesPerElement;
void* mapPtr = runtime->commandQueue().enqueueMapBuffer(
openCLBuffer(mStateCache->mConvState.get()), true, CL_MAP_WRITE, 0, bufferBytes, nullptr, nullptr, &res);
if (mapPtr != nullptr && res == CL_SUCCESS) {
::memset(mapPtr, 0, bufferBytes);
runtime->commandQueue().enqueueUnmapMemObject(openCLBuffer(mStateCache->mConvState.get()), mapPtr);
}
}
} else {
// Prefill (seqLen > 1): reset state for new sequence
{
cl_int res;
int bufferBytes = mStateCache->mRecurrentState->elementSize() * bytesPerElement;
void* mapPtr = runtime->commandQueue().enqueueMapBuffer(
openCLBuffer(mStateCache->mRecurrentState.get()), true, CL_MAP_WRITE, 0, bufferBytes, nullptr, nullptr, &res);
if (mapPtr != nullptr && res == CL_SUCCESS) {
::memset(mapPtr, 0, bufferBytes);
runtime->commandQueue().enqueueUnmapMemObject(openCLBuffer(mStateCache->mRecurrentState.get()), mapPtr);
}
}
if (mStateCache->mConvState.get() != nullptr) {
cl_int res;
int bufferBytes = mStateCache->mConvState->elementSize() * bytesPerElement;
void* mapPtr = runtime->commandQueue().enqueueMapBuffer(
openCLBuffer(mStateCache->mConvState.get()), true, CL_MAP_WRITE, 0, bufferBytes, nullptr, nullptr, &res);
if (mapPtr != nullptr && res == CL_SUCCESS) {
::memset(mapPtr, 0, bufferBytes);
runtime->commandQueue().enqueueUnmapMemObject(openCLBuffer(mStateCache->mConvState.get()), mapPtr);
}
}
}
// ─── Allocate temporary buffers ───
// IMPORTANT: All DYNAMIC buffers that are used together during execution must have
// overlapping lifetimes (acquire all before releasing any) to prevent the memory
// planner from aliasing them. mConvOutPrefill is read by C2-C5 and C7 concurrently
// with chunk buffers, so they must all be alive simultaneously.
mConvOutPrefill.reset(Tensor::createDevice<float>({batch * convDim * seqLen}));
mOpenCLBackend->onAcquireBuffer(mStateCache->mRecurrentStateTune.get(), Backend::DYNAMIC);
mOpenCLBackend->onReleaseBuffer(mStateCache->mRecurrentStateTune.get(), Backend::DYNAMIC);
int chunkSize = mChunkSize;
int numChunks = UP_DIV(seqLen, chunkSize);
mNumChunks = numChunks;
// Allocate intermediate buffers
// Critical buffers use float32 for precision (allocate 2x elements in half mode to get 4N bytes = N floats)
// Non-critical buffers use FLOAT (matches onAcquireBuffer precision)
int fpBytes = mOpenCLBackend->fpBytes();
auto f32Elems = [fpBytes](int n) { return (n * 4 + fpBytes - 1) / fpBytes; };
mGCumsumBuf.reset(Tensor::createDevice<float>({f32Elems(batch * H * numChunks * chunkSize)}));
mAttnMatrixBuf.reset(Tensor::createDevice<float>({f32Elems(batch * H * numChunks * chunkSize * chunkSize)}));
mVCorrectedBuf.reset(Tensor::createDevice<float>({f32Elems(batch * H * numChunks * chunkSize * dv)}));
mKCumdecayBuf.reset(Tensor::createDevice<float>({f32Elems(batch * H * numChunks * chunkSize * dk)}));
mVNewBuf.reset(Tensor::createDevice<float>({f32Elems(batch * H * chunkSize * dv)}));
// Acquire all buffers used concurrently during execution BEFORE releasing any
mOpenCLBackend->onAcquireBuffer(mConvOutPrefill.get(), Backend::DYNAMIC);
mOpenCLBackend->onAcquireBuffer(mGCumsumBuf.get(), Backend::DYNAMIC);
mOpenCLBackend->onAcquireBuffer(mAttnMatrixBuf.get(), Backend::DYNAMIC);
mOpenCLBackend->onAcquireBuffer(mVCorrectedBuf.get(), Backend::DYNAMIC);
mOpenCLBackend->onAcquireBuffer(mKCumdecayBuf.get(), Backend::DYNAMIC);
mOpenCLBackend->onAcquireBuffer(mVNewBuf.get(), Backend::DYNAMIC);
// Release all together — planner now sees overlapping lifetimes, no aliasing
mOpenCLBackend->onReleaseBuffer(mConvOutPrefill.get(), Backend::DYNAMIC);
mOpenCLBackend->onReleaseBuffer(mGCumsumBuf.get(), Backend::DYNAMIC);
mOpenCLBackend->onReleaseBuffer(mAttnMatrixBuf.get(), Backend::DYNAMIC);
mOpenCLBackend->onReleaseBuffer(mVCorrectedBuf.get(), Backend::DYNAMIC);
mOpenCLBackend->onReleaseBuffer(mKCumdecayBuf.get(), Backend::DYNAMIC);
mOpenCLBackend->onReleaseBuffer(mVNewBuf.get(), Backend::DYNAMIC);
// ─── Build common kernels for prefill ───
std::set<std::string> buildOptions;
int local_size = 16;
buildOptions.emplace("-DLOCAL_SIZE=" + std::to_string(local_size));
buildOptions.emplace("-DK_SIZE=" + std::to_string(dv));
// Conv1D + SiLU
mKernelConvSiluPrefill = runtime->buildKernel("linear_attention_buf", "linear_attn_conv_silu", buildOptions, mOpenCLBackend->getPrecision());
int totalConvSilu = batch * convDim * seqLen;
mGWSConvSiluPrefill = {(uint32_t)totalConvSilu, 1, 1};
{
auto maxWorkGroupSize = static_cast<uint32_t>(runtime->getMaxWorkGroupSize(mKernelConvSiluPrefill));
uint32_t lwsConv = std::min(maxWorkGroupSize, (uint32_t)256);
lwsConv = std::min(lwsConv, (uint32_t)totalConvSilu);
mLWSConvSiluPrefill = {lwsConv, 1, 1};
}
{
uint32_t idx = 0;
cl_int ret = CL_SUCCESS;
ret |= mKernelConvSiluPrefill->get().setArg(idx++, totalConvSilu);
ret |= mKernelConvSiluPrefill->get().setArg(idx++, openCLBuffer(inputs[0]));
ret |= mKernelConvSiluPrefill->get().setArg(idx++, openCLBuffer(mStateCache->mConvState.get()));
ret |= mKernelConvSiluPrefill->get().setArg(idx++, openCLBuffer(inputs[3]));
ret |= mKernelConvSiluPrefill->get().setArg(idx++, openCLBuffer(mConvOutPrefill.get()));
ret |= mKernelConvSiluPrefill->get().setArg(idx++, batch);
ret |= mKernelConvSiluPrefill->get().setArg(idx++, convDim);
ret |= mKernelConvSiluPrefill->get().setArg(idx++, seqLen);
ret |= mKernelConvSiluPrefill->get().setArg(idx++, K_conv);
ret |= mKernelConvSiluPrefill->get().setArg(idx++, convStateSize);
MNN_CHECK_CL_SUCCESS(ret, "setArg linear_attn_conv_silu (prefill)");
}
mGWSConvSiluPrefill[0] = ROUND_UP(mGWSConvSiluPrefill[0], mLWSConvSiluPrefill[0]);
// Conv state update
if (convStateSize > 0) {
mKernelConvStateUpdatePrefill = runtime->buildKernel("linear_attention_buf", "linear_attn_conv_state_update", buildOptions, mOpenCLBackend->getPrecision());
int totalConvUpdate = batch * convDim * convStateSize;
mGWSConvStateUpdatePrefill = {(uint32_t)totalConvUpdate, 1, 1};
{
auto maxWorkGroupSize = static_cast<uint32_t>(runtime->getMaxWorkGroupSize(mKernelConvStateUpdatePrefill));
uint32_t lwsUpdate = std::min(maxWorkGroupSize, (uint32_t)256);
lwsUpdate = std::min(lwsUpdate, (uint32_t)totalConvUpdate);
mLWSConvStateUpdatePrefill = {lwsUpdate, 1, 1};
}
{
uint32_t idx = 0;
cl_int ret = CL_SUCCESS;
ret |= mKernelConvStateUpdatePrefill->get().setArg(idx++, totalConvUpdate);
ret |= mKernelConvStateUpdatePrefill->get().setArg(idx++, openCLBuffer(inputs[0]));
ret |= mKernelConvStateUpdatePrefill->get().setArg(idx++, openCLBuffer(mStateCache->mConvState.get()));
ret |= mKernelConvStateUpdatePrefill->get().setArg(idx++, batch);
ret |= mKernelConvStateUpdatePrefill->get().setArg(idx++, convDim);
ret |= mKernelConvStateUpdatePrefill->get().setArg(idx++, seqLen);
ret |= mKernelConvStateUpdatePrefill->get().setArg(idx++, convStateSize);
MNN_CHECK_CL_SUCCESS(ret, "setArg linear_attn_conv_state_update (prefill)");
}
mGWSConvStateUpdatePrefill[0] = ROUND_UP(mGWSConvStateUpdatePrefill[0], mLWSConvStateUpdatePrefill[0]);
}
// L2 norm
if (mUseQKL2Norm) {
auto l2BuildOptions = buildOptions;
l2BuildOptions.emplace("-DUSE_VEC");
mKernell2NormPrefill = runtime->buildKernel("linear_attention_buf", "l2_norm", l2BuildOptions, mOpenCLBackend->getPrecision());
mGWSl2NormPrefill = {128, (uint32_t)(mNumKHeads * UP_DIV(seqLen, 4)), (uint32_t)(batch * 2)};
mLWSl2NormPrefill = {128, 1, 1};
uint32_t idx = 0;
cl_int ret = CL_SUCCESS;
ret |= mKernell2NormPrefill->get().setArg(idx++, openCLBuffer(mConvOutPrefill.get()));
ret |= mKernell2NormPrefill->get().setArg(idx++, openCLBuffer(mConvOutPrefill.get()));
ret |= mKernell2NormPrefill->get().setArg(idx++, convDim);
ret |= mKernell2NormPrefill->get().setArg(idx++, dk);
ret |= mKernell2NormPrefill->get().setArg(idx++, 1);
ret |= mKernell2NormPrefill->get().setArg(idx++, key_dim);
ret |= mKernell2NormPrefill->get().setArg(idx++, seqLen);
MNN_CHECK_CL_SUCCESS(ret, "setArg l2 norm (prefill)");
}
// ─── Build chunked prefill kernels ───
std::set<std::string> chunkOpts = buildOptions;
chunkOpts.emplace("-DCHUNK_PREFILL");
chunkOpts.emplace("-DCHUNK_SIZE=" + std::to_string(chunkSize));
// C1: chunk_g_cumsum
mKernelChunkGCumsum = runtime->buildKernel("linear_attention_buf", "chunk_g_cumsum", chunkOpts, mOpenCLBackend->getPrecision());
mGWSChunkGCumsum = {(uint32_t)H, (uint32_t)numChunks, (uint32_t)batch};
mLWSChunkGCumsum = {1, 1, 1};
{
uint32_t idx = 0;
cl_int ret = CL_SUCCESS;
ret |= mKernelChunkGCumsum->get().setArg(idx++, openCLBuffer(inputs[1])); // gate
ret |= mKernelChunkGCumsum->get().setArg(idx++, openCLBuffer(mGCumsumBuf.get())); // g_cumsum
ret |= mKernelChunkGCumsum->get().setArg(idx++, H);
ret |= mKernelChunkGCumsum->get().setArg(idx++, seqLen);
ret |= mKernelChunkGCumsum->get().setArg(idx++, numChunks);
MNN_CHECK_CL_SUCCESS(ret, "setArg chunk_g_cumsum");
}
{
{
// C2: chunk_build_neumann_attn
mKernelChunkNeumannAttn0 = runtime->buildKernel("linear_attention_buf", "chunk_build_neumann_attn_step0", chunkOpts, mOpenCLBackend->getPrecision());
mGWSChunkNeumannAttn0 = {(uint32_t)chunkSize * chunkSize, (uint32_t)(H * numChunks), (uint32_t)batch};
uint32_t idx = 0;
cl_int ret = CL_SUCCESS;
ret |= mKernelChunkNeumannAttn0->get().setArg(idx++, openCLBuffer(mConvOutPrefill.get()));
ret |= mKernelChunkNeumannAttn0->get().setArg(idx++, openCLBuffer(inputs[2])); // beta
ret |= mKernelChunkNeumannAttn0->get().setArg(idx++, openCLBuffer(mGCumsumBuf.get()));
ret |= mKernelChunkNeumannAttn0->get().setArg(idx++, openCLBuffer(mAttnMatrixBuf.get()));
ret |= mKernelChunkNeumannAttn0->get().setArg(idx++, batch);
ret |= mKernelChunkNeumannAttn0->get().setArg(idx++, convDim);
ret |= mKernelChunkNeumannAttn0->get().setArg(idx++, seqLen);
ret |= mKernelChunkNeumannAttn0->get().setArg(idx++, H);
ret |= mKernelChunkNeumannAttn0->get().setArg(idx++, dk);
ret |= mKernelChunkNeumannAttn0->get().setArg(idx++, key_dim);
ret |= mKernelChunkNeumannAttn0->get().setArg(idx++, gqa_factor);
ret |= mKernelChunkNeumannAttn0->get().setArg(idx++, numChunks);
MNN_CHECK_CL_SUCCESS(ret, "setArg chunk_build_neumann_attn_step0");
auto maxWorkGroupSize = static_cast<uint32_t>(runtime->getMaxWorkGroupSize(mKernelChunkNeumannAttn0));
mLWSChunkNeumannAttn0 = localWS3DDefault(mGWSChunkNeumannAttn0, maxWorkGroupSize, mOpenCLBackend->getOpenCLRuntime(), "chunk_build_neumann_attn_step0", mKernelChunkNeumannAttn0, mOpenCLBackend->getCLTuneLevel(), "linear_attention_buf").first;
}
{
// C2: chunk_build_neumann_attn
mKernelChunkNeumannAttn1 = runtime->buildKernel("linear_attention_buf", "chunk_build_neumann_attn_step1", chunkOpts, mOpenCLBackend->getPrecision());
mGWSChunkNeumannAttn1 = {(uint32_t)chunkSize, (uint32_t)(H * numChunks), (uint32_t)batch};
mLWSChunkNeumannAttn1 = {(uint32_t)chunkSize, 1, 1};
uint32_t idx = 0;
cl_int ret = CL_SUCCESS;
ret |= mKernelChunkNeumannAttn1->get().setArg(idx++, openCLBuffer(mAttnMatrixBuf.get()));
ret |= mKernelChunkNeumannAttn1->get().setArg(idx++, batch);
ret |= mKernelChunkNeumannAttn1->get().setArg(idx++, convDim);
ret |= mKernelChunkNeumannAttn1->get().setArg(idx++, seqLen);
ret |= mKernelChunkNeumannAttn1->get().setArg(idx++, H);
ret |= mKernelChunkNeumannAttn1->get().setArg(idx++, dk);
ret |= mKernelChunkNeumannAttn1->get().setArg(idx++, key_dim);
ret |= mKernelChunkNeumannAttn1->get().setArg(idx++, gqa_factor);
ret |= mKernelChunkNeumannAttn1->get().setArg(idx++, numChunks);
MNN_CHECK_CL_SUCCESS(ret, "setArg chunk_build_neumann_attn_step1");
}
}
// C3: chunk_correct_v
mKernelChunkCorrectV = runtime->buildKernel("linear_attention_buf", "chunk_correct_v", chunkOpts, mOpenCLBackend->getPrecision());
mGWSChunkCorrectV = {(uint32_t)UP_DIV(dv, 4), (uint32_t)(chunkSize * numChunks), (uint32_t)(batch * H)};
{
uint32_t idx = 0;
cl_int ret = CL_SUCCESS;
ret |= mKernelChunkCorrectV->get().setArg(idx++, openCLBuffer(mAttnMatrixBuf.get()));
ret |= mKernelChunkCorrectV->get().setArg(idx++, openCLBuffer(mConvOutPrefill.get()));
ret |= mKernelChunkCorrectV->get().setArg(idx++, openCLBuffer(inputs[2])); // beta
ret |= mKernelChunkCorrectV->get().setArg(idx++, openCLBuffer(mGCumsumBuf.get()));
ret |= mKernelChunkCorrectV->get().setArg(idx++, openCLBuffer(mVCorrectedBuf.get()));
ret |= mKernelChunkCorrectV->get().setArg(idx++, openCLBuffer(mKCumdecayBuf.get()));
ret |= mKernelChunkCorrectV->get().setArg(idx++, mGWSChunkCorrectV[0]);
ret |= mKernelChunkCorrectV->get().setArg(idx++, mGWSChunkCorrectV[1]);
ret |= mKernelChunkCorrectV->get().setArg(idx++, mGWSChunkCorrectV[2]);
ret |= mKernelChunkCorrectV->get().setArg(idx++, convDim);
ret |= mKernelChunkCorrectV->get().setArg(idx++, seqLen);
ret |= mKernelChunkCorrectV->get().setArg(idx++, H);
ret |= mKernelChunkCorrectV->get().setArg(idx++, dk);
ret |= mKernelChunkCorrectV->get().setArg(idx++, dv);
ret |= mKernelChunkCorrectV->get().setArg(idx++, key_dim);
ret |= mKernelChunkCorrectV->get().setArg(idx++, gqa_factor);
ret |= mKernelChunkCorrectV->get().setArg(idx++, numChunks);
MNN_CHECK_CL_SUCCESS(ret, "setArg chunk_correct_v");
auto maxWorkGroupSize = static_cast<uint32_t>(runtime->getMaxWorkGroupSize(mKernelChunkCorrectV));
mLWSChunkCorrectV = localWS3DDefault(mGWSChunkCorrectV, maxWorkGroupSize, mOpenCLBackend->getOpenCLRuntime(), "chunk_correct_v", mKernelChunkCorrectV, mOpenCLBackend->getCLTuneLevel(), "linear_attention_buf").first;
}
// C5: chunk_qk_attn (reuses attn_matrix buffer)
mKernelChunkQKAttn = runtime->buildKernel("linear_attention_buf", "chunk_qk_attn", chunkOpts, mOpenCLBackend->getPrecision());
mGWSChunkQKAttn = {(uint32_t)chunkSize, (uint32_t)(chunkSize * numChunks), (uint32_t)(batch * H)};
{
uint32_t idx = 0;
cl_int ret = CL_SUCCESS;
ret |= mKernelChunkQKAttn->get().setArg(idx++, openCLBuffer(mConvOutPrefill.get()));
ret |= mKernelChunkQKAttn->get().setArg(idx++, openCLBuffer(mGCumsumBuf.get()));
ret |= mKernelChunkQKAttn->get().setArg(idx++, openCLBuffer(mAttnMatrixBuf.get())); // overwrite
ret |= mKernelChunkQKAttn->get().setArg(idx++, mGWSChunkQKAttn[0]);
ret |= mKernelChunkQKAttn->get().setArg(idx++, mGWSChunkQKAttn[1]);
ret |= mKernelChunkQKAttn->get().setArg(idx++, mGWSChunkQKAttn[2]);
ret |= mKernelChunkQKAttn->get().setArg(idx++, convDim);
ret |= mKernelChunkQKAttn->get().setArg(idx++, seqLen);
ret |= mKernelChunkQKAttn->get().setArg(idx++, H);
ret |= mKernelChunkQKAttn->get().setArg(idx++, dk);
ret |= mKernelChunkQKAttn->get().setArg(idx++, key_dim);
ret |= mKernelChunkQKAttn->get().setArg(idx++, gqa_factor);
ret |= mKernelChunkQKAttn->get().setArg(idx++, numChunks);
ret |= mKernelChunkQKAttn->get().setArg(idx++, qScale);
MNN_CHECK_CL_SUCCESS(ret, "setArg chunk_qk_attn");
auto maxWorkGroupSize = static_cast<uint32_t>(runtime->getMaxWorkGroupSize(mKernelChunkQKAttn));
mLWSChunkQKAttn = localWS3DDefault(mGWSChunkQKAttn, maxWorkGroupSize, mOpenCLBackend->getOpenCLRuntime(), "chunk_qk_attn", mKernelChunkQKAttn, mOpenCLBackend->getCLTuneLevel(), "linear_attention_buf").first;
}
// C6: chunk_compute_vnew (per-chunk, chunk_idx=11 set dynamically)
mKernelChunkVnew = runtime->buildKernel("linear_attention_buf", "chunk_compute_vnew", chunkOpts, mOpenCLBackend->getPrecision());
mGWSChunkVnew = {(uint32_t)UP_DIV(dv, 4), (uint32_t)chunkSize, (uint32_t)(batch * H)};
{
uint32_t idx = 0;
cl_int ret = CL_SUCCESS;
ret |= mKernelChunkVnew->get().setArg(idx++, openCLBuffer(mVCorrectedBuf.get()));
ret |= mKernelChunkVnew->get().setArg(idx++, openCLBuffer(mKCumdecayBuf.get()));
ret |= mKernelChunkVnew->get().setArg(idx++, openCLBuffer(mStateCache->mRecurrentStateTune.get())); // arg 2: state (tune first)
ret |= mKernelChunkVnew->get().setArg(idx++, openCLBuffer(mVNewBuf.get()));
ret |= mKernelChunkVnew->get().setArg(idx++, mGWSChunkVnew[0]);
ret |= mKernelChunkVnew->get().setArg(idx++, mGWSChunkVnew[1]);
ret |= mKernelChunkVnew->get().setArg(idx++, mGWSChunkVnew[2]);
ret |= mKernelChunkVnew->get().setArg(idx++, dk);
ret |= mKernelChunkVnew->get().setArg(idx++, dv);
ret |= mKernelChunkVnew->get().setArg(idx++, H);
ret |= mKernelChunkVnew->get().setArg(idx++, numChunks);
ret |= mKernelChunkVnew->get().setArg(idx++, 0); // chunk_idx placeholder
MNN_CHECK_CL_SUCCESS(ret, "setArg chunk_compute_vnew");
auto maxWorkGroupSize = static_cast<uint32_t>(runtime->getMaxWorkGroupSize(mKernelChunkVnew));
mLWSChunkVnew = localWS3DDefault(mGWSChunkVnew, maxWorkGroupSize, mOpenCLBackend->getOpenCLRuntime(), "chunk_compute_vnew", mKernelChunkVnew, mOpenCLBackend->getCLTuneLevel(), "linear_attention_buf").first;
// Swap to real state buffer after tuning
ret |= mKernelChunkVnew->get().setArg(2, openCLBuffer(mStateCache->mRecurrentState.get()));
}
// C6.5: chunk_output (per-chunk, chunk_idx=17 set dynamically)
mKernelChunkOutput = runtime->buildKernel("linear_attention_buf", "chunk_output", chunkOpts, mOpenCLBackend->getPrecision());
mGWSChunkOutput = {(uint32_t)UP_DIV(dv, 4) * chunkSize, (uint32_t)H, (uint32_t)batch};
{
uint32_t idx = 0;
cl_int ret = CL_SUCCESS;
ret |= mKernelChunkOutput->get().setArg(idx++, openCLBuffer(mConvOutPrefill.get()));
ret |= mKernelChunkOutput->get().setArg(idx++, openCLBuffer(mAttnMatrixBuf.get())); // qk_attn after C5
ret |= mKernelChunkOutput->get().setArg(idx++, openCLBuffer(mVNewBuf.get()));
ret |= mKernelChunkOutput->get().setArg(idx++, openCLBuffer(mGCumsumBuf.get()));
ret |= mKernelChunkOutput->get().setArg(idx++, openCLBuffer(mStateCache->mRecurrentState.get())); // arg 4: state (tune first)
ret |= mKernelChunkOutput->get().setArg(idx++, openCLBuffer(outputs[0]));
ret |= mKernelChunkOutput->get().setArg(idx++, mGWSChunkOutput[0]);
ret |= mKernelChunkOutput->get().setArg(idx++, mGWSChunkOutput[1]);
ret |= mKernelChunkOutput->get().setArg(idx++, mGWSChunkOutput[2]);
ret |= mKernelChunkOutput->get().setArg(idx++, convDim);
ret |= mKernelChunkOutput->get().setArg(idx++, seqLen);
ret |= mKernelChunkOutput->get().setArg(idx++, H);
ret |= mKernelChunkOutput->get().setArg(idx++, dk);
ret |= mKernelChunkOutput->get().setArg(idx++, dv);
ret |= mKernelChunkOutput->get().setArg(idx++, key_dim);
ret |= mKernelChunkOutput->get().setArg(idx++, gqa_factor);
ret |= mKernelChunkOutput->get().setArg(idx++, numChunks);
ret |= mKernelChunkOutput->get().setArg(idx++, 0); // chunk_idx placeholder
ret |= mKernelChunkOutput->get().setArg(idx++, qScale);
MNN_CHECK_CL_SUCCESS(ret, "setArg chunk_output");
auto maxWorkGroupSize = static_cast<uint32_t>(runtime->getMaxWorkGroupSize(mKernelChunkOutput));
mLWSChunkOutput = localWS3DDefault(mGWSChunkOutput, maxWorkGroupSize, mOpenCLBackend->getOpenCLRuntime(), "chunk_output", mKernelChunkOutput, mOpenCLBackend->getCLTuneLevel(), "linear_attention_buf").first;
}
// C7: chunk_output_state_update (per-chunk, chunk_idx=17 set dynamically)
mKernelChunkOutputUpdate = runtime->buildKernel("linear_attention_buf", "chunk_output_state_update", chunkOpts, mOpenCLBackend->getPrecision());
mGWSChunkOutputUpdate = {(uint32_t)UP_DIV(dv, 4) * dk, (uint32_t)H, (uint32_t)batch};
{
uint32_t idx = 0;
cl_int ret = CL_SUCCESS;
ret |= mKernelChunkOutputUpdate->get().setArg(idx++, openCLBuffer(mConvOutPrefill.get()));
ret |= mKernelChunkOutputUpdate->get().setArg(idx++, openCLBuffer(mAttnMatrixBuf.get())); // qk_attn after C5
ret |= mKernelChunkOutputUpdate->get().setArg(idx++, openCLBuffer(mVNewBuf.get()));
ret |= mKernelChunkOutputUpdate->get().setArg(idx++, openCLBuffer(mGCumsumBuf.get()));
ret |= mKernelChunkOutputUpdate->get().setArg(idx++, openCLBuffer(mStateCache->mRecurrentStateTune.get())); // arg 4: state (tune first)
ret |= mKernelChunkOutputUpdate->get().setArg(idx++, openCLBuffer(outputs[0]));
ret |= mKernelChunkOutputUpdate->get().setArg(idx++, mGWSChunkOutputUpdate[0]);
ret |= mKernelChunkOutputUpdate->get().setArg(idx++, mGWSChunkOutputUpdate[1]);
ret |= mKernelChunkOutputUpdate->get().setArg(idx++, mGWSChunkOutputUpdate[2]);
ret |= mKernelChunkOutputUpdate->get().setArg(idx++, convDim);
ret |= mKernelChunkOutputUpdate->get().setArg(idx++, seqLen);
ret |= mKernelChunkOutputUpdate->get().setArg(idx++, H);
ret |= mKernelChunkOutputUpdate->get().setArg(idx++, dk);
ret |= mKernelChunkOutputUpdate->get().setArg(idx++, dv);
ret |= mKernelChunkOutputUpdate->get().setArg(idx++, key_dim);
ret |= mKernelChunkOutputUpdate->get().setArg(idx++, gqa_factor);
ret |= mKernelChunkOutputUpdate->get().setArg(idx++, numChunks);
ret |= mKernelChunkOutputUpdate->get().setArg(idx++, 0); // chunk_idx placeholder
ret |= mKernelChunkOutputUpdate->get().setArg(idx++, qScale);
MNN_CHECK_CL_SUCCESS(ret, "setArg chunk_output_state_update");
auto maxWorkGroupSize = static_cast<uint32_t>(runtime->getMaxWorkGroupSize(mKernelChunkOutputUpdate));
mLWSChunkOutputUpdate = localWS3DDefault(mGWSChunkOutputUpdate, maxWorkGroupSize, mOpenCLBackend->getOpenCLRuntime(), "chunk_output_state_update", mKernelChunkOutputUpdate, mOpenCLBackend->getCLTuneLevel(), "linear_attention_buf").first;
// Swap to real state buffer after tuning
ret |= mKernelChunkOutputUpdate->get().setArg(4, openCLBuffer(mStateCache->mRecurrentState.get()));
}
// Round up chunked GWS
for (auto& gws_lws : std::vector<std::pair<std::vector<uint32_t>*, std::vector<uint32_t>*>>{
{&mGWSChunkCorrectV, &mLWSChunkCorrectV}, {&mGWSChunkNeumannAttn0, &mLWSChunkNeumannAttn0}, {&mGWSChunkNeumannAttn1, &mLWSChunkNeumannAttn1},
{&mGWSChunkQKAttn, &mLWSChunkQKAttn}, {&mGWSChunkVnew, &mLWSChunkVnew},{&mGWSChunkOutput, &mLWSChunkOutput},
{&mGWSChunkOutputUpdate, &mLWSChunkOutputUpdate}}) {
for (int d = 0; d < 3; ++d) {
(*gws_lws.first)[d] = ROUND_UP((*gws_lws.first)[d], std::max((uint32_t)1, (*gws_lws.second)[d]));
}
}
return NO_ERROR;
}
ErrorCode LinearAttentionBufExecution::onExecuteChunkedPrefill(const std::vector<Tensor *> &inputs, const std::vector<Tensor *> &outputs) {
auto runtime = mOpenCLBackend->getOpenCLRuntime();
int convStateSize = inputs[3]->length(2) - 1;
#ifdef ENABLE_OPENCL_TIME_PROFILER
{ cl::Event event; run3DKernelDefault(mKernelConvSiluPrefill, mGWSConvSiluPrefill, mLWSConvSiluPrefill, runtime, &event); runtime->pushEvent({"linear_attn_conv_silu", event}); }
if (convStateSize > 0) {
cl::Event event; run3DKernelDefault(mKernelConvStateUpdatePrefill, mGWSConvStateUpdatePrefill, mLWSConvStateUpdatePrefill, runtime, &event); runtime->pushEvent({"linear_attn_conv_state_update", event});
}
if (mUseQKL2Norm) {
cl::Event event; run3DKernelDefault(mKernell2NormPrefill, mGWSl2NormPrefill, mLWSl2NormPrefill, runtime, &event); runtime->pushEvent({"l2_norm", event});
}
{ cl::Event e; run3DKernelDefault(mKernelChunkGCumsum, mGWSChunkGCumsum, mLWSChunkGCumsum, runtime, &e); runtime->pushEvent({"chunk_g_cumsum", e}); }
{ cl::Event e; run3DKernelDefault(mKernelChunkNeumannAttn0, mGWSChunkNeumannAttn0, mLWSChunkNeumannAttn0, runtime, &e); runtime->pushEvent({"chunk_build_neumann_attn0", e}); }
{ cl::Event e; run3DKernelDefault(mKernelChunkNeumannAttn1, mGWSChunkNeumannAttn1, mLWSChunkNeumannAttn1, runtime, &e); runtime->pushEvent({"chunk_build_neumann_attn1", e}); }
{ cl::Event e; run3DKernelDefault(mKernelChunkCorrectV, mGWSChunkCorrectV, mLWSChunkCorrectV, runtime, &e); runtime->pushEvent({"chunk_correct_v", e}); }
{ cl::Event e; run3DKernelDefault(mKernelChunkQKAttn, mGWSChunkQKAttn, mLWSChunkQKAttn, runtime, &e); runtime->pushEvent({"chunk_qk_attn", e}); }
for (int c = 0; c < mNumChunks; ++c) {
mKernelChunkVnew->get().setArg(11, c);
mKernelChunkOutput->get().setArg(17, c);
mKernelChunkOutputUpdate->get().setArg(17, c);
{ cl::Event e; run3DKernelDefault(mKernelChunkVnew, mGWSChunkVnew, mLWSChunkVnew, runtime, &e); runtime->pushEvent({"chunk_vnew_" + std::to_string(c), e}); }
{ cl::Event e; runKernel2D(mKernelChunkOutput, mGWSChunkOutput, mLWSChunkOutput, runtime, &e); runtime->pushEvent({"chunk_output_" + std::to_string(c), e}); }
{ cl::Event e; run3DKernelDefault(mKernelChunkOutputUpdate, mGWSChunkOutputUpdate, mLWSChunkOutputUpdate, runtime, &e); runtime->pushEvent({"chunk_update" + std::to_string(c), e}); }
}
#else
// Common kernels
run3DKernelDefault(mKernelConvSiluPrefill, mGWSConvSiluPrefill, mLWSConvSiluPrefill, runtime);
if (convStateSize > 0) {
run3DKernelDefault(mKernelConvStateUpdatePrefill, mGWSConvStateUpdatePrefill, mLWSConvStateUpdatePrefill, runtime);
}
if (mUseQKL2Norm) {
run3DKernelDefault(mKernell2NormPrefill, mGWSl2NormPrefill, mLWSl2NormPrefill, runtime);
}
// Chunked prefill: C1 → C2 → C3, C4 → C5 → loop(C6, C7)
run3DKernelDefault(mKernelChunkGCumsum, mGWSChunkGCumsum, mLWSChunkGCumsum, runtime);
run3DKernelDefault(mKernelChunkNeumannAttn0, mGWSChunkNeumannAttn0, mLWSChunkNeumannAttn0, runtime);
run3DKernelDefault(mKernelChunkNeumannAttn1, mGWSChunkNeumannAttn1, mLWSChunkNeumannAttn1, runtime);
run3DKernelDefault(mKernelChunkCorrectV, mGWSChunkCorrectV, mLWSChunkCorrectV, runtime);
run3DKernelDefault(mKernelChunkQKAttn, mGWSChunkQKAttn, mLWSChunkQKAttn, runtime);
for (int c = 0; c < mNumChunks; ++c) {
mKernelChunkVnew->get().setArg(11, c); // chunk_idx at position 11
mKernelChunkOutput->get().setArg(17, c); // chunk_idx at position 17
mKernelChunkOutputUpdate->get().setArg(17, c); // chunk_idx at position 17
run3DKernelDefault(mKernelChunkVnew, mGWSChunkVnew, mLWSChunkVnew, runtime);
runKernel2D(mKernelChunkOutput, mGWSChunkOutput, mLWSChunkOutput, runtime);
run3DKernelDefault(mKernelChunkOutputUpdate, mGWSChunkOutputUpdate, mLWSChunkOutputUpdate, runtime);
}
#endif
return NO_ERROR;
}
ErrorCode LinearAttentionBufExecution::onExecute(const std::vector<Tensor *> &inputs, const std::vector<Tensor *> &outputs) {
// onResize() may be skipped when shapes are unchanged. Ensure state is reset here too.
int seqLen = inputs[0]->length(2);
if (seqLen > 1 && mMeta != nullptr && mMeta->previous == mMeta->remove) {
bool loadingFromDisk = (mMeta->file_flag == KVMeta::PendingRead && mMeta->file_name.size() > 0);
if (!loadingFromDisk) {
auto runtime = mOpenCLBackend->getOpenCLRuntime();
int bytesPerElement = mOpenCLBackend->fpBytes();
if (mStateCache->mConvState.get() != nullptr) {
cl_int res;
int bufferBytes = mStateCache->mConvState->elementSize() * bytesPerElement;
void* mapPtr = runtime->commandQueue().enqueueMapBuffer(
openCLBuffer(mStateCache->mConvState.get()), true, CL_MAP_WRITE, 0, bufferBytes, nullptr, nullptr, &res);
if (mapPtr != nullptr && res == CL_SUCCESS) {
::memset(mapPtr, 0, bufferBytes);
runtime->commandQueue().enqueueUnmapMemObject(openCLBuffer(mStateCache->mConvState.get()), mapPtr);
}
}
{
cl_int res;
int bufferBytes = mStateCache->mRecurrentState->elementSize() * bytesPerElement;
void* mapPtr = runtime->commandQueue().enqueueMapBuffer(
openCLBuffer(mStateCache->mRecurrentState.get()), true, CL_MAP_WRITE, 0, bufferBytes, nullptr, nullptr, &res);
if (mapPtr != nullptr && res == CL_SUCCESS) {
::memset(mapPtr, 0, bufferBytes);
runtime->commandQueue().enqueueUnmapMemObject(openCLBuffer(mStateCache->mRecurrentState.get()), mapPtr);
}
}
}
}
if (mUseChunkedPrefill) {
return onExecuteChunkedPrefill(inputs, outputs);
}
auto runtime = mOpenCLBackend->getOpenCLRuntime();
int convStateSize = inputs[3]->length(2) - 1;
#ifdef ENABLE_OPENCL_TIME_PROFILER
{
cl::Event event;
run3DKernelDefault(mKernelConvSilu, mGWSConvSilu, mLWSConvSilu, runtime, &event);
runtime->pushEvent({"linear_attn_conv_silu", event});
}
if (convStateSize > 0) {
cl::Event event;
run3DKernelDefault(mKernelConvStateUpdate, mGWSConvStateUpdate, mLWSConvStateUpdate, runtime, &event);
runtime->pushEvent({"linear_attn_conv_state_update", event});
}
if(mUseQKL2Norm){
cl::Event event;
run3DKernelDefault(mKernell2Norm, mGWSl2Norm, mLWSl2Norm, runtime, &event);
runtime->pushEvent({"l2_norm", event});
}
{
cl::Event event;
runKernel2D(mKernelGatedDeltaRule, mGWSGatedDeltaRule, mLWSGatedDeltaRule, runtime, &event);
runtime->pushEvent({"linear_attn_gated_delta_rule", event});
}
#else
if(mOpenCLBackend->isUseRecordQueue()){
mOpenCLBackend->addRecord(mRecording, mOpRecordUpdateInfo);
return NO_ERROR;
}
run3DKernelDefault(mKernelConvSilu, mGWSConvSilu, mLWSConvSilu, runtime);
if (convStateSize > 0) {
run3DKernelDefault(mKernelConvStateUpdate, mGWSConvStateUpdate, mLWSConvStateUpdate, runtime);
}
if(mUseQKL2Norm){
run3DKernelDefault(mKernell2Norm, mGWSl2Norm, mLWSl2Norm, runtime);
}
runKernel2D(mKernelGatedDeltaRule, mGWSGatedDeltaRule, mLWSGatedDeltaRule, runtime);
#endif
return NO_ERROR;
}
bool LinearAttentionBufExecution::onClone(Backend* bn, const Op* op, Execution** dst) {
if (nullptr == dst) {
return true;
}
auto exe = new LinearAttentionBufExecution(op, bn);
// Share persistent state buffers between prefill and decode Executions
exe->mStateCache = mStateCache;
*dst = exe;
return true;
}
class LinearAttentionBufCreator : public OpenCLBackend::Creator {
public:
virtual Execution *onCreate(const std::vector<Tensor *> &inputs, const std::vector<Tensor *> &outputs,
const MNN::Op *op, Backend *backend) const override {
for (int i = 0; i < inputs.size(); ++i) {
TensorUtils::setTensorSupportPack(inputs[i], false);
}
for (int i = 0; i < outputs.size(); ++i) {
TensorUtils::setTensorSupportPack(outputs[i], false);
}
OPENCL_CREATOR_CHECK(new LinearAttentionBufExecution(op, backend));
}
};
REGISTER_OPENCL_OP_CREATOR_TRANSFORMER(LinearAttentionBufCreator, OpType_LinearAttention, BUFFER);
} // namespace OpenCL
} // namespace MNN
#endif /* MNN_SUPPORT_TRANSFORMER_FUSE */