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

326 lines
12 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// VulkanCoopMatSpeed.cpp
// MNNTests
//
// Generic GEMM/GEMV performance benchmark via Conv1x1.
// Supports CPU / OpenCL / Vulkan backends.
// Supports float / int8(block0) / int4(block64) weight quantization.
//
// Usage:
// # CPU backend (default)
// ./run_test.out speed/GemmSpeedAll
//
// # Vulkan backend (type=7), precision=Low(2)
// ./run_test.out speed/GemmSpeedAll 7 2
//
// # OpenCL buffer mode: type=3, precision=2, numthread=68
// # (68 = MNN_GPU_MEMORY_BUFFER(64) | MNN_GPU_TUNING_FAST(4))
// ./run_test.out speed/GemmSpeedAll 3 2 68
//
// # Individual tests:
// ./run_test.out speed/GemmSpeedFloat 7 2
// ./run_test.out speed/GemmSpeedInt8 7 2
// ./run_test.out speed/GemmSpeedInt4 7 2
//
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
#include <MNN/expr/Expr.hpp>
#include <MNN/expr/ExprCreator.hpp>
#include <MNN/expr/Executor.hpp>
#include <MNN/expr/ExecutorScope.hpp>
#include "MNNTestSuite.h"
#include "MNN_generated.h"
#include "CommonOpCreator.hpp"
#include <MNN/MNNForwardType.h>
#define MNN_OPEN_TIME_TRACE
#include <MNN/AutoTime.hpp>
using namespace MNN;
using namespace MNN::Express;
// ---------------------------------------------------------------------------
// Helper: print current backend configuration
// ---------------------------------------------------------------------------
static void printBackendConfig(int forwardType, int precision, int thread) {
const char* backendName = "CPU";
switch (forwardType) {
case MNN_FORWARD_OPENCL:
backendName = "OpenCL";
break;
case MNN_FORWARD_VULKAN:
backendName = "Vulkan";
break;
case MNN_FORWARD_METAL:
backendName = "Metal";
break;
case MNN_FORWARD_CUDA:
backendName = "CUDA";
break;
default:
break;
}
MNN_PRINT("Backend: %s (type=%d), precision=%d, numthread=%d\n", backendName, forwardType, precision, thread);
if (forwardType == MNN_FORWARD_OPENCL && thread > 0) {
bool isBuffer = (thread & MNN_GPU_MEMORY_BUFFER) != 0;
MNN_PRINT(" OpenCL memory mode: %s\n", isBuffer ? "BUFFER" : "IMAGE");
}
}
// ---------------------------------------------------------------------------
// Mode 0: Float Conv1x1
// ---------------------------------------------------------------------------
static VARP buildFloatConv1x1(VARP x, int ic, int oc) {
std::vector<float> weight(oc * ic);
for (int i = 0; i < (int)weight.size(); ++i) {
weight[i] = ((float)(i % 127) - 63.0f) / 1000.0f;
}
std::vector<float> bias(oc, 0.0f);
return _Conv(std::move(weight), std::move(bias), x, {ic, oc}, {1, 1}, PaddingMode::VALID, {1, 1}, {1, 1}, 1, {0, 0},
false, false);
}
// ---------------------------------------------------------------------------
// Mode 1: Int4-block64 quantized Conv1x1 (asymmetric, via _HybridConv)
// Mode 2: Int8-block0 quantized Conv1x1 (asymmetric, blockSize=K, via _HybridConv)
// ---------------------------------------------------------------------------
static VARP buildHybridConv1x1(VARP x, int ic, int oc, int nbit, int blockSize) {
MNN_ASSERT(ic % blockSize == 0);
int blockNum = ic / blockSize;
// Generate float weights
std::vector<float> weightFp32(oc * ic);
float fac = 0.23f;
for (int i = 0; i < oc; ++i) {
for (int j = 0; j < ic; ++j) {
weightFp32[i * ic + j] = ((i * ic + j) % nbit) * fac;
}
}
// Generate asymmetric scale: alpha = [offset0, scale0, offset1, scale1, ...]
// Layout: oc * blockNum pairs
std::vector<float> wScale(2 * oc * blockNum);
for (int k = 0; k < oc; ++k) {
for (int b = 0; b < blockNum; ++b) {
wScale[2 * (k * blockNum + b)] = -0.5f; // offset
wScale[2 * (k * blockNum + b) + 1] = 0.01f; // scale
}
}
std::vector<float> bias(oc, 0.0f);
return _HybridConv(weightFp32, std::move(bias), wScale, x, {ic, oc}, {1, 1}, PaddingMode::CAFFE, {1, 1}, {1, 1}, 1,
{0, 0}, false, false, nbit, true);
}
// ---------------------------------------------------------------------------
// Benchmark runner
// mode: 0=float, 1=int4-block64, 2=int8-block0
// ---------------------------------------------------------------------------
static void benchConv1x1(const char* tag, int M, int K, int N, int mode, int forwardType, int precision, int thread) {
// Create executor with Memory_Low for quantized modes
BackendConfig bnConfig;
bnConfig.precision = (BackendConfig::PrecisionMode)precision;
bnConfig.memory = BackendConfig::Memory_Low;
auto exe = Executor::newExecutor((MNNForwardType)forwardType, bnConfig, thread);
ExecutorScope scope(exe);
// Input: {batch=1, channel=K, height=1, width=M}
auto x = _Input({1, K, 1, M}, NC4HW4, halide_type_of<float>());
VARP y;
switch (mode) {
case 0:
y = buildFloatConv1x1(x, K, N);
break;
case 1:
y = buildHybridConv1x1(x, K, N, /*nbit=*/4, /*blockSize=*/64);
break;
case 2:
y = buildHybridConv1x1(x, K, N, /*nbit=*/8, /*blockSize=*/K);
break;
default:
MNN_ASSERT(false);
return;
}
x.fix(VARP::INPUT);
// Warm up (multiple rounds to stabilize GPU clocks and caches)
for (int w = 0; w < 3; ++w) {
auto xPtr = x->writeMap<float>();
::memset(xPtr, 0, x->getInfo()->size * sizeof(float));
y->readMap<float>();
}
// Benchmark
const int LOOP = 10;
auto executor = ExecutorScope::Current();
{
MNN::Timer _t;
for (int i = 0; i < LOOP; ++i) {
x->writeMap<float>();
y->readMap<float>();
}
float totalMs = (float)_t.durationInUs() / 1000.0f;
float avgMs = totalMs / (float)LOOP;
// Compute GFLOPS: GEMM is 2*M*K*N FLOPs
double flops = 2.0 * (double)M * (double)K * (double)N;
double gflops = flops / (avgMs * 1e6);
// Try to get GPU timestamp-based time from the last execution
float gpuMs = executor->getLastGpuTimeMs();
if (gpuMs > 0.0f) {
double gpuGflops = flops / ((double)gpuMs * 1e6);
MNN_PRINT(" %-28s M=%-5d K=%-5d N=%-5d total=%.3f ms (%.2f GFLOPS) gpu=%.3f ms (%.2f GFLOPS)\n", tag, M,
K, N, avgMs, gflops, gpuMs, gpuGflops);
} else {
MNN_PRINT(" %-28s M=%-5d K=%-5d N=%-5d avg=%.3f ms %.2f GFLOPS\n", tag, M, K, N, avgMs, gflops);
}
}
}
// ---------------------------------------------------------------------------
// Common shape configurations (typical LLM dimensions)
//
// Each entry represents a (K, N) pair for Conv1x1 = GEMM [M, K] × [K, N].
// maxM: maximum M value to test (0 = use all default M values).
// Set to 1 for very large shapes where only GEMV (decode) is practical.
// ---------------------------------------------------------------------------
struct ShapeConfig {
int K;
int N;
int maxM; // 0 = all M values, >0 = only test M <= maxM
const char* label; // annotation for output
};
static const std::vector<ShapeConfig>& defaultConfigs() {
static std::vector<ShapeConfig> configs = {
{2560, 4096, 0, "K=2560 N=4096"}, {2560, 1024, 0, "K=2560 N=1024"}, {4096, 2560, 0, "K=4096 N=2560"},
{2560, 9728, 0, "K=2560 N=9728"}, {9728, 2560, 0, "K=9728 N=2560"},
};
return configs;
}
// M values: GEMM(prefill) only, no GEMV(decode)
static const std::vector<int>& defaultMValues() {
static std::vector<int> mValues = {8, 32, 128, 512};
return mValues;
}
// ---------------------------------------------------------------------------
// Test: Float GEMM
// ---------------------------------------------------------------------------
class GemmSpeedFloat : public MNNTestCase {
public:
virtual bool run(int precision) override {
auto& st = MNNTestSuite::get()->pStaus;
MNN_PRINT("\n===== Float Conv1x1 GEMM Speed =====\n");
printBackendConfig(st.forwardType, st.precision, st.thread);
for (auto& cfg : defaultConfigs()) {
MNN_PRINT("--- %s ---\n", cfg.label);
for (int m : defaultMValues()) {
if (cfg.maxM > 0 && m > cfg.maxM)
continue;
benchConv1x1("float-gemm", m, cfg.K, cfg.N, 0, st.forwardType, st.precision, st.thread);
}
}
return true;
}
};
// ---------------------------------------------------------------------------
// Test: Int8-block0 GEMM
// ---------------------------------------------------------------------------
class GemmSpeedInt8 : public MNNTestCase {
public:
virtual bool run(int precision) override {
auto& st = MNNTestSuite::get()->pStaus;
MNN_PRINT("\n===== Int8-Block0 Conv1x1 GEMM Speed =====\n");
printBackendConfig(st.forwardType, st.precision, st.thread);
for (auto& cfg : defaultConfigs()) {
MNN_PRINT("--- %s ---\n", cfg.label);
for (int m : defaultMValues()) {
if (cfg.maxM > 0 && m > cfg.maxM)
continue;
benchConv1x1("int8b0-gemm", m, cfg.K, cfg.N, 2, st.forwardType, st.precision, st.thread);
}
}
return true;
}
};
// ---------------------------------------------------------------------------
// Test: Int4-block64 GEMM
// ---------------------------------------------------------------------------
class GemmSpeedInt4 : public MNNTestCase {
public:
virtual bool run(int precision) override {
auto& st = MNNTestSuite::get()->pStaus;
MNN_PRINT("\n===== Int4-Block64 Conv1x1 GEMM Speed =====\n");
printBackendConfig(st.forwardType, st.precision, st.thread);
for (auto& cfg : defaultConfigs()) {
if (cfg.K % 64 != 0)
continue;
MNN_PRINT("--- %s ---\n", cfg.label);
for (int m : defaultMValues()) {
if (cfg.maxM > 0 && m > cfg.maxM)
continue;
benchConv1x1("int4b64-gemm", m, cfg.K, cfg.N, 1, st.forwardType, st.precision, st.thread);
}
}
return true;
}
};
// ---------------------------------------------------------------------------
// Combined test: run all modes in one shot
// ---------------------------------------------------------------------------
class GemmSpeedAll : public MNNTestCase {
public:
virtual bool run(int precision) override {
auto& st = MNNTestSuite::get()->pStaus;
MNN_PRINT("\n===== All GEMM Speed Benchmark =====\n");
printBackendConfig(st.forwardType, st.precision, st.thread);
std::vector<int> mValues = {8, 32, 128, 512};
// mode: 0=float, 1=int4b64, 2=int8b0
const char* modeNames[] = {"float", "int8b0", "int4b64"};
int modes[] = {0, 2, 1};
const int numModes = 3;
for (auto& cfg : defaultConfigs()) {
MNN_PRINT("\n--- %s ---\n", cfg.label);
for (int mi = 0; mi < numModes; ++mi) {
// Skip int4 modes if K is not divisible by 64
if (modes[mi] == 1 && cfg.K % 64 != 0)
continue;
for (int m : mValues) {
if (cfg.maxM > 0 && m > cfg.maxM)
continue;
char tag[64];
snprintf(tag, sizeof(tag), "%s-gemm", modeNames[mi]);
benchConv1x1(tag, m, cfg.K, cfg.N, modes[mi], st.forwardType, st.precision, st.thread);
}
}
}
return true;
}
};
// Register all test cases
MNNTestSuiteRegister(GemmSpeedFloat, "speed/GemmSpeedFloat");
MNNTestSuiteRegister(GemmSpeedInt8, "speed/GemmSpeedInt8");
MNNTestSuiteRegister(GemmSpeedInt4, "speed/GemmSpeedInt4");
MNNTestSuiteRegister(GemmSpeedAll, "speed/GemmSpeedAll");