mlos_bench.tunables.tunable =========================== .. py:module:: mlos_bench.tunables.tunable .. autoapi-nested-parse:: Definitions for :py:class:`~.Tunable` parameters. Tunable parameters are one of the core building blocks of the :py:mod:`mlos_bench` framework. Together with :py:class:`~mlos_bench.tunables.tunable_groups.TunableGroups`, they provide a description of a configuration parameter space for a benchmark or an autotuning optimization task. Some details about the configuration of an individual :py:class:`~.Tunable` parameter are available in the Examples docstrings below. However, Tunables are generally provided as a part of a :py:class:`~mlos_bench.tunables.tunable_groups.TunableGroups` config specified in a JSON config file. .. seealso:: :py:mod:`mlos_bench.tunables` For more information on Tunable parameters and their configuration. Classes ------- .. autoapisummary:: mlos_bench.tunables.tunable.Tunable Module Contents --------------- .. py:class:: Tunable(name: str, config: dict) A Tunable parameter definition and its current value. Create an instance of a new Tunable parameter. :param name: Human-readable identifier of the Tunable parameter. NOTE: ``!`` characters are currently disallowed in Tunable names in order handle "special" values sampling logic. See: :py:mod:`mlos_bench.optimizers.convert_configspace` for details. :type name: str :param config: Python dict that represents a Tunable (e.g., deserialized from JSON) NOTE: Must be convertible to a :py:class:`~mlos_bench.tunables.tunable_types.TunableDict`. :type config: dict .. seealso:: :py:mod:`mlos_bench.tunables` For more information on Tunable parameters and their configuration. .. py:method:: __eq__(other: object) -> bool Check if two Tunable objects are equal. :param other: A tunable object to compare to. :type other: Tunable :returns: **is_equal** -- True if the Tunables correspond to the same parameter and have the same value and type. NOTE: ranges and special values are not currently considered in the comparison. :rtype: bool .. py:method:: __lt__(other: object) -> bool Compare the two Tunable objects. We mostly need this to create a canonical list of Tunable objects when hashing a :py:class:`~mlos_bench.tunables.tunable_groups.TunableGroups`. :param other: A tunable object to compare to. :type other: Tunable :returns: **is_less** -- True if the current Tunable is less then the other one, False otherwise. :rtype: bool .. py:method:: __repr__() -> str Produce a human-readable version of the Tunable (mostly for logging). :returns: **string** -- A human-readable version of the Tunable. :rtype: str .. py:method:: copy() -> Tunable Deep copy of the Tunable object. :returns: **tunable** -- A new Tunable object that is a deep copy of the original one. :rtype: Tunable .. py:method:: from_json(name: str, json_str: str) -> Tunable :staticmethod: Create a Tunable object from a JSON string. :param name: Human-readable identifier of the Tunable parameter. :type name: str :param json_str: JSON string that represents a Tunable. :type json_str: str :returns: **tunable** -- A new Tunable object created from the JSON string. :rtype: Tunable .. rubric:: Notes This is mostly for testing purposes. Generally Tunables will be created as a part of loading :py:class:`~mlos_bench.tunables.tunable_groups.TunableGroups`. .. seealso:: :py:meth:`ConfigPersistenceService.load_tunables <mlos_bench.services.config_persistence.ConfigPersistenceService.load_tunables>` .. py:method:: in_range(value: int | float | str | None) -> bool Check if the value is within the range of the Tunable. Do *NOT* check for special values. Return False if the Tunable or value is categorical or None. .. py:method:: is_default() -> bool Checks whether the currently assigned value of the Tunable is at its default. .. py:method:: is_valid(value: mlos_bench.tunables.tunable_types.TunableValue) -> bool Check if the value can be assigned to the Tunable. :param value: Value to validate. :type value: int | float | str :returns: **is_valid** -- True if the value is valid, False otherwise. :rtype: bool .. py:method:: update(value: mlos_bench.tunables.tunable_types.TunableValue) -> bool Assign the value to the Tunable. Return True if it is a new value, False otherwise. :param value: Value to assign. :type value: int | float | str :returns: **is_updated** -- True if the new value is different from the previous one, False otherwise. :rtype: bool .. py:property:: cardinality :type: int | None Gets the cardinality of elements in this Tunable, or else None (e.g., when the Tunable is continuous float and not quantized). If the Tunable has quantization set, this returns the number of quantization bins. :returns: **cardinality** -- Either the number of points in the Tunable or else None. :rtype: int .. rubric:: Examples >>> json_config = ''' ... { ... "type": "categorical", ... "default": "red", ... "values": ["red", "blue", "green"], ... } ... ''' >>> categorical_tunable = Tunable.from_json("categorical_tunable", json_config) >>> categorical_tunable.cardinality 3 >>> json_config = ''' ... { ... "type": "int", ... "default": 0, ... "range": [0, 10000], ... } ... ''' >>> basic_tunable = Tunable.from_json("basic_tunable", json_config) >>> basic_tunable.cardinality 10001 >>> json_config = ''' ... { ... "type": "int", ... "default": 0, ... "range": [0, 10000], ... // Enable quantization. ... "quantization_bins": 10, ... } ... ''' >>> quantized_tunable = Tunable.from_json("quantized_tunable", json_config) >>> quantized_tunable.cardinality 10 >>> json_config = ''' ... { ... "type": "float", ... "default": 50.0, ... "range": [0, 100], ... } ... ''' >>> float_tunable = Tunable.from_json("float_tunable", json_config) >>> assert float_tunable.cardinality is None .. py:property:: categories :type: list[str | None] Get the list of all possible values of a categorical Tunable. Return None if the Tunable is not categorical. :returns: **values** -- List of all possible values of a categorical Tunable. :rtype: list[str] .. seealso:: :py:obj:`Tunable.values` For more examples on getting the categorical values of a Tunable. .. py:property:: category :type: str | None Get the current value of the Tunable as a string. .. py:property:: default :type: mlos_bench.tunables.tunable_types.TunableValue Get the default value of the Tunable. .. py:property:: description :type: str | None Get the description of the Tunable. .. py:property:: distribution :type: mlos_bench.tunables.tunable_types.DistributionName | None Get the name of the distribution if specified. :returns: **distribution** -- Name of the distribution or None. :rtype: str | None .. seealso:: :py:attr:`~.Tunable.distribution_params` For more examples on configuring a Tunable with a distribution. .. rubric:: Examples >>> # Example values of the DistributionName >>> from mlos_bench.tunables.tunable_types import DistributionName >>> DistributionName typing.Literal['uniform', 'normal', 'beta'] .. py:property:: distribution_params :type: dict[str, float] Get the parameters of the distribution, if specified. :returns: **distribution_params** -- Parameters of the distribution or None. :rtype: dict[str, float] .. rubric:: Examples >>> json_config = ''' ... { ... "type": "int", ... "default": 0, ... "range": [0, 10], ... // No distribution specified. ... } ... ''' >>> base_config = json.loads(json_config) >>> basic_tunable = Tunable("basic_tunable", base_config) >>> assert basic_tunable.distribution is None >>> basic_tunable.distribution_params {} >>> # Example of a uniform distribution (the default if not specified) >>> config_with_dist = base_config | { ... "distribution": { ... "type": "uniform" ... } ... } >>> uniform_tunable = Tunable("uniform_tunable", config_with_dist) >>> uniform_tunable.distribution 'uniform' >>> uniform_tunable.distribution_params {} >>> # Example of a normal distribution params >>> config_with_dist = base_config | { ... "distribution": { ... "type": "normal", ... "params": { ... "mu": 0.0, ... "sigma": 1.0, ... } ... } ... } >>> normal_tunable = Tunable("normal_tunable", config_with_dist) >>> normal_tunable.distribution 'normal' >>> normal_tunable.distribution_params {'mu': 0.0, 'sigma': 1.0} >>> # Example of a beta distribution params >>> config_with_dist = base_config | { ... "distribution": { ... "type": "beta", ... "params": { ... "alpha": 1.0, ... "beta": 1.0, ... } ... } ... } >>> beta_tunable = Tunable("beta_tunable", config_with_dist) >>> beta_tunable.distribution 'beta' >>> beta_tunable.distribution_params {'alpha': 1.0, 'beta': 1.0} .. py:property:: dtype :type: mlos_bench.tunables.tunable_types.TunableValueType Get the actual Python data type of the Tunable. This is useful for bulk conversions of the input data. :returns: **dtype** -- Data type of the Tunable - one of: ``{int, float, str}`` :rtype: type .. rubric:: Examples >>> # Example values of the TunableValueType >>> from mlos_bench.tunables.tunable_types import TunableValueType >>> TunableValueType type[int] | type[float] | type[str] >>> # Example values of the TUNABLE_DTYPE >>> from mlos_bench.tunables.tunable_types import TUNABLE_DTYPE >>> TUNABLE_DTYPE {'int': <class 'int'>, 'float': <class 'float'>, 'categorical': <class 'str'>} .. py:property:: is_categorical :type: bool Check if the Tunable is categorical. :returns: **is_categorical** -- True if the Tunable is categorical, False otherwise. :rtype: bool .. py:property:: is_log :type: bool | None Check if numeric Tunable is log scale. :returns: **log** -- True if numeric Tunable is log scale, False if linear. :rtype: bool .. rubric:: Examples >>> # Example values of the log scale >>> json_config = ''' ... { ... "type": "int", ... "default": 0, ... "range": [0, 10000], ... // Enable log sampling. ... "log": true, ... } ... ''' >>> tunable = Tunable.from_json("log_tunable", json_config) >>> tunable.is_log True .. py:property:: is_numerical :type: bool Check if the Tunable is an integer or float. :returns: **is_int** -- True if the Tunable is an integer or float, False otherwise. :rtype: bool .. py:property:: is_special :type: bool Check if the current value of the Tunable is special. :returns: **is_special** -- True if the current value of the Tunable is special, False otherwise. :rtype: bool .. py:property:: meta :type: dict[str, Any] Get the Tunable's metadata. This is a free-form dictionary that can be used to store any additional information about the Tunable (e.g., the unit information) which can be useful when using the ``dump_params_file`` and ``dump_meta_file`` properties of the :py:class:`~mlos_bench.environments` config to generate a configuration file for the target system. .. rubric:: Examples >>> json_config = ''' ... { ... "type": "int", ... "range": [0, 10], ... "default": 1, ... "meta": { ... "unit": "seconds", ... }, ... "description": "Time to wait before timing out a request.", ... } ... ''' >>> tunable = Tunable.from_json("timer_tunable", json_config) >>> tunable.meta {'unit': 'seconds'} .. py:property:: name :type: str Get the name / string ID of the Tunable. .. py:property:: numerical_value :type: int | float Get the current value of the Tunable as a number. .. py:property:: quantization_bins :type: int | None Get the number of quantization bins, if specified. :returns: **quantization_bins** -- Number of quantization bins, or None. :rtype: int | None .. rubric:: Examples >>> json_config = ''' ... { ... "type": "int", ... "default": 0, ... "range": [0, 10000], ... // Enable quantization. ... "quantization_bins": 11, ... } ... ''' >>> quantized_tunable = Tunable.from_json("quantized_tunable", json_config) >>> quantized_tunable.quantization_bins 11 >>> list(quantized_tunable.quantized_values) [0, 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000] >>> json_config = ''' ... { ... "type": "float", ... "default": 0, ... "range": [0, 1], ... // Enable quantization. ... "quantization_bins": 5, ... } ... ''' >>> quantized_tunable = Tunable.from_json("quantized_tunable", json_config) >>> quantized_tunable.quantization_bins 5 >>> list(quantized_tunable.quantized_values) [0.0, 0.25, 0.5, 0.75, 1.0] .. py:property:: quantized_values :type: collections.abc.Iterable[int] | collections.abc.Iterable[float] | None Get a sequence of quantized values for this Tunable. :returns: If the Tunable is quantizable, returns a sequence of those elements, else None (e.g., for unquantized float type Tunables). :rtype: Iterable[int] | Iterable[float] | None .. seealso:: :py:attr:`~.Tunable.quantization_bins` For more examples on configuring a Tunable with quantization. .. py:property:: range :type: tuple[int, int] | tuple[float, float] Get the range of the Tunable if it is numerical, None otherwise. :returns: **range** -- A 2-tuple of numbers that represents the range of the Tunable. Numbers can be int or float, depending on the type of the Tunable. :rtype: tuple[int, int] | tuple[float, float] .. rubric:: Examples >>> json_config = ''' ... { ... "type": "int", ... "default": 0, ... "range": [0, 10000], ... } ... ''' >>> int_tunable = Tunable.from_json("int_tunable", json_config) >>> int_tunable.range (0, 10000) >>> json_config = ''' ... { ... "type": "float", ... "default": 0.0, ... "range": [0.0, 100.0], ... } ... ''' >>> float_tunable = Tunable.from_json("float_tunable", json_config) >>> float_tunable.range (0.0, 100.0) .. py:property:: range_weight :type: float | None Get weight of the range of the numeric Tunable. Return None if there are no weights or a Tunable is categorical. :returns: **weight** -- Weight of the range or None. :rtype: float .. seealso:: :py:obj:`Tunable.weights` For example of range_weight configuration. .. py:property:: span :type: int | float Gets the span of the range. Note: this does not take quantization into account. :returns: (max - min) for numerical Tunables. :rtype: int | float .. py:property:: special :type: list[int] | list[float] Get the special values of the Tunable. Return an empty list if there are none. Special values are used to mark some values as "special" that need more explicit testing. For example, these might indicate "automatic" or "disabled" behavior for the system being tested instead of an explicit size and hence need more explicit sampling. .. rubric:: Notes Only numerical Tunable parameters can have special values. :returns: **special** -- A list of special values of the Tunable. Can be empty. :rtype: [int] | [float] .. rubric:: Examples >>> # Example values of the special values >>> json_config = ''' ... { ... "type": "int", ... "default": 50, ... "range": [1, 100], ... // These are special and sampled ... // Note that the types don't need to match or be in the range. ... "special": [ ... -1, // e.g., auto ... 0, // e.g., disabled ... true, // e.g., enabled ... null, // e.g., unspecified ... ], ... } ... ''' >>> tunable = Tunable.from_json("tunable_with_special", json_config) >>> # JSON values are converted to Python types >>> tunable.special [-1, 0, True, None] .. py:property:: type :type: mlos_bench.tunables.tunable_types.TunableValueTypeName Get the string name of the data type of the Tunable. :returns: **type** -- String representation of the data type of the Tunable. :rtype: TunableValueTypeName .. rubric:: Examples >>> # Example values of the TunableValueTypeName >>> from mlos_bench.tunables.tunable_types import TunableValueTypeName >>> TunableValueTypeName typing.Literal['int', 'float', 'categorical'] .. rubric:: Examples >>> json_config = ''' ... { ... "type": "categorical", ... "default": "red", ... "values": ["red", "blue", "green"], ... } ... ''' >>> categorical_tunable = Tunable.from_json("categorical_tunable", json_config) >>> categorical_tunable.type 'categorical' >>> json_config = ''' ... { ... "type": "int", ... "default": 0, ... "range": [0, 10000], ... } ... ''' >>> int_tunable = Tunable.from_json("int_tunable", json_config) >>> int_tunable.type 'int' >>> json_config = ''' ... { ... "type": "float", ... "default": 0.0, ... "range": [0.0, 10000.0], ... } ... ''' >>> float_tunable = Tunable.from_json("float_tunable", json_config) >>> float_tunable.type 'float' .. py:property:: value :type: mlos_bench.tunables.tunable_types.TunableValue Get the current value of the Tunable. .. py:property:: values :type: collections.abc.Iterable[str | None] | collections.abc.Iterable[int] | collections.abc.Iterable[float] | None Gets the :py:attr:`~.Tunable.categories` or :py:attr:`~.Tunable.quantized_values` for this Tunable. :returns: Categories or quantized values. :rtype: Iterable[str | None] | Iterable[int] | Iterable[float] | None .. rubric:: Examples >>> # Example values of the Tunable categories >>> json_config = ''' ... { ... "type": "categorical", ... "values": ["red", "blue", "green"], ... "default": "red", ... } ... ''' >>> categorical_tunable = Tunable.from_json("categorical_tunable", json_config) >>> list(categorical_tunable.values) ['red', 'blue', 'green'] >>> assert categorical_tunable.values == categorical_tunable.categories >>> # Example values of the Tunable int >>> json_config = ''' ... { ... "type": "int", ... "range": [0, 5], ... "default": 1, ... } ... ''' >>> int_tunable = Tunable.from_json("int_tunable", json_config) >>> list(int_tunable.values) [0, 1, 2, 3, 4, 5] >>> # Example values of the quantized Tunable float >>> json_config = ''' ... { ... "type": "float", ... "range": [0, 1], ... "default": 0.5, ... "quantization_bins": 3, ... } ... ''' >>> float_tunable = Tunable.from_json("float_tunable", json_config) >>> list(float_tunable.values) [0.0, 0.5, 1.0] .. py:property:: weights :type: list[float] | None Get the weights of the categories or special values of the Tunable. Return None if there are none. :returns: **weights** -- A list of weights or None. :rtype: [float] .. rubric:: Examples >>> json_config = ''' ... { ... "type": "categorical", ... "default": "red", ... "values": ["red", "blue", "green"], ... "values_weights": [0.1, 0.2, 0.7], ... } ... ''' >>> categorical_tunable = Tunable.from_json("categorical_tunable", json_config) >>> categorical_tunable.weights [0.1, 0.2, 0.7] >>> dict(zip(categorical_tunable.values, categorical_tunable.weights)) {'red': 0.1, 'blue': 0.2, 'green': 0.7} >>> json_config = ''' ... { ... "type": "float", ... "default": 50.0, ... "range": [1, 100], ... "special": [-1, 0], ... "special_weights": [0.1, 0.2], ... "range_weight": 0.7, ... } ... ''' >>> float_tunable = Tunable.from_json("float_tunable", json_config) >>> float_tunable.weights [0.1, 0.2] >>> dict(zip(float_tunable.special, float_tunable.weights)) {-1: 0.1, 0: 0.2}