// 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 #include #include #include #include #include #include #include #include // // 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(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 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 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 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 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 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 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 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 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 * 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(24, mr.tokens.size())); const llama_tokens tokens_verify(mr.tokens.end() - std::min(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 baseline; if (!decode(tokens_verify, 1, &baseline)) { return case_result::fail(); } const std::vector>> sub_cases = { { "buffer", [&]() { std::vector 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 data; { std::ifstream f(path, std::ios::binary); data.assign(std::istreambuf_iterator(f), std::istreambuf_iterator()); } 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 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> 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 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(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 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 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 make_kv_cases() { std::vector cases; cases.push_back(std::make_unique()); cases.push_back(std::make_unique()); cases.push_back(std::make_unique()); cases.push_back(std::make_unique(false)); cases.push_back(std::make_unique(true)); cases.push_back(std::make_unique(false)); cases.push_back(std::make_unique(true)); cases.push_back(std::make_unique()); cases.push_back(std::make_unique()); cases.push_back(std::make_unique()); cases.push_back(std::make_unique()); cases.push_back(std::make_unique()); cases.push_back(std::make_unique()); return cases; }