From 302ce432829ae7c25e100a5cca718f0aadbe4fd4 Mon Sep 17 00:00:00 2001 From: Dmitrii Agibov Date: Tue, 15 Nov 2022 13:19:53 +0000 Subject: MLIA-649 Support tosa-checker as a backend - Add new type of the backend based on python packages - Add installation class for TOSA checker - Update documentation - Extend support of the parameter "force" in the "install" command Change-Id: I95567b75e1cfe85daa1f1c3d359975bb67b2504e --- src/mlia/tools/metadata/common.py | 173 +++++++++++++++++----------------- src/mlia/tools/metadata/corstone.py | 1 - src/mlia/tools/metadata/py_package.py | 84 +++++++++++++++++ 3 files changed, 169 insertions(+), 89 deletions(-) create mode 100644 src/mlia/tools/metadata/py_package.py (limited to 'src/mlia/tools') diff --git a/src/mlia/tools/metadata/common.py b/src/mlia/tools/metadata/common.py index 927be74..5019da9 100644 --- a/src/mlia/tools/metadata/common.py +++ b/src/mlia/tools/metadata/common.py @@ -11,9 +11,10 @@ from pathlib import Path from typing import Callable from typing import Union +from mlia.core.errors import ConfigurationError +from mlia.core.errors import InternalError from mlia.utils.misc import yes - logger = logging.getLogger(__name__) @@ -124,7 +125,9 @@ class InstallationManager(ABC): """Install backend from the local directory.""" @abstractmethod - def download_and_install(self, backend_name: str, eula_agreement: bool) -> None: + def download_and_install( + self, backend_name: str, eula_agreement: bool, force: bool + ) -> None: """Download and install backends.""" @abstractmethod @@ -153,29 +156,15 @@ class InstallationFiltersMixin: if all(filter_(installation) for filter_ in filters) ] - def could_be_installed_from( - self, backend_path: Path, backend_name: str - ) -> list[Installation]: - """Return installations that could be installed from provided directory.""" - return self.filter_by( - SupportsInstallTypeFilter(InstallFromPath(backend_path)), - SearchByNameFilter(backend_name), - ) - - def could_be_downloaded_and_installed( - self, backend_name: str - ) -> list[Installation]: - """Return installations that could be downloaded and installed.""" - return self.filter_by( - SupportsInstallTypeFilter(DownloadAndInstall()), - SearchByNameFilter(backend_name), - ReadyForInstallationFilter(), - ) + def find_by_name(self, backend_name: str) -> list[Installation]: + """Return list of the backends filtered by name.""" + return self.filter_by(SearchByNameFilter(backend_name)) def already_installed(self, backend_name: str = None) -> list[Installation]: """Return list of backends that are already installed.""" return self.filter_by( - AlreadyInstalledFilter(), SearchByNameFilter(backend_name) + AlreadyInstalledFilter(), + SearchByNameFilter(backend_name), ) def ready_for_installation(self) -> list[Installation]: @@ -193,83 +182,96 @@ class DefaultInstallationManager(InstallationManager, InstallationFiltersMixin): self.installations = installations self.noninteractive = noninteractive - def choose_installation_for_path( - self, backend_path: Path, backend_name: str, force: bool - ) -> Installation | None: - """Check available installation and select one if possible.""" - installs = self.could_be_installed_from(backend_path, backend_name) + def _install( + self, + backend_name: str, + install_type: InstallationType, + prompt: Callable[[Installation], str], + force: bool, + ) -> None: + """Check metadata and install backend.""" + installs = self.find_by_name(backend_name) if not installs: + logger.info("Unknown backend '%s'.", backend_name) logger.info( - "Unfortunatelly, it was not possible to automatically " - "detect type of the installed FVP. " - "Please, check provided path to the installed FVP." + "Please run command 'mlia-backend list' to get list of " + "supported backend names." ) - return None - if len(installs) != 1: - names = ",".join(install.name for install in installs) - logger.info( - "Unable to correctly detect type of the installed FVP." - "The following FVPs are detected %s. Installation skipped.", - names, - ) - return None + return + + if len(installs) > 1: + raise InternalError(f"More than one backend with name {backend_name} found") installation = installs[0] - if installation.already_installed: + if not installation.supports(install_type): + if isinstance(install_type, InstallFromPath): + logger.info( + "Backend '%s' could not be installed using path '%s'.", + installation.name, + install_type.backend_path, + ) + logger.info( + "Please check that '%s' is a valid path to the installed backend.", + install_type.backend_path, + ) + else: + logger.info( + "Backend '%s' could not be downloaded and installed", + installation.name, + ) + logger.info( + "Please refer to the project's documentation for more details." + ) + + return + + if installation.already_installed and not force: + logger.info("Backend '%s' is already installed.", installation.name) + logger.info("Please, consider using --force option.") + return + + proceed = self.noninteractive or yes(prompt(installation)) + if not proceed: + logger.info("%s installation canceled.", installation.name) + return + + if installation.already_installed and force: logger.info( - "%s was found in %s, but it has been already installed " - "in the ML Inference Advisor.", + "Force installing %s, so delete the existing " + "installed backend first.", installation.name, - backend_path, ) - return installation if force else None + installation.uninstall() - return installation + installation.install(install_type) + logger.info("%s successfully installed.", installation.name) def install_from( self, backend_path: Path, backend_name: str, force: bool = False ) -> None: """Install from the provided directory.""" - installation = self.choose_installation_for_path( - backend_path, backend_name, force - ) - - if not installation: - return - if force: - self.uninstall(backend_name) - logger.info( - "Force installing %s, so delete the existing installed backend first.", - installation.name, + def prompt(install: Installation) -> str: + return ( + f"{install.name} was found in {backend_path}. " + "Would you like to install it?" ) - prompt = ( - f"{installation.name} was found in {backend_path}. " - "Would you like to install it?" - ) - self._install(installation, InstallFromPath(backend_path), prompt) + install_type = InstallFromPath(backend_path) + self._install(backend_name, install_type, prompt, force) def download_and_install( - self, backend_name: str, eula_agreement: bool = True + self, backend_name: str, eula_agreement: bool = True, force: bool = False ) -> None: """Download and install available backends.""" - installations = self.could_be_downloaded_and_installed(backend_name) - if not installations: - logger.info("No backends available for the installation.") - return + def prompt(install: Installation) -> str: + return f"Would you like to download and install {install.name}?" - names = ",".join(installation.name for installation in installations) - logger.info("Following backends are available for downloading: %s", names) - - for installation in installations: - prompt = f"Would you like to download and install {installation.name}?" - self._install( - installation, DownloadAndInstall(eula_agreement=eula_agreement), prompt - ) + install_type = DownloadAndInstall(eula_agreement=eula_agreement) + self._install(backend_name, install_type, prompt, force) def show_env_details(self) -> None: """Print current state of the execution environment.""" @@ -299,24 +301,19 @@ class DefaultInstallationManager(InstallationManager, InstallationFiltersMixin): def uninstall(self, backend_name: str) -> None: """Uninstall the backend with name backend_name.""" installations = self.already_installed(backend_name) + if not installations: - raise Exception("No backend available for uninstall") - for installation in installations: - installation.uninstall() + raise ConfigurationError(f"Backend '{backend_name}' is not installed") - def _install( - self, - installation: Installation, - installation_type: InstallationType, - prompt: str, - ) -> None: - proceed = self.noninteractive or yes(prompt) + if len(installations) != 1: + raise InternalError( + f"More than one installed backend with name {backend_name} found" + ) - if proceed: - installation.install(installation_type) - logger.info("%s successfully installed.", installation.name) - else: - logger.info("%s installation canceled.", installation.name) + installation = installations[0] + installation.uninstall() + + logger.info("%s successfully uninstalled.", installation.name) def backend_installed(self, backend_name: str) -> bool: """Return true if requested backend installed.""" diff --git a/src/mlia/tools/metadata/corstone.py b/src/mlia/tools/metadata/corstone.py index 04b13b5..df2dcdb 100644 --- a/src/mlia/tools/metadata/corstone.py +++ b/src/mlia/tools/metadata/corstone.py @@ -209,7 +209,6 @@ class BackendInstallation(Installation): def uninstall(self) -> None: """Uninstall the backend.""" remove_system(self.metadata.fvp_dir_name) - logger.info("%s successfully uninstalled.", self.name) class PackagePathChecker: diff --git a/src/mlia/tools/metadata/py_package.py b/src/mlia/tools/metadata/py_package.py new file mode 100644 index 0000000..716b62a --- /dev/null +++ b/src/mlia/tools/metadata/py_package.py @@ -0,0 +1,84 @@ +# SPDX-FileCopyrightText: Copyright 2022, Arm Limited and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 +"""Module for python package based installations.""" +from __future__ import annotations + +from mlia.tools.metadata.common import DownloadAndInstall +from mlia.tools.metadata.common import Installation +from mlia.tools.metadata.common import InstallationType +from mlia.utils.py_manager import get_package_manager + + +class PyPackageBackendInstallation(Installation): + """Backend based on the python package.""" + + def __init__( + self, + name: str, + description: str, + packages_to_install: list[str], + packages_to_uninstall: list[str], + expected_packages: list[str], + ) -> None: + """Init the backend installation.""" + self._name = name + self._description = description + self._packages_to_install = packages_to_install + self._packages_to_uninstall = packages_to_uninstall + self._expected_packages = expected_packages + + self.package_manager = get_package_manager() + + @property + def name(self) -> str: + """Return name of the backend.""" + return self._name + + @property + def description(self) -> str: + """Return description of the backend.""" + return self._description + + @property + def could_be_installed(self) -> bool: + """Check if backend could be installed.""" + return True + + @property + def already_installed(self) -> bool: + """Check if backend already installed.""" + return self.package_manager.packages_installed(self._expected_packages) + + def supports(self, install_type: InstallationType) -> bool: + """Return true if installation supports requested installation type.""" + return isinstance(install_type, DownloadAndInstall) + + def install(self, install_type: InstallationType) -> None: + """Install the backend.""" + if not self.supports(install_type): + raise Exception(f"Unsupported installation type {install_type}") + + self.package_manager.install(self._packages_to_install) + + def uninstall(self) -> None: + """Uninstall the backend.""" + self.package_manager.uninstall(self._packages_to_uninstall) + + +def get_tosa_backend_installation() -> Installation: + """Get TOSA backend installation.""" + return PyPackageBackendInstallation( + name="tosa-checker", + description="Tool to check if a ML model is compatible " + "with the TOSA specification", + packages_to_install=["mlia[tosa]"], + packages_to_uninstall=["tosa-checker"], + expected_packages=["tosa-checker"], + ) + + +def get_pypackage_backend_installations() -> list[Installation]: + """Return list of the backend installations based on python packages.""" + return [ + get_tosa_backend_installation(), + ] -- cgit v1.2.1