Files
2026-07-13 13:33:03 +08:00

1315 lines
50 KiB
C++

#include "llm/llm.hpp"
#include "core/MNNFileUtils.h"
#include <MNN/AutoTime.hpp>
#include <MNN/expr/ExecutorScope.hpp>
#include "Profiler.hpp"
#include <fstream>
#include <sstream>
#include <regex>
#include <stdlib.h>
#include <initializer_list>
#include <rapidjson/document.h>
#include <rapidjson/stringbuffer.h>
#include <rapidjson/writer.h>
#include <thread>
#include <algorithm>
#include <numeric>
#include <memory>
#define MNN_OPEN_TIME_TRACE
using namespace MNN::Transformer;
struct RuntimeParameters {
std::vector<std::string> model;
std::vector<int> backends;
std::vector<int> threads;
bool useMmap;
std::vector<int> power;
std::vector<int> precision;
std::vector<int> memory;
std::vector<int> dynamicOption;
std::vector<int> divisionRatioSme2Neon;
std::vector<int> smeCoreNum;
std::vector<int> attentionOption;
};
struct TestParameters {
std::vector<int> nPrompt;
std::vector<int> nGenerate;
std::vector<std::pair<int, int>> nPrompGen;
std::vector<int> nRepeat;
std::string kvCache;
std::string loadTime;
};
struct CommandParameters {
std::string model;
int backend;
int threads;
bool useMmap;
int power;
int precision;
int memory;
int dynamicOption;
int divisionRatioSme2Neon;
int smeCoreNum;
int attentionOption;
int nPrompt;
int nGenerate;
std::pair<int, int> nPrompGen;
int nRepeat;
std::string kvCache;
std::string loadingTime;
};
static const RuntimeParameters runtimeParamsDefaults = {
/* model */ { "./Qwen2.5-1.5B-Instruct" },
/* backends */ { 0 },
/* threads */ { 4 },
/* useMmap */ false,
/* power */ { 0 },
/* precision */ { 2 },
/* memory */ { 2 },
/* dynamicOption */ { 0 },
/* divisionRatioSme2Neon*/ { 41 },
/* smeCoreNum */ { 2 },
/* attentionOption */ { 0 }
};
static const TestParameters testParamsDefaults = {
/* nPrompt */ { 512 },
/* nGenerate */ { 128 },
/* nPrompGen */ {std::make_pair(0, 0)},
/* nRepeat */ { 5 },
/* kvCache */ { "false" },
/* loadingTime */ {"false"}
};
struct commandParametersInstance {
CommandParameters mCmdParam;
commandParametersInstance(CommandParameters cmdParam) {
mCmdParam.model = cmdParam.model;
mCmdParam.backend = cmdParam.backend;
mCmdParam.threads = cmdParam.threads;
mCmdParam.useMmap = cmdParam.useMmap;
mCmdParam.power = cmdParam.power;
mCmdParam.precision = cmdParam.precision;
mCmdParam.memory = cmdParam.memory;
mCmdParam.dynamicOption = cmdParam.dynamicOption;
mCmdParam.divisionRatioSme2Neon = cmdParam.divisionRatioSme2Neon;
mCmdParam.attentionOption = cmdParam.attentionOption;
mCmdParam.nPrompt = cmdParam.nPrompt;
mCmdParam.nGenerate = cmdParam.nGenerate;
mCmdParam.nPrompGen = cmdParam.nPrompGen;
mCmdParam.nRepeat = cmdParam.nRepeat;
mCmdParam.kvCache = cmdParam.kvCache;
mCmdParam.loadingTime = cmdParam.loadingTime;
mCmdParam.smeCoreNum = cmdParam.smeCoreNum;
}
CommandParameters get_cmd_parameters() const {
return mCmdParam;
}
bool equal_runtime_params(const commandParametersInstance & other) const {
return mCmdParam.model == other.mCmdParam.model &&
mCmdParam.useMmap == other.mCmdParam.useMmap &&
mCmdParam.power == other.mCmdParam.power &&
mCmdParam.precision == other.mCmdParam.precision &&
mCmdParam.memory == other.mCmdParam.memory &&
mCmdParam.dynamicOption == other.mCmdParam.dynamicOption &&
mCmdParam.attentionOption == other.mCmdParam.attentionOption &&
mCmdParam.smeCoreNum == other.mCmdParam.smeCoreNum &&
mCmdParam.divisionRatioSme2Neon == other.mCmdParam.divisionRatioSme2Neon;
}
};
template <typename T> static T avg(const std::vector<T> & v) {
if (v.empty()) {
return 0;
}
T sum = std::accumulate(v.begin(), v.end(), T(0));
return sum / (T) v.size();
}
template <typename T> static T stdev(const std::vector<T> & v) {
if (v.size() <= 1) {
return 0;
}
T mean = avg(v);
T sq_sum = std::inner_product(v.begin(), v.end(), v.begin(), T(0));
T stdev = std::sqrt(sq_sum / (T) (v.size() - 1) - mean * mean * (T) v.size() / (T) (v.size() - 1));
return stdev;
}
template <class T> static std::string join(const std::vector<T> & values, const std::string & delim) {
std::ostringstream str;
for (size_t i = 0; i < values.size(); i++) {
str << values[i];
if (i < values.size() - 1) {
str << delim;
}
}
return str.str();
}
struct TestInstance {
// static const std::string build_commit;
std::string modelConfigFile;
std::string modelType;
uint64_t modelSize;
int threads;
bool useMmap;
int nPrompt;
int nGenerate;
std::vector<int64_t> nGenerates;
std::vector<int64_t> prefillUs;
std::vector<int64_t> decodeUs;
std::vector<int64_t> samplesUs;
std::vector<double> loadingS;
int backend;
int precision;
int power;
int memory;
int dynamicOption;
int divisionRatioSme2Neon;
int smeCoreNum;
int attentionOption;
TestInstance(const commandParametersInstance & instance) {
modelConfigFile = instance.mCmdParam.model;
threads = instance.mCmdParam.threads;
useMmap = instance.mCmdParam.useMmap;
nPrompt = instance.mCmdParam.nPrompt;
nGenerate = instance.mCmdParam.nGenerate;
backend = instance.mCmdParam.backend;
precision = instance.mCmdParam.precision;
memory = instance.mCmdParam.memory;
power = instance.mCmdParam.power;
dynamicOption = instance.mCmdParam.dynamicOption;
divisionRatioSme2Neon = instance.mCmdParam.divisionRatioSme2Neon;
smeCoreNum = instance.mCmdParam.smeCoreNum;
attentionOption = instance.mCmdParam.attentionOption;
}
std::vector<double> getTokensPerSecond(int n_tokens, std::vector<int64_t> cost_us) const {
std::vector<double> ts;
std::transform(cost_us.begin(), cost_us.end(), std::back_inserter(ts), [n_tokens](int64_t t) { return 1e6 * n_tokens / t; });
return ts;
}
std::vector<double> getTokensPerSecond(std::vector<int64_t> n_tokens, std::vector<int64_t> cost_us) const {
std::vector<double> ts(n_tokens.size());
for (int i = 0; i < n_tokens.size(); ++i) {
ts[i] = 1e6 * n_tokens[i] / cost_us[i];
}
return ts;
}
double getAvgUs(std::vector<double> v) const { return ::avg(v); }
double getStdevUs(std::vector<double> v) const { return ::stdev(v); }
enum fieldType { STRING, BOOL, INT, FLOAT };
static fieldType getFieldType(const std::string & field) {
if (field == "threads") {
return INT;
}
if (field == "useMmap") {
return BOOL;
}
if (field == "t/s" || field == "modelSize" || field == "prefill&decode speed (tok/s)") {
return FLOAT;
}
return STRING;
}
};
static std::string pairString(const std::pair<int, int> & p) {
static char buf[32];
snprintf(buf, sizeof(buf), "%d,%d", p.first, p.second);
return buf;
}
template <typename T, typename F> static std::vector<std::string> transform2String(const std::vector<T> & values, F f) {
std::vector<std::string> str_values;
std::transform(values.begin(), values.end(), std::back_inserter(str_values), f);
return str_values;
}
template<class T>
static std::vector<T> splitString(const std::string & str, char delim) {
std::vector<T> values;
std::istringstream str_stream(str);
std::string token;
while (std::getline(str_stream, token, delim)) {
T value;
std::istringstream tokenStream(token);
tokenStream >> value;
values.push_back(value);
}
return values;
}
struct Printer {
virtual ~Printer() {}
FILE * fout;
virtual void printHeader(const RuntimeParameters & rp, const TestParameters & tp) { (void) rp; (void) tp; }
virtual void printPerformance(const TestInstance & t) = 0;
// virtual void print_footer() {}
};
struct markdownPrinter : public Printer {
std::vector<std::string> fields;
static int getFieldWidth(const std::string & field) {
if (field == "model") {
return -30;
}
if (field == "prefill&decode speed (tok/s)") {
return 20;
}
if (field == "threads") {
return 5;
}
if (field == "useMmap") {
return 4;
}
if (field == "test") {
return -13;
}
int width = std::max((int) field.length(), 10);
if (TestInstance::getFieldType(field) == TestInstance::STRING) {
return -width;
}
return width;
}
static std::string getFieldDisplayName(const std::string & field) {
if (field == "useMmap") {
return "mmap";
}
return field;
}
void printHeader(const RuntimeParameters & rp, const TestParameters & tp) override {
// select fields to print
fields.emplace_back("model");
fields.emplace_back("modelSize");
fields.emplace_back("backend");
fields.emplace_back("threads");
if (rp.precision.size() > 0) {
fields.emplace_back("precision");
}
if (rp.memory.size() > 1) {
fields.emplace_back("memory");
}
if (rp.dynamicOption.size() > 1) {
fields.emplace_back("dynamicOption");
}
if (!(rp.divisionRatioSme2Neon.size() == 1 && rp.divisionRatioSme2Neon[0] == runtimeParamsDefaults.divisionRatioSme2Neon[0])) {
fields.emplace_back("divisionRatioSme2Neon");
}
for (auto x: rp.attentionOption) {
if (x != 0) {
fields.emplace_back("attentionOption");
break;
}
break;
}
if (!(rp.smeCoreNum.size() == 1 && rp.smeCoreNum[0] == runtimeParamsDefaults.smeCoreNum[0])) {
fields.emplace_back("smeCoreNum");
}
if (rp.useMmap) {
fields.emplace_back("useMmap");
}
if (tp.kvCache == "false") {
fields.emplace_back("test");
fields.emplace_back("t/s");
} else {
fields.emplace_back("llm_demo");
fields.emplace_back("speed(tok/s)");
}
if (tp.loadTime == "true") {
fields.emplace_back("loadingTime(s)");
}
fprintf(fout, "|");
for (const auto & field : fields) {
fprintf(fout, " %*s |", getFieldWidth(field), getFieldDisplayName(field).c_str());
}
fprintf(fout, "\n");
fprintf(fout, "|");
for (const auto & field : fields) {
int width = getFieldWidth(field);
fprintf(fout, " %s%s |", std::string(std::abs(width) - 1, '-').c_str(), width > 0 ? ":" : "-");
}
fprintf(fout, "\n");
}
void printPerformance(const TestInstance & t) override {
fprintf(fout, "|");
for (const auto & field : fields) {
std::string value;
char buf[128];
if (field == "model") {
value = t.modelType;
} else if (field == "modelSize") {
if (t.modelSize < 1024 * 1024 * 1024) {
snprintf(buf, sizeof(buf), "%.2f MiB", t.modelSize / 1024.0 / 1024.0);
} else {
snprintf(buf, sizeof(buf), "%.2f GiB", t.modelSize / 1024.0 / 1024.0 / 1024.0);
}
value = buf;
} else if (field == "backend") {
if (t.backend == 1) value = "METAL";
else if (t.backend == 2) value = "CUDA";
else if (t.backend == 3) value = "OPENCL";
else value = "CPU";
} else if (field == "test") {
if (t.nPrompt > 0 && t.nGenerate == 0) {
snprintf(buf, sizeof(buf), "pp%d", t.nPrompt);
} else if (t.nGenerate > 0 && t.nPrompt == 0) {
snprintf(buf, sizeof(buf), "tg%d", t.nGenerate);
} else {
snprintf(buf, sizeof(buf), "pp%d+tg%d", t.nPrompt, t.nGenerate);
}
value = buf;
} else if (field == "llm_demo") {
snprintf(buf, sizeof(buf), "prompt=%d<br>decode=%d", t.nPrompt, t.nGenerate);
value = buf;
} else if (field == "t/s") {
auto spd = t.getTokensPerSecond(t.nPrompt + t.nGenerate, t.samplesUs);
snprintf(buf, sizeof(buf), "%.2f ± %.2f", t.getAvgUs(spd), t.getStdevUs(spd));
value = buf;
} else if (field == "speed(tok/s)") {
auto decode_speed = t.getTokensPerSecond(t.nGenerates, t.decodeUs);
auto prefill_speed = t.getTokensPerSecond(t.nPrompt, t.prefillUs);
snprintf(buf, sizeof(buf), "%.2f ± %.2f<br>%.2f ± %.2f", t.getAvgUs(prefill_speed), t.getStdevUs(prefill_speed), t.getAvgUs(decode_speed), t.getStdevUs(decode_speed));
value = buf;
} else if (field == "precision") {
if (t.precision == 2) value = "Low";
else if (t.precision == 0) value = "Normal";
else value = "High";
} else if (field == "memory") {
if (t.memory == 2) value = "Low";
else if (t.memory == 0) value = "Normal";
else value = "High";
} else if (field == "power") {
if (t.power == 2) value = "Low";
else if (t.power == 0) value = "Normal";
else value = "High";
} else if (field == "threads") {
snprintf(buf, sizeof(buf), "%d", t.threads);
value = buf;
} else if (field == "loadingTime(s)") {
snprintf(buf, sizeof(buf), "%.2f ± %.2f", t.getAvgUs(t.loadingS), t.getStdevUs(t.loadingS));
value = buf;
} else if (field == "useMmap") {
if (t.useMmap) value = "true";
else value = "false";
} else if (field == "divisionRatioSme2Neon") {
snprintf(buf, sizeof(buf), "%d", t.divisionRatioSme2Neon);
value = buf;
} else if (field == "smeCoreNum") {
snprintf(buf, sizeof(buf), "%d", t.smeCoreNum);
value = buf;
} else if (field == "attentionOption") {
snprintf(buf, sizeof(buf), "%d", t.attentionOption);
// value = buf;
if (t.attentionOption == 1) {
value = "Int8 Q,K";
} else if (t.attentionOption == 2) {
value = "Int8 Q,K,V";
} else {
}
}
else {
assert(false);
MNN_ERROR("llm bench print fields error\n");
return;
}
int width = getFieldWidth(field);
if (field == "prefill&decode speed (tok/s)" || field == "t/s") {
// HACK: the utf-8 character is 2 bytes
width += 1;
}
fprintf(fout, " %*s |", width, value.c_str());
}
fprintf(fout, "\n");
}
};
struct jsonAggregator : public Printer {
std::vector<TestInstance> instances;
void printHeader(const RuntimeParameters & rp, const TestParameters & tp) override {
// No header for JSON
}
void printPerformance(const TestInstance & t) override {
instances.push_back(t);
}
~jsonAggregator() {
if (instances.empty() || !fout) return;
rapidjson::StringBuffer s;
rapidjson::Writer<rapidjson::StringBuffer> writer(s);
// Use the first instance for common config
const auto& t = instances[0];
writer.StartObject();
writer.Key("model");
writer.String(t.modelType.c_str());
writer.Key("modelSize");
writer.Double(t.modelSize / 1024.0 / 1024.0 / 1024.0); // GB
writer.Key("backend");
if (t.backend == 1) writer.String("METAL");
else if (t.backend == 3) writer.String("OPENCL");
else writer.String("CPU");
writer.Key("threads");
writer.Int(t.threads);
writer.Key("useMmap");
writer.Bool(t.useMmap);
writer.Key("precision");
writer.Int(t.precision);
writer.Key("memory");
writer.Int(t.memory);
writer.Key("power");
writer.Int(t.power);
writer.Key("attentionOption");
writer.Int(t.attentionOption);
// Store metrics as arrays to avoid duplicate keys
writer.Key("results");
writer.StartArray();
for (const auto& inst : instances) {
writer.StartObject();
// Case 1: Prefill (nPrompt > 0, nGenerate == 0)
if (inst.nPrompt > 0 && inst.nGenerate == 0) {
writer.Key("type");
writer.String("prefill");
writer.Key("prompt_len");
writer.Int(inst.nPrompt);
std::vector<double> speed;
if (!inst.prefillUs.empty()) {
speed = inst.getTokensPerSecond(inst.nPrompt, inst.prefillUs);
} else if (!inst.samplesUs.empty()) {
speed = inst.getTokensPerSecond(inst.nPrompt, inst.samplesUs);
}
if (!speed.empty()) {
writer.Key("tps");
writer.Double(inst.getAvgUs(speed));
writer.Key("std");
writer.Double(inst.getStdevUs(speed));
}
}
// Case 2: Decode (nGenerate > 0, nPrompt == 0)
else if (inst.nGenerate > 0 && inst.nPrompt == 0) {
writer.Key("type");
writer.String("decode");
writer.Key("generate_len");
writer.Int(inst.nGenerate);
std::vector<double> speed;
if (!inst.decodeUs.empty()) {
speed = inst.getTokensPerSecond(inst.nGenerates, inst.decodeUs);
} else if (!inst.samplesUs.empty()) {
speed = inst.getTokensPerSecond(inst.nGenerate, inst.samplesUs);
}
if (!speed.empty()) {
writer.Key("tps");
writer.Double(inst.getAvgUs(speed));
writer.Key("std");
writer.Double(inst.getStdevUs(speed));
}
}
// Case 3: Combined prefill+decode (demo mode)
else if (inst.nPrompt > 0 && inst.nGenerate > 0) {
writer.Key("type");
writer.String("prefill_and_decode");
writer.Key("prompt_len");
writer.Int(inst.nPrompt);
writer.Key("generate_len");
writer.Int(inst.nGenerate);
if (!inst.prefillUs.empty()) {
auto prefill_speed = inst.getTokensPerSecond(inst.nPrompt, inst.prefillUs);
writer.Key("prefill_tps");
writer.Double(inst.getAvgUs(prefill_speed));
writer.Key("prefill_std");
writer.Double(inst.getStdevUs(prefill_speed));
}
if (!inst.decodeUs.empty()) {
auto decode_speed = inst.getTokensPerSecond(inst.nGenerates, inst.decodeUs);
writer.Key("decode_tps");
writer.Double(inst.getAvgUs(decode_speed));
writer.Key("decode_std");
writer.Double(inst.getStdevUs(decode_speed));
}
}
// Loading time
if (!inst.loadingS.empty()) {
writer.Key("loading_time");
writer.Double(inst.getAvgUs(inst.loadingS));
writer.Key("loading_time_std");
writer.Double(inst.getStdevUs(inst.loadingS));
}
writer.EndObject();
}
writer.EndArray();
writer.EndObject();
fprintf(fout, "%s\n", s.GetString());
}
};
struct MultiPrinter : public Printer {
std::vector<std::unique_ptr<Printer>> printers;
void add(std::unique_ptr<Printer> p) {
printers.push_back(std::move(p));
}
void printHeader(const RuntimeParameters & rp, const TestParameters & tp) override {
for (auto& p : printers) {
p->printHeader(rp, tp);
}
}
void printPerformance(const TestInstance & t) override {
for (auto& p : printers) {
p->printPerformance(t);
}
}
~MultiPrinter() {
// unique_ptr automatically handles deletion
}
};
static FILE* openFile(const char* file, bool read) {
#if defined(_MSC_VER)
wchar_t wFilename[1024];
if (0 == MultiByteToWideChar(CP_ACP, 0, file, -1, wFilename, sizeof(wFilename))) {
return nullptr;
}
#if _MSC_VER >= 1400
FILE* mFile = nullptr;
if (read) {
if (0 != _wfopen_s(&mFile, wFilename, L"r")) {
return nullptr;
}
} else {
if (0 != _wfopen_s(&mFile, wFilename, L"a")) {
return nullptr;
}
}
return mFile;
#else
if (read) {
return _wfopen(wFilename, L"r");
} else {
return _wfopen(wFilename, L"a");
}
#endif
#else
if (read) {
return fopen(file, "r");
} else {
return fopen(file, "a");
}
#endif
return nullptr;
}
static std::vector<commandParametersInstance> get_cmd_params_instances(const RuntimeParameters & rp, const TestParameters& tp) {
std::vector<commandParametersInstance> instances;
// this ordering minimizes the number of times that each model needs to be reloaded
// clang-format off
for (const auto & m : rp.model)
for (const auto & backend : rp.backends)
for (const auto & precision : rp.precision)
for (const auto & memory : rp.memory)
for (const auto & power : rp.power)
for (const auto & nt : rp.threads)
for (const auto & dyop : rp.dynamicOption)
for (const auto &mratio: rp.divisionRatioSme2Neon)
for (const auto &smeNum: rp.smeCoreNum)
for (const auto & quantAttn : rp.attentionOption)
if (tp.kvCache == "true") { // MNN llm_demo test standard
for (const auto & nPrompt : tp.nPrompt) {
if (nPrompt == 0) {
continue;
}
for (const auto & nGenerate: tp.nGenerate) {
if (nGenerate == 0) {
continue;
}
CommandParameters tmpParam;
tmpParam.model = m;
tmpParam.backend = backend;
tmpParam.threads = nt;
tmpParam.power = power;
tmpParam.precision = precision;
tmpParam.memory = memory;
tmpParam.nPrompt = nPrompt;
tmpParam.nGenerate = nGenerate;
tmpParam.useMmap = rp.useMmap;
tmpParam.dynamicOption = dyop;
tmpParam.attentionOption = quantAttn;
tmpParam.nRepeat = tp.nRepeat[0];
tmpParam.kvCache = "true";
tmpParam.loadingTime = tp.loadTime;
tmpParam.divisionRatioSme2Neon = mratio;
tmpParam.smeCoreNum = smeNum;
auto instance = commandParametersInstance(tmpParam);
instances.push_back(instance);
}
}
} else { // llama.cpp llama-bench's test standard
for (const auto & nPrompt : tp.nPrompt) {
if (nPrompt == 0) {
continue;
}
CommandParameters tmpParam;
tmpParam.model = m;
tmpParam.nPrompt = nPrompt;
tmpParam.nGenerate = 0;
tmpParam.threads = nt;
tmpParam.useMmap = rp.useMmap;
tmpParam.backend = backend;
tmpParam.power = power;
tmpParam.precision = precision;
tmpParam.memory = memory;
tmpParam.dynamicOption = dyop;
tmpParam.attentionOption = quantAttn;
tmpParam.nRepeat = tp.nRepeat[0];
tmpParam.kvCache = "false";
tmpParam.loadingTime = tp.loadTime;
tmpParam.divisionRatioSme2Neon = mratio;
tmpParam.smeCoreNum = smeNum;
auto instance = commandParametersInstance(tmpParam);
instances.push_back(instance);
}
for (const auto & nGenerate: tp.nGenerate) {
CommandParameters tmpParam;
tmpParam.model = m;
tmpParam.nPrompt = 0;
tmpParam.nGenerate = nGenerate;
tmpParam.threads = nt;
tmpParam.useMmap = rp.useMmap;
tmpParam.backend = backend;
tmpParam.power = power;
tmpParam.precision = precision;
tmpParam.memory = memory;
tmpParam.dynamicOption = dyop;
tmpParam.attentionOption = quantAttn;
tmpParam.nRepeat = tp.nRepeat[0];
tmpParam.kvCache = "false";
tmpParam.loadingTime = tp.loadTime;
tmpParam.divisionRatioSme2Neon = mratio;
tmpParam.smeCoreNum = smeNum;
auto instance = commandParametersInstance(tmpParam);
instances.push_back(instance);
}
for (const auto & nPrompGen : tp.nPrompGen) {
if (nPrompGen.first == 0 && nPrompGen.second == 0) {
continue;
}
CommandParameters tmpParam;
tmpParam.model = m;
tmpParam.nPrompt = nPrompGen.first;
tmpParam.nGenerate = nPrompGen.second;
tmpParam.threads = nt;
tmpParam.useMmap = rp.useMmap;
tmpParam.backend = backend;
tmpParam.power = power;
tmpParam.precision = precision;
tmpParam.memory = memory;
tmpParam.dynamicOption = dyop;
tmpParam.attentionOption = quantAttn;
tmpParam.nRepeat = tp.nRepeat[0];
tmpParam.kvCache = "false";
tmpParam.loadingTime = tp.loadTime;
tmpParam.divisionRatioSme2Neon = mratio;
tmpParam.smeCoreNum = smeNum;
auto instance = commandParametersInstance(tmpParam);
instances.push_back(instance);
}
}
return instances;
}
std::string getDirectoryOf(const std::string& file_path, std::string& modelname) {
// weight filename
std::string weight_name = "llm.mnn.weight";
std::ifstream file(file_path.c_str());
std::string json_str((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
rapidjson::Document doc;
doc.Parse(json_str.c_str());
if (doc.HasMember("llm_weight") && doc["llm_weight"].IsString()) {
weight_name = doc["llm_weight"].GetString();
}
size_t pos = file_path.find_last_of("/\\");
if (pos == std::string::npos) {
MNN_ERROR("Invalid model config path\n");
return "";
}
auto dir = file_path.substr(0, pos);
pos = dir.find_last_of("/\\");
modelname = dir.substr(pos + 1, -1);
return MNNFilePathConcat(dir, weight_name);
}
static void printUsage(int /* argc */, char ** argv) {
printf("usage: %s [options]\n", argv[0]);
printf("\n");
printf("options:\n");
printf(" -h, --help\n");
printf(" -m, --model <filename> (default: ./Qwen2.5-1.5B-Instruct/config.json)\n");
printf(" -a, --backends <cpu,opencl,metal> (default: %s)\n", "cpu");
printf(" -c, --precision <n> (default: %s) | Note: (0:Normal(for cpu bakend, 'Normal' is 'High'),1:High,2:Low)\n", join(runtimeParamsDefaults.precision, ",").c_str());
printf(" -t, --threads <n> (default: %s)\n", join(runtimeParamsDefaults.threads, ",").c_str());
printf(" -p, --n-prompt <n> (default: %s)\n", join(testParamsDefaults.nPrompt, ",").c_str());
printf(" -n, --n-gen <n> (default: %s)\n", join(testParamsDefaults.nGenerate, ",").c_str());
printf(" -pg <pp,tg> (default: %s)\n", join(transform2String(testParamsDefaults.nPrompGen, pairString), ",").c_str());
printf(" -mmp, --mmap <0|1> (default: %s)\n", "0");
printf(" -rep, --n-repeat <n> (default: %s)\n", join(testParamsDefaults.nRepeat, ",").c_str());
printf(" -kv, --kv-cache <true|false> (default: %s) | Note: if true: Every time the LLM model generates a new word, it utilizes the cached KV-cache\n", "false");
printf(" -fp, --file-print <stdout|filename> (default: %s)\n", "stdout");
printf(" -scn, --sme-core-num <n> (default: 2) | Note: Specify the number of smeCoreNum to use.\n");
printf(" -load, --loading-time <true|false> (default: %s)\n", "true");
printf(" -dyo, --dynamicOption <n> (default: 0) | Note: if set 8, trades higher memory usage for better decoding performance\n");
printf(" -mr, --mixedSme2NeonRatio <n> (default: 41) | Note: This parameter is intended to optimize multi-threaded inference performance on backends that support Arm SME instructions. The optimal ratio may vary across different models; we recommend trying values such as 41, 49, 33.\n");
printf(" -qatten, --quant-attention <0|1> (default: 0) | Note: if 1, quantize attention's key value to int8; default 0\n");
printf(" -j, --json <filename> (default: llm_bench.json) | Note: if set, output result to a JSON file\n");
printf(" --profile Enable operator-level profiling to print detailed timing statistics\n");
}
static bool parseCmdParams(int argc, char ** argv, RuntimeParameters & runtimeParams, TestParameters & testParams, FILE** outfile, bool& helpInfo, bool& jsonMode, std::string& jsonFile, bool& enableProfile) {
std::string arg;
bool invalidParam = false;
const std::string argPrefix = "--";
const char splitDelim = ',';
runtimeParams.useMmap = runtimeParamsDefaults.useMmap;
testParams.kvCache = testParamsDefaults.kvCache;
testParams.loadTime = testParamsDefaults.loadTime;
for (int i = 1; i < argc; i++) {
arg = argv[i];
if (arg.compare(0, argPrefix.size(), argPrefix) == 0) {
std::replace(arg.begin(), arg.end(), '_', '-');
}
if (arg == "-h" || arg == "--help") {
printUsage(argc, argv);
helpInfo = true;
return true;
} else if (arg == "-m" || arg == "--model") {
if (++i >= argc) {
invalidParam = true;
break;
}
auto p = splitString<std::string>(argv[i], splitDelim);
runtimeParams.model.insert(runtimeParams.model.end(), p.begin(), p.end());
} else if (arg == "-p" || arg == "--n-prompt") {
if (++i >= argc) {
invalidParam = true;
break;
}
auto p = splitString<int>(argv[i], splitDelim);
testParams.nPrompt.insert(testParams.nPrompt.end(), p.begin(), p.end());
} else if (arg == "-n" || arg == "--n-gen") {
if (++i >= argc) {
invalidParam = true;
break;
}
auto p = splitString<int>(argv[i], splitDelim);
testParams.nGenerate.insert(testParams.nGenerate.end(), p.begin(), p.end());
} else if (arg == "-pg") {
if (++i >= argc) {
invalidParam = true;
break;
}
auto p = splitString<std::string>(argv[i], ',');
if (p.size() != 2) {
invalidParam = true;
break;
}
testParams.nPrompGen.push_back({ std::stoi(p[0]), std::stoi(p[1]) });
} else if (arg == "-a" || arg == "--backends") {
if (++i >= argc) {
invalidParam = true;
break;
}
auto ba = splitString<std::string>(argv[i], splitDelim);
std::vector<int> p;
for (auto& type: ba) {
if (type == "metal") {
p.emplace_back(1);
} else if (type == "cuda") {
p.emplace_back(2);
} else if (type == "opencl") {
p.emplace_back(3);
} else {
p.emplace_back(0);
}
}
runtimeParams.backends.insert(runtimeParams.backends.end(), p.begin(), p.end());
} else if (arg == "-t" || arg == "--threads") {
if (++i >= argc) {
invalidParam = true;
break;
}
auto p = splitString<int>(argv[i], splitDelim);
std::sort(p.begin(), p.end(), std::greater<int>());
runtimeParams.threads.insert(runtimeParams.threads.end(), p.begin(), p.end());
} else if (arg == "-mmp" || arg == "--mmap") {
if (++i >= argc) {
invalidParam = true;
break;
}
auto p = splitString<bool>(argv[i], splitDelim);
runtimeParams.useMmap = p[0];
} else if (arg == "-c" || arg == "--precision") {
if (++i >= argc) {
invalidParam = true;
break;
}
auto p = splitString<int>(argv[i], splitDelim);
runtimeParams.precision.insert(runtimeParams.precision.end(), p.begin(), p.end());
} else if (arg == "--memory") {
if (++i >= argc) {
invalidParam = true;
break;
}
auto p = splitString<int>(argv[i], splitDelim);
runtimeParams.memory.insert(runtimeParams.memory.end(), p.begin(), p.end());
} else if (arg == "--power") {
if (++i >= argc) {
invalidParam = true;
break;
}
auto p = splitString<int>(argv[i], splitDelim);
runtimeParams.power.insert(runtimeParams.power.end(), p.begin(), p.end());
} else if (arg == "-dyo" || arg == "--dynamicOption") {
if (++i >= argc) {
invalidParam = true;
break;
}
auto p = splitString<int>(argv[i], splitDelim);
runtimeParams.dynamicOption.insert(runtimeParams.dynamicOption.end(), p.begin(), p.end());
} else if (arg == "-rep" || arg == "--n-repeat") {
if (++i >= argc) {
invalidParam = true;
break;
}
auto p = splitString<int>(argv[i], splitDelim);
testParams.nRepeat.insert(testParams.nRepeat.end(), p.begin(), p.end());
} else if (arg == "-kv" || arg == "--kv-cache") {
if (++i >= argc) {
invalidParam = true;
break;
}
auto p = splitString<std::string>(argv[i], splitDelim);
testParams.kvCache = p[0];
} else if (arg == "-fp" || arg == "--file-print") {
if (++i >= argc) {
invalidParam = true;
break;
}
auto p = splitString<std::string>(argv[i], splitDelim);
if (!MNNFileExist(p[0].c_str())) {
MNNCreateFile(p[0].c_str());
}
*outfile = openFile(p[0].c_str(), false);
} else if (arg == "-load" || arg == "--loading-time") {
if (++i >= argc) {
invalidParam = true;
break;
}
auto p = splitString<std::string>(argv[i], splitDelim);
testParams.loadTime = p[0];
} else if (arg == "-mr" || arg == "--miexdSme2NeonRatio") {
if (++i >= argc) {
invalidParam = true;
break;
}
auto p = splitString<int>(argv[i], splitDelim);
runtimeParams.divisionRatioSme2Neon.insert(runtimeParams.divisionRatioSme2Neon.end(), p.begin(), p.end());
} else if (arg == "-scn" || arg == "--sme-core-num") {
if (++i >= argc) {
invalidParam = true;
break;
}
auto p = splitString<int>(argv[i], splitDelim);
runtimeParams.smeCoreNum.insert(runtimeParams.smeCoreNum.end(), p.begin(), p.end());
} else if (arg == "-qatten" || arg == "--quant-attention") {
// do nothing, reserved for future use
if (++i >= argc) {
invalidParam = true;
break;
}
auto p = splitString<int>(argv[i], splitDelim);
runtimeParams.attentionOption.insert(runtimeParams.attentionOption.end(), p.begin(), p.end());
} else if (arg == "-j" || arg == "--json") {
jsonMode = true;
if (i + 1 < argc && argv[i+1][0] != '-') {
jsonFile = argv[++i];
}
} else if (arg == "--profile") {
enableProfile = true;
}
else {
invalidParam = true;
break;
}
} // parse end
if (invalidParam) {
fprintf(stderr, "error: invalid parameter for argument: %s\n", arg.c_str());
printUsage(argc, argv);
return false;
}
// set defaults
if (runtimeParams.model.empty()) {
runtimeParams.model = runtimeParamsDefaults.model;
}
if (testParams.nPrompt.empty()) {
testParams.nPrompt = testParamsDefaults.nPrompt;
}
if (testParams.nGenerate.empty()) {
testParams.nGenerate = testParamsDefaults.nGenerate;
}
if (testParams.nPrompGen.empty()) {
testParams.nPrompGen = testParamsDefaults.nPrompGen;
}
if (runtimeParams.backends.empty()) {
runtimeParams.backends = runtimeParamsDefaults.backends;
}
if (runtimeParams.memory.empty()) {
runtimeParams.memory = runtimeParamsDefaults.memory;
}
if (runtimeParams.precision.empty()) {
runtimeParams.precision = runtimeParamsDefaults.precision;
}
if (runtimeParams.power.empty()) {
runtimeParams.power = runtimeParamsDefaults.power;
}
if (runtimeParams.threads.empty()) {
runtimeParams.threads = runtimeParamsDefaults.threads;
}
if (runtimeParams.dynamicOption.empty()) {
runtimeParams.dynamicOption = runtimeParamsDefaults.dynamicOption;
}
if (runtimeParams.divisionRatioSme2Neon.empty()) {
runtimeParams.divisionRatioSme2Neon = runtimeParamsDefaults.divisionRatioSme2Neon;
}
if (runtimeParams.smeCoreNum.empty()) {
runtimeParams.smeCoreNum = runtimeParamsDefaults.smeCoreNum;
}
if (runtimeParams.attentionOption.empty()) {
runtimeParams.attentionOption = runtimeParamsDefaults.attentionOption;
}
if (testParams.nRepeat.empty()) {
testParams.nRepeat = testParamsDefaults.nRepeat;
}
return true;
}
static Llm* buildLLM(const std::string& config_path, int backend, int memory, int precision, int threads, int power, int dynamic_option, bool use_mmap, int divisionRatioSme2Neon, int smeCoreNum, int promptLen, int attention_mode) {
auto llmPtr = Llm::createLLM(config_path);
llmPtr->set_config(R"({
"async":false
})");
// "Set reuse_kv=false for multiple test runs.
// Otherwise, mContext->history_tokens retains data after the first run, skewing true prefill performance metrics."
llmPtr->set_config(R"({"reuse_kv":false})");
std::map<int, std::string> lever = {{0,"normal"}, {1, "high"}, {2, "low"}};
std::map<int, std::string> backend_type = {{0, "cpu"}, {1, "metal"}, {2, "cuda"}, {3, "opencl"}};
std::map<bool, std::string> mmap = {{true,"true"}, {false, "false"}};
bool setSuccess = true;
setSuccess &= llmPtr->set_config("{\"precision\":\"" + lever[precision] + "\"}");
if (!setSuccess) {
MNN_ERROR("precison for LLM config set error\n");
return nullptr;
}
setSuccess &= llmPtr->set_config("{\"memory\":\"" + lever[memory] + "\"}");
if (!setSuccess) {
MNN_ERROR("memory for LLM config set error\n");
return nullptr;
}
setSuccess &= llmPtr->set_config("{\"power\":\"" + lever[power] + "\"}");
if (!setSuccess) {
MNN_ERROR("power for LLM config set error\n");
return nullptr;
}
setSuccess &= llmPtr->set_config("{\"backend_type\":\"" + backend_type[backend] + "\"}");
if (!setSuccess) {
MNN_ERROR("backend_type for LLM config set error\n");
return nullptr;
}
setSuccess &= llmPtr->set_config("{\"thread_num\":" + std::to_string(threads) + "}");
if (!setSuccess) {
MNN_ERROR("thread_num for LLM config set error\n");
return nullptr;
}
auto doy = (promptLen <= 300 && promptLen != 0) ? (dynamic_option % 8) : (dynamic_option % 8 + 8);
setSuccess &= llmPtr->set_config("{\"dynamic_option\":" + std::to_string(doy) + "}");
if (!setSuccess) {
MNN_ERROR("dynamic_option for LLM config set error\n");
return nullptr;
}
setSuccess &= llmPtr->set_config("{\"attention_mode\":" + std::to_string(attention_mode + 8) + "}");
if (!setSuccess) {
MNN_ERROR("attention_mode for LLM config set error\n");
return nullptr;
}
setSuccess &= llmPtr->set_config("{\"use_mmap\":" + mmap[use_mmap] + "}");
if (!setSuccess) {
MNN_ERROR("use_mmap for LLM config set error\n");
return nullptr;
}
setSuccess &= llmPtr->set_config("{\"tmp_path\":\"tmp\"}");
if (!setSuccess) {
MNN_ERROR("tmp_path for LLM config set error\n");
return nullptr;
}
setSuccess &= llmPtr->set_config("{\"cpu_sme2_neon_division_ratio\":" + std::to_string(divisionRatioSme2Neon) + "}");
if (!setSuccess) {
MNN_ERROR("cpu_sme2_neon_division_ratio for LLM config set error\n");
return nullptr;
}
setSuccess &= llmPtr->set_config("{\"cpu_sme_core_num\":" + std::to_string(smeCoreNum) + "}");
if (!setSuccess) {
MNN_ERROR("cpu_sme_core_num for LLM config set error\n");
return nullptr;
}
return llmPtr;
}
static void tuning_prepare(Llm* llm) {
llm->tuning(OP_ENCODER_NUMBER, {1, 5, 10, 20, 30, 50, 100});
}
int main(int argc, char ** argv) {
RuntimeParameters runtimeParams;
TestParameters testParams;
FILE* outfile = stdout;
bool helpInfo = false;
bool jsonMode = false;
std::string jsonFile = "llm_bench.json";
bool enableProfile = false;
bool parseSuccess = parseCmdParams(argc, argv, runtimeParams, testParams, &outfile, helpInfo, jsonMode, jsonFile, enableProfile);
if (!parseSuccess) {
MNN_ERROR("Parse arguments error\n");
return -1;
}
if (parseSuccess && helpInfo) {
return 0;
}
std::vector<commandParametersInstance> paramsInstances = get_cmd_params_instances(runtimeParams, testParams);
// Setup printers using smart pointers
std::unique_ptr<MultiPrinter> multiPrinter(new MultiPrinter());
// Always add markdown printer for stdout
std::unique_ptr<markdownPrinter> mdPrinter(new markdownPrinter());
mdPrinter->fout = outfile;
multiPrinter->add(std::move(mdPrinter));
// If json mode, add json printer for file output
if (jsonMode) {
FILE* fp = fopen(jsonFile.c_str(), "w");
if (fp) {
std::unique_ptr<jsonAggregator> jPrinter(new jsonAggregator());
jPrinter->fout = fp;
multiPrinter->add(std::move(jPrinter));
} else {
MNN_ERROR("Failed to open %s for writing\n", jsonFile.c_str());
}
}
std::unique_ptr<Printer> printer_ = std::move(multiPrinter);
bool printHeader = true;
for (const auto & instance: paramsInstances) {
TestInstance t(instance);
auto llmWeightPath = getDirectoryOf(t.modelConfigFile, t.modelType); // To check path
file_t file = MNNOpenFile(llmWeightPath.c_str(), MNN_FILE_READ);
t.modelSize = MNNGetFileSize(file);
MNN::BackendConfig backendConfig;
// Map backend parameter to MNN forward type (0=CPU, 1=METAL, 2=CUDA, 3=OPENCL)
MNNForwardType forwardType = static_cast<MNNForwardType>(instance.mCmdParam.backend);
auto executor = MNN::Express::Executor::newExecutor(forwardType, backendConfig, 1);
MNN::Express::ExecutorScope scope(executor);
auto llmPtr = buildLLM(instance.mCmdParam.model, instance.mCmdParam.backend, instance.mCmdParam.memory, instance.mCmdParam.precision, instance.mCmdParam.threads, instance.mCmdParam.power, instance.mCmdParam.dynamicOption, instance.mCmdParam.useMmap, instance.mCmdParam.divisionRatioSme2Neon, instance.mCmdParam.smeCoreNum, instance.mCmdParam.nPrompt, instance.mCmdParam.attentionOption);
std::unique_ptr<Llm> llm(llmPtr);
if (enableProfile) {
llm->set_config(R"({"enable_debug":true})");
auto profiler = MNN::Profiler::getInstance();
llm->setDebugCallback(
[profiler](const std::vector<MNN::Tensor*>& inputs, const MNN::OperatorInfo* info) {
profiler->start(info);
return true;
},
[profiler](const std::vector<MNN::Tensor*>& outputs, const MNN::OperatorInfo* info) {
for (auto o : outputs) {
o->wait(MNN::Tensor::MAP_TENSOR_READ, true);
}
profiler->end(info);
return true;
}
);
}
if (instance.mCmdParam.loadingTime == "true") {
for (int k = 0; k < 3; ++k) {
Timer loadingCost;
llm->load();
t.loadingS.push_back((double)loadingCost.durationInUs() / 1e6);
}
} else {
llm->load();
}
tuning_prepare(llm.get());
auto context = llm->getContext();
// Ensure GPU sync for accurate timing
llm->set_config("{\"async\":false}");
if (instance.mCmdParam.nGenerate > 0) {
llm->set_config("{\"max_new_tokens\":1}");
}
auto prompt_tokens = instance.mCmdParam.nPrompt;
auto decodeTokens = instance.mCmdParam.nGenerate;
bool isOpenCL = (instance.mCmdParam.backend == 3); // MNN_FORWARD_OPENCL
// llm_demo test
if (instance.mCmdParam.kvCache == "true") {
std::vector<int> tokens(prompt_tokens, 16);
for (int i = 0; i < instance.mCmdParam.nRepeat + 1; ++i) {
// switchMode handles OpenCL record queue: off for prefill, on for decode
if (isOpenCL) {
llm->switchMode(Llm::Prefill);
}
llm->response(tokens, nullptr, nullptr, decodeTokens);
auto prefillTime = context->prefill_us;
auto decodeTime = context->decode_us;
if (i > 0) { // Exclude the first performance value.
t.prefillUs.push_back(prefillTime);
t.decodeUs.push_back(decodeTime);
if (llm->stoped()) {
t.nGenerates.push_back(context->gen_seq_len - 1);
} else {
t.nGenerates.push_back(context->gen_seq_len);
}
}
}
if (printHeader) {
printer_->printHeader(runtimeParams, testParams);
printHeader = false;
}
printer_->printPerformance(t);
// Cool
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
// llama.cpp llama-bench test
if (instance.mCmdParam.kvCache == "false") {
int tok = 16;
std::vector<int> tokens(prompt_tokens, tok);
std::vector<int> tokens1(1, tok);
for (int i = 0; i < instance.mCmdParam.nRepeat + 1; ++i) {
int64_t sampler_us = 0;
if (prompt_tokens) {
// Disable record queue during prefill for OpenCL
if (isOpenCL) {
llm->switchMode(Llm::Prefill);
}
llm->response(tokens, nullptr, nullptr, 1);
sampler_us += context->prefill_us;
}
if (decodeTokens) {
// Enable record queue during decode for OpenCL
if (isOpenCL) {
llm->switchMode(Llm::Decode);
}
llm->response(tokens1, nullptr, nullptr, decodeTokens);
sampler_us += context->decode_us;
}
if (i > 0) {
t.samplesUs.push_back(sampler_us);
}
}
if (printHeader) {
printer_->printHeader(runtimeParams, testParams);
printHeader = false;
}
printer_->printPerformance(t);
// Cool
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
}
if (enableProfile) {
auto profiler = MNN::Profiler::getInstance();
fprintf(stdout, "\n========== Operator Profile Results ==========\n");
// profiler->printTimeByName(1);
profiler->printTimeByType(1);
}
fprintf(stdout, "\n");
if (outfile != stdout) {
fclose(outfile);
}
return 0;
}