include llama.cpp since there were some edits
This commit is contained in:
1 parent
db7f28d087
commit
8c66990853
3918 files changed
+1640170
-1
No files matched your search
@@ -0,0 +1,787 @@
|
||||
// KV cache state cases
|
||||
//
|
||||
#include "test-case.h"
|
||||
|
||||
#include "test-log.h"
|
||||
#include "test-util.h"
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#include "../../src/llama-ext.h" // llama_model_set_attn_rot
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
//
|
||||
// reference run
|
||||
//
|
||||
|
||||
// - decode all but the last token, saving the state to disk
|
||||
// - decode the last token
|
||||
// - generate n_predict tokens
|
||||
// the logits it produces are the reference of every case with needs_baseline()
|
||||
struct case_baseline : ctx_test {
|
||||
const char * name() const override { return "baseline"; }
|
||||
|
||||
const char * description() const override {
|
||||
return "reference generation over the whole prompt, saves the state before the last token";
|
||||
}
|
||||
|
||||
case_result run(model_run & mr) const override {
|
||||
ctx_opts opts;
|
||||
opts.n_seq_max = 2;
|
||||
|
||||
auto ctx = mr.ctx(opts);
|
||||
auto smpl = mr.sampler();
|
||||
if (!ctx) {
|
||||
return case_result::fail();
|
||||
}
|
||||
|
||||
auto n_past = 0;
|
||||
if (!common_prompt_batch_decode(ctx.get(), mr.tokens, (int) mr.tokens.size(), n_past, mr.params.n_batch, mr.params.out_file, true)) {
|
||||
TLOG_ERR("%s: failed to decode the prompt\n", __func__);
|
||||
return case_result::fail();
|
||||
}
|
||||
|
||||
generation_result result;
|
||||
if (!generate_tokens(ctx.get(), smpl.get(), n_past, mr.params.n_predict, 0, result)) {
|
||||
return case_result::fail();
|
||||
}
|
||||
if (result.empty()) {
|
||||
return case_result::fail();
|
||||
}
|
||||
|
||||
mr.baseline = std::move(result);
|
||||
mr.have_baseline = true;
|
||||
|
||||
return case_result::pass();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// sequences
|
||||
//
|
||||
|
||||
// - decode the same prefix into two sequences
|
||||
// - remove sequence 0
|
||||
// - verify that sequence 1 remains unchanged
|
||||
struct case_seq_rm_isolated : ctx_test {
|
||||
const char * name() const override { return "seq_rm"; }
|
||||
|
||||
const char * description() const override {
|
||||
return "removing one sequence leaves the state of the other sequences untouched";
|
||||
}
|
||||
|
||||
case_result run(model_run & mr) const override {
|
||||
ctx_opts opts;
|
||||
opts.n_ctx = 256;
|
||||
opts.n_seq_max = 2;
|
||||
opts.kv_unified = 1;
|
||||
|
||||
auto ctx = mr.ctx(opts);
|
||||
if (!ctx) {
|
||||
return case_result::fail();
|
||||
}
|
||||
|
||||
const size_t n_tokens = std::min<size_t>(mr.tokens.size(), 128);
|
||||
for (llama_seq_id seq_id = 0; seq_id < 2; ++seq_id) {
|
||||
if (!decode_seq(ctx.get(), mr.tokens, 0, (llama_pos) n_tokens, seq_id, true)) {
|
||||
TLOG_ERR("%s: failed to decode the prompt for sequence %d\n", __func__, seq_id);
|
||||
return case_result::fail();
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<uint8_t> before;
|
||||
if (!get_seq_state(ctx.get(), 1, LLAMA_STATE_SEQ_FLAGS_NONE, before)) {
|
||||
return case_result::fail();
|
||||
}
|
||||
|
||||
if (!seq_rm(ctx.get(), 0)) {
|
||||
return case_result::fail();
|
||||
}
|
||||
|
||||
std::vector<uint8_t> after;
|
||||
if (!get_seq_state(ctx.get(), 1, LLAMA_STATE_SEQ_FLAGS_NONE, after)) {
|
||||
return case_result::fail();
|
||||
}
|
||||
|
||||
CTX_REQUIRE(blobs_equal(before, after), "removing seq 0 changed the state of seq 1");
|
||||
|
||||
return case_result::pass();
|
||||
}
|
||||
};
|
||||
|
||||
// - load the state saved by the baseline case
|
||||
// - replay its last token
|
||||
// - generate again and compare against the baseline logits
|
||||
struct case_state_load : ctx_test {
|
||||
const char * name() const override { return "state_load"; }
|
||||
|
||||
const char * description() const override {
|
||||
return "a saved session replays to the same logits as the run that saved it";
|
||||
}
|
||||
|
||||
bool needs_baseline() const override { return true; }
|
||||
|
||||
case_result run(model_run & mr) const override {
|
||||
ctx_opts opts;
|
||||
opts.n_seq_max = 2;
|
||||
|
||||
auto ctx = mr.ctx(opts);
|
||||
auto smpl = mr.sampler();
|
||||
if (!ctx) {
|
||||
return case_result::fail();
|
||||
}
|
||||
|
||||
int n_past = 0;
|
||||
if (!load_state_replay_last(ctx.get(), mr.params.out_file, mr.tokens, n_past)) {
|
||||
return case_result::fail();
|
||||
}
|
||||
|
||||
if (!generate_tokens_compare(ctx.get(), smpl.get(), n_past, mr.params.n_predict, 0, mr.baseline, NMSE_THRESHOLD)) {
|
||||
return case_result::fail();
|
||||
}
|
||||
|
||||
return case_result::pass();
|
||||
}
|
||||
};
|
||||
|
||||
// - load the state saved by the baseline case and replay its last token
|
||||
// - copy sequence 0 to sequence 1 over the given io path, clearing the cache in between
|
||||
// - continue on sequence 1 and compare against the baseline logits
|
||||
struct case_seq_cp : ctx_test {
|
||||
explicit case_seq_cp(bool on_device_) : on_device(on_device_) {}
|
||||
|
||||
const char * name() const override { return on_device ? "cp_d" : "cp_h"; }
|
||||
|
||||
const char * description() const override {
|
||||
return on_device ? "copying a sequence over the device io path keeps its state"
|
||||
: "copying a sequence over the host io path keeps its state";
|
||||
}
|
||||
|
||||
bool needs_baseline() const override { return true; }
|
||||
|
||||
case_result run(model_run & mr) const override {
|
||||
ctx_opts opts;
|
||||
opts.n_seq_max = 2;
|
||||
|
||||
auto ctx = mr.ctx(opts);
|
||||
auto smpl = mr.sampler();
|
||||
if (!ctx) {
|
||||
return case_result::fail();
|
||||
}
|
||||
|
||||
const uint32_t flags = on_device ? LLAMA_STATE_SEQ_FLAGS_ON_DEVICE : LLAMA_STATE_SEQ_FLAGS_NONE;
|
||||
TLOG_INF("io path: %s\n", on_device ? "device" : "host");
|
||||
|
||||
int n_past = 0;
|
||||
if (!load_state_replay_last(ctx.get(), mr.params.out_file, mr.tokens, n_past)) {
|
||||
return case_result::fail();
|
||||
}
|
||||
|
||||
{
|
||||
std::vector<uint8_t> seq_store;
|
||||
if (!get_seq_state(ctx.get(), 0, flags, seq_store)) {
|
||||
return case_result::fail();
|
||||
}
|
||||
TLOG_TRC("%s: seq 0 copied, %zu bytes\n", __func__, seq_store.size());
|
||||
|
||||
llama_memory_clear(llama_get_memory(ctx.get()), true);
|
||||
TLOG_TRC("%s: kv cache cleared\n", __func__);
|
||||
|
||||
if (!set_seq_state(ctx.get(), 1, flags, seq_store)) {
|
||||
return case_result::fail();
|
||||
}
|
||||
TLOG_TRC("%s: seq 1 restored, %zu bytes\n", __func__, seq_store.size());
|
||||
}
|
||||
|
||||
if (!generate_tokens_compare(ctx.get(), smpl.get(), n_past, mr.params.n_predict, 1, mr.baseline, NMSE_THRESHOLD)) {
|
||||
return case_result::fail();
|
||||
}
|
||||
|
||||
return case_result::pass();
|
||||
}
|
||||
|
||||
private:
|
||||
bool on_device;
|
||||
};
|
||||
|
||||
// - decode the same prefix on two sequences, interleaving seq 0 cells between the seq 1 cells
|
||||
// - save the seq 1 state, free the interleaved seq 0 cells, and restore via the given io path
|
||||
// - the restore destination is non-contiguous: scatter reads are batched per contiguous run
|
||||
// - save again on the host and compare the two blobs byte for byte
|
||||
struct case_seq_cp_scatter : ctx_test {
|
||||
explicit case_seq_cp_scatter(bool on_device_) : on_device(on_device_) {}
|
||||
|
||||
const char * name() const override { return on_device ? "cp_d_s" : "cp_h_s"; }
|
||||
|
||||
const char * description() const override {
|
||||
return "saving and restoring a sequence with a fragmented (scattered) cell range";
|
||||
}
|
||||
|
||||
case_result run(model_run & mr) const override {
|
||||
ctx_opts opts;
|
||||
opts.n_ctx = 256;
|
||||
opts.n_seq_max = 2;
|
||||
opts.kv_unified = 1;
|
||||
|
||||
auto ctx = mr.ctx(opts);
|
||||
if (!ctx) {
|
||||
return case_result::fail();
|
||||
}
|
||||
|
||||
const uint32_t flags = on_device ? LLAMA_STATE_SEQ_FLAGS_ON_DEVICE : LLAMA_STATE_SEQ_FLAGS_NONE;
|
||||
TLOG_INF("io path: %s\n", on_device ? "device" : "host");
|
||||
|
||||
// seq 0 cells 0,1,4 interleave the seq 1 cells 2,3,5
|
||||
if (!decode_one(ctx.get(), mr.tokens[0], 0, 0) ||
|
||||
!decode_one(ctx.get(), mr.tokens[1], 1, 0) ||
|
||||
!decode_one(ctx.get(), mr.tokens[0], 0, 1) ||
|
||||
!decode_one(ctx.get(), mr.tokens[1], 1, 1) ||
|
||||
!decode_one(ctx.get(), mr.tokens[2], 2, 0) ||
|
||||
!decode_one(ctx.get(), mr.tokens[2], 2, 1)) {
|
||||
TLOG_ERR("%s: failed to build the interleaved state\n", __func__);
|
||||
return case_result::fail();
|
||||
}
|
||||
|
||||
// host blob: contains the KV data, used for the byte-for-byte comparison
|
||||
std::vector<uint8_t> state_before;
|
||||
if (!get_seq_state(ctx.get(), 1, LLAMA_STATE_SEQ_FLAGS_NONE, state_before)) {
|
||||
return case_result::fail();
|
||||
}
|
||||
|
||||
// save via the io path under test
|
||||
std::vector<uint8_t> state_save;
|
||||
if (!get_seq_state(ctx.get(), 1, flags, state_save)) {
|
||||
return case_result::fail();
|
||||
}
|
||||
TLOG_TRC("%s: seq 1 saved via %s, %zu bytes\n", __func__, on_device ? "device" : "host", state_save.size());
|
||||
|
||||
// free seq 0's cells so the ring is fragmented: the restore destination (seq 1's interleaved cells) stays non-contiguous
|
||||
if (!seq_rm(ctx.get(), 0)) {
|
||||
return case_result::fail();
|
||||
}
|
||||
|
||||
// restore via the io path under test
|
||||
if (!set_seq_state(ctx.get(), 1, flags, state_save)) {
|
||||
return case_result::fail();
|
||||
}
|
||||
TLOG_TRC("%s: seq 1 restored via %s, %zu bytes\n", __func__, on_device ? "device" : "host", state_save.size());
|
||||
|
||||
std::vector<uint8_t> state_after;
|
||||
if (!get_seq_state(ctx.get(), 1, LLAMA_STATE_SEQ_FLAGS_NONE, state_after)) {
|
||||
return case_result::fail();
|
||||
}
|
||||
|
||||
// the blob is serialized in sequence cell order, so identical bytes iff the restore wrote the same KV
|
||||
CTX_REQUIRE(blobs_equal(state_before, state_after), "the restored KV state is not byte-identical to the saved state");
|
||||
|
||||
return case_result::pass();
|
||||
}
|
||||
|
||||
private:
|
||||
bool on_device;
|
||||
};
|
||||
|
||||
//
|
||||
// sessions
|
||||
//
|
||||
|
||||
// - save a sequence, erase it, restore it, save it again
|
||||
// - the two blobs must be identical
|
||||
// compares blobs rather than generated text: a partially restored cell still decodes to plausible tokens
|
||||
struct case_state_roundtrip : ctx_test {
|
||||
const char * name() const override { return "rt"; }
|
||||
|
||||
const char * description() const override {
|
||||
return "save, erase, restore, save again - the two blobs must be identical";
|
||||
}
|
||||
|
||||
case_result run(model_run & mr) const override {
|
||||
auto ctx = mr.ctx();
|
||||
if (!ctx) {
|
||||
return case_result::fail();
|
||||
}
|
||||
|
||||
common_batch batch = common_batch_get_one(ctx.get(), mr.tokens);
|
||||
if (llama_process(ctx.get(), LLAMA_PROCESS_TYPE_DECODE, batch.get())) {
|
||||
TLOG_ERR("%s: failed to decode the prompt\n", __func__);
|
||||
return case_result::fail();
|
||||
}
|
||||
|
||||
std::vector<uint8_t> blob_a;
|
||||
if (!get_seq_state(ctx.get(), 0, LLAMA_STATE_SEQ_FLAGS_NONE, blob_a)) {
|
||||
return case_result::fail();
|
||||
}
|
||||
|
||||
if (!seq_rm(ctx.get(), 0)) {
|
||||
return case_result::fail();
|
||||
}
|
||||
|
||||
if (!set_seq_state(ctx.get(), 0, LLAMA_STATE_SEQ_FLAGS_NONE, blob_a)) {
|
||||
TLOG_ERR("%s: failed to restore seq 0\n", __func__);
|
||||
return case_result::fail();
|
||||
}
|
||||
|
||||
std::vector<uint8_t> blob_b;
|
||||
if (!get_seq_state(ctx.get(), 0, LLAMA_STATE_SEQ_FLAGS_NONE, blob_b)) {
|
||||
return case_result::fail();
|
||||
}
|
||||
|
||||
CTX_REQUIRE(blobs_equal(blob_a, blob_b), "the state changed across a save / erase / restore cycle");
|
||||
|
||||
return case_result::pass();
|
||||
}
|
||||
};
|
||||
|
||||
// the partial state holds what seq_rm cannot roll back: recurrent states and SWA windows
|
||||
struct case_state_partial_size : ctx_test {
|
||||
const char * name() const override { return "ps"; }
|
||||
|
||||
const char * description() const override {
|
||||
return "the partial state is not larger than the full state and, without SWA, does not grow with the tokens";
|
||||
}
|
||||
|
||||
case_result run(model_run & mr) const override {
|
||||
auto ctx = mr.ctx();
|
||||
if (!ctx) {
|
||||
return case_result::fail();
|
||||
}
|
||||
|
||||
const llama_pos n_tokens = mr.tokens.size();
|
||||
const llama_pos n_half = n_tokens / 2;
|
||||
|
||||
if (!decode_seq(ctx.get(), mr.tokens, 0, n_half, 0, true)) {
|
||||
return case_result::fail();
|
||||
}
|
||||
|
||||
const size_t partial_a = llama_state_seq_get_size_ext(ctx.get(), 0, LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY);
|
||||
const size_t full_a = llama_state_seq_get_size_ext(ctx.get(), 0, LLAMA_STATE_SEQ_FLAGS_NONE);
|
||||
|
||||
if (!decode_seq(ctx.get(), mr.tokens, n_half, n_tokens, 0, true)) {
|
||||
return case_result::fail();
|
||||
}
|
||||
|
||||
const size_t partial_b = llama_state_seq_get_size_ext(ctx.get(), 0, LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY);
|
||||
const size_t full_b = llama_state_seq_get_size_ext(ctx.get(), 0, LLAMA_STATE_SEQ_FLAGS_NONE);
|
||||
|
||||
TLOG_INF("%s: partial %zu -> %zu, full %zu -> %zu\n", __func__, partial_a, partial_b, full_a, full_b);
|
||||
|
||||
CTX_REQUIRE(partial_a <= full_a && partial_b <= full_b, "the partial state is larger than the full state");
|
||||
|
||||
if (llama_model_n_swa(mr.model) == 0) {
|
||||
CTX_REQUIRE(partial_a == partial_b, "the partial state grows with the tokens");
|
||||
}
|
||||
|
||||
return case_result::pass();
|
||||
}
|
||||
};
|
||||
|
||||
// a failed restore must leave the sequence empty and must not change the logits of other sequences
|
||||
struct case_state_restore_failure : ctx_test {
|
||||
const char * name() const override { return "rf"; }
|
||||
|
||||
const char * description() const override {
|
||||
return "restoring a corrupted state fails and leaves the other sequences untouched";
|
||||
}
|
||||
|
||||
case_result run(model_run & mr) const override {
|
||||
ctx_opts opts;
|
||||
opts.n_ctx = 256;
|
||||
opts.n_seq_max = 4;
|
||||
opts.kv_unified = 1;
|
||||
|
||||
// without flash attention, corrupted data left behind by the restore shows up as NaN logits on the other sequences
|
||||
opts.fa = LLAMA_FLASH_ATTN_TYPE_DISABLED;
|
||||
|
||||
auto ctx = mr.ctx(opts);
|
||||
if (!ctx) {
|
||||
return case_result::fail();
|
||||
}
|
||||
|
||||
llama_memory_t mem = llama_get_memory(ctx.get());
|
||||
if (mem == nullptr) {
|
||||
TLOG_INF("no memory to test\n");
|
||||
return case_result::pass();
|
||||
}
|
||||
|
||||
const auto decode = [&](const llama_tokens & inp, llama_seq_id seq_id, std::vector<float> * logits_out) {
|
||||
if (!decode_tokens(ctx.get(), inp, seq_id)) {
|
||||
TLOG_ERR("%s: failed to decode on sequence %d\n", __func__, seq_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (logits_out && !get_current_logits(ctx.get(), *logits_out)) {
|
||||
TLOG_ERR("%s: failed to get logits\n", __func__);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const llama_tokens tokens_save (mr.tokens.begin(), mr.tokens.begin() + std::min<size_t>(24, mr.tokens.size()));
|
||||
const llama_tokens tokens_verify(mr.tokens.end() - std::min<size_t>(8, mr.tokens.size()), mr.tokens.end());
|
||||
|
||||
// the cases share a working directory, so the state file is named after the model
|
||||
const std::string path = "state-restore-failure." + mr.name + ".tmp.bin";
|
||||
|
||||
llama_memory_clear(mem, true);
|
||||
|
||||
std::vector<float> baseline;
|
||||
if (!decode(tokens_verify, 1, &baseline)) {
|
||||
return case_result::fail();
|
||||
}
|
||||
|
||||
const std::vector<std::pair<const char *, std::function<bool()>>> sub_cases = {
|
||||
{ "buffer", [&]() {
|
||||
std::vector<uint8_t> state(llama_state_seq_get_size(ctx.get(), 0));
|
||||
GGML_ASSERT(llama_state_seq_get_data(ctx.get(), state.data(), state.size(), 0) == state.size());
|
||||
llama_memory_seq_rm(mem, 0, -1, -1);
|
||||
|
||||
if (!corrupt_state(state)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return llama_state_seq_set_data(ctx.get(), state.data(), state.size(), 0) == 0;
|
||||
}},
|
||||
{ "file", [&]() {
|
||||
GGML_ASSERT(llama_state_seq_save_file(ctx.get(), path.c_str(), 0, tokens_save.data(), tokens_save.size()) > 0);
|
||||
llama_memory_seq_rm(mem, 0, -1, -1);
|
||||
|
||||
std::vector<uint8_t> data;
|
||||
{
|
||||
std::ifstream f(path, std::ios::binary);
|
||||
data.assign(std::istreambuf_iterator<char>(f), std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
if (!corrupt_state(data)) {
|
||||
std::remove(path.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
{
|
||||
std::ofstream f(path, std::ios::binary);
|
||||
f.write((const char *) data.data(), data.size());
|
||||
}
|
||||
|
||||
llama_tokens tokens_out(tokens_save.size());
|
||||
size_t n_token_count = 0;
|
||||
const size_t nread = llama_state_seq_load_file(ctx.get(), path.c_str(), 0, tokens_out.data(), tokens_out.size(), &n_token_count);
|
||||
std::remove(path.c_str());
|
||||
|
||||
return nread == 0;
|
||||
}},
|
||||
};
|
||||
|
||||
for (const auto & [name, restore_failed] : sub_cases) {
|
||||
llama_memory_clear(mem, true);
|
||||
|
||||
if (!decode(tokens_save, 0, nullptr)) {
|
||||
return case_result::fail();
|
||||
}
|
||||
|
||||
CTX_REQUIRE(restore_failed(), std::string(name) + ": restoring a corrupted state did not fail");
|
||||
|
||||
CTX_REQUIRE(llama_memory_seq_pos_max(mem, 0) == -1,
|
||||
std::string(name) + ": the sequence is not empty after a failed restore");
|
||||
|
||||
std::vector<float> logits;
|
||||
if (!decode(tokens_verify, 1, &logits)) {
|
||||
return case_result::fail();
|
||||
}
|
||||
|
||||
const logits_diff diff = compare_logits(baseline.data(), logits.data(), logits.size());
|
||||
|
||||
CTX_REQUIRE(diff.n_nan == 0 && diff.diff_max <= 1e-6f,
|
||||
std::string(name) + ": the logits changed after a failed restore (max diff = " +
|
||||
std::to_string(diff.diff_max) + ", nan = " + std::to_string(diff.n_nan) + ")");
|
||||
|
||||
TLOG_TRC("%s: %s: logits match (max diff = %g)\n", __func__, name, (double) diff.diff_max);
|
||||
}
|
||||
|
||||
return case_result::pass();
|
||||
}
|
||||
};
|
||||
|
||||
// a KV state saved with attention rotation enabled must restore only into a context with the
|
||||
// same setting
|
||||
// note: rotation is only active for quantized KV caches with a head size that is a multiple of
|
||||
// 64, for other models the restore into the rotation-disabled context is valid and the
|
||||
// test passes vacuously
|
||||
struct case_state_rotation : ctx_test {
|
||||
const char * name() const override { return "rot"; }
|
||||
|
||||
const char * description() const override {
|
||||
return "a state saved with attention rotation restores only into a matching context";
|
||||
}
|
||||
|
||||
case_result run(model_run & mr) const override {
|
||||
const auto make_context = [&](ggml_type type_k, ggml_type type_v, bool disable_rotation) {
|
||||
// a setting of the model, not of the process - a sweep that runs several models at a
|
||||
// time must not let this leak into the contexts of another model
|
||||
llama_model_set_attn_rot(mr.model, !disable_rotation);
|
||||
|
||||
ctx_opts opts;
|
||||
opts.n_ctx = 32;
|
||||
opts.n_batch = 1;
|
||||
opts.n_ubatch = 1;
|
||||
opts.type_k = type_k;
|
||||
opts.type_v = type_v;
|
||||
opts.fa = LLAMA_FLASH_ATTN_TYPE_ENABLED;
|
||||
|
||||
// probing whether this KV type is supported is expected to fail for some models
|
||||
opts.quiet = true;
|
||||
|
||||
return mr.ctx(opts);
|
||||
};
|
||||
|
||||
std::vector<std::pair<ggml_type, ggml_type>> type_pairs;
|
||||
for (const auto & types : { std::pair{GGML_TYPE_Q8_0, GGML_TYPE_Q8_0} }) {
|
||||
if (make_context(types.first, types.second, false)) {
|
||||
type_pairs.push_back(types);
|
||||
}
|
||||
}
|
||||
if (type_pairs.empty()) {
|
||||
return case_result::skip("no supported quantized KV cache type combination");
|
||||
}
|
||||
|
||||
bool success = true;
|
||||
for (const auto & types : type_pairs) {
|
||||
auto src = make_context(types.first, types.second, false);
|
||||
if (!src) {
|
||||
TLOG_ERR("%s: failed to create the source context\n", __func__);
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
|
||||
llama_token token = 0;
|
||||
if (llama_decode(src.get(), llama_batch_get_one(&token, 1))) {
|
||||
TLOG_ERR("%s: failed to decode a token\n", __func__);
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> state;
|
||||
if (!get_seq_state(src.get(), 0, LLAMA_STATE_SEQ_FLAGS_NONE, state)) {
|
||||
if (state.empty()) {
|
||||
continue; // no KV state to test
|
||||
}
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
|
||||
auto matching = make_context(types.first, types.second, false);
|
||||
if (!matching || !set_seq_state(matching.get(), 0, LLAMA_STATE_SEQ_FLAGS_NONE, state)) {
|
||||
TLOG_ERR("%s: failed to restore a matching rotation\n", __func__);
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
|
||||
auto mismatched = make_context(types.first, types.second, true);
|
||||
if (!mismatched) {
|
||||
TLOG_ERR("%s: failed to create a mismatched rotation context\n", __func__);
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
// a model that does not use attention rotation accepts the mismatched restore
|
||||
if (set_seq_state(mismatched.get(), 0, LLAMA_STATE_SEQ_FLAGS_NONE, state)) {
|
||||
TLOG_TRC("%s: state restored into rotation-disabled context, model does not use attention rotation\n", __func__);
|
||||
}
|
||||
}
|
||||
|
||||
llama_model_set_attn_rot(mr.model, true); // the default, the cases that follow rely on it
|
||||
|
||||
if (!success) {
|
||||
return case_result::fail();
|
||||
}
|
||||
|
||||
return case_result::pass();
|
||||
}
|
||||
};
|
||||
|
||||
// restore a sequence into a fragmented cache: the slots freed by the removed sequence are
|
||||
// scattered between live cells of the other sequences, so the restore cannot claim one
|
||||
// contiguous block
|
||||
// the restored state must come back byte for byte, and the sequence must continue to behave
|
||||
// exactly as the same sequence in a context where it was never removed
|
||||
// ref: https://github.com/ggml-org/llama.cpp/issues/17527
|
||||
struct case_state_restore_fragmented : ctx_test {
|
||||
const char * name() const override { return "frag"; }
|
||||
|
||||
const char * description() const override {
|
||||
return "restore a sequence into a cache fragmented by the removal of another sequence";
|
||||
}
|
||||
|
||||
std::string skip_reason(model_run & mr) const override {
|
||||
if (arch_reserves_single_seq(mr.model)) {
|
||||
return "the interleaved batch is a multi-seq graph";
|
||||
}
|
||||
|
||||
return ctx_test::skip_reason(mr);
|
||||
}
|
||||
|
||||
case_result run(model_run & mr) const override {
|
||||
const int n_vocab = llama_vocab_n_tokens(llama_model_get_vocab(mr.model));
|
||||
|
||||
ctx_opts opts;
|
||||
opts.n_ctx = 256;
|
||||
opts.n_seq_max = 3;
|
||||
opts.kv_unified = 1;
|
||||
opts.n_batch = 3*70;
|
||||
opts.n_ubatch = 3*70; // the interleaved prompt is a single multi-seq batch
|
||||
|
||||
// ctx removes and restores seq 1, ctx_keep keeps it untouched as the reference
|
||||
llama_context_ptr ctx = mr.ctx(opts);
|
||||
llama_context_ptr ctx_keep = mr.ctx(opts);
|
||||
if (!ctx || !ctx_keep) {
|
||||
return case_result::fail();
|
||||
}
|
||||
|
||||
const llama_pos n_tokens = (llama_pos) std::min<size_t>(70, mr.tokens.size());
|
||||
|
||||
// interleave the 3 sequences: 01201230123... - seq 1 owns every third cell
|
||||
const auto decode_interleaved = [&](llama_context * cur) {
|
||||
common_batch batch(cur);
|
||||
for (llama_pos i = 0; i < n_tokens; i++) {
|
||||
for (llama_seq_id s = 0; s < 3; ++s) {
|
||||
batch.add(mr.tokens[i], i, s, false);
|
||||
}
|
||||
}
|
||||
batch.set_output(batch.size() - 1, true);
|
||||
|
||||
return llama_process(cur, LLAMA_PROCESS_TYPE_DECODE, batch.get()) == 0;
|
||||
};
|
||||
|
||||
if (!decode_interleaved(ctx.get()) || !decode_interleaved(ctx_keep.get())) {
|
||||
TLOG_ERR("%s: failed to decode the interleaved prompt\n", __func__);
|
||||
return case_result::fail();
|
||||
}
|
||||
|
||||
TLOG_INF("%s: processed the prompt on seq 0, 1, 2 (%d tokens each)\n", __func__, n_tokens);
|
||||
|
||||
std::vector<uint8_t> state_before;
|
||||
if (!get_seq_state(ctx.get(), 1, LLAMA_STATE_SEQ_FLAGS_NONE, state_before)) {
|
||||
return case_result::fail();
|
||||
}
|
||||
|
||||
// clearing seq 1 leaves holes where its cells were - no contiguous block large enough
|
||||
// for the seq 1 state is left in the cache
|
||||
llama_memory_seq_rm(llama_get_memory(ctx.get()), 1, -1, -1);
|
||||
|
||||
if (!set_seq_state(ctx.get(), 1, LLAMA_STATE_SEQ_FLAGS_NONE, state_before)) {
|
||||
TLOG_ERR("%s: failed to restore the seq state into a fragmented cache\n", __func__);
|
||||
return case_result::fail();
|
||||
}
|
||||
|
||||
TLOG_INF("%s: restored seq 1 into a fragmented cache, %zu bytes\n", __func__, state_before.size());
|
||||
|
||||
// the restore has to be lossless - the cells the sequence ends up in are irrelevant, its
|
||||
// state has to come back exactly as it was saved
|
||||
std::vector<uint8_t> state_after;
|
||||
if (!get_seq_state(ctx.get(), 1, LLAMA_STATE_SEQ_FLAGS_NONE, state_after)) {
|
||||
return case_result::fail();
|
||||
}
|
||||
|
||||
CTX_REQUIRE(blobs_equal(state_before, state_after), "restoring into a fragmented cache changed the state of seq 1");
|
||||
|
||||
// the restored sequence must continue exactly as the untouched one - decode the same token
|
||||
// on seq 1 in both contexts and compare the logits they produce
|
||||
const llama_token token = gen_token(n_vocab, 1, n_tokens);
|
||||
|
||||
if (!decode_one(ctx.get(), token, n_tokens, 1) || !decode_one(ctx_keep.get(), token, n_tokens, 1)) {
|
||||
TLOG_ERR("%s: failed to decode with the restored state\n", __func__);
|
||||
return case_result::fail();
|
||||
}
|
||||
|
||||
const float * logits = llama_get_logits(ctx.get());
|
||||
const float * logits_keep = llama_get_logits(ctx_keep.get());
|
||||
if (!logits || !logits_keep) {
|
||||
TLOG_ERR("%s: missing logits after the restore\n", __func__);
|
||||
return case_result::fail();
|
||||
}
|
||||
|
||||
const logits_diff diff = compare_logits(logits, logits_keep, n_vocab);
|
||||
|
||||
CTX_REQUIRE(diff.nmse() <= NMSE_THRESHOLD,
|
||||
"the logits of the restored seq 1 differ from the reference: nmse " + std::to_string(diff.nmse()));
|
||||
|
||||
TLOG_INF("%s: restored seq 1 continued identically to the reference, nmse %g\n", __func__, diff.nmse());
|
||||
|
||||
return case_result::pass();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// no_alloc
|
||||
//
|
||||
|
||||
// - load a second instance of the model with no_alloc, so the tensor data stays on disk
|
||||
// - create a context from it, which reserves the compute graphs over the dummy buffers
|
||||
// - the memory use must still be reported, and nothing real may be allocated for the memory
|
||||
struct case_noalloc : ctx_test {
|
||||
const char * name() const override { return "noalloc"; }
|
||||
|
||||
const char * description() const override {
|
||||
return "with no_alloc the memory use is simulated: non-zero breakdown, null buffer bases";
|
||||
}
|
||||
|
||||
case_result run(model_run & mr) const override {
|
||||
const std::string & path = mr.params.model.path;
|
||||
CTX_REQUIRE(!path.empty(), "the model path is not known");
|
||||
|
||||
llama_model_params mparams = llama_model_default_params();
|
||||
mparams.no_alloc = true;
|
||||
mparams.load_mode = LLAMA_LOAD_MODE_NONE;
|
||||
|
||||
llama_model_ptr model(llama_model_load_from_file(path.c_str(), mparams));
|
||||
CTX_REQUIRE(model, "failed to load the model with no_alloc");
|
||||
|
||||
auto ctx = init_ctx(model.get(), common_context_params_to_llama(mr.params), 0);
|
||||
CTX_REQUIRE(ctx, "failed to create the context with no_alloc");
|
||||
|
||||
size_t n_ctx_mem = 0;
|
||||
for (const auto & [_, data] : llama_get_memory_breakdown(ctx.get())) {
|
||||
n_ctx_mem += data.context;
|
||||
}
|
||||
|
||||
// models without a memory module (BERT, diffusion) have a null memory
|
||||
if (llama_get_memory(ctx.get()) != nullptr) {
|
||||
CTX_REQUIRE(n_ctx_mem > 0, "the simulated memory breakdown is zero");
|
||||
} else {
|
||||
CTX_REQUIRE(n_ctx_mem == 0, "a model without memory reported context memory");
|
||||
}
|
||||
|
||||
for (ggml_backend_buffer_t buf : llama_memory_get_buffers(llama_get_memory(ctx.get()))) {
|
||||
CTX_REQUIRE(ggml_backend_buffer_get_base(buf) == nullptr, "a memory buffer was really allocated");
|
||||
}
|
||||
|
||||
return case_result::pass();
|
||||
}
|
||||
};
|
||||
|
||||
std::vector<ctx_test_ptr> make_kv_cases() {
|
||||
std::vector<ctx_test_ptr> cases;
|
||||
|
||||
cases.push_back(std::make_unique<case_baseline>());
|
||||
cases.push_back(std::make_unique<case_seq_rm_isolated>());
|
||||
cases.push_back(std::make_unique<case_state_load>());
|
||||
cases.push_back(std::make_unique<case_seq_cp>(false));
|
||||
cases.push_back(std::make_unique<case_seq_cp>(true));
|
||||
cases.push_back(std::make_unique<case_seq_cp_scatter>(false));
|
||||
cases.push_back(std::make_unique<case_seq_cp_scatter>(true));
|
||||
cases.push_back(std::make_unique<case_state_roundtrip>());
|
||||
cases.push_back(std::make_unique<case_state_partial_size>());
|
||||
cases.push_back(std::make_unique<case_state_restore_failure>());
|
||||
cases.push_back(std::make_unique<case_state_rotation>());
|
||||
cases.push_back(std::make_unique<case_state_restore_fragmented>());
|
||||
cases.push_back(std::make_unique<case_noalloc>());
|
||||
|
||||
return cases;
|
||||
}
|
||||
Reference in new issue
Block a user