Files
msime-windows/engine/tests/src/bench_neural_rescore.cpp
T
Lu Fan(SonnyCalcr) 649747f8cd feat(server): 神经网络整句效果优先模型改为按需下载 (#838)
效果优先模型(sentence-model-desktop.safetensors,约 25 MB)不再随安装包分发,
改为和八股文模型一样在设置页下载后才能使用。

- engine:SentenceAssociationOptions 新增 neural_desktop_model 路径,留空即模型缺席、
  静默不重排;词典不再从资源目录固定加载效果档。load_file 按 UTF-8 打开路径,
  数据目录在非 ASCII 用户目录下也能读到模型
- server:新增 settings/neural_model,下载到 <数据目录>\models\neural-desktop\,
  下载与导入都按 neural-model/lock.json 的字节数和 SHA256 校验后原子落位;
  会话层只在开关打开时解析模型路径
- ui-html:设置页增加下载 / 导入 / 删除 / 浏览器下载,模型未就绪时开关置灰
- installer:不再打包效果档模型;旧版装进数据目录根下的那份在覆盖安装与卸载时清掉

原有配置项 sentence_neural_desktop 保持不变,用户的开关状态升级后保留。
2026-10-11 18:08:33 +08:00

270 lines
9.1 KiB
C++

// Measure the engine async rerank path with real dictionaries and neural models.
// Run by hand: METASEQUOIA_IME_DATA_DIR=<data directory> bench_neural_rescore [repeats]
// Each row measures the final key. Prefix keys run before the timer starts.
// session_cold and session_warm describe InputSession prefix/query caches.
// Warm mode types the prefix twice without clearing those caches.
// SentenceModel prefix K/V cache is process-wide and is not reset between rows.
// Model loading happens before timing starts.
// Active prefix jobs can delay the final job; async_ms includes that delay.
#include "core/input_session.h"
#include "core/data_path.h"
#include "core/runtime_paths.h"
#include "core/sentence_association_options.h"
#include "contracts/assets/assets.h"
#include "neural/neural_decoder.h"
#include "neural/rescore_worker.h"
#include <algorithm>
#include <array>
#include <chrono>
#include <condition_variable>
#include <cstdio>
#include <cstdlib>
#include <deque>
#include <filesystem>
#include <mutex>
#include <string>
#include <utility>
namespace
{
using Clock = std::chrono::steady_clock;
using TimePoint = Clock::time_point;
constexpr auto kTimeout = std::chrono::seconds(30);
struct CallbackEvents
{
std::mutex mutex;
std::condition_variable ready;
std::deque<TimePoint> times;
void push()
{
std::lock_guard<std::mutex> lock(mutex);
times.push_back(Clock::now());
ready.notify_one();
}
void clear()
{
std::lock_guard<std::mutex> lock(mutex);
times.clear();
}
bool wait_until(TimePoint deadline, TimePoint &time)
{
std::unique_lock<std::mutex> lock(mutex);
if (!ready.wait_until(lock, deadline, [&] { return !times.empty(); }))
return false;
// A slow initial query can overlap prefix and final callbacks. Use the newest event.
time = times.back();
times.clear();
return true;
}
};
double milliseconds(TimePoint start, TimePoint end)
{
return std::chrono::duration<double, std::milli>(end - start).count();
}
bool has_source(const metasequoia::InputSession &session, CandidateSource source)
{
const auto &candidates = session.candidates();
return std::any_of(candidates.begin(), candidates.end(),
[source](const WordItem &candidate) { return candidate.source == source; });
}
bool type_prefix(metasequoia::InputSession &session, const std::string &input)
{
for (std::size_t index = 0; index + 1 < input.size(); ++index)
if (!session.handle_character(input[index]).handled)
return false;
return true;
}
std::string context_of_length(std::size_t characters)
{
constexpr std::array<const char *, 18> phrase = {"今", "天", "下", "午", "我", "们", "讨", "论", "输",
"入", "法", "排", "序", "和", "候", "选", "体", "验"};
std::string context;
for (std::size_t index = 0; index < characters; ++index)
context += phrase[index % phrase.size()];
return context;
}
struct Case
{
const char *model;
CandidateSource source;
const char *context_name;
std::string context;
const char *length_name;
const char *pinyin;
const char *session_cache_name;
bool warm_session_cache;
};
void print_failure(const Case &item, int repeat, const char *status)
{
std::printf("%s,%s,%s,%s,%s,%d,NA,NA,NA,NA,%s\n", item.model, item.context_name, item.length_name, item.pinyin,
item.session_cache_name, repeat, status);
}
bool run_case(const Case &item, int repeat, CallbackEvents &events)
{
neural::RescoreWorker &worker = neural::RescoreWorker::instance();
worker.clear();
events.clear();
metasequoia::InputSession session(SchemeType::Quanpin);
SentenceAssociationOptions options;
options.word_lattice = true;
options.show_next_on_duplicate = true;
options.neural_keyboard = item.source == CandidateSource::NeuralKeyboard;
options.neural_desktop = item.source == CandidateSource::NeuralDesktop;
options.neural_desktop_model = metasequoia::path_to_utf8(
metasequoia::RuntimePaths::legacy().resource(metasequoia::assets::neural_model_desktop));
session.set_sentence_association(options);
session.set_rescoring_context(item.context);
const std::string input = item.pinyin;
if (item.warm_session_cache)
{
if (!type_prefix(session, input))
{
print_failure(item, repeat, "invalid_prefix");
return false;
}
session.reset_state();
}
else
{
session.reset_cache();
}
if (!type_prefix(session, input))
{
print_failure(item, repeat, "invalid_prefix");
return false;
}
events.clear();
const TimePoint start = Clock::now();
if (!session.handle_character(input.back()).handled)
{
print_failure(item, repeat, "invalid_final_key");
return false;
}
const TimePoint initial = Clock::now();
const bool static_visible = has_source(session, CandidateSource::Generated);
const bool neural_visible = has_source(session, item.source);
if (!static_visible || neural_visible)
{
print_failure(item, repeat, static_visible ? "neural_already_visible" : "no_static_sentence");
return false;
}
const TimePoint deadline = initial + kTimeout;
TimePoint callback;
while (events.wait_until(deadline, callback))
{
if (callback < start)
continue;
session.reset_sentence_cache();
session.recompute_candidates();
const TimePoint requery_done = Clock::now();
const bool settled = has_source(session, item.source);
if (!settled)
continue;
std::printf("%s,%s,%s,%s,%s,%d,%.3f,%.3f,%.3f,%.3f,ok\n", item.model, item.context_name, item.length_name,
item.pinyin, item.session_cache_name, repeat, milliseconds(start, initial),
milliseconds(initial, callback), milliseconds(callback, requery_done),
milliseconds(start, requery_done));
return true;
}
print_failure(item, repeat, "neural_timeout");
return false;
}
bool run_all(long repeats, CallbackEvents &events)
{
for (const auto &[model, source] :
{std::pair{"keyboard", CandidateSource::NeuralKeyboard}, std::pair{"desktop", CandidateSource::NeuralDesktop}})
{
for (const auto &[context_name, context_chars] :
{std::pair{"empty", 0u}, std::pair{"16-char", 16u}, std::pair{"64-char", 64u}})
{
for (const auto &[length_name, pinyin] :
{std::pair{"short", "nihaoshijie"}, std::pair{"medium", "womenzaijianmian"},
std::pair{"long", "zhegewentiyinggaizenmejiejue"}})
{
for (bool warm : {false, true})
{
const Case item{model,
source,
context_name,
context_of_length(context_chars),
length_name,
pinyin,
warm ? "session_warm" : "session_cold",
warm};
for (int repeat = 1; repeat <= repeats; ++repeat)
if (!run_case(item, repeat, events))
return false;
}
}
}
}
return true;
}
} // namespace
int main(int argc, char **argv)
{
if (argc > 2)
{
std::fprintf(stderr, "Usage: bench_neural_rescore [repeats]\n");
return 2;
}
char *end = nullptr;
const long repeats = argc == 2 ? std::strtol(argv[1], &end, 10) : 3;
if (repeats < 1 || repeats > 100 || (argc == 2 && *end != '\0'))
{
std::fprintf(stderr, "repeats must be between 1 and 100\n");
return 2;
}
const auto paths = metasequoia::RuntimePaths::legacy();
for (const char *name :
{metasequoia::assets::main_dictionary, metasequoia::assets::pinyin_model, metasequoia::assets::language_model,
metasequoia::assets::neural_model_keyboard, metasequoia::assets::neural_model_desktop})
{
if (!std::filesystem::is_regular_file(paths.resource(name)))
{
std::fprintf(stderr, "Missing benchmark asset: %s\n", name);
return 2;
}
}
for (const char *name : {metasequoia::assets::neural_model_keyboard, metasequoia::assets::neural_model_desktop})
{
if (neural::shared_sentence_model(metasequoia::path_to_utf8(paths.resource(name))) == nullptr)
{
std::fprintf(stderr, "Invalid benchmark model: %s\n", name);
return 2;
}
}
CallbackEvents events;
neural::RescoreWorker &worker = neural::RescoreWorker::instance();
worker.set_ready_callback([&events] { events.push(); });
std::printf(
"model,context,pinyin_class,pinyin,session_cache,repeat,initial_ms,async_ms,requery_ms,settle_ms,status\n");
std::fflush(stdout);
const bool passed = run_all(repeats, events);
worker.set_ready_callback(nullptr);
worker.shutdown();
return passed ? 0 : 1;
}