Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(server): improve rapfi engine support with ai workers #297

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
2 changes: 2 additions & 0 deletions GomokuClient/packages/eslint-config/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export default [
"messages.ts",
"src/api/**",
"tailwind.config.js",
"src/ai/**",
"public/build/**",
],
},

Expand Down
Binary file not shown.

Large diffs are not rendered by default.

Binary file not shown.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file not shown.

Large diffs are not rendered by default.

Binary file not shown.
165 changes: 165 additions & 0 deletions GomokuClient/packages/gomoku-core/src/ai/engine.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { checkSharedArrayBufferSupport } from "./util.js";

const STEngineURL = "/build/rapfi-single.js";
const MTEngineURL = "/build/rapfi-multi.js";
const supportSAB = checkSharedArrayBufferSupport();
var callback, worker;

function init(f) {
callback = f;

if (supportSAB) {
console.log("Attempting to initialize multi-threaded engine...");
import(/* @vite-ignore */ MTEngineURL)
.then(() => {
console.log("Multi-threaded engine imported.");
if (Bridge.ready) {
console.log("Bridge is ready.");
callback({ ok: true });
} else {
Bridge.setReady = () => {
console.log("Bridge is now ready.");
callback({ ok: true });
};
}

Bridge.readStdout = (d) => {
console.log("Processing output from multi-threaded engine:", d);
processOutput(d);
};
})
.catch((err) => {
console.error("Error importing multi-threaded engine:", err);
callback({ ok: false, error: err });
});
} else {
console.log(
"SharedArrayBuffer not supported. Falling back to single-threaded engine...",
);
worker = new Worker(STEngineURL, { type: "module" });

worker.onmessage = function (e) {
if (e.data.ready) {
console.log("Worker is ready.");
callback({ ok: true });
} else {
console.log(
"Processing output from single-threaded worker:",
e.data.output,
);
processOutput(e.data.output);
}
};

worker.onerror = function (ev) {
worker.terminate();
console.error("Worker spawn error: " + ev.message + ". Retrying...");
setTimeout(() => init(f), 200);
};
}
}

// Returns true if force stoped, otherwise returns false
function stopThinking() {
if (!supportSAB) {
console.warn("No support for SAB, failed to stop thinking.");

worker.terminate();
init(callback); // Use previous callback function

return true;
} else {
sendCommand("YXSTOP");
return false;
}
}

function sendCommand(cmd) {
console.log("Sending command to engine:", cmd);
if (typeof cmd !== "string" || cmd.length == 0) return;

if (supportSAB) {
console.log("Sending command to engine:", cmd);
// eslint-disable-next-line
Bridge.writeStdin(cmd);
} else {
console.log("Sending command to worker:", cmd);
worker.postMessage(cmd);
}
}

function processOutput(output) {
console.log("Engine output received:", output);

if (typeof callback !== "function") {
console.warn("No valid callback function set.");
return;
}

let i = output.indexOf(" ");

if (i === -1) {
if (output === "OK") {
console.log("Engine acknowledged command.");
return;
} else if (output === "SWAP") {
callback({ swap: true });
} else {
let coord = output.split(",");
callback({ pos: [+coord[0], +coord[1]] });
}
return;
}

let head = output.substring(0, i);
let tail = output.substring(i + 1);

if (head === "MESSAGE") {
console.log("Processing MESSAGE from engine:", tail);
if (tail.startsWith("REALTIME")) {
let r = tail.split(" ");
if (r.length < 3) {
callback({ realtime: { type: r[1] } });
} else {
let coord = r[2].split(",");
callback({ realtime: { type: r[1], pos: [+coord[0], +coord[1]] } });
}
} else {
callback({ msg: tail });
}
} else if (head === "INFO") {
console.log("Processing INFO from engine:", tail);
i = tail.indexOf(" ");
head = tail.substring(0, i);
tail = tail.substring(i + 1);

if (head === "PV") callback({ multipv: tail });
else if (head === "NUMPV") callback({ numpv: +tail });
else if (head === "DEPTH") callback({ depth: +tail });
else if (head === "SELDEPTH") callback({ seldepth: +tail });
else if (head === "NODES") callback({ nodes: +tail });
else if (head === "TOTALNODES") callback({ totalnodes: +tail });
else if (head === "TOTALTIME") callback({ totaltime: +tail });
else if (head === "SPEED") callback({ speed: +tail });
else if (head === "EVAL") callback({ eval: tail });
else if (head === "WINRATE") callback({ winrate: parseFloat(tail) });
else if (head === "BESTLINE") {
callback({
bestline: tail.match(/([A-Z]\d+)/g).map((s) => {
let coord = s.match(/([A-Z])(\d+)/);
let x = coord[1].charCodeAt(0) - "A".charCodeAt(0);
let y = +coord[2] - 1;
return [x, y];
}),
});
}
} else if (head === "ERROR") {
console.error("Engine ERROR:", tail);
callback({ error: tail });
} else {
console.log("Unknown output type:", head, tail);
callback({ unknown: tail });
}
}

export { init, sendCommand, stopThinking };
121 changes: 121 additions & 0 deletions GomokuClient/packages/gomoku-core/src/ai/rapfi-bridge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
if (typeof Module === "undefined") {
// Bridge object for writing stdin & reading stdout
var Bridge =
typeof Bridge !== "undefined"
? Bridge
: {
ready: false,
writeStdin() {},
readStdout() {},
setReady() {
this.ready = true;
},
};

// Setup webassembly glue module (only for main thread)
var Module = {
preInit: [
function () {
FS.createLazyFile(
"/",
"mix6freestyle_bs15.bin.lz4",
"/network/mix6freestyle_bs15.bin.lz4",
true,
false,
);
FS.createLazyFile(
"/",
"mix6freestyle_bsmix.bin.lz4",
"/network/mix6freestyle_bsmix.bin.lz4",
true,
false,
);
FS.createLazyFile(
"/",
"mix6standard_bs15.bin.lz4",
"/network/mix6standard_bs15.bin.lz4",
true,
false,
);
FS.createLazyFile(
"/",
"mix6renju_bs15_black.bin.lz4",
"/network/mix6renju_bs15_black.bin.lz4",
true,
false,
);
FS.createLazyFile(
"/",
"mix6renju_bs15_white.bin.lz4",
"/network/mix6renju_bs15_white.bin.lz4",
true,
false,
);
},
],
preRun: [
function () {
let input = {
str: "",
index: 0,
set: function (str) {
this.str = str + "\n";
this.index = 0;
},
};

let output = {
str: "",
flush: function () {
Bridge.readStdout(this.str);
this.str = "";
},
};

function stdin() {
// Return ASCII code of character, or null if no input
let char = input.str.charCodeAt(input.index++);
return isNaN(char) ? null : char;
}

function stdout(char) {
if (!char || char == "\n".charCodeAt(0)) {
output.flush();
} else {
output.str += String.fromCharCode(char);
}
}

FS.init(stdin, stdout, stdout);
let pipeLoopOnce = Module.cwrap("gomocupLoopOnce", "number", []);
Bridge.writeStdin = function (data) {
input.set(data);
pipeLoopOnce();
};
},
],
onRuntimeInitialized() {
console.log("Rapfi bridge initialized");
Bridge.setReady();
},
};

// If we are running in a worker, setup onmessage & postMessage
if (typeof importScripts === "function") {
self.onmessage = function (e) {
Bridge.writeStdin(e.data);
};
Bridge.readStdout = function (data) {
postMessage({ output: data });
};
Bridge.setReady = function () {
postMessage({ ready: true });
};
} else {
// Otherwise we are running in the main window, adjust URL
Module.locateFile = function (url) {
return "/build/" + url;
};
Module.mainScriptUrlOrBlob = "/build/rapfi-multi.js";
}
}
17 changes: 17 additions & 0 deletions GomokuClient/packages/gomoku-core/src/ai/util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Check whether SharedArrayBuffer is supported
export function checkSharedArrayBufferSupport() {
// Do not request cross origin isolated now
// as it is not widely support on all browsers
//if (!self.crossOriginIsolated) return false

let supportSAB = typeof self.SharedArrayBuffer !== "undefined";
if (supportSAB) {
let tempMemory = new WebAssembly.Memory({
initial: 1,
maximum: 1,
shared: true,
});
supportSAB = tempMemory.buffer instanceof self.SharedArrayBuffer;
}
return supportSAB;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { createContext, useContext, useEffect, useState, useMemo } from "react";

import type { PropsWithChildren } from "react";

// @ts-ignore
import { init, sendCommand, stopThinking } from "@/ai/engine.js";

interface WasmEngineContextType {
isEngineReady: boolean;
sendCommand: (cmd: string) => void;
stopThinking: () => void;
}

const WasmEngineContext = createContext<WasmEngineContextType | undefined>(
undefined,
);

export const WasmEngineProvider = ({ children }: PropsWithChildren) => {
const [isEngineReady, setIsEngineReady] = useState(false);

useEffect(() => {
console.log("Initializing engine...");
init(({ ok }: { ok: boolean }) => {
if (ok) {
console.log("Engine initialized successfully.");
setIsEngineReady(true);
} else {
console.error("Engine failed to initialize.");
}
});
}, []);

const contextValue: WasmEngineContextType = useMemo(
() => ({
isEngineReady,
sendCommand,
stopThinking,
}),
[isEngineReady],
);

return (
<WasmEngineContext.Provider value={contextValue}>
{children}
</WasmEngineContext.Provider>
);
};

export const useWasmEngine = (): WasmEngineContextType => {
const context = useContext(WasmEngineContext);
if (!context) {
throw new Error("useWasmEngine must be used within a WasmEngineProvider");
}
return context;
};
Loading
Loading