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