/* * 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; private tensorCacheUrl: string; private tensorCacheDevice: string; private initProgressCallback?: runtime.InitProgressCallback; private asyncOnServerLoad?: (inst: runtime.Instance) => Promise; private pendingSend: Promise = Promise.resolve(); private name: string; private inst?: runtime.Instance = undefined; private globalObjects: Array = []; 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 = []; constructor( url: string, key: string, getImports: () => Record, logger: (msg: string) => void = console.log, tensorCacheUrl = "", tensorCacheDevice = "cpu", initProgressCallback: runtime.InitProgressCallback | undefined = undefined, asyncOnServerLoad: ((inst: runtime.Instance) => Promise) | 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, header: Uint8Array, body: Uint8Array ): void { // start the server assert(args[0] === "rpc.WasmSession"); assert(this.pendingBytes === 0); const asyncInitServer = async (): Promise => { 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 => { 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; } }