Files
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

536 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 Runner"""
import concurrent.futures
import os.path as osp
from collections.abc import Callable
from contextlib import contextmanager
from tvm.ir.utils import derived_object
from tvm.rpc import RPCSession
from tvm.runtime import Device, Module
from tvm.support.popen_pool import PopenPoolExecutor
from ..logging import get_logger
from ..profiler import Profiler
from ..utils import (
get_global_func_on_rpc_session,
get_global_func_with_default_on_worker,
)
from .config import EvaluatorConfig, RPCConfig
from .runner import PyRunner, PyRunnerFuture, RunnerFuture, RunnerInput, RunnerResult
from .utils import (
T_ARG_INFO_JSON_OBJ_LIST,
T_ARGUMENT_LIST,
alloc_argument_common,
run_evaluator_common,
)
logger = get_logger(__name__) # pylint: disable=invalid-name
T_CREATE_SESSION = Callable[ # pylint: disable=invalid-name
[RPCConfig], # The RPC configuration
RPCSession, # The RPC Session
]
T_UPLOAD_MODULE = Callable[ # pylint: disable=invalid-name
[
RPCSession, # The RPC Session
str, # local path to the artifact
str, # remote path to the artifact
],
Module, # the Module opened on the remote
]
T_ALLOC_ARGUMENT = Callable[ # pylint: disable=invalid-name
[
RPCSession, # The RPC Session
Device, # The device on the remote
T_ARG_INFO_JSON_OBJ_LIST, # The metadata information of the arguments to be allocated
int, # The number of repeated allocations to be done
],
list[T_ARGUMENT_LIST], # A list of argument lists
]
T_RUN_EVALUATOR = Callable[ # pylint: disable=invalid-name
[
RPCSession, # The RPC Session
Module, # The Module opened on the remote
Device, # The device on the remote
EvaluatorConfig, # The evaluator configuration
list[T_ARGUMENT_LIST], # A list of argument lists
],
list[float], # A list of running time
]
T_CLEANUP = Callable[ # pylint: disable=invalid-name
[
RPCSession | None, # The RPC Session to be cleaned up
str | None, # remote path to the artifact
],
None,
]
@derived_object
class RPCRunnerFuture(PyRunnerFuture):
"""RPC based runner future
Parameters
----------
future: concurrent.futures.Future
The concurrent function to check when the function is done and to return the result.
timeout_sec: float
The timeout in seconds.
"""
future: concurrent.futures.Future
timeout_sec: float
def __init__(self, future: concurrent.futures.Future, timeout_sec: float) -> None:
"""Constructor
Parameters
----------
future: concurrent.futures.Future
The concurrent function to check when the function is done and to return the result.
timeout_sec: float
The timeout in seconds.
"""
super().__init__()
self.future = future
self.timeout_sec = timeout_sec
def done(self) -> bool:
return self.future.done()
def result(self) -> RunnerResult:
try:
run_secs: list[float] = self.future.result()
except TimeoutError:
return RunnerResult(
None,
error_msg=f"RPCRunner: Timeout, killed after {self.timeout_sec} seconds",
)
except Exception as exception: # pylint: disable=broad-except
return RunnerResult(
None,
error_msg="RPCRunner: An exception occurred\n" + str(exception),
)
return RunnerResult(run_secs, None)
@derived_object
class RPCRunner(PyRunner):
"""RPC based runner
Parameters
----------
rpc_config: RPCConfig
The rpc configuration.
evaluator_config: EvaluatorConfig
The evaluator configuration.
cooldown_sec: float
The cooldown in seconds. TODO(@junrushao1994,@zxybazh): This is not used yet.
alloc_repeat: int
The number of times to repeat the allocation.
f_create_session: Optional[str, Callable]
The function name to create the session or the function itself.
f_upload_module: Optional[str, Callable]
The function name to upload the module or the function itself.
f_alloc_argument: Optional[str, Callable]
The function name to allocate the arguments or the function itself.
f_run_evaluator: Optional[str, Callable]
The function name to run the evaluator or the function itself.
f_cleanup: Optional[str, Callable]
The function name to cleanup the session or the function itself.
pool: PopenPoolExecutor
The popen pool executor.
Attributes
----------
T_CREATE_SESSION : typing._GenericAlias
The signature of the function `f_create_session`, which is:
.. code-block:: python
def default_create_session(rpc_config: RPCConfig) -> RPCSession:
...
T_UPLOAD_MODULE : typing._GenericAlias
The signature of the function `f_upload_module`, which is:
.. code-block:: python
def default_upload_module(
session: RPCSession,
local_path: str,
remote_path: str,
) -> Module:
...
T_ALLOC_ARGUMENT : typing._GenericAlias
The signature of the function `f_alloc_argument`, which is:
.. code-block:: python
def default_alloc_argument(
session: RPCSession,
device: Device,
args_info: T_ARG_INFO_JSON_OBJ_LIST,
alloc_repeat: int,
) -> List[T_ARGUMENT_LIST]:
...
T_RUN_EVALUATOR : typing._GenericAlias
The signature of the function `f_run_evaluator`, which is:
.. code-block:: python
def default_run_evaluator(
session: RPCSession,
rt_mod: Module,
device: Device,
evaluator_config: EvaluatorConfig,
repeated_args: List[T_ARGUMENT_LIST],
) -> List[float]:
...
T_CLEANUP : typing._GenericAlias
The signature of the function `f_cleanup`, which is:
.. code-block:: python
def default_cleanup(
session: Optional[RPCSession],
remote_path: Optional[str],
) -> None:
...
"""
rpc_config: RPCConfig
evaluator_config: EvaluatorConfig
cooldown_sec: float
alloc_repeat: int
f_create_session: T_CREATE_SESSION | str | None
f_upload_module: T_UPLOAD_MODULE | str | None
f_alloc_argument: T_ALLOC_ARGUMENT | str | None
f_run_evaluator: T_RUN_EVALUATOR | str | None
f_cleanup: T_CLEANUP | str | None
pool: PopenPoolExecutor
def __init__(
self,
rpc_config: RPCConfig | None = None,
evaluator_config: EvaluatorConfig | None = None,
cooldown_sec: float = 0.0,
alloc_repeat: int = 1,
f_create_session: T_CREATE_SESSION | str | None = None,
f_upload_module: T_UPLOAD_MODULE | str | None = None,
f_alloc_argument: T_ALLOC_ARGUMENT | str | None = None,
f_run_evaluator: T_RUN_EVALUATOR | str | None = None,
f_cleanup: T_CLEANUP | str | None = None,
max_workers: int | None = None,
initializer: Callable[[], None] | None = None,
) -> None:
"""Constructor
Parameters
----------
rpc_config: RPCConfig
The rpc configuration.
evaluator_config: EvaluatorConfig
The evaluator configuration.
cooldown_sec: float
The cooldown in seconds.
alloc_repeat: int
The number of times to random fill the allocation.
f_create_session: Union[T_CREATE_SESSION, str, None]
The function name to create the session or the function itself.
f_upload_module: Union[T_UPLOAD_MODULE, str, None]
The function name to upload the module or the function itself.
f_alloc_argument: Union[T_ALLOC_ARGUMENT, str, None]
The function name to allocate the arguments or the function itself.
f_run_evaluator: Union[T_RUN_EVALUATOR, str, None]
The function name to run the evaluator or the function itself.
f_cleanup: Union[T_CLEANUP, str, None]
The function name to cleanup the session or the function itself.
max_workers: Optional[int] = None
The maximum number of connections. Defaults to 1.
initializer: Optional[Callable[[], None]]
The initializer function.
"""
super().__init__()
self.rpc_config = RPCConfig._normalized(rpc_config)
self.evaluator_config = EvaluatorConfig._normalized(evaluator_config)
self.cooldown_sec = cooldown_sec
self.alloc_repeat = alloc_repeat
self.f_create_session = f_create_session
self.f_upload_module = f_upload_module
self.f_alloc_argument = f_alloc_argument
self.f_run_evaluator = f_run_evaluator
self.f_cleanup = f_cleanup
if max_workers is None:
max_workers = 1
logger.info("RPCRunner: max_workers = %d", max_workers)
self.pool = PopenPoolExecutor(
max_workers=max_workers,
initializer=initializer,
)
self._sanity_check()
def run(self, runner_inputs: list[RunnerInput]) -> list[RunnerFuture]:
results: list[RunnerFuture] = []
for runner_input in runner_inputs:
future = RPCRunnerFuture(
future=self.pool.submit(
_worker_func,
self.f_create_session,
self.f_upload_module,
self.f_alloc_argument,
self.f_run_evaluator,
self.f_cleanup,
self.rpc_config,
self.evaluator_config,
self.alloc_repeat,
str(runner_input.artifact_path),
str(runner_input.device_type),
tuple(arg_info.as_json() for arg_info in runner_input.args_info),
),
timeout_sec=self.rpc_config.session_timeout_sec,
)
results.append(future) # type: ignore
return results
def _sanity_check(self) -> None:
def _check(
f_create_session,
f_upload_module,
f_alloc_argument,
f_run_evaluator,
f_cleanup,
) -> None:
get_global_func_with_default_on_worker(name=f_create_session, default=None)
get_global_func_with_default_on_worker(name=f_upload_module, default=None)
get_global_func_with_default_on_worker(name=f_alloc_argument, default=None)
get_global_func_with_default_on_worker(name=f_run_evaluator, default=None)
get_global_func_with_default_on_worker(name=f_cleanup, default=None)
value = self.pool.submit(
_check,
self.f_create_session,
self.f_upload_module,
self.f_alloc_argument,
self.f_run_evaluator,
self.f_cleanup,
)
value.result()
def _worker_func(
_f_create_session: T_CREATE_SESSION | str | None,
_f_upload_module: T_UPLOAD_MODULE | str | None,
_f_alloc_argument: T_ALLOC_ARGUMENT | str | None,
_f_run_evaluator: T_RUN_EVALUATOR | str | None,
_f_cleanup: T_CLEANUP | str | None,
rpc_config: RPCConfig,
evaluator_config: EvaluatorConfig,
alloc_repeat: int,
artifact_path: str,
device_type: str,
args_info: T_ARG_INFO_JSON_OBJ_LIST,
) -> list[float]:
# Step 0. Get the registered functions
f_create_session: T_CREATE_SESSION = get_global_func_with_default_on_worker(
_f_create_session, default_create_session
)
f_upload_module: T_UPLOAD_MODULE = get_global_func_with_default_on_worker(
_f_upload_module, default_upload_module
)
f_alloc_argument: T_ALLOC_ARGUMENT = get_global_func_with_default_on_worker(
_f_alloc_argument, default_alloc_argument
)
f_run_evaluator: T_RUN_EVALUATOR = get_global_func_with_default_on_worker(
_f_run_evaluator, default_run_evaluator
)
f_cleanup: T_CLEANUP = get_global_func_with_default_on_worker(_f_cleanup, default_cleanup)
# Managed resources
session: RPCSession | None = None
remote_path: str | None = None
@contextmanager
def resource_handler():
try:
yield
finally:
# Final step. Always clean up
with Profiler.timeit("RPCRunner/cleanup"):
f_cleanup(session, remote_path)
with resource_handler():
# Step 1. Create session
with Profiler.timeit("RPCRunner/create_session"):
session = f_create_session(rpc_config)
device = session.device(device_type, 0)
# Step 2. Upload the module
with Profiler.timeit("RPCRunner/upload_module"):
_, remote_path = osp.split(artifact_path)
local_path: str = artifact_path
rt_mod: Module = f_upload_module(session, local_path, remote_path)
# Step 3: Allocate input arguments
with Profiler.timeit("RPCRunner/alloc_argument"):
repeated_args: list[T_ARGUMENT_LIST] = f_alloc_argument(
session,
device,
args_info,
alloc_repeat,
)
# Step 4: Run time_evaluator
with Profiler.timeit("LocalRunner/run_evaluator"):
costs: list[float] = f_run_evaluator(
session,
rt_mod,
device,
evaluator_config,
repeated_args,
)
return costs
def default_create_session(rpc_config: RPCConfig) -> RPCSession:
"""Default function to create the session
Parameters
----------
rpc_config : RPCConfig
The configuration of the RPC session
Returns
-------
session : RPCSession
The created rpc session
"""
return rpc_config.connect_server()
def default_upload_module(
session: RPCSession,
local_path: str,
remote_path: str,
) -> Module:
"""Default function to upload the module
Parameters
----------
session: RPCSession
The session to upload the module
local_path: str
The local path of the module
remote_path: str
The remote path to place the module
Returns
-------
rt_mod : Module
The runtime module
"""
session.upload(local_path, remote_path)
rt_mod: Module = session.load_module(remote_path)
return rt_mod
def default_alloc_argument(
session: RPCSession,
device: Device,
args_info: T_ARG_INFO_JSON_OBJ_LIST,
alloc_repeat: int,
) -> list[T_ARGUMENT_LIST]:
"""Default function to allocate the arguments
Parameters
----------
session: RPCSession
The session to allocate the arguments
device: Device
The device to allocate the arguments
args_info: T_ARG_INFO_JSON_OBJ_LIST
The arguments info
alloc_repeat: int
The number of times to repeat the allocation
Returns
-------
repeated_args: List[Args]
The allocation args
"""
f_random_fill = get_global_func_on_rpc_session(
session,
"tvm.contrib.random.random_fill_for_measure",
"Please make sure 'USE_RANDOM' is turned ON in the config.cmake on the RPC server.",
)
return alloc_argument_common(f_random_fill, device, args_info, alloc_repeat)
def default_run_evaluator(
session: RPCSession, # pylint: disable=unused-argument
rt_mod: Module,
device: Device,
evaluator_config: EvaluatorConfig,
repeated_args: list[T_ARGUMENT_LIST],
) -> list[float]:
"""Default function to run the evaluator
Parameters
----------
session: RPCSession
The session to run the evaluator
rt_mod: Module
The runtime module
device: Device
The device to run the evaluator
evaluator_config: EvaluatorConfig
The evaluator config
repeated_args: List[T_ARGUMENT_LIST]
The repeated arguments
Returns
-------
costs: List[float]
The evaluator results
"""
return run_evaluator_common(rt_mod, device, evaluator_config, repeated_args)
def default_cleanup(
session: RPCSession | None,
remote_path: str | None,
) -> None:
"""Default function to clean up the session
Parameters
----------
session: RPCSession
The session to clean up
remote_path: str
The remote path to clean up
"""
if session is not None and remote_path is not None:
session.remove(remote_path)
session.remove(remote_path + ".so")
session.remove("")