// // llm_demo.cpp // // Created by MNN on 2023/03/24. // ZhaodeWang // #include "llm/llm.hpp" #define MNN_OPEN_TIME_TRACE #include #include #include #include #include #include using namespace MNN::Transformer; std::vector> parse_csv(const std::vector& lines) { std::vector> csv_data; std::string line; std::vector row; std::string cell; bool insideQuotes = false; bool startCollecting = false; // content to stream std::string content = ""; for (auto line : lines) { content = content + line + "\n"; } std::istringstream stream(content); while (stream.peek() != EOF) { char c = stream.get(); if (c == '"') { if (insideQuotes && stream.peek() == '"') { // quote cell += '"'; stream.get(); // skip quote } else { insideQuotes = !insideQuotes; // start or end text in quote } startCollecting = true; } else if (c == ',' && !insideQuotes) { // end element, start new element row.push_back(cell); cell.clear(); startCollecting = false; } else if ((c == '\n' || stream.peek() == EOF) && !insideQuotes) { // end line row.push_back(cell); csv_data.push_back(row); cell.clear(); row.clear(); startCollecting = false; } else { cell += c; startCollecting = true; } } return csv_data; } static bool fileExists(const std::string& path) { FILE* f = fopen(path.c_str(), "r"); if (f) { fclose(f); return true; } return false; } static void verifySyncFiles(const std::string& prefixDir, const std::string& fileName) { int layerCount = 0; bool allExist = true; for (int i = 0; i < 256; i++) { std::string base = prefixDir + "/" + fileName + "_" + std::to_string(i); std::string k_file = base + ".k"; std::string v_file = base + ".v"; if (!fileExists(k_file) && !fileExists(v_file)) { break; } std::string k_sync = base + "_sync.k"; std::string v_sync = base + "_sync.v"; if (!fileExists(k_sync)) { MNN_PRINT("[TEST FAIL] Missing: %s\n", k_sync.c_str()); allExist = false; } if (!fileExists(v_sync)) { MNN_PRINT("[TEST FAIL] Missing: %s\n", v_sync.c_str()); allExist = false; } layerCount++; } if (allExist && layerCount > 0) { MNN_PRINT("[TEST PASS] All %d layers sync files verified.\n", layerCount); } else if (layerCount == 0) { MNN_PRINT("[TEST FAIL] No KV cache files found in %s/\n", prefixDir.c_str()); } else { MNN_PRINT("[TEST FAIL] %d layers checked, some sync files missing.\n", layerCount); } } static int benchmark(Llm* llm, const std::vector& prompts, int max_token_number, bool is_prompt_cache) { if (prompts.size() < 3) { MNN_ERROR("Need larger than 3 inputs\n"); return 0; } auto context = llm->getContext(); int initSize = 2; if (max_token_number <= 0) { max_token_number = 512; } if(is_prompt_cache) { MNN_PRINT("Prefix prompt cache demo\n"); auto prompt_base = prompts[0]; auto prompt_add_0 = prompts[1]; auto prompt_add_1 = prompts[2]; std::vector history; // step 1: set prefix cache file name llm->setPrefixCacheFile("model_prompt_config_mnnversion"); // step 2: prefill prefix prompt llm->response(prompt_base, &std::cout, nullptr, 0); // Verify: sync files should exist after first response (completePrefixWrite) verifySyncFiles("prefixcache", "model_prompt_config_mnnversion"); auto prompt_len = context->prompt_len; auto decode_len = context->gen_seq_len; auto prefill_time = context->prefill_us; auto decode_time = context->decode_us; auto sample_time = context->sample_us; auto first_prefill_time = prefill_time; // step 3: prompt_add_0 for response llm->response(prompt_add_0); // step 4: erase first prompt_add_0 history history.emplace_back(llm->getCurrentHistory()); llm->eraseHistory(prompt_len, history[0]); prompt_len += context->prompt_len; decode_len += context->gen_seq_len; prefill_time += context->prefill_us; decode_time += context->decode_us; sample_time += context->sample_us; // step 5: prompt_add_1 for response llm->response(prompt_add_1); prompt_len += context->prompt_len; decode_len += context->gen_seq_len; prefill_time += context->prefill_us; decode_time += context->decode_us; sample_time += context->sample_us; float prefill_s = prefill_time / 1e6; float decode_s = decode_time / 1e6; float sample_s = sample_time / 1e6; MNN_PRINT("\n#################################\n"); MNN_PRINT("prompt tokens num = %d\n", prompt_len); MNN_PRINT("decode tokens num = %d\n", decode_len); MNN_PRINT("first prefill time = %.2f s\n", (float)(first_prefill_time / 1e6)); MNN_PRINT("prefill time = %.2f s\n", prefill_s); MNN_PRINT(" decode time = %.2f s\n", decode_s); MNN_PRINT(" sample time = %.2f s\n", sample_s); MNN_PRINT("prefill speed = %.2f tok/s\n", prompt_len / prefill_s); MNN_PRINT(" decode speed = %.2f tok/s\n", decode_len / decode_s); MNN_PRINT("##################################\n"); // step 6 (Stage 3): full clear after prefix load + new prompt. // Exercises the only path where the alignment block (PR #4424 reset) // in CPULinearAttention::onExecute changes behavior vs. the prior // snapshot restore: previous == remove > 0 AND not loading from disk. // We then reset + re-run the same prompt to get a clean-session // baseline, and compare textually. if (prompts.size() >= 4) { MNN_PRINT("\n[Stage 3] Full clear after prefix load, then new prompt\n"); const auto& new_prompt = prompts[3]; // Path A: from current prefix-loaded session, full clear then re-prefill+decode. // max_new_tokens=-1 lets response() decode to EOS (the demo's other decode // calls do the same via default arg). Passing 0 here would prefill-only. llm->eraseHistory(0, llm->getCurrentHistory()); std::ostringstream after_clear_out; llm->response(new_prompt, &after_clear_out, nullptr, -1); std::string after_clear_text = after_clear_out.str(); MNN_PRINT("[Stage 3] Response after full clear:\n%s\n", after_clear_text.c_str()); // Path B: reset to a fully clean state and run the same prompt. llm->reset(); llm->eraseHistory(0, 0); std::ostringstream baseline_out; llm->response(new_prompt, &baseline_out, nullptr, -1); std::string baseline_text = baseline_out.str(); MNN_PRINT("[Stage 3] Baseline (clean session) response:\n%s\n", baseline_text.c_str()); if (after_clear_text == baseline_text) { MNN_PRINT("[TEST PASS] Stage 3: full clear after prefix load matches clean session.\n"); } else { MNN_PRINT("[TEST FAIL] Stage 3: output mismatch after full clear vs clean session.\n"); } } else { MNN_PRINT("[Stage 3] Skipped: needs at least 4 prompts (got %zu)\n", prompts.size()); } } else { MNN_PRINT("Prefill\n"); std::vector history; for (int i = 0; i < 3; i++) { const auto& prompt = prompts[i]; llm->response(prompt, &std::cout, nullptr, 0); history.emplace_back(llm->getCurrentHistory()); } MNN_PRINT("\n"); MNN_PRINT("[LLM Test: Erase 1]\n"); llm->eraseHistory(history[0], history[1]); llm->response(prompts[prompts.size()-1], &std::cout, nullptr, 0); while (!llm->stoped() && context->gen_seq_len < max_token_number) { llm->generate(1); } MNN_PRINT("\n[LLM Test End]\n"); llm->eraseHistory(0, 0); history.clear(); for (int i = 0; i < 3; i++) { const auto& prompt = prompts[i]; llm->response(prompt, &std::cout, nullptr, 0); history.emplace_back(llm->getCurrentHistory()); } MNN_PRINT("[LLM Test: Erase 2]\n"); llm->eraseHistory(history[1], history[2]); llm->response(prompts[prompts.size()-1], &std::cout, nullptr, 0); while (!llm->stoped() && context->gen_seq_len < max_token_number) { llm->generate(1); } MNN_PRINT("\n[LLM Test End]\n"); MNN_PRINT("[LLM Test For Init]\n"); llm->reset(); llm->eraseHistory(0, 0); llm->response(prompts[prompts.size()-1], &std::cout, nullptr, 0); while (!llm->stoped() && context->gen_seq_len < max_token_number) { llm->generate(1); } MNN_PRINT("\n[LLM Test End]\n"); } return 0; } static int eval(Llm* llm, std::string prompt_file, int max_token_number, bool is_prompt_cache) { std::cout << "prompt file is " << prompt_file << std::endl; std::ifstream prompt_fs(prompt_file); std::vector prompts; std::string prompt; while (std::getline(prompt_fs, prompt)) { if (prompt.back() == '\r') { prompt.pop_back(); } prompts.push_back(prompt); } prompt_fs.close(); if (prompts.empty()) { return 1; } return benchmark(llm, prompts, max_token_number, is_prompt_cache); } int main(int argc, const char* argv[]) { if (argc < 2) { std::cout << "Usage: " << argv[0] << " config.json prompt.txt " << std::endl; return 0; } MNN::BackendConfig backendConfig; auto executor = MNN::Express::Executor::newExecutor(MNN_FORWARD_CPU, backendConfig, 1); MNN::Express::ExecutorScope s(executor); std::string config_path = argv[1]; std::cout << "config path is " << config_path << std::endl; std::unique_ptr llm(Llm::createLLM(config_path)); llm->set_config("{\"tmp_path\":\"tmp\"}"); llm->set_config("{\"prefix_cache_path\":\"prefixcache\"}"); { AUTOTIME; llm->load(); } std::string prompt_file = argv[2]; int enable_cache_prompt = 0; if (argc >= 4) { std::istringstream os(argv[3]); os >> enable_cache_prompt; if(enable_cache_prompt != 0 && enable_cache_prompt != 1) { MNN_PRINT("[Warning]: cache_prefix_in_disk value only accept 0 or 1.\n"); } } int max_token_number = -1; if (argc >= 5) { std::istringstream os(argv[4]); os >> max_token_number; } return eval(llm.get(), prompt_file, max_token_number, enable_cache_prompt == 1); }