From 9bd60a1474fcae47c53ed4a757dd234b3984c702 Mon Sep 17 00:00:00 2001 From: dhb <1084714805@qq.com> Date: Fri, 19 Jul 2024 16:10:09 +0800 Subject: [PATCH] update wasm build updated module interface in preamble.js, build with support for runtime exit and simd [skip_fishtest] --- Rapfi/CMakeLists.txt | 56 ++++++++++++------ Rapfi/command/command.cpp | 61 ++++++++++--------- Rapfi/command/command.h | 7 +-- Rapfi/command/gomocup.cpp | 45 +++++++++++---- Rapfi/config.cpp | 8 +-- Rapfi/config.h | 4 +- Rapfi/emscripten/preamble.js | 36 +++++++----- Rapfi/main.cpp | 106 +++++++++++++++------------------- Rapfi/search/searchthread.cpp | 17 +++++- Rapfi/search/searchthread.h | 1 + 10 files changed, 193 insertions(+), 148 deletions(-) diff --git a/Rapfi/CMakeLists.txt b/Rapfi/CMakeLists.txt index a2fef98c..1e7ea9db 100644 --- a/Rapfi/CMakeLists.txt +++ b/Rapfi/CMakeLists.txt @@ -18,6 +18,8 @@ option(USE_BMI2 "Enable BMI2 instruction" OFF) option(USE_VNNI "Enable AVX512_VNNI/AVX_VNNI instruction" OFF) option(USE_NEON "Enable NEON instruction" OFF) option(USE_NEON_DOTPROD "Enable NEON dotprod instruction" OFF) +option(USE_WASM_SIMD "Enable WebAssembly SIMD instruction" OFF) +option(USE_WASM_SIMD_RELAXED "Enable WebAssembly Relaxed SIMD instruction" OFF) option(ENABLE_LTO "Enable link time optimization" OFF) option(FORCE_ASSERTION "Enable force assertion for all build type" OFF) @@ -173,17 +175,12 @@ if(EMSCRIPTEN) "Command modules is not supported for wasm build. " "Must set option NO_COMMAND_MODULES to ON.") endif() - set(USE_AVX2 OFF) - set(USE_AVX512 OFF) - set(USE_BMI2 OFF) - set(USE_VNNI OFF) - - target_compile_options(rapfi - PRIVATE "-fexceptions" - PRIVATE "-flto" - # PRIVATE "-msimd128" - # PRIVATE "-msse" - ) + if(USE_SSE OR USE_AVX2 OR USE_AVX512 OR USE_BMI2 OR USE_VNNI) + message(FATAL_ERROR + "SSE, AVX2, AVX512, BMI2, VNNI instruction set is not supported for wasm build.") + endif() + + target_compile_options(rapfi PRIVATE "-fexceptions" PRIVATE "-flto") string(CONCAT EMCC_FLAGS "-fexceptions " "-flto " @@ -192,19 +189,19 @@ if(EMSCRIPTEN) "--closure=1 " "-s MODULARIZE=1 " "-s EXPORT_NAME=Rapfi " - "-s ENVIRONMENT='web,worker' " - "-s EXPORTED_FUNCTIONS=\"['_gomocupLoopOnce']\" " + "-s EXPORTED_FUNCTIONS=\"['_main','_gomocupLoopOnce','_emscripten_force_exit']\" " "-s EXPORTED_RUNTIME_METHODS=\"['cwrap']\" " - "-s DEMANGLE_SUPPORT=1 " + "-s EXIT_RUNTIME=1 " "-s ALLOW_MEMORY_GROWTH=1 " - "-s INITIAL_MEMORY=134217728 " - "-s MAXIMUM_MEMORY=1073741824 " + "-s IMPORTED_MEMORY=1 " + "-s STACK_SIZE=2MB " ) + set(WASM_OUTPUT_NAME "rapfi") if(NO_MULTI_THREADING) - set_target_properties(rapfi PROPERTIES OUTPUT_NAME "rapfi-single") + string(CONCAT WASM_OUTPUT_NAME ${WASM_OUTPUT_NAME} "-single") else() - set_target_properties(rapfi PROPERTIES OUTPUT_NAME "rapfi-multi") + string(CONCAT WASM_OUTPUT_NAME ${WASM_OUTPUT_NAME} "-multi") target_compile_options(rapfi PRIVATE "-pthread") string(CONCAT EMCC_FLAGS ${EMCC_FLAGS} "-s USE_PTHREADS=1 " @@ -212,11 +209,32 @@ if(EMSCRIPTEN) ) endif() + if(USE_WASM_SIMD OR USE_WASM_SIMD_RELAXED) + string(CONCAT WASM_OUTPUT_NAME ${WASM_OUTPUT_NAME} "-simd128") + target_compile_options(rapfi PRIVATE "-msimd128") + target_compile_definitions(rapfi PRIVATE USE_WASM_SIMD) + if(USE_WASM_SIMD_RELAXED) + string(CONCAT WASM_OUTPUT_NAME ${WASM_OUTPUT_NAME} "-relaxed") + target_compile_options(rapfi PRIVATE "-mrelaxed-simd") + target_compile_definitions(rapfi PRIVATE USE_WASM_SIMD_RELAXED) + endif() + endif() + if(FORCE_ASSERTION) string(CONCAT EMCC_FLAGS ${EMCC_FLAGS} "-s ASSERTIONS=1 ") endif() + if(CMAKE_BUILD_TYPE EQUAL "DEBUG") + string(CONCAT EMCC_FLAGS ${EMCC_FLAGS} + "--emit-symbol-map " + ) + endif() + + message(STATUS "WASM_OUTPUT_NAME: ${WASM_OUTPUT_NAME}") - set_target_properties(rapfi PROPERTIES LINK_FLAGS ${EMCC_FLAGS}) + set_target_properties(rapfi PROPERTIES + OUTPUT_NAME ${WASM_OUTPUT_NAME} + LINK_FLAGS ${EMCC_FLAGS} + ) endif() diff --git a/Rapfi/command/command.cpp b/Rapfi/command/command.cpp index 36f2654a..2df49eee 100644 --- a/Rapfi/command/command.cpp +++ b/Rapfi/command/command.cpp @@ -14,7 +14,7 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . -*/ + */ #include "command.h" @@ -101,40 +101,46 @@ namespace CommandLine { } // namespace CommandLine +/// The path to the config file +/// ConfigPath must be absolute or relative to current working directory. +/// Can be empty which means we will try to load from the default config path. std::filesystem::path configPath; -std::filesystem::path overrideModelPath; -bool allowInternalConfigFallback = true; -/// loadConfig() loads config from configPath and optional override -/// model path. configPath must be absolute or relative to current -/// working directory. +/// Whether to allow fallback to internal config if the specified file is not found. +bool allowInternalConfig = true; + +/// loadConfig() trys to load config according to the following order: +/// 1. Load from the current config path. If config file exists but fails to load, +/// it will not continue to load other config. +/// 2. Try to load from the default config path, which is the "config.toml" in the +/// current working directory or the binary executable directory. +/// 3. If the above two steps fail, and allowInternalConfig is true, it will +/// try to load from the internal config string. Internal config is only available +/// when the program is built with it. bool loadConfig() { - bool success = false; - const bool loadOverrideModel = !overrideModelPath.empty(); + bool success = false; std::ifstream configFile(configPath); - if (configFile.is_open()) { MESSAGEL("Load config from " << configPath); - success = Config::loadConfig(configFile, loadOverrideModel); - if (!success) - return false; - if (loadOverrideModel) - success = loadModelFromFile(overrideModelPath); - } - else if (!allowInternalConfigFallback) { - ERRORL("Unable to open config file: " << configPath); - return false; + success = Config::loadConfig(configFile); } - - if (!success) { - if (!Config::InternalConfig.empty()) { - std::istringstream internalConfig(Config::InternalConfig); - success = Config::loadConfig(internalConfig); + else { + std::filesystem::path defaultConfigPath = CommandLine::getDefaultConfigPath(); + configFile.open(defaultConfigPath); + if (configFile.is_open()) { + MESSAGEL("Load config from " << defaultConfigPath); + success = Config::loadConfig(configFile); + } + else if (allowInternalConfig) { + if (!Config::InternalConfig.empty()) { + std::istringstream internalConfig(Config::InternalConfig); + success = Config::loadConfig(internalConfig); + } + else + ERRORL("This version is not built with an internal config. " + "Must specify an external config!"); } - else - ERRORL("This version is not built with an internal config. " - "Must specify an external config!"); } if (success && Config::ClearHashAfterConfigLoaded) @@ -156,9 +162,8 @@ std::filesystem::path getModelFullPath(std::filesystem::path modelPath) // If not succeeded, try to open from config directory if path is relative if (std::filesystem::path(modelPath).is_relative()) { std::filesystem::path configModelPath = configPath.remove_filename() / modelPath; - if (std::filesystem::exists(configModelPath)) { + if (std::filesystem::exists(configModelPath)) return configModelPath; - } } return modelPath; diff --git a/Rapfi/command/command.h b/Rapfi/command/command.h index a0f1120f..ce52a7c3 100644 --- a/Rapfi/command/command.h +++ b/Rapfi/command/command.h @@ -37,8 +37,7 @@ namespace CommandLine { // Config loading extern std::filesystem::path configPath; -extern std::filesystem::path overrideModelPath; -extern bool allowInternalConfigFallback; +extern bool allowInternalConfig; bool loadConfig(); std::filesystem::path getModelFullPath(std::filesystem::path modelPath); @@ -47,10 +46,6 @@ bool loadModelFromFile(std::filesystem::path modelPath); // ------------------------------------------------- // Gomocup protocol -namespace GomocupProtocol { - extern "C" bool gomocupLoopOnce(); -} - void gomocupLoop(); // ------------------------------------------------- diff --git a/Rapfi/command/gomocup.cpp b/Rapfi/command/gomocup.cpp index f6f1e205..72334913 100644 --- a/Rapfi/command/gomocup.cpp +++ b/Rapfi/command/gomocup.cpp @@ -404,7 +404,8 @@ void getOption() } else if (token == "EVALUATOR_DRAW_BLACK_WINRATE") { std::cin >> Config::EvaluatorDrawBlackWinRate; - Config::EvaluatorDrawBlackWinRate = std::clamp(Config::EvaluatorDrawBlackWinRate, 0.0f, 1.0f); + Config::EvaluatorDrawBlackWinRate = + std::clamp(Config::EvaluatorDrawBlackWinRate, 0.0f, 1.0f); } else if (token == "EVALUATOR_DRAW_RATIO") { std::cin >> Config::EvaluatorDrawRatio; @@ -453,10 +454,9 @@ void loadHash() void reloadConfig() { - configPath = readPathFromInput(); - overrideModelPath.clear(); - allowInternalConfigFallback = configPath.empty(); - if (allowInternalConfigFallback) + configPath = readPathFromInput(); + allowInternalConfig = configPath.empty(); + if (allowInternalConfig) MESSAGEL("No external config specified, reload internal config."); if (!loadConfig()) @@ -1242,7 +1242,7 @@ void loadModel() /// Enter protocol loop once and fetch and execute one command from stdin. /// @return True if program should exit now. -extern "C" bool gomocupLoopOnce() +bool runProtocol() { std::string cmd; std::cin >> cmd; @@ -1261,9 +1261,10 @@ extern "C" bool gomocupLoopOnce() }; // clang-format off - if (cmd == "STOP") { Search::Threads.stopThinking(); return false; } - else if (cmd == "YXSTOP") { Search::Threads.stopThinking(); return false; } - else if (thinking) return false; + if (cmd == "END") { Search::Threads.stopThinking(); return true; } + else if (cmd == "STOP") { Search::Threads.stopThinking(); return false; } + else if (cmd == "YXSTOP") { Search::Threads.stopThinking(); return false; } + else if (thinking) return false; #ifdef MULTI_THREADING std::lock_guard lock(mtx); @@ -1278,8 +1279,7 @@ extern "C" bool gomocupLoopOnce() && cmd != "YXQUERYDATABASETEXT" && cmd != "YXQUERYDATABASEALLT") Search::Threads.stopThinking(); - if (cmd == "END") return true; - else if (cmd == "ABOUT") std::cout << getEngineInfo() << std::endl; + if (cmd == "ABOUT") std::cout << getEngineInfo() << std::endl; else if (cmd == "START") start(); else if (cmd == "RECTSTART") rectStart(); else if (cmd == "INFO") getOption(); @@ -1337,10 +1337,16 @@ extern "C" bool gomocupLoopOnce() /// This will only return after all searching threads have ended. void Command::gomocupLoop() { +#ifdef __EMSCRIPTEN__ + // We do not run infinite loop in wasm build, instead we manually call + // gomocupLoopOnce() for each command to avoid hanging the main thread. + return; +#endif + // Init tuning parameter table Tuning::TuneMap::init(); - while (!GomocupProtocol::gomocupLoopOnce()) { + while (!GomocupProtocol::runProtocol()) { #ifdef MULTI_THREADING // For multi-threading build, yield before reading the next // command to avoid possible busy waiting. @@ -1351,3 +1357,18 @@ void Command::gomocupLoop() // If there is any thread still running, wait until they exited. Search::Threads.waitForIdle(); } + + +#ifdef __EMSCRIPTEN__ + #include + +extern "C" void gomocupLoopOnce() +{ + bool shouldExit = Command::GomocupProtocol::runProtocol(); + if (shouldExit) { + // If there is any thread still running, wait until they exited. + Search::Threads.waitForIdle(); + emscripten_force_exit(EXIT_SUCCESS); + } +} +#endif diff --git a/Rapfi/config.cpp b/Rapfi/config.cpp index 6773efec..41fb3a12 100644 --- a/Rapfi/config.cpp +++ b/Rapfi/config.cpp @@ -231,7 +231,7 @@ void readValueModel(const cpptoml::table &t, SetterType setter); /// @param skipModelLoading Whether to skip model loading. Can be useful when /// model is loaded separately from a binary file. /// @return Returns true if loading succeeded, otherwise returns false. -bool Config::loadConfig(std::istream &configStream, bool skipModelLoading) +bool Config::loadConfig(std::istream &configStream) { Search::Threads.setupEvaluator(nullptr); Search::Threads.setupDatabase(nullptr); @@ -251,10 +251,8 @@ bool Config::loadConfig(std::istream &configStream, bool skipModelLoading) if (auto database = c->get_table("database")) readDatabase(*database); - if (!skipModelLoading) { - if (auto model = c->get_table("model")) - readModel(*model); - } + if (auto model = c->get_table("model")) + readModel(*model); } catch (const std::exception &e) { ERRORL("Failed to load config: " << e.what()); diff --git a/Rapfi/config.h b/Rapfi/config.h index e8bd12a9..6a45d673 100644 --- a/Rapfi/config.h +++ b/Rapfi/config.h @@ -219,9 +219,9 @@ inline Value winRateToValue(float winRate) /// @param skipModelLoading Whether to skip model loading. This can be set when /// model is overrided in command line. /// @return True if config is loaded successfully. -bool loadConfig(std::istream &configStream, bool skipModelLoading = false); +bool loadConfig(std::istream &configStream); -/// Load a LZ4 compressed classical model from a binary stream. +/// Load a LZ4 compressed classical evaluation model from a binary stream. /// @return Returns true if loaded successfully, otherwise returns false. bool loadModel(std::istream &inStream); diff --git a/Rapfi/emscripten/preamble.js b/Rapfi/emscripten/preamble.js index 00fb92a9..07bc9b90 100644 --- a/Rapfi/emscripten/preamble.js +++ b/Rapfi/emscripten/preamble.js @@ -1,43 +1,49 @@ Module["sendCommand"] = Module["sendCommand"] || null; -Module["receiveStdout"] = Module["receiveStdout"] || ((o) => console.log(o)); -Module["receiveStderr"] = Module["receiveStderr"] || ((o) => console.error(o)); -Module["onEngineReady"] = Module["onEngineReady"] || (() => {}); +Module["terminate"] = Module["terminate"] || null; +Module["onReceiveStdout"] = Module["onReceiveStdout"] || ((o) => console.log(o)); +Module["onReceiveStderr"] = Module["onReceiveStderr"] || ((o) => console.error(o)); +Module["onExit"] = Module["onExit"] || ((code) => console.log("exited with code " + code)); +Module["noExitRuntime"] = true; // Only exit when we explicitly want to do so if (!Module["preRun"]) Module["preRun"] = []; Module["preRun"].push(function () { - let stdin_buffer = { buffer: "", index: 0 }; + let stdin_buffer = { data: "", index: 0 }; let stdout_buffer = ""; let stderr_buffer = ""; function stdin() { - if (stdin_buffer.index < stdin_buffer.buffer.length) - return stdin_buffer.buffer.charCodeAt(stdin_buffer.index++); + if (stdin_buffer.index < stdin_buffer.data.length) + return stdin_buffer.data.charCodeAt(stdin_buffer.index++); else return null; } + newline_charcode = "\n".charCodeAt(0) + stdout_fn = Module["onReceiveStdout"] + stderr_fn = Module["onReceiveStderr"] + function stdout(char) { - if (!char || char == "\n".charCodeAt(0)) { - Module["receiveStdout"](stdout_buffer); + if (!char || char == newline_charcode) { + stdout_fn(stdout_buffer); stdout_buffer = ""; } else stdout_buffer += String.fromCharCode(char); } function stderr(char) { - if (!char || char == "\n".charCodeAt(0)) { - Module["receiveStderr"](stderr_buffer); + if (!char || char == newline_charcode) { + stderr_fn(stderr_buffer); stderr_buffer = ""; } else stderr_buffer += String.fromCharCode(char); } // Redirect stdin, stdout, stderr FS.init(stdin, stdout, stderr); - let execute_command = Module["cwrap"]("gomocupLoopOnce", "number", []); + const execute_command = Module["cwrap"]("gomocupLoopOnce", "number", []); Module["sendCommand"] = function (data) { - stdin_buffer.buffer = data + "\n"; + stdin_buffer.data = data + "\n"; stdin_buffer.index = 0; execute_command(); }; + Module["terminate"] = function () { + Module["_emscripten_force_exit"](0); + }; }); -Module["onRuntimeInitialized"] = function () { - Module["onEngineReady"](); -}; diff --git a/Rapfi/main.cpp b/Rapfi/main.cpp index 77f7ec7d..f8dcb32d 100644 --- a/Rapfi/main.cpp +++ b/Rapfi/main.cpp @@ -32,23 +32,6 @@ int main(int argc, char *argv[]) Command::CommandLine::init(argc, argv); #ifdef COMMAND_MODULES - cxxopts::Options options("rapfi"); - options.add_options() // - ("mode", - "One of [gomocup, bench, opengen, tuning, selfplay, dataprep, database] running modes", - cxxopts::value()->default_value("gomocup")) // - ("config", - "Path to the specified config file", - cxxopts::value()) // - ("model", - "Override model in config file", - cxxopts::value()) // - ("h,help", "Print usage"); - options.parse_positional("mode"); - options.positional_help("[mode]"); - options.show_positional_help(); - options.allow_unrecognised_options(); - // The running mode of current Rapfi instance enum RunMode { GOMOCUP_PROTOCOL, @@ -60,56 +43,63 @@ int main(int argc, char *argv[]) DATABASE, } runMode = GOMOCUP_PROTOCOL; - try { - auto result = options.parse(argc, argv); + { + cxxopts::Options options("rapfi"); + options.add_options() // + ("mode", + "One of [gomocup, bench, opengen, tuning, selfplay, dataprep, database] run modes", + cxxopts::value()->default_value("gomocup")) // + ("config", + "Path to the specified config file", + cxxopts::value()) // + ("h,help", "Print usage"); + options.parse_positional("mode"); + options.positional_help("[mode]"); + options.show_positional_help(); + options.allow_unrecognised_options(); - std::string mode = result["mode"].as(); - upperInplace(mode); - if (mode == "GOMOCUP") - runMode = GOMOCUP_PROTOCOL; - else if (mode == "BENCH") - runMode = BENCHMARK; - else if (mode == "OPENGEN") - runMode = OPENGEN; - else if (mode == "TUNING") - runMode = TUNING; - else if (mode == "SELFPLAY") - runMode = SELFPLAY; - else if (mode == "DATAPREP") - runMode = DATAPREP; - else if (mode == "DATABASE") - runMode = DATABASE; - else - throw std::invalid_argument("unknown mode " + mode); + try { + auto result = options.parse(argc, argv); - if (result.count("help") && (runMode == GOMOCUP_PROTOCOL || runMode == BENCHMARK)) { - std::cout << options.help() << std::endl; - std::exit(EXIT_SUCCESS); - } + std::string mode = result["mode"].as(); + upperInplace(mode); + if (mode == "GOMOCUP") + runMode = GOMOCUP_PROTOCOL; + else if (mode == "BENCH") + runMode = BENCHMARK; + else if (mode == "OPENGEN") + runMode = OPENGEN; + else if (mode == "TUNING") + runMode = TUNING; + else if (mode == "SELFPLAY") + runMode = SELFPLAY; + else if (mode == "DATAPREP") + runMode = DATAPREP; + else if (mode == "DATABASE") + runMode = DATABASE; + else + throw std::invalid_argument("unknown mode " + mode); - if (result.count("config")) { - Command::configPath = result["config"].as(); - Command::allowInternalConfigFallback = false; + if (result.count("help") && (runMode == GOMOCUP_PROTOCOL || runMode == BENCHMARK)) { + std::cout << options.help() << std::endl; + std::exit(EXIT_SUCCESS); + } + + if (result.count("config")) { + Command::configPath = result["config"].as(); + Command::allowInternalConfig = false; + } } - else { - Command::configPath = Command::CommandLine::getDefaultConfigPath(); + catch (const std::exception &e) { + ERRORL("parsing argument: " << e.what()); + return EXIT_FAILURE; } - - if (result.count("model")) - Command::overrideModelPath = result["model"].as(); } - catch (const std::exception &e) { - ERRORL("parsing argument: " << e.what()); - std::exit(EXIT_FAILURE); - } - -#else - Command::configPath = Command::CommandLine::getDefaultConfigPath(); #endif if (!Command::loadConfig()) { ERRORL("Failed to load config, please check if config is correct."); - std::exit(EXIT_FAILURE); + return EXIT_FAILURE; } #ifdef COMMAND_MODULES @@ -126,7 +116,5 @@ int main(int argc, char *argv[]) Command::gomocupLoop(); #endif - // Explicitly free all threads - Search::Threads.setNumThreads(0); return EXIT_SUCCESS; } diff --git a/Rapfi/search/searchthread.cpp b/Rapfi/search/searchthread.cpp index 65e7208b..428d953d 100644 --- a/Rapfi/search/searchthread.cpp +++ b/Rapfi/search/searchthread.cpp @@ -14,7 +14,7 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . -*/ + */ #include "searchthread.h" @@ -325,7 +325,10 @@ void ThreadPool::startThinking(const Board &board, const SearchOptions &options, if (options.balanceMode == Search::SearchOptions::BALANCE_TWO) { // Use candidates before first move std::unordered_set cands; - FOR_EVERY_CAND_POS(main()->board, pos) { cands.insert(pos); } + FOR_EVERY_CAND_POS(main()->board, pos) + { + cands.insert(pos); + } main()->board->move(options.rule, m); @@ -401,4 +404,14 @@ ThreadPool::ThreadPool() setupSearcher(std::make_unique()); } +ThreadPool::~ThreadPool() +{ +#ifdef MULTI_THREADING + // Stop if there are still some threads thinking + stopThinking(); +#endif + // Explicitly free all threads + setNumThreads(0); +} + } // namespace Search diff --git a/Rapfi/search/searchthread.h b/Rapfi/search/searchthread.h index f77ec5d2..ef9d667c 100644 --- a/Rapfi/search/searchthread.h +++ b/Rapfi/search/searchthread.h @@ -221,6 +221,7 @@ class ThreadPool : public std::vector> uint64_t nodesSearched() const { return sum(&SearchThread::numNodes); } ThreadPool(); + ~ThreadPool(); }; template