#include #include #include "MNNTestSuite.h" #include "TestUtils.h" #include "core/TensorUtils.hpp" using namespace MNN; using namespace MNN::Express; static VARPS makeComplexGraph(VARP x) { // Input: NCHW float, shape {1, 4, 32, 32} // Graph intent: // - Introduce multiple ops (convert/conv/pool/concat/transpose) to increase // intermediate allocations. // - Keep an early large tensor as an output (Aux) to make later output tensor // more likely to be allocated with non-zero offset on Metal. auto x4 = _Convert(x, NC4HW4); auto c0 = _Conv(0.01f, 0.0f, x4, {4, 8}, {3, 3}, SAME, {1, 1}, {1, 1}, 1); c0 = _Relu(c0); auto maxP = _MaxPool(c0, {2, 2}, {2, 2}, VALID); auto aveP = _AvePool(c0, {2, 2}, {2, 2}, VALID); auto aux = _Concat({maxP, aveP}, 1); aux->setName("Aux"); auto c1 = _Conv(0.02f, 0.01f, aux, {16, 4}, {1, 1}, SAME, {1, 1}, {1, 1}, 1); c1 = _Relu6(c1); auto y = _Convert(c1, NCHW); y = _Transpose(y, {0, 2, 3, 1}); y = _Transpose(y, {0, 3, 1, 2}); y = y + _Scalar(1.0f); y = _ReduceSum(y, {2}, true); y->setName("Output"); auto s = _Shape(y); s->setName("Shape"); return {aux, y, s}; } static VARP makeInput(float base) { auto x = _Input({1, 4, 32, 32}, NCHW, halide_type_of()); auto ptr = x->writeMap(); for (int i = 0; i < x->getInfo()->size; ++i) { ptr[i] = base + (float)i * 0.001f; } x->unMap(); return x; } static bool isMetalRuntime() { auto rtInfo = Express::ExecutorScope::Current()->getRuntime(); if (rtInfo.first.empty()) { return false; } return rtInfo.first.begin()->first == MNN_FORWARD_METAL; } class StaticModuleOutputReuseTest : public MNNTestCase { public: bool run(int precision) override { auto executor = cloneCurrentExecutor(); ExecutorScope scope(executor); std::vector buffer; { auto x = _Input({1, 4, 32, 32}, NCHW, halide_type_of()); x->setName("Input"); auto outputs = makeComplexGraph(x); buffer = Variable::save(outputs); } Module::Config config; config.shapeMutable = true; std::shared_ptr module( Module::load({"Input"}, {"Aux", "Output", "Shape"}, (const uint8_t*)buffer.data(), buffer.size(), &config), Module::destroy); if (nullptr == module) { return false; } { auto x = _Input({1, 4, 320, 320}, NCHW, halide_type_of()); auto ptr = x->writeMap(); module->onForward({x}); } auto input0 = makeInput(0.0f); auto outputs0 = module->onForward({input0}); if (outputs0.size() != 3) { return false; } auto aux0Info = outputs0[0]->getInfo(); auto output0Info = outputs0[1]->getInfo(); auto shape0Info = outputs0[2]->getInfo(); if (nullptr == aux0Info || nullptr == output0Info || nullptr == shape0Info) { return false; } if (aux0Info->dim != std::vector({1, 16, 16, 16})) { return false; } if (output0Info->dim != std::vector({1, 4, 1, 16})) { return false; } if (shape0Info->dim != std::vector({4})) { return false; } int expectedShape[4] = {1, 4, 1, 16}; auto shapeResult0 = outputs0[2]->readMap(); if (!checkVector(shapeResult0, expectedShape, 4, 0)) { return false; } // Snapshot first inference results. We'll verify they remain unchanged after // the next inference (to catch output buffer reuse issues). std::vector aux0Snapshot(aux0Info->size); std::vector output0Snapshot(output0Info->size); std::vector shape0Snapshot(4); { auto auxPtr = outputs0[0]->readMap(); auto outPtr = outputs0[1]->readMap(); for (int i = 0; i < aux0Info->size; ++i) { aux0Snapshot[i] = auxPtr[i]; } for (int i = 0; i < output0Info->size; ++i) { output0Snapshot[i] = outPtr[i]; } for (int i = 0; i < 4; ++i) { shape0Snapshot[i] = shapeResult0[i]; } } auto aux0Tensor = outputs0[0]->getTensor(); auto output0Tensor = outputs0[1]->getTensor(); auto shape0Tensor = outputs0[2]->getTensor(); if (nullptr == aux0Tensor || nullptr == output0Tensor || nullptr == shape0Tensor) { return false; } auto aux0DescribeOrigin = TensorUtils::getDescribeOrigin(aux0Tensor); auto output0DescribeOrigin = TensorUtils::getDescribeOrigin(output0Tensor); auto shape0DescribeOrigin = TensorUtils::getDescribeOrigin(shape0Tensor); if (nullptr == aux0DescribeOrigin || nullptr == output0DescribeOrigin || nullptr == shape0DescribeOrigin) { return false; } int aux0Offset = aux0DescribeOrigin->offset; int output0Offset = output0DescribeOrigin->offset; int shape0Offset = shape0DescribeOrigin->offset; if (isMetalRuntime() && output0Offset <= 0) { // Not a hard assert: offset depends on allocator strategy / platform. // But the model is designed to make output buffer more likely to be a // non-zero slice inside a shared MTLBuffer. MNN_PRINT("[StaticModuleOutputReuseTest] Metal output offset=%d (aux=%d, shape=%d)\n", output0Offset, aux0Offset, shape0Offset); } auto input1 = makeInput(10.0f); auto outputs1 = module->onForward({input1}); for (int i=0; i<10; ++i) { outputs1 = module->onForward({input1}); } if (outputs1.size() != 3) { return false; } auto aux1Info = outputs1[0]->getInfo(); auto output1Info = outputs1[1]->getInfo(); auto shape1Info = outputs1[2]->getInfo(); if (nullptr == aux1Info || nullptr == output1Info || nullptr == shape1Info) { return false; } if (aux1Info->dim != std::vector({1, 16, 16, 16})) { return false; } if (output1Info->dim != std::vector({1, 4, 1, 16})) { return false; } if (shape1Info->dim != std::vector({4})) { return false; } auto shapeResult1 = outputs1[2]->readMap(); if (!checkVector(shapeResult1, expectedShape, 4, 0)) { return false; } // Ensure the previous forward's outputs are still valid, and their offsets // stay unchanged after the next forward. auto aux0InfoAfter = outputs0[0]->getInfo(); auto output0InfoAfter = outputs0[1]->getInfo(); auto shape0InfoAfter = outputs0[2]->getInfo(); if (nullptr == aux0InfoAfter || nullptr == output0InfoAfter || nullptr == shape0InfoAfter) { return false; } if (aux0InfoAfter->dim != std::vector({1, 16, 16, 16})) { return false; } if (output0InfoAfter->dim != std::vector({1, 4, 1, 16})) { return false; } if (shape0InfoAfter->dim != std::vector({4})) { return false; } // Ensure the previous inference results remain unchanged after the next forward. if (!checkVector(outputs0[0]->readMap(), aux0Snapshot.data(), (int)aux0Snapshot.size(), 1e-4f)) { return false; } if (!checkVector(outputs0[1]->readMap(), output0Snapshot.data(), (int)output0Snapshot.size(), 1e-4f)) { return false; } if (!checkVector(outputs0[2]->readMap(), shape0Snapshot.data(), (int)shape0Snapshot.size(), 0)) { return false; } auto aux0DescribeAfterOrigin = TensorUtils::getDescribeOrigin(outputs0[0]->getTensor()); auto output0DescribeAfterOrigin = TensorUtils::getDescribeOrigin(outputs0[1]->getTensor()); auto shape0DescribeAfterOrigin = TensorUtils::getDescribeOrigin(outputs0[2]->getTensor()); if (nullptr == aux0DescribeAfterOrigin || nullptr == output0DescribeAfterOrigin || nullptr == shape0DescribeAfterOrigin) { return false; } if (aux0Offset != aux0DescribeAfterOrigin->offset) { return false; } if (output0Offset != output0DescribeAfterOrigin->offset) { return false; } if (shape0Offset != shape0DescribeAfterOrigin->offset) { return false; } return true; } }; MNNTestSuiteRegister(StaticModuleOutputReuseTest, "expr/StaticModuleOutputReuseTest");