diff --git a/bittensor/_cli/__init__.py b/bittensor/_cli/__init__.py index a69a65b65f..eb7c1fd374 100644 --- a/bittensor/_cli/__init__.py +++ b/bittensor/_cli/__init__.py @@ -832,32 +832,38 @@ def check_overview_config( config: 'bittensor.Config' ): def _check_for_cuda_reg_config( config: 'bittensor.Config' ) -> None: """Checks, when CUDA is available, if the user would like to register with their CUDA device.""" if torch.cuda.is_available(): - if config.subtensor.register.cuda.get('use_cuda') is None: - # Ask about cuda registration only if a CUDA device is available. - cuda = Confirm.ask("Detected CUDA device, use CUDA for registration?\n") - config.subtensor.register.cuda.use_cuda = cuda - - # Only ask about which CUDA device if the user has more than one CUDA device. - if config.subtensor.register.cuda.use_cuda and config.subtensor.register.cuda.get('dev_id') is None and torch.cuda.device_count() > 0: - devices: List[str] = [str(x) for x in range(torch.cuda.device_count())] - device_names: List[str] = [torch.cuda.get_device_name(x) for x in range(torch.cuda.device_count())] - console.print("Available CUDA devices:") - choices_str: str = "" - for i, device in enumerate(devices): - choices_str += (" {}: {}\n".format(device, device_names[i])) - console.print(choices_str) - dev_id = IntListPrompt.ask("Which GPU(s) would you like to use? Please list one, or comma-separated", choices=devices, default='All') - if dev_id == 'All': - dev_id = list(range(torch.cuda.device_count())) - else: - try: - # replace the commas with spaces then split over whitespace., - # then strip the whitespace and convert to ints. - dev_id = [int(dev_id.strip()) for dev_id in dev_id.replace(',', ' ').split()] - except ValueError: - console.error(":cross_mark:[red]Invalid GPU device[/red] [bold white]{}[/bold white]\nAvailable CUDA devices:{}".format(dev_id, choices_str)) - sys.exit(1) - config.subtensor.register.cuda.dev_id = dev_id + if not config.no_prompt: + if config.subtensor.register.cuda.get('use_cuda') == None: # flag not set + # Ask about cuda registration only if a CUDA device is available. + cuda = Confirm.ask("Detected CUDA device, use CUDA for registration?\n") + config.subtensor.register.cuda.use_cuda = cuda + + + # Only ask about which CUDA device if the user has more than one CUDA device. + if config.subtensor.register.cuda.use_cuda and config.subtensor.register.cuda.get('dev_id') is None: + devices: List[str] = [str(x) for x in range(torch.cuda.device_count())] + device_names: List[str] = [torch.cuda.get_device_name(x) for x in range(torch.cuda.device_count())] + console.print("Available CUDA devices:") + choices_str: str = "" + for i, device in enumerate(devices): + choices_str += (" {}: {}\n".format(device, device_names[i])) + console.print(choices_str) + dev_id = IntListPrompt.ask("Which GPU(s) would you like to use? Please list one, or comma-separated", choices=devices, default='All') + if dev_id.lower() == 'all': + dev_id = list(range(torch.cuda.device_count())) + else: + try: + # replace the commas with spaces then split over whitespace., + # then strip the whitespace and convert to ints. + dev_id = [int(dev_id.strip()) for dev_id in dev_id.replace(',', ' ').split()] + except ValueError: + console.log(":cross_mark:[red]Invalid GPU device[/red] [bold white]{}[/bold white]\nAvailable CUDA devices:{}".format(dev_id, choices_str)) + sys.exit(1) + config.subtensor.register.cuda.dev_id = dev_id + else: + # flag was not set, use default value. + if config.subtensor.register.cuda.get('use_cuda') is None: + config.subtensor.register.cuda.use_cuda = bittensor.defaults.subtensor.register.cuda.use_cuda def check_register_config( config: 'bittensor.Config' ): if config.subtensor.get('network') == bittensor.defaults.subtensor.network and not config.no_prompt: diff --git a/bittensor/_cli/cli_impl.py b/bittensor/_cli/cli_impl.py index 369da862be..de117d9a4e 100644 --- a/bittensor/_cli/cli_impl.py +++ b/bittensor/_cli/cli_impl.py @@ -246,8 +246,10 @@ def register( self ): TPB = self.config.subtensor.register.cuda.get('TPB', None), update_interval = self.config.subtensor.register.get('update_interval', None), num_processes = self.config.subtensor.register.get('num_processes', None), - cuda = self.config.subtensor.register.cuda.get('use_cuda', None), - dev_id = self.config.subtensor.register.cuda.get('dev_id', None) + cuda = self.config.subtensor.register.cuda.get('use_cuda', bittensor.defaults.subtensor.register.cuda.use_cuda), + dev_id = self.config.subtensor.register.cuda.get('dev_id', None), + output_in_place = self.config.subtensor.register.get('output_in_place', bittensor.defaults.subtensor.register.output_in_place), + log_verbose = self.config.subtensor.register.get('verbose', bittensor.defaults.subtensor.register.verbose), ) def transfer( self ): diff --git a/bittensor/_config/__init__.py b/bittensor/_config/__init__.py index 76b4eff293..a327ca451c 100644 --- a/bittensor/_config/__init__.py +++ b/bittensor/_config/__init__.py @@ -68,16 +68,16 @@ def __new__( cls, parser: ArgumentParser = None, strict: bool = False, args: Opt # this can fail if the --config has already been added. pass + # Get args from argv if not passed in. + if args == None: + args = sys.argv[1:] + # 1.1 Optionally load defaults if the --config is set. try: config_file_path = str(os.getcwd()) + '/' + vars(parser.parse_known_args(args)[0])['config'] except Exception as e: config_file_path = None - # Get args from argv if not passed in. - if args == None: - args = sys.argv[1:] - # Parse args not strict params = cls.__parse_args__(args=args, parser=parser, strict=False) diff --git a/bittensor/_subtensor/__init__.py b/bittensor/_subtensor/__init__.py index 8dd68c973a..eab11fd1cc 100644 --- a/bittensor/_subtensor/__init__.py +++ b/bittensor/_subtensor/__init__.py @@ -23,6 +23,8 @@ from substrateinterface import SubstrateInterface from torch.cuda import is_available as is_cuda_available +from bittensor.utils import strtobool_with_default + from . import subtensor_impl, subtensor_mock logger = logger.opt(colors=True) @@ -187,13 +189,17 @@ def add_args(cls, parser: argparse.ArgumentParser, prefix: str = None ): help='''The subtensor endpoint flag. If set, overrides the --network flag. ''') parser.add_argument('--' + prefix_str + 'subtensor._mock', action='store_true', help='To turn on subtensor mocking for testing purposes.', default=bittensor.defaults.subtensor._mock) - - parser.add_argument('--' + prefix_str + 'subtensor.register.num_processes', '-n', dest='subtensor.register.num_processes', help="Number of processors to use for registration", type=int, default=bittensor.defaults.subtensor.register.num_processes) + # registration args. Used for register and re-register and anything that calls register. + parser.add_argument('--' + prefix_str + 'subtensor.register.num_processes', '-n', dest=prefix_str + 'subtensor.register.num_processes', help="Number of processors to use for registration", type=int, default=bittensor.defaults.subtensor.register.num_processes) parser.add_argument('--' + prefix_str + 'subtensor.register.update_interval', '--' + prefix_str + 'subtensor.register.cuda.update_interval', '--' + prefix_str + 'cuda.update_interval', '-u', help="The number of nonces to process before checking for next block during registration", type=int, default=bittensor.defaults.subtensor.register.update_interval) - # registration args. Used for register and re-register and anything that calls register. - parser.add_argument( '--' + prefix_str + 'subtensor.register.cuda.use_cuda', '--' + prefix_str + 'cuda', '--' + prefix_str + 'cuda.use_cuda', default=argparse.SUPPRESS, help='''Set true to use CUDA.''', action='store_true', required=False ) - parser.add_argument( '--' + prefix_str + 'subtensor.register.cuda.dev_id', '--' + prefix_str + 'cuda.dev_id', type=int, nargs='+', default=argparse.SUPPRESS, help='''Set the CUDA device id(s). Goes by the order of speed. (i.e. 0 is the fastest).''', required=False ) + parser.add_argument('--' + prefix_str + 'subtensor.register.output_in_place', help="Whether to ouput the registration statistics in-place. Set flag to enable.", action='store_true', required=False, default=bittensor.defaults.subtensor.register.output_in_place) + parser.add_argument('--' + prefix_str + 'subtensor.register.verbose', help="Whether to ouput the registration statistics verbosely.", action='store_true', required=False, default=bittensor.defaults.subtensor.register.verbose) + + ## Registration args for CUDA registration. + parser.add_argument( '--' + prefix_str + 'subtensor.register.cuda.use_cuda', '--' + prefix_str + 'cuda', '--' + prefix_str + 'cuda.use_cuda', default=argparse.SUPPRESS, help='''Set flag to use CUDA to register.''', action="store_true", required=False ) + parser.add_argument( '--' + prefix_str + 'subtensor.register.cuda.no_cuda', '--' + prefix_str + 'no_cuda', '--' + prefix_str + 'cuda.no_cuda', dest=prefix_str + 'subtensor.register.cuda.use_cuda', default=argparse.SUPPRESS, help='''Set flag to not use CUDA for registration''', action="store_false", required=False ) + parser.add_argument( '--' + prefix_str + 'subtensor.register.cuda.dev_id', '--' + prefix_str + 'cuda.dev_id', type=int, nargs='+', default=argparse.SUPPRESS, help='''Set the CUDA device id(s). Goes by the order of speed. (i.e. 0 is the fastest).''', required=False ) parser.add_argument( '--' + prefix_str + 'subtensor.register.cuda.TPB', '--' + prefix_str + 'cuda.TPB', type=int, default=bittensor.defaults.subtensor.register.cuda.TPB, help='''Set the number of Threads Per Block for CUDA.''', required=False ) except argparse.ArgumentError: @@ -212,12 +218,16 @@ def add_defaults(cls, defaults ): defaults.subtensor.register = bittensor.Config() defaults.subtensor.register.num_processes = os.getenv('BT_SUBTENSOR_REGISTER_NUM_PROCESSES') if os.getenv('BT_SUBTENSOR_REGISTER_NUM_PROCESSES') != None else None # uses processor count by default within the function defaults.subtensor.register.update_interval = os.getenv('BT_SUBTENSOR_REGISTER_UPDATE_INTERVAL') if os.getenv('BT_SUBTENSOR_REGISTER_UPDATE_INTERVAL') != None else 50_000 + defaults.subtensor.register.output_in_place = True + defaults.subtensor.register.verbose = False defaults.subtensor.register.cuda = bittensor.Config() defaults.subtensor.register.cuda.dev_id = [0] defaults.subtensor.register.cuda.use_cuda = False defaults.subtensor.register.cuda.TPB = 256 + + @staticmethod def check_config( config: 'bittensor.Config' ): assert config.subtensor @@ -225,7 +235,7 @@ def check_config( config: 'bittensor.Config' ): if config.subtensor.get('register') and config.subtensor.register.get('cuda'): assert all((isinstance(x, int) or isinstance(x, str) and x.isnumeric() ) for x in config.subtensor.register.cuda.get('dev_id', [])) - if config.subtensor.register.cuda.get('use_cuda', False): + if config.subtensor.register.cuda.get('use_cuda', bittensor.defaults.subtensor.register.cuda.use_cuda): try: import cubit except ImportError: diff --git a/bittensor/_subtensor/subtensor_impl.py b/bittensor/_subtensor/subtensor_impl.py index 9770c1d001..747826c59b 100644 --- a/bittensor/_subtensor/subtensor_impl.py +++ b/bittensor/_subtensor/subtensor_impl.py @@ -500,11 +500,13 @@ def register ( wait_for_finalization: bool = True, prompt: bool = False, max_allowed_attempts: int = 3, + output_in_place: bool = True, cuda: bool = False, dev_id: Union[List[int], int] = 0, TPB: int = 256, num_processes: Optional[int] = None, update_interval: Optional[int] = None, + log_verbose: bool = False, ) -> bool: r""" Registers the wallet to chain. Args: @@ -530,6 +532,8 @@ def register ( The number of processes to use to register. update_interval (int): The number of nonces to solve between updates. + log_verbose (bool): + If true, the registration process will log more information. Returns: success (bool): flag is true if extrinsic was finalized or uncluded in the block. @@ -556,9 +560,9 @@ def register ( if prompt: bittensor.__console__.error('CUDA is not available.') return False - pow_result = bittensor.utils.create_pow( self, wallet, cuda, dev_id, TPB, num_processes=num_processes, update_interval=update_interval ) + pow_result = bittensor.utils.create_pow( self, wallet, output_in_place, cuda, dev_id, TPB, num_processes=num_processes, update_interval=update_interval, log_verbose=log_verbose ) else: - pow_result = bittensor.utils.create_pow( self, wallet, num_processes=num_processes, update_interval=update_interval) + pow_result = bittensor.utils.create_pow( self, wallet, output_in_place, num_processes=num_processes, update_interval=update_interval, log_verbose=log_verbose ) # pow failed if not pow_result: diff --git a/bittensor/_wallet/__init__.py b/bittensor/_wallet/__init__.py index 3f83f6b40d..090b7c3054 100644 --- a/bittensor/_wallet/__init__.py +++ b/bittensor/_wallet/__init__.py @@ -23,6 +23,7 @@ import os import bittensor +from bittensor.utils import strtobool from . import wallet_impl, wallet_mock @@ -115,7 +116,7 @@ def add_args(cls, parser: argparse.ArgumentParser, prefix: str = None ): parser.add_argument('--' + prefix_str + 'wallet.hotkeys', '--' + prefix_str + 'wallet.exclude_hotkeys', required=False, action='store', default=bittensor.defaults.wallet.hotkeys, type=str, nargs='*', help='''Specify the hotkeys by name. (e.g. hk1 hk2 hk3)''') parser.add_argument('--' + prefix_str + 'wallet.all_hotkeys', required=False, action='store_true', default=bittensor.defaults.wallet.all_hotkeys, help='''To specify all hotkeys. Specifying hotkeys will exclude them from this all.''') - parser.add_argument('--' + prefix_str + 'wallet.reregister', required=False, default=bittensor.defaults.wallet.reregister, type=lambda x: bool(strtobool(x)), help='''Whether to reregister the wallet if it is not already registered.''') + parser.add_argument('--' + prefix_str + 'wallet.reregister', required=False, action='store', default=bittensor.defaults.wallet.reregister, type=strtobool, help='''Whether to reregister the wallet if it is not already registered.''') except argparse.ArgumentError as e: pass diff --git a/bittensor/_wallet/wallet_impl.py b/bittensor/_wallet/wallet_impl.py index 5749c487ce..a02cc1319c 100644 --- a/bittensor/_wallet/wallet_impl.py +++ b/bittensor/_wallet/wallet_impl.py @@ -246,16 +246,18 @@ def reregister( if not self.config.wallet.get('reregister'): sys.exit(0) - subtensor.register( - wallet = self, + self.register( + subtensor = subtensor, prompt = prompt, TPB = self.config.subtensor.register.cuda.get('TPB', None), update_interval = self.config.subtensor.register.cuda.get('update_interval', None), num_processes = self.config.subtensor.register.get('num_processes', None), - cuda = self.config.subtensor.register.cuda.get('use_cuda', None), + cuda = self.config.subtensor.register.cuda.get('use_cuda', bittensor.defaults.subtensor.register.cuda.use_cuda), dev_id = self.config.subtensor.register.cuda.get('dev_id', None), wait_for_inclusion = wait_for_inclusion, wait_for_finalization = wait_for_finalization, + output_in_place = self.config.subtensor.register.get('output_in_place', bittensor.defaults.subtensor.register.output_in_place), + log_verbose = self.config.subtensor.register.get('verbose', bittensor.defaults.subtensor.register.verbose), ) return self @@ -272,6 +274,8 @@ def register ( TPB: int = 256, num_processes: Optional[int] = None, update_interval: Optional[int] = None, + output_in_place: bool = True, + log_verbose: bool = False, ) -> 'bittensor.Wallet': """ Registers the wallet to chain. Args: @@ -297,6 +301,10 @@ def register ( The number of processes to use to register. update_interval (int): The number of nonces to solve between updates. + output_in_place (bool): + If true, the registration output is printed in-place. + log_verbose (bool): + If true, the registration output is more verbose. Returns: success (bool): flag is true if extrinsic was finalized or uncluded in the block. @@ -309,11 +317,13 @@ def register ( wait_for_inclusion = wait_for_inclusion, wait_for_finalization = wait_for_finalization, prompt=prompt, max_allowed_attempts=max_allowed_attempts, + output_in_place = output_in_place, cuda=cuda, dev_id=dev_id, TPB=TPB, num_processes=num_processes, - update_interval=update_interval + update_interval=update_interval, + log_verbose=log_verbose, ) return self diff --git a/bittensor/utils/__init__.py b/bittensor/utils/__init__.py index 490dea5c96..ff0d9af119 100644 --- a/bittensor/utils/__init__.py +++ b/bittensor/utils/__init__.py @@ -9,7 +9,7 @@ import time from dataclasses import dataclass from queue import Empty -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Any, Dict, List, Optional, Tuple, Union, Callable import backoff import bittensor @@ -19,6 +19,8 @@ from Crypto.Hash import keccak from substrateinterface import Keypair from substrateinterface.utils import ss58 +from rich import console as rich_console, status as rich_status +from datetime import timedelta from .register_cuda import solve_cuda @@ -156,9 +158,6 @@ class SolverBase(multiprocessing.Process): The total number of processes running. update_interval: int The number of nonces to try to solve before checking for a new block. - best_queue: multiprocessing.Queue - The queue to put the best nonce the process has found during the pow solve. - New nonces are added each update_interval. time_queue: multiprocessing.Queue The queue to put the time the process took to finish each update_interval. Used for calculating the average time per update_interval across all processes. @@ -193,7 +192,6 @@ class SolverBase(multiprocessing.Process): proc_num: int num_proc: int update_interval: int - best_queue: Optional[multiprocessing.Queue] time_queue: multiprocessing.Queue solution_queue: multiprocessing.Queue newBlockEvent: multiprocessing.Event @@ -204,12 +202,11 @@ class SolverBase(multiprocessing.Process): check_block: multiprocessing.Lock limit: int - def __init__(self, proc_num, num_proc, update_interval, best_queue, time_queue, solution_queue, stopEvent, curr_block, curr_block_num, curr_diff, check_block, limit): + def __init__(self, proc_num, num_proc, update_interval, time_queue, solution_queue, stopEvent, curr_block, curr_block_num, curr_diff, check_block, limit): multiprocessing.Process.__init__(self) self.proc_num = proc_num self.num_proc = num_proc self.update_interval = update_interval - self.best_queue = best_queue self.time_queue = time_queue self.solution_queue = solution_queue self.newBlockEvent = multiprocessing.Event() @@ -264,7 +261,7 @@ class CUDASolver(SolverBase): TPB: int def __init__(self, proc_num, num_proc, update_interval, time_queue, solution_queue, stopEvent, curr_block, curr_block_num, curr_diff, check_block, limit, dev_id: int, TPB: int): - super().__init__(proc_num, num_proc, update_interval, None, time_queue, solution_queue, stopEvent, curr_block, curr_block_num, curr_diff, check_block, limit) + super().__init__(proc_num, num_proc, update_interval, time_queue, solution_queue, stopEvent, curr_block, curr_block_num, curr_diff, check_block, limit) self.dev_id = dev_id self.TPB = TPB @@ -327,8 +324,6 @@ def solve_for_nonce_block_cuda(solver: CUDASolver, nonce_start: int, update_inte def solve_for_nonce_block(solver: Solver, nonce_start: int, nonce_end: int, block_bytes: bytes, difficulty: int, limit: int, block_number: int) -> Tuple[Optional[POWSolution], int]: - best_local = float('inf') - best_seal_local = [0]*32 start = time.time() for nonce in range(nonce_start, nonce_end): # Create seal. @@ -345,12 +340,6 @@ def solve_for_nonce_block(solver: Solver, nonce_start: int, nonce_end: int, bloc # Found a solution, save it. return POWSolution(nonce, block_number, difficulty, seal), time.time() - start - if (product - limit) < best_local: - best_local = product - limit - best_seal_local = seal - - # Send best solution to best queue. - solver.best_queue.put((best_local, best_seal_local)) return None, time.time() - start @@ -372,6 +361,7 @@ def update_curr_block(curr_diff: multiprocessing.Array, curr_block: multiprocess curr_block[i] = block_bytes[i] registration_diff_pack(diff, curr_diff) + def get_cpu_count(): try: return len(os.sched_getaffinity(0)) @@ -379,7 +369,65 @@ def get_cpu_count(): # OSX does not have sched_getaffinity return os.cpu_count() -def solve_for_difficulty_fast( subtensor, wallet, num_processes: Optional[int] = None, update_interval: Optional[int] = None ) -> Optional[POWSolution]: +@dataclass +class RegistrationStatistics: + """Statistics for a registration.""" + time_spent_total: float + time_average_perpetual: float + rounds_total: int + time_average: float + time_spent: float + hash_rate_perpetual: float + hash_rate: float + difficulty: int + block_number: int + block_hash: bytes + + +class RegistrationStatisticsLogger: + """Logs statistics for a registration.""" + console: rich_console.Console + status: Optional[rich_status.Status] + + def __init__( self, console: rich_console.Console, output_in_place: bool = True) -> None: + self.console = console + + if output_in_place: + self.status = self.console.status("Solving") + else: + self.status = None + + def start( self ) -> None: + if self.status is not None: + self.status.start() + + def stop( self ) -> None: + if self.status is not None: + self.status.stop() + + + def get_status_message(cls, stats: RegistrationStatistics, verbose: bool = False) -> str: + message = f"""Solving + time spent: {timedelta(seconds=stats.time_spent)}""" + \ + (f""" + time spent total: {stats.time_spent_total:.2f} s + time average perpetual: {timedelta(seconds=stats.time_average_perpetual)} + """ if verbose else "") + f""" + Difficulty: [bold white]{millify(stats.difficulty)}[/bold white] + Iters: [bold white]{get_human_readable(int(stats.hash_rate), 'H')}/s[/bold white] + Block: [bold white]{stats.block_number}[/bold white] + Block_hash: [bold white]{stats.block_hash.encode('utf-8')}[/bold white]""" + return message.replace(" ", "") + + + def update( self, stats: RegistrationStatistics, verbose: bool = False ) -> None: + if self.status is not None: + self.status.update( self.get_status_message(stats, verbose=verbose) ) + else: + self.console.log( self.get_status_message(stats, verbose=verbose), ) + + +def solve_for_difficulty_fast( subtensor, wallet, output_in_place: bool = True, num_processes: Optional[int] = None, update_interval: Optional[int] = None, log_verbose: bool = False ) -> Optional[POWSolution]: """ Solves the POW for registration using multiprocessing. Args: @@ -387,10 +435,14 @@ def solve_for_difficulty_fast( subtensor, wallet, num_processes: Optional[int] = Subtensor to connect to for block information and to submit. wallet: Wallet to use for registration. + output_in_place: bool + If true, prints the status in place. Otherwise, prints the status on a new line. num_processes: int Number of processes to use. update_interval: int Number of nonces to solve before updating block information. + log_verbose: bool + If true, prints more verbose logging of the registration metrics. Note: - We can also modify the update interval to do smaller blocks of work, while still updating the block information after a different number of nonces, @@ -405,30 +457,21 @@ def solve_for_difficulty_fast( subtensor, wallet, num_processes: Optional[int] = limit = int(math.pow(2,256)) - 1 - console = bittensor.__console__ - status = console.status("Solving") - - best_seal: bytes - best_number: int - best_number = float('inf') - curr_block = multiprocessing.Array('h', 64, lock=True) # byte array curr_block_num = multiprocessing.Value('i', 0, lock=True) # int curr_diff = multiprocessing.Array('Q', [0, 0], lock=True) # [high, low] - - status.start() # Establish communication queues ## See the Solver class for more information on the queues. stopEvent = multiprocessing.Event() stopEvent.clear() - best_queue = multiprocessing.Queue() + solution_queue = multiprocessing.Queue() time_queue = multiprocessing.Queue() check_block = multiprocessing.Lock() # Start consumers - solvers = [ Solver(i, num_processes, update_interval, best_queue, time_queue, solution_queue, stopEvent, curr_block, curr_block_num, curr_diff, check_block, limit) + solvers = [ Solver(i, num_processes, update_interval, time_queue, solution_queue, stopEvent, curr_block, curr_block_num, curr_diff, check_block, limit) for i in range(num_processes) ] # Get first block @@ -449,11 +492,30 @@ def solve_for_difficulty_fast( subtensor, wallet, num_processes: Optional[int] = for w in solvers: w.start() # start the solver processes - start_time = time.time() + curr_stats = RegistrationStatistics( + time_spent_total = 0.0, + time_average_perpetual = 0.0, + time_average = 0.0, + rounds_total = 0, + time_spent = 0.0, + hash_rate_perpetual = 0.0, + hash_rate = 0.0, + difficulty = difficulty, + block_number = block_number, + block_hash = block_hash + ) + + start_time_perpetual = time.time() + + console = bittensor.__console__ + logger = RegistrationStatisticsLogger(console, output_in_place) + logger.start() + solution = None - best_seal = None - itrs_per_sec = 0 + while not wallet.is_registered(subtensor): + start_time = time.time() + time_avg: Optional[float] = None # Wait until a solver finds a solution try: solution = solution_queue.get(block=True, timeout=0.25) @@ -478,6 +540,11 @@ def solve_for_difficulty_fast( subtensor, wallet, num_processes: Optional[int] = # Set new block events for each solver for w in solvers: w.newBlockEvent.set() + + # update stats + curr_stats.block_number = block_number + curr_stats.block_hash = block_hash + curr_stats.difficulty = difficulty # Get times for each solver time_total = 0 @@ -493,31 +560,22 @@ def solve_for_difficulty_fast( subtensor, wallet, num_processes: Optional[int] = # Calculate average time per solver for the update_interval if num_time > 0: time_avg = time_total / num_time - itrs_per_sec = update_interval*num_processes / time_avg - - # get best solution from each solver using the best_queue - for _ in solvers: - try: - num, seal = best_queue.get_nowait() - if num < best_number: - best_number = num - best_seal = seal - - except Empty: - break + curr_stats.hash_rate = update_interval*num_processes / time_avg - message = f"""Solving - time spent: {time.time() - start_time} - Difficulty: [bold white]{millify(difficulty)}[/bold white] - Iters: [bold white]{get_human_readable(int(itrs_per_sec), 'H')}/s[/bold white] - Block: [bold white]{block_number}[/bold white] - Block_hash: [bold white]{block_hash.encode('utf-8')}[/bold white] - Best: [bold white]{binascii.hexlify(bytes(best_seal) if best_seal else bytes(0))}[/bold white]""" - status.update(message.replace(" ", "")) + curr_stats.time_spent = time.time() - start_time + new_time_spent_total = time.time() - start_time_perpetual + curr_stats.time_average = time_avg if not None else curr_stats.time_average + curr_stats.time_average_perpetual = (curr_stats.time_average_perpetual*curr_stats.rounds_total + curr_stats.time_spent)/(curr_stats.rounds_total+1) + curr_stats.rounds_total += 1 + curr_stats.hash_rate_perpetual = (curr_stats.time_spent_total*curr_stats.hash_rate_perpetual + curr_stats.hash_rate)/ new_time_spent_total + curr_stats.time_spent_total = new_time_spent_total + + # Update the logger + logger.update(curr_stats, verbose=log_verbose) # exited while, solution contains the nonce or wallet is registered stopEvent.set() # stop all other processes - status.stop() + logger.stop() return solution @@ -565,7 +623,7 @@ def __exit__(self, *args): multiprocessing.set_start_method(self._old_start_method, force=True) -def solve_for_difficulty_fast_cuda( subtensor: 'bittensor.Subtensor', wallet: 'bittensor.Wallet', update_interval: int = 50_000, TPB: int = 512, dev_id: Union[List[int], int] = 0, use_kernel_launch_optimization: bool = False ) -> Optional[POWSolution]: +def solve_for_difficulty_fast_cuda( subtensor: 'bittensor.Subtensor', wallet: 'bittensor.Wallet', output_in_place: bool = True, update_interval: int = 50_000, TPB: int = 512, dev_id: Union[List[int], int] = 0, log_verbose: bool = False ) -> Optional[POWSolution]: """ Solves the registration fast using CUDA Args: @@ -573,12 +631,16 @@ def solve_for_difficulty_fast_cuda( subtensor: 'bittensor.Subtensor', wallet: 'b The subtensor node to grab blocks wallet: bittensor.Wallet The wallet to register + output_in_place: bool + If true, prints the output in place, otherwise prints to new lines update_interval: int The number of nonces to try before checking for more blocks TPB: int The number of threads per block. CUDA param that should match the GPU capability dev_id: Union[List[int], int] The CUDA device IDs to execute the registration on, either a single device or a list of devices + log_verbose: bool + If true, prints more verbose logging of the registration metrics. """ if isinstance(dev_id, int): dev_id = [dev_id] @@ -593,11 +655,7 @@ def solve_for_difficulty_fast_cuda( subtensor: 'bittensor.Subtensor', wallet: 'b limit = int(math.pow(2,256)) - 1 - console = bittensor.__console__ - status = console.status("Solving") - # Set mp start to use spawn so CUDA doesn't complain - # Force the set start method in-case of re-register with UsingSpawnStartMethod(force=True): curr_block = multiprocessing.Array('h', 64, lock=True) # byte array curr_block_num = multiprocessing.Value('i', 0, lock=True) # int @@ -610,8 +668,6 @@ def update_curr_block(block_number: int, block_bytes: bytes, diff: int, lock: mu curr_block[i] = block_bytes[i] registration_diff_pack(diff, curr_diff) - status.start() - # Establish communication queues stopEvent = multiprocessing.Event() stopEvent.clear() @@ -633,6 +689,7 @@ def update_curr_block(block_number: int, block_bytes: bytes, diff: int, lock: mu block_hash = subtensor.substrate.get_block_hash( block_number ) block_bytes = block_hash.encode('utf-8')[2:] old_block_number = block_number + # Set to current block update_curr_block(block_number, block_bytes, difficulty, check_block) @@ -643,11 +700,30 @@ def update_curr_block(block_number: int, block_bytes: bytes, diff: int, lock: mu for w in solvers: w.start() # start the solver processes - start_time = time.time() - time_since = 0.0 + curr_stats = RegistrationStatistics( + time_spent_total = 0.0, + time_average_perpetual = 0.0, + time_average = 0.0, + rounds_total = 0, + time_spent = 0.0, + hash_rate_perpetual = 0.0, + hash_rate = 0.0, + difficulty = difficulty, + block_number = block_number, + block_hash = block_hash + ) + + start_time_perpetual = time.time() + + console = bittensor.__console__ + logger = RegistrationStatisticsLogger(console, output_in_place) + logger.start() + solution = None - itrs_per_sec = 0 + while not wallet.is_registered(subtensor): + start_time = time.time() + time_avg: Optional[float] = None # Wait until a solver finds a solution try: solution = solution_queue.get(block=True, timeout=0.15) @@ -657,8 +733,6 @@ def update_curr_block(block_number: int, block_bytes: bytes, diff: int, lock: mu # No solution found, try again pass - # check for new block - block_number = subtensor.get_current_block() if block_number != old_block_number: old_block_number = block_number # update block information @@ -672,13 +746,18 @@ def update_curr_block(block_number: int, block_bytes: bytes, diff: int, lock: mu # Set new block events for each solver for w in solvers: w.newBlockEvent.set() + + # update stats + curr_stats.block_number = block_number + curr_stats.block_hash = block_hash + curr_stats.difficulty = difficulty # Get times for each solver time_total = 0 num_time = 0 for _ in solvers: try: - time_ = time_queue.get_nowait() + time_ = time_queue.get(timeout=0.01) time_total += time_ num_time += 1 @@ -687,32 +766,48 @@ def update_curr_block(block_number: int, block_bytes: bytes, diff: int, lock: mu if num_time > 0: time_avg = time_total / num_time - itrs_per_sec = TPB*update_interval*num_processes / time_avg - time_since = time.time() - start_time + curr_stats.hash_rate = TPB*update_interval*num_processes / time_avg - message = f"""Solving - time spent: {time_since} - Difficulty: [bold white]{millify(difficulty)}[/bold white] - Iters: [bold white]{get_human_readable(int(itrs_per_sec), 'H')}/s[/bold white] - Block: [bold white]{block_number}[/bold white] - Block_hash: [bold white]{block_hash.encode('utf-8')}[/bold white]""" - status.update(message.replace(" ", "")) + curr_stats.time_spent = time.time() - start_time + new_time_spent_total = time.time() - start_time_perpetual + curr_stats.time_average = time_avg if not None else curr_stats.time_average + curr_stats.time_average_perpetual = (curr_stats.time_average_perpetual*curr_stats.rounds_total + curr_stats.time_spent)/(curr_stats.rounds_total+1) + curr_stats.rounds_total += 1 + curr_stats.hash_rate_perpetual = (curr_stats.time_spent_total*curr_stats.hash_rate_perpetual + curr_stats.hash_rate)/ new_time_spent_total + curr_stats.time_spent_total = new_time_spent_total + + # Update the logger + logger.update(curr_stats, verbose=log_verbose) # exited while, found_solution contains the nonce or wallet is registered if solution is not None: stopEvent.set() # stop all other processes - status.stop() + logger.stop() return solution - status.stop() + logger.stop() return None -def create_pow( subtensor, wallet, cuda: bool = False, dev_id: Union[List[int], int] = 0, tpb: int = 256, num_processes: int = None, update_interval: int = None) -> Optional[Dict[str, Any]]: +def create_pow( + subtensor, + wallet, + output_in_place: bool = True, + cuda: bool = False, + dev_id: Union[List[int], int] = 0, + tpb: int = 256, + num_processes: int = None, + update_interval: int = None, + log_verbose: bool = False + ) -> Optional[Dict[str, Any]]: if cuda: - solution: POWSolution = solve_for_difficulty_fast_cuda( subtensor, wallet, dev_id=dev_id, TPB=tpb, update_interval=update_interval ) + solution: POWSolution = solve_for_difficulty_fast_cuda( subtensor, wallet, output_in_place=output_in_place, \ + dev_id=dev_id, TPB=tpb, update_interval=update_interval, log_verbose=log_verbose + ) else: - solution: POWSolution = solve_for_difficulty_fast( subtensor, wallet, num_processes=num_processes, update_interval=update_interval ) + solution: POWSolution = solve_for_difficulty_fast( subtensor, wallet, output_in_place=output_in_place, \ + num_processes=num_processes, update_interval=update_interval, log_verbose=log_verbose + ) return None if solution is None else { 'nonce': solution.nonce, @@ -800,3 +895,34 @@ def is_valid_bittensor_address_or_public_key( address: Union[str, bytes] ) -> bo else: # Invalid address type return False + +def strtobool_with_default( default: bool ) -> Callable[[str], bool]: + """ + Creates a strtobool function with a default value. + + Args: + default(bool): The default value to return if the string is empty. + + Returns: + The strtobool function with the default value. + """ + return lambda x: strtobool(x) if x != "" else default + + +def strtobool(val: str) -> bool: + """ + Converts a string to a boolean value. + + truth-y values are 'y', 'yes', 't', 'true', 'on', and '1'; + false-y values are 'n', 'no', 'f', 'false', 'off', and '0'. + + Raises ValueError if 'val' is anything else. + """ + val = val.lower() + if val in ('y', 'yes', 't', 'true', 'on', '1'): + return True + elif val in ('n', 'no', 'f', 'false', 'off', '0'): + return False + else: + raise ValueError("invalid truth value %r" % (val,)) + diff --git a/tests/integration_tests/test_cli.py b/tests/integration_tests/test_cli.py index 4b8a7985d4..73ad97227e 100644 --- a/tests/integration_tests/test_cli.py +++ b/tests/integration_tests/test_cli.py @@ -1086,7 +1086,7 @@ def test_register( self ): with patch('bittensor.Subtensor.register', return_value=True): cli = bittensor.cli(config) cli.run() - + def test_stake( self ): wallet = TestCli.generate_wallet() bittensor.Subtensor.neuron_for_pubkey = MagicMock(return_value=self.mock_neuron) @@ -1327,33 +1327,72 @@ def test_list_no_wallet( self ): # This shouldn't raise an error anymore cli.run() -def test_btcli_help(): - """ - Verify the correct help text is output when the --help flag is passed - """ - with pytest.raises(SystemExit) as pytest_wrapped_e: - with patch('argparse.ArgumentParser._print_message', return_value=None) as mock_print_message: - args = [ - '--help' + def test_btcli_help(self): + """ + Verify the correct help text is output when the --help flag is passed + """ + with pytest.raises(SystemExit) as pytest_wrapped_e: + with patch('argparse.ArgumentParser._print_message', return_value=None) as mock_print_message: + args = [ + '--help' + ] + bittensor.cli(args=args).run() + + # Should try to print help + mock_print_message.assert_called_once() + + call_args = mock_print_message.call_args + args, _ = call_args + help_out = args[0] + + # Expected help output even if parser isn't working well + ## py3.6-3.9 or py3.10+ + assert 'optional arguments' in help_out or 'options' in help_out + # Expected help output if all commands are listed + assert 'positional arguments' in help_out + # Verify that cli is printing the help message for + assert 'overview' in help_out + assert 'run' in help_out + + + def test_register_cuda_use_cuda_flag(self): + class ExitEarlyException(Exception): + """Raised by mocked function to exit early""" + pass + + base_args = [ + "register", + "--subtensor._mock", + "--subtensor.network", "mock", + "--wallet.path", "tmp/walletpath", + "--wallet.name", "mock", + "--wallet.hotkey", "hk0", + "--no_prompt", + "--cuda.dev_id", "0", ] - bittensor.cli(args=args).run() - # Should try to print help - mock_print_message.assert_called_once() + with patch('torch.cuda.is_available', return_value=True): + with patch('bittensor.Subtensor.register', side_effect=ExitEarlyException): + # Should be able to set true without argument + args = base_args + [ + "--subtensor.register.cuda.use_cuda", # should be True without any arugment + ] + with pytest.raises(ExitEarlyException): + cli = bittensor.cli(args=args) + cli.run() + + assert cli.config.subtensor.register.cuda.get('use_cuda') == True # should be None - call_args = mock_print_message.call_args - args, _ = call_args - help_out = args[0] + # Should be able to set to false with no argument - # Expected help output even if parser isn't working well - ## py3.6-3.9 or py3.10+ - assert 'optional arguments' in help_out or 'options' in help_out - # Expected help output if all commands are listed - assert 'positional arguments' in help_out - # Verify that cli is printing the help message for - assert 'overview' in help_out - assert 'run' in help_out + args = base_args + [ + "--subtensor.register.cuda.no_cuda", + ] + with pytest.raises(ExitEarlyException): + cli = bittensor.cli(args=args) + cli.run() + assert cli.config.subtensor.register.cuda.use_cuda == False class TestCLIUsingArgs(unittest.TestCase): """ Test the CLI by passing args directly to the bittensor.cli factory diff --git a/tests/unit_tests/bittensor_tests/test_wallet.py b/tests/unit_tests/bittensor_tests/test_wallet.py index 660eb5bf99..2ff6177558 100644 --- a/tests/unit_tests/bittensor_tests/test_wallet.py +++ b/tests/unit_tests/bittensor_tests/test_wallet.py @@ -16,7 +16,7 @@ # DEALINGS IN THE SOFTWARE. import unittest -from unittest.mock import patch +from unittest.mock import patch, MagicMock import pytest import bittensor @@ -94,3 +94,131 @@ def test_regen_hotkey_from_hex_seed_str(self): seed_str_bad = "0x659c024d5be809000d0d93fe378cfde020846150b01c49a201fc2a02041f763" # 1 character short with pytest.raises(ValueError): self.mock_wallet.regenerate_hotkey(seed=seed_str_bad) + +class TestWalletReregister(unittest.TestCase): + def test_wallet_reregister_use_cuda_flag_none(self): + config = bittensor.Config() + config.wallet = bittensor.Config() + config.wallet.reregister = True + + config.subtensor = bittensor.Config() + config.subtensor.register = bittensor.Config() + config.subtensor.register.cuda = bittensor.Config() + config.subtensor.register.cuda.use_cuda = None # don't set the argument, but do specify the flag + # No need to specify the other config options as they are default to None + + mock_wallet = bittensor.wallet.mock() + mock_wallet.config = config + + class MockException(Exception): + pass + + def exit_early(*args, **kwargs): + raise MockException('exit_early') + + with patch('bittensor.Subtensor.register', side_effect=exit_early) as mock_register: + # Should be able to set without argument + with pytest.raises(MockException): + mock_wallet.reregister() + + call_args = mock_register.call_args + _, kwargs = call_args + + mock_register.assert_called_once() + self.assertEqual(kwargs['cuda'], None) # should be None when no argument, but flag set + + def test_wallet_reregister_use_cuda_flag_true(self): + config = bittensor.Config() + config.wallet = bittensor.Config() + config.wallet.reregister = True + + config.subtensor = bittensor.Config() + config.subtensor.register = bittensor.Config() + config.subtensor.register.cuda = bittensor.Config() + config.subtensor.register.cuda.use_cuda = True + config.subtensor.register.cuda.dev_id = 0 + # No need to specify the other config options as they are default to None + + mock_wallet = bittensor.wallet.mock() + mock_wallet.config = config + + class MockException(Exception): + pass + + def exit_early(*args, **kwargs): + raise MockException('exit_early') + + with patch('bittensor.Subtensor.register', side_effect=exit_early) as mock_register: + # Should be able to set without argument + with pytest.raises(MockException): + mock_wallet.reregister() + + call_args = mock_register.call_args + _, kwargs = call_args + + mock_register.assert_called_once() + self.assertEqual(kwargs['cuda'], True) # should be default when no argument + + def test_wallet_reregister_use_cuda_flag_false(self): + config = bittensor.Config() + config.wallet = bittensor.Config() + config.wallet.reregister = True + + config.subtensor = bittensor.Config() + config.subtensor.register = bittensor.Config() + config.subtensor.register.cuda = bittensor.Config() + config.subtensor.register.cuda.use_cuda = False + config.subtensor.register.cuda.dev_id = 0 + # No need to specify the other config options as they are default to None + + mock_wallet = bittensor.wallet.mock() + mock_wallet.config = config + + class MockException(Exception): + pass + + def exit_early(*args, **kwargs): + raise MockException('exit_early') + + with patch('bittensor.Subtensor.register', side_effect=exit_early) as mock_register: + # Should be able to set without argument + with pytest.raises(MockException): + mock_wallet.reregister() + + call_args = mock_register.call_args + _, kwargs = call_args + + mock_register.assert_called_once() + self.assertEqual(kwargs['cuda'], False) # should be default when no argument + + def test_wallet_reregister_use_cuda_flag_not_specified_false(self): + config = bittensor.Config() + config.wallet = bittensor.Config() + config.wallet.reregister = True + + config.subtensor = bittensor.Config() + config.subtensor.register = bittensor.Config() + config.subtensor.register.cuda = bittensor.Config() + #config.subtensor.register.cuda.use_cuda # don't specify the flag + config.subtensor.register.cuda.dev_id = 0 + # No need to specify the other config options as they are default to None + + mock_wallet = bittensor.wallet.mock() + mock_wallet.config = config + + class MockException(Exception): + pass + + def exit_early(*args, **kwargs): + raise MockException('exit_early') + + with patch('bittensor.Subtensor.register', side_effect=exit_early) as mock_register: + # Should be able to set without argument + with pytest.raises(MockException): + mock_wallet.reregister() + + call_args = mock_register.call_args + _, kwargs = call_args + + mock_register.assert_called_once() + self.assertEqual(kwargs['cuda'], False) # should be False when no flag was set