463 lines
15 KiB
TypeScript
463 lines
15 KiB
TypeScript
/*
|
|
* Licensed to the Apache Software Foundation (ASF) under one
|
|
* or more contributor license agreements. See the NOTICE file
|
|
* distributed with this work for additional information
|
|
* regarding copyright ownership. The ASF licenses this file
|
|
* to you under the Apache License, Version 2.0 (the
|
|
* "License"); you may not use this file except in compliance
|
|
* with the License. You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing,
|
|
* software distributed under the License is distributed on an
|
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
* KIND, either express or implied. See the License for the
|
|
* specific language governing permissions and limitations
|
|
* under the License.
|
|
*/
|
|
|
|
import { SizeOf, TypeIndex } from "./ctypes";
|
|
import { assert, StringToUint8Array, Uint8ArrayToString } from "./support";
|
|
import { detectGPUDevice, GPUDeviceDetectOutput } from "./webgpu";
|
|
import * as compact from "./compact";
|
|
import * as runtime from "./runtime";
|
|
import { Disposable } from "./types";
|
|
|
|
enum RPCServerState {
|
|
InitHeader,
|
|
InitHeaderKey,
|
|
InitServer,
|
|
WaitForCallback,
|
|
ReceivePacketHeader,
|
|
ReceivePacketBody,
|
|
}
|
|
|
|
/** RPC magic header */
|
|
const RPC_MAGIC = 0xff271;
|
|
|
|
/**
|
|
* An utility class to read from binary bytes.
|
|
*/
|
|
class ByteStreamReader {
|
|
offset = 0;
|
|
bytes: Uint8Array;
|
|
|
|
constructor(bytes: Uint8Array) {
|
|
this.bytes = bytes;
|
|
}
|
|
|
|
readU32(): number {
|
|
const i = this.offset;
|
|
const b = this.bytes;
|
|
const val = b[i] | (b[i + 1] << 8) | (b[i + 2] << 16) | (b[i + 3] << 24);
|
|
this.offset += 4;
|
|
return val;
|
|
}
|
|
|
|
readU64(): number {
|
|
const val = this.readU32();
|
|
this.offset += 4;
|
|
return val;
|
|
}
|
|
|
|
readByteArray(): Uint8Array {
|
|
const len = this.readU64();
|
|
assert(this.offset + len <= this.bytes.byteLength);
|
|
const ret = new Uint8Array(len);
|
|
ret.set(this.bytes.slice(this.offset, this.offset + len));
|
|
this.offset += len;
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A websocket based RPC
|
|
*/
|
|
export class RPCServer {
|
|
url: string;
|
|
key: string;
|
|
socket: WebSocket;
|
|
state: RPCServerState = RPCServerState.InitHeader;
|
|
logger: (msg: string) => void;
|
|
getImports: () => Record<string, unknown>;
|
|
private tensorCacheUrl: string;
|
|
private tensorCacheDevice: string;
|
|
private initProgressCallback?: runtime.InitProgressCallback;
|
|
private asyncOnServerLoad?: (inst: runtime.Instance) => Promise<void>;
|
|
private pendingSend: Promise<void> = Promise.resolve();
|
|
private name: string;
|
|
private inst?: runtime.Instance = undefined;
|
|
private globalObjects: Array<Disposable> = [];
|
|
private serverRecvData?: (header: Uint8Array, body: Uint8Array) => void;
|
|
private currPacketHeader?: Uint8Array;
|
|
private currPacketLength = 0;
|
|
private remoteKeyLength = 0;
|
|
private pendingBytes = 0;
|
|
private buffredBytes = 0;
|
|
private messageQueue: Array<Uint8Array> = [];
|
|
|
|
constructor(
|
|
url: string,
|
|
key: string,
|
|
getImports: () => Record<string, unknown>,
|
|
logger: (msg: string) => void = console.log,
|
|
tensorCacheUrl = "",
|
|
tensorCacheDevice = "cpu",
|
|
initProgressCallback: runtime.InitProgressCallback | undefined = undefined,
|
|
asyncOnServerLoad: ((inst: runtime.Instance) => Promise<void>) | undefined = undefined,
|
|
) {
|
|
this.url = url;
|
|
this.key = key;
|
|
this.name = "WebSocketRPCServer[" + this.key + "]: ";
|
|
this.getImports = getImports;
|
|
this.logger = logger;
|
|
this.tensorCacheUrl = tensorCacheUrl;
|
|
this.tensorCacheDevice = tensorCacheDevice;
|
|
this.initProgressCallback = initProgressCallback;
|
|
this.asyncOnServerLoad = asyncOnServerLoad;
|
|
this.checkLittleEndian();
|
|
this.socket = compact.createWebSocket(url);
|
|
this.socket.binaryType = "arraybuffer";
|
|
|
|
this.socket.addEventListener("open", (event: Event) => {
|
|
return this.onOpen(event);
|
|
});
|
|
this.socket.addEventListener("message", (event: MessageEvent) => {
|
|
return this.onMessage(event);
|
|
});
|
|
this.socket.addEventListener("close", (event: CloseEvent) => {
|
|
return this.onClose(event);
|
|
});
|
|
}
|
|
|
|
private onClose(_event: CloseEvent): void {
|
|
if (this.inst !== undefined) {
|
|
this.globalObjects.forEach(obj => {
|
|
obj.dispose();
|
|
});
|
|
this.log(this.inst.runtimeStatsText());
|
|
this.inst.dispose();
|
|
}
|
|
if (this.state === RPCServerState.ReceivePacketHeader) {
|
|
this.log("Closing the server in clean state");
|
|
this.log("Automatic reconnecting..");
|
|
new RPCServer(
|
|
this.url, this.key, this.getImports, this.logger,
|
|
this.tensorCacheUrl, this.tensorCacheDevice,
|
|
this.initProgressCallback, this.asyncOnServerLoad);
|
|
} else {
|
|
this.log("Closing the server, final state=" + this.state);
|
|
}
|
|
}
|
|
|
|
private onOpen(_event: Event): void {
|
|
// Send the headers
|
|
let bkey = StringToUint8Array("server:" + this.key);
|
|
bkey = bkey.slice(0, bkey.length - 1);
|
|
const intbuf = new Int32Array(1);
|
|
intbuf[0] = RPC_MAGIC;
|
|
this.socket.send(intbuf);
|
|
intbuf[0] = bkey.length;
|
|
this.socket.send(intbuf);
|
|
this.socket.send(bkey);
|
|
this.log("connected...");
|
|
// request bytes: magic + keylen
|
|
this.requestBytes(SizeOf.I32 + SizeOf.I32);
|
|
this.state = RPCServerState.InitHeader;
|
|
}
|
|
|
|
/** Handler for raw message. */
|
|
private onMessage(event: MessageEvent): void {
|
|
const buffer = event.data;
|
|
this.buffredBytes += buffer.byteLength;
|
|
this.messageQueue.push(new Uint8Array(buffer));
|
|
this.processEvents();
|
|
}
|
|
/** Process ready events. */
|
|
private processEvents(): void {
|
|
while (this.buffredBytes >= this.pendingBytes && this.pendingBytes != 0) {
|
|
this.onDataReady();
|
|
}
|
|
}
|
|
/** State machine to handle each request */
|
|
private onDataReady(): void {
|
|
switch (this.state) {
|
|
case RPCServerState.InitHeader: {
|
|
this.handleInitHeader();
|
|
break;
|
|
}
|
|
case RPCServerState.InitHeaderKey: {
|
|
this.handleInitHeaderKey();
|
|
break;
|
|
}
|
|
case RPCServerState.ReceivePacketHeader: {
|
|
this.currPacketHeader = this.readFromBuffer(SizeOf.I64);
|
|
const reader = new ByteStreamReader(this.currPacketHeader);
|
|
this.currPacketLength = reader.readU64();
|
|
assert(this.pendingBytes === 0);
|
|
this.requestBytes(this.currPacketLength);
|
|
this.state = RPCServerState.ReceivePacketBody;
|
|
break;
|
|
}
|
|
case RPCServerState.ReceivePacketBody: {
|
|
const body = this.readFromBuffer(this.currPacketLength);
|
|
assert(this.pendingBytes === 0);
|
|
assert(this.currPacketHeader !== undefined);
|
|
this.onPacketReady(this.currPacketHeader, body);
|
|
break;
|
|
}
|
|
case RPCServerState.WaitForCallback: {
|
|
assert(this.pendingBytes === 0);
|
|
break;
|
|
}
|
|
default: {
|
|
throw new Error("Cannot handle state " + this.state);
|
|
}
|
|
}
|
|
}
|
|
|
|
private onPacketReady(header: Uint8Array, body: Uint8Array): void {
|
|
if (this.inst === undefined) {
|
|
// initialize server.
|
|
const reader = new ByteStreamReader(body);
|
|
const code = reader.readU32();
|
|
const ver = Uint8ArrayToString(reader.readByteArray());
|
|
const nargs = reader.readU32();
|
|
|
|
// nargs=0 means no session_constructor_args (LocalSession request).
|
|
// WASM RPC requires ["rpc.WasmSession", wasm_binary]. Wait for proper init.
|
|
if (nargs === 0) {
|
|
this.log("Received LocalSession init (nargs=0), waiting for WasmSession init...");
|
|
this.requestBytes(SizeOf.I64);
|
|
this.state = RPCServerState.ReceivePacketHeader;
|
|
return;
|
|
}
|
|
|
|
const args = [];
|
|
for (let i = 0; i < nargs; ++i) {
|
|
const typeIndex = reader.readU32();
|
|
if (typeIndex === TypeIndex.kTVMFFIRawStr) {
|
|
const str = Uint8ArrayToString(reader.readByteArray());
|
|
args.push(str);
|
|
} else if (typeIndex === TypeIndex.kTVMFFIStr) {
|
|
reader.readU32(); // skip duplicate type_index
|
|
const str = Uint8ArrayToString(reader.readByteArray());
|
|
args.push(str);
|
|
} else if (typeIndex === TypeIndex.kTVMFFIByteArrayPtr) {
|
|
args.push(reader.readByteArray());
|
|
} else if (typeIndex === TypeIndex.kTVMFFIBytes) {
|
|
reader.readU32(); // skip duplicate type_index
|
|
args.push(reader.readByteArray());
|
|
} else {
|
|
throw new Error("cannot support type index " + typeIndex);
|
|
}
|
|
}
|
|
this.onInitServer(args, header, body);
|
|
} else {
|
|
assert(this.serverRecvData !== undefined);
|
|
this.serverRecvData(header, body);
|
|
this.requestBytes(SizeOf.I64);
|
|
this.state = RPCServerState.ReceivePacketHeader;
|
|
}
|
|
}
|
|
|
|
/** Event handler during server initialization. */
|
|
private onInitServer(
|
|
args: Array<unknown>,
|
|
header: Uint8Array,
|
|
body: Uint8Array
|
|
): void {
|
|
// start the server
|
|
assert(args[0] === "rpc.WasmSession");
|
|
assert(this.pendingBytes === 0);
|
|
|
|
const asyncInitServer = async (): Promise<void> => {
|
|
assert(args[1] instanceof Uint8Array);
|
|
const inst = await runtime.instantiate(
|
|
args[1].buffer as ArrayBuffer,
|
|
this.getImports(),
|
|
this.logger
|
|
);
|
|
|
|
try {
|
|
const output: GPUDeviceDetectOutput | undefined = await detectGPUDevice();
|
|
if (output !== undefined) {
|
|
const label = "WebGPU: "+ output.adapterInfo.description;
|
|
this.log("Initialize GPU device: " + label);
|
|
inst.initWebGPU(output.device);
|
|
} else {
|
|
this.log("Cannot find WebGPU device in the env");
|
|
}
|
|
} catch (err) {
|
|
this.log("Cannnot initialize WebGPU, " + err.toString());
|
|
}
|
|
|
|
this.inst = inst;
|
|
// begin scope to allow handling of objects
|
|
this.inst.beginScope();
|
|
if (this.initProgressCallback !== undefined) {
|
|
this.inst.registerInitProgressCallback(this.initProgressCallback);
|
|
}
|
|
|
|
if (this.tensorCacheUrl.length != 0) {
|
|
if (this.tensorCacheDevice === "cpu") {
|
|
await this.inst.fetchTensorCache(this.tensorCacheUrl, this.inst.cpu());
|
|
} else {
|
|
assert(this.tensorCacheDevice === "webgpu");
|
|
await this.inst.fetchTensorCache(this.tensorCacheUrl, this.inst.webgpu());
|
|
}
|
|
}
|
|
|
|
assert(this.inst !== undefined);
|
|
if (this.asyncOnServerLoad !== undefined) {
|
|
await this.asyncOnServerLoad(this.inst);
|
|
}
|
|
const fcreate = this.inst.getGlobalFunc("rpc.CreateEventDrivenServer");
|
|
const messageHandler = fcreate(
|
|
(cbytes: Uint8Array): runtime.Scalar => {
|
|
assert(this.inst !== undefined);
|
|
if (this.socket.readyState === 1) {
|
|
// WebSocket will automatically close the socket
|
|
// if we burst send data that exceeds its internal buffer
|
|
// wait a bit before we send next one.
|
|
const sendDataWithCongestionControl = async (): Promise<void> => {
|
|
const packetSize = 4 << 10;
|
|
const maxBufferAmount = 4 * packetSize;
|
|
const waitTimeMs = 20;
|
|
for (
|
|
let offset = 0;
|
|
offset < cbytes.length;
|
|
offset += packetSize
|
|
) {
|
|
const end = Math.min(offset + packetSize, cbytes.length);
|
|
while (this.socket.bufferedAmount >= maxBufferAmount) {
|
|
await new Promise((r) => setTimeout(r, waitTimeMs));
|
|
}
|
|
this.socket.send(cbytes.slice(offset, end));
|
|
}
|
|
};
|
|
// Chain up the pending send so that the async send is always in-order.
|
|
this.pendingSend = this.pendingSend.then(
|
|
sendDataWithCongestionControl
|
|
);
|
|
// Directly return since the data are "sent" from the caller's pov.
|
|
return this.inst.scalar(cbytes.length, "int32");
|
|
} else {
|
|
return this.inst.scalar(0, "int32");
|
|
}
|
|
},
|
|
this.name,
|
|
this.key
|
|
);
|
|
// message handler should persist across RPC runs
|
|
this.globalObjects.push(
|
|
this.inst.detachFromCurrentScope(messageHandler)
|
|
);
|
|
const writeFlag = this.inst.scalar(3, "int32");
|
|
|
|
this.serverRecvData = (header: Uint8Array, body: Uint8Array): void => {
|
|
if (messageHandler(header, writeFlag) === 0) {
|
|
this.socket.close();
|
|
}
|
|
if (messageHandler(body, writeFlag) === 0) {
|
|
this.socket.close();
|
|
}
|
|
};
|
|
|
|
// Forward the same init sequence to the wasm RPC.
|
|
// The RPC will look for "rpc.wasmSession"
|
|
// and we will redirect it to the correct local session.
|
|
// register the callback to redirect the session to local.
|
|
const flocal = this.inst.getGlobalFunc("wasm.LocalSession");
|
|
const localSession = flocal();
|
|
assert(localSession instanceof runtime.Module);
|
|
|
|
this.inst.registerFunc(
|
|
"rpc.WasmSession",
|
|
(_args: unknown): runtime.Module => {
|
|
return localSession;
|
|
}
|
|
);
|
|
messageHandler(header, writeFlag);
|
|
messageHandler(body, writeFlag);
|
|
|
|
this.log("Finish initializing the Wasm Server..");
|
|
this.requestBytes(SizeOf.I64);
|
|
this.state = RPCServerState.ReceivePacketHeader;
|
|
// call process events in case there are bufferred data.
|
|
this.processEvents();
|
|
// recycle all values.
|
|
this.inst.endScope();
|
|
};
|
|
|
|
this.state = RPCServerState.WaitForCallback;
|
|
asyncInitServer();
|
|
}
|
|
|
|
private log(msg: string): void {
|
|
this.logger(this.name + msg);
|
|
}
|
|
|
|
private handleInitHeader(): void {
|
|
const reader = new ByteStreamReader(this.readFromBuffer(SizeOf.I32 * 2));
|
|
const magic = reader.readU32();
|
|
if (magic === RPC_MAGIC + 1) {
|
|
throw new Error("key: " + this.key + " has already been used in proxy");
|
|
} else if (magic === RPC_MAGIC + 2) {
|
|
throw new Error("RPCProxy do not have matching client key " + this.key);
|
|
}
|
|
assert(magic === RPC_MAGIC, this.url + " is not an RPC Proxy");
|
|
this.remoteKeyLength = reader.readU32();
|
|
assert(this.pendingBytes === 0);
|
|
this.requestBytes(this.remoteKeyLength);
|
|
this.state = RPCServerState.InitHeaderKey;
|
|
}
|
|
|
|
private handleInitHeaderKey(): void {
|
|
const remoteKey = Uint8ArrayToString(
|
|
this.readFromBuffer(this.remoteKeyLength)
|
|
);
|
|
assert(this.pendingBytes === 0);
|
|
this.requestBytes(SizeOf.I64);
|
|
this.state = RPCServerState.ReceivePacketHeader;
|
|
}
|
|
|
|
private checkLittleEndian(): void {
|
|
const a = new ArrayBuffer(4);
|
|
const b = new Uint8Array(a);
|
|
const c = new Uint32Array(a);
|
|
b[0] = 0x11;
|
|
b[1] = 0x22;
|
|
b[2] = 0x33;
|
|
b[3] = 0x44;
|
|
assert(c[0] === 0x44332211, "RPCServer little endian to work");
|
|
}
|
|
|
|
private requestBytes(nbytes: number): void {
|
|
this.pendingBytes += nbytes;
|
|
}
|
|
|
|
private readFromBuffer(nbytes: number): Uint8Array {
|
|
const ret = new Uint8Array(nbytes);
|
|
let ptr = 0;
|
|
while (ptr < nbytes) {
|
|
assert(this.messageQueue.length != 0);
|
|
const nleft = nbytes - ptr;
|
|
if (this.messageQueue[0].byteLength <= nleft) {
|
|
const buffer = this.messageQueue.shift() as Uint8Array;
|
|
ret.set(buffer, ptr);
|
|
ptr += buffer.byteLength;
|
|
} else {
|
|
const buffer = this.messageQueue[0];
|
|
ret.set(buffer.slice(0, nleft), ptr);
|
|
this.messageQueue[0] = buffer.slice(nleft, buffer.byteLength);
|
|
ptr += nleft;
|
|
}
|
|
}
|
|
this.buffredBytes -= nbytes;
|
|
this.pendingBytes -= nbytes;
|
|
return ret;
|
|
}
|
|
}
|