Tencent / ncnn

ncnn is a high-performance neural network inference framework optimized for the mobile platform
Other
20.17k stars 4.15k forks source link

我这边人脸关键点检测和人脸特征检测模型在安卓端:10000次相同输入的压力测试时,出现了**偶发性**的推理结果不一致的情况,经测试分析**经ncnn库预处理后的Mat输入是一致的**,**经过模型推理出现了差异**,麻烦帮忙分析一下: #3143

Closed bestpower closed 1 year ago

bestpower commented 3 years ago

我这边人脸关键点检测和人脸特征检测模型在安卓端:10000次相同输入的压力测试时,出现了偶发性的推理结果不一致的情况,经测试分析经ncnn库预处理后的Mat输入是一致的经过模型推理出现了差异,麻烦帮忙分析一下: 关键点推理核心代码:

        ......
        //输入Bitmap转换
        ncnn::Mat ncnn_img = ncnn::Mat::from_android_bitmap_resize(env, obj_bitmap, ncnn::Mat::PIXEL_BGR2RGB, 112, 112);
        //归一化
        const float mean_vals[3] = {0.485f*255.f, 0.456f*255.f, 0.406f*255.f};
        const float norm_vals[3] = {1/0.229f/255.f, 1/0.224f/255.f, 1/0.225f/255.f};
        ncnn_img.substract_mean_normalize(mean_vals, norm_vals);
        ////人脸关键点检测
        ncnn::Extractor ex = LandmarkDetnet.create_extractor();
        ex.input(face68Landmark_opt_param_id::BLOB_data, ncnn_img);                         // input node
        ncnn::Mat out;//输出136维float数组
        int status = ex.extract(face68Landmark_opt_param_id::BLOB_landmark, out);     // output node
        ......

特征推理关键代码:

        ......
        //输入Bitmap转换
        ncnn::Mat ncnn_img = ncnn::Mat::from_android_bitmap(env, obj_bitmap, ncnn::Mat::PIXEL_BGR2RGB);
        //人脸对齐
        ncnn::Mat det1 = mRecognize->preprocess(ncnn_img, 5Landmarks);
        //人脸特征提取
        ncnn::Extractor ex = Recognet.create_extractor();
        ex.input(faceFeature_opt_param_id::BLOB_data, det1 );                      // input node
        ncnn::Mat out;//输出128维float数组
        int status = ex.extract(faceFeature_opt_param_id::BLOB_fc1, out);     // output node
        ......

当前所用ncnn库版本为:android ios macos linux windows webassembly 预编译库 20210525 f6c4952 模型文件详见附件: models.zip

Originally posted by @bestpower in https://github.com/Tencent/ncnn/issues/1983#issuecomment-893194776

nihui commented 3 years ago

这里有两个模型,关键点模型输出就已经偶发不一致吗?

bestpower commented 3 years ago

这里有两个模型,关键点模型输出就已经偶发不一致吗?

是的,两个模型都会出现偶发推理结果不一致的情况

bestpower commented 3 years ago

关键点检测在

ex.input(face68Landmark_opt_param_id::BLOB_data, ncnn_img);

之前ncnn_img数据还是一致的,但在

int status = ex.extract(face68Landmark_opt_param_id::BLOB_landmark, out);

推理之后out出现了不一致的情况

特征提取在

ex.input(faceFeature_opt_param_id::BLOB_data, det1);

之前det1数据还是一致的,但在

int status = ex.extract(faceFeature_opt_param_id::BLOB_fc1, out);

推理之后out出现了不一致的情况

bestpower commented 3 years ago

如果你们那边不好复现这个问题的话,能否在commit:f6c4952的基础上提供一份针对推理部分extract函数打印详细日志的android版本库,我这边复现出来再把日志传给你们来分析?

nihui commented 3 years ago

可以 extract 中间blob,二分查找,找到是模型里哪个blob开始结果不一致

bestpower commented 3 years ago

可以 extract 中间blob,二分查找,找到是模型里哪个blob开始结果不一致

按这个方法测试复现了一下,出现不一致的情况在关键点模型推理到第二个节点BLOB_272便开始出现差异了,后面的节点输出也一直都存在差异。

bestpower commented 3 years ago

我这边又集中复现了一下不一致性问题,主要统计信息如下: <html xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns="http://www.w3.org/TR/REC-html40">

模型路径 | 起始差异节点序号 | 起始差异节点BLOB | 出现次数 -- | -- | -- | -- models/opt/faceLandmark | 2 | BLOB_272 | 5 models/opt/faceLandmark | 8 | BLOB_283 | 1 models/opt/faceLandmark | 40 | BLOB_339 | 1 models/opt/faceFeature | 7 | BLOB_dconv_23_conv_sep_batchnorm | 1 models/opt/faceFeature | 55 | BLOB_res_4_block0_conv_proj_batchnorm | 1 models/opt/faceFeature | 71 | BLOB_res_4_block2_conv_proj_batchnorm | 1 models/opt/faceFeature | 75 | BLOB_res_4_block3_conv_sep_batchnorm | 1 models/opt/faceFeature | 79 | BLOB_res_4_block3_conv_proj_batchnorm | 1

关键点模型在BLOB_272节点出现差异的概率比较高,特征提取模型差异的节点比较分散

另外再补充一些信息: 测试机: cpu:Qualcomm Technologies Inc MSM8953 8xAArch64 Processor os:Android 7.1.1 测试模型: opt:onnx2ncnn->ncnnoptimize->ncnn2mem 原模型: no_opt:onnx2ncnn->ncnn2mem 出现不一致性看是否是ncnnoptimize的原因?

模型文件详见附件: models_add.zip

bestpower commented 3 years ago

目前靠这些信息可以分析出问题原因吗?如果还需要其他信息可以再说。

bestpower commented 3 years ago

我这边自测ncnnoptimize之前的模型也会出现不一致的情况,应该跟ncnnoptimize操作无关,从出现差异的节点及节点输出数值上来看并没有什么规律性,随机性比较强,所以难以从模型本身去做修正,我觉的应该从ncnn底层向前推理的代码中来定位,可能在这里存在隐藏BUG,可以提供带有日志信息打印的ncnn库吗?或者有其他更好的问题分析建议?

bestpower commented 3 years ago

请问这个问题有其他更好的分析办法吗?问题复现和日志收集我这边可以来做,如果能证明不是ncnn的问题我们就从其他方面来找原因了

bestpower commented 3 years ago

目前这个问题已经在不同硬件配置的设备(至少三种)上有复现,我们之前也有怀疑是设备cpu、ram等单体问题,但是我们在出现该问题的各种设备上做了大量的模拟推理的浮点运算压测,并没有复现不一致的问题,貌似是硬件或单体问题的可能性并不大。 该问题已经拖了快一个月了,我们一直卡在这里发不了版,请问有办法帮忙解决吗,哪怕提供ncnn库的详细日志打印信息来或者能证明不是ncnn本身问题的证据也可以,望回复,不胜感激!

nihui commented 3 years ago

我尝试死循环跑,始终无法复现。。。

复现出现blob结果错误时,检查下输入的 ncnn_img 是不是已经不一样了

nihui commented 3 years ago

另外,对于可能的内存越界读导致偶发性数据错误,通过 clang address sanitizer 检查也没有发现任何异常

贴一下我复现失败的程序代码,循环内包括生成图片数据,和仿照的api调用,memcmp 检查结果

#include <stdio.h>

#include "../tests/prng.h"

#include "datareader.h"
#include "net.h"
#include "gpu.h"

static struct prng_rand_t g_prng_rand_state;
#define SRAND(seed) prng_srand(seed, &g_prng_rand_state)
#define RAND()      prng_rand(&g_prng_rand_state)

static int RandomInt(int a = -10000, int b = 10000)
{
    float random = ((float)RAND()) / (float)uint64_t(-1); //RAND_MAX;
    int diff = b - a;
    float r = random * diff;
    return a + (int)r;
}

static signed char RandomS8()
{
    return (signed char)RandomInt(-127, 127);
}

static void RandomizeS8(ncnn::Mat& m)
{
    for (size_t i = 0; i < m.total(); i++)
    {
        ((signed char*)m)[i] = RandomS8();
    }
}

int main(int argc, char** argv)
{
    ncnn::Net net;

    net.load_param("face68Landmark_opt.param");
    net.load_model("face68Landmark_opt.bin");

    ncnn::Mat last;

    for (int x = 0; x < 10000; x++)
    {

    ncnn::Extractor ex = net.create_extractor();

    ncnn::Mat pp(224, 256, (size_t)3u, 3);

    SRAND(1000);
    RandomizeS8(pp);

    ncnn::Mat in = ncnn::Mat::from_pixels_resize((const unsigned char*)pp.data, ncnn::Mat::PIXEL_BGR2RGB, pp.w, pp.h, 112, 112);

    const float mean_vals[3] = {0.485f*255.f, 0.456f*255.f, 0.406f*255.f};
    const float norm_vals[3] = {1/0.229f/255.f, 1/0.224f/255.f, 1/0.225f/255.f};
    in.substract_mean_normalize(mean_vals, norm_vals);

    ex.input("data", in);

    ncnn::Mat out0;
    ex.extract("272", out0);

    if (last.empty())
    {
        last = out0;
    }
    else
    {
        // compare
        int cmp = memcmp(last.data, out0.data, out0.w*out0.h*out0.c*sizeof(float));
        if (cmp != 0)
        {
            fprintf(stderr, "cmp = %d    %d %d %d %d\n", cmp, out0.dims, out0.w, out0.h, out0.c);
        }

        last = out0;
    }
    }

    return 0;
}
bestpower commented 3 years ago

我尝试死循环跑,始终无法复现。。。

复现出现blob结果错误时,检查下输入的 ncnn_img 是不是已经不一样了

无论blob结果是否错误,输入的ncnn_img一直都是一致的,这个之前已经测试验证过

bestpower commented 3 years ago

另外,对于可能的内存越界读导致偶发性数据错误,通过 clang address sanitizer 检查也没有发现任何异常

贴一下我复现失败的程序代码,循环内包括生成图片数据,和仿照的api调用,memcmp 检查结果

#include <stdio.h>

#include "../tests/prng.h"

#include "datareader.h"
#include "net.h"
#include "gpu.h"

static struct prng_rand_t g_prng_rand_state;
#define SRAND(seed) prng_srand(seed, &g_prng_rand_state)
#define RAND()      prng_rand(&g_prng_rand_state)

static int RandomInt(int a = -10000, int b = 10000)
{
    float random = ((float)RAND()) / (float)uint64_t(-1); //RAND_MAX;
    int diff = b - a;
    float r = random * diff;
    return a + (int)r;
}

static signed char RandomS8()
{
    return (signed char)RandomInt(-127, 127);
}

static void RandomizeS8(ncnn::Mat& m)
{
    for (size_t i = 0; i < m.total(); i++)
    {
        ((signed char*)m)[i] = RandomS8();
    }
}

int main(int argc, char** argv)
{
    ncnn::Net net;

    net.load_param("face68Landmark_opt.param");
    net.load_model("face68Landmark_opt.bin");

    ncnn::Mat last;

    for (int x = 0; x < 10000; x++)
    {

    ncnn::Extractor ex = net.create_extractor();

    ncnn::Mat pp(224, 256, (size_t)3u, 3);

    SRAND(1000);
    RandomizeS8(pp);

    ncnn::Mat in = ncnn::Mat::from_pixels_resize((const unsigned char*)pp.data, ncnn::Mat::PIXEL_BGR2RGB, pp.w, pp.h, 112, 112);

    const float mean_vals[3] = {0.485f*255.f, 0.456f*255.f, 0.406f*255.f};
    const float norm_vals[3] = {1/0.229f/255.f, 1/0.224f/255.f, 1/0.225f/255.f};
    in.substract_mean_normalize(mean_vals, norm_vals);

    ex.input("data", in);

    ncnn::Mat out0;
    ex.extract("272", out0);

    if (last.empty())
    {
        last = out0;
    }
    else
    {
        // compare
        int cmp = memcmp(last.data, out0.data, out0.w*out0.h*out0.c*sizeof(float));
        if (cmp != 0)
        {
            fprintf(stderr, "cmp = %d    %d %d %d %d\n", cmp, out0.dims, out0.w, out0.h, out0.c);
        }

        last = out0;
    }
    }

    return 0;
}

我把你的这段代码编译到我们的测试设备里复现试试看

bestpower commented 3 years ago

我想把你这个测试代码在ncnn/examples/中编译成android设备中的可执行文件: 在ncnn/CMakeList.txt中改了配置:option(NCNN_BUILD_EXAMPLES "build examples" ON) 在cmake命令中增加-DOpenCV_DIR=${OpenCVConfig.cmake absolute path} 但执行命令后仍提示 CMake Warning at examples/CMakeLists.txt:23 (message): OpenCV not found, examples won't be built 请问该如何解决?

bestpower commented 3 years ago

直接编译运行这个代码是没问题的,但是与实际复现问题的条件不符,这个方法生成的随机Mat数据,所有我把你代码里的关于推理输入的模拟构造数据的方法去掉了,换成了通过opencv读取实际图像的方式,所以需要用到opencv环境

bestpower commented 3 years ago

我这边修改你给的测试代码编译成安卓可执行文件后测试已经复现了这个问题: 修改后的代码如下:

#include <stdio.h>
#include "../tests/prng.h"
#include "datareader.h"
#include "net.h"
#include "gpu.h"

static struct prng_rand_t g_prng_rand_state;
#define SRAND(seed) prng_srand(seed, &g_prng_rand_state)
#define RAND()      prng_rand(&g_prng_rand_state)

static int RandomInt(int a = -10000, int b = 10000)
{
    float random = ((float)RAND()) / (float)uint64_t(-1); //RAND_MAX;
    int diff = b - a;
    float r = random * diff;
    return a + (int)r;
}

static signed char RandomS8()
{
    return (signed char)RandomInt(-127, 127);
}

static void RandomizeS8(ncnn::Mat& m)
{
    for (size_t i = 0; i < m.total(); i++)
    {
        ((signed char*)m)[i] = RandomS8();
    }
}

int main(int argc, char** argv)
{

    // cv::Mat input_img = cv::imread("test.jpg");

    ncnn::Mat pp(224, 256, (size_t)3u, 3);
    SRAND(1000);
    RandomizeS8(pp);

    ncnn::Net net;
    net.load_param("face68Landmark_opt.param");
    net.load_model("face68Landmark_opt.bin");

    ncnn::Mat first;
    int error_num = 0;

    for (int x = 0; x < 10000; x++)
    {
        ncnn::Extractor ex = net.create_extractor();
        ex.set_num_threads(8);
        ncnn::Mat in = ncnn::Mat::from_pixels_resize((const unsigned char*)pp.data, ncnn::Mat::PIXEL_BGR2RGB, pp.w, pp.h, 112, 112);
        const float mean_vals[3] = {0.485f*255.f, 0.456f*255.f, 0.406f*255.f};
        const float norm_vals[3] = {1/0.229f/255.f, 1/0.224f/255.f, 1/0.225f/255.f};
        in.substract_mean_normalize(mean_vals, norm_vals);

        ex.input("data", in);
        ncnn::Mat out0;
        ex.extract("landmark", out0);

        if (first.empty())
        {
            first = out0;
        }
        else 
        {
            // compare
            int cmp = memcmp(first.data, out0.data, out0.w*out0.h*out0.c*sizeof(float));
            if (cmp != 0)
            {
                fprintf(stderr, "cmp = %d    %d %d %d %d\n", cmp, out0.dims, out0.w, out0.h, out0.c);
                error_num++;
            }
            fprintf(stderr, "cmp = %d   test num = %d   error num = %d\n", cmp, x, error_num);
        }
    }
    return 0;
}

出问题的log打印如下: cmp = 0 test num = 7830 error num = 0 cmp = 0 test num = 7831 error num = 0 cmp = 0 test num = 7832 error num = 0 cmp = 128 1 136 1 1 cmp = 128 test num = 7833 error num = 1 cmp = 0 test num = 7834 error num = 1 cmp = 0 test num = 7835 error num = 1 说明ncnn可能还是有问题的

nihui commented 3 years ago

暂时测试了两个设备,qcom865 和 vim3,没有复现问题 我用的是 ndk-r21e,你用的 ndk 版本比较老?

bestpower commented 3 years ago

暂时测试了两个设备,qcom865 和 vim3,没有复现问题 我用的是 ndk-r21e,你用的 ndk 版本比较老?

我用的ndk是r19c,这个问题和ndk版本也有关系吗?

nihui commented 3 years ago

r19c 是够用来编译的,不过你也试试看用 r21e 能不能复现,我也试试 r19c

cmdbug commented 3 years ago

大佬正在b站直播,可以在线问答。https://live.bilibili.com/1264617 (2021/08/26)

bestpower commented 3 years ago

大佬正在b站直播,可以在线问答。https://live.bilibili.com/1264617 (2021/08/26)

在加班,以后有机会再看吧

cmdbug commented 3 years ago

大佬正在b站直播,可以在线问答。https://live.bilibili.com/1264617 (2021/08/26)

在加班,以后有机会再看吧

就是在调你这个"bug"。qwq

bestpower commented 3 years ago

r19c 是够用来编译的,不过你也试试看用 r21e 能不能复现,我也试试 r19c

我用r21e也复现了,我用的CPU是Qualcomm Snapdragon Kryo 240/250/260/280

nihui commented 3 years ago

我这边手头现成有的手机是 qcom410 qcom425 qcom821 qcom865 qcom870

测试了 qcom865 和 qcom821 无法复现

bestpower commented 3 years ago

我这边手头现成有的手机是 qcom410 qcom425 qcom821 qcom865 qcom870

测试了 qcom865 和 qcom821 无法复现

复现的时候是用ncnn多线程跑的吗

bestpower commented 3 years ago

今天我这边自测试发现了一个现象就是:

在问题机器上设置多线程推理运行测试代码,每跑一轮1W次压测基本会出现20~60次的不一致问题

net.opt.lightmode = true;
net.opt.num_threads = ncnn::get_big_cpu_count();

在问题机器上设置单线程推理运行测试代码,跑了三轮1W次压测还没有出现不一致问题

net.opt.lightmode = true;
net.opt.num_threads = 1;

所以这个问题是否和ncnn的多线程推理机制有关?

nihui commented 3 years ago

我这边手头现成有的手机是 qcom410 qcom425 qcom821 qcom865 qcom870 测试了 qcom865 和 qcom821 无法复现

复现的时候是用ncnn多线程跑的吗

是多线程,设置线程数如代码所示为8

nihui commented 3 years ago

今天我这边自测试发现了一个现象就是:

在问题机器上设置多线程推理运行测试代码,每跑一轮1W次压测基本会出现20~60次的不一致问题

net.opt.lightmode = true;
net.opt.num_threads = ncnn::get_big_cpu_count();

在问题机器上设置单线程推理运行测试代码,跑了三轮1W次压测还没有出现不一致问题

net.opt.lightmode = true;
net.opt.num_threads = 1;

所以这个问题是否和ncnn的多线程推理机制有关?

这个信息有用!

bestpower commented 3 years ago

今天我这边自测试发现了一个现象就是: 在问题机器上设置多线程推理运行测试代码,每跑一轮1W次压测基本会出现20~60次的不一致问题

net.opt.lightmode = true;
net.opt.num_threads = ncnn::get_big_cpu_count();

在问题机器上设置单线程推理运行测试代码,跑了三轮1W次压测还没有出现不一致问题

net.opt.lightmode = true;
net.opt.num_threads = 1;

所以这个问题是否和ncnn的多线程推理机制有关?

这个信息有用!

针对这种情况有办法改进吗?是否需要优化ncnn的代码?

bestpower commented 3 years ago

另外我这边测试还发现了一个现象是: NCNN在安卓端默认的编译选项为-DANDROID_ARM_NEON=ON,但在修改编译选项为-DANDROID_ARM_NEON=OFF后编译出可执行文件在问题设备上进行测试,跑了多轮1W次压测没有再出现不一致问题,怀疑是问题设备CPU架构对NEON的支持存在差异引起的问题,应该属于CPU设计的限制而不是CPU硬件BUG。所以如果要彻底避免该问题只能从软件层面来规避了。 目前测出来问题的主要是两种CPU:QCM2290、MSM8953,这两种CPU都属于A53系列,如果贵司有时间可以进行针对性的优化。