// // AttentionTest.cpp // MNNTests // // Created by MNN on 2024/07/23. // Copyright © 2018, Alibaba Group Holding Limited // #ifdef MNN_SUPPORT_TRANSFORMER_FUSE #include #include #include #include "core/OpCommonUtils.hpp" #include "MNNTestSuite.h" #include "TestUtils.h" #include #include #include using namespace MNN::Express; using MNN::KVMeta; int NumHead = 16; int KvNumHead = 2; int HeadDim = 128; const float diff_threshold = 0.001; const float diff_percent_threshold = 0.1; const int pastLength = 101; #define GENERATE_TOKENS 128 static KVMeta gMeta; static std::shared_ptr _makeAttentionModule(int attentionMode = 8, bool outputC4 = false) { auto Q = _Input(); auto K = _Input(); auto V = _Input(); auto mask = _Input(); std::shared_ptr attention(new MNN::OpT); attention->type = MNN::OpType_Attention; attention->main.type = MNN::OpParameter_AttentionParam; attention->main.value = new MNN::AttentionParamT; attention->main.AsAttentionParam()->kv_cache = true; attention->main.AsAttentionParam()->output_c4 = outputC4; auto o = Variable::create(Expr::create(attention.get(), {Q, K, V, mask})); auto buffer = Variable::save({o}); MNN::ScheduleConfig config; auto status = MNNTestSuite::get()->pStaus; config.type = (MNNForwardType)status.forwardType; MNN::BackendConfig bnConfig; bnConfig.memory = (MNN::BackendConfig::MemoryMode)status.memory; bnConfig.precision = (MNN::BackendConfig::PrecisionMode)status.precision; bnConfig.power = (MNN::BackendConfig::PowerMode)status.power; config.backendConfig = &bnConfig; config.numThread = 1; std::shared_ptr rtmgr(Executor::RuntimeManager::createRuntimeManager(config)); rtmgr->setHintPtr(MNN::Interpreter::KVCACHE_INFO, &gMeta); rtmgr->setHint(MNN::Interpreter::ATTENTION_OPTION, attentionMode); std::shared_ptr m(Module::load({}, {}, (uint8_t*)buffer.data(), buffer.size(), rtmgr)); return m; } struct KVCache { VARP pastK; VARP pastV; VARP pastMask; int current = 0; KVCache() { pastK = _Input({1, KvNumHead, 1, pastLength, HeadDim}, NCHW); pastV = _Input({1, KvNumHead, 1, pastLength, HeadDim}, NCHW); pastMask = _Input({pastLength}, NCHW); ::memset(pastK->writeMap(), 0, pastK->getInfo()->size * sizeof(float)); ::memset(pastV->writeMap(), 0, pastK->getInfo()->size * sizeof(float)); for (int v=0; vwriteMap()[v] = std::numeric_limits::lowest(); } } }; static VARP _computeAttentionExpr(VARP Q, VARP K, VARP V, VARP mask, KVCache cache) { auto qinfo = Q->getInfo(); auto kinfo = K->getInfo(); auto vinfo = V->getInfo(); auto seqLength = qinfo->dim[1]; auto numHead = qinfo->dim[2]; auto headDim = qinfo->dim[3]; auto kvNumHead = kinfo->dim[2]; auto batch = qinfo->dim[0]; auto group = numHead / kvNumHead; if (mask->getInfo()->type.code == halide_type_int) { mask = (_Scalar(1.0) - _Cast(mask)) * _Scalar(std::numeric_limits::lowest()); } Q = _Reshape(Q, {batch, seqLength, kvNumHead,group, headDim}); Q = _Transpose(Q, {0, 2, 3, 1, 4}); K = _Reshape(K, {batch, seqLength, kvNumHead, 1, headDim}); K = _Transpose(K, {0, 2, 3, 1, 4}); auto scale = 1.0f / sqrtf(headDim); K = K * _Scalar(scale); K.fix(VARP::CONSTANT); auto QK = _MatMul(Q, K, false, true); // [batch, kvNumHead, group , seq_len, seq_len] QK = QK + mask; auto QKPast = _MatMul(Q, cache.pastK, false, true); QKPast = QKPast + cache.pastMask; QK = _Concat({QKPast, QK}, -1); QK = _Softmax(QK, -1); V = _Reshape(V, {batch, seqLength, kvNumHead, 1, headDim}); V = _Transpose(V, {0, 2, 3, 1, 4}); V.fix(VARP::CONSTANT); auto totalV = _Concat({cache.pastV, V}, 3); auto QKV = _MatMul(QK, totalV, false, false); auto info = QKV->getInfo(); auto O = _Transpose(QKV, {0, 3, 1, 2, 4}); O = _Reshape(O, {batch, seqLength, -1}); O.fix(VARP::CONSTANT); // Update KVCache for (int y=0; ywriteMap() + y * pastLength * headDim + cache.current * headDim, K->readMap() + y * seqLength * headDim, seqLength * headDim * sizeof(float)); ::memcpy(cache.pastV->writeMap() + y * pastLength * headDim + cache.current * headDim, V->readMap() + y * seqLength * headDim, seqLength * headDim * sizeof(float)); } for (int i=0; iwriteMap()[i+cache.current] = 0.0f; } cache.current += seqLength; return O; } static std::vector< std::vector< std::vector > > generateRandTensor(int C, int H, int W, int precision) { std::vector< std::vector< std::vector > > a; a.resize(C); for (int i = 0; i < C; i++) { a[i].resize(H); for (int j = 0; j < H; j++) { a[i][j].resize(W); for (int k = 0; k < W; k++) { if (precision == 2) { a[i][j][k] = ((i + j + k) % 10) * 0.002; } else { a[i][j][k] = ((i + j + k) % 10) * 0.16 - 5.6; } } } } return a; } VARP vector_to_var(std::vector< std::vector< std::vector > > & a) { int C = a.size(); int H = a[0].size(); int W = a[0][0].size(); VARP var = _Input({1, C, H, W}, NCHW, halide_type_of()); float * ptr = var->writeMap(); for (int i = 0; i < C; i++) { for (int j = 0; j < H; j++) { for (int k = 0; k < W; k++) { ptr[i * H * W + j * W + k] = a[i][j][k]; } } } var->unMap(); return var; } VARP vector_to_c4_value(std::vector< std::vector< std::vector > > & a) { int seqLen = a.size(); int kvNumHead = a[0].size(); int headDim = a[0][0].size(); int channel = kvNumHead * headDim; std::vector packed(((channel + 3) / 4) * seqLen * 4, 0.0f); for (int s = 0; s < seqLen; ++s) { for (int h = 0; h < kvNumHead; ++h) { for (int d = 0; d < headDim; ++d) { int c = h * headDim + d; packed[(c / 4) * seqLen * 4 + s * 4 + (c % 4)] = a[s][h][d]; } } } VARP var = _Input({seqLen, channel, 1, 1}, NC4HW4, halide_type_of()); ::memcpy(var->writeMap(), packed.data(), packed.size() * sizeof(float)); var->unMap(); return var; } VARP vector_to_var(std::vector< std::vector > & a) { int H = a.size(); int W = a[0].size(); VARP var = _Input({1, 1, H, W}, NCHW, halide_type_of()); int * ptr = var->writeMap(); for (int i = 0; i < H; i++) { for (int j = 0; j < W; j++) { ptr[i * W + j] = a[i][j]; } } var->unMap(); return var; } static std::vector< std::vector< std::vector > > computeAttention ( std::vector< std::vector< std::vector > > & query, std::vector< std::vector< std::vector > > & key, std::vector< std::vector< std::vector > > & value, std::vector< std::vector > & mask, int seq_len, int kv_seq_len ) { int group_size = NumHead / KvNumHead; std::vector< std::vector< std::vector > > output(seq_len); for (int i = 0; i < seq_len; i++) { output[i].resize(NumHead); for (int j = 0; j < NumHead; j++) { output[i][j].resize(HeadDim); } } for (int h = 0; h < NumHead; h++) { int kv_h = h / group_size; /*---- Q * K ----*/ std::vector< std::vector > qk(seq_len, std::vector(kv_seq_len, 0.0f)); for (int i = 0; i < seq_len; i++) { for (int j = 0; j < kv_seq_len; j++) { qk[i][j] = 0.0f; for (int k = 0; k < HeadDim; k++) { qk[i][j] += query[i][h][k] * key[j][kv_h][k]; } } } /*---- Mask QK ----*/ if(mask.size() > 0) { float scale = 1.0 / sqrt(HeadDim); if (mask[0].size() == seq_len) { auto diff = kv_seq_len - seq_len; for (int i = 0; i < seq_len; i++) { for (int j = 0; j < seq_len; j++) { qk[i][j+diff] = qk[i][j+diff] * scale + (1.f - mask[i][j]) * std::numeric_limits::lowest(); } } } else { for (int i = 0; i < seq_len; i++) { for (int j = 0; j < kv_seq_len; j++) { qk[i][j] = qk[i][j] * scale + (1.f - mask[i][j]) * std::numeric_limits::lowest(); } } } } else { float scale = 1.0 / sqrt(HeadDim); for (int i = 0; i < seq_len; i++) { for (int j = 0; j < kv_seq_len; j++) { qk[i][j] *= scale; } } } /*---- Softmax QK ----*/ for (int i = 0; i < seq_len; i++) { float maxValue = qk[i][0]; for (int j = 1; j < kv_seq_len; j++) { maxValue = ALIMAX(maxValue, qk[i][j]); } for (int j = 0; j < kv_seq_len; j++) { qk[i][j] -= maxValue; } float sum = 0.0f; for (int j = 0; j < kv_seq_len; j++) { sum += exp(qk[i][j]); } for (int j = 0; j < kv_seq_len; j++) { qk[i][j] = exp(qk[i][j]) / sum; } } /*---- QK * V ----*/ for (int i = 0; i < seq_len; i++) { for (int j = 0; j < HeadDim; j++) { output[i][h][j] = 0.0f; for (int k = 0; k < kv_seq_len; k++) { output[i][h][j] += qk[i][k] * value[k][kv_h][j]; } } } } return output; } class NaiveAttention { private: std::vector< std::vector< std::vector > > mPastKey, mPastValue; int mPastLen; public: NaiveAttention() : mPastLen(0) {} ~NaiveAttention() = default; std::vector< std::vector< std::vector > > onExecute ( std::vector< std::vector< std::vector > > & query, std::vector< std::vector< std::vector > > & key, std::vector< std::vector< std::vector > > & value, std::vector< std::vector > & mask, int seq_len ) { for (int i = 0; i < seq_len; i++) { mPastKey.push_back(key[i]); mPastValue.push_back(value[i]); } mPastLen += seq_len; return computeAttention(query, mPastKey, mPastValue, mask, seq_len, mPastLen); } }; class AttentionTest : public MNNTestCase { protected: std::vector< std::vector< std::vector > > query; std::vector< std::vector< std::vector > > key; std::vector< std::vector< std::vector > > value; std::vector< std::vector > mask; std::vector< std::vector< std::vector > > expected_result; VARP Query, Key, Value, Mask, Output; VARP Query1, Key1, Value1, Mask1; public: AttentionTest() = default; virtual ~AttentionTest() = default; void generateInput(int seq_len, int precision, bool genDecodeInput = false) { query = generateRandTensor(seq_len, NumHead, HeadDim, precision); key = generateRandTensor(seq_len, KvNumHead, HeadDim, precision); value = generateRandTensor(seq_len, KvNumHead, HeadDim, precision); Query = vector_to_var(query); Key = vector_to_var(key); Value = vector_to_var(value); if (genDecodeInput) { auto vecquery = generateRandTensor(1, NumHead, HeadDim, precision); auto veckey = generateRandTensor(1, KvNumHead, HeadDim, precision); auto vecvalue = generateRandTensor(1, KvNumHead, HeadDim, precision); Query1 = vector_to_var(vecquery); Key1 = vector_to_var(veckey); Value1 = vector_to_var(vecvalue); } } void generateChunkMask(int seq_len, int kv_seq_len, int chunk_size, bool genDecodeInput = false) { // 防止除以0 if (chunk_size <= 0) chunk_size = 1; mask.resize(seq_len); // 计算历史长度 (Gap),用于处理 KV 长度大于 Seq 长度的情况 (Right Alignment) // j < gap 的部分通常被视为 History,默认可见 int gap = kv_seq_len - seq_len; for (int i = 0; i < seq_len; i++) { mask[i].resize(kv_seq_len); // --- 核心逻辑对应 --- // MNN Expr: auto N = _Divide(i, rankVar) * rankVar + rankVar; // i 是当前行 (Query),计算当前块的右边界 (不包含) // 比如 rank=2, i=0, block_end_rel=2; i=2, block_end_rel=4 int block_end_rel = (i / chunk_size) * chunk_size + chunk_size; for (int j = 0; j < kv_seq_len; j++) { // 将 j 转换为相对于当前 seq_len 的坐标 int j_rel = j - gap; if (j_rel < 0) { // 情况 1: j 在 Gap 区域 (历史 KV Cache) // 通常历史数据对当前所有 Token 都是可见的 mask[i][j] = 1; } else { // 情况 2: j 在当前处理的序列范围内 // 对应 MNN Expr: _Less(j, N) if (j_rel < block_end_rel) { mask[i][j] = 1; } else { mask[i][j] = 0; } } } } // 转为 VARP 并处理成 -inf / 0.0 格式 Mask = vector_to_var(mask); Mask = (_Scalar(1.0) - _Cast(Mask)) * _Scalar(std::numeric_limits::lowest()); // Decode Input 部分通常保持全 1 (即看清所有历史),或者根据需求修改 if (genDecodeInput) { std::vector> vecmask; vecmask.resize(1); vecmask[0].resize(gMeta.previous + 1); for (int i = 0; i < gMeta.previous + 1; ++i) { vecmask[0][i] = 1; } Mask1 = vector_to_var(vecmask); Mask1 = (_Scalar(1.0) - _Cast(Mask1)) * _Scalar(std::numeric_limits::lowest()); } } void generateMask(int seq_len, int kv_seq_len, bool genDecodeInput = false) { mask.resize(seq_len); for (int i = 0; i < seq_len; i++) { mask[i].resize(kv_seq_len); for (int j = 0; j < kv_seq_len; j++) { if (j - i <= kv_seq_len - seq_len) { mask[i][j] = 1; } else { mask[i][j] = 0; } } } Mask = _Input({}, NCHW, halide_type_of()); Mask1 = _Input({}, NCHW, halide_type_of()); Mask->writeMap()[0] = 0.0f; Mask1->writeMap()[0] = 0.0f; } bool compareResult(int seq_len) { const float * resultPtr = Output->readMap(); for (int i = 0; i < seq_len; i++) { for (int j = 0; j < NumHead; j++) { for (int k = 0; k < HeadDim; k++) { float diff = fabs(resultPtr[i * NumHead * HeadDim + j * HeadDim + k] - expected_result[i][j][k]); float diff_percent = fabs(diff / expected_result[i][j][k]); if (diff > diff_threshold && diff_percent > diff_percent_threshold) { printf("Result Mismatch: expected %lf but got %lf in CPU Attention Test\n", expected_result[i][j][k], resultPtr[i * NumHead * HeadDim + j * HeadDim + k]); printf("Error Position: Output[%d][%d][%d]\n", i, j, k); return false; } } } } Output->unMap(); return true; } virtual bool run(int precision) { srand(2024); // unit test 1 { std::shared_ptr naiveAttention(new NaiveAttention); std::shared_ptr attention(new MNN::OpT); attention->type = MNN::OpType_Attention; attention->main.type = MNN::OpParameter_AttentionParam; attention->main.value = new MNN::AttentionParamT; attention->main.AsAttentionParam()->kv_cache = true; int seq_len = 10; generateInput(seq_len, precision); generateMask(seq_len, seq_len); expected_result = naiveAttention->onExecute(query, key, value, mask, seq_len); auto attn = _makeAttentionModule(); gMeta.add = seq_len; Output = attn->onForward({Query, Key, Value, Mask})[0]; gMeta.sync(); KVCache kvCache; bool pass = compareResult(seq_len); if (!pass) { printf("Error: LowerTriangular Attention with kv_cache unit test failed!\n"); return false; } /* generate mask expr */ /* generate mask expr */ auto MaskExpr = vector_to_var(mask); MaskExpr = (_Scalar(1.0) - _Cast(MaskExpr)) * _Scalar(std::numeric_limits::lowest()); Output = _computeAttentionExpr(Query, Key, Value, MaskExpr, kvCache); pass = compareResult(seq_len); if (!pass) { FUNC_PRINT(1); return false; } // naiveAttention with history is error, use expr to test Output = _computeAttentionExpr(Query, Key, Value, MaskExpr, kvCache); gMeta.add = seq_len; auto output2 = attn->onForward({Query, Key, Value, Mask})[0]; gMeta.sync(); auto diff = _ReduceMax(output2 - Output)->readMap()[0]; if (diff >= 0.01f) { FUNC_PRINT_ALL(diff, f); return false; } } // test2 { std::shared_ptr naiveAttention(new NaiveAttention); std::shared_ptr attention(new MNN::OpT); attention->type = MNN::OpType_Attention; attention->main.type = MNN::OpParameter_AttentionParam; attention->main.value = new MNN::AttentionParamT; attention->main.AsAttentionParam()->kv_cache = true; int seq_len = 10; generateInput(seq_len, precision); generateChunkMask(seq_len, seq_len, 2); expected_result = naiveAttention->onExecute(query, key, value, mask, seq_len); auto attn = _makeAttentionModule(); gMeta.previous = 0; gMeta.add = seq_len; Output = attn->onForward({Query, Key, Value, Mask})[0]; gMeta.sync(); KVCache kvCache; bool pass = compareResult(seq_len); if (!pass) { printf("Error: Not LowerTriangular Attention with kv_cache unit test failed!\n"); return false; } Output = _computeAttentionExpr(Query, Key, Value, Mask, kvCache); pass = compareResult(seq_len); if (!pass) { FUNC_PRINT(1); return false; } // naiveAttention with history is error, use expr to test Output = _computeAttentionExpr(Query, Key, Value, Mask, kvCache); gMeta.add = seq_len; auto output2 = attn->onForward({Query, Key, Value, Mask})[0]; gMeta.sync(); auto diff = _ReduceMax(output2 - Output)->readMap()[0]; if (diff >= 0.01f) { FUNC_PRINT_ALL(diff, f); return false; } } // unit test 3 { auto rtInfo = ExecutorScope::Current()->getRuntime().first; bool cpuInfer = true; for(auto &rt : rtInfo) { if(rt.first != MNN_FORWARD_CPU) { cpuInfer = false; break; } } if(cpuInfer) { // TODO: CPU support kv_cache == false return true; } // MNN: kv_cache=false also falls back to CPU on OpenCL with // MNN_GPU_MEMORY_IMAGE (no IMAGE-memtype Attention creator) and // on Vulkan, so it hits the same CPUAttention "kv_cache == false" // TODO and crashes. Skip until the CPU fallback is completed. for(auto &rt : rtInfo) { if(rt.first == MNN_FORWARD_OPENCL || rt.first == MNN_FORWARD_VULKAN) { return true; } } std::shared_ptr naiveAttention(new NaiveAttention); std::shared_ptr attention(new MNN::OpT); attention->type = MNN::OpType_Attention; attention->main.type = MNN::OpParameter_AttentionParam; attention->main.value = new MNN::AttentionParamT; attention->main.AsAttentionParam()->kv_cache = false; int seq_len = 128; generateInput(seq_len, precision); mask.clear(); expected_result = naiveAttention->onExecute(query, key, value, mask, seq_len); Output = Variable::create(Expr::create(attention.get(), {Query, Key, Value})); bool pass = compareResult(seq_len); if (!pass) { printf("Error: Attention without kv_cacheunit test failed!\n"); return false; } } return true; } }; class SpeedAttentionTest : public AttentionTest { protected: std::vector< std::vector< std::vector > > query; std::vector< std::vector< std::vector > > key; std::vector< std::vector< std::vector > > value; std::vector< std::vector > mask; std::vector< std::vector< std::vector > > expected_result; public: SpeedAttentionTest() = default; virtual ~SpeedAttentionTest() = default; virtual bool run(int precision) { std::vector seqs = {4096}; std::shared_ptr naiveAttention(new NaiveAttention); std::shared_ptr attention(new MNN::OpT); attention->type = MNN::OpType_Attention; attention->main.type = MNN::OpParameter_AttentionParam; attention->main.value = new MNN::AttentionParamT; attention->main.AsAttentionParam()->kv_cache = true; /* 3 attention module */ std::vector quantQKV = {8, 9, 10}; std::vector testNames = {"float qkv", "quant qk", "quant qkv"}; for (int n = 0; n < seqs.size(); ++n) { int seq_len = seqs[n]; MNN_PRINT(">>> seq_len=%d, decode_len=%d\n", seq_len, GENERATE_TOKENS); generateInput(seqs[n], precision, true); generateMask(seqs[n], seq_len, true); for (int m = 0; m < testNames.size(); ++m) { gMeta.previous = 0; gMeta.add = seq_len; auto _module = _makeAttentionModule(quantQKV[m]); MNN::Timer t1; for (int x = 0; x < 5; ++x) { Output = _module->onForward({Query, Key, Value, Mask})[0]; } auto time = (float)t1.durationInUs() / 1000.0f / 5.f; MNN_PRINT("%s: prefill cost = %.2f\n", testNames[m].c_str(), time); gMeta.sync(); MNN::Timer t2; for (int x = 0; x < GENERATE_TOKENS; ++x) { gMeta.add = 1; auto output2 = _module->onForward({Query1, Key1, Value1, Mask1})[0]; gMeta.sync(); } time = (float)t2.durationInUs() / 1000.0f; MNN_PRINT("%s: decode cost = %f\n", testNames[m].c_str(), time); } } return true; } }; MNNTestSuiteRegister(AttentionTest, "op/attention"); class AttentionC4Test : public AttentionTest { public: AttentionC4Test() = default; virtual ~AttentionC4Test() = default; bool compareC4Result(int seqLen) { const float* resultPtr = Output->readMap(); const int hidden = NumHead * HeadDim; std::vector actual(seqLen * hidden); std::vector expected(seqLen * hidden); for (int i = 0; i < seqLen; ++i) { for (int h = 0; h < NumHead; ++h) { for (int d = 0; d < HeadDim; ++d) { int c = h * HeadDim + d; int c4Index = (c % 4) + 4 * i + 4 * seqLen * (c / 4); int logicalIndex = i * hidden + c; actual[logicalIndex] = resultPtr[c4Index]; expected[logicalIndex] = expected_result[i][h][d]; } } } if (!checkVectorByRelativeError(actual.data(), expected.data(), actual.size(), 0.02f)) { MNN_ERROR("AttentionC4Test failed!\n"); return false; } return true; } bool runOne(int seqLen, int precision) { std::shared_ptr naiveAttention(new NaiveAttention); generateInput(seqLen, precision); generateMask(seqLen, seqLen); expected_result = naiveAttention->onExecute(query, key, value, mask, seqLen); gMeta.previous = 0; gMeta.remove = 0; gMeta.add = seqLen; auto attn = _makeAttentionModule(8, true); Output = attn->onForward({Query, Key, Value, Mask})[0]; gMeta.sync(); if (!compareC4Result(seqLen)) { return false; } auto valueC4 = vector_to_c4_value(value); gMeta.previous = 0; gMeta.remove = 0; gMeta.add = seqLen; auto attnValueC4 = _makeAttentionModule(8, true); Output = attnValueC4->onForward({Query, Key, valueC4, Mask})[0]; gMeta.sync(); return compareC4Result(seqLen); } virtual bool run(int precision) { srand(2024); return runOne(10, precision) && runOne(32, precision); } }; MNNTestSuiteRegister(AttentionC4Test, "op/attention_c4"); MNNTestSuiteRegister(SpeedAttentionTest, "speed/attention"); #endif