mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-09-12 16:09:01 +02:00
a2878d30df
* metal : rework fusion patterns into a single table All fusable op patterns for the Metal backend are now declared once in a fusion table (ggml-metal-fuse.cpp) and consumed by both the graph optimizer (ggml_metal_fuse_max, packing) and the op encoders (ggml_metal_fuse_next, compute). The two phases share the same pattern table plus ggml_can_fuse_subgraph_ext for the structural checks, and differ only in the mode used for the pattern check (STRUCTURAL at optimize time, since tensors are not allocated yet, and FULL at compute time, including Metal buffer placement). This also protects the snake activation (MUL + SIN + SQR + MUL + ADD) from being reordered during graph optimization, which was previously unprotected. Assisted-by: pi:llama.cpp/DeepSeek-V4-Flash-0731 * metal : fix absolute output indices in fusion patterns ggml_can_fuse_subgraph_ext expects the outputs array to contain absolute graph node indices (it indexes cgraph->nodes[outputs[i]]), but the fusion table query was passing a relative index (n_ops - 1). As a result the last node of every pattern was not recognized as an output and was subjected to the elidable use-count check, which failed for essentially all fusions. This silently disabled the norm/MUL fusion and caused a ~5% token-generation regression. Pass the absolute graph index of the last node instead. Assisted-by: pi:llama.cpp/DeepSeek-V4-Flash-0731 * metal : fuse gated_delta_net with cache cpy Add GGML_METAL_FUSE_GDN_CACHE to the fusion table: when the gated_delta_net kernel is followed by a cpy that scatters its recurrent state snapshots into the KV cache, the kernel writes the snapshots straight into the cache buffer and the trailing cpy is elided. The gdn output has other consumers (the attn scores view), so unlike the elision-chain patterns this is not a simple chain: a 'raw' flag on the fusion pattern skips the generic chain/shape and ggml_can_fuse_subgraph_ext checks, making the pattern-specific check callback the sole validator. Packing (ggml_metal_fuse_max) now matches on the same view-transparent node sequence that the compute phase uses, so the gdn + cache cpy group is packed along with any intermediate views and stays adjacent through the reorder. The fused cpy is a view consumer of the gdn (it writes the cache directly), so its mem-range is skipped in the encoder; the skip is restricted to CPY nodes consuming the previous fused node through a view so other fusions are unaffected. Add test_gated_delta_net_cache_fusion and register 5 cases. Assisted-by: pi:llama.cpp/DeepSeek-V4-Flash-0731 * metal : drop is_view_consumer mem-range skip The is_view_consumer skip was carried over from the upstream gated_delta_net cache-fusion draft, but it is not needed: keeping the elided cpy's mem-range in the concurrency tracker only ever adds a (conservative) memory barrier at the fusion point. It can never remove a barrier, so it cannot introduce a race. The worst case is one spurious barrier per gdn+cache-cpy fusion, which is within run-to-run noise on Qwen3.5-0.8B Q8_0. Dropping the check keeps the mem-range loop uniform for all fused groups. Assisted-by: pi:llama.cpp/DeepSeek-V4-Flash-0731 * metal : rename gated_delta_net fused state output args Rename the fused cache-write kernel argument to match the rest of the kargs: state_out_stride -> nb_out (and widen it to uint64_t), and the local buffer id bid_state_out -> bid_out. Assisted-by: pi:llama.cpp/DeepSeek-V4-Flash-0731 * metal : rename raw fusion flag to unsafe raw did not convey that the flag opts a fusion pattern out of the generic elision-chain safety net (ggml_can_fuse_subgraph_ext + chain/shape checks). rename it to 'unsafe' to make explicit that the pattern's check callback is the sole validator and must re-establish the safety guarantees itself. Assisted-by: pi:llama.cpp/DeepSeek-V4-Flash-0731 * metal : tidy fusion pattern checks and table - const-correct ggml_metal_fuse_outputs buffer - annotate unused check-callback parameters - drop a redundant size_t cast - align the ops/table initializers and add blank-line separation Assisted-by: pi:llama.cpp/DeepSeek-V4-Flash-0731 * metal : add generic fusion stats via ad-hoc proc-address API Add a device-owned fusion context that lets a test tool count how many times each fusion pattern fires and toggle fusion. It is exposed through the ad-hoc ggml_backend_reg_get_proc_address mechanism with generic names so the testing tool is backend-agnostic: - ggml_backend_fusion_stats_init: start collecting fusion stats; when a context is created afterwards it registers the labels/counters and encodes single-threaded (n_cb == 0) so the counters are race-free - ggml_backend_fusion_stats_reset / _get_stats / _set_enabled The context lives on the metal device (not on the last backend context), so counters accumulate across contexts and reads are always consistent. The enable/disable toggle is initialized from GGML_METAL_FUSION_DISABLE and can be overridden by the test through set_enabled. Labels are synthesized from the fuse table via ggml_metal_fuse_label (e.g. "GATED_DELTA_NET+CPY"). Assisted-by: pi:llama.cpp/DeepSeek-V4-Flash-0731 * tests : add fusion count regression test with per-backend baseline test-fusion runs every dummy model generated by test-llama-archs on a single backend (single-threaded encoding, n_cb == 0) with fusion enabled and disabled, and for each mode (prefill / decode) reports the per-fusion counters and the NMSE between the fused and unfused logits, plus the NMSE against a CPU reference. A fusion pattern that silently stops matching (or fires when it should not) is caught as a regression by comparing the counters against a committed per-backend TSV baseline: - --record writes the golden baseline, --check (default) validates it - the unfused run doubles as a control: its counters must be all-zero - NMSE is skipped when it is NaN or the arch is already broken on the device (e.g. plamo2 on Metal), so the count check is the hard gate - baseline counts depend only on graph structure, not weights (verified stable across weight seeds) - the fusion stats API is resolved through the ad-hoc get_proc_address mechanism with generic names; a backend that does not export it makes the test fail with an error The committed MTL0.tsv baseline covers 110 dummy archs (298 rows). Assisted-by: pi:llama.cpp/DeepSeek-V4-Flash-0731 * tests : rename fusion api helpers to match stats_init signature Align the test with the ad-hoc fusion stats API: fusion_stats_init no longer takes an enable bool (stats are turned on by calling it), so the proc-address wrappers and typedefs are renamed to the api_* convention. Assisted-by: pi:llama.cpp/DeepSeek-V4-Flash-0731 * tests : rename backend to device in fusion test CLI The fusion test operates on a compute device (e.g. MTL0), not a backend, so rename the --backend argument to --device and the backend_name variable to device_name. Keep "backend" where it refers to the ggml backend interface (the ad-hoc proc-address mechanism). Assisted-by: pi:llama.cpp/DeepSeek-V4-Flash-0731 * tests : add --model and --help to fusion test --model FILE runs the fusion regression test over a single model file instead of enumerating a --models DIR. --models and --model are mutually exclusive. Also add a --help/-h option that prints the usage. Assisted-by: pi:llama.cpp/DeepSeek-V4-Flash-0731 * tests : use backend base name for fusion baseline output The fusion test is invoked with a specific device name (e.g. MTL0), but its output - the recorded baseline and the header it writes - should be named after the backend base name (e.g. MTL, via ggml_backend_reg_name), since the counters depend on the backend, not on the specific device index. Rename the committed baseline to MTL.tsv. Assisted-by: pi:llama.cpp/DeepSeek-V4-Flash-0731 * tests : run fusion test from ci instead of ctest The fusion test needs Metal and generates a lot of dummy models, so it does not belong in the generic ctest suite. Move it to ci/run.sh as gg_run_test_fusion, gated on GG_BUILD_METAL like gg_run_test_llama_archs_tensor_split: it generates the dummy models with test-llama-archs -o and then validates the fusion counts against the committed baseline. test-fusion.cpp is still built (llama_build) but no longer registered as a ctest. Assisted-by: pi:llama.cpp/DeepSeek-V4-Flash-0731 * tests : align fusion baseline TSV columns Pad the TSV fields to fixed widths so the columns line up regardless of the variable arch and fusion-label lengths, and trim each field on parse so the padded file is still accepted. Regenerate the committed MTL.tsv baseline in the padded format (data unchanged, verified identical modulo padding). Assisted-by: pi:llama.cpp/DeepSeek-V4-Flash-0731 * tests : widen label column and align fusion TSV header Give the label column more room (28 chars) and fix the column header widths so they match the data rows (moe/mode/label), keeping the header aligned with the values. Regenerate the MTL.tsv baseline in the new format (data unchanged). Assisted-by: pi:llama.cpp/DeepSeek-V4-Flash-0731 * tests : switch fusion baseline from TSV to CSV Use comma-separated values like the rest of the project, keeping the padded, aligned columns. Split on ',' and trim on parse. Rename the committed baseline to MTL.csv (data unchanged, verified identical modulo padding/separator). Update the ci/run.sh check path accordingly. Assisted-by: pi:llama.cpp/DeepSeek-V4-Flash-0731 * cont : rebase + update MTL stats * tests : avoid graph reallocations for some archs * metal : tidy fusion debugging context and op init - simplify the shared fusion debugging context comments - shorten the ggml_metal_fusion struct comment - align the ggml_metal_fuse struct fields and comments - move the fusion parameter of ggml_metal_op_init right after dev Assisted-by: pi:llama.cpp/DeepSeek-V4-Flash-0731 * tests : dedup fusion baseline into any mode prefill and decode always produce the same per-graph fusion count, so store a single row per label with mode = "any" and the per-graph count instead of two rows. this halves the baseline size and keeps the check stable. Assisted-by: pi:llama.cpp/DeepSeek-V4-Flash-0731 * ci : move fusion model generation to a separate step the dummy models generated by test-llama-archs are reused by other tests, so generate them once in their own step instead of inside test_fusion. Assisted-by: pi:llama.cpp/DeepSeek-V4-Flash-0731 * tests : bump nmse thold * models : fix plamo2 graph * tests : remove "skip" logic from test-fusion * tests : set qwen3tts dummy vocab to codec head size the dummy qwen3tts model used a vocab of 4096 while the codec head is 3072, so the graph padded the output with -inf which made the NMSE in test-fusion produce NaN. use the exact codec head size instead so the padding is not generated at all. Assisted-by: pi:llama.cpp/DeepSeek-V4-Flash-0731 * tests : regen fusion baseline reflect the plamo2 graph fix, which changed its fusion pattern split (RMS_NORM+MUL 11->10, RMS_NORM+MUL+ADD 3->4; same total). Assisted-by: pi:llama.cpp/DeepSeek-V4-Flash-0731 * ci : skip dummy model generation on OpenVINO test-llama-archs does not build on the OpenVINO platform, so do not try to generate the dummy models there. Assisted-by: pi:llama.cpp/DeepSeek-V4-Flash-0731 * cont : minor * tests : enable test-llama-archs on windows * cont : disable on windows + workaround * metal : naming nits * test-fusion : add instructions to update baseline * context : fix Kimi-K3 graph reserve * fusion : update MTL * cont : fix naming * metal : rework fusion info storage Assisted-by: pi:llama.cpp/DeepSeek-V4-Flash-Vision-Exp * metal : align fusion info API Assisted-by: pi:llama.cpp/DeepSeek-V4-Flash-Vision-Exp * metal : use opaque fusion handle in ad-hoc API Assisted-by: pi:llama.cpp/DeepSeek-V4-Flash-Vision-Exp * ci : move fusion test to dedicated workflow Assisted-by: pi:llama.cpp/DeepSeek-V4-Flash-Vision-Exp * cont : run only on ggml changes * cont : simplify * fusion : remove multi-output stuff for now * ci : fix typo
566 lines
23 KiB
C++
566 lines
23 KiB
C++
// test-fusion: verify the backend fusion logic against a per-device baseline.
|
|
//
|
|
// for every dummy model generated by test-llama-archs, the tool runs the model on a single
|
|
// device with fusion enabled and disabled, and reports:
|
|
// - the per-fusion-type counters for each mode (prefill / decode, merged into "any" when the
|
|
// per-graph counts match)
|
|
// - the NMSE between the fused and unfused logits
|
|
// - the NMSE between the device and a CPU reference
|
|
//
|
|
// the per-fusion-type counters are compared against a per-device baseline file (CSV) so a
|
|
// fusion pattern that silently stops matching (or fires when it should not) is caught as a
|
|
// regression.
|
|
//
|
|
// usage:
|
|
// test-fusion --models DIR --device MTL0 --record baseline.csv # generate a baseline
|
|
// test-fusion --models DIR --device MTL0 --check baseline.csv # validate against it
|
|
// test-fusion --model FILE --device MTL0 --check baseline.csv # validate a single model
|
|
|
|
#include "common.h"
|
|
#include "log.h"
|
|
#include "llama-cpp.h"
|
|
|
|
#include "ggml.h"
|
|
#include "gguf.h"
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <iomanip>
|
|
#include <iostream>
|
|
#include <map>
|
|
#include <random>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
// generic fusion debugging API, resolved through the ad-hoc get_proc_address mechanism
|
|
// (not part of the official ggml backend interface yet). a backend that adopts fusion debugging
|
|
// exports these exact names.
|
|
typedef void * ggml_backend_fusion_t;
|
|
|
|
typedef ggml_backend_fusion_t ( * fusion_get_t) (ggml_backend_dev_t);
|
|
typedef void ( * fusion_stats_init_t) (ggml_backend_fusion_t);
|
|
typedef void ( * fusion_stats_reset_t) (ggml_backend_fusion_t);
|
|
typedef int ( * fusion_stats_get_t) (ggml_backend_fusion_t, const char **, uint64_t *, int);
|
|
typedef void ( * fusion_set_enabled_t) (ggml_backend_fusion_t, bool);
|
|
|
|
static bool silent_model_load_progress(float, void *) {
|
|
return true;
|
|
}
|
|
|
|
struct gguf_context_ptr {
|
|
gguf_context * ctx;
|
|
gguf_context_ptr(gguf_context * c) : ctx(c) {}
|
|
~gguf_context_ptr() { if (ctx) { gguf_free(ctx); } }
|
|
gguf_context * get() const { return ctx; }
|
|
gguf_context_ptr(const gguf_context_ptr &) = delete;
|
|
gguf_context_ptr & operator=(const gguf_context_ptr &) = delete;
|
|
};
|
|
|
|
// NMSE between two vectors (same as tests/test-llama-archs.cpp)
|
|
static double nmse(const std::vector<float> & a, const std::vector<float> & b) {
|
|
GGML_ASSERT(a.size() == b.size());
|
|
double mse_a_b = 0.0;
|
|
double mse_a_0 = 0.0;
|
|
|
|
for (size_t i = 0; i < a.size(); i++) {
|
|
const float a_i = a[i];
|
|
const float b_i = b[i];
|
|
|
|
mse_a_b += (a_i - b_i) * (a_i - b_i);
|
|
mse_a_0 += a_i * a_i;
|
|
}
|
|
|
|
return mse_a_b / mse_a_0;
|
|
}
|
|
|
|
// deterministic token sequence
|
|
static std::vector<llama_token> get_tokens(const uint32_t n_tokens, const uint32_t n_vocab, const size_t seed) {
|
|
std::mt19937 gen(seed);
|
|
std::uniform_int_distribution<> dis(0, n_vocab - 1);
|
|
std::vector<llama_token> ret;
|
|
ret.reserve(n_tokens);
|
|
for (uint32_t i = 0; i < n_tokens; i++) {
|
|
ret.push_back(dis(gen));
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
// trim leading/trailing whitespace (used when parsing padded CSV columns)
|
|
static std::string trim(const std::string & s) {
|
|
const size_t b = s.find_first_not_of(" \t\r\n");
|
|
if (b == std::string::npos) {
|
|
return "";
|
|
}
|
|
const size_t e = s.find_last_not_of(" \t\r\n");
|
|
return s.substr(b, e - b + 1);
|
|
}
|
|
|
|
static std::string get_arch(const std::string & path) {
|
|
gguf_init_params params = { /*no_alloc=*/true, /*ctx=*/nullptr };
|
|
gguf_context_ptr ctx(gguf_init_from_file(path.c_str(), params));
|
|
if (!ctx.get()) {
|
|
throw std::runtime_error("failed to read gguf: " + path);
|
|
}
|
|
const int idx = gguf_find_key(ctx.get(), "general.architecture");
|
|
if (idx < 0) {
|
|
return "unknown";
|
|
}
|
|
const char * val = gguf_get_val_str(ctx.get(), idx);
|
|
return val ? val : "unknown";
|
|
}
|
|
|
|
static llama_model_ptr load_model(const std::string & path, ggml_backend_dev_t dev) {
|
|
llama_model_params model_params = llama_model_default_params();
|
|
model_params.progress_callback = silent_model_load_progress;
|
|
std::vector<ggml_backend_dev_t> devs = { dev, nullptr };
|
|
model_params.devices = devs.data();
|
|
model_params.split_mode = LLAMA_SPLIT_MODE_LAYER;
|
|
|
|
llama_model_ptr model(llama_model_load_from_file(path.c_str(), model_params));
|
|
if (!model) {
|
|
throw std::runtime_error("failed to load model: " + path);
|
|
}
|
|
return model;
|
|
}
|
|
|
|
// a fresh context (fresh state) from an already-loaded model
|
|
static llama_context_ptr create_ctx(llama_model * model, int n_ubatch) {
|
|
llama_context_params ctx_params = llama_context_default_params();
|
|
ctx_params.n_ctx = 0;
|
|
ctx_params.n_threads = 4;
|
|
ctx_params.n_threads_batch = 4;
|
|
ctx_params.n_ubatch = n_ubatch;
|
|
ctx_params.n_batch = n_ubatch;
|
|
|
|
llama_context_ptr lctx(llama_init_from_model(model, ctx_params));
|
|
if (!lctx) {
|
|
throw std::runtime_error("failed to init context");
|
|
}
|
|
return lctx;
|
|
}
|
|
|
|
// decode all tokens in one batch; returns the logits of every token
|
|
static std::vector<float> decode_prefill(llama_model * model, llama_context * lctx, const std::vector<llama_token> & tokens) {
|
|
const uint32_t n_vocab = llama_vocab_n_tokens(llama_model_get_vocab(model));
|
|
llama_batch batch = llama_batch_init(tokens.size(), 0, 1);
|
|
for (size_t i = 0; i < tokens.size(); i++) {
|
|
common_batch_add(batch, tokens[i], i, { 0 }, true);
|
|
}
|
|
batch.n_tokens = tokens.size();
|
|
if (llama_decode(lctx, batch)) {
|
|
llama_batch_free(batch);
|
|
throw std::runtime_error("prefill decode failed");
|
|
}
|
|
|
|
std::vector<float> ret;
|
|
ret.reserve(tokens.size() * n_vocab);
|
|
for (size_t i = 0; i < tokens.size(); i++) {
|
|
const float * logits_ith = llama_get_logits_ith(lctx, i);
|
|
for (uint32_t j = 0; j < n_vocab; j++) {
|
|
ret.push_back(logits_ith[j]);
|
|
}
|
|
}
|
|
llama_batch_free(batch);
|
|
return ret;
|
|
}
|
|
|
|
// decode one token at a time; returns the logits of the last token of each step
|
|
static std::vector<float> decode_gen(llama_model * model, llama_context * lctx, const std::vector<llama_token> & tokens) {
|
|
const uint32_t n_vocab = llama_vocab_n_tokens(llama_model_get_vocab(model));
|
|
llama_batch batch = llama_batch_init(1, 0, 1);
|
|
std::vector<float> ret;
|
|
for (size_t i = 0; i < tokens.size(); i++) {
|
|
common_batch_clear(batch);
|
|
common_batch_add(batch, tokens[i], i, { 0 }, true);
|
|
if (llama_decode(lctx, batch)) {
|
|
llama_batch_free(batch);
|
|
throw std::runtime_error("decode failed");
|
|
}
|
|
const float * logits = llama_get_logits_ith(lctx, 0);
|
|
for (uint32_t j = 0; j < n_vocab; j++) {
|
|
ret.push_back(logits[j]);
|
|
}
|
|
}
|
|
llama_batch_free(batch);
|
|
return ret;
|
|
}
|
|
|
|
static void read_counts(fusion_stats_get_t api_stats_get, ggml_backend_fusion_t finfo,
|
|
std::vector<const char *> & labels, std::vector<uint64_t> & counts) {
|
|
const int n = api_stats_get(finfo, nullptr, nullptr, 0);
|
|
labels.assign(n, nullptr);
|
|
counts.assign(n, 0);
|
|
api_stats_get(finfo, labels.data(), counts.data(), n);
|
|
}
|
|
|
|
// one row of the per-label report
|
|
struct fusion_row {
|
|
std::string arch;
|
|
bool moe;
|
|
std::string mode;
|
|
std::string label;
|
|
uint64_t count_fused;
|
|
uint64_t count_unfused;
|
|
uint64_t expected;
|
|
double nmse_fus;
|
|
double nmse_dev;
|
|
bool ok_count; // counts match the baseline
|
|
bool ok_nmse; // nmse within epsilon
|
|
};
|
|
|
|
static void usage(const char * argv0) {
|
|
printf("%s: verify fusion counts on a device against a per-device baseline\n\n", argv0);
|
|
printf("usage: %s [options]\n\n", argv0);
|
|
printf("options:\n");
|
|
printf(" --models DIR run over all .gguf models in a directory\n");
|
|
printf(" --model FILE run over a single model file (mutually exclusive with --models)\n");
|
|
printf(" --device NAME device to run on (e.g. MTL0, CPU)\n");
|
|
printf(" --record CSV write the golden baseline\n");
|
|
printf(" --check CSV validate the counters against a baseline (default)\n");
|
|
printf(" -h, --help show this message and exit\n");
|
|
}
|
|
|
|
int main(int argc, char ** argv) {
|
|
std::string models_dir;
|
|
std::string model_file;
|
|
std::string device_name;
|
|
std::string record_path;
|
|
std::string check_path;
|
|
|
|
for (int i = 1; i < argc; i++) {
|
|
const std::string arg = argv[i];
|
|
const auto next = [&](const char * name) -> std::string {
|
|
if (i + 1 >= argc) {
|
|
LOG_ERR("%s: %s requires an argument\n", __func__, name);
|
|
exit(1);
|
|
}
|
|
return argv[++i];
|
|
};
|
|
if (arg == "-h" || arg == "--help") {
|
|
usage(argv[0]);
|
|
exit(0);
|
|
}
|
|
if (arg == "--models") { models_dir = next("--models"); }
|
|
else if (arg == "--model") { model_file = next("--model"); }
|
|
else if (arg == "--device"){ device_name = next("--device"); }
|
|
else if (arg == "--record"){ record_path = next("--record"); }
|
|
else if (arg == "--check") { check_path = next("--check"); }
|
|
else {
|
|
LOG_ERR("%s: unknown argument: %s\n", __func__, arg.c_str());
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
if (device_name.empty()) {
|
|
LOG_ERR("%s: --device NAME is required\n", __func__);
|
|
return 1;
|
|
}
|
|
if (models_dir.empty() && model_file.empty()) {
|
|
LOG_ERR("%s: --models DIR or --model FILE is required\n", __func__);
|
|
return 1;
|
|
}
|
|
if (!models_dir.empty() && !model_file.empty()) {
|
|
LOG_ERR("%s: --models DIR and --model FILE are mutually exclusive\n", __func__);
|
|
return 1;
|
|
}
|
|
if (!record_path.empty() && !check_path.empty()) {
|
|
LOG_ERR("%s: --record and --check are mutually exclusive\n", __func__);
|
|
return 1;
|
|
}
|
|
|
|
std::vector<std::string> models;
|
|
if (!model_file.empty()) {
|
|
if (!std::filesystem::is_regular_file(model_file)) {
|
|
LOG_ERR("%s: model file '%s' does not exist\n", __func__, model_file.c_str());
|
|
return 1;
|
|
}
|
|
models.push_back(model_file);
|
|
} else {
|
|
if (!std::filesystem::exists(models_dir) || !std::filesystem::is_directory(models_dir)) {
|
|
LOG_ERR("%s: models directory '%s' does not exist\n", __func__, models_dir.c_str());
|
|
return 1;
|
|
}
|
|
for (const auto & entry : std::filesystem::directory_iterator(models_dir)) {
|
|
if (entry.is_regular_file() && entry.path().extension() == ".gguf") {
|
|
models.push_back(entry.path().string());
|
|
}
|
|
}
|
|
std::sort(models.begin(), models.end());
|
|
|
|
if (models.empty()) {
|
|
LOG_ERR("%s: no .gguf models found in '%s'\n", __func__, models_dir.c_str());
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
common_init();
|
|
ggml_backend_load_all();
|
|
|
|
ggml_backend_dev_t dev = ggml_backend_dev_by_name(device_name.c_str());
|
|
if (!dev) {
|
|
LOG_WRN("%s: device '%s' not found - skipping (baseline is device-specific)\n",
|
|
__func__, device_name.c_str());
|
|
return 0;
|
|
}
|
|
|
|
// resolve the generic fusion debugging functions through the ad-hoc get_proc_address
|
|
// mechanism; a backend that does not adopt fusion debugging exports none of them
|
|
auto * reg = ggml_backend_dev_backend_reg(dev);
|
|
|
|
// output naming uses the backend base name (e.g. "MTL") rather than the specific device
|
|
// name (e.g. "MTL0") the test was invoked with
|
|
const std::string base_name = ggml_backend_reg_name(reg);
|
|
|
|
auto api_get = (fusion_get_t) ggml_backend_reg_get_proc_address(reg, "ggml_backend_fusion_get");
|
|
auto api_stats_init = (fusion_stats_init_t) ggml_backend_reg_get_proc_address(reg, "ggml_backend_fusion_stats_init");
|
|
auto api_stats_reset = (fusion_stats_reset_t) ggml_backend_reg_get_proc_address(reg, "ggml_backend_fusion_stats_reset");
|
|
auto api_stats_get = (fusion_stats_get_t) ggml_backend_reg_get_proc_address(reg, "ggml_backend_fusion_stats_get");
|
|
auto api_set_enabled = (fusion_set_enabled_t) ggml_backend_reg_get_proc_address(reg, "ggml_backend_fusion_set_enabled");
|
|
|
|
if (!api_get || !api_stats_init || !api_set_enabled || !api_stats_reset || !api_stats_get) {
|
|
LOG_ERR("%s: device '%s' does not export the generic fusion debugging API "
|
|
"(ggml_backend_fusion_*) - cannot run the fusion regression test\n",
|
|
__func__, device_name.c_str());
|
|
return 1;
|
|
}
|
|
|
|
ggml_backend_fusion_t finfo = api_get(dev);
|
|
|
|
// enable fusions stats
|
|
api_stats_init(finfo);
|
|
|
|
const bool has_counts = true;
|
|
|
|
// load the baseline (if any): key arch|moe|mode|label -> expected count
|
|
std::map<std::string, uint64_t> baseline;
|
|
if (!check_path.empty()) {
|
|
std::ifstream in(check_path);
|
|
if (!in) {
|
|
LOG_ERR("%s: cannot open baseline '%s'\n", __func__, check_path.c_str());
|
|
return 1;
|
|
}
|
|
std::string line;
|
|
while (std::getline(in, line)) {
|
|
if (line.empty() || line[0] == '#') {
|
|
continue;
|
|
}
|
|
std::vector<std::string> cols;
|
|
size_t pos = 0;
|
|
while ((pos = line.find(',')) != std::string::npos) {
|
|
cols.push_back(trim(line.substr(0, pos)));
|
|
line.erase(0, pos + 1);
|
|
}
|
|
cols.push_back(trim(line));
|
|
if (cols.size() != 5) {
|
|
continue;
|
|
}
|
|
baseline[cols[0] + "|" + cols[1] + "|" + cols[2] + "|" + cols[3]] = std::stoull(cols[4]);
|
|
}
|
|
}
|
|
|
|
std::vector<fusion_row> rows;
|
|
|
|
LOG_INF("%s: running fusion test over %zu models on '%s'\n", __func__, models.size(), base_name.c_str());
|
|
|
|
const size_t seed = 1;
|
|
|
|
for (const auto & model_path : models) {
|
|
const std::string arch = get_arch(model_path);
|
|
const bool moe = arch.find("moe") != std::string::npos;
|
|
|
|
llama_model_ptr model;
|
|
llama_model_ptr model_cpu;
|
|
uint32_t n_vocab = 0;
|
|
try {
|
|
model = load_model(model_path, dev);
|
|
model_cpu = load_model(model_path, ggml_backend_dev_by_name("CPU"));
|
|
n_vocab = llama_vocab_n_tokens(llama_model_get_vocab(model.get()));
|
|
} catch (const std::exception & e) {
|
|
LOG_ERR("%s: %s: %s\n", __func__, model_path.c_str(), e.what());
|
|
continue;
|
|
}
|
|
|
|
struct mode_cfg {
|
|
std::string name;
|
|
std::vector<float> (*decode)(llama_model *, llama_context *, const std::vector<llama_token> &);
|
|
int n_tokens;
|
|
int n_graphs; // graph runs per mode (prefill=1, decode=16)
|
|
};
|
|
const mode_cfg modes[] = {
|
|
{ "prefill", decode_prefill, 32, 1 },
|
|
{ "decode", decode_gen, 16, 16 },
|
|
};
|
|
|
|
// per-label, per-mode data for this model; prefill and decode are merged into a single
|
|
// "any" row when their per-graph counts match
|
|
struct mode_data {
|
|
bool present;
|
|
uint64_t count_fused; // per graph
|
|
uint64_t count_unfused; // per graph
|
|
double nmse_fus;
|
|
double nmse_dev;
|
|
bool ok_nmse;
|
|
};
|
|
std::map<std::string, std::array<mode_data, 2>> mdata;
|
|
|
|
for (int mi = 0; mi < 2; mi++) {
|
|
const mode_cfg & mode = modes[mi];
|
|
const auto tokens = get_tokens(mode.n_tokens, n_vocab, seed);
|
|
|
|
// CPU reference for this mode (fresh context, fresh state)
|
|
std::vector<float> logits_cpu;
|
|
try {
|
|
llama_context_ptr ctx = create_ctx(model_cpu.get(), 32);
|
|
logits_cpu = mode.decode(model_cpu.get(), ctx.get(), tokens);
|
|
} catch (const std::exception & e) {
|
|
LOG_WRN("%s: %s: cpu reference: %s\n", __func__, model_path.c_str(), e.what());
|
|
}
|
|
|
|
// fused run on a fresh context (fresh state)
|
|
std::vector<float> logits_fused;
|
|
std::vector<const char *> labels;
|
|
std::vector<uint64_t> counts_fused;
|
|
{
|
|
llama_context_ptr ctx = create_ctx(model.get(), 32);
|
|
if (has_counts) {
|
|
api_set_enabled(finfo, true);
|
|
api_stats_reset(finfo);
|
|
}
|
|
logits_fused = mode.decode(model.get(), ctx.get(), tokens);
|
|
if (has_counts) {
|
|
read_counts(api_stats_get, finfo, labels, counts_fused);
|
|
}
|
|
}
|
|
|
|
// unfused run on another fresh context (fresh state)
|
|
std::vector<float> logits_unfused;
|
|
std::vector<uint64_t> counts_unfused;
|
|
{
|
|
llama_context_ptr ctx = create_ctx(model.get(), 32);
|
|
if (has_counts) {
|
|
api_set_enabled(finfo, false);
|
|
api_stats_reset(finfo);
|
|
}
|
|
logits_unfused = mode.decode(model.get(), ctx.get(), tokens);
|
|
if (has_counts) {
|
|
read_counts(api_stats_get, finfo, labels, counts_unfused);
|
|
}
|
|
}
|
|
|
|
const double nmse_fus = nmse(logits_fused, logits_unfused);
|
|
const double nmse_dev = logits_cpu.empty() ? 0.0 : nmse(logits_fused, logits_cpu);
|
|
|
|
if (has_counts) {
|
|
for (int i = 0; i < (int) labels.size(); i++) {
|
|
const uint64_t fused = counts_fused[i] / mode.n_graphs;
|
|
const uint64_t unfused = counts_unfused[i] / mode.n_graphs;
|
|
if (fused == 0 && unfused == 0) {
|
|
continue;
|
|
}
|
|
auto & d = mdata[labels[i]][mi];
|
|
d.present = true;
|
|
d.count_fused = fused;
|
|
d.count_unfused = unfused;
|
|
d.nmse_fus = nmse_fus;
|
|
d.nmse_dev = nmse_dev;
|
|
d.ok_nmse = nmse_fus <= 1e-4;
|
|
}
|
|
} else {
|
|
rows.push_back({ arch, moe, mode.name, "?", 0, 0, 0, nmse_fus, nmse_dev, true, nmse_fus <= 1e-4 });
|
|
}
|
|
}
|
|
|
|
// build the per-label rows, merging prefill and decode into "any" when the per-graph
|
|
// counts match (they always do for the deterministic fusion table)
|
|
if (has_counts) {
|
|
for (auto & kv : mdata) {
|
|
const std::string & label = kv.first;
|
|
const auto & d = kv.second;
|
|
const bool both = d[0].present && d[1].present;
|
|
const bool match = both && d[0].count_fused == d[1].count_fused;
|
|
|
|
if (match) {
|
|
// one "any" row; use the worst NMSE across the two modes
|
|
const std::string any_key = arch + "|" + (moe ? "1" : "0") + "|any|" + label;
|
|
const uint64_t expected = baseline.count(any_key) ? baseline.at(any_key) : 0;
|
|
const bool ok_count = check_path.empty() || d[0].count_fused == expected;
|
|
const bool ok_nmse = d[0].ok_nmse && d[1].ok_nmse;
|
|
const double nmse_fus = std::max(d[0].nmse_fus, d[1].nmse_fus);
|
|
const double nmse_dev = std::max(d[0].nmse_dev, d[1].nmse_dev);
|
|
rows.push_back({ arch, moe, "any", label, d[0].count_fused, d[0].count_unfused,
|
|
expected, nmse_fus, nmse_dev, ok_count, ok_nmse });
|
|
} else {
|
|
// counts differ - keep a separate row per mode
|
|
for (int mi = 0; mi < 2; mi++) {
|
|
if (!d[mi].present) {
|
|
continue;
|
|
}
|
|
const mode_data & a = d[mi];
|
|
const std::string mode_key = arch + "|" + (moe ? "1" : "0") + "|" + modes[mi].name + "|" + label;
|
|
const uint64_t expected = baseline.count(mode_key) ? baseline.at(mode_key) : 0;
|
|
const bool ok_count = check_path.empty() || a.count_fused == expected;
|
|
rows.push_back({ arch, moe, modes[mi].name, label, a.count_fused, a.count_unfused,
|
|
expected, a.nmse_fus, a.nmse_dev, ok_count, a.ok_nmse });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
LOG_INF("%s: %-20s (%s) done\n", __func__, arch.c_str(), model_path.c_str());
|
|
}
|
|
|
|
// print the report
|
|
{
|
|
std::ofstream out(record_path);
|
|
std::ostream & os = record_path.empty() ? std::cout : out;
|
|
if (!record_path.empty()) {
|
|
os << "# test-fusion baseline for device " << base_name << "\n";
|
|
os << "# " << std::left
|
|
<< std::setw(18) << "arch" << ','
|
|
<< std::setw(4) << "moe" << ','
|
|
<< std::setw(8) << "mode" << ','
|
|
<< std::setw(28) << "label" << ','
|
|
<< std::right << std::setw(7) << "count" << '\n';
|
|
}
|
|
|
|
LOG_INF("%-20s %-4s %-8s %-22s %7s %7s %7s %10s %10s %s\n",
|
|
"arch", "moe", "mode", "label", "fused", "unfused", "expected", "nmse_fus", "nmse_dev", "status");
|
|
int n_ok = 0;
|
|
int n_bad = 0;
|
|
for (const auto & r : rows) {
|
|
const bool ok = r.ok_count && r.ok_nmse;
|
|
const char * status = ok ? "ok" : "FAIL";
|
|
if (ok) { n_ok++; } else { n_bad++; }
|
|
LOG_INF("%-20s %-4s %-8s %-22s %7llu %7llu %7llu %10.2e %10.2e %s\n",
|
|
r.arch.c_str(), r.moe ? "moe" : "dense", r.mode.c_str(), r.label.c_str(),
|
|
(unsigned long long) r.count_fused, (unsigned long long) r.count_unfused,
|
|
(unsigned long long) r.expected, r.nmse_fus, r.nmse_dev, status);
|
|
if (!record_path.empty()) {
|
|
os << std::left
|
|
<< std::setw(20) << r.arch << ','
|
|
<< std::setw(4) << (r.moe ? "1" : "0") << ','
|
|
<< std::setw(8) << r.mode << ','
|
|
<< std::setw(28) << r.label << ','
|
|
<< std::right << std::setw(7) << r.count_fused << '\n';
|
|
}
|
|
}
|
|
LOG_INF("summary: %d ok, %d failed\n", n_ok, n_bad);
|
|
if (!record_path.empty()) {
|
|
LOG_INF("%s: baseline written to '%s'\n", __func__, record_path.c_str());
|
|
}
|
|
|
|
if (n_bad && !models_dir.empty() && !check_path.empty()) {
|
|
LOG_WRN("%s: if the fusion counts are expected to change, run with --record to update the baseline:\n"
|
|
"\n"
|
|
"./bin/test-llama-archs -o %s\n"
|
|
"%s --device %s --models %s --record %s\n",
|
|
__func__, models_dir.c_str(), argv[0], device_name.c_str(), models_dir.c_str(), check_path.c_str());
|
|
}
|
|
|
|
return n_bad;
|
|
}
|
|
}
|