// // CPULinearAttention.cpp // MNN // // Created by MNN on 2026/02/10. // Copyright © 2018, Alibaba Group Holding Limited // #ifdef MNN_SUPPORT_TRANSFORMER_FUSE #include #include #include #include #include "CPULinearAttention.hpp" #include "CPUBackend.hpp" #include "core/MNNFileUtils.h" #include "compute/CommonOptFunction.h" #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" namespace MNN { // ─── Byte-aware element access helpers ─── static inline float _readElement(const int8_t* ptr, int index, int bytes) { #ifdef __aarch64__ if (bytes == 2) return (float)((const __fp16*)ptr)[index]; #endif return ((const float*)ptr)[index]; } static inline void _writeElement(int8_t* ptr, int index, float val, int bytes) { #ifdef __aarch64__ if (bytes == 2) { ((__fp16*)ptr)[index] = (__fp16)val; return; } #endif ((float*)ptr)[index] = val; } // Snapshot the post-prefix recurrent state (lazy-allocated) for eraseHistory // rollback. Allocation failure leaves mSnapshotValid=false; rollback then // falls back to zeroing. static void snapshotPrefixState(StateCache* cache, Backend* backend) { if (cache == nullptr || cache->mConvState.get() == nullptr) { return; } int convStateBytes = cache->mConvState->elementSize(); if (cache->mConvStateSnapshot.get() == nullptr) { cache->mConvStateSnapshot.reset(Tensor::createDevice({convStateBytes})); if (!backend->onAcquireBuffer(cache->mConvStateSnapshot.get(), Backend::STATIC)) { cache->mConvStateSnapshot.reset(); return; } } ::memcpy(cache->mConvStateSnapshot->host(), cache->mConvState->host(), convStateBytes); if (cache->mRecurrentState.get() != nullptr) { int rnnBytes = cache->mRecurrentState->elementSize(); if (cache->mRecurrentStateSnapshot.get() == nullptr) { cache->mRecurrentStateSnapshot.reset(Tensor::createDevice({rnnBytes})); if (!backend->onAcquireBuffer(cache->mRecurrentStateSnapshot.get(), Backend::STATIC)) { cache->mRecurrentStateSnapshot.reset(); return; } } ::memcpy(cache->mRecurrentStateSnapshot->host(), cache->mRecurrentState->host(), rnnBytes); } cache->mSnapshotValid = true; } ErrorCode CPULinearAttention::onResize(const std::vector& inputs, const std::vector& outputs) { auto qkv = inputs[0]; auto convWeight = inputs[3]; int batch = qkv->length(0); int convDim = qkv->length(1); // D (total projection dim) int seqLen = qkv->length(2); // L int kernelSize = convWeight->length(2); int convStateSize = kernelSize - 1; // ─── Per-type parameters ─── int convChannels = convDim; bool needRecurrentState = false; if (mAttentionType == "short_conv") { convChannels = mHeadVDim; } else if (mAttentionType == "gated_delta_rule") { needRecurrentState = true; } // ─── Persistent state buffers (STATIC): allocate once, shared via onClone ─── if (mStateCache->mConvState.get() == nullptr) { mStateCache->mConvState.reset(Tensor::createDevice({batch * convChannels * convStateSize * mBytes})); bool success = backend()->onAcquireBuffer(mStateCache->mConvState.get(), Backend::STATIC); if (!success) return OUT_OF_MEMORY; ::memset(mStateCache->mConvState->host(), 0, batch * convChannels * convStateSize * mBytes); if (needRecurrentState) { int H = mNumVHeads, dk = mHeadKDim, dv = mHeadVDim; mStateCache->mRecurrentState.reset(Tensor::createDevice({batch * H * dk * dv * mBytes})); success = backend()->onAcquireBuffer(mStateCache->mRecurrentState.get(), Backend::STATIC); if (!success) return OUT_OF_MEMORY; ::memset(mStateCache->mRecurrentState->host(), 0, batch * H * dk * dv * mBytes); } } else if (seqLen > 1) { // Prefill: decide keep/restore/reset from meta. LA state isn't // token-indexed, so eraseHistory triggers a snapshot restore rather // than a truncation. bool loadingFromDisk = (mMeta != nullptr && mMeta->file_flag == KVMeta::PendingRead && mMeta->file_name.size() > 0); bool isExplicitRollback = (mMeta != nullptr && mMeta->remove > 0); bool isFreshPrefill = (mMeta == nullptr || mMeta->previous == 0); int convStateBytes = batch * convChannels * convStateSize * mBytes; int rnnBytes = 0; if (mStateCache->mRecurrentState.get() != nullptr) { int H = mNumVHeads, dk = mHeadKDim, dv = mHeadVDim; rnnBytes = batch * H * dk * dv * mBytes; } if (loadingFromDisk) { // onExecute will mmap-load the prefix state and snapshot it. } else if (isExplicitRollback) { // eraseHistory(): roll back to the saved post-prefix snapshot. If no // snapshot exists (rollback before any prefix prefill ran), zero out. if (mStateCache->mSnapshotValid && mStateCache->mConvStateSnapshot.get() != nullptr) { ::memcpy(mStateCache->mConvState->host(), mStateCache->mConvStateSnapshot->host(), convStateBytes); if (mStateCache->mRecurrentState.get() != nullptr && mStateCache->mRecurrentStateSnapshot.get() != nullptr) { ::memcpy(mStateCache->mRecurrentState->host(), mStateCache->mRecurrentStateSnapshot->host(), rnnBytes); } } else { ::memset(mStateCache->mConvState->host(), 0, convStateBytes); if (mStateCache->mRecurrentState.get() != nullptr) { ::memset(mStateCache->mRecurrentState->host(), 0, rnnBytes); } } } else if (isFreshPrefill) { // Fresh sequence: zero the state and drop any stale snapshot. ::memset(mStateCache->mConvState->host(), 0, convStateBytes); if (mStateCache->mRecurrentState.get() != nullptr) { ::memset(mStateCache->mRecurrentState->host(), 0, rnnBytes); } mStateCache->mSnapshotValid = false; } // Else (mMeta->previous > 0 && mMeta->remove == 0): reuse_kv continuation. // Keep the live state so the new prefill extends from it. } // ─── Temporary buffers (DYNAMIC) ─── int totalLen = convStateSize + seqLen; mConvPadded.reset(Tensor::createDevice({batch * convChannels * totalLen * mBytes})); bool success = backend()->onAcquireBuffer(mConvPadded.get(), Backend::DYNAMIC); if (!success) return OUT_OF_MEMORY; mConvOut.reset(Tensor::createDevice({batch * convChannels * seqLen * mBytes})); success = backend()->onAcquireBuffer(mConvOut.get(), Backend::DYNAMIC); if (!success) return OUT_OF_MEMORY; if (needRecurrentState) { int dk = mHeadKDim, dv = mHeadVDim; int threadNum = static_cast(backend())->threadNumber(); // Per-thread scratch holds q_local + k_local + v_local + vPred + delta. // Prefill uses MNNFusedGatedDelta (only needs first 2*dk+dv) but decode // falls back to the legacy two-call path (MNNDualMatVec + scalar // correction + MNNDecayRankOneUpdate) which needs the full 2*dk+3*dv: // the fused kernel regressed FP32 decode by ~3.5% on small L=1 shapes. int perThread = 2 * dk + 3 * dv; mThreadLocalBuf.reset(Tensor::createDevice({threadNum * perThread * mBytes})); success = backend()->onAcquireBuffer(mThreadLocalBuf.get(), Backend::DYNAMIC); if (!success) return OUT_OF_MEMORY; // Pre-computed decay buffer: exp(gate) for all [B, L, H] // Always fp32 — MNNExp requires fp32, decay is a scalar per timestep // Use int8_t with explicit byte count to avoid Arm82 backend halving the allocation mDecayBuf.reset(Tensor::createDevice({batch * seqLen * mNumVHeads * (int)sizeof(float)})); success = backend()->onAcquireBuffer(mDecayBuf.get(), Backend::DYNAMIC); if (!success) return OUT_OF_MEMORY; backend()->onReleaseBuffer(mDecayBuf.get(), Backend::DYNAMIC); backend()->onReleaseBuffer(mThreadLocalBuf.get(), Backend::DYNAMIC); } // fp16 path: per-thread fp32 temp buffer for Conv1D + SiLu (MNNSiLu requires fp32) if (mBytes == 2) { int threadNum = static_cast(backend())->threadNumber(); // Need totalLen floats for padded input + L floats for SiLu output = (totalLen + seqLen) per thread mConvFp32Buf.reset(Tensor::createDevice({threadNum * (totalLen + seqLen) * (int)sizeof(float)})); success = backend()->onAcquireBuffer(mConvFp32Buf.get(), Backend::DYNAMIC); if (!success) return OUT_OF_MEMORY; backend()->onReleaseBuffer(mConvFp32Buf.get(), Backend::DYNAMIC); } backend()->onReleaseBuffer(mConvPadded.get(), Backend::DYNAMIC); backend()->onReleaseBuffer(mConvOut.get(), Backend::DYNAMIC); return NO_ERROR; } void CPULinearAttention::gated_delta_rule_ref(const std::vector& inputs, const std::vector& outputs) { // Reference implementation (fp32 only, for correctness verification) auto qkvTensor = inputs[0]; auto gateTensor = inputs[1]; auto betaTensor = inputs[2]; auto convWTensor = inputs[3]; auto outTensor = outputs[0]; const float* qkvPtr = qkvTensor->host(); const float* gatePtr = gateTensor->host(); const float* betaPtr = betaTensor->host(); const float* convWPtr = convWTensor->host(); float* outPtr = outTensor->host(); const int B = qkvTensor->length(0); const int D = qkvTensor->length(1); const int L = qkvTensor->length(2); const int H_k = mNumKHeads; const int H_v = mNumVHeads; const int d_k = mHeadKDim; const int d_v = mHeadVDim; const int key_dim = H_k * d_k; const int val_dim = H_v * d_v; const int K_conv = convWTensor->length(2); const int convStateSize = K_conv - 1; const bool useL2Norm = mUseQKL2Norm; const int gqa_factor = (H_v > H_k) ? (H_v / H_k) : 1; const int H = H_v; // Step 1: Depthwise Conv1D + SiLU const int totalLen = convStateSize + L; std::vector convInput(B * D * totalLen, 0.0f); float* convStatePtr = mStateCache->mConvState->host(); for (int b = 0; b < B; ++b) { for (int d = 0; d < D; ++d) { float* dst = convInput.data() + b * D * totalLen + d * totalLen; const float* stateChannel = convStatePtr + b * D * convStateSize + d * convStateSize; ::memcpy(dst, stateChannel, convStateSize * sizeof(float)); const float* inputChannel = qkvPtr + b * D * L + d * L; ::memcpy(dst + convStateSize, inputChannel, L * sizeof(float)); } } std::vector convOut(B * D * L, 0.0f); for (int b = 0; b < B; ++b) { for (int d = 0; d < D; ++d) { const float* src = convInput.data() + b * D * totalLen + d * totalLen; const float* weight = convWPtr + d * K_conv; float* out = convOut.data() + b * D * L + d * L; for (int l = 0; l < L; ++l) { float sum = 0.0f; for (int k = 0; k < K_conv; ++k) { sum += src[l + k] * weight[k]; } float sigmoid_val = 1.0f / (1.0f + expf(-sum)); out[l] = sum * sigmoid_val; } } } for (int b = 0; b < B; ++b) { for (int d = 0; d < D; ++d) { const float* src = convInput.data() + b * D * totalLen + d * totalLen + (totalLen - convStateSize); float* dst = convStatePtr + b * D * convStateSize + d * convStateSize; ::memcpy(dst, src, convStateSize * sizeof(float)); } } // Step 2: Split Q, K, V std::vector Q(B * L * H * d_k, 0.0f); std::vector K(B * L * H * d_k, 0.0f); std::vector V(B * L * H * d_v, 0.0f); for (int b = 0; b < B; ++b) { for (int l = 0; l < L; ++l) { for (int h = 0; h < H_k; ++h) { for (int dk = 0; dk < d_k; ++dk) { int srcChannel = h * d_k + dk; float val = convOut[b * D * L + srcChannel * L + l]; for (int r = 0; r < gqa_factor; ++r) { int dstHead = h * gqa_factor + r; Q[(b * L + l) * H * d_k + dstHead * d_k + dk] = val; } } } for (int h = 0; h < H_k; ++h) { for (int dk = 0; dk < d_k; ++dk) { int srcChannel = key_dim + h * d_k + dk; float val = convOut[b * D * L + srcChannel * L + l]; for (int r = 0; r < gqa_factor; ++r) { int dstHead = h * gqa_factor + r; K[(b * L + l) * H * d_k + dstHead * d_k + dk] = val; } } } for (int h = 0; h < H_v; ++h) { for (int dv = 0; dv < d_v; ++dv) { int srcChannel = 2 * key_dim + h * d_v + dv; float val = convOut[b * D * L + srcChannel * L + l]; V[(b * L + l) * H * d_v + h * d_v + dv] = val; } } } } // Step 3: Optional L2 Normalization if (useL2Norm) { const float eps = 1e-6f; for (int i = 0; i < B * L * H; ++i) { float* qHead = Q.data() + i * d_k; float sumSq = 0.0f; for (int dk = 0; dk < d_k; ++dk) sumSq += qHead[dk] * qHead[dk]; float invNorm = 1.0f / sqrtf(sumSq + eps); for (int dk = 0; dk < d_k; ++dk) qHead[dk] *= invNorm; float* kHead = K.data() + i * d_k; sumSq = 0.0f; for (int dk = 0; dk < d_k; ++dk) sumSq += kHead[dk] * kHead[dk]; invNorm = 1.0f / sqrtf(sumSq + eps); for (int dk = 0; dk < d_k; ++dk) kHead[dk] *= invNorm; } } // Step 4: Scale Q const float qScale = 1.0f / sqrtf((float)d_k); for (int i = 0; i < B * L * H * d_k; ++i) Q[i] *= qScale; // Step 5: Gated Delta Rule float* rnnStatePtr = mStateCache->mRecurrentState->host(); for (int b = 0; b < B; ++b) { for (int t = 0; t < L; ++t) { for (int h = 0; h < H; ++h) { float* state = rnnStatePtr + (b * H + h) * d_k * d_v; const float* q_t = Q.data() + (b * L + t) * H * d_k + h * d_k; const float* k_t = K.data() + (b * L + t) * H * d_k + h * d_k; const float* v_t = V.data() + (b * L + t) * H * d_v + h * d_v; float g_t = gatePtr[b * L * H + t * H + h]; float beta_t = betaPtr[b * L * H + t * H + h]; float decay = expf(g_t); for (int i = 0; i < d_k * d_v; ++i) state[i] *= decay; std::vector v_pred(d_v, 0.0f); for (int dk = 0; dk < d_k; ++dk) for (int dv = 0; dv < d_v; ++dv) v_pred[dv] += state[dk * d_v + dv] * k_t[dk]; std::vector delta(d_v); for (int dv = 0; dv < d_v; ++dv) delta[dv] = beta_t * (v_t[dv] - v_pred[dv]); for (int dk = 0; dk < d_k; ++dk) for (int dv = 0; dv < d_v; ++dv) state[dk * d_v + dv] += k_t[dk] * delta[dv]; float* o_t = outPtr + (b * L + t) * H * d_v + h * d_v; for (int dv = 0; dv < d_v; ++dv) { float sum = 0.0f; for (int dk = 0; dk < d_k; ++dk) sum += state[dk * d_v + dv] * q_t[dk]; o_t[dv] = sum; } } } } } ErrorCode CPULinearAttention::onExecute(const std::vector& inputs, const std::vector& 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) { if (mStateCache->mConvState.get() != nullptr) { ::memset(mStateCache->mConvState->host(), 0, mStateCache->mConvState->elementSize()); } if (mStateCache->mRecurrentState.get() != nullptr) { ::memset(mStateCache->mRecurrentState->host(), 0, mStateCache->mRecurrentState->elementSize()); } } } // Capture layer_index once per prefix-cache session (chunk 1, marked by // previous == remove); chunks 2..N reuse it. Mirrors CPUKVCacheManager, // which only advances layer_index in onAlloc (chunk 1) and not in onRealloc // (chunks 2..N) — advancing on every chunk would drift LA past FA layers // and clobber FA's prefix files (SIGBUS in hybrid models). if (mMeta != nullptr && mMeta->file_name.size() > 0 && (mMeta->file_flag == KVMeta::PendingWrite || mMeta->file_flag == KVMeta::PendingRead) && mMeta->previous == mMeta->remove) { mStateCache->mPrefixLayerIndex = mMeta->layer_index; mMeta->layer_index = (mMeta->layer_index + 1) % mMeta->layer_nums; } // Load prefix cache from disk (PendingRead) if (mMeta != nullptr && mMeta->file_name.size() > 0 && mMeta->file_flag == KVMeta::PendingRead) { // Sentinel guard: capture above only fires on previous == remove. // On other paths (e.g. partial eraseHistory) the index stays -1; using // it would write/read "_-1.k" and corrupt the cache dir, so skip with // a diagnostic. if (mStateCache->mPrefixLayerIndex < 0) { MNN_ERROR( "CPULinearAttention: PendingRead skipped, no prefix-layer-index captured " "for this session (previous=%zu remove=%zu — capture predicate requires " "previous == remove)\n", mMeta->previous, mMeta->remove); } else { int layer_index = mStateCache->mPrefixLayerIndex; std::string basePath = MNNFilePathConcat(mPrefixCacheDir, mMeta->file_name) + "_" + std::to_string(layer_index); std::string pathk = basePath + ".k"; std::string pathv = basePath + ".v"; // Load conv state (.k file) auto kfd = MNNOpenFile(pathk.c_str(), MNN_FILE_READ); if (kfd != INVALID_FILE) { size_t kSize = MNNGetFileSize(kfd); if (kSize > 0 && kSize != INVALID_SIZE) { void* kMap = MNNMmapFile(kfd, kSize, true); if (kMap != nullptr) { ::memcpy(mStateCache->mConvState->host(), kMap, kSize); MNNUnmapFile(kMap, kSize); } } MNNCloseFile(kfd); } else { MNN_PRINT("CPULinearAttention: Failed to open prefix cache file: %s\n", pathk.c_str()); } // Load recurrent state (.v file) auto vfd = MNNOpenFile(pathv.c_str(), MNN_FILE_READ); if (vfd != INVALID_FILE) { size_t vSize = MNNGetFileSize(vfd); if (vSize > 0 && vSize != INVALID_SIZE && mStateCache->mRecurrentState.get() != nullptr) { void* vMap = MNNMmapFile(vfd, vSize, true); if (vMap != nullptr) { ::memcpy(mStateCache->mRecurrentState->host(), vMap, vSize); MNNUnmapFile(vMap, vSize); } } MNNCloseFile(vfd); } else { MNN_PRINT("CPULinearAttention: Failed to open prefix cache file: %s\n", pathv.c_str()); } // Snapshot the loaded state for in-memory eraseHistory rollback. snapshotPrefixState(mStateCache.get(), backend()); } } // Normal execution if (mAttentionType == "short_conv") { short_conv(inputs, outputs); } else { gated_delta_rule_mnn(inputs, outputs); } // Save prefix cache to disk (PendingWrite) if (mMeta != nullptr && mMeta->file_name.size() > 0 && mMeta->file_flag == KVMeta::PendingWrite) { // Sentinel guard: same rationale as PendingRead above. if (mStateCache->mPrefixLayerIndex < 0) { MNN_ERROR( "CPULinearAttention: PendingWrite skipped, no prefix-layer-index captured " "for this session (previous=%zu remove=%zu — capture predicate requires " "previous == remove)\n", mMeta->previous, mMeta->remove); } else { MNNCreateDir(mPrefixCacheDir.c_str()); int layer_index = mStateCache->mPrefixLayerIndex; std::string basePath = MNNFilePathConcat(mPrefixCacheDir, mMeta->file_name) + "_" + std::to_string(layer_index); std::string pathk = basePath + ".k"; std::string pathv = basePath + ".v"; // Save conv state (.k file) size_t convBytes = mStateCache->mConvState->elementSize(); auto kfd = MNNCreateFile(pathk.c_str()); if (kfd != INVALID_FILE) { MNNSetFileSize(kfd, convBytes); void* kMap = MNNMmapFile(kfd, convBytes); if (kMap != nullptr) { ::memcpy(kMap, mStateCache->mConvState->host(), convBytes); MNNUnmapFile(kMap, convBytes); } MNNCloseFile(kfd); } else { MNN_PRINT("CPULinearAttention: Failed to create prefix cache file: %s\n", pathk.c_str()); } // Save recurrent state (.v file) — may be empty for short_conv size_t recurrentBytes = (mStateCache->mRecurrentState.get() != nullptr) ? mStateCache->mRecurrentState->elementSize() : 0; auto vfd = MNNCreateFile(pathv.c_str()); if (vfd != INVALID_FILE) { if (recurrentBytes > 0) { MNNSetFileSize(vfd, recurrentBytes); void* vMap = MNNMmapFile(vfd, recurrentBytes); if (vMap != nullptr) { ::memcpy(vMap, mStateCache->mRecurrentState->host(), recurrentBytes); MNNUnmapFile(vMap, recurrentBytes); } } MNNCloseFile(vfd); } else { MNN_PRINT("CPULinearAttention: Failed to create prefix cache file: %s\n", pathv.c_str()); } // Snapshot the written state for in-memory eraseHistory rollback. snapshotPrefixState(mStateCache.get(), backend()); } } return NO_ERROR; } void CPULinearAttention::gated_delta_rule_mnn(const std::vector& inputs, const std::vector& outputs) { auto qkvTensor = inputs[0]; auto gateTensor = inputs[1]; auto betaTensor = inputs[2]; auto convWTensor = inputs[3]; auto outTensor = outputs[0]; const int8_t* qkvPtr = qkvTensor->host(); const int8_t* gatePtr = gateTensor->host(); const int8_t* betaPtr = betaTensor->host(); const int8_t* convWPtr = convWTensor->host(); int8_t* outPtr = outTensor->host(); const int B = qkvTensor->length(0); const int D = qkvTensor->length(1); const int L = qkvTensor->length(2); // Decode fast path: L=1, skip decay buffer, stride=1 contiguous access if (L == 1) { gated_delta_rule_decode(inputs, outputs); return; } const int H_k = mNumKHeads; const int H_v = mNumVHeads; const int d_k = mHeadKDim; const int d_v = mHeadVDim; const int key_dim = H_k * d_k; const int val_dim = H_v * d_v; const int K_conv = convWTensor->length(2); const int convStateSize = K_conv - 1; const bool useL2Norm = mUseQKL2Norm; const int gqa_factor = (H_v > H_k) ? (H_v / H_k) : 1; const int H = H_v; const int bytes = mBytes; // Get pre-allocated buffers int8_t* convPadded = mConvPadded->host(); int8_t* convOut = mConvOut->host(); int8_t* convStatePtr = mStateCache->mConvState->host(); // ─── Step 1: Depthwise Conv1D + SiLU (multi-threaded across B×D channels) ─── const int totalLen = convStateSize + L; const int totalChannels = B * D; int threadNum = static_cast(backend())->threadNumber(); // fp16 path uses per-thread fp32 temp buffers for vectorized Conv1D + SiLu float* convFp32Base = (bytes == 2) ? mConvFp32Buf->host() : nullptr; MNN_CONCURRENCY_BEGIN(tId, threadNum) { // Per-thread fp32 buffers (only used for fp16 path) float* fp32Padded = (bytes == 2) ? convFp32Base + (int)tId * (totalLen + L) : nullptr; float* fp32Out = (bytes == 2) ? fp32Padded + totalLen : nullptr; for (int idx = (int)tId; idx < totalChannels; idx += threadNum) { int d = idx % D; // 1a. Build padded input: cat(convState, qkv) for this channel int8_t* padded = convPadded + idx * totalLen * bytes; const int8_t* stateChannel = convStatePtr + idx * convStateSize * bytes; ::memcpy(padded, stateChannel, convStateSize * bytes); const int8_t* inputChannel = qkvPtr + idx * L * bytes; ::memcpy(padded + convStateSize * bytes, inputChannel, L * bytes); // 1b. Save conv state first (before we overwrite padded) const int8_t* newState = padded + (totalLen - convStateSize) * bytes; int8_t* dstState = convStatePtr + idx * convStateSize * bytes; ::memcpy(dstState, newState, convStateSize * bytes); // 1c. Conv1D + SiLU const int8_t* weight = convWPtr + d * K_conv * bytes; int8_t* out = convOut + idx * L * bytes; if (bytes == 2) { // fp16 path: convert to fp32, compute Conv1D + SiLu vectorized, convert back auto coreFn = static_cast(backend())->functions(); coreFn->MNNLowpToFp32((const int16_t*)padded, fp32Padded, totalLen); float w0 = fp32Padded[0]; // dummy, will read weights separately #ifdef __aarch64__ w0 = (float)((__fp16*)weight)[0]; float w1 = (float)((__fp16*)weight)[1]; float w2 = (float)((__fp16*)weight)[2]; float w3 = (float)((__fp16*)weight)[3]; #else float w1 = 0, w2 = 0, w3 = 0; #endif if (K_conv == 4) { int l = 0; for (; l + 3 < L; l += 4) { fp32Padded[l] = fp32Padded[l]*w0 + fp32Padded[l+1]*w1 + fp32Padded[l+2]*w2 + fp32Padded[l+3]*w3; fp32Padded[l+1] = fp32Padded[l+1]*w0 + fp32Padded[l+2]*w1 + fp32Padded[l+3]*w2 + fp32Padded[l+4]*w3; fp32Padded[l+2] = fp32Padded[l+2]*w0 + fp32Padded[l+3]*w1 + fp32Padded[l+4]*w2 + fp32Padded[l+5]*w3; fp32Padded[l+3] = fp32Padded[l+3]*w0 + fp32Padded[l+4]*w1 + fp32Padded[l+5]*w2 + fp32Padded[l+6]*w3; } for (; l < L; ++l) { fp32Padded[l] = fp32Padded[l]*w0 + fp32Padded[l+1]*w1 + fp32Padded[l+2]*w2 + fp32Padded[l+3]*w3; } } else { for (int l = 0; l < L; ++l) { float sum = 0.0f; for (int k = 0; k < K_conv; ++k) { float wk = _readElement(weight, k, bytes); sum += fp32Padded[l + k] * wk; } fp32Padded[l] = sum; } } MNNSiLu(fp32Out, fp32Padded, L); coreFn->MNNFp32ToLowp(fp32Out, (int16_t*)out, L); } else { // fp32 path: direct compute float* fPadded = (float*)padded; if (K_conv == 4) { float w0 = ((float*)weight)[0], w1 = ((float*)weight)[1]; float w2 = ((float*)weight)[2], w3 = ((float*)weight)[3]; int l = 0; for (; l + 3 < L; l += 4) { fPadded[l] = fPadded[l]*w0 + fPadded[l+1]*w1 + fPadded[l+2]*w2 + fPadded[l+3]*w3; fPadded[l+1] = fPadded[l+1]*w0 + fPadded[l+2]*w1 + fPadded[l+3]*w2 + fPadded[l+4]*w3; fPadded[l+2] = fPadded[l+2]*w0 + fPadded[l+3]*w1 + fPadded[l+4]*w2 + fPadded[l+5]*w3; fPadded[l+3] = fPadded[l+3]*w0 + fPadded[l+4]*w1 + fPadded[l+5]*w2 + fPadded[l+6]*w3; } for (; l < L; ++l) { fPadded[l] = fPadded[l]*w0 + fPadded[l+1]*w1 + fPadded[l+2]*w2 + fPadded[l+3]*w3; } } else { for (int l = 0; l < L; ++l) { float sum = 0.0f; for (int k = 0; k < K_conv; ++k) sum += fPadded[l + k] * ((float*)weight)[k]; fPadded[l] = sum; } } MNNSiLu((float*)out, fPadded, L); } } } MNN_CONCURRENCY_END(); // ─── Step 1.5: Batch exp(gate) ─── // Decay buffer is always fp32. Convert gate to fp32 if needed, then MNNExp. float* decayPtr = mDecayBuf->host(); const int gateTotalSize = B * L * H; if (bytes == 4) { float expOffset[4] = {1.0f, 0.0f, 0.0f, 0.0f}; MNNExp(decayPtr, (const float*)gatePtr, expOffset, gateTotalSize); } else { // fp16: compute exp per-element (gate is small: B*L*H) for (int i = 0; i < gateTotalSize; ++i) { decayPtr[i] = expf(_readElement(gatePtr, i, bytes)); } } // ─── Steps 2-5 fused: Split + L2Norm + Scale + Gated Delta Rule ─── const float qScale = 1.0f / sqrtf((float)d_k); auto gcore = static_cast(backend())->functions(); int8_t* rnnStatePtr = mStateCache->mRecurrentState->host(); const int totalHeads = B * H; int8_t* threadBufBase = mThreadLocalBuf->host(); // Prefill uses fused kernel (only first 2*dk+dv touched) but the per-thread // stride must match the larger allocation (decode's 2*dk+3*dv). const int perThread = 2 * d_k + 3 * d_v; MNN_CONCURRENCY_BEGIN(tId, threadNum) { int8_t* tBuf = threadBufBase + (int)tId * perThread * bytes; // Local buffers in native format (fp16 or fp32) int8_t* q_local = tBuf; int8_t* k_local = tBuf + d_k * bytes; int8_t* v_local = tBuf + 2 * d_k * bytes; for (int idx = (int)tId; idx < totalHeads; idx += threadNum) { int b = idx / H; int h = idx % H; int k_head = h / gqa_factor; int8_t* state = rnnStatePtr + idx * d_k * d_v * bytes; const int8_t* convBase = convOut + b * D * L * bytes; // Pre-compute base pointers for this head's channels const int8_t* qBase = convBase + k_head * d_k * L * bytes; const int8_t* kBase = convBase + (key_dim + k_head * d_k) * L * bytes; const int8_t* vBase = convBase + (2 * key_dim + h * d_v) * L * bytes; for (int t = 0; t < L; ++t) { // ── Step 2: Extract q_t, k_t, v_t from convOut (strided access) ── for (int i = 0; i < d_k; ++i) { float qv = _readElement(qBase, i * L + t, bytes); float kv = _readElement(kBase, i * L + t, bytes); _writeElement(q_local, i, qv, bytes); _writeElement(k_local, i, kv, bytes); } for (int i = 0; i < d_v; ++i) { float vv = _readElement(vBase, i * L + t, bytes); _writeElement(v_local, i, vv, bytes); } // ── Step 3+4: L2 Normalization + Scale (fused) ── if (useL2Norm) { const float eps = 1e-6f; float qSumSq = 0.0f, kSumSq = 0.0f; for (int i = 0; i < d_k; ++i) { float qi = _readElement(q_local, i, bytes); float ki = _readElement(k_local, i, bytes); qSumSq += qi * qi; kSumSq += ki * ki; } float qNormScale = qScale / sqrtf(qSumSq + eps); float kInvNorm = 1.0f / sqrtf(kSumSq + eps); for (int i = 0; i < d_k; ++i) { _writeElement(q_local, i, _readElement(q_local, i, bytes) * qNormScale, bytes); _writeElement(k_local, i, _readElement(k_local, i, bytes) * kInvNorm, bytes); } } else { for (int i = 0; i < d_k; ++i) { _writeElement(q_local, i, _readElement(q_local, i, bytes) * qScale, bytes); } } // ── Step 5: Gated Delta Rule recurrence ── float decay = decayPtr[b * L * H + t * H + h]; float beta_t = _readElement(betaPtr, b * L * H + t * H + h, bytes); // dot(k, q) — small reduction in fp32 for precision. float kq = 0.0f; for (int i = 0; i < d_k; ++i) { kq += _readElement(k_local, i, bytes) * _readElement(q_local, i, bytes); } // out_t is written; state S is updated in-place. int8_t* o_t = outPtr + ((b * L + t) * H * d_v + h * d_v) * bytes; gcore->MNNFusedGatedDelta((float*)state, (float*)k_local, (float*)q_local, (float*)v_local, (float*)o_t, decay, beta_t, kq, d_k, d_v); } // end timestep } // end head } MNN_CONCURRENCY_END(); } void CPULinearAttention::gated_delta_rule_decode(const std::vector& inputs, const std::vector& outputs) const { auto qkvTensor = inputs[0]; auto gateTensor = inputs[1]; auto betaTensor = inputs[2]; auto convWTensor = inputs[3]; auto outTensor = outputs[0]; const int8_t* qkvPtr = qkvTensor->host(); const int8_t* gatePtr = gateTensor->host(); const int8_t* betaPtr = betaTensor->host(); const int8_t* convWPtr = convWTensor->host(); int8_t* outPtr = outTensor->host(); const int B = qkvTensor->length(0); const int D = qkvTensor->length(1); // L == 1 guaranteed const int H_k = mNumKHeads; const int H_v = mNumVHeads; const int d_k = mHeadKDim; const int d_v = mHeadVDim; const int key_dim = H_k * d_k; const int K_conv = convWTensor->length(2); const int convStateSize = K_conv - 1; const bool useL2Norm = mUseQKL2Norm; const int gqa_factor = (H_v > H_k) ? (H_v / H_k) : 1; const int H = H_v; const int bytes = mBytes; auto* convOut = mConvOut->host(); auto* convStatePtr = mStateCache->mConvState->host(); const int threadNum = static_cast(backend())->threadNumber(); // ─── Step 1: Conv1D + SiLU (L=1, one output per channel) ─── // Each channel: dot product of [convState, input_val] with weight, then SiLU const int totalChannels = B * D; MNN_CONCURRENCY_BEGIN(tId, threadNum) { for (int idx = (int)tId; idx < totalChannels; idx += threadNum) { const int d = idx % D; // Read the single input value for this channel const float inputVal = _readElement(qkvPtr, idx, bytes); // Compute conv: dot(cat(state, input), weight) float sum = 0.0f; const int8_t* stateChannel = convStatePtr + idx * convStateSize * bytes; const int8_t* weight = convWPtr + d * K_conv * bytes; for (int k = 0; k < convStateSize; ++k) { sum += _readElement(stateChannel, k, bytes) * _readElement(weight, k, bytes); } sum += inputVal * _readElement(weight, convStateSize, bytes); // SiLU activation const float sigmoid_val = 1.0f / (1.0f + expf(-sum)); const float convResult = sum * sigmoid_val; _writeElement(convOut, idx, convResult, bytes); // Update conv state: shift left by 1, append new input for (int k = 0; k < convStateSize - 1; ++k) { const float v = _readElement(stateChannel, k + 1, bytes); _writeElement(convStatePtr + idx * convStateSize * bytes, k, v, bytes); } _writeElement(convStatePtr + idx * convStateSize * bytes, convStateSize - 1, inputVal, bytes); } } MNN_CONCURRENCY_END(); // ─── Steps 2-5 fused: QKV extraction + L2Norm + Scale + Gated Delta Rule ─── const float qScale = 1.0f / sqrtf((float)d_k); const auto gcore = static_cast(backend())->functions(); auto* rnnStatePtr = mStateCache->mRecurrentState->host(); const int totalHeads = B * H; auto* threadBufBase = mThreadLocalBuf->host(); // Decode (L=1) keeps the legacy two-call path: the fused kernel regressed // FP32 decode by ~3.5% on this shape (small d_v, single timestep). const int perThread = 2 * d_k + 3 * d_v; MNN_CONCURRENCY_BEGIN(tId, threadNum) { int8_t* tBuf = threadBufBase + (int)tId * perThread * bytes; int8_t* q_local = tBuf; int8_t* k_local = tBuf + d_k * bytes; int8_t* v_local = tBuf + 2 * d_k * bytes; int8_t* localVPred = tBuf + (2 * d_k + d_v) * bytes; int8_t* localDelta = tBuf + (2 * d_k + 2 * d_v) * bytes; for (int idx = (int)tId; idx < totalHeads; idx += threadNum) { const int b = idx / H; const int h = idx % H; const int k_head = h / gqa_factor; int8_t* state = rnnStatePtr + idx * d_k * d_v * bytes; // L=1: conv_out is [B, D, 1], stride=1, contiguous read const int8_t* convBase = convOut + b * D * bytes; const int8_t* qBase = convBase + k_head * d_k * bytes; const int8_t* kBase = convBase + (key_dim + k_head * d_k) * bytes; const int8_t* vBase = convBase + (2 * key_dim + h * d_v) * bytes; // ── Step 2: Extract q, k, v (contiguous copy, stride=1) ── ::memcpy(q_local, qBase, d_k * bytes); ::memcpy(k_local, kBase, d_k * bytes); ::memcpy(v_local, vBase, d_v * bytes); // ── Step 3+4: L2 Normalization + Scale (fused) ── if (useL2Norm) { const float eps = 1e-6f; float qSumSq = 0.0f, kSumSq = 0.0f; for (int i = 0; i < d_k; ++i) { const float qi = _readElement(q_local, i, bytes); const float ki = _readElement(k_local, i, bytes); qSumSq += qi * qi; kSumSq += ki * ki; } const float qNormScale = qScale / sqrtf(qSumSq + eps); const float kInvNorm = 1.0f / sqrtf(kSumSq + eps); for (int i = 0; i < d_k; ++i) { _writeElement(q_local, i, _readElement(q_local, i, bytes) * qNormScale, bytes); _writeElement(k_local, i, _readElement(k_local, i, bytes) * kInvNorm, bytes); } } else { for (int i = 0; i < d_k; ++i) { _writeElement(q_local, i, _readElement(q_local, i, bytes) * qScale, bytes); } } // ── Step 5: Gated Delta Rule (legacy two-call path) ── const float decay = expf(_readElement(gatePtr, b * H + h, bytes)); const float beta_t = _readElement(betaPtr, b * H + h, bytes); // Pass 1 (read-only): out_k = S^T @ k → localVPred, // out_q = S^T @ q → o_t (overwritten by correction below). int8_t* o_t = outPtr + (b * H * d_v + h * d_v) * bytes; gcore->MNNDualMatVec((float*)state, (float*)k_local, (float*)q_local, (float*)localVPred, (float*)o_t, d_k, d_v); // Analytic correction: delta = beta * (v - decay * vPred); // out = decay * out_q + dot(k,q) * delta. float kq = 0.0f; for (int i = 0; i < d_k; ++i) { kq += _readElement(k_local, i, bytes) * _readElement(q_local, i, bytes); } for (int i = 0; i < d_v; ++i) { const float vPred_i = decay * _readElement(localVPred, i, bytes); const float v_i = _readElement(v_local, i, bytes); const float delta_i = beta_t * (v_i - vPred_i); const float out_i = decay * _readElement(o_t, i, bytes) + kq * delta_i; _writeElement(localDelta, i, delta_i, bytes); _writeElement(o_t, i, out_i, bytes); } // Pass 2: S = decay * S + k ⊗ delta. gcore->MNNDecayRankOneUpdate((float*)state, (float*)k_local, (float*)localDelta, decay, d_k, d_v); } } MNN_CONCURRENCY_END(); } void CPULinearAttention::short_conv(const std::vector& inputs, const std::vector& outputs) { auto qkvTensor = inputs[0]; auto convWTensor = inputs[3]; auto outTensor = outputs[0]; const int8_t* qkvPtr = qkvTensor->host(); const int8_t* convWPtr = convWTensor->host(); int8_t* outPtr = outTensor->host(); const int B = qkvTensor->length(0); const int D = qkvTensor->length(1); // 3H const int L = qkvTensor->length(2); const int H = D / 3; const int K_conv = convWTensor->length(2); const int convStateSize = K_conv - 1; const int bytes = mBytes; int8_t* convPadded = mConvPadded->host(); int8_t* convOut = mConvOut->host(); int8_t* convStatePtr = mStateCache->mConvState->host(); int threadNum = static_cast(backend())->threadNumber(); const int totalLen = convStateSize + L; const int totalChannels = B * H; MNN_CONCURRENCY_BEGIN(tId, threadNum) { for (int idx = (int)tId; idx < totalChannels; idx += threadNum) { int b = idx / H; int h = idx % H; // 1a. Compute Bx = B_[b,h,:] * x_[b,h,:] and build padded input int8_t* padded = convPadded + idx * totalLen * bytes; const int8_t* stateChannel = convStatePtr + idx * convStateSize * bytes; ::memcpy(padded, stateChannel, convStateSize * bytes); for (int l = 0; l < L; ++l) { float b_val = _readElement(qkvPtr, b * D * L + h * L + l, bytes); float x_val = _readElement(qkvPtr, b * D * L + (2 * H + h) * L + l, bytes); _writeElement(padded, convStateSize + l, b_val * x_val, bytes); } // 1b. Depthwise Conv1D (no SiLU) int8_t* out = convOut + idx * L * bytes; for (int l = 0; l < L; ++l) { float sum = 0.0f; for (int k = 0; k < K_conv; ++k) { sum += _readElement(padded, l + k, bytes) * _readElement(convWPtr, h * K_conv + k, bytes); } _writeElement(out, l, sum, bytes); } // 1c. Update conv state const int8_t* newState = padded + (totalLen - convStateSize) * bytes; int8_t* dstState = convStatePtr + idx * convStateSize * bytes; ::memcpy(dstState, newState, convStateSize * bytes); } } MNN_CONCURRENCY_END(); // Step 2: y = C_ * conv_out, transpose to output [B, L, 1, H] MNN_CONCURRENCY_BEGIN(tId, threadNum) { for (int idx = (int)tId; idx < totalChannels; idx += threadNum) { int b = idx / H; int h = idx % H; for (int l = 0; l < L; ++l) { float c_val = _readElement(qkvPtr, b * D * L + (H + h) * L + l, bytes); float conv_val = _readElement(convOut, idx * L + l, bytes); _writeElement(outPtr, (b * L + l) * H + h, c_val * conv_val, bytes); } } } MNN_CONCURRENCY_END(); } bool CPULinearAttention::onClone(Backend* bn, const Op* op, Execution** dst) { if (nullptr == dst) { return true; } auto tmp = new CPULinearAttention(bn, op); // Share persistent state buffers between prefill and decode Executions tmp->mStateCache = mStateCache; *dst = tmp; return true; } CPULinearAttention::CPULinearAttention(Backend *backend, const MNN::Op* op) : Execution(backend) { 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(); mBytes = static_cast(backend)->functions()->bytes; mStateCache.reset(new StateCache); mMeta = (KVMeta*)(backend->getMetaPtr()); mPrefixCacheDir = static_cast(backend)->getRuntime()->hint().prefixcacheDirPath; } CPULinearAttention::~CPULinearAttention() { } class CPULinearAttentionCreator : public CPUBackend::Creator { public: virtual Execution* onCreate(const std::vector& inputs, const std::vector& outputs, const MNN::Op* op, Backend* backend) const override { return new CPULinearAttention(backend, op); } }; REGISTER_CPU_OP_CREATOR_TRANSFORMER(CPULinearAttentionCreator, OpType_LinearAttention); } // namespace MNN #endif // MNN_SUPPORT_TRANSFORMER_FUSE