mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-08-30 00:51:19 +02:00
4e97ac86eb
* tests : run test-save-load-state across all architectures test-save-load-state previously only ran in ctest against a single downloaded model (tinyllamas/stories15M), i.e. only the llama arch. Add a --models DIR mode to test-save-load-state that runs the full save/load suite over every *.gguf in a directory, reporting a per-model PASS/FAIL and exiting non-zero if any model fails, and wire a ctest to run it over all architectures using the existing generate-models fixture (test-llama-archs). The single-model -m mode is preserved (still used by ci/run.sh). Also bump the dummy-model training context in test-llama-archs from 128 to 256 so that the per-sequence context (which is padded up to a multiple of 256) no longer exceeds n_ctx_train and emits the "possible training context overflow" warning. The test is expected to fail until the affected arches are fixed: deepseek4 (host seq-copy), gemma2/gpt-oss/lfm2 (device seq-copy), minimax-01 (state load). It aborts at the first arch that crashes. Assisted-by: pi:llama.cpp/Qwen3.8-27B * tests : match dummy DSA indexer to fused Lightning Indexer kernel The dummy DSA indexer (deepseek32, glm-dsa, ...) used key_length=64 and head_count=1, so the fused Lightning Indexer op's q tensor was shaped [64, 1, ...]. The Metal fused kernel is fixed to DK=128, NH=64, so it rejected the op and the scheduler fell back to CPU, emitting a 'layer assigned to MTL but Lightning Indexer on CPU' warning. Bump key_length to 128 and the DSA head_count to 64 so the fused op runs on the GPU. Assisted-by: pi:llama.cpp/Qwen3.8-27B * tests : add --help and document -o in test-llama-archs Add a --help/-h flag to test-llama-archs and list the existing -o/--out option in the usage text, which was previously missing. Assisted-by: pi:llama.cpp/Qwen3.8-27B * tests : use 64 indexer heads for deepseek4 deepseek4's indexer head count was set to n_head (8), which does not match the fused Lightning Indexer kernel's fixed NH=64, so the fused op fell back to the CPU backend and emitted a device-mismatch warning. Give it the same fixed 64 as the other indexer archs by dropping it from the n_head ternary (only minimax-m3 keeps n_head, since it does not use the fused Lightning Indexer op). Assisted-by: pi:llama.cpp/Qwen3.8-27B * tests : fix dsv4 save-load n_stream mismatch The dsv4 KV cache keeps per-sequence KV/state streams even in unified mode, so its n_stream equals n_seq_max. The test saved the state in the baseline with n_seq_max=1 but loaded it in the seq-copy tests with n_seq_max=2, so state_read threw an n_stream mismatch. Use n_seq_max=2 in the baseline and state-load tests so the save and load agree. Assisted-by: pi:llama.cpp/Qwen3.8-27B * context : relax on-device seq-copy chunk alignment The on-device state seq copy (llama_state_seq_set_data with LLAMA_STATE_SEQ_FLAGS_ON_DEVICE) copied the write-side cpy tensors to the read-side targets 1:1 by index, requiring the writer and reader to emit the same number of chunks in the same order with the same per-chunk sizes. state_write_data chunks per cell-range while state_read_data chunks contiguous-or-per-cell, so the counts diverged for non-contiguous sources (dsv4, SWA) and the copy aborted with "memory buffer mismatch". All state writers and readers enumerate the same logical data in the same order, differing only in chunking. Copy the flat write-side data into the read-side targets with a byte cursor that walks both tensor lists across their boundaries, so the chunking no longer needs to match. Keep the total-size guard; drop the n_tensors equality check. Assisted-by: pi:llama.cpp/Qwen3.8-27B * model : fix dangling hparams ref in minimax-01 LA graph input llm_graph_input_la stored const llama_hparams & hparams, bound to the llm_graph_params temporary in llama_context::process_ubatch. The input object outlives that temporary (it is kept in llm_graph_result::inputs for graph reuse), so set_input() read destroyed stack memory on every graph reuse - test-save-load-state crashed for minimax-01 when the stack region was overwritten (n_layer_all read as 0, abort in llama_hparams::n_head). Store a copy like every other graph input class. Assisted-by: pi:llama.cpp/Qwen3.8-27B * context : handle "worst case" graph and add TODO
526 lines
18 KiB
C++
526 lines
18 KiB
C++
#include "arg.h"
|
|
#include "common.h"
|
|
#include "log.h"
|
|
#include "llama-cpp.h"
|
|
|
|
#include <algorithm>
|
|
#include <clocale>
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
#include <random>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
struct llama_batch_ptr {
|
|
llama_batch batch;
|
|
|
|
llama_batch_ptr(int32_t n_tokens, int32_t embd, int32_t n_seq_max)
|
|
: batch{llama_batch_init(n_tokens, embd, n_seq_max)} {}
|
|
|
|
~llama_batch_ptr() { llama_batch_free(batch); }
|
|
|
|
llama_batch_ptr(const llama_batch_ptr &) = delete;
|
|
llama_batch_ptr & operator=(const llama_batch_ptr &) = delete;
|
|
llama_batch_ptr(llama_batch_ptr &&) = default;
|
|
llama_batch_ptr & operator=(llama_batch_ptr &&) = default;
|
|
|
|
llama_batch & get() { return batch; }
|
|
const llama_batch & get() const { return batch; }
|
|
};
|
|
|
|
static llama_tokens generate_tokens(llama_context * ctx, llama_sampler * smpl, int & n_past, int32_t n_predict, llama_seq_id seq_id) {
|
|
llama_tokens result;
|
|
llama_batch_ptr batch(1, 0, 1);
|
|
|
|
for (int i = 0; i < n_predict; i++) {
|
|
auto next_token = llama_sampler_sample(smpl, ctx, -1);
|
|
|
|
LOG("%d ", next_token);
|
|
result.push_back(next_token);
|
|
|
|
common_batch_clear(batch.get());
|
|
common_batch_add(batch.get(), next_token, n_past, {seq_id}, true);
|
|
|
|
if (llama_decode(ctx, batch.get())) {
|
|
LOG_ERR("\n%s: failed to evaluate\n", __func__);
|
|
return {};
|
|
}
|
|
n_past++;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// Test 1: baseline
|
|
// - decode all but the last token
|
|
// - save state to disk
|
|
// - decode the last token
|
|
// - generate n_predict tokens
|
|
static llama_tokens test_baseline(struct llama_model * model, const struct common_params & params, const llama_tokens & tokens) {
|
|
auto params_ctx = common_context_params_to_llama(params);
|
|
params_ctx.n_seq_max = 2;
|
|
auto ctx = llama_context_ptr{llama_init_from_model(model, params_ctx)};
|
|
|
|
auto sparams = llama_sampler_chain_default_params();
|
|
auto smpl = llama_sampler_ptr{llama_sampler_chain_init(sparams)};
|
|
llama_sampler_chain_add(smpl.get(), llama_sampler_init_dist(params.sampling.seed));
|
|
|
|
auto n_past = 0;
|
|
if (!common_prompt_batch_decode(ctx.get(), tokens, (int)tokens.size(), n_past, params.n_batch, params.out_file, true)) {
|
|
LOG_ERR("%s: failed to decode prompt\n", __func__);
|
|
return {};
|
|
}
|
|
|
|
LOG("\n=== Test 1: baseline ===\n");
|
|
|
|
auto result = generate_tokens(ctx.get(), smpl.get(), n_past, params.n_predict, 0);
|
|
if (result.empty()) {
|
|
return {};
|
|
}
|
|
|
|
LOG("\n");
|
|
|
|
return result;
|
|
}
|
|
|
|
|
|
// Test 2: sequence removal isolation
|
|
// - decode the same prefix into two sequences
|
|
// - remove sequence 0
|
|
// - verify that sequence 1 remains unchanged
|
|
static bool test_seq_rm_isolated(
|
|
struct llama_model * model,
|
|
const struct common_params & params,
|
|
const llama_tokens & tokens) {
|
|
auto params_ctx = common_context_params_to_llama(params);
|
|
params_ctx.n_ctx = 256;
|
|
params_ctx.n_seq_max = 2;
|
|
params_ctx.kv_unified = true;
|
|
|
|
auto ctx = llama_context_ptr{llama_init_from_model(model, params_ctx)};
|
|
if (!ctx) {
|
|
LOG_ERR("%s: failed to create context\n", __func__);
|
|
return false;
|
|
}
|
|
|
|
LOG("\n=== Test 2: sequence removal isolation ===\n");
|
|
|
|
const size_t n_tokens = tokens.size() < 128 ? tokens.size() : 128;
|
|
for (llama_seq_id seq_id = 0; seq_id < 2; ++seq_id) {
|
|
llama_batch_ptr batch(n_tokens, 0, 1);
|
|
for (size_t i = 0; i < n_tokens; ++i) {
|
|
common_batch_add(batch.get(), tokens[i], i, { seq_id }, false);
|
|
}
|
|
|
|
if (llama_decode(ctx.get(), batch.get())) {
|
|
LOG_ERR("%s: failed to decode prompt for sequence %d\n", __func__, seq_id);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
const auto get_seq_state = [&](llama_seq_id seq_id, std::vector<uint8_t> & state) {
|
|
const size_t state_size = llama_state_seq_get_size(ctx.get(), seq_id);
|
|
if (state_size == 0) {
|
|
LOG_ERR("%s: sequence state is empty\n", __func__);
|
|
return false;
|
|
}
|
|
|
|
state.resize(state_size);
|
|
const size_t ncopy = llama_state_seq_get_data(ctx.get(), state.data(), state.size(), seq_id);
|
|
if (ncopy != state.size()) {
|
|
LOG_ERR("%s: sequence state length %zu does not match expected length %zu\n",
|
|
__func__, ncopy, state.size());
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
std::vector<uint8_t> state_before;
|
|
if (!get_seq_state(1, state_before)) {
|
|
return false;
|
|
}
|
|
|
|
if (!llama_memory_seq_rm(llama_get_memory(ctx.get()), 0, -1, -1)) {
|
|
LOG_ERR("%s: failed to remove sequence 0\n", __func__);
|
|
return false;
|
|
}
|
|
|
|
std::vector<uint8_t> state_after;
|
|
if (!get_seq_state(1, state_after)) {
|
|
return false;
|
|
}
|
|
|
|
if (state_before != state_after) {
|
|
LOG_ERR("%s: removing sequence 0 changed sequence 1\n", __func__);
|
|
return false;
|
|
}
|
|
|
|
LOG("PASS\n");
|
|
return true;
|
|
}
|
|
|
|
|
|
// Test 3: state load
|
|
// - create a new context
|
|
// - load state from file
|
|
// - replay the last prompt token
|
|
// - generate n_predict tokens and compare against expected result
|
|
static bool test_state_load(struct llama_model * model, const struct common_params & params, const llama_tokens & tokens, const llama_tokens & expected_result) {
|
|
auto params_ctx = common_context_params_to_llama(params);
|
|
params_ctx.n_seq_max = 2;
|
|
auto ctx = llama_context_ptr{llama_init_from_model(model, params_ctx)};
|
|
|
|
auto sparams = llama_sampler_chain_default_params();
|
|
auto smpl = llama_sampler_ptr{llama_sampler_chain_init(sparams)};
|
|
llama_sampler_chain_add(smpl.get(), llama_sampler_init_dist(params.sampling.seed));
|
|
|
|
LOG("\n=== Test 3: state load ===\n");
|
|
|
|
// Load state from file
|
|
llama_tokens unused_sts(tokens.size());
|
|
size_t n_token_count_out = 0;
|
|
|
|
if (!llama_state_load_file(ctx.get(), params.out_file.data(), unused_sts.data(), unused_sts.size(), &n_token_count_out)) {
|
|
LOG_ERR("\n%s: failed to load state\n", __func__);
|
|
return false;
|
|
}
|
|
|
|
LOG_TRC("%s: loaded state with %zu tokens\n", __func__, n_token_count_out);
|
|
|
|
// Replay last token
|
|
int n_past = (int) n_token_count_out - 1;
|
|
if (!common_replay_last_token(ctx.get(), tokens.back(), n_past)) {
|
|
return false;
|
|
}
|
|
n_past++;
|
|
|
|
// Generate tokens
|
|
auto result = generate_tokens(ctx.get(), smpl.get(), n_past, params.n_predict, 0);
|
|
if (result.empty()) {
|
|
return false;
|
|
}
|
|
|
|
if (result != expected_result) {
|
|
LOG_ERR("\n%s: error: generation differs from expected\n", __func__);
|
|
return false;
|
|
}
|
|
|
|
LOG("\nPASS\n");
|
|
return true;
|
|
}
|
|
|
|
|
|
// Test 4: seq copy (host)
|
|
// - create a multi-seq context
|
|
// - load state from file
|
|
// - replay the last prompt token
|
|
// - migrate KV cache from seq 0 to seq 1 via the CPU path
|
|
// - generate n_predict tokens on seq 1 and compare against expected result
|
|
static bool test_seq_cp_host(struct llama_model * model, const struct common_params & params, const llama_tokens & tokens, const llama_tokens & expected_result) {
|
|
auto params_ctx = common_context_params_to_llama(params);
|
|
params_ctx.n_seq_max = 2;
|
|
auto ctx = llama_context_ptr{llama_init_from_model(model, params_ctx)};
|
|
|
|
auto sparams = llama_sampler_chain_default_params();
|
|
auto smpl = llama_sampler_ptr{llama_sampler_chain_init(sparams)};
|
|
llama_sampler_chain_add(smpl.get(), llama_sampler_init_dist(params.sampling.seed));
|
|
|
|
LOG("\n=== Test 4: seq copy (host) ===\n");
|
|
|
|
// Load state from file
|
|
llama_tokens unused_sts(tokens.size());
|
|
size_t n_token_count_out = 0;
|
|
|
|
if (!llama_state_load_file(ctx.get(), params.out_file.data(), unused_sts.data(), unused_sts.size(), &n_token_count_out)) {
|
|
LOG_ERR("\n%s: failed to load state\n", __func__);
|
|
return false;
|
|
}
|
|
|
|
LOG_TRC("%s: loaded state with %zu tokens\n", __func__, n_token_count_out);
|
|
|
|
// Replay last token
|
|
int n_past = (int) n_token_count_out - 1;
|
|
if (!common_replay_last_token(ctx.get(), tokens.back(), n_past)) {
|
|
return false;
|
|
}
|
|
n_past++;
|
|
|
|
// Migrate KV cache from seq 0 to seq 1 (CPU path)
|
|
{
|
|
std::vector<uint8_t> seq_store(llama_state_seq_get_size(ctx.get(), 0));
|
|
const size_t ncopy = llama_state_seq_get_data(ctx.get(), seq_store.data(), seq_store.size(), 0);
|
|
if (ncopy != seq_store.size()) {
|
|
LOG_ERR("\n%s: seq copy data length %zd does not match expected length %zd\n", __func__, ncopy, seq_store.size());
|
|
return false;
|
|
}
|
|
LOG_TRC("%s: seq 0 copied, %zd bytes\n", __func__, ncopy);
|
|
|
|
llama_memory_clear(llama_get_memory(ctx.get()), true);
|
|
LOG_TRC("%s: kv cache cleared\n", __func__);
|
|
|
|
const size_t nset = llama_state_seq_set_data(ctx.get(), seq_store.data(), seq_store.size(), 1);
|
|
if (nset != seq_store.size()) {
|
|
LOG_ERR("\n%s: seq set data length %zd does not match expected length %zd\n", __func__, nset, seq_store.size());
|
|
return false;
|
|
}
|
|
LOG_TRC("%s: seq 1 restored, %zd bytes\n", __func__, nset);
|
|
}
|
|
|
|
// Generate tokens on seq 1
|
|
auto result = generate_tokens(ctx.get(), smpl.get(), n_past, params.n_predict, 1);
|
|
if (result.empty()) {
|
|
return false;
|
|
}
|
|
|
|
if (result != expected_result) {
|
|
LOG_ERR("\n%s: error: generation differs from expected\n", __func__);
|
|
return false;
|
|
}
|
|
|
|
LOG("\nPASS\n");
|
|
return true;
|
|
}
|
|
|
|
|
|
// Test 5: seq copy (device)
|
|
// - create a multi-seq context
|
|
// - load state from file
|
|
// - replay the last prompt token
|
|
// - migrate KV cache from seq 0 to seq 1 via the on-device path
|
|
// - generate n_predict tokens on seq 1 and compare against expected result
|
|
static bool test_seq_cp_device(struct llama_model * model, const struct common_params & params, const llama_tokens & tokens, const llama_tokens & expected_result) {
|
|
auto params_ctx = common_context_params_to_llama(params);
|
|
params_ctx.n_seq_max = 2;
|
|
auto ctx = llama_context_ptr{llama_init_from_model(model, params_ctx)};
|
|
|
|
auto sparams = llama_sampler_chain_default_params();
|
|
auto smpl = llama_sampler_ptr{llama_sampler_chain_init(sparams)};
|
|
llama_sampler_chain_add(smpl.get(), llama_sampler_init_dist(params.sampling.seed));
|
|
|
|
LOG("\n=== Test 5: seq copy (device) ===\n");
|
|
|
|
// Load state from file
|
|
llama_tokens unused_sts(tokens.size());
|
|
size_t n_token_count_out = 0;
|
|
|
|
if (!llama_state_load_file(ctx.get(), params.out_file.data(), unused_sts.data(), unused_sts.size(), &n_token_count_out)) {
|
|
LOG_ERR("\n%s: failed to load state\n", __func__);
|
|
return false;
|
|
}
|
|
|
|
LOG_TRC("%s: loaded state with %zu tokens\n", __func__, n_token_count_out);
|
|
|
|
// Replay last token
|
|
int n_past = (int) n_token_count_out - 1;
|
|
if (!common_replay_last_token(ctx.get(), tokens.back(), n_past)) {
|
|
return false;
|
|
}
|
|
n_past++;
|
|
|
|
// Migrate KV cache from seq 0 to seq 1 (on-device path)
|
|
{
|
|
std::vector<uint8_t> seq_store(llama_state_seq_get_size_ext(ctx.get(), 0, LLAMA_STATE_SEQ_FLAGS_ON_DEVICE));
|
|
const size_t ncopy = llama_state_seq_get_data_ext(ctx.get(), seq_store.data(), seq_store.size(), 0, LLAMA_STATE_SEQ_FLAGS_ON_DEVICE);
|
|
if (ncopy != seq_store.size()) {
|
|
LOG_ERR("\n%s: seq copy data length %zd does not match expected length %zd\n", __func__, ncopy, seq_store.size());
|
|
return false;
|
|
}
|
|
LOG_TRC("%s: seq 0 copied, %zd bytes\n", __func__, ncopy);
|
|
|
|
llama_memory_clear(llama_get_memory(ctx.get()), true);
|
|
LOG_TRC("%s: kv cache cleared\n", __func__);
|
|
|
|
const size_t nset = llama_state_seq_set_data_ext(ctx.get(), seq_store.data(), seq_store.size(), 1, LLAMA_STATE_SEQ_FLAGS_ON_DEVICE);
|
|
if (nset != seq_store.size()) {
|
|
LOG_ERR("\n%s: seq set data length %zd does not match expected length %zd\n", __func__, nset, seq_store.size());
|
|
return false;
|
|
}
|
|
LOG_TRC("%s: seq 1 restored, %zd bytes\n", __func__, nset);
|
|
}
|
|
|
|
// Generate tokens on seq 1
|
|
auto result = generate_tokens(ctx.get(), smpl.get(), n_past, params.n_predict, 1);
|
|
if (result.empty()) {
|
|
return false;
|
|
}
|
|
|
|
if (result != expected_result) {
|
|
LOG_ERR("\n%s: error: generation differs from expected\n", __func__);
|
|
return false;
|
|
}
|
|
|
|
LOG("\nPASS\n");
|
|
return true;
|
|
}
|
|
|
|
|
|
// Run the full save/load test suite (tests 1-5) for a single model.
|
|
// Returns true if all tests pass, false otherwise.
|
|
static bool run_save_load_tests_for_model(const std::string & model_path, const struct common_params & base_params) {
|
|
struct common_params params = base_params;
|
|
params.model.path = model_path;
|
|
|
|
auto llama_init = common_init_from_params(params, true);
|
|
auto * model = llama_init->model();
|
|
|
|
if (model == nullptr) {
|
|
LOG_ERR("%s: failed to init model '%s'\n", __func__, model_path.c_str());
|
|
return false;
|
|
}
|
|
|
|
GGML_ASSERT(llama_init->context() == nullptr);
|
|
|
|
// Tokenize prompt or generate random tokens
|
|
llama_tokens tokens;
|
|
if (params.prompt.empty()) {
|
|
const int n_prompt = params.n_batch;
|
|
|
|
// this path is useful for model files that do not have a tokenizer
|
|
LOG_INF("%s: no prompt provided, generating %d (n_batch) random tokens\n", __func__, n_prompt);
|
|
|
|
const auto * vocab = llama_model_get_vocab(model);
|
|
const auto n_vocab = llama_vocab_n_tokens(vocab);
|
|
|
|
std::mt19937 rng(params.sampling.seed);
|
|
std::uniform_int_distribution<llama_token> dist(0, n_vocab - 1);
|
|
for (int i = 0; i < n_prompt; i++) {
|
|
tokens.push_back(dist(rng));
|
|
}
|
|
} else {
|
|
LOG_INF("%s: tokenizing prompt '%s'\n", __func__, params.prompt.c_str());
|
|
|
|
auto ctx = llama_context_ptr{llama_init_from_model(model, common_context_params_to_llama(params))};
|
|
tokens = common_tokenize(ctx.get(), params.prompt, true);
|
|
}
|
|
|
|
LOG_INF("%s: the input prompt is %d tokens\n", __func__, (int)tokens.size());
|
|
|
|
// Test 1: baseline (saves state to disk)
|
|
auto result_baseline = test_baseline(model, params, tokens);
|
|
if (result_baseline.empty()) {
|
|
return false;
|
|
}
|
|
|
|
// Test 2: sequence removal isolation
|
|
if (!test_seq_rm_isolated(model, params, tokens)) {
|
|
return false;
|
|
}
|
|
|
|
// Test 3: state load
|
|
if (!test_state_load(model, params, tokens, result_baseline)) {
|
|
return false;
|
|
}
|
|
|
|
// Test 4: seq copy (host)
|
|
if (!test_seq_cp_host(model, params, tokens, result_baseline)) {
|
|
return false;
|
|
}
|
|
|
|
// Test 5: seq copy (device)
|
|
if (!test_seq_cp_device(model, params, tokens, result_baseline)) {
|
|
return false;
|
|
}
|
|
|
|
LOG("\nAll tests passed.\n");
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
int main(int argc, char ** argv) {
|
|
std::setlocale(LC_NUMERIC, "C");
|
|
|
|
common_params params;
|
|
params.prompt = "";
|
|
params.n_batch = 100;
|
|
params.out_file = "dump_state.bin";
|
|
params.sampling.seed = 1234;
|
|
|
|
common_init();
|
|
|
|
// extract our own --models DIR option before handing the rest to the common arg parser
|
|
std::string models_dir;
|
|
std::vector<char *> filtered_argv;
|
|
filtered_argv.push_back(argv[0]);
|
|
for (int i = 1; i < argc; i++) {
|
|
if (strcmp(argv[i], "--models") == 0) {
|
|
if (i + 1 >= argc) {
|
|
LOG_ERR("%s: --models requires a directory argument\n", __func__);
|
|
return 1;
|
|
}
|
|
models_dir = argv[i + 1];
|
|
i++;
|
|
} else {
|
|
filtered_argv.push_back(argv[i]);
|
|
}
|
|
}
|
|
filtered_argv.push_back(nullptr);
|
|
const int fargc = (int)filtered_argv.size() - 1;
|
|
|
|
// in --models mode there is no single model; set a placeholder so the common parser's
|
|
// "--model is required" check passes (each model is set individually inside the loop)
|
|
if (!models_dir.empty()) {
|
|
params.model.path = models_dir;
|
|
}
|
|
|
|
if (!common_params_parse(fargc, filtered_argv.data(), params, LLAMA_EXAMPLE_COMMON)) {
|
|
return 1;
|
|
}
|
|
|
|
if (params.n_parallel == 1) {
|
|
LOG_TRC("%s: n_parallel == 1, enabling unified kv cache\n", __func__);
|
|
params.kv_unified = true;
|
|
}
|
|
|
|
if (params.n_predict < 0) {
|
|
params.n_predict = 16;
|
|
}
|
|
|
|
ggml_backend_load_all();
|
|
|
|
if (!models_dir.empty()) {
|
|
// run the suite over every dummy model in the directory
|
|
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;
|
|
}
|
|
|
|
std::vector<std::string> models;
|
|
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;
|
|
}
|
|
|
|
LOG_INF("%s: running save/load tests over %zu models in '%s'\n", __func__, models.size(), models_dir.c_str());
|
|
|
|
size_t n_pass = 0;
|
|
size_t n_fail = 0;
|
|
for (const auto & model_path : models) {
|
|
LOG("\n================================================================\n");
|
|
LOG_INF("%s: model %s\n", __func__, model_path.c_str());
|
|
|
|
if (run_save_load_tests_for_model(model_path, params)) {
|
|
n_pass++;
|
|
} else {
|
|
n_fail++;
|
|
}
|
|
}
|
|
|
|
LOG("\n================================================================\n");
|
|
LOG_INF("%s: summary: %zu passed, %zu failed (of %zu)\n", __func__, n_pass, n_fail, models.size());
|
|
|
|
return n_fail == 0 ? 0 : 1;
|
|
}
|
|
|
|
// single-model mode
|
|
return run_save_load_tests_for_model(params.model.path, params) ? 0 : 1;
|
|
}
|