Files
apache--tvm/python/tvm/rpc/tracker.py
T
wehub-resource-sync 26446540fa
Lint / lint (push) Waiting to run
CI / MacOS (push) Waiting to run
CI / Windows (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:36:25 +08:00

524 lines
17 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.
"""RPC Tracker, tracks and distributes the TVM RPC resources.
This folder implements the tracker server logic.
Note
----
Tracker is a TCP based rest api with the following protocol:
- Initial handshake to the peer
- RPC_TRACKER_MAGIC
- Normal message: [size(int32), json-data]
- Each message is initiated by the client, and the tracker replies with a json.
List of available APIs:
- PING: check if tracker is alive
- input: [TrackerCode.PING]
- return: TrackerCode.SUCCESS
- PUT: report resource to tracker
- input: [TrackerCode.PUT, [port, match-key]]
- return: TrackerCode.SUCCESS
- note: match-key is a randomly generated identify the resource during connection.
- REQUEST: request a new resource from tracker
- input: [TrackerCode.REQUEST, [key, user, priority]]
- return: [TrackerCode.SUCCESS, [url, port, match-key]]
"""
# pylint: disable=invalid-name
import asyncio
import errno
import heapq
import json
import logging
import socket
import struct
import sys
import threading
from tvm.support.popen_pool import PopenWorker
try:
from tornado import ioloop
from . import tornado_util
except ImportError as error_msg:
raise ImportError(
f"RPCTracker module requires tornado package {error_msg}. Try 'pip install tornado'."
)
from . import base
from .base import RPC_TRACKER_MAGIC, TrackerCode
logger = logging.getLogger("RPCTracker")
console_handler = logging.StreamHandler()
console_handler.setFormatter(
logging.Formatter(
fmt="%(asctime)s.%(msecs)03d %(levelname)s %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
)
)
logger.addHandler(console_handler)
logger.setLevel(logging.INFO)
logger.propagate = False
# Maximum size in bytes for a single tracker message. Tracker frames carry
# small JSON command tuples; 1 MiB is well above any legitimate payload and
# bounds memory growth when a peer sends an oversized or malformed size
# header on the wire.
MAX_TRACKER_MSG_BYTES = 1 << 20
class Scheduler:
"""Abstract interface of scheduler."""
def put(self, value):
"""Push a resource into the scheduler.
This function can trigger callbacks in the scheduler.
Parameters
----------
value : object
The resource to be put in the scheduler.
"""
raise NotImplementedError()
def request(self, user, priority, callback):
"""Request a resource.
Parameters
----------
user : str
The user who is requesting the resource.
priority : int
The job priority
callback : function: value->bool
Callback function to receive an resource when ready
returns True if the resource is consumed.
"""
raise NotImplementedError()
def remove(self, value):
"""Remove a resource in the scheduler
Parameters
----------
value: object
The resource to remove
"""
def summary(self):
"""Get summary information of the scheduler."""
raise NotImplementedError()
class PriorityScheduler(Scheduler):
"""Priority based scheduler, FIFO based on request order"""
def __init__(self, key):
self._key = key
self._request_cnt = 0
self._lock = threading.Lock()
self._values = []
self._requests = []
def _schedule(self):
while self._requests and self._values:
value = self._values.pop(0)
item = heapq.heappop(self._requests)
callback = item[-1]
if callback(value[1:]):
value[0].pending_matchkeys.remove(value[-1])
else:
self._values.append(value)
def put(self, value):
self._values.append(value)
self._schedule()
def request(self, user, priority, callback):
with self._lock:
heapq.heappush(self._requests, (-priority, self._request_cnt, callback))
self._request_cnt += 1
self._schedule()
def remove(self, value):
if value in self._values:
self._values.remove(value)
self._schedule()
def summary(self):
"""Get summary information of the scheduler."""
return {"free": len(self._values), "pending": len(self._requests)}
class TCPEventHandler(tornado_util.TCPHandler):
"""Base asynchronize message handler.
The tracker and client follows a simple message protocol.
The message is in form [nbytes(int32)] [json-str].
All the information is packed in json-str
"""
def __init__(self, tracker, sock, addr):
super().__init__(sock)
self._data = bytearray()
self._tracker = tracker
self._msg_size = 0
self._addr = addr
self._init_req_nbytes = 4
self._info = {}
# list of pending match keys that has not been used.
self.pending_matchkeys = set()
self._tracker._connections.add(self)
self.put_values = []
def name(self):
"""name of connection"""
return f"TCPSocket: {self._addr!s}"
def summary(self):
"""Summary of this connection"""
return self._info
def _init_conn(self, message):
"""Initialize the connection"""
if len(message) != 4:
logger.warning("Invalid connection from %s", self.name())
self.close()
magic = struct.unpack("<i", message)[0]
if magic != RPC_TRACKER_MAGIC:
logger.warning("Invalid magic from %s", self.name())
self.close()
self.write_message(struct.pack("<i", RPC_TRACKER_MAGIC), binary=True)
self._init_req_nbytes = 0
def on_message(self, message):
"""Callback when a message is received.
Parameters
----------
message : bytearray
The bytes received
"""
assert isinstance(message, bytes)
if self._init_req_nbytes:
self._init_conn(message)
return
self._data += message
while True:
if self._msg_size == 0:
if len(self._data) >= 4:
self._msg_size = struct.unpack("<i", self._data[:4])[0]
if self._msg_size <= 0 or self._msg_size > MAX_TRACKER_MSG_BYTES:
logger.warning(
"Invalid msg_size %d from %s; closing connection",
self._msg_size,
self.name(),
)
self.close()
return
del self._data[:4]
else:
return
if self._msg_size != 0 and len(self._data) >= self._msg_size:
msg = (bytes(self._data[: self._msg_size])).decode("utf-8")
del self._data[: self._msg_size]
self._msg_size = 0
try:
self.call_handler(json.loads(msg))
except Exception: # pylint: disable=broad-except
logger.warning("Error handling message from %s", self.name(), exc_info=True)
self.close()
return
else:
return
def ret_value(self, data):
"""return value to the output"""
data = json.dumps(data)
self.write_message(struct.pack("<i", len(data)), binary=True)
self.write_message(data.encode("utf-8"), binary=True)
def call_handler(self, args):
"""Event handler when json request arrives."""
code = args[0]
if code == TrackerCode.PUT:
key = args[1]
port, matchkey = args[2]
self.pending_matchkeys.add(matchkey)
# got custom address (from rpc server)
if len(args) >= 4 and args[3] is not None:
value = (self, args[3], port, matchkey)
else:
value = (self, self._addr[0], port, matchkey)
self._tracker.put(key, value)
self.put_values.append(value)
self.ret_value(TrackerCode.SUCCESS)
elif code == TrackerCode.REQUEST:
key = args[1]
user = args[2]
priority = args[3]
def _cb(value):
# if the connection is already closed
if not self._sock:
return False
try:
self.ret_value([TrackerCode.SUCCESS, value])
except OSError:
return False
return True
self._tracker.request(key, user, priority, _cb)
elif code == TrackerCode.PING:
self.ret_value(TrackerCode.SUCCESS)
elif code == TrackerCode.GET_PENDING_MATCHKEYS:
self.ret_value(list(self.pending_matchkeys))
elif code == TrackerCode.STOP:
# safe stop tracker
if self._tracker._stop_key == args[1]:
self.ret_value(TrackerCode.SUCCESS)
self._tracker.stop()
else:
self.ret_value(TrackerCode.FAIL)
elif code == TrackerCode.UPDATE_INFO:
info = args[1]
assert isinstance(info, dict)
if info["addr"][0] is None:
info["addr"][0] = self._addr[0]
self._info.update(info)
self.ret_value(TrackerCode.SUCCESS)
elif code == TrackerCode.SUMMARY:
status = self._tracker.summary()
self.ret_value([TrackerCode.SUCCESS, status])
else:
logger.warning("Unknown tracker code %d", code)
self.close()
def on_close(self):
self._tracker.close(self)
def on_error(self, err):
logger.warning("%s: Error in RPC Tracker: %s", self.name(), err)
self.close()
class TrackerServerHandler:
"""Tracker that tracks the resources."""
def __init__(self, sock, stop_key):
self._scheduler_map = {}
self._sock = sock
self._sock.setblocking(0)
self._ioloop = ioloop.IOLoop.current()
self._stop_key = stop_key
self._connections = set()
def _event_handler(_, events):
self._on_event(events)
self._ioloop.add_handler(self._sock.fileno(), _event_handler, self._ioloop.READ)
def _on_event(self, _):
while True:
try:
conn, addr = self._sock.accept()
TCPEventHandler(self, conn, addr)
except OSError as err:
if err.args[0] in (errno.EAGAIN, errno.EWOULDBLOCK):
break
def create_scheduler(self, key):
"""Create a new scheduler."""
return PriorityScheduler(key)
def put(self, key, value):
"""Report a new resource to the tracker."""
if key not in self._scheduler_map:
self._scheduler_map[key] = self.create_scheduler(key)
self._scheduler_map[key].put(value)
def request(self, key, user, priority, callback):
"""Request a new resource."""
if key not in self._scheduler_map:
self._scheduler_map[key] = self.create_scheduler(key)
self._scheduler_map[key].request(user, priority, callback)
def close(self, conn):
self._connections.remove(conn)
if "key" in conn._info:
for value in conn.put_values:
_, _, _, key = value
rpc_key, _ = base.split_random_key(key)
self._scheduler_map[rpc_key].remove(value)
def stop(self):
"""Safely stop tracker."""
for conn in list(self._connections):
conn.close()
self._sock.close()
self._ioloop.stop()
def summary(self):
"""Return a dict summarizing current status."""
qinfo = {}
for k, v in self._scheduler_map.items():
qinfo[k] = v.summary()
cinfo = []
# ignore client connections without key
for conn in self._connections:
res = conn.summary()
if res.get("key", "").startswith("server"):
cinfo.append(res)
return {"queue_info": qinfo, "server_info": cinfo}
def run(self):
"""Run the tracker server"""
self._ioloop.start()
def _tracker_server(listen_sock, stop_key):
asyncio.set_event_loop(asyncio.new_event_loop())
handler = TrackerServerHandler(listen_sock, stop_key)
handler.run()
class PopenTrackerServerState:
"""Internal PopenTrackerServer State"""
current = None
def __init__(self, host, port=9190, port_end=9199, silent=False, reuse_addr=True, timeout=None):
if silent:
logger.setLevel(logging.WARN)
sock = socket.socket(base.get_addr_family((host, port)), socket.SOCK_STREAM)
# Never set socket SO_REUSEADDR on Windows. The SO_REUSEADDR flag allow reusing the
# inactivate TIME_WATI state sockets on POSIX, but on Windows it will allow two or more
# activate sockets to bind on the same address and port if they all set SO_REUSEADDR,
# and result in indeterminate behavior.
if reuse_addr and sys.platform != "win32":
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
if timeout is not None:
sock.settimeout(timeout)
self.port = None
self.stop_key = base.random_key("tracker")
for my_port in range(port, port_end):
try:
sock.bind((host, my_port))
self.port = my_port
break
except OSError as sock_err:
if sock_err.errno in [errno.EADDRINUSE]:
continue
raise sock_err
if not self.port:
raise ValueError(f"cannot bind to any port in [{port}, {port_end})")
logger.info("bind to %s:%d", host, self.port)
sock.listen(1)
self.thread = threading.Thread(target=_tracker_server, args=(sock, self.stop_key))
self.thread.start()
self.host = host
def _popen_start_tracker_server(
host, port=9190, port_end=9199, silent=False, reuse_addr=True, timeout=None
):
# This is a function that will be sent to the
# Popen worker to run on a separate process.
# Create and start the server in a different thread
state = PopenTrackerServerState(host, port, port_end, silent, reuse_addr, timeout)
PopenTrackerServerState.current = state
# returns the port so that the main can get the port number.
return (state.port, state.stop_key)
class Tracker:
"""Start RPC tracker on a separate process.
Python implementation based on PopenWorker.
Parameters
----------
host : str
The host url of the server.
port : int
The TCP port to be bind to
port_end : int, optional
The end TCP port to search
silent: bool, optional
Whether run in silent mode
reuse_addr: bool, optional
Allows the kernel to reuse a local socket in TIME_WAIT state.
timeout: float, optional
set a timeout for all operations on the socket
"""
def __init__(
self, host="0.0.0.0", port=9190, port_end=9199, silent=False, reuse_addr=True, timeout=None
):
if silent:
logger.setLevel(logging.WARN)
self.proc = PopenWorker()
# send the function
self.proc.send(
_popen_start_tracker_server, [host, port, port_end, silent, reuse_addr, timeout]
)
# receive the port
self.port, self.stop_key = self.proc.recv()
self.host = host
def _stop_tracker(self):
sock = socket.socket(base.get_addr_family((self.host, self.port)), socket.SOCK_STREAM)
sock.connect(("127.0.0.1", self.port))
sock.sendall(struct.pack("<i", base.RPC_TRACKER_MAGIC))
magic = struct.unpack("<i", base.recvall(sock, 4))[0]
assert magic == base.RPC_TRACKER_MAGIC
base.sendjson(sock, [TrackerCode.STOP, self.stop_key])
assert base.recvjson(sock) == TrackerCode.SUCCESS
sock.close()
def terminate(self):
"""Terminate the server process"""
if self.proc:
if self.proc.is_alive():
self._stop_tracker()
self.proc.join(0.1)
if self.proc.is_alive():
logger.info("Terminating Tracker Server...")
self.proc.kill()
self.proc = None
def __del__(self):
try:
self.terminate()
except TypeError:
pass