Files

846 lines
25 KiB
C++

// Copyright 2026 Tencent
// SPDX-License-Identifier: BSD-3-Clause
#include "perfutil.h"
#include "benchmark.h"
#include "cpu.h"
#include "layer.h"
#include "modelbin.h"
#include <float.h>
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#if NCNN_VULKAN
#include "command.h"
#include "gpu.h"
#endif // NCNN_VULKAN
// default benchmark parameters
#define PERF_WARMUP_COUNT 10
#define PERF_GPU_WARMUP_COUNT 20
#define PERF_GPU_WARMUP_BATCH 100
#define PERF_RUN_COUNT 20
#define PERF_TARGET_MIN_MS 5.0
// benchmark result for a single test case
struct PerfResult
{
double time_min;
double time_max;
double time_avg;
double time_median;
int loop_count; // inner loops per iteration (for short ops)
};
ncnn::Mat PerfMat(int w, float v)
{
ncnn::Mat m(w);
m.fill(v);
return m;
}
ncnn::Mat PerfMat(int w, int h, float v)
{
ncnn::Mat m(w, h);
m.fill(v);
return m;
}
ncnn::Mat PerfMat(int w, int h, int c, float v)
{
ncnn::Mat m(w, h, c);
m.fill(v);
return m;
}
ncnn::Mat PerfMat(int w, int h, int d, int c, float v)
{
ncnn::Mat m(w, h, d, c);
m.fill(v);
return m;
}
static void sort_doubles(double* arr, int n)
{
for (int i = 1; i < n; i++)
{
double key = arr[i];
int j = i - 1;
while (j >= 0 && arr[j] > key)
{
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
}
}
// convert input mat to the optimal layout that the layer expects
// this handles fp16/bf16 cast and packing conversion
// so that these costs are excluded from timing
static void convert_input_layout(const ncnn::Mat& src, ncnn::Mat& dst, const ncnn::Option& opt, const ncnn::Layer* op)
{
ncnn::Mat casted;
// clang-format off
// *INDENT-OFF*
#if NCNN_ARM82
if (opt.use_fp16_storage && ncnn::cpu_support_arm_asimdhp() && op->support_fp16_storage)
{
ncnn::cast_float32_to_float16(src, casted, opt);
}
else
#endif // NCNN_ARM82
#if NCNN_VFPV4
if (opt.use_fp16_storage && !opt.use_bf16_storage && ncnn::cpu_support_arm_vfpv4() && op->support_fp16_storage)
{
ncnn::cast_float32_to_float16(src, casted, opt);
}
else
#endif // NCNN_VFPV4
#if NCNN_ZFH
if (opt.use_fp16_storage && (ncnn::cpu_support_riscv_zvfh() || (!ncnn::cpu_support_riscv_v() && ncnn::cpu_support_riscv_zfh())) && op->support_fp16_storage)
{
ncnn::cast_float32_to_float16(src, casted, opt);
}
else
#endif // NCNN_ZFH
#if NCNN_BF16
if (opt.use_bf16_storage && op->support_bf16_storage)
{
ncnn::cast_float32_to_bfloat16(src, casted, opt);
}
else
#endif // NCNN_BF16
if (opt.use_fp16_storage && op->support_fp16_storage)
{
ncnn::cast_float32_to_float16(src, casted, opt);
}
else
{
casted = src;
}
// *INDENT-ON*
// clang-format on
// packing conversion
if (opt.use_packing_layout && op->support_packing)
{
int dims = casted.dims;
int elemcount = 0;
if (dims == 1) elemcount = casted.elempack * casted.w;
if (dims == 2) elemcount = casted.elempack * casted.h;
if (dims == 3 || dims == 4) elemcount = casted.elempack * casted.c;
int elembits = casted.elembits();
int dst_elempack = 1;
if (elembits == 32)
{
#if NCNN_AVX512
if (elemcount % 16 == 0 && ncnn::cpu_support_x86_avx512())
dst_elempack = 16;
else if (elemcount % 8 == 0 && ncnn::cpu_support_x86_avx())
dst_elempack = 8;
else if (elemcount % 4 == 0)
dst_elempack = 4;
#elif NCNN_AVX
if (elemcount % 8 == 0 && ncnn::cpu_support_x86_avx())
dst_elempack = 8;
else if (elemcount % 4 == 0)
dst_elempack = 4;
#elif NCNN_RVV || NCNN_XTHEADVECTOR
const int packn = ncnn::cpu_riscv_vlenb() / 4;
if (elemcount % packn == 0)
dst_elempack = packn;
#elif NCNN_LASX
if (elemcount % 8 == 0 && ncnn::cpu_support_loongarch_lasx())
dst_elempack = 8;
else if (elemcount % 4 == 0)
dst_elempack = 4;
#else
if (elemcount % 4 == 0)
dst_elempack = 4;
#endif
}
if (elembits == 16)
{
#if NCNN_ARM82
if (elemcount % 8 == 0 && ncnn::cpu_support_arm_asimdhp() && opt.use_fp16_arithmetic && op->support_fp16_storage)
dst_elempack = 8;
else if (elemcount % 4 == 0)
dst_elempack = 4;
#elif NCNN_RVV || NCNN_XTHEADVECTOR
const int packn = ncnn::cpu_riscv_vlenb() / 2;
if (elemcount % packn == 0)
dst_elempack = packn;
#elif NCNN_LASX
if (elemcount % 8 == 0 && ncnn::cpu_support_loongarch_lasx() && opt.use_bf16_storage && op->support_bf16_storage)
dst_elempack = 8;
else if (elemcount % 8 == 0 && ncnn::cpu_support_loongarch_lsx() && opt.use_bf16_storage && op->support_bf16_storage)
dst_elempack = 8;
else if (elemcount % 4 == 0)
dst_elempack = 4;
#elif NCNN_LSX
if (elemcount % 8 == 0 && ncnn::cpu_support_loongarch_lsx() && opt.use_bf16_storage && op->support_bf16_storage)
dst_elempack = 8;
else if (elemcount % 4 == 0)
dst_elempack = 4;
#elif NCNN_MSA
if (elemcount % 8 == 0 && ncnn::cpu_support_mips_msa() && opt.use_bf16_storage && op->support_bf16_storage)
dst_elempack = 8;
else if (elemcount % 4 == 0)
dst_elempack = 4;
#else
if (elemcount % 4 == 0)
dst_elempack = 4;
#endif
}
if (elembits == 8)
{
#if NCNN_RVV || NCNN_XTHEADVECTOR
const int packn = ncnn::cpu_riscv_vlenb() / 1;
if (elemcount % packn == 0)
dst_elempack = packn;
#else
if (elemcount % 8 == 0)
dst_elempack = 8;
#endif
}
ncnn::convert_packing(casted, dst, dst_elempack, opt);
}
else
{
dst = casted;
}
}
// run a single forward pass (pure compute, no conversion)
static int run_layer_forward_cpu(ncnn::Layer* op,
const std::vector<ncnn::Mat>& converted_inputs,
int top_blob_count,
const ncnn::Option& opt)
{
if (op->one_blob_only)
{
if (op->support_inplace)
{
ncnn::Mat blob = converted_inputs[0].clone();
return op->forward_inplace(blob, opt);
}
else
{
ncnn::Mat out;
return op->forward(converted_inputs[0], out, opt);
}
}
else
{
if (op->support_inplace)
{
std::vector<ncnn::Mat> blobs(converted_inputs.size());
for (size_t i = 0; i < converted_inputs.size(); i++)
{
blobs[i] = converted_inputs[i].clone();
}
return op->forward_inplace(blobs, opt);
}
else
{
std::vector<ncnn::Mat> outputs(top_blob_count);
return op->forward(converted_inputs, outputs, opt);
}
}
}
// forced_inner_loops > 0: use the given value, skip calibration
// forced_inner_loops <= 0: calibrate from warmup min time
static int perf_layer_cpu(const char* layer_type, const ncnn::ParamDict& pd,
const std::vector<ncnn::Mat>& weights,
const std::vector<ncnn::Mat>& inputs,
int top_blob_count,
const ncnn::Option& opt,
int forced_inner_loops,
PerfResult& result)
{
ncnn::Layer* op = ncnn::create_layer_cpu(layer_type);
if (!op)
{
fprintf(stderr, "perf_layer_cpu: create_layer_cpu(%s) failed\n", layer_type);
return -1;
}
op->load_param(pd);
ncnn::ModelBinFromMatArray mb(weights.data());
op->load_model(mb);
op->create_pipeline(opt);
// pre-convert inputs to the layout the layer expects
std::vector<ncnn::Mat> converted(inputs.size());
for (size_t i = 0; i < inputs.size(); i++)
{
convert_input_layout(inputs[i], converted[i], opt, op);
}
// warmup and calibrate inner loop count from warmup min time
double warmup_min_ms = DBL_MAX;
for (int i = 0; i < PERF_WARMUP_COUNT; i++)
{
double t0 = ncnn::get_current_time();
int ret = run_layer_forward_cpu(op, converted, top_blob_count, opt);
double t1 = ncnn::get_current_time();
if (ret != 0)
{
op->destroy_pipeline(opt);
delete op;
return -1;
}
double t = t1 - t0;
if (t < warmup_min_ms) warmup_min_ms = t;
}
int inner_loops;
if (forced_inner_loops > 0)
{
inner_loops = forced_inner_loops;
}
else
{
// calibrate inner loop count to power of 10, so total time >= PERF_TARGET_MIN_MS
inner_loops = 1;
if (warmup_min_ms > 0 && warmup_min_ms < PERF_TARGET_MIN_MS)
{
while (inner_loops * warmup_min_ms < PERF_TARGET_MIN_MS)
inner_loops *= 10;
}
}
double* times = new double[PERF_RUN_COUNT];
double time_sum = 0;
double time_min_val = DBL_MAX;
double time_max_val = -DBL_MAX;
for (int i = 0; i < PERF_RUN_COUNT; i++)
{
double start = ncnn::get_current_time();
for (int k = 0; k < inner_loops; k++)
{
run_layer_forward_cpu(op, converted, top_blob_count, opt);
}
double end = ncnn::get_current_time();
double t = end - start;
times[i] = t;
time_sum += t;
if (t < time_min_val) time_min_val = t;
if (t > time_max_val) time_max_val = t;
}
sort_doubles(times, PERF_RUN_COUNT);
double time_median_val;
if (PERF_RUN_COUNT % 2 == 0)
time_median_val = (times[PERF_RUN_COUNT / 2 - 1] + times[PERF_RUN_COUNT / 2]) / 2.0;
else
time_median_val = times[PERF_RUN_COUNT / 2];
result.time_min = time_min_val;
result.time_max = time_max_val;
result.time_avg = time_sum / PERF_RUN_COUNT;
result.time_median = time_median_val;
result.loop_count = inner_loops;
delete[] times;
op->destroy_pipeline(opt);
delete op;
return 0;
}
#if NCNN_VULKAN
static void run_layer_forward_gpu(ncnn::Layer* op,
const std::vector<ncnn::VkMat>& vk_inputs,
int top_blob_count,
ncnn::VkCompute& cmd, const ncnn::Option& opt)
{
if (op->one_blob_only)
{
if (op->support_inplace)
{
ncnn::VkMat vk_in = vk_inputs[0];
op->forward_inplace(vk_in, cmd, opt);
}
else
{
ncnn::VkMat vk_out;
op->forward(vk_inputs[0], vk_out, cmd, opt);
}
}
else
{
if (op->support_inplace)
{
std::vector<ncnn::VkMat> vk_in(vk_inputs);
op->forward_inplace(vk_in, cmd, opt);
}
else
{
std::vector<ncnn::VkMat> vk_out(top_blob_count);
op->forward(vk_inputs, vk_out, cmd, opt);
}
}
}
static int perf_layer_gpu(const char* layer_type, const ncnn::ParamDict& pd,
const std::vector<ncnn::Mat>& weights,
const std::vector<ncnn::Mat>& inputs,
int top_blob_count,
ncnn::VulkanDevice* vkdev,
const ncnn::Option& _opt,
int forced_inner_loops,
PerfResult& result)
{
ncnn::Layer* op = ncnn::create_layer_vulkan(layer_type);
if (!op)
{
return -1;
}
op->load_param(pd);
if (!op->support_vulkan)
{
delete op;
return -1;
}
op->vkdev = vkdev;
ncnn::Option opt = _opt;
opt.use_vulkan_compute = true;
ncnn::VkAllocator* blob_vkallocator = vkdev->acquire_blob_allocator();
ncnn::VkAllocator* staging_vkallocator = vkdev->acquire_staging_allocator();
opt.blob_vkallocator = blob_vkallocator;
opt.workspace_vkallocator = blob_vkallocator;
opt.staging_vkallocator = staging_vkallocator;
// respect device capabilities
if (!vkdev->info.support_fp16_packed()) opt.use_fp16_packed = false;
if (!vkdev->info.support_fp16_storage()) opt.use_fp16_storage = false;
if (!vkdev->info.support_fp16_uniform()) opt.use_fp16_uniform = false;
if (!vkdev->info.support_fp16_arithmetic()) opt.use_fp16_arithmetic = false;
if (!vkdev->info.support_bf16_packed()) opt.use_bf16_packed = false;
if (!vkdev->info.support_bf16_storage()) opt.use_bf16_storage = false;
if (!vkdev->info.support_int8_packed()) opt.use_int8_packed = false;
if (!vkdev->info.support_int8_storage()) opt.use_int8_storage = false;
if (!vkdev->info.support_int8_uniform()) opt.use_int8_uniform = false;
if (!vkdev->info.support_int8_arithmetic()) opt.use_int8_arithmetic = false;
if (!vkdev->info.support_cooperative_matrix()) opt.use_cooperative_matrix = false;
if (!vkdev->info.support_subgroup_ops()) opt.use_subgroup_ops = false;
ncnn::VkWeightAllocator weight_vkallocator(vkdev);
ncnn::VkWeightStagingAllocator weight_staging_vkallocator(vkdev);
ncnn::ModelBinFromMatArray mb(weights.data());
op->load_model(mb);
op->create_pipeline(opt);
if (!op->support_vulkan)
{
op->destroy_pipeline(opt);
delete op;
vkdev->reclaim_blob_allocator(blob_vkallocator);
vkdev->reclaim_staging_allocator(staging_vkallocator);
return -1;
}
// upload model weights
{
ncnn::VkTransfer cmd(vkdev);
ncnn::Option opt_upload = opt;
opt_upload.blob_vkallocator = &weight_vkallocator;
opt_upload.workspace_vkallocator = &weight_vkallocator;
opt_upload.staging_vkallocator = &weight_staging_vkallocator;
op->upload_model(cmd, opt_upload);
cmd.submit_and_wait();
}
// pre-upload and pack inputs on GPU, exclude from timing
std::vector<ncnn::VkMat> vk_inputs(inputs.size());
{
ncnn::VkCompute cmd(vkdev);
for (size_t j = 0; j < inputs.size(); j++)
{
cmd.record_upload(inputs[j], vk_inputs[j], opt);
}
// convert packing on gpu if needed
if (op->support_vulkan_packing)
{
for (size_t j = 0; j < vk_inputs.size(); j++)
{
int elemcount = 0;
int dims = inputs[j].dims;
if (dims == 1) elemcount = inputs[j].elempack * inputs[j].w;
if (dims == 2) elemcount = inputs[j].elempack * inputs[j].h;
if (dims == 3 || dims == 4) elemcount = inputs[j].elempack * inputs[j].c;
int dst_elempack = elemcount % 4 == 0 ? 4 : 1;
if (vk_inputs[j].elempack != dst_elempack)
{
ncnn::VkMat vk_packed;
vkdev->convert_packing(vk_inputs[j], vk_packed, dst_elempack, cmd, opt);
vk_inputs[j] = vk_packed;
}
}
}
cmd.submit_and_wait();
}
// warmup and calibrate inner loop count from warmup min time
// batch multiple forwards per submit to amortize submit_and_wait overhead
double warmup_min_ms = DBL_MAX;
for (int i = 0; i < PERF_GPU_WARMUP_COUNT; i++)
{
ncnn::VkCompute cmd(vkdev);
for (int b = 0; b < PERF_GPU_WARMUP_BATCH; b++)
{
run_layer_forward_gpu(op, vk_inputs, top_blob_count, cmd, opt);
}
double t0 = ncnn::get_current_time();
cmd.submit_and_wait();
double t1 = ncnn::get_current_time();
double t = (t1 - t0) / PERF_GPU_WARMUP_BATCH;
if (t < warmup_min_ms) warmup_min_ms = t;
}
int inner_loops;
if (forced_inner_loops > 0)
{
inner_loops = forced_inner_loops;
}
else
{
// calibrate inner loop count to power of 10, so total time >= PERF_TARGET_MIN_MS
inner_loops = 1;
if (warmup_min_ms > 0 && warmup_min_ms < PERF_TARGET_MIN_MS)
{
while (inner_loops * warmup_min_ms < PERF_TARGET_MIN_MS)
inner_loops *= 10;
}
}
// record inner_loops forwards into one command buffer, single submit
// this measures pure GPU kernel time, excluding per-launch overhead
double* times = new double[PERF_RUN_COUNT];
double time_sum = 0;
double time_min_val = DBL_MAX;
double time_max_val = -DBL_MAX;
for (int i = 0; i < PERF_RUN_COUNT; i++)
{
ncnn::VkCompute cmd(vkdev);
for (int k = 0; k < inner_loops; k++)
{
run_layer_forward_gpu(op, vk_inputs, top_blob_count, cmd, opt);
}
double start = ncnn::get_current_time();
cmd.submit_and_wait();
double end = ncnn::get_current_time();
double t = end - start;
times[i] = t;
time_sum += t;
if (t < time_min_val) time_min_val = t;
if (t > time_max_val) time_max_val = t;
}
sort_doubles(times, PERF_RUN_COUNT);
double time_median_val;
if (PERF_RUN_COUNT % 2 == 0)
time_median_val = (times[PERF_RUN_COUNT / 2 - 1] + times[PERF_RUN_COUNT / 2]) / 2.0;
else
time_median_val = times[PERF_RUN_COUNT / 2];
result.time_min = time_min_val;
result.time_max = time_max_val;
result.time_avg = time_sum / PERF_RUN_COUNT;
result.time_median = time_median_val;
result.loop_count = inner_loops;
delete[] times;
op->destroy_pipeline(opt);
delete op;
vkdev->reclaim_blob_allocator(blob_vkallocator);
vkdev->reclaim_staging_allocator(staging_vkallocator);
return 0;
}
#endif // NCNN_VULKAN
static void print_perf_result(const char* tag, const PerfResult& result)
{
if (result.loop_count > 1)
{
fprintf(stdout, "%-72s min = %8.2f max = %8.2f avg = %8.2f median = %8.2f (x%d)\n",
tag, result.time_min, result.time_max, result.time_avg, result.time_median, result.loop_count);
}
else
{
fprintf(stdout, "%-72s min = %8.2f max = %8.2f avg = %8.2f median = %8.2f\n",
tag, result.time_min, result.time_max, result.time_avg, result.time_median);
}
fflush(stdout);
}
static void format_shape(char* buf, int bufsize, const ncnn::Mat& m)
{
if (m.dims == 1)
snprintf(buf, bufsize, "[%d]", m.w);
else if (m.dims == 2)
snprintf(buf, bufsize, "[%d,%d]", m.w, m.h);
else if (m.dims == 3)
snprintf(buf, bufsize, "[%d,%d,%d]", m.w, m.h, m.c);
else if (m.dims == 4)
snprintf(buf, bufsize, "[%d,%d,%d,%d]", m.w, m.h, m.d, m.c);
else
snprintf(buf, bufsize, "[empty]");
}
// build tag: "LayerType (shape)+(shape) params"
static void build_tag(char* tag, int tagsize,
const char* layer_type,
const std::vector<ncnn::Mat>& inputs,
const char* param_fmt, va_list args)
{
int pos = 0;
int remain = tagsize;
int n;
// input shapes
for (size_t i = 0; i < inputs.size() && remain > 0; i++)
{
if (i > 0)
{
n = snprintf(tag + pos, remain, ",");
if (n > 0 && n < remain)
{
pos += n;
remain -= n;
}
else
{
remain = 0;
break;
}
}
char shape[64];
format_shape(shape, sizeof(shape), inputs[i]);
n = snprintf(tag + pos, remain, "%s", shape);
if (n > 0 && n < remain)
{
pos += n;
remain -= n;
}
else
{
remain = 0;
}
}
// pad to fixed column for params
while (pos < 32 && remain > 0)
{
tag[pos++] = ' ';
remain--;
}
tag[pos] = '\0';
// layer-specific params
if (param_fmt && param_fmt[0] && remain > 0)
{
n = snprintf(tag + pos, remain, " ");
if (n > 0 && n < remain)
{
pos += n;
remain -= n;
}
else
{
remain = 0;
}
if (remain > 0)
{
n = vsnprintf(tag + pos, remain, param_fmt, args);
if (n > 0 && n < remain)
{
pos += n;
remain -= n;
}
else
{
remain = 0;
}
}
}
tag[tagsize - 1] = '\0';
}
static ncnn::Option make_perf_option(bool use_fp16_ps, bool use_fp16_arith, bool use_bf16)
{
ncnn::Option opt;
opt.lightmode = true;
opt.num_threads = 1;
opt.use_packing_layout = true;
opt.use_fp16_packed = use_fp16_ps;
opt.use_fp16_storage = use_fp16_ps;
opt.use_fp16_arithmetic = use_fp16_arith;
opt.use_bf16_packed = use_bf16;
opt.use_bf16_storage = use_bf16;
opt.use_vulkan_compute = false;
opt.use_winograd_convolution = true;
opt.use_sgemm_convolution = true;
opt.use_int8_inference = true;
return opt;
}
// precision configs
struct PrecisionConfig
{
bool fp16_ps;
bool fp16_arith;
bool bf16;
const char* label;
};
static const PrecisionConfig s_configs[] = {
{false, false, false, "fp32"},
{true, false, false, "fp16ps"},
{true, true, false, "fp16psa"},
#if NCNN_BF16
{false, false, true, "bf16ps"},
#endif
};
static const int s_num_configs = sizeof(s_configs) / sizeof(s_configs[0]);
static void perf_layer_impl(const char* layer_type, const ncnn::ParamDict& pd,
const std::vector<ncnn::Mat>& weights,
const std::vector<ncnn::Mat>& inputs,
int top_blob_count,
const char* tag)
{
// --- CPU ---
// run fp32 first to calibrate inner_loops, then reuse for all precisions
int cpu_inner_loops = 0;
for (int i = 0; i < s_num_configs; i++)
{
ncnn::Option opt = make_perf_option(s_configs[i].fp16_ps, s_configs[i].fp16_arith, s_configs[i].bf16);
PerfResult result;
int ret = perf_layer_cpu(layer_type, pd, weights, inputs, top_blob_count, opt, cpu_inner_loops, result);
if (ret != 0)
continue;
// use the first successful calibration for all subsequent configs
if (cpu_inner_loops <= 0)
cpu_inner_loops = result.loop_count;
char full_tag[512];
snprintf(full_tag, sizeof(full_tag), "%s %s", tag, s_configs[i].label);
print_perf_result(full_tag, result);
}
fprintf(stdout, "\n");
fflush(stdout);
#if NCNN_VULKAN
// --- GPU ---
{
static bool gpu_initialized = false;
if (!gpu_initialized)
{
ncnn::create_gpu_instance();
gpu_initialized = true;
}
int gpu_count = ncnn::get_gpu_count();
for (int gpu_id = 0; gpu_id < gpu_count; gpu_id++)
{
ncnn::VulkanDevice* vkdev = ncnn::get_gpu_device(gpu_id);
if (!vkdev) continue;
// run fp32 first to calibrate inner_loops, then reuse for all precisions
int gpu_inner_loops = 0;
for (int i = 0; i < s_num_configs; i++)
{
ncnn::Option opt = make_perf_option(s_configs[i].fp16_ps, s_configs[i].fp16_arith, s_configs[i].bf16);
PerfResult result;
int ret = perf_layer_gpu(layer_type, pd, weights, inputs, top_blob_count, vkdev, opt, gpu_inner_loops, result);
if (ret != 0)
continue;
if (gpu_inner_loops <= 0)
gpu_inner_loops = result.loop_count;
char full_tag[512];
snprintf(full_tag, sizeof(full_tag), "%s gpu-%d %s", tag, gpu_id, s_configs[i].label);
print_perf_result(full_tag, result);
}
fprintf(stdout, "\n");
fflush(stdout);
}
}
#endif // NCNN_VULKAN
}
void perf_layer(const char* layer_type, const ncnn::ParamDict& pd,
const std::vector<ncnn::Mat>& weights,
const std::vector<ncnn::Mat>& inputs, int top_blob_count,
const char* param_fmt, ...)
{
char tag[256];
va_list args;
va_start(args, param_fmt);
build_tag(tag, sizeof(tag), layer_type, inputs, param_fmt, args);
va_end(args);
perf_layer_impl(layer_type, pd, weights, inputs, top_blob_count, tag);
}
void perf_layer(const char* layer_type, const ncnn::ParamDict& pd,
const std::vector<ncnn::Mat>& weights,
const ncnn::Mat& input, const char* param_fmt, ...)
{
std::vector<ncnn::Mat> inputs(1);
inputs[0] = input;
char tag[256];
va_list args;
va_start(args, param_fmt);
build_tag(tag, sizeof(tag), layer_type, inputs, param_fmt, args);
va_end(args);
perf_layer_impl(layer_type, pd, weights, inputs, 1, tag);
}