Files
apache--tvm/python/tvm/micro/project_api/server.py
T
Gustavo Romero 53cb5eadfb [TVMC][microTVM] Add new micro context (#9229)
* [microTVM] zephyr: Make platform options comply with RFC-0020

Make Zephyr platform options comply with RFC-0020 specification.

Project options now need to specify the required metadata for every
option, i.e. 'required', 'optional', and 'type'.

Signed-off-by: Gustavo Romero <gustavo.romero@linaro.org>

* [microTVM] arduino: Make platform options comply with RFC-0020

Make Arduino platform options comply with RFC-0020 specification.

Project options now need to specify the required metadata for every
option, i.e. 'required', 'optional', and 'type'.

Signed-off-by: Gustavo Romero <gustavo.romero@linaro.org>

* [microTVM] crt: Make crt options comply with RFC-0020

Make crt project options comply with RFC-0020 specification.

Project options now need to specify the required metadata for every
option, i.e. 'required', 'optional', and 'type'.

Signed-off-by: Gustavo Romero <gustavo.romero@linaro.org>

* [microTVM][Unittest] Adapt test to RFC-0020

Adapt test to new metadata fields accordingly to RFC-0020 specification.

Signed-off-by: Gustavo Romero <gustavo.romero@linaro.org>

* [microTVM] Add info() method to GeneratedProject class

Add info() method to GeneratedProject class so one can use the Project
API to query options for project dirs instead of only for template
projects.

This commit also adds for the sake of convenience a setter and a getter
for 'options' in case it's necessary to set or get 'options' after a
GeneratedProject class is instantiated without initializing 'options'.

Signed-off-by: Gustavo Romero <gustavo.romero@linaro.org>

* [microTVM] Fix typo in python/tvm/micro/session.py

Fix typo in comment.

Signed-off-by: Gustavo Romero <gustavo.romero@linaro.org>

* Allow multiple runs on micro targets

Currently there is a limitation on microTVM / TVM which doesn't allow
running a model multiple times in sequence without previously flashing
the model to the device.

Root cause is that RPCModuleNode class destructor is called once a run
finishes. The destructor sends a RPCCode::kFreeHandle packet with
type_code = kTVMModuleHandle to the device which wipes entries in
crt/src/runtime/crt/common/crt_runtime_api.c:147:static const TVMModule*
registered_modules[TVM_CRT_MAX_REGISTERED_MODULES] when TVMFreeMod() is
called when the target receives a kFreeHandle packet.

Hence when one tries to re-run a model registered_modules[0] == NULL
causes a backtrace on the host side. Probably never before a model on
microTVM was run without being flashed just before the run, so tvmc run
implementation for micro targets exposed the issue.

This commit fixes it by not calling TVMFreeMod() for system_lib_handle
on the target side when a session terminates so the pointer to the
system_lib_handle is not flushed from 'registered_modules', allowing
multiple runs on micro targets.

Signed-off-by: Gustavo Romero <gustavo.romero@linaro.org>

* [TVMC] Pass main parser when calling add_*_parser functions

Currently when a add_*_parser functions are called in main.py to build
and add the various subparsers to the main parser only a subparser is
passed to the functions. However if one of these functions need to build
a dynamic parser it needs also to call the main parser at least once to
parse once the command line and get the arguments necessary to finally
build the complete parser.

This commit fixes that limitation by passing also the main parser when
calling the subparser builders so it can be used to build the dynamic
subparses.

Signed-off-by: Gustavo Romero <gustavo.romero@linaro.org>

* [TVMC] micro: Add new micro context

This commit introduces support for micro targets (targets supported by
microTVM). It creates a new micro context under the new TVMC command
'tvmc micro'. Moreover, three new subcommands are made available in the
new context under 'tvmc micro': 'create-project', 'build', and 'flash'.

The new support relies on the Project API to query all the options
available for a selected platform (like Zephyr and Arduino) and also
from any adhoc platform template directory which provides a custom
Project API server.

Signed-off-by: Gustavo Romero <gustavo.romero@linaro.org>

* [TVMC] run: Add support for micro devices

Add support for micro devices using the Project API to query all options
available for a given platform and open a session with an specified
micro device. Use of 'tvmc run' with micro device is enabled via the
'--device micro' option in addition to the project directory.

Once the project directory is specified 'tvmc run' will make all options
specific to the platform found in the project dir available as options
in 'tvmc run'. They can be listed by '--list-options' and passed via
'--options'.

Signed-off-by: Gustavo Romero <gustavo.romero@linaro.org>
2021-11-19 10:14:04 -08:00

800 lines
27 KiB
Python

# 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.
"""Defines a basic Project API server template.
This file is meant to be imported or copied into Project API servers, so it should not have any
imports or dependencies outside of things strictly required to run the API server.
"""
import abc
import argparse
import base64
import collections
import enum
import io
import json
import logging
import os
import pathlib
import re
import select
import sys
import time
import traceback
import typing
_LOG = logging.getLogger(__name__)
_ProjectOption = collections.namedtuple(
"ProjectOption", ("name", "choices", "default", "type", "required", "optional", "help")
)
class ProjectOption(_ProjectOption):
"""Class used to keep the metadata associated to project options."""
def __new__(cls, name, **kw):
"""Override __new__ to force all options except name to be specified as kwargs."""
assert "name" not in kw
assert (
"required" in kw or "optional" in kw
), "at least one of 'required' or 'optional' must be specified."
assert "type" in kw, "'type' field must be specified."
kw["name"] = name
for param in ["choices", "default", "required", "optional"]:
kw.setdefault(param, None)
return super().__new__(cls, **kw)
ServerInfo = collections.namedtuple(
"ServerInfo", ("platform_name", "is_template", "model_library_format_path", "project_options")
)
# Timeouts supported by the underlying C++ MicroSession.
#
# session_start_retry_timeout_sec : float
# Number of seconds to wait for the device to send a kSessionStartReply after sending the
# initial session start message. After this time elapses another
# kSessionTerminated-kSessionStartInit train is sent. 0 disables this.
# session_start_timeout_sec : float
# Total number of seconds to wait for the session to be established. After this time, the
# client gives up trying to establish a session and raises an exception.
# session_established_timeout_sec : float
# Number of seconds to wait for a reply message after a session has been established. 0
# disables this.
TransportTimeouts = collections.namedtuple(
"TransportTimeouts",
[
"session_start_retry_timeout_sec",
"session_start_timeout_sec",
"session_established_timeout_sec",
],
)
class ErrorCode(enum.IntEnum):
"""Enumerates error codes which can be returned. Includes JSON-RPC standard and custom codes."""
# Custom (in reserved error code space).
SERVER_ERROR = -32000 # A generic error was raised while processing the request.
# JSON-RPC standard
PARSE_ERROR = -32700
INVALID_REQUEST = -32600
METHOD_NOT_FOUND = -32601
INVALID_PARAMS = -32602
INTERNAL_ERROR = -32603
class JSONRPCError(Exception):
"""An error class with properties that meet the JSON-RPC error spec."""
def __init__(self, code, message, data, client_context=None):
Exception.__init__(self)
self.code = code
self.message = message
self.data = data
self.client_context = client_context
def to_json(self):
return {
"code": self.code,
"message": self.message,
"data": self.data,
}
def __str__(self):
data_str = ""
if self.data:
if isinstance(self.data, dict) and self.data.get("traceback"):
data_str = f'\n{self.data["traceback"]}'
else:
data_str = f"\n{self.data!r}"
return f"JSON-RPC error # {self.code}: {self.message}" + data_str
@classmethod
def from_json(cls, client_context, json_error):
"""Convert an encapsulated ServerError into JSON-RPC compliant format."""
found_server_error = False
try:
if ErrorCode(json_error["code"]) == ErrorCode.SERVER_ERROR:
found_server_error = True
except ValueError:
ServerError.from_json(client_context, json_error)
if found_server_error:
return ServerError.from_json(client_context, json_error)
return cls(
json_error["code"],
json_error["message"],
json_error.get("data", None),
client_context=client_context,
)
class ServerError(JSONRPCError):
"""Superclass for JSON-RPC errors which occur while processing valid requests."""
@classmethod
def from_exception(cls, exc, **kw):
to_return = cls(**kw)
to_return.set_traceback(traceback.TracebackException.from_exception(exc).format())
return to_return
def __init__(self, message=None, data=None, client_context=None):
if self.__class__ == ServerError:
assert message is not None, "Plain ServerError must have message="
else:
assert (
message is None
), f"ServerError subclasses must not supply message=; got {message!r}"
message = self.__class__.__name__
super(ServerError, self).__init__(ErrorCode.SERVER_ERROR, message, data)
self.client_context = client_context
def __str__(self):
context_str = f"{self.client_context}: " if self.client_context is not None else ""
super_str = super(ServerError, self).__str__()
return context_str + super_str
def set_traceback(self, traceback): # pylint: disable=redefined-outer-name
"""Format a traceback to be embedded in the JSON-RPC format."""
if self.data is None:
self.data = {}
if "traceback" not in self.data:
# NOTE: TVM's FFI layer reorders Python stack traces several times and strips
# intermediary lines that start with "Traceback". This logic adds a comment to the first
# stack frame to explicitly identify the first stack frame line that occurs on the
# server.
traceback_list = list(traceback)
# The traceback list contains one entry per stack frame, and each entry contains 1-2
# lines:
# File "path/to/file", line 123, in <method>:
# <copy of the line>
# We want to place a comment on the first line of the outermost frame to indicate this
# is the server-side stack frame.
first_frame_list = traceback_list[1].split("\n")
self.data["traceback"] = (
traceback_list[0]
+ f"{first_frame_list[0]} # <--- Outermost server-side stack frame\n"
+ "\n".join(first_frame_list[1:])
+ "".join(traceback_list[2:])
)
@classmethod
def from_json(cls, client_context, json_error):
assert json_error["code"] == ErrorCode.SERVER_ERROR
for sub_cls in cls.__subclasses__():
if sub_cls.__name__ == json_error["message"]:
return sub_cls(
data=json_error.get("data"),
client_context=client_context,
)
return cls(
json_error["message"], data=json_error.get("data"), client_context=client_context
)
class TransportClosedError(ServerError):
"""Raised when a transport can no longer be used due to underlying I/O problems."""
class IoTimeoutError(ServerError):
"""Raised when the I/O operation could not be completed before the timeout.
Specifically:
- when no data could be read before the timeout
- when some of the write data could be written before the timeout
Note the asymmetric behavior of read() vs write(), since in one case the total length of the
data to transfer is known.
"""
class UnsupportedTVMVersionError(ServerError):
"""Raised when the version of TVM supplied to server_info_query is unsupported."""
class ProjectAPIHandler(metaclass=abc.ABCMeta):
"""The interface class for all Project API implementations.
Extend this class in your microtvm_api_server.py and implement each function defined here.
"""
@abc.abstractmethod
def server_info_query(self, tvm_version: str) -> ServerInfo:
"""Initial request issued by TVM to retrieve metadata about this API server and project.
Should this API server not
Parameters
----------
tvm_version : str
The value of tvm.__version__.
Returns
-------
ServerInfo :
A ServerInfo namedtuple containing the metadata needed by TVM.
Raises
------
UnsupportedTVMVersionError :
When tvm_version indicates a known-unsupported version of TVM.
"""
raise NotImplementedError()
@abc.abstractmethod
def generate_project(
self,
model_library_format_path: pathlib.Path,
standalone_crt_dir: pathlib.Path,
project_dir: pathlib.Path,
options: dict,
):
"""Generate a project from the given artifacts, copying ourselves to that project.
Parameters
----------
model_library_format_path : pathlib.Path
Path to the Model Library Format tar archive.
standalone_crt_dir : pathlib.Path
Path to the root directory of the "standalone_crt" TVM build artifact. This contains the
TVM C runtime.
project_dir : pathlib.Path
Path to a nonexistent directory which should be created and filled with the generated
project.
options : dict
Dict mapping option name to ProjectOption.
"""
raise NotImplementedError()
@abc.abstractmethod
def build(self, options: dict):
"""Build the project, enabling the flash() call to made.
Parameters
----------
options : Dict[str, ProjectOption]
ProjectOption which may influence the build, keyed by option name.
"""
raise NotImplementedError()
@abc.abstractmethod
def flash(self, options: dict):
"""Program the project onto the device.
Parameters
----------
options : Dict[str, ProjectOption]
ProjectOption which may influence the programming process, keyed by option name.
"""
raise NotImplementedError()
@abc.abstractmethod
def open_transport(self, options: dict) -> TransportTimeouts:
"""Open resources needed for the transport layer.
This function might e.g. open files or serial ports needed in write_transport or
read_transport.
Calling this function enables the write_transport and read_transport calls. If the
transport is not open, this method is a no-op.
Parameters
----------
options : Dict[str, ProjectOption]
ProjectOption which may influence the programming process, keyed by option name.
"""
raise NotImplementedError()
@abc.abstractmethod
def close_transport(self):
"""Close resources needed to operate the transport layer.
This function might e.g. close files or serial ports needed in write_transport or
read_transport.
Calling this function disables the write_transport and read_transport calls. If the
transport is not open, this method is a no-op.
"""
raise NotImplementedError()
@abc.abstractmethod
# pylint: disable=unidiomatic-typecheck
def read_transport(self, n: int, timeout_sec: typing.Union[float, type(None)]) -> bytes:
"""Read data from the transport.
Parameters
----------
n : int
The exact number of bytes to read from the transport.
timeout_sec : Union[float, None]
Number of seconds to wait for at least one byte to be written before timing out. If
timeout_sec is 0, write should attempt to service the request in a non-blocking fashion.
If timeout_sec is None, write should block until all `n` bytes of data can be returned.
Returns
-------
bytes :
Data read from the channel. Should be exactly `n` bytes long.
Raises
------
TransportClosedError :
When the transport layer determines that the transport can no longer send or receive
data due to an underlying I/O problem (i.e. file descriptor closed, cable removed, etc).
IoTimeoutError :
When `timeout_sec` elapses without receiving any data.
"""
raise NotImplementedError()
@abc.abstractmethod
def write_transport(self, data: bytes, timeout_sec: float):
"""Write data to the transport.
This function should either write all bytes in `data` or raise an exception.
Parameters
----------
data : bytes
The data to write over the channel.
timeout_sec : Union[float, None]
Number of seconds to wait for all bytes to be written before timing out. If timeout_sec
is 0, write should attempt to service the request in a non-blocking fashion. If
timeout_sec is None, write should block until it has written all data.
Raises
------
TransportClosedError :
When the transport layer determines that the transport can no longer send or receive
data due to an underlying I/O problem (i.e. file descriptor closed, cable removed, etc).
IoTimeoutError :
When `timeout_sec` elapses without receiving any data.
"""
raise NotImplementedError()
class ProjectAPIServer:
"""Base class for Project API Servers.
This API server implements communication using JSON-RPC 2.0:
https://www.jsonrpc.org/specification
Suggested use of this class is to import this module or copy this file into Project Generator
implementations, then instantiate it with server.start().
This RPC server is single-threaded, blocking, and one-request-at-a-time. Don't get anxious.
"""
_PROTOCOL_VERSION = 1
def __init__(
self, read_file: typing.BinaryIO, write_file: typing.BinaryIO, handler: ProjectAPIHandler
):
"""Initialize a new ProjectAPIServer.
Parameters
----------
read_file : BinaryIO
A file-like object used to read binary data from the client.
write_file : BinaryIO
A file-like object used to write binary data to the client.
handler : ProjectAPIHandler
A class which extends the abstract class ProjectAPIHandler and implements the server RPC
functions.
"""
self._read_file = io.TextIOWrapper(read_file, encoding="UTF-8", errors="strict")
self._write_file = io.TextIOWrapper(
write_file, encoding="UTF-8", errors="strict", write_through=True
)
self._handler = handler
def serve_forever(self):
"""Serve requests until no more are available."""
has_more = True
while has_more:
has_more = self.serve_one_request()
def serve_one_request(self):
"""Read, process, and reply to a single request from read_file.
When errors occur reading the request line or loading the request into JSON, they are
propagated to the caller (the stream is then likely corrupted and no further requests
should be served. When errors occur past this point, they are caught and send back to the
client.
Return
----------
bool :
True when more data could be read from read_file, False otherwise.
"""
try:
line = self._read_file.readline()
_LOG.debug("read request <- %s", line)
if not line:
return False
request = json.loads(line)
except EOFError:
_LOG.error("EOF")
return False
except Exception as exc: # pylint: disable=broad-except
_LOG.error("Caught error reading request", exc_info=1)
return False
did_validate = False
try:
self._validate_request(request)
did_validate = True
self._dispatch_request(request)
except JSONRPCError as exc:
if isinstance(exc, ServerError):
exc.set_traceback(traceback.TracebackException.from_exception(exc).format())
request_id = None if not did_validate else request.get("id")
self._reply_error(request_id, exc)
return did_validate
except Exception as exc: # pylint: disable=broad-except
message = "validating request"
if did_validate:
message = f"calling method {request['method']}"
exc = ServerError.from_exception(exc, message=message)
request_id = None if not isinstance(request, dict) else request.get("id")
self._reply_error(request_id, exc)
return did_validate
return True
VALID_METHOD_RE = re.compile("^[a-zA-Z0-9_]+$")
def _validate_request(self, request):
if not isinstance(request, dict):
raise JSONRPCError(
ErrorCode.INVALID_REQUEST, f"request: want dict; got {request!r}", None
)
jsonrpc = request.get("jsonrpc")
if jsonrpc != "2.0":
raise JSONRPCError(
ErrorCode.INVALID_REQUEST, f'request["jsonrpc"]: want "2.0"; got {jsonrpc!r}', None
)
method = request.get("method")
if not isinstance(method, str):
raise JSONRPCError(
ErrorCode.INVALID_REQUEST, f'request["method"]: want str; got {method!r}', None
)
if not self.VALID_METHOD_RE.match(method):
raise JSONRPCError(
ErrorCode.INVALID_REQUEST,
f'request["method"]: should match regex {self.VALID_METHOD_RE.pattern}; '
f"got {method!r}",
None,
)
params = request.get("params")
if not isinstance(params, dict):
raise JSONRPCError(
ErrorCode.INVALID_REQUEST, f'request["params"]: want dict; got {type(params)}', None
)
request_id = request.get("id")
# pylint: disable=unidiomatic-typecheck
if not isinstance(request_id, (str, int, type(None))):
raise JSONRPCError(
ErrorCode.INVALID_REQUEST,
f'request["id"]: want str, number, null; got {request_id!r}',
None,
)
def _dispatch_request(self, request):
method = request["method"]
interface_method = getattr(ProjectAPIHandler, method, None)
if interface_method is None:
raise JSONRPCError(
ErrorCode.METHOD_NOT_FOUND, f'{request["method"]}: no such method', None
)
has_preprocessing = True
dispatch_method = getattr(self, f"_dispatch_{method}", None)
if dispatch_method is None:
dispatch_method = getattr(self._handler, method)
has_preprocessing = False
request_params = request["params"]
params = {}
for var_name, var_type in typing.get_type_hints(interface_method).items():
if var_name in ("self", "return"):
continue
# NOTE: types can only be JSON-compatible types, so var_type is expected to be of type
# 'type'.
if var_name not in request_params:
raise JSONRPCError(
ErrorCode.INVALID_PARAMS,
f'method {request["method"]}: parameter {var_name} not given',
None,
)
param = request_params[var_name]
if not has_preprocessing and not isinstance(param, var_type):
raise JSONRPCError(
ErrorCode.INVALID_PARAMS,
f'method {request["method"]}: parameter {var_name}: want {var_type!r}, '
f"got {type(param)!r}",
None,
)
params[var_name] = param
extra_params = [p for p in request["params"] if p not in params]
if extra_params:
raise JSONRPCError(
ErrorCode.INVALID_PARAMS,
f'{request["method"]}: extra parameters: {", ".join(extra_params)}',
None,
)
return_value = dispatch_method(**params)
self._write_reply(request["id"], result=return_value)
def _write_reply(self, request_id, result=None, error=None):
reply_dict = {
"jsonrpc": "2.0",
"id": request_id,
}
if error is not None:
assert (
result is None
), f"Want either result= or error=, got result={result!r} and error={error!r})"
reply_dict["error"] = error
else:
reply_dict["result"] = result
reply_str = json.dumps(reply_dict)
_LOG.debug("write reply -> %r", reply_dict)
self._write_file.write(reply_str)
self._write_file.write("\n")
def _reply_error(self, request_id, exception):
self._write_reply(request_id, error=exception.to_json())
def _dispatch_generate_project(
self, model_library_format_path, standalone_crt_dir, project_dir, options
):
return self._handler.generate_project(
pathlib.Path(model_library_format_path),
pathlib.Path(standalone_crt_dir),
pathlib.Path(project_dir),
options,
)
def _dispatch_server_info_query(self, tvm_version):
query_reply = self._handler.server_info_query(tvm_version)
to_return = query_reply._asdict()
if to_return["model_library_format_path"] is not None:
to_return["model_library_format_path"] = str(to_return["model_library_format_path"])
to_return.setdefault("protocol_version", self._PROTOCOL_VERSION)
to_return["project_options"] = [o._asdict() for o in query_reply.project_options]
return to_return
def _dispatch_open_transport(self, options):
reply = self._handler.open_transport(options)
return {"timeouts": reply._asdict()}
def _dispatch_read_transport(self, n, timeout_sec):
reply_data = self._handler.read_transport(n, timeout_sec)
return {"data": str(base64.b85encode(reply_data), "utf-8")}
def _dispatch_write_transport(self, data, timeout_sec):
self._handler.write_transport(base64.b85decode(data), timeout_sec)
def _await_nonblocking_ready(rlist, wlist, timeout_sec=None, end_time=None):
if end_time is None:
return True
if timeout_sec is None:
timeout_sec = max(0, end_time - time.monotonic())
rlist, wlist, xlist = select.select(rlist, wlist, rlist + wlist, timeout_sec)
if not rlist and not wlist and not xlist:
raise IoTimeoutError()
return True
def read_with_timeout(fd, n, timeout_sec): # pylint: disable=invalid-name
"""Read data from a file descriptor, with timeout.
This function is intended as a helper function for implementations of ProjectAPIHandler
read_transport. Tested on Linux and OS X. Not tested on Windows.
Parameters
----------
fd : int
File descriptor to read from. Must be opened in non-blocking mode (e.g. with O_NONBLOCK)
if timeout_sec is not None.
n : int
Maximum number of bytes to read.
timeout_sec : float or None
If not None, maximum number of seconds to wait before raising IoTimeoutError.
Returns
-------
bytes :
If at least one byte was received before timeout_sec, returns a bytes object with length
in [1, n]. If timeout_sec is None, returns the equivalent of os.read(fd, n).
Raises
------
IoTimeoutException :
When timeout_sec is not None and that number of seconds elapses before any data is read.
"""
end_time = None if timeout_sec is None else time.monotonic() + timeout_sec
while True:
_await_nonblocking_ready([fd], [], end_time=end_time)
try:
to_return = os.read(fd, n)
break
except BlockingIOError:
pass
# When EOF is reached, close the file.
if not to_return:
os.close(fd)
raise TransportClosedError()
return to_return
def write_with_timeout(fd, data, timeout_sec): # pylint: disable=invalid-name
"""Write data to a file descriptor, with timeout.
This function is intended as a helper function for implementations of ProjectAPIHandler
write_transport. Tested on Linux and OS X. Not tested on Windows.
Parameters
----------
fd : int
File descriptor to read from. Must be opened in non-blocking mode (e.g. with O_NONBLOCK)
if timeout_sec is not None.
data : bytes
Data to write.
timeout_sec : float or None
If not None, maximum number of seconds to wait before raising IoTimeoutError.
Returns
-------
int :
The number of bytes written to the file descriptor, if any bytes were written. A value
in [1, len(data)]. If timeout_sec is None, returns the equivalent of os.write(fd, data).
Raises
------
IoTimeoutException :
When timeout_sec is not None and that number of seconds elapses before any data is read.
"""
end_time = None if timeout_sec is None else time.monotonic() + timeout_sec
num_written = 0
while data:
try:
_await_nonblocking_ready([], [fd], end_time=end_time)
except IoTimeoutError as exc:
if num_written:
return num_written
raise exc
num_written_this_cycle = os.write(fd, data)
if not num_written_this_cycle:
os.close(fd)
raise base.TransportClosedError()
data = data[num_written_this_cycle:]
num_written += num_written_this_cycle
return num_written
def main(handler: ProjectAPIHandler, argv: typing.List[str] = None):
"""Start a Project API server.
Parameters
----------
argv : list[str]
Command-line parameters to this program. If not given, sys.argv is used.
handler : ProjectAPIHandler
Handler class that implements the API server RPC calls.
"""
if argv is None:
argv = sys.argv[1:]
parser = argparse.ArgumentParser(description="Generic TVM Project API server entry point")
parser.add_argument(
"--read-fd",
type=int,
required=True,
help="Numeric file descriptor where RPC requests should be read.",
)
parser.add_argument(
"--write-fd",
type=int,
required=True,
help="Numeric file descriptor where RPC replies should be written.",
)
parser.add_argument(
"--debug", action="store_true", help="When given, configure logging at DEBUG level."
)
args = parser.parse_args()
logging.basicConfig(level="DEBUG" if args.debug else "INFO", stream=sys.stderr)
read_file = os.fdopen(args.read_fd, "rb", buffering=0)
write_file = os.fdopen(args.write_fd, "wb", buffering=0)
server = ProjectAPIServer(read_file, write_file, handler)
server.serve_forever()