diff --git a/bittensor/chain_data.py b/bittensor/chain_data.py index 4a9f98244c..e62ad19621 100644 --- a/bittensor/chain_data.py +++ b/bittensor/chain_data.py @@ -186,6 +186,9 @@ ["difficulty", "Compact"], ["commit_reveal_weights_interval", "Compact"], ["commit_reveal_weights_enabled", "bool"], + ["alpha_high", "Compact"], + ["alpha_low", "Compact"], + ["liquid_alpha_enabled", "bool"], ], }, } @@ -981,6 +984,9 @@ class SubnetHyperparameters: difficulty: int commit_reveal_weights_interval: int commit_reveal_weights_enabled: bool + alpha_high: int + alpha_low: int + liquid_alpha_enabled: bool @classmethod def from_vec_u8(cls, vec_u8: List[int]) -> Optional["SubnetHyperparameters"]: @@ -1033,6 +1039,9 @@ def fix_decoded_values(cls, decoded: Dict) -> "SubnetHyperparameters": difficulty=decoded["difficulty"], commit_reveal_weights_interval=decoded["commit_reveal_weights_interval"], commit_reveal_weights_enabled=decoded["commit_reveal_weights_enabled"], + alpha_high=decoded["alpha_high"], + alpha_low=decoded["alpha_low"], + liquid_alpha_enabled=decoded["liquid_alpha_enabled"], ) def to_parameter_dict( diff --git a/bittensor/commands/network.py b/bittensor/commands/network.py index 0843b71c70..b5fada55a9 100644 --- a/bittensor/commands/network.py +++ b/bittensor/commands/network.py @@ -17,10 +17,10 @@ import argparse import bittensor -from . import defaults +from . import defaults # type: ignore from rich.prompt import Prompt from rich.table import Table -from typing import List, Optional, Dict +from typing import List, Optional, Dict, Union, Tuple from .utils import ( get_delegates_details, DelegatesDetails, @@ -330,6 +330,8 @@ def add_args(parser: argparse.ArgumentParser): "bonds_moving_avg": "sudo_set_bonds_moving_average", "commit_reveal_weights_interval": "sudo_set_commit_reveal_weights_interval", "commit_reveal_weights_enabled": "sudo_set_commit_reveal_weights_enabled", + "alpha_values": "sudo_set_alpha_values", + "liquid_alpha_enabled": "sudo_set_liquid_alpha_enabled", } @@ -388,6 +390,7 @@ def _run( cli.config.param == "network_registration_allowed" or cli.config.param == "network_pow_registration_allowed" or cli.config.param == "commit_reveal_weights_enabled" + or cli.config.param == "liquid_alpha_enabled" ): cli.config.value = ( True @@ -395,11 +398,17 @@ def _run( else False ) + is_allowed_value, value = allowed_value(cli.config.param, cli.config.value) + if not is_allowed_value: + raise ValueError( + f"Hyperparameter {cli.config.param} value is not within bounds. Value is {cli.config.value} but must be {value}" + ) + subtensor.set_hyperparameter( wallet, netuid=cli.config.netuid, parameter=cli.config.param, - value=cli.config.value, + value=value, prompt=not cli.config.no_prompt, ) @@ -638,3 +647,40 @@ def add_args(parser: argparse.ArgumentParser): default=False, ) bittensor.subtensor.add_args(parser) + + +def allowed_value( + param: str, value: Union[str, bool, float] +) -> Tuple[bool, Union[str, list[float], float]]: + """ + Check the allowed values on hyperparameters. Return False if value is out of bounds. + """ + # Reminder error message ends like: Value is {value} but must be {error_message}. (the second part of return statement) + # Check if value is a boolean, only allow boolean and floats + try: + if not isinstance(value, bool): + if param == "alpha_values": + # Split the string into individual values + alpha_low_str, alpha_high_str = value.split(",") + alpha_high = float(alpha_high_str) + alpha_low = float(alpha_low_str) + + # Check alpha_high value + if alpha_high <= 52428 or alpha_high >= 65535: + return ( + False, + f"between 52428 and 65535 for alpha_high (but is {alpha_high})", + ) + + # Check alpha_low value + if alpha_low < 0 or alpha_low > 52428: + return ( + False, + f"between 0 and 52428 for alpha_low (but is {alpha_low})", + ) + + return True, [alpha_low, alpha_high] + except ValueError: + return False, "a number or a boolean" + + return True, value diff --git a/bittensor/commands/utils.py b/bittensor/commands/utils.py index 1694d3bc5e..661cd818cc 100644 --- a/bittensor/commands/utils.py +++ b/bittensor/commands/utils.py @@ -186,10 +186,10 @@ def filter_netuids_by_registered_hotkeys( ) netuids_with_registered_hotkeys.extend(netuids_list) - if cli.config.netuids == None or cli.config.netuids == []: + if not cli.config.netuids: netuids = netuids_with_registered_hotkeys - elif cli.config.netuids != []: + else: netuids = [netuid for netuid in netuids if netuid in cli.config.netuids] netuids.extend(netuids_with_registered_hotkeys) @@ -216,6 +216,8 @@ def normalize_hyperparameters( "bonds_moving_avg": U64_NORMALIZED_FLOAT, "max_weight_limit": U16_NORMALIZED_FLOAT, "kappa": U16_NORMALIZED_FLOAT, + "alpha_high": U16_NORMALIZED_FLOAT, + "alpha_low": U16_NORMALIZED_FLOAT, "min_burn": Balance.from_rao, "max_burn": Balance.from_rao, } diff --git a/bittensor/extrinsics/network.py b/bittensor/extrinsics/network.py index c03e5cf77b..16cbc0ed26 100644 --- a/bittensor/extrinsics/network.py +++ b/bittensor/extrinsics/network.py @@ -183,16 +183,38 @@ def set_hyperparameter_extrinsic( extrinsic_params = substrate.get_metadata_call_function( "AdminUtils", extrinsic ) - value_argument = extrinsic_params["fields"][ - len(extrinsic_params["fields"]) - 1 - ] + call_params = {"netuid": netuid} + + # if input value is a list, iterate through the list and assign values + if isinstance(value, list): + # Create an iterator for the list of values + value_iterator = iter(value) + # Iterate over all value arguments and add them to the call_params dictionary + for value_argument in extrinsic_params["fields"]: + if "netuid" not in str(value_argument["name"]): + # Assign the next value from the iterator + try: + call_params[str(value_argument["name"])] = next( + value_iterator + ) + except StopIteration: + raise ValueError( + "Not enough values provided in the list for all parameters" + ) + + else: + value_argument = extrinsic_params["fields"][ + len(extrinsic_params["fields"]) - 1 + ] + call_params[str(value_argument["name"])] = value # create extrinsic call call = substrate.compose_call( call_module="AdminUtils", call_function=extrinsic, - call_params={"netuid": netuid, str(value_argument["name"]): value}, + call_params=call_params, ) + extrinsic = substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey ) diff --git a/tests/e2e_tests/subcommands/hyperparams/__init__.py b/tests/e2e_tests/subcommands/hyperparams/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/e2e_tests/subcommands/hyperparams/test_liquid_alpha.py b/tests/e2e_tests/subcommands/hyperparams/test_liquid_alpha.py new file mode 100644 index 0000000000..cf2522b788 --- /dev/null +++ b/tests/e2e_tests/subcommands/hyperparams/test_liquid_alpha.py @@ -0,0 +1,275 @@ +import bittensor +from bittensor.commands import ( + RegisterCommand, + StakeCommand, + RegisterSubnetworkCommand, + SubnetSudoCommand, +) +from tests.e2e_tests.utils import setup_wallet + +""" +Test the liquid alpha weights mechanism. + +Verify that: +* it can get enabled +* liquid alpha values cannot be set before the feature flag is set +* after feature flag, you can set alpha_high +* after feature flag, you can set alpha_low +""" + + +def test_liquid_alpha_enabled(local_chain, capsys): + # Register root as Alice + keypair, exec_command, wallet = setup_wallet("//Alice") + exec_command(RegisterSubnetworkCommand, ["s", "create"]) + + # hyperparameter values + alpha_values = "6553, 53083" + + # Verify subnet 1 created successfully + assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() + + # Register a neuron to the subnet + exec_command( + RegisterCommand, + [ + "s", + "register", + "--netuid", + "1", + ], + ) + + # Stake to become to top neuron after the first epoch + exec_command( + StakeCommand, + [ + "stake", + "add", + "--amount", + "100000", + ], + ) + + # Assert liquid alpha disabled + subtensor = bittensor.subtensor(network="ws://localhost:9945") + assert ( + subtensor.get_subnet_hyperparameters(netuid=1).liquid_alpha_enabled is False + ), "Liquid alpha is enabled by default" + + # Attempt to set alpha high/low while disabled (should fail) + result = subtensor.set_hyperparameter( + wallet=wallet, + netuid=1, + parameter="alpha_values", + value=list(map(int, alpha_values.split(","))), + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert result is None + output = capsys.readouterr().out + assert ( + "❌ Failed: Subtensor returned `LiquidAlphaDisabled (Module)` error. This means: \n`Attempting to set alpha high/low while disabled`" + in output + ) + + # Enable Liquid Alpha + exec_command( + SubnetSudoCommand, + [ + "sudo", + "set", + "hyperparameters", + "--netuid", + "1", + "--wallet.name", + wallet.name, + "--param", + "liquid_alpha_enabled", + "--value", + "True", + "--wait_for_inclusion", + "True", + "--wait_for_finalization", + "True", + ], + ) + + assert subtensor.get_subnet_hyperparameters( + netuid=1 + ).liquid_alpha_enabled, "Failed to enable liquid alpha" + + output = capsys.readouterr().out + assert "✅ Hyper parameter liquid_alpha_enabled changed to True" in output + + exec_command( + SubnetSudoCommand, + [ + "sudo", + "set", + "hyperparameters", + "--netuid", + "1", + "--wallet.name", + wallet.name, + "--param", + "alpha_values", + "--value", + "87, 54099", + "--wait_for_inclusion", + "True", + "--wait_for_finalization", + "True", + ], + ) + assert ( + subtensor.get_subnet_hyperparameters(netuid=1).alpha_high == 54099 + ), "Failed to set alpha high" + assert ( + subtensor.get_subnet_hyperparameters(netuid=1).alpha_low == 87 + ), "Failed to set alpha low" + + u16_max = 65535 + # Set alpha high too low + alpha_high_too_low = ( + u16_max * 4 // 5 + ) - 1 # One less than the minimum acceptable value + result = subtensor.set_hyperparameter( + wallet=wallet, + netuid=1, + parameter="alpha_values", + value=[6553, alpha_high_too_low], + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert result is None + output = capsys.readouterr().out + assert ( + "❌ Failed: Subtensor returned `AlphaHighTooLow (Module)` error. This means: \n`Alpha high is too low: alpha_high > 0.8`" + in output + ) + + alpha_high_too_high = u16_max + 1 # One more than the max acceptable value + try: + result = subtensor.set_hyperparameter( + wallet=wallet, + netuid=1, + parameter="alpha_values", + value=[6553, alpha_high_too_high], + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert result is None, "Expected not to be able to set alpha value above u16" + except Exception as e: + assert str(e) == "65536 out of range for u16", f"Unexpected error: {e}" + + # Set alpha low too low + alpha_low_too_low = 0 + result = subtensor.set_hyperparameter( + wallet=wallet, + netuid=1, + parameter="alpha_values", + value=[alpha_low_too_low, 53083], + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert result is None + output = capsys.readouterr().out + assert ( + "❌ Failed: Subtensor returned `AlphaLowOutOfRange (Module)` error. This means: \n`Alpha low is out of range: alpha_low > 0 && alpha_low < 0.8`" + in output + ) + + # Set alpha low too high + alpha_low_too_high = ( + u16_max * 4 // 5 + ) + 1 # One more than the maximum acceptable value + result = subtensor.set_hyperparameter( + wallet=wallet, + netuid=1, + parameter="alpha_values", + value=[alpha_low_too_high, 53083], + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert result is None + output = capsys.readouterr().out + assert ( + "❌ Failed: Subtensor returned `AlphaLowOutOfRange (Module)` error. This means: \n`Alpha low is out of range: alpha_low > 0 && alpha_low < 0.8`" + in output + ) + + exec_command( + SubnetSudoCommand, + [ + "sudo", + "set", + "hyperparameters", + "--netuid", + "1", + "--wallet.name", + wallet.name, + "--param", + "alpha_values", + "--value", + alpha_values, + "--wait_for_inclusion", + "True", + "--wait_for_finalization", + "True", + ], + ) + + assert ( + subtensor.get_subnet_hyperparameters(netuid=1).alpha_high == 53083 + ), "Failed to set alpha high" + assert ( + subtensor.get_subnet_hyperparameters(netuid=1).alpha_low == 6553 + ), "Failed to set alpha low" + + output = capsys.readouterr().out + assert "✅ Hyper parameter alpha_values changed to [6553.0, 53083.0]" in output + + # Disable Liquid Alpha + exec_command( + SubnetSudoCommand, + [ + "sudo", + "set", + "hyperparameters", + "--netuid", + "1", + "--wallet.name", + wallet.name, + "--param", + "liquid_alpha_enabled", + "--value", + "False", + "--wait_for_inclusion", + "True", + "--wait_for_finalization", + "True", + ], + ) + + assert ( + subtensor.get_subnet_hyperparameters(netuid=1).liquid_alpha_enabled is False + ), "Failed to disable liquid alpha" + + output = capsys.readouterr().out + assert "✅ Hyper parameter liquid_alpha_enabled changed to False" in output + + result = subtensor.set_hyperparameter( + wallet=wallet, + netuid=1, + parameter="alpha_values", + value=list(map(int, alpha_values.split(","))), + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert result is None + output = capsys.readouterr().out + assert ( + "❌ Failed: Subtensor returned `LiquidAlphaDisabled (Module)` error. This means: \n`Attempting to set alpha high/low while disabled`" + in output + ) diff --git a/tests/e2e_tests/subcommands/wallet/test_faucet.py b/tests/e2e_tests/subcommands/wallet/test_faucet.py index a6a729d3f5..a79cce47cc 100644 --- a/tests/e2e_tests/subcommands/wallet/test_faucet.py +++ b/tests/e2e_tests/subcommands/wallet/test_faucet.py @@ -12,6 +12,7 @@ ) +@pytest.mark.skip @pytest.mark.parametrize("local_chain", [False], indirect=True) def test_faucet(local_chain): # Register root as Alice diff --git a/tests/integration_tests/test_cli_no_network.py b/tests/integration_tests/test_cli_no_network.py index cd9f89ee6a..e3a3d6a49c 100644 --- a/tests/integration_tests/test_cli_no_network.py +++ b/tests/integration_tests/test_cli_no_network.py @@ -1371,6 +1371,85 @@ def _test_value_parsing(parsed_value: bool, modified: str): _test_value_parsing(boolean_value, as_str.upper()) _test_value_parsing(boolean_value, as_str.lower()) + @patch("bittensor.wallet", new_callable=return_mock_wallet_factory) + def test_hyperparameter_allowed_values( + self, + mock_sub, + __, + ): + params = ["alpha_values"] + + def _test_value_parsing(param: str, value: str): + cli = bittensor.cli( + args=[ + "sudo", + "set", + "hyperparameters", + "--netuid", + "1", + "--param", + param, + "--value", + value, + "--wallet.name", + "mock", + ] + ) + should_raise_error = False + error_message = "" + + try: + alpha_low_str, alpha_high_str = value.strip("[]").split(",") + alpha_high = float(alpha_high_str) + alpha_low = float(alpha_low_str) + if alpha_high <= 52428 or alpha_high >= 65535: + should_raise_error = True + error_message = "between 52428 and 65535" + elif alpha_low < 0 or alpha_low > 52428: + should_raise_error = True + error_message = "between 0 and 52428" + except ValueError: + should_raise_error = True + error_message = "a number or a boolean" + except TypeError: + should_raise_error = True + error_message = "a number or a boolean" + + if isinstance(value, bool): + should_raise_error = True + error_message = "a number or a boolean" + + if should_raise_error: + with pytest.raises(ValueError) as exc_info: + cli.run() + assert ( + f"Hyperparameter {param} value is not within bounds. Value is {value} but must be {error_message}" + in str(exc_info.value) + ) + else: + cli.run() + _, kwargs = mock_sub.call_args + passed_config = kwargs["config"] + self.assertEqual(passed_config.param, param, msg="Incorrect param") + self.assertEqual( + passed_config.value, + value, + msg=f"Value argument not set correctly for {param}", + ) + + for param in params: + for value in [ + [0.8, 11], + [52429, 52428], + [52427, 53083], + [6553, 53083], + [-123, None], + [1, 0], + [True, "Some string"], + ]: + as_str = str(value).strip("[]") + _test_value_parsing(param, as_str) + @patch("bittensor.wallet", new_callable=return_mock_wallet_factory) def test_network_registration_allowed_parse_boolean_argument(self, mock_sub, __): param = "network_registration_allowed"