From 8818aa01c236de54b5127b073c618de1de6775eb Mon Sep 17 00:00:00 2001 From: distributedstatemachine! Date: Mon, 30 Dec 2024 10:13:29 +0000 Subject: [PATCH 01/15] feat demo + docker+ watchtower --- .env.example | 1 + .github/workflows/docker.yml | 57 + .gitignore | 57 +- README.md | 2 +- docker/Dockerfile | 40 + docker/compose.yml | 37 + docker/docker-compose-test.yml | 66 + hparams.json | 45 +- neurons/miner.py | 778 +++------- neurons/validator.py | 1061 ++++--------- pyproject.toml | 51 +- run.py | 395 +++++ scripts/docker_run | 55 + scripts/entrypoint.sh | 61 + src/tplr/__init__.py | 34 + src/tplr/autoupdate.py | 347 +++++ src/tplr/chain.py | 457 ++++++ src/tplr/comms.py | 612 ++++++++ src/tplr/compress.py | 306 ++++ src/tplr/config.py | 46 + src/tplr/dataset.py | 512 +++++++ src/tplr/hparams.py | 139 ++ src/tplr/logging.py | 99 ++ src/tplr/schemas.py | 45 + src/tplr/wandb.py | 92 ++ start.sh | 6 + uv.lock | 2629 ++++++++++++++++++++++++++++++++ 27 files changed, 6614 insertions(+), 1416 deletions(-) create mode 100644 .env.example create mode 100644 .github/workflows/docker.yml create mode 100644 docker/Dockerfile create mode 100644 docker/compose.yml create mode 100644 docker/docker-compose-test.yml create mode 100644 run.py create mode 100644 scripts/docker_run create mode 100644 scripts/entrypoint.sh create mode 100644 src/tplr/__init__.py create mode 100644 src/tplr/autoupdate.py create mode 100644 src/tplr/chain.py create mode 100644 src/tplr/comms.py create mode 100644 src/tplr/compress.py create mode 100644 src/tplr/config.py create mode 100644 src/tplr/dataset.py create mode 100644 src/tplr/hparams.py create mode 100644 src/tplr/logging.py create mode 100644 src/tplr/schemas.py create mode 100644 src/tplr/wandb.py create mode 100755 start.sh create mode 100644 uv.lock diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8f350e1 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +WANDB_API_KEY=your_api_key_here \ No newline at end of file diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..34c0585 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,57 @@ +name: Docker Build and Publish + +on: + release: + types: [published] + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: tplr-ai/templar + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }} + type=sha,prefix= + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./docker/Dockerfile + push: true + platforms: linux/amd64,linux/arm64 + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max \ No newline at end of file diff --git a/.gitignore b/.gitignore index 77c70eb..1443233 100644 --- a/.gitignore +++ b/.gitignore @@ -1,53 +1,18 @@ -.env.yaml -.env -*.pth - -# Python +# Python-generated files __pycache__/ -*.py[cod] -*$py.class -*.so -.Python +*.py[oc] build/ -develop-eggs/ dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ wheels/ -*.egg-info/ -.installed.cfg -*.egg +wandb/ +wandb/* +*.egg-info -# UV specific -.uv/ -.venv/ -venv/ -.env/ -uv.lock +# Virtual environments +.venv -# IDE -.idea/ -.vscode/ -*.swp -*.swo -.DS_Store +.env.yaml + +.env -# Project specific -wandb -checkpoint-* -checkpoints.ipynb -analysis.ipynb -scratch* -bad_slices/ -good_slices -slices -foo* -# Evals -lm-evaluation-harness -models \ No newline at end of file +logs \ No newline at end of file diff --git a/README.md b/README.md index 099c936..ce1433e 100644 --- a/README.md +++ b/README.md @@ -21,4 +21,4 @@ Documentation: --> + --> \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..8711fb7 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,40 @@ +# Use NVIDIA CUDA base image +FROM nvidia/cuda:12.6.0-runtime-ubuntu22.04 + +# Set working directory +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + python3 \ + python3-pip \ + python3-venv \ + git \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Install uv +RUN pip install uv + +# Copy project files +COPY . . + +# Install dependencies using uv +RUN uv sync + +# Environment variables that can be passed +ENV WALLET_NAME="" +ENV WALLET_HOTKEY="" +ENV CUDA_DEVICE="cuda:0" +ENV NETWORK="test" +ENV DEBUG="false" +ENV NODE_TYPE="" + +# Make entrypoint script executable +RUN chmod +x /app/scripts/entrypoint.sh + +# Create volumes for persistence +VOLUME ["/root/.bittensor/wallets", "/app/logs"] + +# Set entrypoint +ENTRYPOINT ["/app/scripts/entrypoint.sh"] \ No newline at end of file diff --git a/docker/compose.yml b/docker/compose.yml new file mode 100644 index 0000000..f3fa1d9 --- /dev/null +++ b/docker/compose.yml @@ -0,0 +1,37 @@ +services: + node: + image: ghcr.io/tplr-ai/templar:latest + container_name: templar-${NODE_TYPE:-miner}-${WALLET_HOTKEY} + restart: unless-stopped + volumes: + - ~/.bittensor/wallets:/root/.bittensor/wallets + - ./logs:/app/logs + environment: + - NODE_TYPE=${NODE_TYPE:-miner} + - WALLET_NAME=${WALLET_NAME} + - WALLET_HOTKEY=${WALLET_HOTKEY} + - CUDA_DEVICE=${CUDA_DEVICE:-cuda:0} + - NETWORK=${NETWORK:-finney} + - DEBUG=${DEBUG:-false} + - WANDB_API_KEY=${WANDB_API_KEY} + deploy: + resources: + reservations: + devices: + - driver: nvidia + device_ids: [ '0', '1', '2' ] + capabilities: [ gpu ] + labels: + - "com.centurylinklabs.watchtower.enable=true" + + watchtower: + image: containrrr/watchtower + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - /root/.docker/config.json:/config.json + command: --interval 30 --cleanup --label-enable + restart: unless-stopped + environment: + - WATCHTOWER_CLEANUP=true + - WATCHTOWER_LABEL_ENABLE=true + - WATCHTOWER_INCLUDE_RESTARTING=true diff --git a/docker/docker-compose-test.yml b/docker/docker-compose-test.yml new file mode 100644 index 0000000..2509ccf --- /dev/null +++ b/docker/docker-compose-test.yml @@ -0,0 +1,66 @@ +services: + miner1: + build: . + container_name: templar-miner-M111 + volumes: + - ~/.bittensor/wallets:/root/.bittensor/wallets + - ./logs:/app/logs + environment: + - NODE_TYPE=miner + - WALLET_NAME=Bistro + - WALLET_HOTKEY=M111 + - CUDA_DEVICE=cuda:0 + - NETWORK=test + - DEBUG=true + - WANDB_API_KEY=${WANDB_API_KEY} + deploy: + resources: + reservations: + devices: + - driver: nvidia + device_ids: [ '0', '1', '2' ] + capabilities: [ gpu ] + + miner2: + build: . + container_name: templar-miner-M222 + volumes: + - ~/.bittensor/wallets:/root/.bittensor/wallets + - ./logs:/app/logs + environment: + - NODE_TYPE=miner + - WALLET_NAME=Bistro + - WALLET_HOTKEY=M222 + - CUDA_DEVICE=cuda:1 + - NETWORK=test + - DEBUG=true + - WANDB_API_KEY=${WANDB_API_KEY} + deploy: + resources: + reservations: + devices: + - driver: nvidia + device_ids: [ '0', '1', '2' ] + capabilities: [ gpu ] + + validator: + build: . + container_name: templar-validator-V11 + volumes: + - ~/.bittensor/wallets:/root/.bittensor/wallets + - ./logs:/app/logs + environment: + - NODE_TYPE=validator + - WALLET_NAME=Bistro + - WALLET_HOTKEY=V11 + - CUDA_DEVICE=cuda:2 + - NETWORK=test + - DEBUG=true + - WANDB_API_KEY=${WANDB_API_KEY} + deploy: + resources: + reservations: + devices: + - driver: nvidia + device_ids: [ '0', '1', '2' ] + capabilities: [ gpu ] diff --git a/hparams.json b/hparams.json index cf500ed..520eb88 100644 --- a/hparams.json +++ b/hparams.json @@ -1,36 +1,27 @@ { - "epoch_length": 250000, - "compression": 100, + "spec_version": 5, + "project": "dough", "sequence_length": 2048, + "pages_per_window": 2, + "batch_size": 6, + "learning_rate": 4e-4, + "blocks_per_window": 2, + "windows_per_sync": 100, + "windows_per_weights": 10, + "momentum_decay": 0.999, + "topk_compression": 32, + "target_chunk": 64, + "scores_alpha": 0.001, "tokenizer_name": "togethercomputer/LLaMA-2-7B-32K", - "num_hidden_layers": 16, "hidden_size": 2048, - "intermediate_size": 8192, + "num_hidden_layers": 16, "num_attention_heads": 8, + "intermediate_size": 8192, "num_key_value_heads": 8, "activation_function": "swiGLU", "max_position_embeddings": 2048, - "mixed_precision_param": "bfloat16", - "mixed_precision_reduce": "float32", - "window_length": 3, - "desired_batch_size": 512, - "learning_rate": 7e-5, - "optimizer_beta1": 0.9, - "optimizer_beta2": 0.95, - "optimizer_weight_decay": 0.1, - "grad_clip": 1.0, - "cosine_epoch_length": 5000, - "num_warmup_steps": 1000, - "num_stable_steps": 1000000000000, - "num_decay_steps": 5000, - "eta_min": 1e-05, - "max_history": 10, - "window_speed": 100, - "validator_moving_alpha": 0.85, - "validator_norm_regularization": 0.01, - "validator_weights_temperature": 10, - "validator_window_eval_size": 3, - "validator_sample_rate": 0.01, - "validator_non_submission_decay": 0.9, - "validator_learning_rate_scale_factor": 0.1 + "weight_decay": 0.1, + "warmup_steps": 250, + "alpha_f": 0.1, + "t_max": 20000 } \ No newline at end of file diff --git a/neurons/miner.py b/neurons/miner.py index 7383d4b..e17d42a 100644 --- a/neurons/miner.py +++ b/neurons/miner.py @@ -2,600 +2,326 @@ # © 2024 templar.tech # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# documentation files (the "Software"), to deal in the Software without restriction, including without limitation # the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, # and to permit persons to whom the Software is furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in all copies or substantial portions of # the Software. -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO # THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. # fmt: off -# ruff: noqa - -# Global imports. +# Standard library +import os import sys -import time -import wandb -import torch +import time import random import asyncio import argparse import threading +from typing import Dict + +# Third party +import torch import numpy as np -from tqdm import tqdm import bittensor as bt -import torch.optim as optim +from torch.optim import SGD from transformers import LlamaForCausalLM -from rich.markup import escape -import os -import tempfile - -# Import local files. -import templar as tplr - -# GPU optimizations. -torch.backends.cudnn.benchmark = True +from torch.optim.lr_scheduler import ( + CosineAnnealingWarmRestarts, + LinearLR, + SequentialLR, +) + +# Local +import tplr + + +# GPU optimizations +torch.manual_seed(42) +torch.cuda.manual_seed_all(42) +np.random.seed(42) +random.seed(42) +torch.backends.cudnn.deterministic = True +torch.backends.cudnn.benchmark = False torch.backends.cuda.matmul.allow_tf32 = True torch.backends.cudnn.allow_tf32 = True -class Miner: +class Miner: + + # Command line config items. @staticmethod def config(): parser = argparse.ArgumentParser(description='Miner script') - parser.add_argument('--project', type=str, default='templar', help='Optional wandb project name') - parser.add_argument('--netuid', type=int, default=3, help='Bittensor network UID.') - parser.add_argument('--actual_batch_size', type=int, default=8, help='Training batch size per accumulation.') - parser.add_argument('--device', type=str, default='cuda', help='Device to use for training (e.g., cpu or cuda)') + parser.add_argument('--netuid', type=int, default=268, help='Bittensor network UID.') + parser.add_argument('--project', type=str, default='llama-demo-1', help='Wandb project.') + parser.add_argument('--device', type=str, default='cuda', help='Device to use for training') parser.add_argument('--debug', action='store_true', help='Enable debug logging') parser.add_argument('--trace', action='store_true', help='Enable trace logging') - parser.add_argument('--random', action='store_true', help='Train on random') - parser.add_argument('--sync_state', action='store_true', help='Syncs the model state by pulling from the history.') - parser.add_argument('--baseline', action='store_true', help='Dont perform syncing with other peers, just train.') - parser.add_argument('--test', action='store_true', help='Run on test network') - parser.add_argument('--local', action='store_true', help='Run on local network') - parser.add_argument('--no_autoupdate', action='store_true', help='Disable automatic updates') - parser.add_argument('--checkpoint_path', type=str, default=None, help='Path to save/load the checkpoint. If None, the path is set to checkpoint-M.pth.') - parser.add_argument('--save-location', type=str, default=None, help='Directory to save/load slice files') - bt.wallet.add_args(parser) + parser.add_argument('--use_wandb', action='store_true', help='Use Weights and Biases for logging') + parser.add_argument('--peers', type=int, nargs='+', default=[], help='List of UIDs to peer with') bt.subtensor.add_args(parser) + bt.logging.add_args(parser) + bt.wallet.add_args(parser) config = bt.config(parser) - if config.test: - config.subtensor.network = 'test' - config.subtensor.chain_endpoint = 'wss://test.finney.opentensor.ai:443/' - elif config.local: - config.subtensor.network = 'local' - config.subtensor.chain_endpoint = 'ws://127.0.0.1:9944' if config.debug: tplr.debug() if config.trace: tplr.trace() - if not config.no_autoupdate: - autoupdater = tplr.AutoUpdate() - autoupdater.daemon = True # Ensure thread exits when main program exits - autoupdater.start() return config - - + def __init__(self): - # Init config. + tplr.logger.debug("Starting initialization...") + + # Init config and load hparams self.config = Miner.config() - tplr.logger.info('\n' + '-' * 40 + ' Config ' + '-' * 40) - tplr.logger.info(self.config) - - # Init bittensor objects. + self.hparams = tplr.load_hparams() + + # Init bittensor objects self.wallet = bt.wallet(config=self.config) self.subtensor = bt.subtensor(config=self.config) - self.metagraph = self.subtensor.metagraph(netuid=self.config.netuid) - self.chain_manager = tplr.chain.ChainManager( - subtensor=self.subtensor, wallet=self.wallet, netuid=self.config.netuid - ) + self.metagraph = self.subtensor.metagraph(self.config.netuid) if self.wallet.hotkey.ss58_address not in self.metagraph.hotkeys: - tplr.logger.error(f'\n\t[bold]The wallet {self.wallet} is not registered on subnet: {self.metagraph.netuid}[/bold]. You need to register first with: [blue]`btcli subnet register`[/blue]\n') + tplr.logger.error(f'\n\t[bold]The wallet {self.wallet} is not registered on subnet: {self.metagraph.netuid}[/bold]') sys.exit() self.uid = self.metagraph.hotkeys.index(self.wallet.hotkey.ss58_address) - tplr.logger.info('\n' + '-' * 40 + ' Objects ' + '-' * 40) - tplr.logger.info(f'\nWallet: {self.wallet}\nSubtensor: {self.subtensor}\nMetagraph: {self.metagraph}\nUID: {self.uid}') - - # Set checkpoint path - if self.config.checkpoint_path is None: - # Default path if none provided - self.checkpoint_path = f"checkpoints/checkpoint-M{self.uid}.pth" - else: - self.checkpoint_path = self.config.checkpoint_path - - # Create checkpoint directory if it doesn't exist - os.makedirs(os.path.dirname(self.checkpoint_path), exist_ok=True) - - # Init bucket. - try: - tplr.logger.debug(f'bucket_name: {tplr.config.BUCKET_SECRETS["bucket_name"]}') - commitment = self.chain_manager.get_commitment(self.uid) - - # Convert Bucket object back to concatenated string format for comparison - commitment_str = commitment.name + commitment.access_key_id + commitment.secret_access_key - - current_bucket = ( - tplr.config.BUCKET_SECRETS["bucket_name"] + - tplr.config.BUCKET_SECRETS["read"]["access_key_id"] + - tplr.config.BUCKET_SECRETS["read"]["secret_access_key"] - ) - tplr.logger.debug(f'Comparing:\nCommitment: {commitment_str}\nCurrent: {current_bucket}') - - if current_bucket != commitment_str: - raise ValueError("Bucket commitment data does not match.") - - except Exception as e: - tplr.logger.error(f"Commitment error: {str(e)}") - tplr.commit(self.subtensor, self.wallet, self.config.netuid) - - # Init Wandb. - # Ensure the wandb directory exists - wandb_dir = os.path.join(os.getcwd(), 'wandb') - os.makedirs(wandb_dir, exist_ok=True) - - # Define the run ID file path inside the wandb directory - run_id_file = os.path.join(wandb_dir, f"wandb_run_id_M{self.uid}_{tplr.__version__}.txt") - - # Attempt to read the existing run ID - if os.path.exists(run_id_file): - with open(run_id_file, 'r') as f: - run_id = f.read().strip() - tplr.logger.info(f"Resuming WandB run with id {run_id}") - else: - run_id = None - tplr.logger.info("Starting a new WandB run.") - - # Initialize WandB - self.wandb = tplr.initialize_wandb( - run_prefix='M', - uid=self.uid, - config=self.config, - group='miner', - job_type='training' - ) - - # Init model. - tplr.logger.info('\n' + '-' * 40 + ' Hparams ' + '-' * 40) - self.hparams = tplr.load_hparams() - torch.manual_seed(42) - np.random.seed(42) - random.seed(42) - self.model = LlamaForCausalLM(config=self.hparams.model_config) + + # Init model with hparams config + self.model = LlamaForCausalLM(self.hparams.model_config) self.model.to(self.config.device) - self.model.train() - - self.optimizer = optim.AdamW( - self.model.parameters(), - lr=self.hparams.learning_rate, # Peak learning rate - betas=(self.hparams.optimizer_beta1, self.hparams.optimizer_beta2), # B1 and B2 - weight_decay=self.hparams.optimizer_weight_decay, # Weight decay - foreach=True, # more memory usage, but faster - ) - - # Initialize learning rate scheduler - self.scheduler = tplr.get_wsd_scheduler( - optimizer=self.optimizer, - num_warmup_steps=self.hparams.num_warmup_steps, - num_stable_steps=self.hparams.num_stable_steps, - num_decay_steps=self.hparams.num_decay_steps, - ) + self.tokenizer = self.hparams.tokenizer - # Retrieve bucket info for all neurons - self.buckets = tplr.get_all_buckets( - netuid=self.config.netuid, - metagraph=self.metagraph, - config= self.config + # Init optimizer and momentum + self.optimizer = SGD(self.model.parameters(), lr=self.hparams.learning_rate) + self.momentum = {} + for n, p in self.model.named_parameters(): + self.momentum[n] = torch.zeros_like(p) + + # Set up scheduler + warmup_scheduler = LinearLR( + self.optimizer, + start_factor=0.1, + end_factor=1.0, + total_iters=250, ) - - - - # Initialize checkpoint manager - self.checkpoint_manager = tplr.CheckpointManager( - model=self.model, - checkpoint_path=self.checkpoint_path, - wallet=self.wallet, - device=self.config.device, - optimizer=self.optimizer, - scheduler=self.scheduler + cosine_scheduler = CosineAnnealingWarmRestarts( + self.optimizer, + T_0=1000, + T_mult=2, + eta_min=self.hparams.learning_rate * 0.1, ) - - # Load initial checkpoint - self.global_step = asyncio.run( - self.checkpoint_manager.load_from_highest_stake( - metagraph=self.metagraph, - buckets=self.buckets, - optimizer=self.optimizer, - scheduler=self.scheduler, - is_validator=False, - hparams=self.hparams - ) + self.scheduler = SequentialLR( + self.optimizer, + schedulers=[warmup_scheduler, cosine_scheduler], + milestones=[250], ) - # Init run state. - self.sample_rate = 1.0 - self.current_block = self.subtensor.block - self.current_window = self.block_to_window( self.current_block ) - self.window_seeds = {self.current_window: self.window_to_seed( self.current_window) } - self.new_block_event = asyncio.Event() - self.new_window_event = asyncio.Event() - self.stop_event = asyncio.Event() - self.last_full_steps = self.hparams.desired_batch_size // self.config.actual_batch_size - if self.config.save_location is None: - # Default to system temp dir with unique neuron directory - self.save_location = os.path.join( - tempfile.gettempdir(), f"neuron_{self.wallet.hotkey.ss58_address}" - ) - else: - # Append neuron-specific directory to save_location - self.save_location = os.path.join( - self.config.save_location, f"neuron_{self.wallet.hotkey.ss58_address}" - ) - - # Create the directory if it doesn't exist - os.makedirs(self.save_location, exist_ok=True) - self.checkpoint_tasks = set() - print ( self.hparams ) - - async def update(self): - """Continuously updates the global state by polling every 10 minutes.""" - await asyncio.sleep(600) # Initial sleep before starting updates - while not self.stop_event.is_set(): - st = tplr.T() - await self.perform_update() - tplr.logger.info(f"{tplr.P(self.current_window, tplr.T() - st)} Updated global state.") - await asyncio.sleep(600) - - async def perform_update(self): - """Updates subtensor connection, metagraph, hyperparameters, and buckets.""" - self.subtensor = bt.subtensor(config=self.config) - self.metagraph = self.subtensor.metagraph(self.config.netuid) - + # Init compression + self.transformer = tplr.compress.TransformDCT( + self.model, + target_chunk=self.hparams.target_chunk, + ) + self.compressor = tplr.compress.CompressDCT() - # Fetch all commitments at once - buckets = tplr.get_all_commitments( - substrate=self.subtensor.substrate, + # Init comms + self.comms = tplr.comms.Comms( + wallet=self.wallet, + save_location='/tmp', + key_prefix='model', + config=self.config, netuid=self.config.netuid, - metagraph=self.metagraph + metagraph=self.metagraph, + hparams=self.hparams, ) - self.buckets = [] - for uid in self.metagraph.uids: - bucket = buckets.get(uid) - if isinstance(bucket, bytes): - bucket = bucket.decode('utf-8') - if bucket is not None: - tplr.logger.debug(f"UID {uid}: Valid bucket found: {bucket}") - self.buckets.append(bucket) - else: - tplr.logger.debug(f"UID {uid}: Invalid or missing bucket: {bucket}") - self.buckets.append(None) - - async def load_checkpoint_background(self): - """Handles checkpoint loading in the background.""" - try: - tplr.logger.info(f"Loading checkpoint at step {self.global_step}") - - # Load the checkpoint into a temporary model - temp_model = LlamaForCausalLM(config=self.hparams.model_config).to(self.config.device) - temp_optimizer = optim.AdamW( - temp_model.parameters(), - lr=self.hparams.learning_rate, - betas=(self.hparams.optimizer_beta1, self.hparams.optimizer_beta2), - weight_decay=self.hparams.optimizer_weight_decay, - foreach=True, - ) - temp_scheduler = tplr.get_wsd_scheduler( - optimizer=temp_optimizer, - num_warmup_steps=self.hparams.num_warmup_steps, - num_stable_steps=self.hparams.num_stable_steps, - num_decay_steps=self.hparams.num_decay_steps, - ) - temp_checkpoint_manager = tplr.CheckpointManager( - model=temp_model, - checkpoint_path=self.checkpoint_path, - wallet=self.wallet, - device=self.config.device, - optimizer=temp_optimizer, - scheduler=temp_scheduler - ) - - # Load the checkpoint from the highest stake - await temp_checkpoint_manager.load_from_highest_stake( - metagraph=self.metagraph, - buckets=self.buckets - ) - - # Safely update the main model's parameters - for param, temp_param in zip(self.model.parameters(), temp_model.parameters()): - param.data.copy_(temp_param.data) - - tplr.logger.info(f"Checkpoint loaded at step {self.global_step}") + # Init peers + if not self.config.peers: + self.peers = self.comms.peers + tplr.logger.info(f'Filtered peers with buckets: {self.peers}') + else: + self.peers = self.config.peers + if self.uid not in self.peers: + self.peers.append(self.uid) - # Clean up the temporary model to free memory - del temp_model, temp_optimizer, temp_scheduler, temp_checkpoint_manager - torch.cuda.empty_cache() + # Init state params + self.stop_event = asyncio.Event() + self.current_block = self.subtensor.block + self.current_window = int(self.current_block / self.hparams.blocks_per_window) - except Exception as e: - tplr.logger.error(f"Error loading checkpoint in background: {str(e)}") + # Init wandb + if self.config.use_wandb: + self.wandb = tplr.WandbManager( + uid=self.uid, + config=self.config, + is_validator=False, + ).run + # Main training loop. async def run(self): - # Main loop. - self.loop = asyncio.get_running_loop() - self.update_task = asyncio.create_task(self.update()) - self.listener = threading.Thread(target=self.block_listener, args=(self.loop,), daemon=True).start() - self.checkpoint_tasks = set() # Track checkpoint tasks - - # Optionally sync the model state by pulling model states from the history. - if self.config.sync_state: - st = tplr.T() - history_windows = [self.current_window - i for i in range(self.hparams.max_history - 1, -1, -1)] - for window in tqdm(history_windows, desc="Syncing state"): - max_global_step, _ = await tplr.apply_slices_to_model( - model = self.model, - window = window, - seed = window, - compression = self.hparams.compression, - save_location=self.save_location, - key = 'state' + # Try to load latest checkpoint + validator_uid, stake = self.comms.get_highest_stake_validator() + if stake > 0: + try: + state_dict = await self.comms.get( + uid=str(validator_uid), + window=self.current_window, + key='checkpoint', + timeout=240 ) - if max_global_step is not None: - self.global_step = max(self.global_step, max_global_step) - self.scheduler.last_epoch = self.global_step - 1 # Update scheduler - tplr.logger.info(f"{tplr.P(window, tplr.T() - st)}: Applied history and updated global step to {self.global_step}.") - torch.cuda.empty_cache() - try: - # Main training loop. - while True: - try: - # Start the window step. - tplr.logger.info('[bold]' + '\n' + '-' * 40 + f' Step: {self.global_step} ' + '-' * 40) - self.global_step += 1 - start_step = tplr.T() - window = self.current_window - - # Run for non-baseline miners. - if not self.config.baseline: - st = tplr.T() - valid_buckets = [b for b in self.buckets if b is not None] - - if not valid_buckets: - tplr.logger.info(f"No valid buckets to download state slices for window {window}") - # Wait for the next window - while self.current_window == window: - await asyncio.sleep(0.1) - continue - - state_slices = await tplr.download_slices_for_buckets_and_windows( - buckets=valid_buckets, - windows=[window], - key='state', - save_location=self.save_location - ) - - n_state_slices = len(state_slices[window]) if window in state_slices else 0 - tplr.logger.info(f"{tplr.P(window, tplr.T() - st)}: Downloaded {n_state_slices} window states.") - - # Download the delta from the previous window. - st = tplr.T() - delta_slices = await tplr.download_slices_for_buckets_and_windows( - buckets = self.buckets, - windows = [ window - 1 ], - key = 'delta', - save_location=self.save_location - ) - n_slices = len(delta_slices[ window - 1 ]) if window - 1 in delta_slices else 0 - tplr.logger.info(f"{tplr.P(window, tplr.T() - st)}: Download {n_slices} window deltas.") - - # Apply the state for the current window. - st = tplr.T() - max_global_step, _ = await tplr.apply_slices_to_model( - model=self.model, - window=window, - seed=window, - compression=self.hparams.compression, - save_location=self.save_location, - key='state' - ) - if max_global_step is not None: - self.global_step = max(self.global_step, max_global_step) - self.scheduler.last_epoch = self.global_step - 1 # Update scheduler - tplr.logger.info(f"{tplr.P(window, tplr.T() - st)}: Applied window state and updated global step to {self.global_step}.") + if state_dict is not None: + self.model.load_state_dict(state_dict) + tplr.logger.info(f"Loaded checkpoint from validator {validator_uid} at window {self.current_window}") + else: + tplr.logger.info("No checkpoint found, starting from scratch") + except Exception as e: + tplr.logger.warning(f"Failed to load checkpoint: {e}") + else: + tplr.logger.info("No active validators found, starting from scratch") - # Download the page for the current window. - st = tplr.T() - pages = await tplr.dataset.DatasetLoader.next_pages( - offset = window, - n_pages = self.hparams.validator_window_eval_size, - seed = self.uid if not self.config.random else random.randint(0, 1000) - ) - random.shuffle( pages ) - dataset = await tplr.dataset.DatasetLoader.create( - batch_size = self.config.actual_batch_size, - sequence_length = self.hparams.sequence_length, - pages_info = pages, - tokenizer = self.hparams.tokenizer + # Start background block listener + self.loop = asyncio.get_running_loop() + self.listener = threading.Thread( + target=self.block_listener, + args=(self.loop,), + daemon=True, + ).start() + + while True: + step_window = self.current_window + tplr.logger.info(f"\n{'-' * 40} Window: {step_window} {'-' * 40}") + + # Get the pages for this window. + pages = await tplr.dataset.DatasetLoader.next_pages( + offset = step_window, + n_pages = self.hparams.pages_per_window, + seed = self.metagraph.hotkeys[self.uid] + ) + loader = await tplr.dataset.DatasetLoader.create( + batch_size = self.hparams.batch_size, + sequence_length = self.hparams.sequence_length, + pages_info = pages, + tokenizer = self.tokenizer + ) + tplr.logger.info(f"Pages: {[p[1] for p in pages]} for UID: {self.config.uid} and Window: {step_window}") + + # Accumulate gradient. + start_time = time.time() + tplr.logger.info("Start accumulating...") + self.optimizer.zero_grad() + self.model.zero_grad() + total_loss = 0 + for i, batch in enumerate(loader): + input_ids = torch.tensor(batch, dtype=torch.long).to(self.model.device) + labels = input_ids.clone() + labels = torch.where(labels == self.tokenizer.pad_token_id, -100, labels) + with torch.amp.autocast(device_type=self.model.device.type, dtype=torch.bfloat16): + outputs = self.model(input_ids=input_ids, labels=labels) + total_loss += outputs.loss.item() + outputs.loss.backward() + tplr.logger.info(f'loss: {outputs.loss.item()}') + if self.current_window != step_window: + tplr.logger.info('') + break + tplr.logger.info(f"Stopped accumulating: {i+1} batches with {(i+1) * self.hparams.batch_size * self.hparams.sequence_length} tokens") + duration = time.time() - start_time + + # Log metrics + if self.config.use_wandb: + self.wandb.log({ + "loss": total_loss/(i+1), + "tokens_per_sec": ((i+1) * self.hparams.batch_size * self.hparams.sequence_length)/duration + }) + + # Reduce gradient using DeMo. + gradient = {} + xshapes = {} + totalks = {} + transmitted = {} + for n, p in self.model.named_parameters(): + # Step-Weight decay + p.data.mul_(1.0 - self.scheduler.get_last_lr()[0] * self.hparams.weight_decay) + # Momentum decay + self.momentum[n].mul_(self.hparams.momentum_decay) + # Add the grad to the momentum + self.momentum[n].add_(p.grad, alpha=self.scheduler.get_last_lr()[0]) + # Compress gradient + idxs, vals, xshape, totalk = self.compressor.compress( + self.transformer.encode(self.momentum[n]), + self.hparams.topk_compression + ) + # Estimate transmitted gradient + transmit_grad = self.transformer.decode( + self.compressor.decompress(p, idxs, vals, xshape, totalk) + ) + # Remove transmitted from delta + self.momentum[n].sub_(transmit_grad) + # Add to share_state + transmitted[n] = transmit_grad + gradient[n + 'idxs'] = idxs + gradient[n + 'vals'] = vals + xshapes[n] = xshape + totalks[n] = totalk + + # All-gather share state from peers + tplr.logger.info(f"Start gather: {self.peers}") + gather_result = await self.comms.gather( + state_dict=gradient, + my_uid=self.uid, + uids=self.peers, + window=step_window, + key='gradient', + timeout=5, + device=self.config.device + ) + + # Decompress state and apply grad + for n, p in self.model.named_parameters(): + # Decompress all gradients in batch form + new_grad = self.transformer.decode( + self.compressor.batch_decompress( + p, gather_result[n + 'idxs'], gather_result[n + 'vals'], + xshapes[n], totalks[n] ) - tplr.logger.info(f"{tplr.P(window, tplr.T() - st)}: Downloaded training page: [light_steel_blue]{[p[1] for p in pages]}[/light_steel_blue] random = {self.config.random}") - - # Accumualte gradients on the model applied to the base state. - train_start = tplr.T() - self.model.zero_grad() - self.model.eval() - total_loss = 0.0 - full_steps = 0 - total_steps = 0 - exhausted_window = False - for batch in dataset: - total_steps += 1 - if random.random() < self.sample_rate and not exhausted_window: - full_steps += 1 - input_ids = torch.tensor(batch, dtype=torch.long).to(self.model.device) - labels = input_ids.clone() - labels = torch.where(labels == self.hparams.tokenizer.pad_token_id, -100, labels) - with torch.amp.autocast(device_type=self.model.device.type, dtype=torch.bfloat16): # Enable autocasting - outputs = self.model(input_ids=input_ids, labels=labels) - total_loss += outputs.loss.item() - outputs.loss.backward() - if window != self.current_window and not self.config.baseline: - exhausted_window = True - continue - if self.hparams.grad_clip: - torch.nn.utils.clip_grad_norm_(self.model.parameters(), self.hparams.grad_clip) - self.optimizer.step() - self.scheduler.step() - self.optimizer.zero_grad() - torch.cuda.empty_cache() - step_loss = total_loss/(full_steps+1) - train_duration = tplr.T() - train_start - tokens_per_step = self.hparams.sequence_length * self.config.actual_batch_size * (full_steps + 1) - tokens_per_second = tokens_per_step / train_duration - tplr.logger.info(f"{tplr.P(window, train_duration)} Accumulated gradients:") - tplr.logger.info(f"{tplr.P(window, train_duration)} \tTotal steps: [tan]{full_steps}/{total_steps}[/tan], Rate: [tan]{(full_steps/total_steps):.2f}[/tan], Target: [tan]{self.sample_rate:.2f}[/tan]") - tplr.logger.info(f"{tplr.P(window, train_duration)} \tTotal tokens: [tan]{tokens_per_step}[/tan], Tokens per second: [tan]{tokens_per_second:.2f}[/tan]") - tplr.logger.info(f"{tplr.P(window, train_duration)} \tLoss: [tan]{step_loss}[tan]") - if exhausted_window: - self.sample_rate = max(0.0001, self.sample_rate * 0.95) - else: - self.sample_rate = min(1, self.sample_rate * 1.05) - - # Run for non-baseline nodes. - if not self.config.baseline: - # Upload the delta for the previous window. - st = tplr.T() - slice_metric = { - "batch_size": self.config.actual_batch_size, - "tokens_per_step": tokens_per_step, - "tokens_per_second": tokens_per_second, - "loss": step_loss, - "sample_rate": self.sample_rate, - "learning_rate": self.scheduler.get_last_lr()[0], - } - await tplr.upload_slice_for_window( - bucket = tplr.config.BUCKET_SECRETS["bucket_name"], - model = self.model, - window = window, - seed = window, - wallet = self.wallet, - compression = self.hparams.compression, - save_location = self.save_location, - key = 'delta', - global_step = self.global_step, - slice_metric = slice_metric, - ) - tplr.logger.info(f"{tplr.P(window, tplr.T() - st)}: Uploaded the delta.") - - # Apply the delta from the previous window. - st = tplr.T() - max_global_step, _ = await tplr.apply_slices_to_model( - model=self.model, - window=window - 1, - seed=window - 1, - compression=self.hparams.compression, - save_location=self.save_location, - key='delta' - ) - if max_global_step is not None: - self.global_step = max(self.global_step, max_global_step) - self.scheduler.last_epoch = self.global_step - 1 # Update scheduler - tplr.logger.info(f"{tplr.P(window, tplr.T() - st)}: Applied window delta and updated global step to {self.global_step}.") - - # Upload the state for the current window. - st = tplr.T() - await tplr.upload_slice_for_window( - bucket = tplr.config.BUCKET_SECRETS["bucket_name"], - model = self.model, - window = window + 1, - seed = window + 1, - wallet = self.wallet, - compression = self.hparams.compression, - save_location = self.save_location, - key = 'state', - global_step = self.global_step - ) - tplr.logger.info(f"{tplr.P(window, tplr.T() - st)}: Uploaded the state.") - - # Clean file history. - st = tplr.T() - await tplr.delete_files_before_window(window_max=window - self.hparams.max_history, save_location=self.save_location, key='state') - await tplr.delete_files_before_window(window_max=window - self.hparams.max_history, save_location=self.save_location, key='delta') - await tplr.delete_files_from_bucket_before_window( bucket = tplr.config.BUCKET_SECRETS["bucket_name"], window_max = window - self.hparams.max_history, key = 'state' ) - await tplr.delete_files_from_bucket_before_window( bucket = tplr.config.BUCKET_SECRETS["bucket_name"], window_max = window - self.hparams.max_history, key = 'delta' ) - tplr.logger.info(f"{tplr.P(window, tplr.T() - st)}: Cleaned file history.") - - # Wait until we are on a new window. - end_step = tplr.T() - while self.current_window == window: - await asyncio.sleep(0.1) - window_time_delta = self.window_time - end_step - window_delta_str = f"[red]{window_time_delta:.2f}[/red]" if window_time_delta < 0 else f"[green]+{window_time_delta:.2f}[/green]" - tplr.logger.info(f"{tplr.P(window, end_step - start_step)}[{window_delta_str}]: Finished step.") - wandb.log({ - "miner/loss": step_loss, - "miner/tokens_per_step": tokens_per_step, - "miner/tokens_per_second": tokens_per_second, - "miner/sample_rate": self.sample_rate, - "miner/utilization": train_duration / (end_step - start_step), - "miner/learning_rate": self.scheduler.get_last_lr()[0] - }, step=self.global_step) - - # Catch keyboard interrrupt. - except KeyboardInterrupt: - tplr.logger.info("Training interrupted by user. Stopping the run.") - self.stop_event.set() - await self.update_task - sys.exit(0) - - - # Catch unknown. - except Exception as e: - message = f"Exception during training loop: {escape(str(e))}" - tplr.logger.exception(message) - continue - finally: - # Wait for any pending checkpoint tasks to complete - if self.checkpoint_tasks: - tplr.logger.info(f"Waiting for {len(self.checkpoint_tasks)} checkpoint tasks to complete...") - await asyncio.gather(*self.checkpoint_tasks) - self.checkpoint_manager.cleanup() - tplr.logger.info("Miner shutdown complete.") - - - # Returns the slice window based on a block. - def block_to_window(self, block: int) -> int: - return int( block / self.hparams.window_length ) # floor - - # Returns the slice window based on a blotplr. - def window_to_seed(self, window: int) -> int: - return str( self.subtensor.get_block_hash( window * self.hparams.window_length ) ) - - # A listener thread which posts the block event - # when the chain announces a new blotplr. + ) + # Set recomputed gathered gradient + if p.grad is None: + p.grad = new_grad + else: + p.grad.copy_(new_grad) + # Sign-SGD + p.grad.sign_() + + # Apply optimizer step + tplr.logger.info("Finish and step.") + self.optimizer.step() + self.scheduler.step() + if self.config.use_wandb: + self.wandb.log({"lr": self.scheduler.get_last_lr()[0]}) + + # Wait for end of window + tplr.logger.info("Wait for next window...") + while self.current_window == step_window: + await asyncio.sleep(0.1) + + # Listens for new blocks and sets self.current_block and self.current_window def block_listener(self, loop): def handler(event, _u, _s): self.current_block = int(event['header']['number']) - loop.call_soon_threadsafe(self.new_block_event.set) - if self.block_to_window(self.current_block) != self.current_window: - self.window_seeds[ self.block_to_window(self.current_block) ] = self.window_to_seed( self.block_to_window(self.current_block) ) - self.current_window = self.block_to_window(self.current_block) - self.window_duration = tplr.T() - self.window_time if hasattr(self, 'window_time') else 0 - self.window_time = tplr.T() - loop.call_soon_threadsafe(self.new_window_event.set) - tplr.logger.info(f"{tplr.P(self.current_window, self.window_duration)} New Window.") - # Run listener with retry. + if int(self.current_block / self.hparams.blocks_per_window) != self.current_window: + self.current_window = int(self.current_block / self.hparams.blocks_per_window) while not self.stop_event.is_set(): try: bt.subtensor(config=self.config).substrate.subscribe_block_headers(handler) break - except Exception as e: - tplr.logger.error(f"Failed to subscribe to block headers: {e}.\nRetrying in 1 seconds...") + except Exception: time.sleep(1) +# Start miner/validator. if __name__ == "__main__": asyncio.run(Miner().run()) diff --git a/neurons/validator.py b/neurons/validator.py index a7001ed..1183c8d 100644 --- a/neurons/validator.py +++ b/neurons/validator.py @@ -16,847 +16,352 @@ # DEALINGS IN THE SOFTWARE. # fmt: off -# Global imports. -import argparse -import asyncio -import bittensor as bt -import numpy as np -import os -import random +# Standard library import sys -import threading import time +import random +import asyncio +import argparse +import threading + +# Third party import torch -from tqdm import tqdm +import numpy as np +import bittensor as bt +from torch.optim import SGD from transformers import LlamaForCausalLM -import wandb -import wandb.plot -from asyncio import TimeoutError -from functools import partial -import tempfile +from torch.optim.lr_scheduler import ( + CosineAnnealingWarmRestarts, + LinearLR, + SequentialLR, +) -# Local imports. -import templar as tplr +# Local +import tplr # GPU optimizations. -torch.backends.cudnn.benchmark = True +torch.manual_seed(42) +torch.cuda.manual_seed_all(42) +np.random.seed(42) +random.seed(42) +torch.backends.cudnn.deterministic = True +torch.backends.cudnn.benchmark = False torch.backends.cuda.matmul.allow_tf32 = True torch.backends.cudnn.allow_tf32 = True class Validator: - @staticmethod def config(): parser = argparse.ArgumentParser(description='Validator script') - parser.add_argument('--project', type=str, default='templar', help='Optional wandb project name') - parser.add_argument('--netuid', type=int, default=3, help='Bittensor network UID.') - parser.add_argument('--actual_batch_size', type=int, default=8, help='Training batch size per accumulation.') - parser.add_argument('--device', type=str, default='cuda', help='Device to use for training (e.g., cpu or cuda)') - parser.add_argument('--use_wandb', action='store_true', help='Use Weights and Biases for logging') + parser.add_argument('--netuid', type=int, default=268, help='Bittensor network UID.') + parser.add_argument('--project', type=str, default='llama-demo-1', help='Wandb project.') + parser.add_argument('--device', type=str, default='cuda', help='Device to use for training') parser.add_argument('--debug', action='store_true', help='Enable debug logging') parser.add_argument('--trace', action='store_true', help='Enable trace logging') - parser.add_argument('--sync_state', action='store_true', help='Syncs the model state by pulling from the history.') - parser.add_argument('--test', action='store_true', help='Run on test network') - parser.add_argument('--local', action='store_true', help='Run on local network') - parser.add_argument('--no_autoupdate', action='store_true', help='Disable automatic updates') - parser.add_argument("--process_name", type=str, help="The name of the PM2 process") - parser.add_argument('--checkpoint_path', type=str, default=None, help='Path to save/load the checkpoint. If None, the path is set to checkpoint-V.pth.') - parser.add_argument('--save-location', type=str, default=None, help='Directory to save/load slice files') - bt.wallet.add_args(parser) + parser.add_argument('--use_wandb', action='store_true', help='Use Weights and Biases for logging') + parser.add_argument('--peers', type=int, nargs='+', default=[], help='List of UIDs to peer with') bt.subtensor.add_args(parser) + bt.logging.add_args(parser) + bt.wallet.add_args(parser) config = bt.config(parser) - if config.test: - config.subtensor.network = 'test' - config.subtensor.chain_endpoint = 'wss://test.finney.opentensor.ai:443/' - elif config.local: - config.subtensor.network = 'local' - config.subtensor.chain_endpoint = 'ws://127.0.0.1:9944' if config.debug: tplr.debug() if config.trace: tplr.trace() - if not config.no_autoupdate: - autoupdater = tplr.AutoUpdate() - autoupdater.daemon = True # Ensure thread exits when main program exits - autoupdater.start() return config - + def __init__(self): - # Init config. + tplr.logger.debug("Starting initialization...") + + # Init config and load hparams self.config = Validator.config() - tplr.logger.info('\n' + '-' * 40 + ' Config ' + '-' * 40) - tplr.logger.info(self.config) - - # Init bittensor objects. + self.hparams = tplr.load_hparams() + + # Init bittensor objects self.wallet = bt.wallet(config=self.config) self.subtensor = bt.subtensor(config=self.config) - self.metagraph = self.subtensor.metagraph(netuid=self.config.netuid) + self.metagraph = self.subtensor.metagraph(self.config.netuid) if self.wallet.hotkey.ss58_address not in self.metagraph.hotkeys: - tplr.logger.error(f'\n\t[bold]The wallet {self.wallet} is not registered on subnet: {self.metagraph.netuid}[/bold]. You need to register first with: [blue]`btcli subnet register`[/blue]\n') + tplr.logger.error(f'\n\t[bold]The wallet {self.wallet} is not registered on subnet: {self.metagraph.netuid}[/bold]') sys.exit() self.uid = self.metagraph.hotkeys.index(self.wallet.hotkey.ss58_address) - self.chain_manager = tplr.chain.ChainManager( - subtensor=self.subtensor, wallet=self.wallet, netuid=self.config.netuid + + # Init model with hparams config + self.model = LlamaForCausalLM(self.hparams.model_config) + self.model.to(self.config.device) + self.tokenizer = self.hparams.tokenizer + + # Init compression + self.transformer = tplr.compress.TransformDCT( + self.model, + target_chunk=self.hparams.target_chunk ) - tplr.logger.info('\n' + '-' * 40 + ' Objects ' + '-' * 40) - tplr.logger.info(f'\nWallet: {self.wallet}\nSubtensor: {self.subtensor}\nMetagraph: {self.metagraph}\nUID: {self.uid}') - - # Init bucket. - try: - tplr.logger.debug(f'bucket_name: {tplr.config.BUCKET_SECRETS["bucket_name"]}') - commitment = self.chain_manager.get_commitment(self.uid) - - # Convert Bucket object back to concatenated string format for comparison - commitment_str = commitment.name + commitment.access_key_id + commitment.secret_access_key - - current_bucket = ( - tplr.config.BUCKET_SECRETS["bucket_name"] + - tplr.config.BUCKET_SECRETS["read"]["access_key_id"] + - tplr.config.BUCKET_SECRETS["read"]["secret_access_key"] + self.compressor = tplr.compress.CompressDCT() + + # Init optimizer and momentum + self.optimizer = SGD(self.model.parameters(), lr=self.hparams.learning_rate) + self.momentum = {} + self.xshapes = {} + self.totalks = {} + for n, p in self.model.named_parameters(): + self.momentum[n] = torch.zeros_like(p) + _, _, xshape, totalk = self.compressor.compress( + self.transformer.encode(self.momentum[n]), + self.hparams.topk_compression ) - tplr.logger.debug(f'Comparing:\nCommitment: {commitment_str}\nCurrent: {current_bucket}') - - if current_bucket != commitment_str: - raise ValueError("Bucket commitment data does not match.") - - except Exception as e: - tplr.logger.error(f"Commitment error: {str(e)}") - tplr.commit(self.subtensor, self.wallet, self.config.netuid) + self.xshapes[n] = xshape + self.totalks[n] = totalk - # Init Wandb. - # Ensure the wandb directory exists - wandb_dir = os.path.join(os.getcwd(), 'wandb') - os.makedirs(wandb_dir, exist_ok=True) - - # Define the run ID file path inside the wandb directory - run_id_file = os.path.join(wandb_dir, f"wandb_run_id_V{self.uid}_{tplr.__version__}.txt") - - # Attempt to read the existing run ID - if os.path.exists(run_id_file): - with open(run_id_file, 'r') as f: - run_id = f.read().strip() - tplr.logger.info(f"Resuming WandB run with id {run_id}") - else: - run_id = None - tplr.logger.info("Starting a new WandB run.") - - # Initialize WandB - self.wandb = tplr.initialize_wandb( - run_prefix='V', - uid=self.uid, - config=self.config, - group='validator', - job_type='validation' + # Set up scheduler setup + warmup_scheduler = LinearLR( + self.optimizer, + total_iters=250 ) - - - # Set checkpoint path - if self.config.checkpoint_path is None: - # Default path if none provided - self.checkpoint_path = f"checkpoints/checkpoint-V{self.uid}.pth" - else: - self.checkpoint_path = self.config.checkpoint_path - - # Create checkpoint directory if it doesn't exist - os.makedirs(os.path.dirname(self.checkpoint_path), exist_ok=True) - - # Retrieve bucket info for all neurons - self.buckets = tplr.get_all_buckets( - netuid=self.config.netuid, - metagraph=self.metagraph, - config= self.config + cosine_scheduler = CosineAnnealingWarmRestarts( + self.optimizer, + T_0=10000, + T_mult=1, + eta_min=self.hparams.learning_rate * 0.1 ) - - # Init model. - tplr.logger.info('\n' + '-' * 40 + ' Hparams ' + '-' * 40) - self.hparams = tplr.load_hparams() - torch.manual_seed(42) - np.random.seed(42) - random.seed(42) - self.model = LlamaForCausalLM(config=self.hparams.model_config) - self.model.to(self.config.device) - self.model.eval() - - self.optimizer = torch.optim.AdamW( - self.model.parameters(), - lr=self.hparams.learning_rate*self.hparams.validator_learning_rate_scale_factor, - betas=(self.hparams.optimizer_beta1, self.hparams.optimizer_beta2), - weight_decay=self.hparams.optimizer_weight_decay, - foreach=True + self.scheduler = SequentialLR( + self.optimizer, + schedulers=[warmup_scheduler, cosine_scheduler], + milestones=[250] ) - self.scheduler = tplr.get_wsd_scheduler( - optimizer=self.optimizer, - num_warmup_steps=self.hparams.num_warmup_steps, - num_stable_steps=self.hparams.num_stable_steps, - num_decay_steps=self.hparams.num_decay_steps, - ) - - # Initialize checkpoint manager - self.checkpoint_manager = tplr.CheckpointManager( - model=self.model, - checkpoint_path=self.checkpoint_path, + # Init comms + self.comms = tplr.comms.Comms( wallet=self.wallet, - device=self.config.device, - ) - - # Load initial checkpoint - self.global_step = asyncio.run( - self.checkpoint_manager.load_from_highest_stake( - metagraph=self.metagraph, - buckets=self.buckets, - optimizer=self.optimizer, - scheduler=self.scheduler, - is_validator=True, - hparams=self.hparams - ) - ) - - self.last_window = 0 - self.optimal_pages_per_step = 4 - self.current_block = self.subtensor.block - self.current_window = self.block_to_window( self.current_block ) - self.window_seeds = {self.current_window: self.window_to_seed( self.current_window) } - self.block_event = asyncio.Event() - self.new_window_event = asyncio.Event() - self.stop_event = asyncio.Event() - self.step_scores = torch.zeros( 256, dtype = torch.float32 ) - self.step_loss_scores = torch.zeros( 256, dtype = torch.float32 ) - self.scores = torch.zeros( 256, dtype = torch.float32 ) - self.weights = torch.zeros( 256, dtype = torch.float32 ) - self.sample_rate = 1.0 - self.save_location = self.config.save_location - if self.save_location is None: - # Default to system temp dir with unique neuron directory - self.save_location = os.path.join( - tempfile.gettempdir(), f"neuron_{self.wallet.hotkey.ss58_address}" - ) - else: - # Append neuron-specific directory to save_location - self.save_location = os.path.join( - self.config.save_location, f"neuron_{self.wallet.hotkey.ss58_address}" - ) - - # Create the directory if it doesn't exist - os.makedirs(self.save_location, exist_ok=True) - self.checkpoint_tasks = set() - print ( self.hparams ) - - # Configuration for weight setting - self.weight_setting_config = { - 'timeout': 60, # seconds - 'max_retries': 3, - 'retry_delay': 5, - 'health_check_interval': 300 # 5 minutes - } - - # At the beginning of the Validator class, add a new attribute to track checkpoint tasks - self.checkpoint_tasks = set() # Track checkpoint tasks - self.checkpoint_lock = asyncio.Lock() # Add lock for thread safety - - async def update(self): - """Continuously updates the global state by polling every 10 minutes.""" - await asyncio.sleep(600) # Initial sleep before starting updates - while not self.stop_event.is_set(): - st = tplr.T() - await self.perform_update() - tplr.logger.info(f"{tplr.P(self.current_window, tplr.T() - st)} Updated global state.") - await asyncio.sleep(600) - - async def perform_update(self): - """Updates subtensor connection, metagraph, hyperparameters, and buckets.""" - self.subtensor = bt.subtensor(config=self.config) - self.metagraph = self.subtensor.metagraph(self.config.netuid) - - buckets = tplr.get_all_commitments( - substrate=self.subtensor.substrate, + save_location='/tmp', + key_prefix='model', + config=self.config, netuid=self.config.netuid, - metagraph=self.metagraph + metagraph=self.metagraph, + hparams=self.hparams, ) - self.buckets = [] - for uid in self.metagraph.uids: - bucket = buckets.get(uid) - if isinstance(bucket, bytes): - bucket = bucket.decode('utf-8') - if bucket is not None: - tplr.logger.debug(f"UID {uid}: Valid bucket found: {bucket}") - self.buckets.append(bucket) - else: - tplr.logger.debug(f"UID {uid}: Invalid or missing bucket: {bucket}") - self.buckets.append(None) - - async def load_checkpoint_background(self): - """Handles checkpoint loading in the background.""" - try: - tplr.logger.info(f"Loading checkpoint at step {self.global_step}") - - # Load the checkpoint into a temporary model - temp_model = LlamaForCausalLM(config=self.hparams.model_config).to(self.config.device) - temp_checkpoint_manager = tplr.CheckpointManager( - model=temp_model, - checkpoint_path=self.checkpoint_path, - wallet=self.wallet, - device=self.config.device, - ) - - # Load the checkpoint from the highest stake - await temp_checkpoint_manager.load_from_highest_stake( - metagraph=self.metagraph, - buckets=self.buckets - ) - # Safely update the main model's parameters - for param, temp_param in zip(self.model.parameters(), temp_model.parameters()): - param.data.copy_(temp_param.data) + # Init peers + if not self.config.peers: + self.peers = self.comms.peers + tplr.logger.info(f'Filtered peers with buckets: {self.peers}') + else: + self.peers = self.config.peers - tplr.logger.info(f"Checkpoint loaded at step {self.global_step}") + # Init state params + self.stop_event = asyncio.Event() + self.current_block = self.subtensor.block + self.current_window = int(self.current_block / self.hparams.blocks_per_window) + self.sync_window = self.current_window - # Clean up the temporary model to free memory - del temp_model, temp_checkpoint_manager - torch.cuda.empty_cache() + # Init scores + self.scores = torch.zeros(self.metagraph.n, dtype=torch.float32) - except Exception as e: - tplr.logger.error(f"Error loading checkpoint in background: {str(e)}") + # Init wandb + if self.config.use_wandb: + self.wandb = tplr.WandbManager( + uid=self.uid, + config=self.config, + is_validator=True + ).run async def run(self): - # Main loop. - self.loop = asyncio.get_running_loop() - self.update_task = asyncio.create_task(self.update()) - self.listener = threading.Thread(target=self.block_listener, args=(self.loop,), daemon=True).start() - self.checkpoint_tasks = set() - - # Optionally sync the model state by pulling model states from the history. - if self.config.sync_state: - st = tplr.T() - history_windows = [ self.current_window - i for i in range (self.hparams.max_history) ] - state_slices = await tplr.download_slices_for_buckets_and_windows( - buckets=[b for b in self.buckets if b is not None], - windows = history_windows, - key = 'state', - save_location=self.save_location - ) - for window in tqdm(history_windows, desc="Syncing state"): - max_global_step, _ = await tplr.apply_slices_to_model( - model=self.model, - window=window, - seed=window, - compression=self.hparams.compression, - save_location=self.save_location, - key='state', + # Try to load latest checkpoint + validator_uid, stake = self.comms.get_highest_stake_validator() + if stake > 0: + try: + state_dict = await self.comms.get( + uid=str(validator_uid), + window=self.current_window, + key='checkpoint', + timeout=240 ) - if max_global_step is not None: - self.global_step = max(self.global_step, max_global_step) - tplr.logger.info(f"{tplr.P(window, tplr.T() - st)}: Applied historical state and updated global step to {self.global_step}.") - torch.cuda.empty_cache() - - try: - # Run validation. - while True: + if state_dict is not None: + self.model.load_state_dict(state_dict) + tplr.logger.info(f"Loaded checkpoint from validator {validator_uid} at window {self.current_window}") + else: + tplr.logger.info("No checkpoint found, starting from scratch") + except Exception as e: + tplr.logger.warning(f"Failed to load checkpoint: {e}") + else: + tplr.logger.info("No active validators found, starting from scratch") + # Start block listener + self.loop = asyncio.get_running_loop() + self.listener = threading.Thread( + target=self.block_listener, + args=(self.loop,), + daemon=True + ).start() + + while True: + # Wait for validator offset + while self.sync_window >= (self.current_window - self.hparams.validator_offset): + tplr.logger.info(f'Waiting for validator window offset, synced: {self.sync_window}, current:{self.current_window}, offset:{self.hparams.validator_offset}') + await asyncio.sleep(12) + + # Check if checkpointing is needed (every 500 windows) + if self.current_window % 500 == 0: + tplr.logger.info(f'Creating checkpoint at window {self.current_window}') + try: - # Get the window we are evalling. - tplr.logger.info('[bold]' + '\n' + '-' * 40 + f' Step: {self.global_step} ' + '-' * 40) - gs_start = tplr.T() - self.global_step += 1 - offset = 2 - window = self.current_window - offset - - - # Upload checkpoint every 500 steps - if self.global_step % 500 == 0: - # Create background task for checkpoint operations - checkpoint_task = asyncio.create_task( - self.save_checkpoint_background( - global_step=self.global_step, - block_number=self.current_block, - scores=self.scores.clone(), # Clone to avoid race conditions - weights=self.weights.clone() # Clone to avoid race conditions - ) - ) - self.checkpoint_tasks.add(checkpoint_task) - checkpoint_task.add_done_callback(self.checkpoint_tasks.discard) - - # Download the state for the eval window. - st = tplr.T() - valid_buckets = [b for b in self.buckets if b is not None] - - if not valid_buckets: - tplr.logger.info(f"No valid buckets to download state slices for window {window}") - # Wait for the next window - while self.current_window - offset == window: - await asyncio.sleep(0.1) # Keep waiting until the window changes - - - state_slices = await tplr.download_slices_for_buckets_and_windows( - buckets=valid_buckets, - windows=[window], - key='state', - save_location=self.save_location + # Upload the model state directly using put + await self.comms.put( + state_dict_or_path=self.model.state_dict(), + uid=self.uid, + window_or_block=self.current_window, + key='checkpoint' ) - n_state_slices = len(state_slices[window]) if window in state_slices else 0 - tplr.logger.info(f"{tplr.P(window, tplr.T() - st)}: Downloaded {n_state_slices} window states.") - - # Download the delta for the eval window. - st = tplr.T() - eval_slices = await tplr.download_slices_for_buckets_and_windows( - buckets = self.buckets, - windows = [ window ], - key = 'delta', - save_location=self.save_location - ) - n_eval_slices = len(eval_slices[ window ]) if window in eval_slices else 0 - tplr.logger.info(f"{tplr.P(window, tplr.T() - st)}: Downloaded {n_eval_slices} window deltas.") - # Collect UIDs of miners who submitted slices - submitted_uids = set() - if window in eval_slices: - for slice_info in eval_slices[window]: - if getattr(slice_info, 'version', None) == tplr.__version__: - try: - uid = self.metagraph.hotkeys.index(slice_info.hotkey) - submitted_uids.add(uid) - except ValueError: - tplr.logger.warning(f"Hotkey {slice_info.hotkey} not found in metagraph") - if n_eval_slices == 0: - tplr.logger.info(f"{tplr.P(window, tplr.T() - st)}: No slices to eval, continue ...") - while self.current_window - offset == window: - await asyncio.sleep(0.1) # Wait for next window. - continue + tplr.logger.info(f"Successfully created checkpoint at window {self.current_window}") + except Exception as e: + tplr.logger.error(f"Failed to create checkpoint: {e}") + + # Catch up to current - validator_offset + while self.sync_window < (self.current_window - self.hparams.validator_offset): + self.sync_window += 1 + tplr.logger.info(f'Syncing window: {self.sync_window} current: {self.current_window}') + + # Gather gradients from this window + step_grads = await self.comms.gather( + state_dict={}, + my_uid=self.uid, + uids=self.peers, + window=self.sync_window, + key='gradient', + timeout=5, + device=self.config.device, + local=True, + ) - # Applied the model state for the eval window. - st = tplr.T() - max_global_step, _ = await tplr.apply_slices_to_model( - model=self.model, - window=window, - seed=window, - compression=self.hparams.compression, - save_location=self.save_location, - key='state', + # Decompress state and apply to gradients + for n, p in self.model.named_parameters(): + new_grad = self.transformer.decode( + self.compressor.batch_decompress( + p.to(self.config.device), + step_grads.state_dict[n + 'idxs'], + step_grads.state_dict[n + 'vals'], + self.xshapes[n], self.totalks[n] + ) ) - if max_global_step is not None: - self.global_step = max(self.global_step, max_global_step) - tplr.logger.info(f"{tplr.P(window, tplr.T() - st)}: Applied window state and updated global step to {self.global_step}.") - - # Obtain the indicies for the eval window. - st = tplr.T() - indices = await tplr.get_indices_for_window( - model = self.model, - seed = window, - compression = self.hparams.compression - ) - tplr.logger.info(f"{tplr.P(window, tplr.T() - st)}: Obtained window indices.") - - - # Attain the UID of this slice. - st = tplr.T() - valid_eval_slices = [s for s in eval_slices[window] if getattr(s, 'version', None) == tplr.__version__] - if not valid_eval_slices: - tplr.logger.warning(f"{tplr.P(window, tplr.T() - st)}: No valid slices with matching version {tplr.__version__}, continuing...") - while self.current_window - offset == window: - await asyncio.sleep(0.1) # Wait for next window. - continue - eval_slice_info = random.choice(valid_eval_slices) - try: - eval_uid = self.metagraph.hotkeys.index(eval_slice_info.hotkey) - except ValueError: - tplr.logger.warning(f"{tplr.P(window, tplr.T() - st)}: {eval_slice_info.hotkey} not found in metagraph") - continue - eval_slice_data = await tplr.get_slices(eval_slice_info.temp_file, self.model.device) - tplr.logger.info(f"{tplr.P(window, tplr.T() - st)}: Loaded window slices for uid: [dark_sea_green]{eval_uid}[/dark_sea_green].") - - # Download the eval page for this uid. - st = tplr.T() - eval_pages = await tplr.dataset.DatasetLoader.next_pages( - offset = window, - n_pages = self.hparams.validator_window_eval_size, - seed = eval_uid - ) - random.shuffle(eval_pages) - eval_dataset = await tplr.dataset.DatasetLoader.create( - batch_size = self.config.actual_batch_size, - sequence_length = self.hparams.sequence_length, - pages_info = eval_pages, - tokenizer = self.hparams.tokenizer - ) - tplr.logger.info(f"{tplr.P(window, tplr.T() - st)}: Downloaded eval pages: [light_steel_blue]{[p[1] for p in eval_pages]}[/light_steel_blue].") - - - # Accumulate gradients from this page. - eval_start = tplr.T() - self.model.zero_grad() - total_loss = 0.0 - step_loss_after = 0.0 - full_steps = 0 - total_steps = 0 - exhausted_window = False - with torch.enable_grad(): - for idx, batch in enumerate(eval_dataset): - total_steps += 1 - if random.random() < self.sample_rate and not exhausted_window: - full_steps += 1 - input_ids = torch.tensor(batch, dtype=torch.long).to(self.model.device) - labels = input_ids.clone() - labels = torch.where(labels == self.hparams.tokenizer.pad_token_id, -100, labels) - - # Store original parameters - original_params = {} - for name_i, param_i in self.model.named_parameters(): - original_params[name_i] = param_i.data.clone() - - # Apply miner's slice data to the model - for name_i, param_i in self.model.named_parameters(): - if name_i not in indices or name_i not in eval_slice_data: - continue - - idxs_i = indices[name_i].to(self.model.device) - slice_i = eval_slice_data[name_i].view(-1).to(self.model.device) - - # Update the parameter data at the specified indices - param_i.data.view(-1)[idxs_i] = slice_i - - # Perform forward pass with updated model (no gradients needed) - with torch.no_grad(), torch.amp.autocast(device_type=self.model.device.type, dtype=torch.bfloat16): - outputs_after = self.model(input_ids=input_ids, labels=labels) - step_loss_after += outputs_after.loss.item() - - # Restore original parameters - for name_i, param_i in self.model.named_parameters(): - param_i.data.copy_(original_params[name_i]) - - # Perform forward pass and compute loss with gradients (before applying delta) - with torch.enable_grad(), torch.amp.autocast(device_type=self.model.device.type, dtype=torch.bfloat16): - outputs_before = self.model(input_ids=input_ids, labels=labels) - loss = outputs_before.loss - loss.backward() - total_loss += loss.item() - - if self.current_window - offset != window: - exhausted_window = True - continue - - self.optimizer.step() - self.scheduler.step() - step_loss = total_loss / (full_steps + 1) - step_loss_after = step_loss_after / (full_steps + 1) - - eval_duration = tplr.T() - eval_start - tokens_per_step = self.hparams.sequence_length * self.config.actual_batch_size * (full_steps + 1) - tokens_per_second = tokens_per_step / eval_duration - - tplr.logger.info(f"{tplr.P(window, eval_duration)}: Accumulated gradients:") - tplr.logger.info(f"{tplr.P(window, eval_duration)}: \tTotal steps: [tan]{full_steps}/{total_steps}[/tan], Rate: [tan]{(full_steps/total_steps):.2f}[/tan], Target: [tan]{self.sample_rate:.2f}[/tan]") - tplr.logger.info(f"{tplr.P(window, eval_duration)}: \tTotal tokens: [tan]{tokens_per_step}[/tan], Tokens per second: [tan]{tokens_per_second:.2f}[/tan]") - tplr.logger.info(f"{tplr.P(window, eval_duration)}: \tLoss before applying delta: [tan]{step_loss:.4f}[/tan]") - tplr.logger.info(f"{tplr.P(window, eval_duration)}: \tLoss after applying delta: [tan]{step_loss_after:.4f}[/tan]") - - if exhausted_window: - self.sample_rate = max(0.0001, self.sample_rate * 0.95) + # Set recomputed gathered gradient + if p.grad is None: + p.grad = new_grad else: - self.sample_rate = min(1, self.sample_rate * 1.05) - - # Compute the score for this slice. - st = tplr.T() - - # Collect all delta_i and grad_i into larger vectors - all_delta = [] - all_grad = [] - - for name_i, param_i in self.model.named_parameters(): - if param_i.grad is None: - continue + p.grad.copy_(new_grad) + p.grad.sign_() - if name_i not in indices or name_i not in eval_slice_data: - continue - - idxs_i = indices[name_i].to(self.model.device) - grad_i = param_i.grad.view(-1)[idxs_i].to(self.model.device) - slice_i = eval_slice_data[name_i].view(-1).to(self.model.device) - theta_i = param_i.data.view(-1)[idxs_i] - delta_i = theta_i - slice_i - - all_delta.append(delta_i) - all_grad.append(grad_i) - - if len(all_delta) > 0: - # Concatenate all parts - all_delta = torch.cat(all_delta) - all_grad = torch.cat(all_grad) - - # Compute cosine similarity between miner's delta and validator's gradients - cosine_similarity = torch.nn.functional.cosine_similarity(all_delta, all_grad, dim=0).item() - else: - tplr.logger.warning("No valid parameter tensors found - setting cosine similarity to 0.0") - cosine_similarity = 0.0 - - # Set initial score to 0.0 - score = 0.0 - - # Check if cosine similarity is greater than zero - if cosine_similarity > 0.0: - # Base score from cosine similarity - base_score = 0.0 - - # Compute the loss difference (percentage) - loss_difference = step_loss_after - step_loss # Positive if miner's loss is worse - percentage_loss_difference = loss_difference / step_loss # Fractional change - - if loss_difference < 0: # Miner improved the loss - # Miner improved the loss, add to base score - score = base_score + (-loss_difference * 100) # Negative because loss decreased - elif percentage_loss_difference <= 0.25: - # Loss did not improve but is not worse by more than 25% - score = base_score # Only base score - else: - # Loss is worse by more than 25%, zero out their moving average score - self.scores[eval_uid] = 0.0 - score = 0.0 - else: - tplr.logger.info(f"Cosine similarity ({cosine_similarity:.4f}) not positive. Setting score to 0.0") - score = 0.0 - tplr.logger.info(f"Cosine similarity: [bold dark_sea_green]{cosine_similarity:.4f}[/bold dark_sea_green]") - tplr.logger.info(f"{tplr.P(window, tplr.T() - st)}: Computed score for miner {eval_uid}: [bold dark_sea_green]{score:.4f}[/bold dark_sea_green]") - self.optimizer.zero_grad() - - # Update the score for the evaluated miner - st = tplr.T() - - # First update the step score - self.step_scores[eval_uid] = score - - # Then update the moving average score using EMA - self.scores[eval_uid] = ( - self.hparams.validator_moving_alpha * self.step_scores[eval_uid] + - (1 - self.hparams.validator_moving_alpha) * self.scores[eval_uid] + # Apply the optimizer step + self.optimizer.step() + self.scheduler.step() + if self.config.use_wandb: + self.wandb.log({"lr": self.scheduler.get_last_lr()[0]}) + + # Get a random peer to eval on their gradient at self.sync_window + 1 + eval_uid = random.choice(self.peers) + # Get the pages for the window infront of the current sync window + pages = await tplr.dataset.DatasetLoader.next_pages( + offset=self.sync_window + 1, + n_pages=self.hparams.pages_per_window, + seed=eval_uid + ) + loader = await tplr.dataset.DatasetLoader.create( + batch_size=self.hparams.batch_size, + sequence_length=self.hparams.sequence_length, + pages_info=pages, + tokenizer=self.tokenizer + ) + tplr.logger.info(f'Evaluating uid: {eval_uid} on window: {self.sync_window + 1} with state from: {self.sync_window} and pages: {[p[1] for p in pages]}') + + # Get loss on all samples from this window + loss_before = 0 + for i, batch in enumerate(loader): + input_ids = torch.tensor(batch, dtype=torch.long).to(self.model.device) + labels = input_ids.clone() + labels = torch.where(labels == self.tokenizer.pad_token_id, -100, labels) + loss_before += self.model(input_ids=input_ids, labels=labels).loss.item() + tplr.logger.info(f'Computed total loss before: {loss_before}') + + # Get the gradients from this miner on this window + eval_grad = await self.comms.get( + uid=eval_uid, + window=self.sync_window + 1, + key='gradient', + timeout=5, + local=True, + ) + if eval_grad is None: + score = 0 + tplr.logger.info(f'Miner with uid: {eval_uid} has no gradient for window: {self.sync_window + 1}') + continue + + # Apply grad to model which is at state sync_window + for n, p in self.model.named_parameters(): + # Decompress their gradient + decompressed_grad = self.transformer.decode( + self.compressor.decompress( + p.to(self.config.device), + eval_grad[n + 'idxs'].to(self.config.device), + eval_grad[n + 'vals'].to(self.config.device), + self.xshapes[n], self.totalks[n], ) - - # Apply decay to miners who did not submit slices - all_uids = set(self.metagraph.uids.tolist()) - non_submitted_uids = all_uids - submitted_uids - decay_factor = self.hparams.validator_non_submission_decay - for uid in non_submitted_uids: - self.scores[uid] *= decay_factor - - # Prepare moving scores for normalization - moving_scores_tensor = self.scores.clone() - # Set negative moving scores to 0 - moving_scores_tensor[moving_scores_tensor < 0] = 0 - - # Normalize the positive moving average scores to get weights - positive_scores = moving_scores_tensor - sum_positive_scores = positive_scores.sum() - - if sum_positive_scores > 0: - self.weights = positive_scores / sum_positive_scores - else: - # Handle the case where all scores are zero or negative - self.weights = positive_scores # All zeros in this case - - # Log updated scores and weights - valid_score_indices = torch.nonzero(self.scores > 0).squeeze().view(-1) - for uid_i in valid_score_indices: - uid = uid_i.item() - moving_score = self.scores[uid].item() - weight = self.weights[uid].item() - step_score = self.step_scores[uid].item() - tplr.logger.info( - f"\tuid: [dark_sea_green]{uid}[/dark_sea_green], " - f"step_score: [dark_sea_green]{step_score:.3f}[/dark_sea_green], " - f"moving_score: [dark_sea_green]{moving_score:.3f}[/dark_sea_green], " - f"weight: [dark_sea_green]{weight:.3f}[/dark_sea_green]" - ) - - tplr.logger.info(f"{tplr.P(window, tplr.T() - st)}: Updated scores and weights.") - - # Apply all deltas to the model state. - st = tplr.T() - max_global_step, window_metric = await tplr.apply_slices_to_model( - model=self.model, - window=window, - seed=window, - compression=self.hparams.compression, - save_location=self.save_location, - key='delta', + ) + # Apply this grad to the param of the model using the learning rate of the scheduler + p.data.sub_(decompressed_grad, alpha=self.scheduler.get_last_lr()[0]) + + # Get loss after we apply the gradient + loss_after = 0 + for i, batch in enumerate(loader): + input_ids = torch.tensor(batch, dtype=torch.long).to(self.model.device) + labels = input_ids.clone() + labels = torch.where(labels == self.tokenizer.pad_token_id, -100, labels) + loss_after += self.model(input_ids=input_ids, labels=labels).loss.item() + tplr.logger.info(f'Computed total loss after: {loss_after}') + + # Remove gradient from the model + for n, p in self.model.named_parameters(): + # Decompress their gradient + decompressed_grad = self.transformer.decode( + self.compressor.decompress( + p.to(self.config.device), + eval_grad[n + 'idxs'].to(self.config.device), + eval_grad[n + 'vals'].to(self.config.device), + self.xshapes[n], self.totalks[n], ) - if max_global_step is not None: - self.global_step = max(self.global_step, max_global_step) - tplr.logger.info(f"{tplr.P(window, tplr.T() - st)}: Applied window delta and updated global step to {self.global_step}.") - - # Clean local and remote space from old slices. - st = tplr.T() - await tplr.delete_files_before_window(window_max=window - self.hparams.max_history, save_location=self.save_location, key='state') - await tplr.delete_files_before_window(window_max=window - self.hparams.max_history, save_location=self.save_location, key='delta') - await tplr.delete_files_from_bucket_before_window(bucket=tplr.config.BUCKET_SECRETS["bucket_name"], window_max=window - self.hparams.max_history, key='state') - await tplr.delete_files_from_bucket_before_window(bucket=tplr.config.BUCKET_SECRETS["bucket_name"], window_max=window - self.hparams.max_history, key='delta') - tplr.logger.info(f"{tplr.P(window, tplr.T() - st)}: Cleaned file history.") - - # Finish step. - gs_end = tplr.T() - while self.current_window - offset == window: - await asyncio.sleep(0.1) - window_time_delta = self.window_time - gs_end - window_delta_str = f"[red]{window_time_delta:.2f}[/red]" if window_time_delta < 0 else f"[green]+{window_time_delta:.2f}[/green]" - tplr.logger.info(f"{tplr.P(window, gs_end - gs_start)}[{window_delta_str}]: Finished step.") - - # Log main metrics - wandb.log({ - "validator/loss": step_loss, - "validator/tokens_per_step": sum(slice_metric['tokens_per_step'] for _, slice_metric in window_metric.items()), - "validator/tokens_per_second": sum(slice_metric['tokens_per_second'] for _, slice_metric in window_metric.items()), - "validator/sample_rate": self.sample_rate, - "validator/utilization": eval_duration / (gs_end - gs_start), - "validator/global_batch_size": sum(slice_metric['batch_size'] for _, slice_metric in window_metric.items()), - }, step=self.global_step) - - for hotkey, slice_metric in window_metric.items(): - uid = self.metagraph.hotkeys.index(hotkey) - wandb.log({ - f"miner/loss/{uid}": slice_metric['loss'], - f"miner/tokens_per_step/{uid}": slice_metric['tokens_per_step'], - f"miner/tokens_per_second/{uid}": slice_metric['tokens_per_second'], - f"miner/sample_rate/{uid}": slice_metric['sample_rate'], - f"miner/learning_rate/{uid}": slice_metric['learning_rate'], - }, step=self.global_step) - - for uid_i in valid_score_indices: - wandb.log({ - f"validator/step_scores/{uid_i.item()}": self.step_scores[uid_i].item(), - f"validator/moving_scores/{uid_i.item()}": self.scores[uid_i].item(), - f"validator/weights/{uid_i.item()}": self.weights[uid_i].item(), - }, step=self.global_step) - # Set temperatured weights on the chain. - if self.global_step % 100 == 0: - # Check if all scores are zero - if torch.all(self.weights[self.metagraph.uids] == 0): - tplr.logger.info("All weights are zero, skipping weight setting") - continue - - tplr.logger.info(f"Setting weights on chain: {self.weights[self.metagraph.uids]}") - - max_retries = 3 - retry_delay = 5 - - for attempt in range(max_retries): - result, error = await self.set_weights_with_timeout() - - if result is not None: - tplr.logger.info(f"Successfully set weights on chain: {result}") - break - - if attempt < max_retries - 1: - tplr.logger.warning(f"Failed to set weights (attempt {attempt + 1}/{max_retries}): {error}") - tplr.logger.info(f"Retrying in {retry_delay} seconds...") - await asyncio.sleep(retry_delay) - else: - tplr.logger.error(f"Failed to set weights after {max_retries} attempts: {error}") - # Continue with the next iteration rather than freezing - break - - # Add periodic health check - self.last_active_timestamp = time.time() - - # Add this at the end of each main loop iteration - if time.time() - self.last_active_timestamp > 300: # 5 minutes timeout - tplr.logger.error("Validator appears to be frozen. Initiating recovery...") - # Force proceed to next iteration - continue - - except KeyboardInterrupt: - tplr.logger.info("Training interrupted by user. Stopping the run.") - self.stop_event.set() - await self.update_task - break # Exit the loop to reach the finally block - - except Exception as e: - tplr.logger.exception(f"Exception during training loop: {e}") - continue - - finally: - # Wait for any pending checkpoint tasks to complete - if self.checkpoint_tasks: - tplr.logger.info(f"Waiting for {len(self.checkpoint_tasks)} checkpoint tasks to complete...") - await asyncio.gather(*self.checkpoint_tasks) - self.checkpoint_manager.cleanup() - tplr.logger.info("Validator shutdown complete.") - - # Returns the slice window based on a block. - def block_to_window(self, block: int) -> int: - return int(block / self.hparams.window_length) - # Returns the slice window based on a blotplr. - def window_to_seed(self, window: int) -> int: - return str( self.subtensor.get_block_hash( window * self.hparams.window_length ) ) + ) + # Apply this grad to the param of the model using the learning rate of the scheduler + p.data.add_(decompressed_grad, alpha=self.scheduler.get_last_lr()[0]) + + # Compute score + score = loss_before - loss_after + tplr.logger.info(f'score: {score}') + + # Set weights if needed + if self.sync_window % self.hparams.windows_per_weights == 0: + # Update scores with new score + self.scores[eval_uid] = self.hparams.scores_alpha * score + (1 - self.hparams.scores_alpha) * self.scores[eval_uid] + # Compute weights from scores + weights = torch.softmax(self.scores, dim=0) + + # Set weights on chain + self.subtensor.set_weights( + wallet=self.wallet, + netuid=self.config.netuid, + uids=self.metagraph.uids, + weights=weights, + wait_for_inclusion=False, + wait_for_finalization=False, + ) + tplr.logger.info(f'Set weights on chain for window {self.sync_window}') - # A listener thread which posts the block event - # when the chain announces a new block. def block_listener(self, loop): def handler(event, _u, _s): self.current_block = int(event['header']['number']) - loop.call_soon_threadsafe(self.block_event.set) - if self.block_to_window(self.current_block) != self.current_window: - self.window_seeds[ self.block_to_window(self.current_block) ] = self.window_to_seed( self.block_to_window(self.current_block) ) - self.current_window = self.block_to_window(self.current_block) - self.window_duration = tplr.T() - self.window_time if hasattr(self, 'window_time') else 0 - self.window_time = tplr.T() - loop.call_soon_threadsafe(self.new_window_event.set) - tplr.logger.info(f"{tplr.P(self.current_window, self.window_duration)} New Window.") - - # Run listener with retry. + if int(self.current_block / self.hparams.blocks_per_window) != self.current_window: + self.current_window = int(self.current_block / self.hparams.blocks_per_window) while not self.stop_event.is_set(): try: bt.subtensor(config=self.config).substrate.subscribe_block_headers(handler) break - except Exception as e: - tplr.logger.error(f"Failed to subscribe to block headers: {e}.\nRetrying in 1 seconds...") + except Exception: time.sleep(1) - async def set_weights_with_timeout(self, timeout=30): - """Set weights with timeout and retry logic""" - try: - # Wrap synchronous subtensor call in partial to pass arguments - set_weights_fn = partial( - self.subtensor.set_weights, - wallet=self.wallet, - netuid=self.config.netuid, - uids=self.metagraph.uids, - weights=self.weights[self.metagraph.uids], - version_key= tplr.version_key, - wait_for_inclusion=True, - wait_for_finalization=False, - ) - - # Execute with timeout - result = await asyncio.wait_for( - asyncio.to_thread(set_weights_fn), - timeout=timeout - ) - return result, None - except TimeoutError: - return None, "Timeout while setting weights" - except Exception as e: - return None, str(e) - - async def save_checkpoint_background(self, global_step: int, block_number: int, scores: torch.Tensor, weights: torch.Tensor): - """Handles checkpoint saving and uploading in the background""" - try: - async with self.checkpoint_lock: # Ensure thread safety - await self.checkpoint_manager.save_and_upload( - global_step=global_step, - block_number=block_number, - scores=scores, - weights=weights, - optimizer_state=self.optimizer.state_dict(), - scheduler_state=self.scheduler.state_dict() - ) - except Exception as e: - tplr.logger.error(f"Error in background checkpoint save: {e}") - - def cleanup(self): - """Cleanup resources if needed.""" - self._shutdown = True - # Wait for any pending checkpoint tasks to complete - if self.checkpoint_tasks: - tplr.logger.info(f"Waiting for {len(self.checkpoint_tasks)} checkpoint tasks to complete...") - asyncio.gather(*self.checkpoint_tasks) - self.checkpoint_manager.cleanup() - tplr.logger.info("CheckpointManager shutdown complete") - if __name__ == "__main__": - validator = Validator() - asyncio.run(validator.run()) + asyncio.run(Validator().run()) diff --git a/pyproject.toml b/pyproject.toml index 67b53e2..5f2ec25 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,47 +3,22 @@ requires = ["setuptools", "wheel"] build-backend = "setuptools.build_meta" [project] -name = "templar" +name = "tplr" version = "0.1.0" -description = "Incentivized Foundation Model Training" +description = "Add your description here" readme = "README.md" -license = { text = "MIT" } requires-python = ">=3.11" -authors = [ - { name = "const", email = "dont.email.me@gmail.com" } -] - dependencies = [ - "aiohttp==3.10.11", "bittensor==8.5.1", - "substrate-interface", - "boto3==1.34.131", - "safetensors==0.4.5", - "torch>=2.4.0", - "transformers==4.44.2", - "python-dotenv==1.0.1", - "datasets==3.0.0", - "torchvision==0.19.1", - "wandb>=0.18.3", - "typer==0.12.5", - "numpy==2.0.1", - "aioboto3>=13.1.1", - "loguru==0.7.2", - "uvloop==0.20.0", - "aiofiles>=24.1.0", - "pydantic>=2.9.2", -] - -[tool.uv] -dev-dependencies = [ - "pytest>=8.3.3", - "nest_asyncio>=1.5.8", - "pytest-asyncio" + "bt-decode==0.4.0", + "torch", + "boto3", + "einops", + "aiofiles", + "aioboto3", + "aiobotocore", + "transformers", + "pip", + "wandb", + "python-dotenv", ] - -[project.urls] -Homepage = "https://github.com/tplr-ai/templar" - -[tool.setuptools.packages.find] -where = ["src"] - diff --git a/run.py b/run.py new file mode 100644 index 0000000..93c9574 --- /dev/null +++ b/run.py @@ -0,0 +1,395 @@ +# The MIT License (MIT) +# © 2024 templar.tech + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# fmt: off + +# Global imports +import os +import sys +import time +import torch +import random +import asyncio +import argparse +import threading +import bittensor as bt +import torch.optim as optim +from transformers import LlamaForCausalLM +from torch.optim.lr_scheduler import CosineAnnealingWarmRestarts + +# Local imports +import tplr +import tplr.checkpoint + +# GPU optimizations. +torch.backends.cudnn.benchmark = True +torch.backends.cuda.matmul.allow_tf32 = True +torch.backends.cudnn.allow_tf32 = True + +class Neuron: + + # Command line config items. + @staticmethod + def config(): + parser = argparse.ArgumentParser(description='Miner / Validator script') + parser.add_argument('--netuid', type=int, default=268, help='Bittensor network UID.') + parser.add_argument('--device', type=str, default='cuda', help='Device to use for training (e.g., cpu or cuda)') + parser.add_argument('--debug', action='store_true', help='Enable debug logging') + parser.add_argument('--trace', action='store_true', help='Enable trace logging') + parser.add_argument('--use_wandb', action='store_true', help='Use Weights and Biases for logging') + parser.add_argument('--is_validator', action='store_true', help='If validator, turn on to run evals rather than train for incentive.') + parser.add_argument('--random', action='store_true', help='Trains on a random page instead of correctly assigned.') + parser.add_argument('--peers', type=int, nargs='+', default=[], help='List of UIDs to peer with. e.g., --uids 1 2 3') + parser.add_argument('--checkpoint_path', type=str, default=None, help='Path to save/load the checkpoint. If None, the path is set to checkpoint-M.pth.') + parser.add_argument('--save-location', type=str, default=None, help='Directory to save/load slice files') + bt.wallet.add_args( parser ) + bt.subtensor.add_args( parser ) + bt.logging.add_args( parser ) + config = bt.config( parser ) + if config.debug: + tplr.debug() + if config.trace: + tplr.trace() + return config + + def __init__(self): + tplr.logger.debug("Starting initialization...") + + # Init config from command line + self.config = Neuron.config() + + # # Init AutoUpdate + # self.autoupdate = tplr.autoupdate.AutoUpdate() + + # Load hyperparameters + self.hparams = tplr.load_hparams() + + # Init bittensor objects. + self.wallet = bt.wallet( config = self.config ) + self.subtensor = bt.subtensor( config = self.config ) + self.metagraph = self.subtensor.metagraph( self.config.netuid ) + if self.wallet.hotkey.ss58_address not in self.metagraph.hotkeys: + tplr.logger.error(f'\n\t[bold]The wallet {self.wallet} is not registered on subnet: {self.metagraph.netuid}[/bold]. You need to register first with: [blue]`btcli subnet register`[/blue]\n') + sys.exit() + self.uid = self.metagraph.hotkeys.index(self.wallet.hotkey.ss58_address) + tplr.logger.info('\n' + '-' * 40 + ' Objects ' + '-' * 40) + tplr.logger.info(f'\n{self.wallet}\n{self.subtensor}\n{self.metagraph}\nuid: {self.uid}') + tplr.logger.debug("Initialized bittensor objects...") + tplr.logger.debug("Initializing buckets...") + # Buckets must + self.buckets = {} # Initialize empty dict first + + + # Initialize the model with config from hparams + self.model = LlamaForCausalLM(self.hparams.model_config) + self.model.to(self.config.device) + # Print model parameters + total_params = sum(p.numel() for p in self.model.parameters()) + tplr.logger.info(f"Total parameters: {total_params:,}") + tplr.logger.debug("Initialized model...") + + # Init tokenizer. + self.tokenizer = self.hparams.tokenizer + + # Init optimizer. + self.momentum = {} + self.optimizer = optim.SGD(self.model.parameters(), lr = self.hparams.learning_rate) + for n, p in self.model.named_parameters(): + self.momentum[n] = torch.zeros_like(p) + self.scheduler = tplr.CosineWarmupScheduler( + optimizer=self.optimizer, + warmup_steps=self.hparams.warmup_steps, + alpha_f=self.hparams.alpha_f, + t_max=self.hparams.t_max + ) + + # Init compression. + self.transformer = tplr.compress.TransformDCT( self.model, target_chunk = self.hparams.target_chunk ) + self.compressor = tplr.compress.CompressDCT() + + # # Set checkpoint path => root dir as argumnet and pass root dir, in the init + # if self.config.checkpoint_path is None: + # # Default path if none provided + # self.checkpoint_path = f"checkpoints/checkpoint-{self.uid}.pth" + # else: + # self.checkpoint_path = self.config.checkpoint_path + + # # Create checkpoint directory if it doesn't exist + # os.makedirs(os.path.dirname(self.checkpoint_path), exist_ok=True) + + + # # Initialize checkpoint manager + # self.checkpoint_manager = tplr.checkpoint.CheckpointManager( + # model=self.model, + # checkpoint_path=self.checkpoint_path, + # wallet=self.wallet, + # device=self.config.device, + # optimizer=self.optimizer, + # scheduler=self.scheduler + # ) + + # # Load initial checkpoint + # tplr.logger.debug("Loading checkpoint...") + # self.global_step = asyncio.run( + # self.checkpoint_manager.load_from_highest_stake( + # metagraph=self.metagraph, + # buckets=self.buckets, + # optimizer=self.optimizer, + # scheduler=self.scheduler, + # is_validator=False, + # hparams=self.hparams + # ) + # ) + + + # Initialize Comms + self.comms = tplr.comms.Comms( + wallet=self.wallet, + save_location='/tmp', + key_prefix='model', + config=self.config, + netuid=self.config.netuid, + metagraph=self.metagraph, + hparams=self.hparams, + ) + + # Initialize peers with buckets + if not self.config.peers: + # Use peers with buckets from ChainManager + self.peers = self.comms.peers + tplr.logger.info(f'Filtered peers with buckets: {self.peers}') + else: + self.peers = self.config.peers # Use specified peers + + # Ensure we have at least one peer + if self.config.is_validator and not self.peers: + tplr.logger.error( + "No peers available for validation. Ensure there are miners with buckets registered." + ) + sys.exit(1) + + # Add self to peers if not already included + if self.uid not in self.peers: + self.peers.append(self.uid) + + tplr.logger.info(f'Active peers: {self.peers}') + + # Init state params. + self.stop_event = asyncio.Event() + self.current_block = self.subtensor.block + self.current_window = int( self.current_block / self.hparams.blocks_per_window ) + + # Init scores. + self.scores = torch.zeros(self.metagraph.n, dtype=torch.float32) + + # Init wandb. + if self.config.use_wandb: + self.wandb = tplr.wandb.WandbManager( + uid=self.uid, + config=self.config, + is_validator=self.config.is_validator + ).run + + # Main training loop. + async def run( self ): + + # Start background block listener. + self.loop = asyncio.get_running_loop() + self.listener = threading.Thread(target=self.block_listener, args=(self.loop,), daemon=True).start() + + # Run until stopped. + while True: + + # Record the window we are on. + step_window = self.current_window + # Get the uid to seed data (if validator, take random from peers.) + step_uid = self.uid if not self.config.is_validator else random.choice(self.peers) + tplr.logger.info('\n' + '-' * 40 + f' Window: {step_window} ' + '-' * 40) + + # Checkpoint: every X windows , the validators with the highest stake will comms.put into s3, if model is None + # wait until until next window that % 100 == 0, just gather validator with max stake + + # Optionally sync state. Take this out + if step_window % self.hparams.windows_per_sync == 0: + tplr.logger.info("Sync globally") + # This gather op is way too slow + # When a miner joins the the network , wait until a new checkpoint has being put up by the validator + gather_result = await self.comms.gather( + state_dict = self.model.state_dict(), + my_uid = self.uid, + uids = self.peers, + window = int(self.current_window/self.hparams.windows_per_sync), + key = 'model', + timeout = 30, + device = self.config.device + ) + # Take mean of all peers state + state_dict = {name: torch.mean(torch.stack(gather_result[name]), dim=0) for name in gather_result} + # Load state into model. + self.model.load_state_dict(state_dict) + tplr.logger.info("Done global sync.") + + # Get the pages for this window. + pages = await tplr.dataset.DatasetLoader.next_pages( + offset = step_window, + n_pages = self.hparams.pages_per_window, + seed = self.metagraph.hotkeys[ step_uid ] if not self.config.random else random.randint(10000) # Select seed from step_uid. + ) + loader = await tplr.dataset.DatasetLoader.create( + batch_size = self.hparams.batch_size, + sequence_length = self.hparams.sequence_length, + pages_info = pages, + tokenizer = self.tokenizer + ) + tplr.logger.info(f"Pages: {[p[1] for p in pages]} for UID: {step_uid} and Window: {step_window}") + + # Accumulate gradient. + tplr.logger.info("Start accumulating...") + self.optimizer.zero_grad() + self.model.zero_grad() + total_loss = 0 + for i, batch in enumerate(loader): + input_ids = torch.tensor(batch, dtype=torch.long).to(self.model.device) + labels = input_ids.clone() + labels = torch.where(labels == self.tokenizer.pad_token_id, -100, labels) + with torch.amp.autocast(device_type=self.model.device.type, dtype=torch.bfloat16): + outputs = self.model(input_ids=input_ids, labels=labels) + total_loss += outputs.loss.item() + outputs.loss.backward() + print('loss:', outputs.loss.item()) + if self.current_window != step_window: + break + tplr.logger.info(f"Stopped accumulating: {i+1} batches with {(i+1) * self.hparams.batch_size * self.hparams.sequence_length} tokens ") + # Log to wandb. + if self.config.use_wandb: + self.wandb.log({"loss": outputs.loss.item()}) + + # Reduce gradient using DeMo. + gradient = {} + xshapes = {} + totalks = {} + transmitted = {} + for n, p in self.model.named_parameters(): + # Step-Weight decay + p.data.mul_( 1.0 - self.scheduler.get_last_lr()[0] * self.hparams.weight_decay ) + # Momentum decay + self.momentum[n].mul_( self.hparams.momentum_decay ) + # Add the grad to the momentum. + self.momentum[n].add_( p.grad, alpha=self.scheduler.get_last_lr()[0] ) + # Compress gradient. + idxs, vals, xshape, totalk = self.compressor.compress( + self.transformer.encode(self.momentum[n]), self.hparams.topk_compression + ) + # Estimate transmitted gradient. + transmit_grad = self.transformer.decode( + self.compressor.decompress(p, idxs, vals, xshape, totalk) + ) + # Remove the transmitted from delta (double counting) + self.momentum[n].sub_(transmit_grad) + # Add to share_state + transmitted[ n ] = transmit_grad + gradient[ n + 'idxs'] = idxs + gradient[ n + 'vals'] = vals + xshapes[ n ] = xshape + totalks[ n ] = totalk + + # All-gather share state from all peers with timeout. + tplr.logger.info(f"Start gather: {self.peers}") + gather_result = await self.comms.gather( + state_dict = gradient, + my_uid = self.uid, + uids = self.peers, + window = step_window, + key = 'gradient', + timeout = 5, + device = self.config.device + ) + + # Decompress state and apply to grad. + for n, p in self.model.named_parameters(): + # Decode grad from all nodes + if self.config.is_validator: + # Get gradient for step uid we are evaluating. + eval_idx = gather_result[n + 'idxs'][ self.peers.index(step_uid) ] + eval_val = gather_result[n + 'vals'][ self.peers.index(step_uid) ] + # Decompress their gradient. + their_grad = self.transformer.decode( + self.compressor.decompress(p, eval_idx, eval_val, xshapes[ n ], totalks[ n ]) + ) + # Get my recreated gradient. + my_grad = transmitted[ n ] + # Compute cosine sim score. + score = torch.nn.functional.cosine_similarity(their_grad.flatten(), my_grad.flatten(), dim=0) + # Compute moving scores and weights. + self.scores[step_uid] = self.hparams.scores_alpha * score + (1 - self.hparams.scores_alpha) * self.scores[step_uid].expand_as(score) + self.weights = torch.softmax(self.scores, dim=0) + # Log scores and weights to wandb. + if self.config.use_wandb: + for uid in self.peers: + self.wandb.log({f"s{uid}": self.scores[uid], f"w{uid}": self.weights[uid] }) + + # Decompress all gradients in batch form to produce shared gradient. + new_grad = self.transformer.decode( + self.compressor.batch_decompress( + p, gather_result[n + 'idxs'], gather_result[n + 'vals'], xshapes[ n ], totalks[ n ] + ) + ) + # Set recomputed gathered gradient. + if p.grad is None: + p.grad = new_grad + else: + p.grad.copy_(new_grad) + # Sign-SGD + p.grad.sign_() + + # Apply the optimizer step + tplr.logger.info("Finish and step.") + self.optimizer.step() + self.scheduler.step() + # Set weights on the chain based on current weights. + if self.config.is_validator and step_window % self.hparams.windows_per_weights == 0: + + # Set weights on chain. + self.subtensor.set_weights( + wallet = self.wallet, + netuid = self.config.netuid, + uids = self.metagraph.uids, + weights = self.weights, + wait_for_inclusion = False, # Dont wait, fire and forget. + wait_for_finalization = False, + ) + + + # Wait for end of window (if not already done.) + while self.current_window == step_window: + time.sleep(0.1) + + # Listens for new blocks and sets self.current_block and self.current_window + def block_listener(self, loop): + def handler(event, _u, _s): + self.current_block = int(event['header']['number']) + if int( self.current_block / self.hparams.blocks_per_window ) != self.current_window: + self.current_window = int( self.current_block / self.hparams.blocks_per_window ) + while not self.stop_event.is_set(): + try: + bt.subtensor(config=self.config).substrate.subscribe_block_headers(handler) + break + except Exception: + time.sleep(1) + +# Start miner/validator. +if __name__ == "__main__": + asyncio.run( Neuron().run() ) diff --git a/scripts/docker_run b/scripts/docker_run new file mode 100644 index 0000000..6de9a6a --- /dev/null +++ b/scripts/docker_run @@ -0,0 +1,55 @@ +#!/bin/bash +set -e + +# Function to prompt for input with default value +prompt_with_default() { + local prompt="$1" + local default="$2" + local response + + echo -n "$prompt [$default]: " + read response + echo "${response:-$default}" +} + +# Function to prompt for required input +prompt_required() { + local prompt="$1" + local response="" + + while [ -z "$response" ]; do + echo -n "$prompt: " + read response + if [ -z "$response" ]; then + echo "This field is required" + fi + done + echo "$response" +} + +# Prompt for configuration +echo "🤖 Templar Mining Docker Configuration" +echo "-------------------------------------" + +NODE_TYPE=$(prompt_with_default "Enter node type (miner/validator)" "miner") +WALLET_NAME=$(prompt_required "Enter wallet name") +WALLET_HOTKEY=$(prompt_required "Enter wallet hotkey") +WANDB_API_KEY=$(prompt_required "Enter Weights & Biases API key") +NETWORK=$(prompt_with_default "Enter network" "test") +CUDA_DEVICE=$(prompt_with_default "Enter CUDA device" "cuda:0") +DEBUG=$(prompt_with_default "Enable debug mode? (true/false)" "false") + +# Export variables for docker-compose +export WALLET_NAME WALLET_HOTKEY WANDB_API_KEY NETWORK CUDA_DEVICE DEBUG + +# Choose compose file based on node type +COMPOSE_FILE="docker/compose.${NODE_TYPE}.yml" + +# Start the containers +echo -e "\n📦 Starting containers..." +docker compose -f "$COMPOSE_FILE" up -d + +echo -e "\n✅ Containers started successfully!" +echo "📝 Logs are available in the ./logs directory" +echo -e "\nTo follow logs, run:" +echo "docker compose -f $COMPOSE_FILE logs -f" \ No newline at end of file diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh new file mode 100644 index 0000000..f53f56f --- /dev/null +++ b/scripts/entrypoint.sh @@ -0,0 +1,61 @@ +#!/bin/bash +set -e + +# Check required environment variables +for var in WALLET_NAME WALLET_HOTKEY NODE_TYPE WANDB_API_KEY; do + if [ -z "${!var}" ]; then + echo "Error: $var environment variable is required" + exit 1 + fi +done + +# Activate virtual environment +source /app/.venv/bin/activate + +# Create logs directory +mkdir -p /app/logs + +# Check CUDA availability +if ! python3 -c "import torch; assert torch.cuda.is_available(), 'CUDA not available'"; then + echo "Error: CUDA is not available" + exit 1 +fi + +# Login to wandb non-interactively +wandb login ${WANDB_API_KEY} --relogin + +# Convert DEBUG to --debug flag if true +DEBUG_FLAG="" +if [ "$DEBUG" = "true" ]; then + DEBUG_FLAG="--debug" +fi + +# Check CUDA version +CUDA_VERSION=$(python3 -c "import torch; print(torch.version.cuda)") +if [[ "${CUDA_VERSION}" != "12.6" ]]; then + echo "Warning: Container CUDA version (${CUDA_VERSION}) differs from host CUDA version (12.6)" +fi + +# Check NODE_TYPE and start appropriate process +if [ "$NODE_TYPE" = "miner" ]; then + echo "Starting miner..." + exec python3 neurons/miner.py \ + --wallet.name ${WALLET_NAME} \ + --wallet.hotkey ${WALLET_HOTKEY} \ + --device ${CUDA_DEVICE} \ + --subtensor.network ${NETWORK} \ + --use_wandb \ + ${DEBUG_FLAG} +elif [ "$NODE_TYPE" = "validator" ]; then + echo "Starting validator..." + exec python3 neurons/validator.py \ + --wallet.name ${WALLET_NAME} \ + --wallet.hotkey ${WALLET_HOTKEY} \ + --device ${CUDA_DEVICE} \ + --subtensor.network ${NETWORK} \ + --use_wandb \ + ${DEBUG_FLAG} +else + echo "Error: NODE_TYPE must be either \"miner\" or \"validator\"" + exit 1 +fi \ No newline at end of file diff --git a/src/tplr/__init__.py b/src/tplr/__init__.py new file mode 100644 index 0000000..2ae7d44 --- /dev/null +++ b/src/tplr/__init__.py @@ -0,0 +1,34 @@ +# The MIT License (MIT) +# © 2024 templar.tech + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +# ruff: noqa +# pylint: disable=all +# mypy: ignore-errors +# type: ignore + +__version__ = "0.2.0" + +# Import package. +from .autoupdate import * +from .chain import * +from .comms import * +from .compress import * +from .dataset import * +from .hparams import * +from .logging import * +from .wandb import * + diff --git a/src/tplr/autoupdate.py b/src/tplr/autoupdate.py new file mode 100644 index 0000000..8be6591 --- /dev/null +++ b/src/tplr/autoupdate.py @@ -0,0 +1,347 @@ +# The MIT License (MIT) +# © 2024 templar.tech + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# fmt: off + +# Global imports +import os +import git +import sys +import json +import time +import aiohttp +import asyncio +import threading +import subprocess +from packaging import version + +# Local imports +from .logging import logger +from .config import BUCKET_SECRETS +from .comms import delete_old_version_files + + +TARGET_BRANCH = "main" + + +class AutoUpdate(threading.Thread): + """ + Automatic update utility for templar neurons. + """ + + def __init__(self): + super().__init__() + self.daemon = True # Ensure thread exits when main program exits + + try: + self.repo = git.Repo(search_parent_directories=True) + except Exception as e: + logger.exception("Failed to initialize the repository", exc_info=e) + sys.exit(1) # Terminate the thread/application + self.start() + + async def get_remote_version(self): + """ + Asynchronously fetch the remote version string from a remote HTTP endpoint. + """ + try: + url = "https://raw.githubusercontent.com/tplr-ai/templar/main/src/templar/__init__.py" + async with aiohttp.ClientSession() as session: + async with session.get(url, timeout=5) as response: + response.raise_for_status() + content = await response.text() + + for line in content.split("\n"): + if line.startswith("__version__"): + version_info = line.split("=")[1].strip().strip(" \"'") + return version_info + + logger.error("Version string not found in remote __init__.py") + return None + + except Exception as e: + logger.exception( + "Failed to get remote version for version check", exc_info=e + ) + return None + + async def check_version_updated(self): + """ + Asynchronously compares local and remote versions and returns True if the remote version is higher. + """ + remote_version = await self.get_remote_version() + if not remote_version: + logger.error("Failed to get remote version, skipping version check") + return False + + local_version = self.get_local_version() + if not local_version: + logger.error("Failed to get local version, skipping version check") + return False + + local_version_obj = version.parse(local_version) + remote_version_obj = version.parse(remote_version) + logger.info( + f"Version check - remote_version: {remote_version}, local_version: {local_version}" + ) + + if remote_version_obj > local_version_obj: + logger.info( + f"Remote version ({remote_version}) is higher " + f"than local version ({local_version}), automatically updating..." + ) + return True + + return False + + def attempt_update(self): + """ + Attempts to update the local repository to match the remote. + """ + if self.repo.head.is_detached: + logger.error("Repository is in a detached HEAD state. Cannot update.") + return False + + if self.repo.is_dirty(untracked_files=True): + logger.error( + "Repository has uncommitted changes or untracked files. Cannot update." + ) + return False + + try: + origin = self.repo.remote(name="origin") + # Fetch latest changes from remote + origin.fetch() + # Get the current branch + current_branch = self.repo.active_branch + if current_branch.name != TARGET_BRANCH: + logger.error( + f"Current branch ({current_branch.name}) is not the target branch ({TARGET_BRANCH}). Cannot update." + ) + return False + + # Reset local branch to the remote branch + remote_ref = f"origin/{TARGET_BRANCH}" + logger.info( + f"Resetting local branch '{current_branch.name}' to '{remote_ref}'" + ) + self.repo.git.reset("--hard", remote_ref) + logger.info("Successfully reset to the latest commit from remote.") + + # Verify that local and remote commits match + local_commit = self.repo.commit(current_branch) + remote_commit = self.repo.commit(remote_ref) + if local_commit.hexsha != remote_commit.hexsha: + logger.error( + "Local commit does not match remote commit after reset. Rolling back." + ) + self.repo.git.reset("--hard", "HEAD@{1}") # Reset to previous HEAD + return False + + return True + except git.exc.GitCommandError as e: + logger.error(f"Git command failed: {e}") + # Rollback on failure + self.repo.git.reset("--hard", "HEAD@{1}") + return False + except Exception as e: + logger.exception("Failed to update repository.", exc_info=e) + return False + except git.exc.GitCommandError as e: + logger.error(f"Git command failed: {e}") + return False + except Exception as e: + logger.exception("Failed to update repository.", exc_info=e) + return False + + def handle_merge_conflicts(self): + """ + Attempt to automatically resolve any merge conflicts that may have arisen. + """ + try: + self.repo.git.reset("--merge") + origin = self.repo.remote(name="origin") + current_branch = self.repo.active_branch.name + origin.pull(current_branch) + + for item in self.repo.index.diff(None): + file_path = item.a_path + logger.info(f"Resolving conflict in file: {file_path}") + self.repo.git.checkout("--theirs", file_path) + self.repo.index.commit("Resolved merge conflicts automatically") + logger.info("Merge conflicts resolved, repository updated to remote state.") + logger.info("✅ Successfully updated") + return True + except git.GitCommandError as e: + logger.exception( + "Failed to resolve merge conflicts. Please manually pull and update.", + exc_info=e, + ) + return False + + def attempt_package_update(self): + """ + Synchronize dependencies using 'uv sync --extra all'. + """ + logger.info("Attempting to update packages using 'uv sync --extra all'...") + + try: + uv_executable = "uv" + # TODO: Allow specifying the path to 'uv' if it's not in PATH + + subprocess.check_call( + [uv_executable, "sync", "--extra", "all"], + timeout=300, + ) + logger.info("Successfully updated packages using 'uv sync --extra all'.") + except subprocess.CalledProcessError as e: + logger.exception("Failed to synchronize dependencies with uv", exc_info=e) + except FileNotFoundError: + logger.error( + "uv executable not found. Please ensure 'uv' is installed and in PATH." + ) + except Exception as e: + logger.exception( + "Unexpected error during package synchronization", exc_info=e + ) + + async def cleanup_old_versions(self): + """ + Cleans up old version slices from the S3 bucket. + """ + from templar import __version__ + + logger.info( + f"Cleaning up old versions from bucket {BUCKET_SECRETS['bucket_name']}" + ) + await delete_old_version_files(BUCKET_SECRETS["bucket_name"], __version__) + + def try_update(self): + """ + Automatic update entrypoint method. + """ + + if self.repo.head.is_detached or self.repo.active_branch.name != TARGET_BRANCH: + logger.info("Not on the target branch, skipping auto-update") + return + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + logger.info("Checking for updates...") + # Check if remote version is newer + is_update_needed = loop.run_until_complete(self.check_version_updated()) + if not is_update_needed: + logger.info("Local version is up to date. No updates needed.") + return + + logger.info("Attempting auto update") + # Attempt to update code + update_applied = self.attempt_update() + if not update_applied: + logger.info("No updates were applied. Continuing without restart.") + return + + # Now read the local version + local_version = self.get_local_version() + logger.info(f"Local version after update: {local_version}") + + # Synchronize dependencies + self.attempt_package_update() + + # Clean up old versions from the bucket + loop.run_until_complete(self.cleanup_old_versions()) + + # Restart application + logger.info("Attempting to restart the application...") + self.restart_app() + except Exception as e: + logger.exception("Exception during autoupdate process", exc_info=e) + finally: + loop.close() + + def get_pm2_process_name(self): + """ + Attempt to find the current process's PM2 name by using `pm2 jlist` and matching the current PID. + """ + current_pid = os.getpid() + try: + result = subprocess.run( + ["pm2", "jlist"], check=True, capture_output=True, text=True + ) + pm2_data = json.loads(result.stdout) + except Exception as e: + logger.error(f"Error running `pm2 jlist`: {e}") + return None + for proc in pm2_data: + if proc.get("pid") == current_pid: + return proc.get("name") + + return None + + def restart_app(self): + """Restarts the current application appropriately based on the runtime environment.""" + logger.info("Restarting application...") + pm2_name = self.get_pm2_process_name() + if pm2_name: + logger.info( + f"Detected PM2 environment. Restarting PM2 process '{pm2_name}'..." + ) + try: + subprocess.run(["pm2", "restart", pm2_name], check=True) + logger.info(f"Successfully restarted PM2 process '{pm2_name}'.") + sys.exit(0) + except Exception as e: + logger.error(f"Failed to restart PM2 process '{pm2_name}': {e}") + sys.exit(1) + else: + try: + logger.info( + "PM2 process name not found. Performing regular restart using subprocess.Popen" + ) + subprocess.Popen([sys.executable] + sys.argv) + logger.info("New process started. Exiting current process.") + sys.exit(0) + except Exception as e: + logger.exception("Failed to restart application.", exc_info=e) + sys.exit(1) + + def run(self): + """Thread run method to periodically check for updates.""" + while True: + try: + logger.info("Running autoupdate") + self.try_update() + except Exception as e: + logger.exception("Exception during autoupdate check", exc_info=e) + time.sleep(60) + + def get_local_version(self): + """ + Reads the local __version__ from the __init__.py file. + """ + try: + init_py_path = os.path.join(os.path.dirname(__file__), "__init__.py") + with open(init_py_path, "r") as f: + content = f.read() + for line in content.split("\n"): + if line.startswith("__version__"): + local_version = line.split("=")[1].strip().strip(" \"'") + return local_version + logger.error("Could not find __version__ in local __init__.py") + return None + except Exception as e: + logger.exception("Failed to read local version", exc_info=e) + return None diff --git a/src/tplr/chain.py b/src/tplr/chain.py new file mode 100644 index 0000000..4677360 --- /dev/null +++ b/src/tplr/chain.py @@ -0,0 +1,457 @@ +# The MIT License (MIT) +# © 2024 templar.tech + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# fmt: off + +# Global imports +import os +import time +import yaml +import torch +import asyncio +import numpy as np +import bittensor as bt +from typing import Dict, Optional +from bittensor import Wallet +from pydantic import ValidationError + +# Local imports +from .logging import logger +from .schemas import Bucket + + +class ChainManager: + """Base class for handling chain interactions.""" + + def __init__( + self, + config, + netuid: Optional[int] = None, + metagraph=None, + hparams=None, + fetch_interval: int = 60, # Fetch interval in seconds + wallet: Optional["bt.wallet"] = None, + bucket: Optional[Bucket] = None, + ): + """ + Initialize chain commitment handler. + + Args: + subtensor (bt.Subtensor): Subtensor instance for chain operations + netuid (int): Network UID for chain operations + metagraph: Metagraph instance containing network state + hparams: Hyperparameters namespace containing model configuration + fetch_interval (int): Interval in seconds between fetching commitments + wallet (bt.wallet, optional): Wallet to sign commitments + bucket (Bucket, optional): Bucket configuration to commit + """ + # self.subtensor = bt.subtensor(config=config) + # chain argument instead + self.config = config + self.netuid = netuid + self.metagraph = metagraph + self.hparams = hparams or {} + + # Block and window tracking + self.current_block = 0 + self.current_window = 0 + self.window_duration = self.hparams.blocks_per_window + self.window_time = 0 + self.window_seeds = {} + + # Events + self.block_event = asyncio.Event() + self.new_window_event = asyncio.Event() + + # Initialize bucket storage + self.commitments = {} + self.peers = [] + self.fetch_interval = fetch_interval + self._fetch_task = None + + # Store wallet and bucket + self.wallet = wallet + self.bucket = bucket + + # Try to commit bucket to the chain + if self.wallet and self.bucket: + # Commit bucket synchronously + asyncio.run(self.try_commit(self.wallet, self.bucket)) + else: + logger.warning("Wallet and bucket not provided; skipping try_commit.") + + # Fetch commitments synchronously to populate self.commitments + self.fetch_commitments() + + # Start fetching commitments + self.start_commitment_fetcher() + + def start_commitment_fetcher(self): + """Starts the background task to fetch commitments periodically.""" + if self._fetch_task is None: + try: + loop = asyncio.get_event_loop() + except RuntimeError: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + self._fetch_task = loop.create_task(self._fetch_commitments_periodically()) + + async def _fetch_commitments_periodically(self): + """Background task to periodically fetch commitments.""" + while True: + try: + commitments = await self.get_commitments() + if commitments: + self.commitments = commitments + self.update_peers_with_buckets() + logger.debug(f"Updated commitments: {self.commitments}") + except Exception as e: + logger.error(f"Error fetching commitments: {e}") + await asyncio.sleep(self.fetch_interval) + + def get_bucket(self, uid: int) -> Optional[Bucket]: + """Helper function to get the bucket for a given UID. + + Args: + uid (int): The UID to retrieve the bucket for. + + Returns: + Optional[Bucket]: The bucket corresponding to the UID, or None if not found. + """ + return self.commitments.get(uid) + + def get_all_buckets(self) -> Dict[int, Optional[Bucket]]: + """Helper function to get all buckets for all UIDs in the metagraph. + + Returns: + Dict[int, Optional[Bucket]]: Mapping of UIDs to their bucket configurations + """ + return {uid: self.get_bucket(uid) for uid in self.metagraph.uids} + + def block_to_window(self, block: int) -> int: + """Returns the slice window based on a block.""" + return int(block / self.hparams.window_length) + + def window_to_seed(self, window: int) -> str: + """Returns the slice window based on a block.""" + return str(self.subtensor.get_block_hash(window * self.hparams.window_length)) + + def block_listener(self, loop): + """Listens for new blocks and updates current block/window state. + + Args: + loop: The event loop to run the listener in + + This method subscribes to block headers from the subtensor network and: + - Updates self.current_block with the latest block number + - Updates self.current_window when crossing window boundaries + - Retries on connection errors until stop_event is set + """ + + def handler(event, _u, _s): + self.current_block = int(event["header"]["number"]) + if ( + int(self.current_block / self.hparams.blocks_per_window) + != self.current_window + ): + self.current_window = int( + self.current_block / self.hparams.blocks_per_window + ) + + while not self.stop_event.is_set(): + try: + bt.subtensor(config=self.config).substrate.subscribe_block_headers( + handler + ) + break + except Exception: + time.sleep(1) + + async def commit(self, wallet: "bt.wallet", bucket: Bucket) -> None: + """Commits bucket configuration to the chain. + + Args: + wallet (bt.wallet): Wallet to sign the commitment + bucket (Bucket): Bucket configuration to commit + """ + if self.netuid: + raise ValueError("Subtensor and netuid must be set for chain operations") + + concatenated = ( + bucket.account_id + bucket.access_key_id + bucket.secret_access_key + ) + self.subtensor.commit(wallet, self.netuid, concatenated) + logger.info( + f"Committed bucket configuration to chain for hotkey {wallet.hotkey.ss58_address}" + ) + + async def try_commit(self, wallet: Wallet, bucket: Bucket) -> None: + """Attempts to verify existing commitment matches current bucket config and commits if not. + + Args: + wallet (bt.wallet): Wallet to sign the commitment + bucket (Bucket): Current bucket configuration to verify/commit + """ + try: + # Get existing commitment + commitment = self.get_commitment( + self.metagraph.hotkeys.index(wallet.hotkey.ss58_address) + ) + + # Convert Bucket objects to concatenated strings for comparison + commitment_str = ( + commitment.name + + commitment.access_key_id + + commitment.secret_access_key + ) + current_str = bucket.name + bucket.access_key_id + bucket.secret_access_key + + logger.debug( + f"Comparing:\nCommitment: {commitment_str}\nCurrent: {current_str}" + ) + + if current_str != commitment_str: + raise ValueError("Bucket commitment data does not match") + + except Exception as e: + logger.error(f"Commitment error: {str(e)}") + await self.commit(wallet, bucket) + + def get_commitment(self, uid: int) -> Bucket: + """Retrieves and parses committed bucket configuration data for a given + UID. + + This method fetches commitment data for a specific UID from the + subtensor network and decodes it into a structured format. The + retrieved data is split into the following fields: + - Account ID: A string of fixed length 32 characters. + - Access key ID: A string of fixed length 32 characters. + - Secret access key: A string of variable length (up to 64 characters). + + The parsed fields are then mapped to an instance of the `Bucket` class. + When initializing the Bucket object, the account ID is also used as the + bucket name. + + The retrieval process involves: + - Fetching the commitment data for the specified UID using the + configured `netuid` from the subtensor network. + - Splitting the concatenated string into individual fields based on + their expected lengths and order. + - Mapping the parsed fields to a `Bucket` instance. + + **Note:** The order of fields (bucket name, account ID, access key ID, + secret access key) in the concatenated string is critical for accurate + parsing. + + Args: + uid: The UID of the neuron whose commitment data is being + retrieved. + + Returns: + Bucket: An instance of the `Bucket` class containing the parsed + bucket configuration details. + + Raises: + ValueError: If the parsed data does not conform to the expected + format for the `Bucket` class. + Exception: If an error occurs while retrieving the commitment data + from the subtensor network. + """ + + subtensor = bt.subtensor(config=self.config) + try: + concatenated = subtensor.get_commitment(self.netuid, uid) + logger.success(f"Commitment fetched: {concatenated}") + except Exception as e: + raise Exception(f"Couldn't get commitment from uid {uid} because {e}") + if len(concatenated) != 128: + raise ValueError( + f"Commitment '{concatenated}' is of length {len(concatenated)} but should be of length 128." + ) + + try: + return Bucket( + name=concatenated[:32], + account_id=concatenated[:32], + access_key_id=concatenated[32:64], + secret_access_key=concatenated[64:], + ) + except ValidationError as e: + raise ValueError(f"Invalid data in commitment: {e}") + + async def get_commitments(self, block: Optional[int] = None) -> Dict[int, Bucket]: + """Retrieves all bucket commitments from the chain. + + Args: + block (int, optional): Block number to query at + + Returns: + Dict[int, Bucket]: Mapping of UIDs to their bucket configurations + """ + # if self.netuid or not self.metagraph: + # raise ValueError( + # "Subtensor, netuid and metagraph must be set for chain operations" + # ) + subtensor = bt.subtensor(config=self.config) + substrate = subtensor.substrate + result = substrate.query_map( + module="Commitments", + storage_function="CommitmentOf", + params=[self.netuid], + block_hash=None if block is None else substrate.get_block_hash(block), + ) + + hotkey_to_uid = dict(zip(self.metagraph.hotkeys, self.metagraph.uids)) + commitments = {} + + for key, value in result: + hotkey = key.value + if hotkey not in hotkey_to_uid: + continue + + uid = hotkey_to_uid[hotkey] + commitment_info = value.value.get("info", {}) + fields = commitment_info.get("fields", []) + + if not fields or not isinstance(fields[0], dict): + continue + + field_value = next(iter(fields[0].values())) + if field_value.startswith("0x"): + field_value = field_value[2:] + + try: + concatenated = bytes.fromhex(field_value).decode("utf-8").strip() + if len(concatenated) != 128: + logger.error( + f"Invalid commitment length for UID {uid}: {len(concatenated)}" + ) + continue + + bucket = Bucket( + name=concatenated[:32], + account_id=concatenated[:32], + access_key_id=concatenated[32:64], + secret_access_key=concatenated[64:], + ) + commitments[uid] = bucket + logger.success(f"Retrieved bucket commitment for UID {uid}") + + except Exception as e: + logger.error(f"Failed to decode commitment for UID {uid}: {e}") + continue + + return commitments + + async def get_bucket_for_neuron(self, wallet: "bt.wallet") -> Optional[Bucket]: + """Get bucket configuration for a specific neuron's wallet + + Args: + wallet (bt.wallet): The wallet to get bucket for + + Returns: + Optional[Bucket]: The bucket assigned to this neuron, or None if not found + """ + try: + # Get UID by finding hotkey's index in metagraph + uid = self.metagraph.hotkeys.index(wallet.hotkey.ss58_address) + return await self.get_bucket(uid) + except ValueError: + logger.warning( + f"Hotkey {wallet.hotkey.ss58_address} not found in metagraph" + ) + return None + + def fetch_commitments(self): + """Synchronously fetches commitments and updates self.commitments.""" + try: + loop = asyncio.get_event_loop() + except RuntimeError: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + commitments = loop.run_until_complete(self.get_commitments()) + if commitments: + self.commitments = commitments + self.update_peers_with_buckets() + logger.debug(f"Fetched commitments synchronously: {self.commitments}") + else: + logger.warning("No commitments fetched.") + + def get_hotkey(self, uid: int) -> Optional[str]: + """Returns the hotkey for a given UID.""" + # Handle different data types for uids + if isinstance(self.metagraph.uids, (np.ndarray, torch.Tensor)): + uids_list = self.metagraph.uids.tolist() + else: + uids_list = self.metagraph.uids + + # Handle different data types for hotkeys + if isinstance(self.metagraph.hotkeys, (np.ndarray, torch.Tensor)): + hotkeys_list = self.metagraph.hotkeys.tolist() + else: + hotkeys_list = self.metagraph.hotkeys + + if uid in uids_list: + index = uids_list.index(uid) + return hotkeys_list[index] + else: + return None + + def update_peers_with_buckets(self): + """Updates the list of peers (UIDs) that have buckets, excluding validators.""" + # Create a mapping from UIDs to their stakes + uid_to_stake = dict(zip(self.metagraph.uids.tolist(), self.metagraph.S.tolist())) + + # Filter peers that have buckets and have stake <= 10000 (miners) + self.peers = [ + int(uid) for uid in self.commitments.keys() + if uid_to_stake.get(int(uid), 0) <= 10000 + ] + logger.info(f"Updated peers with buckets (excluding validators): {self.peers}") + + +def get_own_bucket() -> Bucket: + """Parses the credentials from .env.yaml to create a Bucket object.""" + env_file = ".env.yaml" + if not os.path.isfile(env_file): + logger.error(f"The {env_file} file was not found.") + raise FileNotFoundError(f"The {env_file} file was not found.") + + try: + with open(env_file, "r") as file: + credentials = yaml.safe_load(file) + except yaml.YAMLError as e: + logger.error(f"Error parsing {env_file}: {e}") + raise e + + try: + account_id = credentials["account_id"] + read_access_key_id = credentials["read"]["access_key_id"] + read_secret_access_key = credentials["read"]["secret_access_key"] + + # Create a Bucket object + bucket = Bucket( + name=account_id, + account_id=account_id, + access_key_id=read_access_key_id, + secret_access_key=read_secret_access_key, + ) + logger.debug(f"Parsed bucket from {env_file}: {bucket}") + return bucket + except KeyError as e: + logger.error(f"Missing key in {env_file}: {e}") + raise e diff --git a/src/tplr/comms.py b/src/tplr/comms.py new file mode 100644 index 0000000..60109f6 --- /dev/null +++ b/src/tplr/comms.py @@ -0,0 +1,612 @@ +# The MIT License (MIT) +# © 2024 templar.tech + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# fmt: off + +# Global imports +import os +import re +import time +import yaml +import torch +import asyncio +import aiofiles +import numpy as np +import bittensor as bt +from typing import List, Dict, Optional, Tuple +from aiobotocore.session import get_session + +# Local imports +from . import __version__ +from .schemas import Bucket +from .logging import logger +from .chain import ChainManager +from .config import client_config, BUCKET_SECRETS + +CF_REGION_NAME: str = "enam" + + +def get_base_url(account_id): + """Constructs the base URL for the R2 storage endpoint.""" + return f"https://{account_id}.r2.cloudflarestorage.com" + + +class Comms(ChainManager): + def __init__( + self, + wallet: "bt.wallet", + save_location: str = "/tmp", + key_prefix: str = "slice", + **kwargs + ): + self.wallet = wallet + self.bucket = self.get_own_bucket() + super().__init__( + config=kwargs.get('config'), + netuid=kwargs.get('netuid'), + metagraph=kwargs.get('metagraph'), + hparams=kwargs.get('hparams'), + wallet=self.wallet, + bucket=self.bucket, + ) + # Use the hotkey directly in the save_location + hotkey = self.wallet.hotkey.ss58_address + self.save_location = os.path.join("/tmp", f"hotkey_{hotkey}") + os.makedirs(self.save_location, exist_ok=True) + self.key_prefix = key_prefix + self.session = get_session() + self.lock = asyncio.Lock() + # Load bucket secrets + self.bucket_secrets = BUCKET_SECRETS + + def get_own_bucket(self) -> Bucket: + """Parses the credentials from .env.yaml to create a Bucket object.""" + env_file = ".env.yaml" + if not os.path.isfile(env_file): + logger.error(f"The {env_file} file was not found.") + raise FileNotFoundError(f"The {env_file} file was not found.") + + try: + with open(env_file, "r") as file: + credentials = yaml.safe_load(file) + except yaml.YAMLError as e: + logger.error(f"Error parsing {env_file}: {e}") + raise e + + try: + account_id = credentials["account_id"] + read_access_key_id = credentials["read"]["access_key_id"] + read_secret_access_key = credentials["read"]["secret_access_key"] + + # Create a Bucket object + bucket = Bucket( + name=account_id, + account_id=account_id, + access_key_id=read_access_key_id, + secret_access_key=read_secret_access_key, + ) + logger.debug(f"Parsed bucket from {env_file}: {bucket}") + return bucket + except KeyError as e: + logger.error(f"Missing key in {env_file}: {e}") + raise e + + async def put( + self, + state_dict_or_path, + uid: str, + window_or_block: int, + key: Optional[str] = None, + ): + """ + Uploads data to the R2 bucket. Handles both small state_dicts and large checkpoint files. + + Args: + state_dict_or_path (dict or str): The state dictionary to upload or the path to the checkpoint file. + uid (str): Unique identifier for the upload (e.g., hotkey or user ID). + window_or_block (int): The window number or block number. + key (str, optional): Custom key for the filename. Defaults to self.key_prefix. + """ + key = key or self.key_prefix + hotkey = self.wallet.hotkey.ss58_address + + if isinstance(state_dict_or_path, dict): + # Handle state_dict upload + filename = f"{key}-{window_or_block}-{hotkey}-v{__version__}.pt" + temp_file_path = os.path.join(self.save_location, filename) + + # Ensure the save directory exists + os.makedirs(self.save_location, exist_ok=True) + + try: + # Save the state_dict to a temporary file + torch.save(state_dict_or_path, temp_file_path) + logger.debug(f"Temporary file saved at {temp_file_path}") + except Exception as e: + logger.error(f"Error saving temporary file: {e}") + raise + + file_path = temp_file_path + elif isinstance(state_dict_or_path, str): + # Handle checkpoint file upload + file_path = state_dict_or_path + filename = os.path.basename(file_path) + else: + raise ValueError("state_dict_or_path must be a state_dict or a file path.") + + # Determine if multipart upload is needed based on file size + file_size = os.path.getsize(file_path) + use_multipart = file_size > 5 * 1024 * 1024 * 1024 # 5 GB threshold + + # Upload the file to R2 bucket + try: + async with self.session.create_client( + "s3", + endpoint_url=get_base_url(BUCKET_SECRETS["account_id"]), + region_name=CF_REGION_NAME, + config=client_config, + aws_access_key_id=BUCKET_SECRETS["write"]["access_key_id"], + aws_secret_access_key=BUCKET_SECRETS["write"]["secret_access_key"], + ) as s3_client: + if use_multipart: + await self._multipart_upload(s3_client, filename, file_path) + else: + async with aiofiles.open(file_path, "rb") as f: + data = await f.read() + await s3_client.put_object( + Bucket=self.bucket.name, Key=filename, Body=data + ) + logger.debug(f"Successfully uploaded {filename} to R2 bucket.") + except Exception as e: + logger.error(f"Failed to upload {filename} to R2 bucket: {e}") + raise + finally: + # Clean up the temporary file if it exists and was created + if isinstance(state_dict_or_path, dict): + logger.debug(f"Attempting to delete temporary file at {temp_file_path}") + try: + if os.path.exists(temp_file_path): + os.remove(temp_file_path) + logger.debug(f"Deleted temporary file at {temp_file_path}") + else: + logger.debug(f"Temporary file does not exist at {temp_file_path}") + except Exception as e: + logger.error(f"Error during cleanup of temporary file: {e}") + + async def _multipart_upload(self, s3_client, filename, file_path): + """Handles multipart upload for large files.""" + bucket = BUCKET_SECRETS["bucket_name"].split("/")[-1] + chunk_size = 16 * 1024 * 1024 # 16MB chunks + max_concurrent_uploads = 10 + max_retries = 3 + retry_delay = 5 + + # Initialize multipart upload + response = await s3_client.create_multipart_upload( + Bucket=bucket, + Key=filename, + CacheControl="no-cache, no-store, must-revalidate", + ) + upload_id = response["UploadId"] + logger.info(f"Initiated multipart upload with ID: {upload_id}") + + try: + total_size = os.path.getsize(file_path) + total_parts = (total_size + chunk_size - 1) // chunk_size + parts = {} + semaphore = asyncio.Semaphore(max_concurrent_uploads) + upload_tasks = [] + + async def upload_part(part_number: int, offset: int): + """Upload a single part with retries.""" + for attempt in range(max_retries): + try: + async with semaphore: + async with aiofiles.open(file_path, "rb") as f: + await f.seek(offset) + chunk = await f.read(min(chunk_size, total_size - offset)) + + response = await s3_client.upload_part( + Bucket=bucket, + Key=filename, + PartNumber=part_number, + UploadId=upload_id, + Body=chunk, + ) + + return { + "PartNumber": part_number, + "ETag": response["ETag"], + } + except Exception as e: + if attempt < max_retries - 1: + logger.warning(f"Retry {attempt + 1}/{max_retries} for part {part_number}: {str(e)}") + await asyncio.sleep(retry_delay) + else: + raise + + # Create upload tasks for all parts + for part_number in range(1, total_parts + 1): + offset = (part_number - 1) * chunk_size + task = asyncio.create_task(upload_part(part_number, offset)) + upload_tasks.append(task) + + # Wait for all uploads and collect results + completed_parts = await asyncio.gather(*upload_tasks) + parts = [part for part in completed_parts if part is not None] + parts.sort(key=lambda x: x["PartNumber"]) + + # Complete multipart upload + await s3_client.complete_multipart_upload( + Bucket=bucket, + Key=filename, + UploadId=upload_id, + MultipartUpload={"Parts": parts}, + ) + logger.info(f"Successfully uploaded checkpoint {filename}") + except Exception as e: + logger.error(f"Error during multipart upload: {str(e)}") + await s3_client.abort_multipart_upload( + Bucket=bucket, Key=filename, UploadId=upload_id + ) + logger.info(f"Aborted multipart upload {upload_id}") + raise + + async def get( + self, + uid: str, + window: int, + key: Optional[str] = None, + timeout: int = 30, + local: bool = False + ) -> Optional[Dict[str, torch.Tensor]]: + """ + Downloads data from the R2 bucket. Handles both model state dicts and large checkpoint files. + + Args: + uid (str): Unique identifier for the download. + window (int): The window number for synchronization. + key (str, optional): Custom key for the filename. + timeout (int): Timeout in seconds for the download operation. + local (bool): If True, keeps the downloaded file on disk. + + Returns: + Optional[Dict[str, torch.Tensor]]: The downloaded state dictionary. + """ + key = key or self.key_prefix + hotkey = self.get_hotkey(int(uid)) + if hotkey is None: + logger.error(f"No hotkey found for uid {uid}") + return None + + filename = f"{key}-{window}-{hotkey}-v{__version__}.pt" + temp_file_path = os.path.join(self.save_location, filename) + + bucket = self.get_bucket(int(uid)) + if bucket is None: + logger.debug(f"Bucket for uid {uid} not found. Skipping...") + return None + + try: + async with self.session.create_client( + "s3", + endpoint_url=get_base_url(bucket.account_id), + region_name=CF_REGION_NAME, + config=client_config, + aws_access_key_id=bucket.access_key_id, + aws_secret_access_key=bucket.secret_access_key, + ) as s3_client: + # Check file size first + response = await s3_client.head_object( + Bucket=bucket.name, + Key=filename + ) + file_size = response['ContentLength'] + + # Use multipart download for files larger than 100MB + if file_size > 100 * 1024 * 1024: + success = await self._download_large_file(s3_client, filename, temp_file_path) + if not success: + return None + else: + async def download(): + response = await s3_client.get_object( + Bucket=bucket.name, + Key=filename + ) + async with aiofiles.open(temp_file_path, "wb") as f: + await f.write(await response["Body"].read()) + + await asyncio.wait_for(download(), timeout=timeout) + + # Load the state_dict + state_dict = torch.load(temp_file_path, map_location="cpu", weights_only=True) + logger.debug(f"Successfully downloaded {filename} from R2 bucket.") + + # Clean up unless local=True + if not local: + os.remove(temp_file_path) + + return state_dict + + except asyncio.TimeoutError: + logger.error(f"Timeout while downloading {filename} from R2 bucket.") + except Exception as e: + logger.error(f"Failed to download {filename} from R2 bucket: {e}") + finally: + if not local and os.path.exists(temp_file_path): + os.remove(temp_file_path) + + return None + + async def get_with_retry( + self, + uid: str, + window: int, + key: Optional[str] = None, + timeout: int = 30, + retry_interval: float = 0.1, + ): + """ + Attempts to download data from the R2 bucket, retrying until success or timeout. + + Args: + uid (str): Unique identifier for the download. + window (int): The window number for synchronization. + key (str, optional): Custom key for the filename. + timeout (int): Total timeout duration for retries. + retry_interval (float): Time to wait between retries. + + Returns: + dict: The state dictionary downloaded from the bucket. + """ + start_time = time.time() + while True: + state_dict = await self.get(uid, window, key, timeout) + if state_dict is not None: + return state_dict + if time.time() - start_time > timeout: + logger.error(f"Exceeded timeout while downloading data for UID {uid}.") + return None + await asyncio.sleep(retry_interval) + + async def gather( + self, + state_dict: Dict[str, torch.Tensor], + my_uid: str, + uids: List[str], + window: int, + key: Optional[str] = None, + timeout: int = 30, + device: str = "cpu", + ) -> Dict[str, List[torch.Tensor]]: + """ + Gathers slices from multiple peers and assembles them for aggregation. + + Args: + state_dict (Dict[str, torch.Tensor]): Local state dictionary. + my_uid (str): This node's unique identifier. + uids (List[str]): List of peer UIDs to gather data from. + window (int): The window number for synchronization. + key (str, optional): Custom key for filenames. + timeout (int): Timeout for gathering data from each peer. + device (str): Device to map tensors onto. + + Returns: + Dict[str, List[torch.Tensor]]: Aggregated state dictionaries from all peers. + """ + key = key or self.key_prefix + # Put own state_dict to the bucket + await self.put(state_dict, my_uid, window, key) + + time.sleep(5) + + # Gather state_dicts from peers + gather_tasks = [ + self.get_with_retry(uid=uid, window=window, key=key, timeout=timeout) + for uid in uids + ] + + responses = await asyncio.gather(*gather_tasks) + # Initialize the gather_result dictionary + gather_result = {param_name: [] for param_name in state_dict.keys()} + # Assemble the results + for idx, peer_state in enumerate(responses): + if peer_state is None: + # Handle missing peer data, e.g., fill with zeros or skip + for param_name in state_dict.keys(): + gather_result[param_name].append( + torch.zeros_like(state_dict[param_name]).to(device) + ) + else: + for param_name in state_dict.keys(): + gather_result[param_name].append(peer_state[param_name].to(device)) + + return gather_result + + def get_highest_stake_validator(self) -> Tuple[Optional[int], float]: + """Returns the UID and stake of the neuron with the highest stake.""" + stakes = self.metagraph.S + logger.info(stakes) + + # Convert numpy array to torch tensor if needed + if isinstance(stakes, np.ndarray): + stakes = torch.from_numpy(stakes) + + # Check if any stakes are non-zero + if torch.all(stakes == 0): + return None, 0.0 + + highest_stake_uid = torch.argmax(stakes).item() + stake = stakes[highest_stake_uid].item() + + # Validate the stake is actually non-zero + if stake == 0: + return None, 0.0 + + return highest_stake_uid, stake + + async def get_latest_checkpoint(self) -> Optional[str]: + """ + Attempts to get the latest checkpoint from the highest stake validator. + Returns the checkpoint path if successful, None otherwise. + """ + validator_uid, stake = self.get_highest_stake_validator() + if stake == 0: + logger.warning("No active validators found") + return None + + # Get the current block and calculate window + current_block = self.subtensor.block + current_window = int(current_block / self.hparams.blocks_per_window) + + # Try last 5 windows to find most recent checkpoint + for window in range(current_window, max(0, current_window - 5), -1): + try: + checkpoint = await self.get( + uid=str(validator_uid), + window=window, + key='checkpoint', + timeout=30 # Longer timeout for checkpoint downloads + ) + if checkpoint is not None: + # Save checkpoint to disk + checkpoint_path = os.path.join(self.save_location, f'checkpoint-{window}.pt') + torch.save(checkpoint, checkpoint_path) + logger.info(f"Downloaded checkpoint from validator {validator_uid} at window {window}") + return checkpoint_path + except Exception as e: + logger.warning(f"Failed to get checkpoint from window {window}: {e}") + continue + + return None + + async def _download_large_file(self, s3_client, filename: str, temp_file_path: str): + """Handles downloading large files using multipart download.""" + try: + # Get file size + response = await s3_client.head_object( + Bucket=self.bucket.name, + Key=filename + ) + file_size = response['ContentLength'] + + # Use 16MB chunks for multipart download + chunk_size = 16 * 1024 * 1024 + total_parts = (file_size + chunk_size - 1) // chunk_size + + async with aiofiles.open(temp_file_path, 'wb') as f: + for part in range(total_parts): + start = part * chunk_size + end = min(start + chunk_size, file_size) + + response = await s3_client.get_object( + Bucket=self.bucket.name, + Key=filename, + Range=f'bytes={start}-{end-1}' + ) + + chunk = await response['Body'].read() + await f.write(chunk) + + logger.debug(f"Successfully downloaded large file {filename}") + return True + except Exception as e: + logger.error(f"Error downloading large file {filename}: {e}") + return False + + async def cleanup_old_checkpoints(self, keep_last: int = 3): + """ + Removes old checkpoints from storage, keeping only the most recent ones. + + Args: + keep_last (int): Number of most recent checkpoints to keep + """ + try: + async with self.session.create_client( + "s3", + endpoint_url=get_base_url(self.bucket.account_id), + region_name=CF_REGION_NAME, + config=client_config, + aws_access_key_id=self.bucket.access_key_id, + aws_secret_access_key=self.bucket.secret_access_key, + ) as s3_client: + # List all checkpoint files + paginator = s3_client.get_paginator("list_objects_v2") + checkpoint_files = [] + + async for page in paginator.paginate( + Bucket=self.bucket.name, + Prefix='checkpoint' + ): + for obj in page.get("Contents", []): + if obj["Key"].startswith("checkpoint"): + checkpoint_files.append(obj) + + # Sort by last modified time + checkpoint_files.sort(key=lambda x: x["LastModified"], reverse=True) + + # Delete older checkpoints + if len(checkpoint_files) > keep_last: + to_delete = checkpoint_files[keep_last:] + await s3_client.delete_objects( + Bucket=self.bucket.name, + Delete={"Objects": [{"Key": obj["Key"]} for obj in to_delete]} + ) + logger.info(f"Deleted {len(to_delete)} old checkpoints") + + except Exception as e: + logger.error(f"Error cleaning up old checkpoints: {e}") + + +async def delete_old_version_files(bucket_name: str, current_version: str): + """ + Deletes files from the S3 bucket that do not match the current version. + + Args: + bucket_name (str): The name of the S3 bucket. + current_version (str): The current version string. + """ + session = get_session() + async with session.create_client( + "s3", + endpoint_url=get_base_url(BUCKET_SECRETS["account_id"]), + region_name=CF_REGION_NAME, + config=client_config, + aws_access_key_id=BUCKET_SECRETS["write"]["access_key_id"], + aws_secret_access_key=BUCKET_SECRETS["write"]["secret_access_key"], + ) as s3_client: + paginator = s3_client.get_paginator("list_objects_v2") + async for page in paginator.paginate(Bucket=bucket_name): + to_delete = [] + for obj in page.get("Contents", []): + filename = obj["Key"] + # Check if the file version matches the current version + match = re.match(r".+-v(.+)\.pt$", filename) + if match: + file_version = match.group(1) + if file_version != current_version: + to_delete.append({"Key": filename}) + logger.debug(f"Scheduled for deletion: {filename}") + # Delete old versions in batches of 1000 (S3 limit for delete_objects) + if to_delete: + response = await s3_client.delete_objects( + Bucket=bucket_name, Delete={"Objects": to_delete} + ) + deleted = response.get("Deleted", []) + logger.info( + f"Deleted {len(deleted)} old version files from bucket {bucket_name}" + ) diff --git a/src/tplr/compress.py b/src/tplr/compress.py new file mode 100644 index 0000000..13cb6e7 --- /dev/null +++ b/src/tplr/compress.py @@ -0,0 +1,306 @@ +# The MIT License (MIT) +# © 2024 templar.tech + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# fmt: off + +# Adapted from https://github.com/bloc97/DeMo and NousResearch +# Original implementation by bloc97 (https://github.com/bloc97/DeMo) + + +# Global imports +import math +import torch +import torch.fft +from einops import rearrange + + +class TransformDCT: + @torch.no_grad() + def __init__(self, model, target_chunk, norm="ortho"): + self.target_chunk = target_chunk + + self.shape_dict = dict() + self.f_dict = dict() + self.b_dict = dict() + + # Get all variants of model tensor sizes + # Generate all possible valid DCT sizes for model tensors + for _, p in model.named_parameters(): + if not p.requires_grad: + continue + for s in p.shape: + # Get the closest smallest divisor to the targeted DCT size + sc = _get_smaller_split(s, self.target_chunk) + self.shape_dict[s] = sc + + # Pregenerate DCT basis matrices + if sc not in self.f_dict: + identity_matrix = torch.eye(sc) + self.f_dict[sc] = _dct(identity_matrix, norm=norm).to(p.dtype).to(p.device) + self.b_dict[sc] = _idct(identity_matrix, norm=norm).to(p.dtype).to(p.device) + + @torch.no_grad() + def einsum_2d(self, x, b, d=None): + if d is None: + return torch.einsum("...ij, jb -> ...ib", x, b) + else: + # Note: b-c axis output is transposed to chunk DCT in 2D + return torch.einsum("...ijkl, jb, ld -> ...ikbd", x, b, d) + + @torch.no_grad() + def einsum_2d_t(self, x, b, d=None): + if d is None: + return torch.einsum("...ij, jb -> ...ib", x, b) + else: + # Note: b-c axis output is transposed to chunk DCT in 2D + return torch.einsum("...ijkl, kb, ld -> ...ibjd", x, b, d) + + @torch.no_grad() + def encode(self, x): + if len(x.shape) > 1: # 2D weights + n1 = self.shape_dict[x.shape[0]] + n2 = self.shape_dict[x.shape[1]] + n1w = self.f_dict[n1].to(x.device) + n2w = self.f_dict[n2].to(x.device) + self.f_dict[n1] = n1w + self.f_dict[n2] = n2w + + x = rearrange(x, "(y h) (x w) -> y h x w", h=n1, w=n2) + x = self.einsum_2d(x, n1w, n2w) + + else: # 1D weights + n1 = self.shape_dict[x.shape[0]] + n1w = self.f_dict[n1].to(x.device) + self.f_dict[n1] = n1w + + x = rearrange(x, "(x w) -> x w", w=n1) + x = self.einsum_2d(x, n1w) + + return x + + @torch.no_grad() + def decode(self, x): + if len(x.shape) > 2: # 2D weights + n1 = x.shape[2] + n2 = x.shape[3] + n1w = self.b_dict[n1].to(x.device) + n2w = self.b_dict[n2].to(x.device) + self.b_dict[n1] = n1w + self.b_dict[n2] = n2w + + x = self.einsum_2d_t(x, n1w, n2w) + x = rearrange(x, "y h x w -> (y h) (x w)") + + else: # 1D weights + n1 = x.shape[1] + n1w = self.b_dict[n1].to(x.device) + self.b_dict[n1] = n1w + + x = self.einsum_2d_t(x, n1w) + x = rearrange(x, "x w -> (x w)") + + return x + + +class CompressDCT: + @torch.no_grad() + def __init__(self): + pass + + def _clamp_topk(self, x, topk): + if topk > x.shape[-1]: + topk = x.shape[-1] + if topk < 1: + topk = 1 + return topk + + @torch.no_grad() + def compress(self, x, topk): + xshape = x.shape + if len(x.shape) > 2: # 2D weights + x = rearrange(x, "y x h w -> y x (h w)") + + # Limit topk to max size + totalk = x.shape[-1] + topk = self._clamp_topk(x, topk) + + idx = torch.topk(x.abs(), k=topk, dim=-1, largest=True, sorted=False).indices + val = torch.gather(x, dim=-1, index=idx) + + return idx, val, xshape, totalk + + @torch.no_grad() + def decompress(self, p, idx, val, xshape, totalk): + x = torch.zeros(xshape, device=p.device, dtype=p.dtype) + + if len(xshape) > 2: # 2D weights + x = rearrange(x, "y x h w -> y x (h w)") + + # TODO: Careful, this is nondeterministic across different CUDA devices! might cause errors to accumulate between nodes! + x.scatter_reduce_(dim=-1, index=idx, src=val, reduce="mean", include_self=False).reshape(xshape) + + if len(x.shape) > 2: # 2D weights + x = rearrange(x, "y x (h w) -> y x h w", h=xshape[2]) + + return x + + @torch.no_grad() + def batch_decompress(self, p, idx, val, xshape, totalk): + idx = torch.concatenate(idx, dim=-1).to(device=p.device) + val = torch.concatenate(val, dim=-1).to(device=p.device) + return self.decompress(p, idx, val, xshape, totalk) + + +# Code modified and sourced from https://github.com/zh217/torch-dct +def _dct_fft_impl(v): + return torch.view_as_real(torch.fft.fft(v, dim=1)) + + +def _idct_irfft_impl(V): + return torch.fft.irfft(torch.view_as_complex(V), n=V.shape[1], dim=1) + + +def _dct(x, norm=None): + """ + Discrete Cosine Transform, Type II (a.k.a. the DCT) + + For the meaning of the parameter `norm`, see: + https://docs.scipy.org/doc/scipy-0.14.0/reference/generated/scipy.fftpack.dct.html + + :param x: the input signal + :param norm: the normalization, None or 'ortho' + :return: the DCT-II of the signal over the last dimension + """ + x_shape = x.shape + N = x_shape[-1] + x = x.contiguous().view(-1, N) + + v = torch.cat([x[:, ::2], x[:, 1::2].flip([1])], dim=1) + + Vc = _dct_fft_impl(v) + + k = -torch.arange(N, dtype=x.dtype, device=x.device)[None, :] * math.pi / (2 * N) + W_r = torch.cos(k) + W_i = torch.sin(k) + + V = Vc[:, :, 0] * W_r - Vc[:, :, 1] * W_i + + if norm == "ortho": + V[:, 0] /= math.sqrt(N) * 2 + V[:, 1:] /= math.sqrt(N / 2) * 2 + + V = 2 * V.view(*x_shape) + + return V + + +def _idct(X, norm=None): + """ + The inverse to DCT-II, which is a scaled Discrete Cosine Transform, Type III + + Our definition of idct is that idct(dct(x)) == x + + For the meaning of the parameter `norm`, see: + https://docs.scipy.org/doc/scipy-0.14.0/reference/generated/scipy.fftpack.dct.html + + :param X: the input signal + :param norm: the normalization, None or 'ortho' + :return: the inverse DCT-II of the signal over the last dimension + """ + + x_shape = X.shape + N = x_shape[-1] + + X_v = X.contiguous().view(-1, x_shape[-1]) / 2 + + if norm == "ortho": + X_v[:, 0] *= math.sqrt(N) * 2 + X_v[:, 1:] *= math.sqrt(N / 2) * 2 + + k = torch.arange(x_shape[-1], dtype=X.dtype, device=X.device)[None, :] * math.pi / (2 * N) + W_r = torch.cos(k) + W_i = torch.sin(k) + + V_t_r = X_v + V_t_i = torch.cat([X_v[:, :1] * 0, -X_v.flip([1])[:, :-1]], dim=1) + + V_r = V_t_r * W_r - V_t_i * W_i + V_i = V_t_r * W_i + V_t_i * W_r + + V = torch.cat([V_r.unsqueeze(2), V_i.unsqueeze(2)], dim=2) + + v = _idct_irfft_impl(V) + x = v.new_zeros(v.shape) + x[:, ::2] += v[:, : N - (N // 2)] + x[:, 1::2] += v.flip([1])[:, : N // 2] + + return x.view(*x_shape) + + +def _get_prime_divisors(n): + divisors = [] + while n % 2 == 0: + divisors.append(2) + n //= 2 + while n % 3 == 0: + divisors.append(3) + n //= 3 + i = 5 + while i * i <= n: + for k in (i, i + 2): + while n % k == 0: + divisors.append(k) + n //= k + i += 6 + if n > 1: + divisors.append(n) + return divisors + + +def _get_divisors(n): + divisors = [] + if n == 1: + divisors.append(1) + elif n > 1: + prime_factors = _get_prime_divisors(n) + divisors = [1] + last_prime = 0 + factor = 0 + slice_len = 0 + # Find all the products that are divisors of n + for prime in prime_factors: + if last_prime != prime: + slice_len = len(divisors) + factor = prime + else: + factor *= prime + for i in range(slice_len): + divisors.append(divisors[i] * factor) + last_prime = prime + divisors.sort() + return divisors + + +def _get_smaller_split(n, close_to): + all_divisors = _get_divisors(n) + for ix, val in enumerate(all_divisors): + if val == close_to: + return val + if val > close_to: + if ix == 0: + return val + return all_divisors[ix - 1] + return n \ No newline at end of file diff --git a/src/tplr/config.py b/src/tplr/config.py new file mode 100644 index 0000000..0cbb8d0 --- /dev/null +++ b/src/tplr/config.py @@ -0,0 +1,46 @@ +# The MIT License (MIT) +# © 2024 templar.tech + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# fmt: off + +# Global imports +import os +import sys +import yaml +from pathlib import Path + +# Local imports +import botocore.config +from dotenv import dotenv_values +from .logging import logger + +# Load environment variables +env_config = {**dotenv_values(".env"), **os.environ} +envfile_path = Path(__file__).parents[2] / ".env.yaml" +try: + with open(envfile_path, "r") as file: + BUCKET_SECRETS = yaml.safe_load(file) +except FileNotFoundError: + logger.error( + f"{envfile_path} not found. Please create it with the help of `.env-template.yaml`." + ) + sys.exit() +BUCKET_SECRETS["bucket_name"] = BUCKET_SECRETS["account_id"] + +# Configure the S3 client +client_config = botocore.config.Config( + max_pool_connections=256, +) diff --git a/src/tplr/dataset.py b/src/tplr/dataset.py new file mode 100644 index 0000000..96e7d0c --- /dev/null +++ b/src/tplr/dataset.py @@ -0,0 +1,512 @@ +# The MIT License (MIT) +# © 2024 templar.tech + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# fmt: off + +# Global imports +import random +import typing +import asyncio +import aiohttp +import numpy as np +from torch.utils.data import IterableDataset +from transformers import AutoTokenizer + + +class SubsetLoader(IterableDataset): + """ + Base class for data-specific subset loader classes. + + # TODO: Make this class abstract + """ + + def __init__( + self, + batch_size=None, + sequence_length=None, + num_pages=None, + tokenizer: AutoTokenizer = None, + pack_samples: bool = False, + ): + self.batch_size = batch_size + self.sequence_length = sequence_length + self.num_pages = num_pages + self.tokenizer = tokenizer + self.pack_samples = pack_samples + + self.num_rows_per_page = 100 + + # Buffer to hold pages loaded from the api + self.buffer = [] + + # Buffer to hold pages already loaded into a batch + self.used_buffer = [] + + # Buffer to hold padded pages + self.padded_buffer = [] + + self.lock = asyncio.Lock() # For thread-safe operations + + async def fetch_data_for_pages(self, pages): + """ + Set the pages to be used to fill the buffer. Then fetch the page data + to the buffer. + """ + + self.pages = pages + + # Empty the buffer if it is not. + self.buffer = [] + + async with aiohttp.ClientSession() as session: + tasks = [self._fetch_data_for_page(page, session) for page in self.pages] + await asyncio.gather(*tasks) + + async def _fetch_data_for_page(self, page, session): + retry_limit = 10 + attempt = 0 + while attempt < retry_limit: + config_name, page_number, split = page + + # Create the request parameters + params = dict( + dataset=self.name, + config=config_name, + split=split, + offset=page_number, + limit=self.num_rows_per_page, + ) + + try: + async with session.get(self.rows_base_url, params=params) as response: + response.raise_for_status() + data = await response.json() + + # Prepare the data to append + buffer_to_append = [] + for row in data["rows"]: + content = row["row"]["text"] + input_ids = self.tokenizer(content, truncation=True)[ + "input_ids" + ] + buffer_to_append.extend(input_ids) + buffer_to_append.append(self.tokenizer.eos_token_id) + + async with self.lock: + self.buffer.extend(buffer_to_append) + self.pages.append((config_name, page_number, split)) + break # Success, exit retry loop + + except aiohttp.ClientResponseError: + attempt += 1 + if attempt < retry_limit: + await asyncio.sleep(5) + else: + raise + + def _get_pad_size(self, input_ids): + """ + Get the number of tokens to be padded to the sample to match + the max allowed sequence length. + If sample packing is activated, then return 1 + """ + + if self.pack_samples: + return 1 + + sample_size = len(input_ids) + + remainder = sample_size % self.sequence_length + pad_size = self.sequence_length - remainder + + # Apply modulo again to guarantee a pad size of 0 if remainder is 0 + pad_size = pad_size % self.sequence_length + + return pad_size + + def _refill_padded_buffer(self): + """ + This methods pulls one page from `self.buffer`, pads it and pushs + it to the `self.padded_buffer`. + """ + + while self.buffer and len(self.padded_buffer) < self.sequence_length: + input_ids = [] + + # search for EOS token index and cut the buffer at it. + EOS_index = self.buffer.index(self.tokenizer.eos_token_id) + input_ids = self.buffer[: EOS_index + 1] + self.buffer = self.buffer[EOS_index + 1 :] + + self.used_buffer += input_ids + + # Add to padded buffer without the EOS token. + self.padded_buffer += input_ids[:-1] + + # Pad + self.padded_buffer += [self.tokenizer.eos_token_id] * self._get_pad_size( + input_ids=input_ids[:-1] + ) + + def __iter__(self): + self.buffer = self.used_buffer + self.buffer + self.padded_buffer = [] + + # Pad and prepare one page for batching + self._refill_padded_buffer() + + return self + + def __next__(self): + batch = [] + + while len(self.padded_buffer) >= self.sequence_length: + batch.append(self.padded_buffer[: self.sequence_length]) + self.padded_buffer = self.padded_buffer[self.sequence_length :] + self._refill_padded_buffer() + + if len(batch) == self.batch_size: + return np.stack(batch) + + raise StopIteration + + +class DatasetLoader(SubsetLoader): + name: str = "airtrain-ai/fineweb-edu-fortified" + rows_base_url: str = "https://datasets-server.huggingface.co/rows" + size_base_url: str = "https://datasets-server.huggingface.co/size" + + retry_limit: int = 10 # Number of retries + retry_delay: int = 5 # Seconds to wait between retries + num_rows_per_page: int = 100 + + @staticmethod + async def next_pages( + offset: int, n_pages: int, seed: str, num_rows_per_page: int = 100 + ): + configs_data = await DatasetLoader.fetch_dataset_configs() + rng = np.random.default_rng( + hash(seed) & 0xFFFFFFFF + ) # Create a generator with a seed + rng.bit_generator.advance(offset) # Efficiently skip ahead `n` steps + result = [] + for _ in range(n_pages): + config = rng.choice(list(configs_data.keys())) + choice = rng.integers( + 0, configs_data[config]["num_rows"] - 1 - num_rows_per_page + ) + result.append((str(config), int(choice), configs_data[config]["split"])) + return result + + def __init__( + self, + batch_size=None, + sequence_length=None, + num_pages=None, + pages_info=None, + tokenizer: AutoTokenizer = None, + pack_samples: bool = False, + ): + super().__init__( + batch_size, sequence_length, num_pages, tokenizer, pack_samples + ) + + # Initialize properties + self.configs_data = None + self.pages = [] + self.buffer = [] + self.lock = asyncio.Lock() # For thread-safe operations + + @classmethod + async def create( + cls, + batch_size=None, + sequence_length=None, + num_pages=None, + pages_info=None, + tokenizer: AutoTokenizer = None, + pack_samples: bool = False, + ): + self = cls( + batch_size=batch_size, + sequence_length=sequence_length, + num_pages=num_pages, + tokenizer=tokenizer, + pack_samples=pack_samples, + ) + + # Fetch dataset configs asynchronously + self.configs_data = await cls.fetch_dataset_configs() + + if pages_info is not None: + await self._fetch(pages_info) + elif self.num_pages: + await self._fetch_data_to_buffer(self.num_pages) + + return self + + async def _fetch(self, page_info: typing.Tuple[str, int, str]): + self.pages = list(page_info) + async with aiohttp.ClientSession() as session: + tasks = [ + self._fetch_data_for_page((config_name, page, split), session) + for (config_name, page, split) in self.pages + ] + await asyncio.gather(*tasks) + + async def _fetch_data_to_buffer(self, num_pages): + """ + Randomly sample pages and add their data to the buffer. + If a page is inaccessible, another one is sampled. + This method sets the `pages` property. + """ + self.pages = [] + pages_to_fetch = self.get_random_pages(num_pages) + + async with aiohttp.ClientSession() as session: + tasks = [ + self._fetch_data_for_page(page, session) for page in pages_to_fetch + ] + await asyncio.gather(*tasks) + + async def fetch_data_to_rows(self, num_pages): + rows = [] + pages_to_fetch = self.get_random_pages(num_pages) + + async with aiohttp.ClientSession() as session: + tasks = [ + self._fetch_rows_for_page(page, session) for page in pages_to_fetch + ] + results = await asyncio.gather(*tasks) + for page_rows in results: + rows.extend(page_rows) + + return rows + + async def _fetch_data_for_page(self, page, session): + """ + Fetches data asynchronously for a single page, processes it without blocking the event loop, + and appends the tokenized data to the buffer. + + Args: + page: A tuple containing the config name, page number, and split. + session: The HTTP session used for making requests. + + Raises: + Exception: If the maximum number of retry attempts is exceeded. + """ + retry_limit = self.retry_limit + attempt = 0 + while attempt < retry_limit: + config_name, page_number, split = page + + # Create the request parameters + params = { + "dataset": self.name, + "config": config_name, + "split": split, + "offset": page_number, + "limit": self.num_rows_per_page, + } + + try: + # Make an asynchronous HTTP GET request to fetch the data + async with session.get(self.rows_base_url, params=params) as response: + response.raise_for_status() # Raise an exception for HTTP errors + data = await response.json() + + # Prepare the data to append + buffer_to_append = [] + + # Asynchronously process each row without blocking the event loop + tasks = [ + self._tokenize_content(row["row"]["text"]) + for row in data["rows"] + ] + + # Gather the tokenized results concurrently + row_input_ids = await asyncio.gather(*tasks) + + # Flatten the list of input IDs and append them to the buffer + for input_ids in row_input_ids: + buffer_to_append.extend(input_ids) + + # Safely append the processed data to the shared buffer + async with self.lock: + self.buffer.extend(buffer_to_append) + self.pages.append((config_name, page_number, split)) + break # Success, exit retry loop + + except aiohttp.ClientResponseError as e: + # Handle HTTP client errors with a retry mechanism + attempt += 1 + if attempt < retry_limit: + await asyncio.sleep(self.retry_delay) # Wait before retrying + else: + raise Exception( + f"Maximum retry attempts exceeded for page {page}" + ) from e + + async def _tokenize_content(self, content): + """ + Asynchronously tokenizes a string of content using the tokenizer in a separate thread. + + Args: + content: The text content to be tokenized. + + Returns: + The list of token IDs for the content, including the EOS token. + """ + # Offload the CPU-bound tokenization to a thread executor to prevent blocking the event loop + input_ids = await asyncio.to_thread( + self.tokenizer.encode, + content, + truncation=True, + max_length=self.sequence_length, + ) + input_ids.append(self.tokenizer.eos_token_id) + return input_ids + + async def _fetch_rows_for_page(self, page, session): + retry_limit = self.retry_limit + attempt = 0 + while attempt < retry_limit: + config_name, page_number, split = page + + # Create the request parameters + params = dict( + dataset=self.name, + config=config_name, + split=split, + offset=page_number, + limit=self.num_rows_per_page, + ) + + try: + async with session.get(self.rows_base_url, params=params) as response: + response.raise_for_status() + data = await response.json() + + # Collect the rows + return [row["row"]["text"] for row in data["rows"]] + + except aiohttp.ClientResponseError: + attempt += 1 + if attempt < retry_limit: + await asyncio.sleep(self.retry_delay) + else: + raise + + def get_random_pages(self, num_pages): + """ + Randomly sample pages. + A page is a row number of a given split of a given dataset dump. + """ + pages = [] + + for _ in range(num_pages): + # Choose a random config + config_name = random.choice(list(self.configs_data.keys())) + + # Choose a random page (row) + page = random.randint( + 0, + self.configs_data[config_name]["num_rows"] - 1 - self.num_rows_per_page, + ) + + split = self.configs_data[config_name]["split"] + + pages.append((config_name, page, split)) + + return pages + + def get_page_names(self): + """ + This is a utility function that returns the page names that were used. + Each page as a single string instead of a tuple. + """ + page_names = [] + + if hasattr(self, "pages"): + page_names = [ + f"{cfg_name}_{num_rows}_{split}" + for cfg_name, num_rows, split in self.pages + ] + + return page_names + + @staticmethod + async def fetch_dataset_configs() -> typing.Dict[str, typing.Dict]: + """ + Fetch the different dump names, aka configs, aka samples, of the + dataset. + The returned value is a dictionary with dump names as keys and + a dict of the number of rows and the split as values. + """ + # Request parameters + params = dict(dataset=DatasetLoader.name) + + attempt = 0 + while attempt < DatasetLoader.retry_limit: + try: + async with aiohttp.ClientSession() as session: + async with session.get( + DatasetLoader.size_base_url, params=params + ) as response: + response.raise_for_status() + + data = await response.json() + + # Extract the configs dict + configs_dict = data["size"]["splits"] + + # Now create a dict with config names (except 'default') as + # keys, and the number of rows as values + configs_data = { + entry["config"]: { + "num_rows": entry["num_rows"], + "split": entry["split"], + } + for entry in configs_dict + if entry["config"] != "default" + } + + return configs_data + + except aiohttp.ClientResponseError: + attempt += 1 + if attempt < DatasetLoader.retry_limit: + await asyncio.sleep(DatasetLoader.retry_delay) + else: + raise + + @staticmethod + async def next_pages_async( + offset: int, n_pages: int, seed: str, num_rows_per_page: int = 100 + ): + configs_data = await DatasetLoader.fetch_dataset_configs() + rng = np.random.default_rng( + hash(seed) & 0xFFFFFFFF + ) # Create a generator with a seed + rng.bit_generator.advance(offset) # Efficiently skip ahead `n` steps + result = [] + for _ in range(n_pages): + config = rng.choice(list(configs_data.keys())) + choice = rng.integers( + 0, configs_data[config]["num_rows"] - 1 - num_rows_per_page + ) + result.append((str(config), int(choice), configs_data[config]["split"])) + return result diff --git a/src/tplr/hparams.py b/src/tplr/hparams.py new file mode 100644 index 0000000..3ed2900 --- /dev/null +++ b/src/tplr/hparams.py @@ -0,0 +1,139 @@ +# The MIT License (MIT) +# © 2024 templar.tech + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# fmt: off + +# Global imports +import json +from types import SimpleNamespace +from transformers import AutoTokenizer, LlamaConfig + +# Local imports +from .logging import logger + +DEFAULT_HPARAMS = { + # Run configuration + "spec_version": 5, + "project": "templar", + + # Model parameters + "sequence_length": 1024, + "pages_per_window": 2, + "batch_size": 8, + "learning_rate": 0.001, + + # Window and sync parameters + "blocks_per_window": 2, + "windows_per_sync": 100, + "windows_per_weights": 10, + + # Optimization parameters + "momentum_decay": 0.999, + "topk_compression": 32, + "target_chunk": 64, + "scores_alpha": 0.001, + + # Model architecture (these should be in your hparams.json) + "tokenizer_name": "huggyllama/llama-7b", + "hidden_size": 4096, + "num_hidden_layers": 32, + "num_attention_heads": 32, + "intermediate_size": 11008, + "num_key_value_heads": 32, + "activation_function": "silu", + "max_position_embeddings": 2048, + + # Bucket configuration + "bucket_name": "your-default-bucket-name", + + # Scheduler parameters + "warmup_steps": 250, + "alpha_f": 0.1, # Final learning rate multiplier + "t_max": 20000, # Total steps for cosine decay +} + +def create_namespace(hparams: dict) -> SimpleNamespace: + """ + Create a SimpleNamespace from the hyperparameters and add model configuration. + + Args: + hparams (dict): Hyperparameters dictionary. + + Returns: + SimpleNamespace: Namespace containing hyperparameters and model configuration. + """ + # Merge with defaults + full_hparams = DEFAULT_HPARAMS.copy() + full_hparams.update(hparams) + + hparams_ns = SimpleNamespace(**full_hparams) + + # Initialize tokenizer + try: + hparams_ns.tokenizer = AutoTokenizer.from_pretrained( + hparams_ns.tokenizer_name, verbose=False, clean_up_tokenization_spaces=True + ) + hparams_ns.tokenizer.pad_token = hparams_ns.tokenizer.eos_token + except Exception as e: + logger.error(f"Failed to load tokenizer: {e}") + raise + + # Initialize model config + try: + hparams_ns.model_config = LlamaConfig( + vocab_size=hparams_ns.tokenizer.vocab_size, + hidden_size=hparams_ns.hidden_size, + num_hidden_layers=hparams_ns.num_hidden_layers, + num_attention_heads=hparams_ns.num_attention_heads, + intermediate_size=hparams_ns.intermediate_size, + num_key_value_heads=hparams_ns.num_key_value_heads, + activation_function=hparams_ns.activation_function, + max_position_embeddings=hparams_ns.max_position_embeddings, + ) + except Exception as e: + logger.error(f"Failed to create model config: {e}") + raise + + return hparams_ns + +def load_hparams(hparams_file: str = "hparams.json") -> SimpleNamespace: + """ + Load hyperparameters from a JSON file. + + Args: + hparams_file (str): Path to the hyperparameters JSON file. + + Returns: + SimpleNamespace: A namespace containing the hyperparameters and model configuration. + + Example: + hparams = load_hparams() + print(hparams.hidden_size) + print(hparams.model_config) + """ + try: + with open(hparams_file, "r") as f: + hparams = json.load(f) + return create_namespace(hparams) + except FileNotFoundError: + logger.warning(f"No {hparams_file} found, using default hyperparameters") + return create_namespace({}) + except json.JSONDecodeError as e: + logger.error(f"Invalid JSON in {hparams_file}: {e}") + raise + except Exception as e: + logger.error(f"Error loading hyperparameters: {e}") + raise \ No newline at end of file diff --git a/src/tplr/logging.py b/src/tplr/logging.py new file mode 100644 index 0000000..e125285 --- /dev/null +++ b/src/tplr/logging.py @@ -0,0 +1,99 @@ +# The MIT License (MIT) +# © 2024 templar.tech + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# fmt: off + +# Global imports +import time +import logging +from rich.logging import RichHandler +from rich.highlighter import NullHighlighter + + +def T() -> float: + """ + Returns the current time in seconds since the epoch. + + Returns: + float: Current time in seconds. + """ + return time.time() + + +def P(window: int, duration: float) -> str: + """ + Formats a log prefix with the window number and duration. + + Args: + window (int): The current window index. + duration (float): The duration in seconds. + + Returns: + str: A formatted string for log messages. + """ + return f"[steel_blue]{window}[/steel_blue] ([grey63]{duration:.2f}s[/grey63])" + + +# Configure the root logger +FORMAT = "%(message)s" +logging.basicConfig( + level=logging.INFO, + format=FORMAT, + datefmt="[%X]", + handlers=[ + RichHandler( + markup=True, # Enable markup parsing to allow color rendering + rich_tracebacks=True, + highlighter=NullHighlighter(), + show_level=False, + show_time=False, + show_path=False, + ) + ], +) + +# Create a logger instance +logger = logging.getLogger("templar") +logger.setLevel(logging.INFO) + + +def debug() -> None: + """ + Sets the logger level to DEBUG. + """ + logger.setLevel(logging.DEBUG) + + +def trace() -> None: + """ + Sets the logger level to TRACE. + + Note: + The TRACE level is not standard in the logging module. + You may need to add it explicitly if required. + """ + TRACE_LEVEL_NUM = 5 + logging.addLevelName(TRACE_LEVEL_NUM, "TRACE") + + def trace_method(self, message, *args, **kws) -> None: + if self.isEnabledFor(TRACE_LEVEL_NUM): + self._log(TRACE_LEVEL_NUM, message, args, **kws) + + logging.Logger.trace = trace_method + logger.setLevel(TRACE_LEVEL_NUM) + + +__all__ = ["logger", "debug", "trace", "P", "T"] diff --git a/src/tplr/schemas.py b/src/tplr/schemas.py new file mode 100644 index 0000000..aaf2028 --- /dev/null +++ b/src/tplr/schemas.py @@ -0,0 +1,45 @@ +# The MIT License (MIT) +# © 2024 templar.tech + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# fmt: off + +# Global imports +from pydantic import BaseModel + + +class Bucket(BaseModel): + """Configuration for a bucket, including name and access credentials.""" + + def __hash__(self): + # Use all fields to generate a unique hash + return hash( + (self.name, self.account_id, self.access_key_id, self.secret_access_key) + ) + + def __eq__(self, other): + # Compare all fields to determine equality + if isinstance(other, Bucket): + return self.dict() == other.dict() + return False + + name: str + account_id: str + access_key_id: str + secret_access_key: str + + class Config: + str_min_length = 1 + str_strip_whitespace = True diff --git a/src/tplr/wandb.py b/src/tplr/wandb.py new file mode 100644 index 0000000..9f4756c --- /dev/null +++ b/src/tplr/wandb.py @@ -0,0 +1,92 @@ +# The MIT License (MIT) +# © 2024 templar.tech + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# fmt: off + +# Global imports +import os +import wandb as wandbm + +# Local imports +from . import __version__, logger + +class WandbManager: + def __init__(self, run_prefix=None, uid=None, config=None, group=None, job_type=None, is_validator=False): + """Initialize WandB manager with proper run management and resumability. + + Args: + run_prefix: Optional prefix for the run name (if None, determined by is_validator) + uid: User ID + config: Config object containing wandb settings + group: Group name (if None, determined by is_validator) + job_type: Job type (if None, determined by is_validator) + is_validator: Boolean indicating if this is a validator run + """ + self.wandb_dir = os.path.join(os.getcwd(), 'wandb') + os.makedirs(self.wandb_dir, exist_ok=True) + self.run = None + + if all(x is not None for x in [uid, config]): + # Set defaults based on validator status if not provided + if run_prefix is None: + run_prefix = 'V' if is_validator else 'M' + if group is None: + group = 'validator' if is_validator else 'miner' + if job_type is None: + job_type = 'validation' if is_validator else 'training' + + # Define the run ID file path inside the wandb directory + run_id_file = os.path.join( + self.wandb_dir, f"wandb_run_id_{run_prefix}{uid}_{__version__}.txt" + ) + + # Check for existing run and verify it still exists in wandb + run_id = None + if os.path.exists(run_id_file): + with open(run_id_file, 'r') as f: + run_id = f.read().strip() + + # Verify if run still exists in wandb + try: + api = wandbm.Api() + api.run(f"tplr/{config.project}-v{__version__}/{run_id}") + logger.info(f"Found existing run ID: {run_id}") + except Exception: + logger.info(f"Previous run {run_id} not found in WandB, starting new run") + run_id = None + os.remove(run_id_file) + + # Initialize WandB + self.run = wandbm.init( + project=f"{config.project}-v{__version__}", + entity='tplr', + id=run_id, + resume='allow', + name=f'{run_prefix}{uid}', + config=config, + group=group, + job_type=job_type, + dir=self.wandb_dir, + settings=wandbm.Settings( + init_timeout=300, + _disable_stats=True, + ) + ) + + # Save run ID for future resumption + if not run_id: + with open(run_id_file, 'w') as f: + f.write(self.run.id) diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..7b0fc0c --- /dev/null +++ b/start.sh @@ -0,0 +1,6 @@ + + +# pm2 delete all +pm2 start neurons/miner.py --interpreter python3 --name TM0 -- --wallet.name Bistro --wallet.hotkey M111 --device cuda:0 --subtensor.network test --use_wandb --debug +pm2 start neurons/miner.py --interpreter python3 --name TM1 -- --wallet.name Bistro --wallet.hotkey M222 --device cuda:1 --subtensor.network test --use_wandb --debug +pm2 start neurons/validator.py --interpreter python3 --name TV1 -- --wallet.name Bistro --wallet.hotkey V11 --device cuda:3 --subtensor.network test --use_wandb --debug diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..664e8f0 --- /dev/null +++ b/uv.lock @@ -0,0 +1,2629 @@ +version = 1 +requires-python = ">=3.11" +resolution-markers = [ + "sys_platform != 'linux'", + "sys_platform == 'linux'", +] + +[[package]] +name = "aioboto3" +version = "13.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiobotocore", extra = ["boto3"] }, + { name = "aiofiles" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/35/1d/a36f39e95d15202236a5fec436377a9db712c5fe5a240325a5e54bc5e3ef/aioboto3-13.2.0.tar.gz", hash = "sha256:92c3232e0bf7dcb5d921cd1eb8c5e0b856c3985f7c1cd32ab3cd51adc5c9b5da", size = 32497 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/66/e4b2d8f3d11687f7c63b1b63e484ee879f9af637b3564026037655d83255/aioboto3-13.2.0-py3-none-any.whl", hash = "sha256:fd894b8d319934dfd75285b58da35560670e57182d0148c54a3d4ee5da730c78", size = 34738 }, +] + +[[package]] +name = "aiobotocore" +version = "2.15.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "aioitertools" }, + { name = "botocore" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/3d/5d54985abed848a4d4dafd10d7eb9ecd6bd7fff9533223911a92c2e6e15d/aiobotocore-2.15.2.tar.gz", hash = "sha256:9ac1cfcaccccc80602968174aa032bf978abe36bd4e55e6781d6500909af1375", size = 107035 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/57/6402242dde160d9ef9903487b4277443dc3da04615f6c4d3b48564a8ab57/aiobotocore-2.15.2-py3-none-any.whl", hash = "sha256:d4d3128b4b558e2b4c369bfa963b022d7e87303adb82eec623cec8aa77ae578a", size = 77400 }, +] + +[package.optional-dependencies] +boto3 = [ + { name = "boto3" }, +] + +[[package]] +name = "aiofiles" +version = "24.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c", size = 30247 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5", size = 15896 }, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7f/55/e4373e888fdacb15563ef6fa9fa8c8252476ea071e96fb46defac9f18bf2/aiohappyeyeballs-2.4.4.tar.gz", hash = "sha256:5fdd7d87889c63183afc18ce9271f9b0a7d32c2303e394468dd45d514a757745", size = 21977 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/74/fbb6559de3607b3300b9be3cc64e97548d55678e44623db17820dbd20002/aiohappyeyeballs-2.4.4-py3-none-any.whl", hash = "sha256:a980909d50efcd44795c4afeca523296716d50cd756ddca6af8c65b996e27de8", size = 14756 }, +] + +[[package]] +name = "aiohttp" +version = "3.10.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/a8/8e2ba36c6e3278d62e0c88aa42bb92ddbef092ac363b390dab4421da5cf5/aiohttp-3.10.11.tar.gz", hash = "sha256:9dc2b8f3dcab2e39e0fa309c8da50c3b55e6f34ab25f1a71d3288f24924d33a7", size = 7551886 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/96/221ec59bc38395a6c205cbe8bf72c114ce92694b58abc8c3c6b7250efa7f/aiohttp-3.10.11-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:80ff08556c7f59a7972b1e8919f62e9c069c33566a6d28586771711e0eea4f07", size = 587742 }, + { url = "https://files.pythonhosted.org/packages/24/17/4e606c969b19de5c31a09b946bd4c37e30c5288ca91d4790aa915518846e/aiohttp-3.10.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c8f96e9ee19f04c4914e4e7a42a60861066d3e1abf05c726f38d9d0a466e695", size = 400357 }, + { url = "https://files.pythonhosted.org/packages/a2/e5/433f59b87ba69736e446824710dd7f26fcd05b24c6647cb1e76554ea5d02/aiohttp-3.10.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fb8601394d537da9221947b5d6e62b064c9a43e88a1ecd7414d21a1a6fba9c24", size = 392099 }, + { url = "https://files.pythonhosted.org/packages/d2/a3/3be340f5063970bb9e47f065ee8151edab639d9c2dce0d9605a325ab035d/aiohttp-3.10.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ea224cf7bc2d8856d6971cea73b1d50c9c51d36971faf1abc169a0d5f85a382", size = 1300367 }, + { url = "https://files.pythonhosted.org/packages/ba/7d/a3043918466cbee9429792ebe795f92f70eeb40aee4ccbca14c38ee8fa4d/aiohttp-3.10.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db9503f79e12d5d80b3efd4d01312853565c05367493379df76d2674af881caa", size = 1339448 }, + { url = "https://files.pythonhosted.org/packages/2c/60/192b378bd9d1ae67716b71ae63c3e97c48b134aad7675915a10853a0b7de/aiohttp-3.10.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0f449a50cc33f0384f633894d8d3cd020e3ccef81879c6e6245c3c375c448625", size = 1374875 }, + { url = "https://files.pythonhosted.org/packages/e0/d7/cd58bd17f5277d9cc32ecdbb0481ca02c52fc066412de413aa01268dc9b4/aiohttp-3.10.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82052be3e6d9e0c123499127782a01a2b224b8af8c62ab46b3f6197035ad94e9", size = 1285626 }, + { url = "https://files.pythonhosted.org/packages/bb/b2/da4953643b7dcdcd29cc99f98209f3653bf02023d95ce8a8fd57ffba0f15/aiohttp-3.10.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20063c7acf1eec550c8eb098deb5ed9e1bb0521613b03bb93644b810986027ac", size = 1246120 }, + { url = "https://files.pythonhosted.org/packages/6c/22/1217b3c773055f0cb172e3b7108274a74c0fe9900c716362727303931cbb/aiohttp-3.10.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:489cced07a4c11488f47aab1f00d0c572506883f877af100a38f1fedaa884c3a", size = 1265177 }, + { url = "https://files.pythonhosted.org/packages/63/5e/3827ad7e61544ed1e73e4fdea7bb87ea35ac59a362d7eb301feb5e859780/aiohttp-3.10.11-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ea9b3bab329aeaa603ed3bf605f1e2a6f36496ad7e0e1aa42025f368ee2dc07b", size = 1257238 }, + { url = "https://files.pythonhosted.org/packages/53/31/951f78751d403da6086b662760e6e8b08201b0dcf5357969f48261b4d0e1/aiohttp-3.10.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ca117819d8ad113413016cb29774b3f6d99ad23c220069789fc050267b786c16", size = 1315944 }, + { url = "https://files.pythonhosted.org/packages/0d/79/06ef7a2a69880649261818b135b245de5a4e89fed5a6987c8645428563fc/aiohttp-3.10.11-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2dfb612dcbe70fb7cdcf3499e8d483079b89749c857a8f6e80263b021745c730", size = 1332065 }, + { url = "https://files.pythonhosted.org/packages/10/39/a273857c2d0bbf2152a4201fbf776931c2dac74aa399c6683ed4c286d1d1/aiohttp-3.10.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9b615d3da0d60e7d53c62e22b4fd1c70f4ae5993a44687b011ea3a2e49051b8", size = 1291882 }, + { url = "https://files.pythonhosted.org/packages/49/39/7aa387f88403febc96e0494101763afaa14d342109329a01b413b2bac075/aiohttp-3.10.11-cp311-cp311-win32.whl", hash = "sha256:29103f9099b6068bbdf44d6a3d090e0a0b2be6d3c9f16a070dd9d0d910ec08f9", size = 363409 }, + { url = "https://files.pythonhosted.org/packages/6f/e9/8eb3dc095ce48499d867ad461d02f1491686b79ad92e4fad4df582f6be7b/aiohttp-3.10.11-cp311-cp311-win_amd64.whl", hash = "sha256:236b28ceb79532da85d59aa9b9bf873b364e27a0acb2ceaba475dc61cffb6f3f", size = 382644 }, + { url = "https://files.pythonhosted.org/packages/01/16/077057ef3bd684dbf9a8273a5299e182a8d07b4b252503712ff8b5364fd1/aiohttp-3.10.11-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7480519f70e32bfb101d71fb9a1f330fbd291655a4c1c922232a48c458c52710", size = 584830 }, + { url = "https://files.pythonhosted.org/packages/2c/cf/348b93deb9597c61a51b6682e81f7c7d79290249e886022ef0705d858d90/aiohttp-3.10.11-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f65267266c9aeb2287a6622ee2bb39490292552f9fbf851baabc04c9f84e048d", size = 397090 }, + { url = "https://files.pythonhosted.org/packages/70/bf/903df5cd739dfaf5b827b3d8c9d68ff4fcea16a0ca1aeb948c9da30f56c8/aiohttp-3.10.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7400a93d629a0608dc1d6c55f1e3d6e07f7375745aaa8bd7f085571e4d1cee97", size = 392361 }, + { url = "https://files.pythonhosted.org/packages/fb/97/e4792675448a2ac5bd56f377a095233b805dd1315235c940c8ba5624e3cb/aiohttp-3.10.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f34b97e4b11b8d4eb2c3a4f975be626cc8af99ff479da7de49ac2c6d02d35725", size = 1309839 }, + { url = "https://files.pythonhosted.org/packages/96/d0/ba19b1260da6fbbda4d5b1550d8a53ba3518868f2c143d672aedfdbc6172/aiohttp-3.10.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e7b825da878464a252ccff2958838f9caa82f32a8dbc334eb9b34a026e2c636", size = 1348116 }, + { url = "https://files.pythonhosted.org/packages/b3/b9/15100ee7113a2638bfdc91aecc54641609a92a7ce4fe533ebeaa8d43ff93/aiohttp-3.10.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9f92a344c50b9667827da308473005f34767b6a2a60d9acff56ae94f895f385", size = 1391402 }, + { url = "https://files.pythonhosted.org/packages/c5/36/831522618ac0dcd0b28f327afd18df7fb6bbf3eaf302f912a40e87714846/aiohttp-3.10.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc6f1ab987a27b83c5268a17218463c2ec08dbb754195113867a27b166cd6087", size = 1304239 }, + { url = "https://files.pythonhosted.org/packages/60/9f/b7230d0c48b076500ae57adb717aa0656432acd3d8febb1183dedfaa4e75/aiohttp-3.10.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1dc0f4ca54842173d03322793ebcf2c8cc2d34ae91cc762478e295d8e361e03f", size = 1256565 }, + { url = "https://files.pythonhosted.org/packages/63/c2/35c7b4699f4830b3b0a5c3d5619df16dca8052ae8b488e66065902d559f6/aiohttp-3.10.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7ce6a51469bfaacff146e59e7fb61c9c23006495d11cc24c514a455032bcfa03", size = 1269285 }, + { url = "https://files.pythonhosted.org/packages/51/48/bc20ea753909bdeb09f9065260aefa7453e3a57f6a51f56f5216adc1a5e7/aiohttp-3.10.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:aad3cd91d484d065ede16f3cf15408254e2469e3f613b241a1db552c5eb7ab7d", size = 1276716 }, + { url = "https://files.pythonhosted.org/packages/0c/7b/a8708616b3810f55ead66f8e189afa9474795760473aea734bbea536cd64/aiohttp-3.10.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f4df4b8ca97f658c880fb4b90b1d1ec528315d4030af1ec763247ebfd33d8b9a", size = 1315023 }, + { url = "https://files.pythonhosted.org/packages/2a/d6/dfe9134a921e05b01661a127a37b7d157db93428905450e32f9898eef27d/aiohttp-3.10.11-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2e4e18a0a2d03531edbc06c366954e40a3f8d2a88d2b936bbe78a0c75a3aab3e", size = 1342735 }, + { url = "https://files.pythonhosted.org/packages/ca/1a/3bd7f18e3909eabd57e5d17ecdbf5ea4c5828d91341e3676a07de7c76312/aiohttp-3.10.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6ce66780fa1a20e45bc753cda2a149daa6dbf1561fc1289fa0c308391c7bc0a4", size = 1302618 }, + { url = "https://files.pythonhosted.org/packages/cf/51/d063133781cda48cfdd1e11fc8ef45ab3912b446feba41556385b3ae5087/aiohttp-3.10.11-cp312-cp312-win32.whl", hash = "sha256:a919c8957695ea4c0e7a3e8d16494e3477b86f33067478f43106921c2fef15bb", size = 360497 }, + { url = "https://files.pythonhosted.org/packages/55/4e/f29def9ed39826fe8f85955f2e42fe5cc0cbe3ebb53c97087f225368702e/aiohttp-3.10.11-cp312-cp312-win_amd64.whl", hash = "sha256:b5e29706e6389a2283a91611c91bf24f218962717c8f3b4e528ef529d112ee27", size = 380577 }, + { url = "https://files.pythonhosted.org/packages/1f/63/654c185dfe3cf5d4a0d35b6ee49ee6ca91922c694eaa90732e1ba4b40ef1/aiohttp-3.10.11-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:703938e22434d7d14ec22f9f310559331f455018389222eed132808cd8f44127", size = 577381 }, + { url = "https://files.pythonhosted.org/packages/4e/c4/ee9c350acb202ba2eb0c44b0f84376b05477e870444192a9f70e06844c28/aiohttp-3.10.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9bc50b63648840854e00084c2b43035a62e033cb9b06d8c22b409d56eb098413", size = 393289 }, + { url = "https://files.pythonhosted.org/packages/3d/7c/30d161a7e3b208cef1b922eacf2bbb8578b7e5a62266a6a2245a1dd044dc/aiohttp-3.10.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f0463bf8b0754bc744e1feb61590706823795041e63edf30118a6f0bf577461", size = 388859 }, + { url = "https://files.pythonhosted.org/packages/79/10/8d050e04be447d3d39e5a4a910fa289d930120cebe1b893096bd3ee29063/aiohttp-3.10.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6c6dec398ac5a87cb3a407b068e1106b20ef001c344e34154616183fe684288", size = 1280983 }, + { url = "https://files.pythonhosted.org/packages/31/b3/977eca40afe643dcfa6b8d8bb9a93f4cba1d8ed1ead22c92056b08855c7a/aiohttp-3.10.11-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcaf2d79104d53d4dcf934f7ce76d3d155302d07dae24dff6c9fffd217568067", size = 1317132 }, + { url = "https://files.pythonhosted.org/packages/1a/43/b5ee8e697ed0f96a2b3d80b3058fa7590cda508e9cd256274246ba1cf37a/aiohttp-3.10.11-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25fd5470922091b5a9aeeb7e75be609e16b4fba81cdeaf12981393fb240dd10e", size = 1362630 }, + { url = "https://files.pythonhosted.org/packages/28/20/3ae8e993b2990fa722987222dea74d6bac9331e2f530d086f309b4aa8847/aiohttp-3.10.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbde2ca67230923a42161b1f408c3992ae6e0be782dca0c44cb3206bf330dee1", size = 1276865 }, + { url = "https://files.pythonhosted.org/packages/02/08/1afb0ab7dcff63333b683e998e751aa2547d1ff897b577d2244b00e6fe38/aiohttp-3.10.11-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:249c8ff8d26a8b41a0f12f9df804e7c685ca35a207e2410adbd3e924217b9006", size = 1230448 }, + { url = "https://files.pythonhosted.org/packages/c6/fd/ccd0ff842c62128d164ec09e3dd810208a84d79cd402358a3038ae91f3e9/aiohttp-3.10.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:878ca6a931ee8c486a8f7b432b65431d095c522cbeb34892bee5be97b3481d0f", size = 1244626 }, + { url = "https://files.pythonhosted.org/packages/9f/75/30e9537ab41ed7cb062338d8df7c4afb0a715b3551cd69fc4ea61cfa5a95/aiohttp-3.10.11-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8663f7777ce775f0413324be0d96d9730959b2ca73d9b7e2c2c90539139cbdd6", size = 1243608 }, + { url = "https://files.pythonhosted.org/packages/c2/e0/3e7a62d99b9080793affddc12a82b11c9bc1312916ad849700d2bddf9786/aiohttp-3.10.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6cd3f10b01f0c31481fba8d302b61603a2acb37b9d30e1d14e0f5a58b7b18a31", size = 1286158 }, + { url = "https://files.pythonhosted.org/packages/71/b8/df67886802e71e976996ed9324eb7dc379e53a7d972314e9c7fe3f6ac6bc/aiohttp-3.10.11-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:4e8d8aad9402d3aa02fdc5ca2fe68bcb9fdfe1f77b40b10410a94c7f408b664d", size = 1313636 }, + { url = "https://files.pythonhosted.org/packages/3c/3b/aea9c3e70ff4e030f46902df28b4cdf486695f4d78fd9c6698827e2bafab/aiohttp-3.10.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:38e3c4f80196b4f6c3a85d134a534a56f52da9cb8d8e7af1b79a32eefee73a00", size = 1273772 }, + { url = "https://files.pythonhosted.org/packages/e9/9e/4b4c5705270d1c4ee146516ad288af720798d957ba46504aaf99b86e85d9/aiohttp-3.10.11-cp313-cp313-win32.whl", hash = "sha256:fc31820cfc3b2863c6e95e14fcf815dc7afe52480b4dc03393c4873bb5599f71", size = 358679 }, + { url = "https://files.pythonhosted.org/packages/28/1d/18ef37549901db94717d4389eb7be807acbfbdeab48a73ff2993fc909118/aiohttp-3.10.11-cp313-cp313-win_amd64.whl", hash = "sha256:4996ff1345704ffdd6d75fb06ed175938c133425af616142e7187f28dc75f14e", size = 378073 }, +] + +[[package]] +name = "aioitertools" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/de/38491a84ab323b47c7f86e94d2830e748780525f7a10c8600b67ead7e9ea/aioitertools-0.12.0.tar.gz", hash = "sha256:c2a9055b4fbb7705f561b9d86053e8af5d10cc845d22c32008c43490b2d8dd6b", size = 19369 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/13/58b70a580de00893223d61de8fea167877a3aed97d4a5e1405c9159ef925/aioitertools-0.12.0-py3-none-any.whl", hash = "sha256:fc1f5fac3d737354de8831cbba3eb04f79dd649d8f3afb4c5b114925e662a796", size = 24345 }, +] + +[[package]] +name = "aiosignal" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/67/0952ed97a9793b4958e5736f6d2b346b414a2cd63e82d05940032f45b32f/aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc", size = 19422 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/ac/a7305707cb852b7e16ff80eaf5692309bde30e2b1100a1fcacdc8f731d97/aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17", size = 7617 }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "anyio" +version = "4.6.2.post1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/09/45b9b7a6d4e45c6bcb5bf61d19e3ab87df68e0601fa8c5293de3542546cc/anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c", size = 173422 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/f5/f2b75d2fc6f1a260f340f0e7c6a060f4dd2961cc16884ed851b0d18da06a/anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d", size = 90377 }, +] + +[[package]] +name = "async-property" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/12/900eb34b3af75c11b69d6b78b74ec0fd1ba489376eceb3785f787d1a0a1d/async_property-0.2.2.tar.gz", hash = "sha256:17d9bd6ca67e27915a75d92549df64b5c7174e9dc806b30a3934dc4ff0506380", size = 16523 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/80/9f608d13b4b3afcebd1dd13baf9551c95fc424d6390e4b1cfd7b1810cd06/async_property-0.2.2-py2.py3-none-any.whl", hash = "sha256:8924d792b5843994537f8ed411165700b27b2bd966cefc4daeefc1253442a9d7", size = 9546 }, +] + +[[package]] +name = "attrs" +version = "24.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/0f/aafca9af9315aee06a89ffde799a10a582fe8de76c563ee80bbcdc08b3fb/attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346", size = 792678 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/21/5b6702a7f963e95456c0de2d495f67bf5fd62840ac655dc451586d23d39a/attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2", size = 63001 }, +] + +[[package]] +name = "backoff" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148 }, +] + +[[package]] +name = "base58" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7f/45/8ae61209bb9015f516102fa559a2914178da1d5868428bd86a1b4421141d/base58-2.1.1.tar.gz", hash = "sha256:c5d0cb3f5b6e81e8e35da5754388ddcc6d0d14b6c6a132cb93d69ed580a7278c", size = 6528 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/45/ec96b29162a402fc4c1c5512d114d7b3787b9d1c2ec241d9568b4816ee23/base58-2.1.1-py3-none-any.whl", hash = "sha256:11a36f4d3ce51dfc1043f3218591ac4eb1ceb172919cebe05b52a5bcc8d245c2", size = 5621 }, +] + +[[package]] +name = "bittensor" +version = "8.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "async-property" }, + { name = "bittensor-cli" }, + { name = "bittensor-commit-reveal" }, + { name = "bittensor-wallet" }, + { name = "bt-decode" }, + { name = "colorama" }, + { name = "fastapi" }, + { name = "msgpack-numpy-opentensor" }, + { name = "munch" }, + { name = "nest-asyncio" }, + { name = "netaddr" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pycryptodome" }, + { name = "pydantic" }, + { name = "python-levenshtein" }, + { name = "python-statemachine" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "retry" }, + { name = "rich" }, + { name = "scalecodec" }, + { name = "setuptools" }, + { name = "substrate-interface" }, + { name = "uvicorn" }, + { name = "websockets" }, + { name = "wheel" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/b2/09ec0664822d32fcf2f708742c69249f8a70a09fb941cef3a1f4ab8eca02/bittensor-8.5.1.tar.gz", hash = "sha256:f1bb033ba1e2641881d37f9d8cfebdcb7145ae20975861863710bdd17941cce4", size = 210235 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/34/29f5b570d734b8474b21071756db41b4717c8cbebc3fa5716f0c499a12e8/bittensor-8.5.1-py3-none-any.whl", hash = "sha256:8dbf9c389d10fd043dab5da163377a43ec2ae1b1715e819a3602e07d36304f94", size = 257860 }, +] + +[[package]] +name = "bittensor-cli" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "async-property" }, + { name = "backoff" }, + { name = "bittensor-wallet" }, + { name = "bt-decode" }, + { name = "fuzzywuzzy" }, + { name = "gitpython" }, + { name = "jinja2" }, + { name = "netaddr" }, + { name = "numpy" }, + { name = "pycryptodome" }, + { name = "pytest" }, + { name = "python-levenshtein" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "scalecodec" }, + { name = "substrate-interface" }, + { name = "typer" }, + { name = "websockets" }, + { name = "wheel" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/77/a4250b215d7a5dabef2090c8b300843aa962a135a85b8af0ca499b15a23e/bittensor-cli-8.4.2.tar.gz", hash = "sha256:43efc081ed2ecf4357bf5c5322ccd6f7d1a5110eb842cf138c75adb3f21686fd", size = 161405 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/7b/3f43e1e453697aba95734f94a44aeb96d72139a2ca83837ecaf170a58de2/bittensor_cli-8.4.2-py3-none-any.whl", hash = "sha256:e7fc5ff510f039fa0cb9c0c701a56c4eb2b644befb019b1cd0fac29546bfb764", size = 171700 }, +] + +[[package]] +name = "bittensor-commit-reveal" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/93/f6361d6d617f1620f1b642308384d7f22c7917c169b821ddb3a90856a0c9/bittensor_commit_reveal-0.1.0.tar.gz", hash = "sha256:1c8bb8d77f6279988902c5c28361cc460167829c63ffa8d788209f8810933211", size = 23249 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/cf/fcc202fb07594933f759287ceea9e891cbb8ce779f24cc84311af2b50802/bittensor_commit_reveal-0.1.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2bb23935ac60a981bfb3d83397b83e858c0b69a11806969cf56486f5ebc90943", size = 493021 }, + { url = "https://files.pythonhosted.org/packages/c6/bd/0e438e505036fda9370f352dc9a8f1ff7fa777a8b07479b9874f5742e7b4/bittensor_commit_reveal-0.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4917b215c24b10bd80c84db921113b9cd1346ca7dcaca75e286905ede81a3b18", size = 493236 }, + { url = "https://files.pythonhosted.org/packages/2b/7a/cded935634bf0a077e8f7454b47164e1b3e45064234eaf9722e6a35c1cbf/bittensor_commit_reveal-0.1.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c46cee3e5fa5fc9e6f6a444793062855f40495c1a00b52df6508e4449ac5e89f", size = 711674 }, + { url = "https://files.pythonhosted.org/packages/06/ab/ea0f20581a786ec4b497bdaab8fb4a046c81d125820fc1ec4bfe79854f96/bittensor_commit_reveal-0.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d56407b879dcf82bdde5eaefede43c8891e122fefc03a32c77a063dfc52e0c8", size = 552162 }, + { url = "https://files.pythonhosted.org/packages/a8/3a/7705ea18c3d61c8affc4696b8ab483bdb7e3d0bfdfb61ca1583a787ef1e0/bittensor_commit_reveal-0.1.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8509250549b6f5c475a9150e941b28fc66e82f30b27fe078fd80fa840943bb7b", size = 491259 }, + { url = "https://files.pythonhosted.org/packages/80/21/02b400750c7d1d5ed081dc22c740e21e22fd72fbb18b72517d5687eca8bd/bittensor_commit_reveal-0.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bed04f82f162121747cfd7f51bb5d625dda0bf763a0699054565f255d219a9c2", size = 492612 }, + { url = "https://files.pythonhosted.org/packages/9a/82/bf02fda4c7bfbe6830709476cf1893ad4e7b591c4e1f62eab2abbfcd0106/bittensor_commit_reveal-0.1.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25af2d9c82cacc4278095460493430d36070cb2843c0aa54b1c563788d0742eb", size = 712159 }, + { url = "https://files.pythonhosted.org/packages/31/d1/7e41e52251c277bf0bebe0fcb3f700e6faf6a488c9cefa8b8fb2bae42cee/bittensor_commit_reveal-0.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8f530793274698aaf4ac7cc8f24e915749d8156df8302c9e1e16446177b429d", size = 551180 }, + { url = "https://files.pythonhosted.org/packages/fa/20/272b35206c52db8b385ff7f2a6579ca700fa996c147e4533cd4d323446a7/bittensor_commit_reveal-0.1.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e52955d652b61f091310e2b9b232d26b9e586a928e3b414a09a1b6615e9cc7a0", size = 491231 }, + { url = "https://files.pythonhosted.org/packages/ee/05/02329c66db0970569a31779c0effcee67a1f6bb20a12ccbd667123d89f3f/bittensor_commit_reveal-0.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7be8c8f79dea2e137f5add6ee4711447c4f5d43668be26616ab7c1cacf317e07", size = 492469 }, + { url = "https://files.pythonhosted.org/packages/f4/47/ca9a347273e6993b8775a2a04e9d3df5569aaab46dc95247bf0c1f1b5ea1/bittensor_commit_reveal-0.1.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b88ecb6a0989c2200486e29419a8e7d3b3f7918bdbde4ec04dbb4464abdee08f", size = 711920 }, + { url = "https://files.pythonhosted.org/packages/fe/87/cbef0fa4b4d3159030d61d09da5a09181c0ca8f25bbb451437cb50627ac7/bittensor_commit_reveal-0.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ac015f9eefa9dbddd2875cd7214e3a0bc2e394a2915772e655bdcc5c0af67de", size = 551137 }, +] + +[[package]] +name = "bittensor-wallet" +version = "2.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "eth-utils" }, + { name = "munch" }, + { name = "password-strength" }, + { name = "py-bip39-bindings" }, + { name = "rich" }, + { name = "substrate-interface" }, + { name = "termcolor" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a4/17/38a9ec85be2167dd2c1aa2e75f0ac7c25ccf7c31859fe9b0d325b474fbbb/bittensor_wallet-2.1.3.tar.gz", hash = "sha256:41927d7e5d68fff1494cef5abd861ede0afc684dff366824b0806cfa3ce13af0", size = 70285 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/5d/3b4a4ed5e4d4bbc3575001455dfd5631620147e65ab07f3f3a31891ea56a/bittensor_wallet-2.1.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a5199c84e9d33ccec451294f89d9354b61568a0b623ceee995f588ccdc14ea5c", size = 800061 }, + { url = "https://files.pythonhosted.org/packages/97/7c/8f55e5dfda6c28a74a63ca60cd4d9e860bb798da5e58ea4b88eead124f38/bittensor_wallet-2.1.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a34e524f21e8c7bd8edfd54db530480b81f48d2334a0a11b86ea22d9e349137c", size = 752208 }, + { url = "https://files.pythonhosted.org/packages/07/5b/bf271ddda747244ff044d8f7e21e30ff684f24d0a5447662cc020c3c301c/bittensor_wallet-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45a1556e02304e1e8e91059cc11bb8346fa2334ac039f79bb1e6f630fa26657f", size = 3146730 }, + { url = "https://files.pythonhosted.org/packages/c2/97/a74c138b92db1d455d2be371cea3777616fc6cb94ac401cecddd27e4d9d4/bittensor_wallet-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9399c753c37dbe63430c5aff4fba0a038e0349dde0061d623506a24e3b4d2cec", size = 2953376 }, + { url = "https://files.pythonhosted.org/packages/a7/b0/a803fb7abe4b004464d67f6812f5067ee0346e7ba0bfb1e3012f569261cd/bittensor_wallet-2.1.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1e2f0d03a21a0c54b1f8cd59f34941d7a60df490e9aab7d7776b03f290de6074", size = 797657 }, + { url = "https://files.pythonhosted.org/packages/24/35/506d88aed623872fe4ecbcc2d6484ac864dc2c639ef8810141628fd28763/bittensor_wallet-2.1.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:24c446b0af4c9ffc3ac122f97a1de25b283c877aa49892748ad06a8a61a74e13", size = 752425 }, + { url = "https://files.pythonhosted.org/packages/eb/37/c6feb7d6ac75c24bfe170ffabbd42f2d91bc34cc75b99575f2417ec486b1/bittensor_wallet-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eafd9c82720644b3eeac2f68deaa9cec4cf175836b16c89206d98ce22590e8e", size = 3146851 }, + { url = "https://files.pythonhosted.org/packages/8e/63/0dfe52c8c4c7d943d3ca2f52530039e1ee0dbdbffb3d16a90d770725b9bd/bittensor_wallet-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f5122b05d8eede2bfc2eb242214b75ecab08f0da5d4f7547ed01ad253349e019", size = 2954118 }, + { url = "https://files.pythonhosted.org/packages/ad/81/670424362f512f96760694839cd44a1d4aa6401d5e1c93ff1bf37f3a3653/bittensor_wallet-2.1.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:88020b18aa2f91b336a6f04354b7acb124701f9678d74e41f5ffb64a7e1e5731", size = 797707 }, + { url = "https://files.pythonhosted.org/packages/e8/de/81744fd99af5339aa196c4c5e559ae3d2dd773d8fc1e39059fd651982b4b/bittensor_wallet-2.1.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb7dd2ed4c12e617574b7302a6c20fb8e915477ce2942627f624293b5de9a003", size = 752028 }, + { url = "https://files.pythonhosted.org/packages/41/3c/309505722c2390337d417c17cc50040ddcbdaee03cc8fc664a34320f777a/bittensor_wallet-2.1.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de47dea7d283e83449465f9780d4dde608fe09da45d6ef8c795806e49ccf4fd2", size = 3145919 }, + { url = "https://files.pythonhosted.org/packages/bc/3f/e973420941b0d0b23d944fd60cd95c3bbbca38f5c582d83409f6243880fa/bittensor_wallet-2.1.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e35adc5303b2186df889e07c79bf0bc074df382df49e6c216a8feb27f00453a4", size = 2953541 }, +] + +[[package]] +name = "boto3" +version = "1.35.36" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/9f/17536f9a1ab4c6ee454c782f27c9f0160558f70502fc55da62e456c47229/boto3-1.35.36.tar.gz", hash = "sha256:586524b623e4fbbebe28b604c6205eb12f263cc4746bccb011562d07e217a4cb", size = 110987 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/6b/8b126c2e1c07fae33185544ea974de67027afc905bd072feef9fbbd38d3d/boto3-1.35.36-py3-none-any.whl", hash = "sha256:33735b9449cd2ef176531ba2cb2265c904a91244440b0e161a17da9d24a1e6d1", size = 139143 }, +] + +[[package]] +name = "botocore" +version = "1.35.36" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/4f/11d2d314f0bdbe7ff975737d125e1a5357115afe28fcc64f13e68b05ba61/botocore-1.35.36.tar.gz", hash = "sha256:354ec1b766f0029b5d6ff0c45d1a0f9e5007b7d2f3ec89bcdd755b208c5bc797", size = 12808757 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/60/056d58b606731f94fe395266c604ea9efcecc10e6857ceb9b10e6831d746/botocore-1.35.36-py3-none-any.whl", hash = "sha256:64241c778bf2dc863d93abab159e14024d97a926a5715056ef6411418cb9ead3", size = 12597046 }, +] + +[[package]] +name = "bt-decode" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "toml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/a9/7449c1073af4ef57520fc01e587a664591ff0331b694a3ec9c1aff3c3133/bt_decode-0.4.0.tar.gz", hash = "sha256:5c7e6286a4f8b9b704f6a0c263ce0e8854fb95d94da5dff6e8835be6de04d508", size = 3496621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/53/43502e90c428e0ff4946112349a6072a52b3c0e73f770284f1c530f5ad53/bt_decode-0.4.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:e2dd446b5956c3c772cdcbfe08fe0d483e68dc07b1606cde5d39c689dffd736c", size = 561621 }, + { url = "https://files.pythonhosted.org/packages/64/f2/a869f4d3bf750a2247a10028b7523e12ba9c62fad072fc88741e64d42236/bt_decode-0.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fcbb0fb758460c5fe7e5276b4406dd15d22ff544d309dd4ebb8fc998ce30d51f", size = 547050 }, + { url = "https://files.pythonhosted.org/packages/b8/c0/d6295ccf4c83dc4b10a19c54a116939a0935350b182d55abf86a36cae7aa/bt_decode-0.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:816f45a75dc78d6beafaf7cc02ab51d73a3dd1c91d4ba0e6b43aae3c637d793d", size = 603391 }, + { url = "https://files.pythonhosted.org/packages/e9/c0/457f63f087b0072e877582e61fac115218b28902df5d9c62d60a42c899d5/bt_decode-0.4.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:39d44102ea27a23644c262d98378ac0ac650e481508f5d6989b8b4e3fd638faf", size = 600597 }, + { url = "https://files.pythonhosted.org/packages/3b/2d/e90271fa86038fcace7eb544923422d91ae36ebf8627291c84ec05d9d22c/bt_decode-0.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:82e959521c60bc48276a91a01bd97726820128a4f4670ae043da35ca11823ca3", size = 669588 }, + { url = "https://files.pythonhosted.org/packages/c0/3e/5d8be99d4d1b3193f526ba12e64fb8c0132511c19859def040f19cdcd2d5/bt_decode-0.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bdea70a4b83e46432999f7743d130dbd49ccf1974c87c87153f7ad3733f5ccea", size = 707978 }, + { url = "https://files.pythonhosted.org/packages/0e/de/2757cab0397594e8547c897696c0983d067c758b1d3ad9cfb944e401bde2/bt_decode-0.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d99b6cc694fe05037c1dca02111d25b2357fd460bea8d8ce9b2432e3ed1d049c", size = 613663 }, + { url = "https://files.pythonhosted.org/packages/7c/15/c0d12ac696b7472f63bb32c61b4b94d75298311840ba315a76b9e2c9a5aa/bt_decode-0.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:645e82838b2e8d7b03686f5cee44e880c56bed3a9dbf2a530c818d1a63544967", size = 664223 }, + { url = "https://files.pythonhosted.org/packages/28/99/c6199f74f1f36279ced846c32d03f245b0d4d8fd2ae1b22842f6cfc4623d/bt_decode-0.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cb32f5c5fda6cada107e3d82b5d760c87cd49075f28105de0900e495ee211659", size = 781056 }, + { url = "https://files.pythonhosted.org/packages/a1/77/896b5f76f4b10d637ffdfd5645f739f5037ff7e7c871cb874528f2c02c40/bt_decode-0.4.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d2ecb71c8b40f3a4abd9c8fda54febffaa298eceafc12a47e9c0cf93e4ccbb8b", size = 861550 }, + { url = "https://files.pythonhosted.org/packages/5a/ea/d2f0b5c8bc2ac59676aa904b4af040f38730caec73fefd8547aabc4222ae/bt_decode-0.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9b7691207021f023485d5adff6758bc0f938f80cf7e1ca05d291189e869217b5", size = 819734 }, + { url = "https://files.pythonhosted.org/packages/dd/82/f7bd11e8b351d5c560daefe87b8884c6e735e1d3eabcd2919684395fb361/bt_decode-0.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:912957e7373014acf4203f3a701f4b820d9d7f5bee1f710298d7346f12bcff59", size = 783927 }, + { url = "https://files.pythonhosted.org/packages/36/0c/0818b22b21ac168cfa07a9f7a46ca7676b175b1e65956dc5700d12c7f744/bt_decode-0.4.0-cp311-cp311-win32.whl", hash = "sha256:fb47926e13f39663e62b4105b436abc84b913cb27edd621308f441cb405956ac", size = 389847 }, + { url = "https://files.pythonhosted.org/packages/96/60/94e86a68062d69c42f3409a48143407a67c6c4cfbcd428ab46d10993fd0a/bt_decode-0.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:001995ff6a20438c5542b13ae0af6458845381ccfd0ef484ae5f7e012c6fb383", size = 416482 }, + { url = "https://files.pythonhosted.org/packages/29/08/090efa626ad7bb545febf8e47a96dd976effcf6c027ff06cf6e053d83104/bt_decode-0.4.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ee9731ecf76ba4f60e10378b16d15bea826b41183ab208e32a9a7fd86d3b7c21", size = 557364 }, + { url = "https://files.pythonhosted.org/packages/6c/53/7e32ff14583db56a9f1ecc2a506a4af9ca6106e2240928d937b0516e0934/bt_decode-0.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6e0ebd9e6f6e710fce9432d448a6add5b266f19af5ec518a2faf19ddd19ce3dc", size = 542812 }, + { url = "https://files.pythonhosted.org/packages/30/39/835655b931dd4b7734743bf66caf28bd94cd5067a8141f6ce22bb8e2de91/bt_decode-0.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fd898558c915dd9374a1860c1aee944cd6acb25f8e0f33f58d18eb989c49fab", size = 604124 }, + { url = "https://files.pythonhosted.org/packages/15/8d/0920fcfa46296fb23093d80554cc305d66a0e66d82b392aea8cd70004dc8/bt_decode-0.4.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4f87500550b030c3d265ab6847ef25f1e4f756b455605f1977329a665e41b330", size = 600859 }, + { url = "https://files.pythonhosted.org/packages/6a/86/0a709fb430d157d0be29733a66e56ee78f8354b2dfba42a64feeb54d6e42/bt_decode-0.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59fa64d5eff9fcc00f536e3ef74932f40aeff1335bd75a469bce90c1762451ae", size = 669825 }, + { url = "https://files.pythonhosted.org/packages/d4/83/58495d791a8be3ee5064af3d6e4039f11a0b13dd3b30e8c91dc247405f23/bt_decode-0.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2be0732720588d047b00eb87e234dd83ebbdb717da8d704b8930b9ab580a6c3", size = 708326 }, + { url = "https://files.pythonhosted.org/packages/56/be/ac3f35a7c23929c428a705e872f596a86afc0eae76d3276b79872abb2817/bt_decode-0.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b4107e8b75966c5be0822a5f0525b568c94dbc1faa8d928090fa48daa329b45", size = 614048 }, + { url = "https://files.pythonhosted.org/packages/7e/ee/6b16c47b5ac00cd511da91ab762c3d2353ba9983f205e8d47a77419221f5/bt_decode-0.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:46e09e7c557fe753c20226ec4db887a4a1b520d36dc4d01eb5d2bd2e2846970e", size = 664008 }, + { url = "https://files.pythonhosted.org/packages/04/09/97f411183dd7497edcf5f0d6cbbd1ef56655395b18e614e272698a9d6802/bt_decode-0.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e817fe5e805bc393b266909709660dc14bd34a671712da0087e164a760b928b4", size = 781116 }, + { url = "https://files.pythonhosted.org/packages/71/f8/ec920e1713e24462142f55aa85c1ad6969d826e2cb32d583ccc37fa8ddb4/bt_decode-0.4.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:59f9a61789003c345b423f1728ee0d774f89cc41be0ab2af0f2ad6e2653084b5", size = 862290 }, + { url = "https://files.pythonhosted.org/packages/8b/c7/5b0504f14f1b8c9b60c69a080832f53774f30db181e472944260e0cfbf1c/bt_decode-0.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:905715452ecf4ce204aa937ee8266ea539fc085377f92bd9506ec76dcd874347", size = 819695 }, + { url = "https://files.pythonhosted.org/packages/13/9e/5d2953e4416db004d21f6c480657c8f9b84ee27b48fe5478d2cdba2ec49a/bt_decode-0.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2e85f5f12e6bb00253e194372d90e60f129d613f0ddedae659d3b9a3049a69cf", size = 784116 }, + { url = "https://files.pythonhosted.org/packages/7e/b2/26f374ee94c88a90310569bd5d2f282c105a7ee1ae298e0282d3ee560f50/bt_decode-0.4.0-cp312-cp312-win32.whl", hash = "sha256:ed4c3c4383c9903f371502c0d62ce88ecd2c531044e04deaeb60c827ae45ad8e", size = 390937 }, + { url = "https://files.pythonhosted.org/packages/7e/35/0610ddaf739013a3fff13961edadeefff4be83fff7735bc0592214f0246b/bt_decode-0.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:68beccbb00f129b75d189d2ffc48fd430bf4eab8a456aab79615b17eec82437d", size = 417431 }, + { url = "https://files.pythonhosted.org/packages/6b/2f/4cdfdf8bd52a38e27b50f36e9b9288085a9bab1d703310cc426e4b4243be/bt_decode-0.4.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:88de7129c3323c36cd6cce28844fb475556a865ec6fc87934ec5deeb95ff2d86", size = 557018 }, + { url = "https://files.pythonhosted.org/packages/43/16/7d29d9f719bab8f3890d6d6dfaaade16aa7616e57bdde8f0114781430134/bt_decode-0.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:056e6245a2119b391306542134651df54df29569136be892411073fc10840c8e", size = 542668 }, + { url = "https://files.pythonhosted.org/packages/c5/d3/a15421174b9943fd86f2470bfe109b6b6a800a2e9cca414b5bb1b2367752/bt_decode-0.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:faa76d0b8fcb0f9ae2107e8c6ae84ea670de81c0adda4967a52d4b7d1de8c605", size = 603689 }, + { url = "https://files.pythonhosted.org/packages/9e/e7/ef333c2c6c2b2319fef3e28ef9d5a2e82c30b8c7f7f3875b182dae7fc957/bt_decode-0.4.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7a3ff15bfe86d482e642dfaa6e5581b65815e7663f337af7502b422fea2fdcc2", size = 600436 }, + { url = "https://files.pythonhosted.org/packages/7c/4c/3bd5c96dcf2ef09d73f0d35cbdc0d32c1b8f9f0c0d9e10af087405f38e7d/bt_decode-0.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa7687c01c516f84274a2e71ba717898eef095e08ec7125823f7a4e230bd46fe", size = 669460 }, + { url = "https://files.pythonhosted.org/packages/81/d7/df22e559dfe7941edfb33357fbc2dc9f6025ae4fb58740213dc09b1dd53b/bt_decode-0.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9d3cf8cfff714600db01c6cd144906fe0a8be85293711e279b8089f6ccaffd71", size = 707396 }, + { url = "https://files.pythonhosted.org/packages/2c/19/0d1eeb47ac8844021e6f7f69c92069c0c80ccee1de1614a9e5dac96da50e/bt_decode-0.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:983972ecc83bd0507e72ae316281960b7e26e31386525c7905f7cdb8fa3e7de1", size = 613845 }, + { url = "https://files.pythonhosted.org/packages/a2/06/308512e5f17e3b3a9472d2271114da0caa394c38523b7d0aa5fc75ee3b89/bt_decode-0.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:32e3950b120b8b59ae5ab70005ba9b5c7560a0e222e805f47878cb259a32ed39", size = 663927 }, + { url = "https://files.pythonhosted.org/packages/a4/53/ec4fc237ffe8b8f7e8e4bd78b54b0c82abad5407f3faed7df0828ba2f0f2/bt_decode-0.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:66d906ac225e3cd169dde1e0af21e8d73e8ea7dea3f7e9afcdec501bced3d83a", size = 781071 }, + { url = "https://files.pythonhosted.org/packages/d2/d7/700ddb1280e5aafd0404f445847ec6c4c27f7df949a7d148e8dc3c0f5a3f/bt_decode-0.4.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:58bf09b004dc182748e285b5bc15ac6305af4ab9c318f995c443ba33bb61fbb6", size = 862093 }, + { url = "https://files.pythonhosted.org/packages/53/32/c9d9a5787f793da0ac8a9b5c950f45ad8b2449a751cf5b84ab430c2bc9f7/bt_decode-0.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c202f22152b3186cbc1c319250d6b0ecfe87cf9a4e8e90b19cc9f83786acdf1a", size = 819486 }, + { url = "https://files.pythonhosted.org/packages/3f/94/c182bd002357d68d663a118dc41b95d5f400aac6e9e5074c53693b6de41a/bt_decode-0.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b6dd31b0947b7b15a36f7f9bfdb8ae30ffe3f3f97e0dc4d60bf79b9baf57f4e5", size = 784067 }, + { url = "https://files.pythonhosted.org/packages/29/9c/a17e71aa0e4f674c7a59b5e65b042d2bdf91bebc316e969a1c31c6b51ef1/bt_decode-0.4.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ebb3b72146e7feb08e235d78457b597697708149d7410f184098b73c5ab38aa", size = 600955 }, + { url = "https://files.pythonhosted.org/packages/f4/c6/429323a3c72251c6bc22926995ea3e490db07bb96e608ac4ca9eaa282e62/bt_decode-0.4.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9571680e6b74fab00cbd10dc255594692a9cdf615e33170d5a32112c1da8e3e4", size = 599227 }, + { url = "https://files.pythonhosted.org/packages/15/f4/3495a7d242668d347e851424e95acbbd2916ae70f7827e0533bd3c59e653/bt_decode-0.4.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dec8af1719ced86da6f7b1dcf70e1d480cfb86e2cf7530692d3e66ad1e16067d", size = 666872 }, + { url = "https://files.pythonhosted.org/packages/f1/3a/f0875014848888259f8646f915c1a8046d420799a155ce80d5af10e77044/bt_decode-0.4.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0d46d2308e13615951f89ff7ba05364a2e3747626b29fd4ee39c085ea56cb5fe", size = 709410 }, + { url = "https://files.pythonhosted.org/packages/be/e5/bc31c0f2a29945c548cda2538c8b5368da722217da7ca0a64eedd4df56a2/bt_decode-0.4.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0df0436d736544587002e0fa4fe3887b28cec8de4a9036c1ea776c560e966b8d", size = 778135 }, + { url = "https://files.pythonhosted.org/packages/25/48/387fd8cef96a86c39e6716455b493a759fbe9a67bcaa2dfe39c3d3b6b11b/bt_decode-0.4.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:579aba5010a078831af2025cd03df9d429fa35008ec46bc1561e6147e2c9769e", size = 860601 }, + { url = "https://files.pythonhosted.org/packages/12/85/1458d9eaf9a74390ac5e0a1a3be5eaf53550aa4f4c28362fb4f80a94c8a6/bt_decode-0.4.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:039e880688d4c5f2ee090980649811b700593e21eccee520b294c07b85008bce", size = 817941 }, + { url = "https://files.pythonhosted.org/packages/70/72/723265284f71fb95556c5b27c83a370b2e38e02666fd17dbb129856fb1f2/bt_decode-0.4.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1a45173a6f0e48b28b190bfb250b6683984d115d70a6d2ff5102a2421d581de6", size = 783857 }, +] + +[[package]] +name = "certifi" +version = "2024.8.30" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 }, + { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 }, + { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 }, + { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 }, + { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 }, + { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 }, + { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 }, + { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 }, + { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 }, + { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 }, + { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 }, + { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 }, + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 }, + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", size = 106620 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/61/73589dcc7a719582bf56aae309b6103d2762b526bffe189d635a7fcfd998/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", size = 193339 }, + { url = "https://files.pythonhosted.org/packages/77/d5/8c982d58144de49f59571f940e329ad6e8615e1e82ef84584c5eeb5e1d72/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", size = 124366 }, + { url = "https://files.pythonhosted.org/packages/bf/19/411a64f01ee971bed3231111b69eb56f9331a769072de479eae7de52296d/charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", size = 118874 }, + { url = "https://files.pythonhosted.org/packages/4c/92/97509850f0d00e9f14a46bc751daabd0ad7765cff29cdfb66c68b6dad57f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", size = 138243 }, + { url = "https://files.pythonhosted.org/packages/e2/29/d227805bff72ed6d6cb1ce08eec707f7cfbd9868044893617eb331f16295/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", size = 148676 }, + { url = "https://files.pythonhosted.org/packages/13/bc/87c2c9f2c144bedfa62f894c3007cd4530ba4b5351acb10dc786428a50f0/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", size = 141289 }, + { url = "https://files.pythonhosted.org/packages/eb/5b/6f10bad0f6461fa272bfbbdf5d0023b5fb9bc6217c92bf068fa5a99820f5/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", size = 142585 }, + { url = "https://files.pythonhosted.org/packages/3b/a0/a68980ab8a1f45a36d9745d35049c1af57d27255eff8c907e3add84cf68f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", size = 144408 }, + { url = "https://files.pythonhosted.org/packages/d7/a1/493919799446464ed0299c8eef3c3fad0daf1c3cd48bff9263c731b0d9e2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", size = 139076 }, + { url = "https://files.pythonhosted.org/packages/fb/9d/9c13753a5a6e0db4a0a6edb1cef7aee39859177b64e1a1e748a6e3ba62c2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", size = 146874 }, + { url = "https://files.pythonhosted.org/packages/75/d2/0ab54463d3410709c09266dfb416d032a08f97fd7d60e94b8c6ef54ae14b/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", size = 150871 }, + { url = "https://files.pythonhosted.org/packages/8d/c9/27e41d481557be53d51e60750b85aa40eaf52b841946b3cdeff363105737/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", size = 148546 }, + { url = "https://files.pythonhosted.org/packages/ee/44/4f62042ca8cdc0cabf87c0fc00ae27cd8b53ab68be3605ba6d071f742ad3/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", size = 143048 }, + { url = "https://files.pythonhosted.org/packages/01/f8/38842422988b795220eb8038745d27a675ce066e2ada79516c118f291f07/charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", size = 94389 }, + { url = "https://files.pythonhosted.org/packages/0b/6e/b13bd47fa9023b3699e94abf565b5a2f0b0be6e9ddac9812182596ee62e4/charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", size = 101752 }, + { url = "https://files.pythonhosted.org/packages/d3/0b/4b7a70987abf9b8196845806198975b6aab4ce016632f817ad758a5aa056/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", size = 194445 }, + { url = "https://files.pythonhosted.org/packages/50/89/354cc56cf4dd2449715bc9a0f54f3aef3dc700d2d62d1fa5bbea53b13426/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", size = 125275 }, + { url = "https://files.pythonhosted.org/packages/fa/44/b730e2a2580110ced837ac083d8ad222343c96bb6b66e9e4e706e4d0b6df/charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", size = 119020 }, + { url = "https://files.pythonhosted.org/packages/9d/e4/9263b8240ed9472a2ae7ddc3e516e71ef46617fe40eaa51221ccd4ad9a27/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", size = 139128 }, + { url = "https://files.pythonhosted.org/packages/6b/e3/9f73e779315a54334240353eaea75854a9a690f3f580e4bd85d977cb2204/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", size = 149277 }, + { url = "https://files.pythonhosted.org/packages/1a/cf/f1f50c2f295312edb8a548d3fa56a5c923b146cd3f24114d5adb7e7be558/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", size = 142174 }, + { url = "https://files.pythonhosted.org/packages/16/92/92a76dc2ff3a12e69ba94e7e05168d37d0345fa08c87e1fe24d0c2a42223/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", size = 143838 }, + { url = "https://files.pythonhosted.org/packages/a4/01/2117ff2b1dfc61695daf2babe4a874bca328489afa85952440b59819e9d7/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", size = 146149 }, + { url = "https://files.pythonhosted.org/packages/f6/9b/93a332b8d25b347f6839ca0a61b7f0287b0930216994e8bf67a75d050255/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", size = 140043 }, + { url = "https://files.pythonhosted.org/packages/ab/f6/7ac4a01adcdecbc7a7587767c776d53d369b8b971382b91211489535acf0/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", size = 148229 }, + { url = "https://files.pythonhosted.org/packages/9d/be/5708ad18161dee7dc6a0f7e6cf3a88ea6279c3e8484844c0590e50e803ef/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", size = 151556 }, + { url = "https://files.pythonhosted.org/packages/5a/bb/3d8bc22bacb9eb89785e83e6723f9888265f3a0de3b9ce724d66bd49884e/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", size = 149772 }, + { url = "https://files.pythonhosted.org/packages/f7/fa/d3fc622de05a86f30beea5fc4e9ac46aead4731e73fd9055496732bcc0a4/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", size = 144800 }, + { url = "https://files.pythonhosted.org/packages/9a/65/bdb9bc496d7d190d725e96816e20e2ae3a6fa42a5cac99c3c3d6ff884118/charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", size = 94836 }, + { url = "https://files.pythonhosted.org/packages/3e/67/7b72b69d25b89c0b3cea583ee372c43aa24df15f0e0f8d3982c57804984b/charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", size = 102187 }, + { url = "https://files.pythonhosted.org/packages/f3/89/68a4c86f1a0002810a27f12e9a7b22feb198c59b2f05231349fbce5c06f4/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", size = 194617 }, + { url = "https://files.pythonhosted.org/packages/4f/cd/8947fe425e2ab0aa57aceb7807af13a0e4162cd21eee42ef5b053447edf5/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", size = 125310 }, + { url = "https://files.pythonhosted.org/packages/5b/f0/b5263e8668a4ee9becc2b451ed909e9c27058337fda5b8c49588183c267a/charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", size = 119126 }, + { url = "https://files.pythonhosted.org/packages/ff/6e/e445afe4f7fda27a533f3234b627b3e515a1b9429bc981c9a5e2aa5d97b6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", size = 139342 }, + { url = "https://files.pythonhosted.org/packages/a1/b2/4af9993b532d93270538ad4926c8e37dc29f2111c36f9c629840c57cd9b3/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", size = 149383 }, + { url = "https://files.pythonhosted.org/packages/fb/6f/4e78c3b97686b871db9be6f31d64e9264e889f8c9d7ab33c771f847f79b7/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", size = 142214 }, + { url = "https://files.pythonhosted.org/packages/2b/c9/1c8fe3ce05d30c87eff498592c89015b19fade13df42850aafae09e94f35/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", size = 144104 }, + { url = "https://files.pythonhosted.org/packages/ee/68/efad5dcb306bf37db7db338338e7bb8ebd8cf38ee5bbd5ceaaaa46f257e6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", size = 146255 }, + { url = "https://files.pythonhosted.org/packages/0c/75/1ed813c3ffd200b1f3e71121c95da3f79e6d2a96120163443b3ad1057505/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", size = 140251 }, + { url = "https://files.pythonhosted.org/packages/7d/0d/6f32255c1979653b448d3c709583557a4d24ff97ac4f3a5be156b2e6a210/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", size = 148474 }, + { url = "https://files.pythonhosted.org/packages/ac/a0/c1b5298de4670d997101fef95b97ac440e8c8d8b4efa5a4d1ef44af82f0d/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", size = 151849 }, + { url = "https://files.pythonhosted.org/packages/04/4f/b3961ba0c664989ba63e30595a3ed0875d6790ff26671e2aae2fdc28a399/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", size = 149781 }, + { url = "https://files.pythonhosted.org/packages/d8/90/6af4cd042066a4adad58ae25648a12c09c879efa4849c705719ba1b23d8c/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482", size = 144970 }, + { url = "https://files.pythonhosted.org/packages/cc/67/e5e7e0cbfefc4ca79025238b43cdf8a2037854195b37d6417f3d0895c4c2/charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", size = 94973 }, + { url = "https://files.pythonhosted.org/packages/65/97/fc9bbc54ee13d33dc54a7fcf17b26368b18505500fc01e228c27b5222d80/charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", size = 102308 }, + { url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446 }, +] + +[[package]] +name = "click" +version = "8.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "platform_system == 'Windows'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "cryptography" +version = "43.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/05/07b55d1fa21ac18c3a8c79f764e2514e6f6a9698f1be44994f5adf0d29db/cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805", size = 686989 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/f3/01fdf26701a26f4b4dbc337a26883ad5bccaa6f1bbbdd29cd89e22f18a1c/cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e", size = 6225303 }, + { url = "https://files.pythonhosted.org/packages/a3/01/4896f3d1b392025d4fcbecf40fdea92d3df8662123f6835d0af828d148fd/cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e", size = 3760905 }, + { url = "https://files.pythonhosted.org/packages/0a/be/f9a1f673f0ed4b7f6c643164e513dbad28dd4f2dcdf5715004f172ef24b6/cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f", size = 3977271 }, + { url = "https://files.pythonhosted.org/packages/4e/49/80c3a7b5514d1b416d7350830e8c422a4d667b6d9b16a9392ebfd4a5388a/cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6", size = 3746606 }, + { url = "https://files.pythonhosted.org/packages/0e/16/a28ddf78ac6e7e3f25ebcef69ab15c2c6be5ff9743dd0709a69a4f968472/cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18", size = 3986484 }, + { url = "https://files.pythonhosted.org/packages/01/f5/69ae8da70c19864a32b0315049866c4d411cce423ec169993d0434218762/cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd", size = 3852131 }, + { url = "https://files.pythonhosted.org/packages/fd/db/e74911d95c040f9afd3612b1f732e52b3e517cb80de8bf183be0b7d413c6/cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73", size = 4075647 }, + { url = "https://files.pythonhosted.org/packages/56/48/7b6b190f1462818b324e674fa20d1d5ef3e24f2328675b9b16189cbf0b3c/cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2", size = 2623873 }, + { url = "https://files.pythonhosted.org/packages/eb/b1/0ebff61a004f7f89e7b65ca95f2f2375679d43d0290672f7713ee3162aff/cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd", size = 3068039 }, + { url = "https://files.pythonhosted.org/packages/30/d5/c8b32c047e2e81dd172138f772e81d852c51f0f2ad2ae8a24f1122e9e9a7/cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984", size = 6222984 }, + { url = "https://files.pythonhosted.org/packages/2f/78/55356eb9075d0be6e81b59f45c7b48df87f76a20e73893872170471f3ee8/cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5", size = 3762968 }, + { url = "https://files.pythonhosted.org/packages/2a/2c/488776a3dc843f95f86d2f957ca0fc3407d0242b50bede7fad1e339be03f/cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4", size = 3977754 }, + { url = "https://files.pythonhosted.org/packages/7c/04/2345ca92f7a22f601a9c62961741ef7dd0127c39f7310dffa0041c80f16f/cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7", size = 3749458 }, + { url = "https://files.pythonhosted.org/packages/ac/25/e715fa0bc24ac2114ed69da33adf451a38abb6f3f24ec207908112e9ba53/cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405", size = 3988220 }, + { url = "https://files.pythonhosted.org/packages/21/ce/b9c9ff56c7164d8e2edfb6c9305045fbc0df4508ccfdb13ee66eb8c95b0e/cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16", size = 3853898 }, + { url = "https://files.pythonhosted.org/packages/2a/33/b3682992ab2e9476b9c81fff22f02c8b0a1e6e1d49ee1750a67d85fd7ed2/cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73", size = 4076592 }, + { url = "https://files.pythonhosted.org/packages/81/1e/ffcc41b3cebd64ca90b28fd58141c5f68c83d48563c88333ab660e002cd3/cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995", size = 2623145 }, + { url = "https://files.pythonhosted.org/packages/87/5c/3dab83cc4aba1f4b0e733e3f0c3e7d4386440d660ba5b1e3ff995feb734d/cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362", size = 3068026 }, +] + +[[package]] +name = "cytoolz" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "toolz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e2/4c/ca9b05bdfa28ddbb4a5365c27021a1d4be61db7d8f6b4e5d4e76aa4ba3b7/cytoolz-1.0.0.tar.gz", hash = "sha256:eb453b30182152f9917a5189b7d99046b6ce90cdf8aeb0feff4b2683e600defd", size = 626708 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/99/b489081777ad02c9bba294c757583416d0bdbd9403017145aba68145c16f/cytoolz-1.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dffc22fd2c91be64dbdbc462d0786f8e8ac9a275cfa1869a1084d1867d4f67e0", size = 406148 }, + { url = "https://files.pythonhosted.org/packages/b2/d3/a4d58bf89924dbca34e8dbb643b26935e08c16b4a2ee255d43a8b7489939/cytoolz-1.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a99e7e29274e293f4ffe20e07f76c2ac753a78f1b40c1828dfc54b2981b2f6c4", size = 384956 }, + { url = "https://files.pythonhosted.org/packages/81/d4/4d09e6571ef3b143f668c590a7a00c97ff24e6df6901f457ea7c782cd2ba/cytoolz-1.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c507a3e0a45c41d66b43f96797290d75d1e7a8549aa03a4a6b8854fdf3f7b8d8", size = 2091688 }, + { url = "https://files.pythonhosted.org/packages/8d/7b/68c89bed2e0490e9b946574c3bc79711179f35b1dc5eb31046c535f1bed2/cytoolz-1.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:643a593ec272ef7429099e1182a22f64ec2696c00d295d2a5be390db1b7ff176", size = 2188448 }, + { url = "https://files.pythonhosted.org/packages/56/a3/4e536fc7b72fd7495e19180463e8160a4fe1d50ab59a5854fc596621d5c3/cytoolz-1.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6ce38e2e42cbae30446190c59b92a8a9029e1806fd79eaf88f48b0fe33003893", size = 2174196 }, + { url = "https://files.pythonhosted.org/packages/5a/7f/0451778af9e22755a95ef4400ee7fc6e41387521ab0f17699593cb07169a/cytoolz-1.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:810a6a168b8c5ecb412fbae3dd6f7ed6c6253a63caf4174ee9794ebd29b2224f", size = 2099823 }, + { url = "https://files.pythonhosted.org/packages/58/b7/8ffdef1ac8f74b0cc650b9d4a74d93d911a7e20fcf7cc0abac0f4bce225f/cytoolz-1.0.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0ce8a2a85c0741c1b19b16e6782c4a5abc54c3caecda66793447112ab2fa9884", size = 1996733 }, + { url = "https://files.pythonhosted.org/packages/1c/ab/9c694c883f3038d167b797cc55c64c2bdb64146428000cea15f235f30a0f/cytoolz-1.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ea4ac72e6b830861035c4c7999af8e55813f57c6d1913a3d93cc4a6babc27bf7", size = 2013725 }, + { url = "https://files.pythonhosted.org/packages/6c/df/859faaee91c795dc969c79cd38159031f373828d44b0b18999feb7d9a44d/cytoolz-1.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a09cdfb21dfb38aa04df43e7546a41f673377eb5485da88ceb784e327ec7603b", size = 1994851 }, + { url = "https://files.pythonhosted.org/packages/34/2a/26ac5a34e859c5ba32351f5a74492f4ed12e7a7e75b6afccf11c4100aa70/cytoolz-1.0.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:658dd85deb375ff7af990a674e5c9058cef1c9d1f5dc89bc87b77be499348144", size = 2155343 }, + { url = "https://files.pythonhosted.org/packages/3d/41/f687d2e40407b29bfcce36a7d456dad368283ea543aa39da53bcc119974e/cytoolz-1.0.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9715d1ff5576919d10b68f17241375f6a1eec8961c25b78a83e6ef1487053f39", size = 2163507 }, + { url = "https://files.pythonhosted.org/packages/39/7c/70d529f909d97ea214d59923c19e3d05a3768fe8e2066542b72550a31ca4/cytoolz-1.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f370a1f1f1afc5c1c8cc5edc1cfe0ba444263a0772af7ce094be8e734f41769d", size = 2054428 }, + { url = "https://files.pythonhosted.org/packages/5a/4a/7bb2eafe4077f6d9867547ca74ca4b75bc8a081e32a47e186e5c067f6cab/cytoolz-1.0.0-cp311-cp311-win32.whl", hash = "sha256:dbb2ec1177dca700f3db2127e572da20de280c214fc587b2a11c717fc421af56", size = 322300 }, + { url = "https://files.pythonhosted.org/packages/5a/ed/a1c955444343224ab1317a41f6575bc640055eb2495e8f9175f8f28bd776/cytoolz-1.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:0983eee73df86e54bb4a79fcc4996aa8b8368fdbf43897f02f9c3bf39c4dc4fb", size = 365539 }, + { url = "https://files.pythonhosted.org/packages/28/2e/a8b71f74ee75f33164bfbc6324ddd1e8d0f425255b1c930141516f51d539/cytoolz-1.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:10e3986066dc379e30e225b230754d9f5996aa8d84c2accc69c473c21d261e46", size = 414110 }, + { url = "https://files.pythonhosted.org/packages/c6/0a/999af6bb896375b0c687e292a3dcd4edb338a2764bbac40c0ce11eb21c64/cytoolz-1.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:16576f1bb143ee2cb9f719fcc4b845879fb121f9075c7c5e8a5ff4854bd02fc6", size = 390900 }, + { url = "https://files.pythonhosted.org/packages/30/04/02f0ee5339f8c6ef785f06caee85e17e8e0b406e7e553c8fd99a55ff8390/cytoolz-1.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3faa25a1840b984315e8b3ae517312375f4273ffc9a2f035f548b7f916884f37", size = 2090729 }, + { url = "https://files.pythonhosted.org/packages/04/de/296ded5f81ada90ae4db8c06cc34b142cf6c51fabb4c3c78583abba91c36/cytoolz-1.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:781fce70a277b20fd95dc66811d1a97bb07b611ceea9bda8b7dd3c6a4b05d59a", size = 2155926 }, + { url = "https://files.pythonhosted.org/packages/cc/ce/d5782bdd3d2fd16d87e83e70e14fcfa65ba67ba21cf7e1007505baef7d79/cytoolz-1.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7a562c25338eb24d419d1e80a7ae12133844ce6fdeb4ab54459daf250088a1b2", size = 2171893 }, + { url = "https://files.pythonhosted.org/packages/d0/02/22a8c74ff13f8a08e8cacd0a0aa34da3a6e3637cf477e376efc61f7567e5/cytoolz-1.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f29d8330aaf070304f7cd5cb7e73e198753624eb0aec278557cccd460c699b5b", size = 2125265 }, + { url = "https://files.pythonhosted.org/packages/50/d1/a3f2e2ced1fa7e2b5607d05ed4de9951491004a4804e96f78778d11bebd4/cytoolz-1.0.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:98a96c54aa55ed9c7cdb23c2f0df39a7b4ee518ac54888480b5bdb5ef69c7ef0", size = 1973962 }, + { url = "https://files.pythonhosted.org/packages/3e/10/174d9585e1011824e2e6e79380f8b1c6e49070c35a278d823d996d1c11e6/cytoolz-1.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:287d6d7f475882c2ddcbedf8da9a9b37d85b77690779a2d1cdceb5ae3998d52e", size = 2021691 }, + { url = "https://files.pythonhosted.org/packages/84/aa/bebdca3ae140698d3d4fe75ffd4c87a15ee999cee6b994e0831e5a24cdd7/cytoolz-1.0.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:05a871688df749b982839239fcd3f8ec3b3b4853775d575ff9cd335fa7c75035", size = 2010169 }, + { url = "https://files.pythonhosted.org/packages/2e/9f/8d5940c953534a4d2ae4419bb4fdc1eb5559345fed1f4838708073d7e6b4/cytoolz-1.0.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:28bb88e1e2f7d6d4b8e0890b06d292c568984d717de3e8381f2ca1dd12af6470", size = 2154314 }, + { url = "https://files.pythonhosted.org/packages/84/bf/a5414601c95afde30a0c038c5d6273c188f1da8ff25a057bd0c683679e5c/cytoolz-1.0.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:576a4f1fc73d8836b10458b583f915849da6e4f7914f4ecb623ad95c2508cad5", size = 2188368 }, + { url = "https://files.pythonhosted.org/packages/67/fe/990ea30d56b9b6602f3bf4af77a1bfd9233e6ffb761b11b8864619fed508/cytoolz-1.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:509ed3799c47e4ada14f63e41e8f540ac6e2dab97d5d7298934e6abb9d3830ec", size = 2077906 }, + { url = "https://files.pythonhosted.org/packages/ab/95/94b936501e1e23460732631e5d7c6efc4f6c09df21a594dfca9bf30b9411/cytoolz-1.0.0-cp312-cp312-win32.whl", hash = "sha256:9ce25f02b910630f6dc2540dd1e26c9326027ddde6c59f8cab07c56acc70714c", size = 322445 }, + { url = "https://files.pythonhosted.org/packages/be/04/a49b73591b132be5a28c0670229629a3c002cfac59582a1d38b16bdc6fed/cytoolz-1.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:7e53cfcce87e05b7f0ae2fb2b3e5820048cd0bb7b701e92bd8f75c9fbb7c9ae9", size = 364595 }, +] + +[[package]] +name = "decorator" +version = "5.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/0c/8d907af351aa16b42caae42f9d6aa37b900c67308052d10fdce809f8d952/decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330", size = 35016 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/50/83c593b07763e1161326b3b8c6686f0f4b0f24d5526546bee538c89837d6/decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186", size = 9073 }, +] + +[[package]] +name = "docker-pycreds" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/e6/d1f6c00b7221e2d7c4b470132c931325c8b22c51ca62417e300f5ce16009/docker-pycreds-0.4.0.tar.gz", hash = "sha256:6ce3270bcaf404cc4c3e27e4b6c70d3521deae82fb508767870fdbf772d584d4", size = 8754 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/e8/f6bd1eee09314e7e6dee49cbe2c5e22314ccdb38db16c9fc72d2fa80d054/docker_pycreds-0.4.0-py2.py3-none-any.whl", hash = "sha256:7266112468627868005106ec19cd0d722702d2b7d5912a28e19b826c3d37af49", size = 8982 }, +] + +[[package]] +name = "ecdsa" +version = "0.19.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/d0/ec8ac1de7accdcf18cfe468653ef00afd2f609faf67c423efbd02491051b/ecdsa-0.19.0.tar.gz", hash = "sha256:60eaad1199659900dd0af521ed462b793bbdf867432b3948e87416ae4caf6bf8", size = 197791 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/e7/ed3243b30d1bec41675b6394a1daae46349dc2b855cb83be846a5a918238/ecdsa-0.19.0-py2.py3-none-any.whl", hash = "sha256:2cea9b88407fdac7bbeca0833b189e4c9c53f2ef1e1eaa29f6224dbc809b707a", size = 149266 }, +] + +[[package]] +name = "einops" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/ca/9f5dcb8bead39959454c3912266bedc4c315839cee0e0ca9f4328f4588c1/einops-0.8.0.tar.gz", hash = "sha256:63486517fed345712a8385c100cb279108d9d47e6ae59099b07657e983deae85", size = 58861 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/5a/f0b9ad6c0a9017e62d4735daaeb11ba3b6c009d69a26141b258cd37b5588/einops-0.8.0-py3-none-any.whl", hash = "sha256:9572fb63046264a862693b0a87088af3bdc8c068fde03de63453cbbde245465f", size = 43223 }, +] + +[[package]] +name = "eth-hash" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/b6/57c89b91cf2dbb02b3019337f97bf346167d06cd23d3bde43c9fe52cae7e/eth-hash-0.7.0.tar.gz", hash = "sha256:bacdc705bfd85dadd055ecd35fd1b4f846b671add101427e089a4ca2e8db310a", size = 12463 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/f0/a35e791bd73fa425838d8d0157754150ded141a94cf30d567dfeb9d57316/eth_hash-0.7.0-py3-none-any.whl", hash = "sha256:b8d5a230a2b251f4a291e3164a23a14057c4a6de4b0aa4a16fa4dc9161b57e2f", size = 8650 }, +] + +[[package]] +name = "eth-keys" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "eth-typing" }, + { name = "eth-utils" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/4a/aabe0bff4e299858845fba5598c435f2bee0646366b9635750133904e2d8/eth_keys-0.6.0.tar.gz", hash = "sha256:ba33230f851d02c894e83989185b21d76152c49b37e35b61b1d8a6d9f1d20430", size = 28944 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/ee/583612eed5d49f10bd1749d7dda9e93691ab02724b7af84830046e31c64c/eth_keys-0.6.0-py3-none-any.whl", hash = "sha256:b396fdfe048a5bba3ef3990739aec64901eb99901c03921caa774be668b1db6e", size = 21210 }, +] + +[[package]] +name = "eth-typing" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/32/d5a1bdf872f92a7c3361396b684aeba7abaabb341bd22a80029abcd1f68e/eth_typing-5.0.1.tar.gz", hash = "sha256:83debf88c9df286db43bb7374974681ebcc9f048fac81be2548dbc549a3203c0", size = 22716 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/ef/a66ff9b83df51b83c1af468fc7b5e4a3855d9e3c01e2365ecfe1c5e84077/eth_typing-5.0.1-py3-none-any.whl", hash = "sha256:f30d1af16aac598f216748a952eeb64fbcb6e73efa691d2de31148138afe96de", size = 20085 }, +] + +[[package]] +name = "eth-utils" +version = "2.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cytoolz", marker = "implementation_name == 'cpython'" }, + { name = "eth-hash" }, + { name = "eth-typing" }, + { name = "toolz", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/1f/b112416a32cae14cf986f22f85abcccec54054d3d4c699ce831faaf7bf37/eth-utils-2.2.2.tar.gz", hash = "sha256:5ca6265177ce544d9d43cdf2272ae2227e5d6d9529c270bbb707d17339087101", size = 21129 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/3d/f01836312cd8b4a8768546e78b48feb52375123e2f4343119b27e78db9b9/eth_utils-2.2.2-py3-none-any.whl", hash = "sha256:2580a8065273f62ca1ec4c175228c52e626a5f1007e965d2117e5eca1a93cae8", size = 23893 }, +] + +[[package]] +name = "fastapi" +version = "0.110.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/22/7b9ee50b0a8c48f076a111d6e4071a9d4c25623dc67689c5f3aa375f779b/fastapi-0.110.3.tar.gz", hash = "sha256:555700b0159379e94fdbfc6bb66a0f1c43f4cf7060f25239af3d84b63a656626", size = 287508 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/d1/5958526c3bdbed74f88bf69b86506db5b25a600207f0f688473667690de6/fastapi-0.110.3-py3-none-any.whl", hash = "sha256:fd7600612f755e4050beb74001310b5a7e1796d149c2ee363124abdfa0289d32", size = 91834 }, +] + +[[package]] +name = "filelock" +version = "3.16.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/db/3ef5bb276dae18d6ec2124224403d1d67bccdbefc17af4cc8f553e341ab1/filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435", size = 18037 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163 }, +] + +[[package]] +name = "frozenlist" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8f/ed/0f4cec13a93c02c47ec32d81d11c0c1efbadf4a471e3f3ce7cad366cbbd3/frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817", size = 39930 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/43/0bed28bf5eb1c9e4301003b74453b8e7aa85fb293b31dde352aac528dafc/frozenlist-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fd74520371c3c4175142d02a976aee0b4cb4a7cc912a60586ffd8d5929979b30", size = 94987 }, + { url = "https://files.pythonhosted.org/packages/bb/bf/b74e38f09a246e8abbe1e90eb65787ed745ccab6eaa58b9c9308e052323d/frozenlist-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2f3f7a0fbc219fb4455264cae4d9f01ad41ae6ee8524500f381de64ffaa077d5", size = 54584 }, + { url = "https://files.pythonhosted.org/packages/2c/31/ab01375682f14f7613a1ade30149f684c84f9b8823a4391ed950c8285656/frozenlist-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f47c9c9028f55a04ac254346e92977bf0f166c483c74b4232bee19a6697e4778", size = 52499 }, + { url = "https://files.pythonhosted.org/packages/98/a8/d0ac0b9276e1404f58fec3ab6e90a4f76b778a49373ccaf6a563f100dfbc/frozenlist-1.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0996c66760924da6e88922756d99b47512a71cfd45215f3570bf1e0b694c206a", size = 276357 }, + { url = "https://files.pythonhosted.org/packages/ad/c9/c7761084fa822f07dac38ac29f841d4587570dd211e2262544aa0b791d21/frozenlist-1.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2fe128eb4edeabe11896cb6af88fca5346059f6c8d807e3b910069f39157869", size = 287516 }, + { url = "https://files.pythonhosted.org/packages/a1/ff/cd7479e703c39df7bdab431798cef89dc75010d8aa0ca2514c5b9321db27/frozenlist-1.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a8ea951bbb6cacd492e3948b8da8c502a3f814f5d20935aae74b5df2b19cf3d", size = 283131 }, + { url = "https://files.pythonhosted.org/packages/59/a0/370941beb47d237eca4fbf27e4e91389fd68699e6f4b0ebcc95da463835b/frozenlist-1.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de537c11e4aa01d37db0d403b57bd6f0546e71a82347a97c6a9f0dcc532b3a45", size = 261320 }, + { url = "https://files.pythonhosted.org/packages/b8/5f/c10123e8d64867bc9b4f2f510a32042a306ff5fcd7e2e09e5ae5100ee333/frozenlist-1.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c2623347b933fcb9095841f1cc5d4ff0b278addd743e0e966cb3d460278840d", size = 274877 }, + { url = "https://files.pythonhosted.org/packages/fa/79/38c505601ae29d4348f21706c5d89755ceded02a745016ba2f58bd5f1ea6/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cee6798eaf8b1416ef6909b06f7dc04b60755206bddc599f52232606e18179d3", size = 269592 }, + { url = "https://files.pythonhosted.org/packages/19/e2/39f3a53191b8204ba9f0bb574b926b73dd2efba2a2b9d2d730517e8f7622/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f5f9da7f5dbc00a604fe74aa02ae7c98bcede8a3b8b9666f9f86fc13993bc71a", size = 265934 }, + { url = "https://files.pythonhosted.org/packages/d5/c9/3075eb7f7f3a91f1a6b00284af4de0a65a9ae47084930916f5528144c9dd/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:90646abbc7a5d5c7c19461d2e3eeb76eb0b204919e6ece342feb6032c9325ae9", size = 283859 }, + { url = "https://files.pythonhosted.org/packages/05/f5/549f44d314c29408b962fa2b0e69a1a67c59379fb143b92a0a065ffd1f0f/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bdac3c7d9b705d253b2ce370fde941836a5f8b3c5c2b8fd70940a3ea3af7f4f2", size = 287560 }, + { url = "https://files.pythonhosted.org/packages/9d/f8/cb09b3c24a3eac02c4c07a9558e11e9e244fb02bf62c85ac2106d1eb0c0b/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03d33c2ddbc1816237a67f66336616416e2bbb6beb306e5f890f2eb22b959cdf", size = 277150 }, + { url = "https://files.pythonhosted.org/packages/37/48/38c2db3f54d1501e692d6fe058f45b6ad1b358d82cd19436efab80cfc965/frozenlist-1.5.0-cp311-cp311-win32.whl", hash = "sha256:237f6b23ee0f44066219dae14c70ae38a63f0440ce6750f868ee08775073f942", size = 45244 }, + { url = "https://files.pythonhosted.org/packages/ca/8c/2ddffeb8b60a4bce3b196c32fcc30d8830d4615e7b492ec2071da801b8ad/frozenlist-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:0cc974cc93d32c42e7b0f6cf242a6bd941c57c61b618e78b6c0a96cb72788c1d", size = 51634 }, + { url = "https://files.pythonhosted.org/packages/79/73/fa6d1a96ab7fd6e6d1c3500700963eab46813847f01ef0ccbaa726181dd5/frozenlist-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21", size = 94026 }, + { url = "https://files.pythonhosted.org/packages/ab/04/ea8bf62c8868b8eada363f20ff1b647cf2e93377a7b284d36062d21d81d1/frozenlist-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d", size = 54150 }, + { url = "https://files.pythonhosted.org/packages/d0/9a/8e479b482a6f2070b26bda572c5e6889bb3ba48977e81beea35b5ae13ece/frozenlist-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e", size = 51927 }, + { url = "https://files.pythonhosted.org/packages/e3/12/2aad87deb08a4e7ccfb33600871bbe8f0e08cb6d8224371387f3303654d7/frozenlist-1.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a", size = 282647 }, + { url = "https://files.pythonhosted.org/packages/77/f2/07f06b05d8a427ea0060a9cef6e63405ea9e0d761846b95ef3fb3be57111/frozenlist-1.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a", size = 289052 }, + { url = "https://files.pythonhosted.org/packages/bd/9f/8bf45a2f1cd4aa401acd271b077989c9267ae8463e7c8b1eb0d3f561b65e/frozenlist-1.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee", size = 291719 }, + { url = "https://files.pythonhosted.org/packages/41/d1/1f20fd05a6c42d3868709b7604c9f15538a29e4f734c694c6bcfc3d3b935/frozenlist-1.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6", size = 267433 }, + { url = "https://files.pythonhosted.org/packages/af/f2/64b73a9bb86f5a89fb55450e97cd5c1f84a862d4ff90d9fd1a73ab0f64a5/frozenlist-1.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e", size = 283591 }, + { url = "https://files.pythonhosted.org/packages/29/e2/ffbb1fae55a791fd6c2938dd9ea779509c977435ba3940b9f2e8dc9d5316/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9", size = 273249 }, + { url = "https://files.pythonhosted.org/packages/2e/6e/008136a30798bb63618a114b9321b5971172a5abddff44a100c7edc5ad4f/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039", size = 271075 }, + { url = "https://files.pythonhosted.org/packages/ae/f0/4e71e54a026b06724cec9b6c54f0b13a4e9e298cc8db0f82ec70e151f5ce/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784", size = 285398 }, + { url = "https://files.pythonhosted.org/packages/4d/36/70ec246851478b1c0b59f11ef8ade9c482ff447c1363c2bd5fad45098b12/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631", size = 294445 }, + { url = "https://files.pythonhosted.org/packages/37/e0/47f87544055b3349b633a03c4d94b405956cf2437f4ab46d0928b74b7526/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f", size = 280569 }, + { url = "https://files.pythonhosted.org/packages/f9/7c/490133c160fb6b84ed374c266f42800e33b50c3bbab1652764e6e1fc498a/frozenlist-1.5.0-cp312-cp312-win32.whl", hash = "sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8", size = 44721 }, + { url = "https://files.pythonhosted.org/packages/b1/56/4e45136ffc6bdbfa68c29ca56ef53783ef4c2fd395f7cbf99a2624aa9aaa/frozenlist-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f", size = 51329 }, + { url = "https://files.pythonhosted.org/packages/da/3b/915f0bca8a7ea04483622e84a9bd90033bab54bdf485479556c74fd5eaf5/frozenlist-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953", size = 91538 }, + { url = "https://files.pythonhosted.org/packages/c7/d1/a7c98aad7e44afe5306a2b068434a5830f1470675f0e715abb86eb15f15b/frozenlist-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0", size = 52849 }, + { url = "https://files.pythonhosted.org/packages/3a/c8/76f23bf9ab15d5f760eb48701909645f686f9c64fbb8982674c241fbef14/frozenlist-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2", size = 50583 }, + { url = "https://files.pythonhosted.org/packages/1f/22/462a3dd093d11df623179d7754a3b3269de3b42de2808cddef50ee0f4f48/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f", size = 265636 }, + { url = "https://files.pythonhosted.org/packages/80/cf/e075e407fc2ae7328155a1cd7e22f932773c8073c1fc78016607d19cc3e5/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608", size = 270214 }, + { url = "https://files.pythonhosted.org/packages/a1/58/0642d061d5de779f39c50cbb00df49682832923f3d2ebfb0fedf02d05f7f/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b", size = 273905 }, + { url = "https://files.pythonhosted.org/packages/ab/66/3fe0f5f8f2add5b4ab7aa4e199f767fd3b55da26e3ca4ce2cc36698e50c4/frozenlist-1.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840", size = 250542 }, + { url = "https://files.pythonhosted.org/packages/f6/b8/260791bde9198c87a465224e0e2bb62c4e716f5d198fc3a1dacc4895dbd1/frozenlist-1.5.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439", size = 267026 }, + { url = "https://files.pythonhosted.org/packages/2e/a4/3d24f88c527f08f8d44ade24eaee83b2627793fa62fa07cbb7ff7a2f7d42/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de", size = 257690 }, + { url = "https://files.pythonhosted.org/packages/de/9a/d311d660420b2beeff3459b6626f2ab4fb236d07afbdac034a4371fe696e/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641", size = 253893 }, + { url = "https://files.pythonhosted.org/packages/c6/23/e491aadc25b56eabd0f18c53bb19f3cdc6de30b2129ee0bc39cd387cd560/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e", size = 267006 }, + { url = "https://files.pythonhosted.org/packages/08/c4/ab918ce636a35fb974d13d666dcbe03969592aeca6c3ab3835acff01f79c/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9", size = 276157 }, + { url = "https://files.pythonhosted.org/packages/c0/29/3b7a0bbbbe5a34833ba26f686aabfe982924adbdcafdc294a7a129c31688/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03", size = 264642 }, + { url = "https://files.pythonhosted.org/packages/ab/42/0595b3dbffc2e82d7fe658c12d5a5bafcd7516c6bf2d1d1feb5387caa9c1/frozenlist-1.5.0-cp313-cp313-win32.whl", hash = "sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c", size = 44914 }, + { url = "https://files.pythonhosted.org/packages/17/c4/b7db1206a3fea44bf3b838ca61deb6f74424a8a5db1dd53ecb21da669be6/frozenlist-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28", size = 51167 }, + { url = "https://files.pythonhosted.org/packages/c6/c8/a5be5b7550c10858fcf9b0ea054baccab474da77d37f1e828ce043a3a5d4/frozenlist-1.5.0-py3-none-any.whl", hash = "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3", size = 11901 }, +] + +[[package]] +name = "fsspec" +version = "2024.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a0/52/f16a068ebadae42526484c31f4398e62962504e5724a8ba5dc3409483df2/fsspec-2024.10.0.tar.gz", hash = "sha256:eda2d8a4116d4f2429db8550f2457da57279247dd930bb12f821b58391359493", size = 286853 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/b2/454d6e7f0158951d8a78c2e1eb4f69ae81beb8dca5fee9809c6c99e9d0d0/fsspec-2024.10.0-py3-none-any.whl", hash = "sha256:03b9a6785766a4de40368b88906366755e2819e758b83705c88cd7cb5fe81871", size = 179641 }, +] + +[[package]] +name = "fuzzywuzzy" +version = "0.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/4b/0a002eea91be6048a2b5d53c5f1b4dafd57ba2e36eea961d05086d7c28ce/fuzzywuzzy-0.18.0.tar.gz", hash = "sha256:45016e92264780e58972dca1b3d939ac864b78437422beecebb3095f8efd00e8", size = 28888 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ff/74f23998ad2f93b945c0309f825be92e04e0348e062026998b5eefef4c33/fuzzywuzzy-0.18.0-py2.py3-none-any.whl", hash = "sha256:928244b28db720d1e0ee7587acf660ea49d7e4c632569cad4f1cd7e68a5f0993", size = 18272 }, +] + +[[package]] +name = "gitdb" +version = "4.0.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "smmap" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/0d/bbb5b5ee188dec84647a4664f3e11b06ade2bde568dbd489d9d64adef8ed/gitdb-4.0.11.tar.gz", hash = "sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b", size = 394469 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/5b/8f0c4a5bb9fd491c277c21eff7ccae71b47d43c4446c9d0c6cff2fe8c2c4/gitdb-4.0.11-py3-none-any.whl", hash = "sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4", size = 62721 }, +] + +[[package]] +name = "gitpython" +version = "3.1.43" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitdb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/a1/106fd9fa2dd989b6fb36e5893961f82992cf676381707253e0bf93eb1662/GitPython-3.1.43.tar.gz", hash = "sha256:35f314a9f878467f5453cc1fee295c3e18e52f1b99f10f6cf5b1682e968a9e7c", size = 214149 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/bd/cc3a402a6439c15c3d4294333e13042b915bbeab54edc457c723931fed3f/GitPython-3.1.43-py3-none-any.whl", hash = "sha256:eec7ec56b92aad751f9912a73404bc02ba212a23adb2c7098ee668417051a1ff", size = 207337 }, +] + +[[package]] +name = "h11" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, +] + +[[package]] +name = "huggingface-hub" +version = "0.26.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/66/fa78b1cbcae512a30c2d4c702eba0e3a771ad7b304f85d5df0b339ad82f7/huggingface_hub-0.26.3.tar.gz", hash = "sha256:90e1fe62ffc26757a073aaad618422b899ccf9447c2bba8c902a90bef5b42e1d", size = 375690 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/9b/3068fb3ae0b498eb66960ca5f4d92a81c91458cacd4dc17bfa6d40ce90fb/huggingface_hub-0.26.3-py3-none-any.whl", hash = "sha256:e66aa99e569c2d5419240a9e553ad07245a5b1300350bfbc5a4945cf7432991b", size = 447570 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "jinja2" +version = "3.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 }, +] + +[[package]] +name = "jmespath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256 }, +] + +[[package]] +name = "levenshtein" +version = "0.26.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "rapidfuzz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/e6/79807d3b59a67dd78bb77072ca6a28d8db0935161fecf935e6c38c5f6825/levenshtein-0.26.1.tar.gz", hash = "sha256:0d19ba22330d50609b2349021ec3cf7d905c6fe21195a2d0d876a146e7ed2575", size = 374307 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/b4/86e447173ca8d936b7ef270d21952a0053e799040e73b843a4a5ac9a15a1/levenshtein-0.26.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:44c51f5d33b3cfb9db518b36f1288437a509edd82da94c4400f6a681758e0cb6", size = 177037 }, + { url = "https://files.pythonhosted.org/packages/27/b3/e15e14e5836dfc23ed014c21b307cbf77b3c6fd75e11d0675ce9a0d43b31/levenshtein-0.26.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56b93203e725f9df660e2afe3d26ba07d71871b6d6e05b8b767e688e23dfb076", size = 157478 }, + { url = "https://files.pythonhosted.org/packages/32/f1/f4d0904c5074e4e9d33dcaf304144e02eae9eec9d61b63bf17b1108ce228/levenshtein-0.26.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:270d36c5da04a0d89990660aea8542227cbd8f5bc34e9fdfadd34916ff904520", size = 153873 }, + { url = "https://files.pythonhosted.org/packages/f9/0d/cd5abe809421ce0d4a2cae60fd2fdf62cb43890068515a8a0069e2b17894/levenshtein-0.26.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:480674c05077eeb0b0f748546d4fcbb386d7c737f9fff0010400da3e8b552942", size = 186850 }, + { url = "https://files.pythonhosted.org/packages/a8/69/03f4266ad83781f2602b1976a2e5a98785c148f9bfc77c343e5aa1840f64/levenshtein-0.26.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13946e37323728695ba7a22f3345c2e907d23f4600bc700bf9b4352fb0c72a48", size = 187527 }, + { url = "https://files.pythonhosted.org/packages/36/fa/ec3be1162b1a757f80e713220470fe5b4db22e23f886f50ac59a48f0a84d/levenshtein-0.26.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ceb673f572d1d0dc9b1cd75792bb8bad2ae8eb78a7c6721e23a3867d318cb6f2", size = 162673 }, + { url = "https://files.pythonhosted.org/packages/9e/d6/dc8358b6a4174f413532aa27463dc4d167ac25742826f58916bb6e6417b1/levenshtein-0.26.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:42d6fa242e3b310ce6bfd5af0c83e65ef10b608b885b3bb69863c01fb2fcff98", size = 250413 }, + { url = "https://files.pythonhosted.org/packages/57/5e/a87bf39686482a1df000fdc265fdd812f0cd316d5fb0a25f52654504a82b/levenshtein-0.26.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b8b68295808893a81e0a1dbc2274c30dd90880f14d23078e8eb4325ee615fc68", size = 1078713 }, + { url = "https://files.pythonhosted.org/packages/c5/04/30ab2f27c4ff7d6d98b3bb6bf8541521535ad2d05e50ac8fd00ab701c080/levenshtein-0.26.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b01061d377d1944eb67bc40bef5d4d2f762c6ab01598efd9297ce5d0047eb1b5", size = 1331174 }, + { url = "https://files.pythonhosted.org/packages/e4/68/9c7f60ccb097a86420d058dcc3f575e6b3d663b3a5cde3651443f7087e14/levenshtein-0.26.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9d12c8390f156745e533d01b30773b9753e41d8bbf8bf9dac4b97628cdf16314", size = 1207733 }, + { url = "https://files.pythonhosted.org/packages/64/21/222f54a1a654eca1c1cd015d32d972d70529eb218d469d516f13eac2149d/levenshtein-0.26.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:48825c9f967f922061329d1481b70e9fee937fc68322d6979bc623f69f75bc91", size = 1356116 }, + { url = "https://files.pythonhosted.org/packages/6f/65/681dced2fa798ea7882bff5682ab566689a4920006ed9aca4fd8d1edb2d2/levenshtein-0.26.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d8ec137170b95736842f99c0e7a9fd8f5641d0c1b63b08ce027198545d983e2b", size = 1135459 }, + { url = "https://files.pythonhosted.org/packages/a1/e8/1ff8a634c428ed908d20482f77491cca08fa16c96738ad82d9219da138a1/levenshtein-0.26.1-cp311-cp311-win32.whl", hash = "sha256:798f2b525a2e90562f1ba9da21010dde0d73730e277acaa5c52d2a6364fd3e2a", size = 87265 }, + { url = "https://files.pythonhosted.org/packages/8f/fb/44e9747558a7381ea6736e10ac2f871414007915afb94efac423e68cf441/levenshtein-0.26.1-cp311-cp311-win_amd64.whl", hash = "sha256:55b1024516c59df55f1cf1a8651659a568f2c5929d863d3da1ce8893753153bd", size = 98518 }, + { url = "https://files.pythonhosted.org/packages/04/90/c476a74d8ec25d680b9cbf51966d638623a82a2fd4e99b988a383f22a681/levenshtein-0.26.1-cp311-cp311-win_arm64.whl", hash = "sha256:e52575cbc6b9764ea138a6f82d73d3b1bc685fe62e207ff46a963d4c773799f6", size = 88086 }, + { url = "https://files.pythonhosted.org/packages/4c/53/3685ee7fbe9b8eb4b82d8045255e59dd6943f94e8091697ef3808e7ecf63/levenshtein-0.26.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cc741ca406d3704dc331a69c04b061fc952509a069b79cab8287413f434684bd", size = 176447 }, + { url = "https://files.pythonhosted.org/packages/82/7f/7d6fe9b76bd030200f8f9b162f3de862d597804d292af292ec3ce9ae8bee/levenshtein-0.26.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:821ace3b4e1c2e02b43cf5dc61aac2ea43bdb39837ac890919c225a2c3f2fea4", size = 157589 }, + { url = "https://files.pythonhosted.org/packages/bc/d3/44539e952df93c5d88a95a0edff34af38e4f87330a76e8335bfe2c0f31bf/levenshtein-0.26.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92694c9396f55d4c91087efacf81297bef152893806fc54c289fc0254b45384", size = 153306 }, + { url = "https://files.pythonhosted.org/packages/ba/fe/21443c0c50824314e2d2ce7e1e9cd11d21b3643f3c14da156b15b4d399c7/levenshtein-0.26.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:51ba374de7a1797d04a14a4f0ad3602d2d71fef4206bb20a6baaa6b6a502da58", size = 184409 }, + { url = "https://files.pythonhosted.org/packages/f0/7b/c95066c64bb18628cf7488e0dd6aec2b7cbda307d93ba9ede68a21af2a7b/levenshtein-0.26.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f7aa5c3327dda4ef952769bacec09c09ff5bf426e07fdc94478c37955681885b", size = 193134 }, + { url = "https://files.pythonhosted.org/packages/36/22/5f9760b135bdefb8cf8d663890756136754db03214f929b73185dfa33f05/levenshtein-0.26.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33e2517e8d3c221de2d1183f400aed64211fcfc77077b291ed9f3bb64f141cdc", size = 162266 }, + { url = "https://files.pythonhosted.org/packages/11/50/6b1a5f3600caae40db0928f6775d7efc62c13dec2407d3d540bc4afdb72c/levenshtein-0.26.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9092b622765c7649dd1d8af0f43354723dd6f4e570ac079ffd90b41033957438", size = 246339 }, + { url = "https://files.pythonhosted.org/packages/26/eb/ede282fcb495570898b39a0d2f21bbc9be5587d604c93a518ece80f3e7dc/levenshtein-0.26.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:fc16796c85d7d8b259881d59cc8b5e22e940901928c2ff6924b2c967924e8a0b", size = 1077937 }, + { url = "https://files.pythonhosted.org/packages/35/41/eebe1c4a75f592d9bdc3c2595418f083bcad747e0aec52a1a9ffaae93f5c/levenshtein-0.26.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4370733967f5994ceeed8dc211089bedd45832ee688cecea17bfd35a9eb22b9", size = 1330607 }, + { url = "https://files.pythonhosted.org/packages/12/8e/4d34b1857adfd69c2a72d84bca1b8538d4cfaaf6fddd8599573f4281a9d1/levenshtein-0.26.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3535ecfd88c9b283976b5bc61265855f59bba361881e92ed2b5367b6990c93fe", size = 1197505 }, + { url = "https://files.pythonhosted.org/packages/c0/7b/6afcda1b0a0622cedaa4f7a5b3507c2384a7358fc051ccf619e5d2453bf2/levenshtein-0.26.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:90236e93d98bdfd708883a6767826fafd976dac8af8fc4a0fb423d4fa08e1bf0", size = 1352832 }, + { url = "https://files.pythonhosted.org/packages/21/5e/0ed4e7b5c820b6bc40e2c391633292c3666400339042a3d306f0dc8fdcb4/levenshtein-0.26.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:04b7cabb82edf566b1579b3ed60aac0eec116655af75a3c551fee8754ffce2ea", size = 1135970 }, + { url = "https://files.pythonhosted.org/packages/c9/91/3ff1abacb58642749dfd130ad855370e01b9c7aeaa73801964361f6e355f/levenshtein-0.26.1-cp312-cp312-win32.whl", hash = "sha256:ae382af8c76f6d2a040c0d9ca978baf461702ceb3f79a0a3f6da8d596a484c5b", size = 87599 }, + { url = "https://files.pythonhosted.org/packages/7d/f9/727f3ba7843a3fb2a0f3db825358beea2a52bc96258874ee80cb2e5ecabb/levenshtein-0.26.1-cp312-cp312-win_amd64.whl", hash = "sha256:fd091209798cfdce53746f5769987b4108fe941c54fb2e058c016ffc47872918", size = 98809 }, + { url = "https://files.pythonhosted.org/packages/d4/f4/f87f19222d279dbac429b9bc7ccae271d900fd9c48a581b8bc180ba6cd09/levenshtein-0.26.1-cp312-cp312-win_arm64.whl", hash = "sha256:7e82f2ea44a81ad6b30d92a110e04cd3c8c7c6034b629aca30a3067fa174ae89", size = 88227 }, + { url = "https://files.pythonhosted.org/packages/7e/d6/b4b522b94d7b387c023d22944590befc0ac6b766ac6d197afd879ddd77fc/levenshtein-0.26.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:790374a9f5d2cbdb30ee780403a62e59bef51453ac020668c1564d1e43438f0e", size = 175836 }, + { url = "https://files.pythonhosted.org/packages/25/76/06d1e26a8e6d0de68ef4a157dd57f6b342413c03550309e4aa095a453b28/levenshtein-0.26.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7b05c0415c386d00efda83d48db9db68edd02878d6dbc6df01194f12062be1bb", size = 157036 }, + { url = "https://files.pythonhosted.org/packages/7e/23/21209a9e96b878aede3bea104533866762ba621e36fc344aa080db5feb02/levenshtein-0.26.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3114586032361722ddededf28401ce5baf1cf617f9f49fb86b8766a45a423ff", size = 153326 }, + { url = "https://files.pythonhosted.org/packages/06/38/9fc68685fffd8863b13864552eba8f3eb6a82a4dc558bf2c6553c2347d6c/levenshtein-0.26.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2532f8a13b68bf09f152d906f118a88da2063da22f44c90e904b142b0a53d534", size = 183693 }, + { url = "https://files.pythonhosted.org/packages/f6/82/ccd7bdd7d431329da025e649c63b731df44f8cf31b957e269ae1c1dc9a8e/levenshtein-0.26.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:219c30be6aa734bf927188d1208b7d78d202a3eb017b1c5f01ab2034d2d4ccca", size = 190581 }, + { url = "https://files.pythonhosted.org/packages/6e/c5/57f90b4aea1f89f853872b27a5a5dbce37b89ffeae42c02060b3e82038b2/levenshtein-0.26.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:397e245e77f87836308bd56305bba630010cd8298c34c4c44bd94990cdb3b7b1", size = 162446 }, + { url = "https://files.pythonhosted.org/packages/fc/da/df6acca738921f896ce2d178821be866b43a583f85e2d1de63a4f8f78080/levenshtein-0.26.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeff6ea3576f72e26901544c6c55c72a7b79b9983b6f913cba0e9edbf2f87a97", size = 247123 }, + { url = "https://files.pythonhosted.org/packages/22/fb/f44a4c0d7784ccd32e4166714fea61e50f62b232162ae16332f45cb55ab2/levenshtein-0.26.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a19862e3539a697df722a08793994e334cd12791e8144851e8a1dee95a17ff63", size = 1077437 }, + { url = "https://files.pythonhosted.org/packages/f0/5e/d9b9e7daa13cc7e2184a3c2422bb847f05d354ce15ba113b20d83e9ab366/levenshtein-0.26.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:dc3b5a64f57c3c078d58b1e447f7d68cad7ae1b23abe689215d03fc434f8f176", size = 1330362 }, + { url = "https://files.pythonhosted.org/packages/bf/67/480d85bb516798014a6849be0225b246f35df4b54499c348c9c9e311f936/levenshtein-0.26.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bb6c7347424a91317c5e1b68041677e4c8ed3e7823b5bbaedb95bffb3c3497ea", size = 1198721 }, + { url = "https://files.pythonhosted.org/packages/9a/7d/889ff7d86903b6545665655627113d263c88c6d596c68fb09a640ee4f0a7/levenshtein-0.26.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b817376de4195a207cc0e4ca37754c0e1e1078c2a2d35a6ae502afde87212f9e", size = 1351820 }, + { url = "https://files.pythonhosted.org/packages/b9/29/cd42273150f08c200ed2d1879486d73502ee35265f162a77952f101d93a0/levenshtein-0.26.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7b50c3620ff47c9887debbb4c154aaaac3e46be7fc2e5789ee8dbe128bce6a17", size = 1135747 }, + { url = "https://files.pythonhosted.org/packages/1d/90/cbcfa3dd86023e82036662a19fec2fcb48782d3f9fa322d44dc898d95a5d/levenshtein-0.26.1-cp313-cp313-win32.whl", hash = "sha256:9fb859da90262eb474c190b3ca1e61dee83add022c676520f5c05fdd60df902a", size = 87318 }, + { url = "https://files.pythonhosted.org/packages/83/73/372edebc79fd09a8b2382cf1244d279ada5b795124f1e1c4fc73d9fbb00f/levenshtein-0.26.1-cp313-cp313-win_amd64.whl", hash = "sha256:8adcc90e3a5bfb0a463581d85e599d950fe3c2938ac6247b29388b64997f6e2d", size = 98418 }, + { url = "https://files.pythonhosted.org/packages/b2/6d/f0160ea5a7bb7a62b3b3d56e9fc5024b440cb59555a90be2347abf2e7888/levenshtein-0.26.1-cp313-cp313-win_arm64.whl", hash = "sha256:c2599407e029865dc66d210b8804c7768cbdbf60f061d993bb488d5242b0b73e", size = 87792 }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, +] + +[[package]] +name = "more-itertools" +version = "10.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/78/65922308c4248e0eb08ebcbe67c95d48615cc6f27854b6f2e57143e9178f/more-itertools-10.5.0.tar.gz", hash = "sha256:5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6", size = 121020 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/7e/3a64597054a70f7c86eb0a7d4fc315b8c1ab932f64883a297bdffeb5f967/more_itertools-10.5.0-py3-none-any.whl", hash = "sha256:037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef", size = 60952 }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198 }, +] + +[[package]] +name = "msgpack" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/d0/7555686ae7ff5731205df1012ede15dd9d927f6227ea151e901c7406af4f/msgpack-1.1.0.tar.gz", hash = "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e", size = 167260 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/5e/a4c7154ba65d93be91f2f1e55f90e76c5f91ccadc7efc4341e6f04c8647f/msgpack-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d364a55082fb2a7416f6c63ae383fbd903adb5a6cf78c5b96cc6316dc1cedc7", size = 150803 }, + { url = "https://files.pythonhosted.org/packages/60/c2/687684164698f1d51c41778c838d854965dd284a4b9d3a44beba9265c931/msgpack-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79ec007767b9b56860e0372085f8504db5d06bd6a327a335449508bbee9648fa", size = 84343 }, + { url = "https://files.pythonhosted.org/packages/42/ae/d3adea9bb4a1342763556078b5765e666f8fdf242e00f3f6657380920972/msgpack-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6ad622bf7756d5a497d5b6836e7fc3752e2dd6f4c648e24b1803f6048596f701", size = 81408 }, + { url = "https://files.pythonhosted.org/packages/dc/17/6313325a6ff40ce9c3207293aee3ba50104aed6c2c1559d20d09e5c1ff54/msgpack-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e59bca908d9ca0de3dc8684f21ebf9a690fe47b6be93236eb40b99af28b6ea6", size = 396096 }, + { url = "https://files.pythonhosted.org/packages/a8/a1/ad7b84b91ab5a324e707f4c9761633e357820b011a01e34ce658c1dda7cc/msgpack-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1da8f11a3dd397f0a32c76165cf0c4eb95b31013a94f6ecc0b280c05c91b59", size = 403671 }, + { url = "https://files.pythonhosted.org/packages/bb/0b/fd5b7c0b308bbf1831df0ca04ec76fe2f5bf6319833646b0a4bd5e9dc76d/msgpack-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:452aff037287acb1d70a804ffd022b21fa2bb7c46bee884dbc864cc9024128a0", size = 387414 }, + { url = "https://files.pythonhosted.org/packages/f0/03/ff8233b7c6e9929a1f5da3c7860eccd847e2523ca2de0d8ef4878d354cfa/msgpack-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8da4bf6d54ceed70e8861f833f83ce0814a2b72102e890cbdfe4b34764cdd66e", size = 383759 }, + { url = "https://files.pythonhosted.org/packages/1f/1b/eb82e1fed5a16dddd9bc75f0854b6e2fe86c0259c4353666d7fab37d39f4/msgpack-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:41c991beebf175faf352fb940bf2af9ad1fb77fd25f38d9142053914947cdbf6", size = 394405 }, + { url = "https://files.pythonhosted.org/packages/90/2e/962c6004e373d54ecf33d695fb1402f99b51832631e37c49273cc564ffc5/msgpack-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a52a1f3a5af7ba1c9ace055b659189f6c669cf3657095b50f9602af3a3ba0fe5", size = 396041 }, + { url = "https://files.pythonhosted.org/packages/f8/20/6e03342f629474414860c48aeffcc2f7f50ddaf351d95f20c3f1c67399a8/msgpack-1.1.0-cp311-cp311-win32.whl", hash = "sha256:58638690ebd0a06427c5fe1a227bb6b8b9fdc2bd07701bec13c2335c82131a88", size = 68538 }, + { url = "https://files.pythonhosted.org/packages/aa/c4/5a582fc9a87991a3e6f6800e9bb2f3c82972912235eb9539954f3e9997c7/msgpack-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd2906780f25c8ed5d7b323379f6138524ba793428db5d0e9d226d3fa6aa1788", size = 74871 }, + { url = "https://files.pythonhosted.org/packages/e1/d6/716b7ca1dbde63290d2973d22bbef1b5032ca634c3ff4384a958ec3f093a/msgpack-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d46cf9e3705ea9485687aa4001a76e44748b609d260af21c4ceea7f2212a501d", size = 152421 }, + { url = "https://files.pythonhosted.org/packages/70/da/5312b067f6773429cec2f8f08b021c06af416bba340c912c2ec778539ed6/msgpack-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5dbad74103df937e1325cc4bfeaf57713be0b4f15e1c2da43ccdd836393e2ea2", size = 85277 }, + { url = "https://files.pythonhosted.org/packages/28/51/da7f3ae4462e8bb98af0d5bdf2707f1b8c65a0d4f496e46b6afb06cbc286/msgpack-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58dfc47f8b102da61e8949708b3eafc3504509a5728f8b4ddef84bd9e16ad420", size = 82222 }, + { url = "https://files.pythonhosted.org/packages/33/af/dc95c4b2a49cff17ce47611ca9ba218198806cad7796c0b01d1e332c86bb/msgpack-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676e5be1b472909b2ee6356ff425ebedf5142427842aa06b4dfd5117d1ca8a2", size = 392971 }, + { url = "https://files.pythonhosted.org/packages/f1/54/65af8de681fa8255402c80eda2a501ba467921d5a7a028c9c22a2c2eedb5/msgpack-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17fb65dd0bec285907f68b15734a993ad3fc94332b5bb21b0435846228de1f39", size = 401403 }, + { url = "https://files.pythonhosted.org/packages/97/8c/e333690777bd33919ab7024269dc3c41c76ef5137b211d776fbb404bfead/msgpack-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a51abd48c6d8ac89e0cfd4fe177c61481aca2d5e7ba42044fd218cfd8ea9899f", size = 385356 }, + { url = "https://files.pythonhosted.org/packages/57/52/406795ba478dc1c890559dd4e89280fa86506608a28ccf3a72fbf45df9f5/msgpack-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2137773500afa5494a61b1208619e3871f75f27b03bcfca7b3a7023284140247", size = 383028 }, + { url = "https://files.pythonhosted.org/packages/e7/69/053b6549bf90a3acadcd8232eae03e2fefc87f066a5b9fbb37e2e608859f/msgpack-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:398b713459fea610861c8a7b62a6fec1882759f308ae0795b5413ff6a160cf3c", size = 391100 }, + { url = "https://files.pythonhosted.org/packages/23/f0/d4101d4da054f04274995ddc4086c2715d9b93111eb9ed49686c0f7ccc8a/msgpack-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06f5fd2f6bb2a7914922d935d3b8bb4a7fff3a9a91cfce6d06c13bc42bec975b", size = 394254 }, + { url = "https://files.pythonhosted.org/packages/1c/12/cf07458f35d0d775ff3a2dc5559fa2e1fcd06c46f1ef510e594ebefdca01/msgpack-1.1.0-cp312-cp312-win32.whl", hash = "sha256:ad33e8400e4ec17ba782f7b9cf868977d867ed784a1f5f2ab46e7ba53b6e1e1b", size = 69085 }, + { url = "https://files.pythonhosted.org/packages/73/80/2708a4641f7d553a63bc934a3eb7214806b5b39d200133ca7f7afb0a53e8/msgpack-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:115a7af8ee9e8cddc10f87636767857e7e3717b7a2e97379dc2054712693e90f", size = 75347 }, + { url = "https://files.pythonhosted.org/packages/c8/b0/380f5f639543a4ac413e969109978feb1f3c66e931068f91ab6ab0f8be00/msgpack-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:071603e2f0771c45ad9bc65719291c568d4edf120b44eb36324dcb02a13bfddf", size = 151142 }, + { url = "https://files.pythonhosted.org/packages/c8/ee/be57e9702400a6cb2606883d55b05784fada898dfc7fd12608ab1fdb054e/msgpack-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0f92a83b84e7c0749e3f12821949d79485971f087604178026085f60ce109330", size = 84523 }, + { url = "https://files.pythonhosted.org/packages/7e/3a/2919f63acca3c119565449681ad08a2f84b2171ddfcff1dba6959db2cceb/msgpack-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1964df7b81285d00a84da4e70cb1383f2e665e0f1f2a7027e683956d04b734", size = 81556 }, + { url = "https://files.pythonhosted.org/packages/7c/43/a11113d9e5c1498c145a8925768ea2d5fce7cbab15c99cda655aa09947ed/msgpack-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59caf6a4ed0d164055ccff8fe31eddc0ebc07cf7326a2aaa0dbf7a4001cd823e", size = 392105 }, + { url = "https://files.pythonhosted.org/packages/2d/7b/2c1d74ca6c94f70a1add74a8393a0138172207dc5de6fc6269483519d048/msgpack-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0907e1a7119b337971a689153665764adc34e89175f9a34793307d9def08e6ca", size = 399979 }, + { url = "https://files.pythonhosted.org/packages/82/8c/cf64ae518c7b8efc763ca1f1348a96f0e37150061e777a8ea5430b413a74/msgpack-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65553c9b6da8166e819a6aa90ad15288599b340f91d18f60b2061f402b9a4915", size = 383816 }, + { url = "https://files.pythonhosted.org/packages/69/86/a847ef7a0f5ef3fa94ae20f52a4cacf596a4e4a010197fbcc27744eb9a83/msgpack-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7a946a8992941fea80ed4beae6bff74ffd7ee129a90b4dd5cf9c476a30e9708d", size = 380973 }, + { url = "https://files.pythonhosted.org/packages/aa/90/c74cf6e1126faa93185d3b830ee97246ecc4fe12cf9d2d31318ee4246994/msgpack-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4b51405e36e075193bc051315dbf29168d6141ae2500ba8cd80a522964e31434", size = 387435 }, + { url = "https://files.pythonhosted.org/packages/7a/40/631c238f1f338eb09f4acb0f34ab5862c4e9d7eda11c1b685471a4c5ea37/msgpack-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4c01941fd2ff87c2a934ee6055bda4ed353a7846b8d4f341c428109e9fcde8c", size = 399082 }, + { url = "https://files.pythonhosted.org/packages/e9/1b/fa8a952be252a1555ed39f97c06778e3aeb9123aa4cccc0fd2acd0b4e315/msgpack-1.1.0-cp313-cp313-win32.whl", hash = "sha256:7c9a35ce2c2573bada929e0b7b3576de647b0defbd25f5139dcdaba0ae35a4cc", size = 69037 }, + { url = "https://files.pythonhosted.org/packages/b6/bc/8bd826dd03e022153bfa1766dcdec4976d6c818865ed54223d71f07862b3/msgpack-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:bce7d9e614a04d0883af0b3d4d501171fbfca038f12c77fa838d9f198147a23f", size = 75140 }, +] + +[[package]] +name = "msgpack-numpy-opentensor" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "msgpack" }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/69/2a6af13c3be6934a9ba149120a78bf63cf1455ddb1d11ec2cc5e5d6f8186/msgpack-numpy-opentensor-0.5.0.tar.gz", hash = "sha256:213232c20e2efd528ec8a9882b605e8ad87cfc35b57dfcfefe05d33aaaabe574", size = 9661 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/22/590508afb85d5c27ebcb2837410413f4613eebdda6e4e02997fe08ba78e4/msgpack_numpy_opentensor-0.5.0-py2.py3-none-any.whl", hash = "sha256:8a61c597a976425a87094d8e89846aa9528eb1f037e97ff1428fe3cd61a238e7", size = 7209 }, +] + +[[package]] +name = "multidict" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/be/504b89a5e9ca731cd47487e91c469064f8ae5af93b7259758dcfc2b9c848/multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a", size = 64002 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/13/df3505a46d0cd08428e4c8169a196131d1b0c4b515c3649829258843dde6/multidict-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6", size = 48570 }, + { url = "https://files.pythonhosted.org/packages/f0/e1/a215908bfae1343cdb72f805366592bdd60487b4232d039c437fe8f5013d/multidict-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156", size = 29316 }, + { url = "https://files.pythonhosted.org/packages/70/0f/6dc70ddf5d442702ed74f298d69977f904960b82368532c88e854b79f72b/multidict-6.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb", size = 29640 }, + { url = "https://files.pythonhosted.org/packages/d8/6d/9c87b73a13d1cdea30b321ef4b3824449866bd7f7127eceed066ccb9b9ff/multidict-6.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b", size = 131067 }, + { url = "https://files.pythonhosted.org/packages/cc/1e/1b34154fef373371fd6c65125b3d42ff5f56c7ccc6bfff91b9b3c60ae9e0/multidict-6.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72", size = 138507 }, + { url = "https://files.pythonhosted.org/packages/fb/e0/0bc6b2bac6e461822b5f575eae85da6aae76d0e2a79b6665d6206b8e2e48/multidict-6.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304", size = 133905 }, + { url = "https://files.pythonhosted.org/packages/ba/af/73d13b918071ff9b2205fcf773d316e0f8fefb4ec65354bbcf0b10908cc6/multidict-6.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351", size = 129004 }, + { url = "https://files.pythonhosted.org/packages/74/21/23960627b00ed39643302d81bcda44c9444ebcdc04ee5bedd0757513f259/multidict-6.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb", size = 121308 }, + { url = "https://files.pythonhosted.org/packages/8b/5c/cf282263ffce4a596ed0bb2aa1a1dddfe1996d6a62d08842a8d4b33dca13/multidict-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3", size = 132608 }, + { url = "https://files.pythonhosted.org/packages/d7/3e/97e778c041c72063f42b290888daff008d3ab1427f5b09b714f5a8eff294/multidict-6.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399", size = 127029 }, + { url = "https://files.pythonhosted.org/packages/47/ac/3efb7bfe2f3aefcf8d103e9a7162572f01936155ab2f7ebcc7c255a23212/multidict-6.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423", size = 137594 }, + { url = "https://files.pythonhosted.org/packages/42/9b/6c6e9e8dc4f915fc90a9b7798c44a30773dea2995fdcb619870e705afe2b/multidict-6.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3", size = 134556 }, + { url = "https://files.pythonhosted.org/packages/1d/10/8e881743b26aaf718379a14ac58572a240e8293a1c9d68e1418fb11c0f90/multidict-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753", size = 130993 }, + { url = "https://files.pythonhosted.org/packages/45/84/3eb91b4b557442802d058a7579e864b329968c8d0ea57d907e7023c677f2/multidict-6.1.0-cp311-cp311-win32.whl", hash = "sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80", size = 26405 }, + { url = "https://files.pythonhosted.org/packages/9f/0b/ad879847ecbf6d27e90a6eabb7eff6b62c129eefe617ea45eae7c1f0aead/multidict-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926", size = 28795 }, + { url = "https://files.pythonhosted.org/packages/fd/16/92057c74ba3b96d5e211b553895cd6dc7cc4d1e43d9ab8fafc727681ef71/multidict-6.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa", size = 48713 }, + { url = "https://files.pythonhosted.org/packages/94/3d/37d1b8893ae79716179540b89fc6a0ee56b4a65fcc0d63535c6f5d96f217/multidict-6.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436", size = 29516 }, + { url = "https://files.pythonhosted.org/packages/a2/12/adb6b3200c363062f805275b4c1e656be2b3681aada66c80129932ff0bae/multidict-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761", size = 29557 }, + { url = "https://files.pythonhosted.org/packages/47/e9/604bb05e6e5bce1e6a5cf80a474e0f072e80d8ac105f1b994a53e0b28c42/multidict-6.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e", size = 130170 }, + { url = "https://files.pythonhosted.org/packages/7e/13/9efa50801785eccbf7086b3c83b71a4fb501a4d43549c2f2f80b8787d69f/multidict-6.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef", size = 134836 }, + { url = "https://files.pythonhosted.org/packages/bf/0f/93808b765192780d117814a6dfcc2e75de6dcc610009ad408b8814dca3ba/multidict-6.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95", size = 133475 }, + { url = "https://files.pythonhosted.org/packages/d3/c8/529101d7176fe7dfe1d99604e48d69c5dfdcadb4f06561f465c8ef12b4df/multidict-6.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925", size = 131049 }, + { url = "https://files.pythonhosted.org/packages/ca/0c/fc85b439014d5a58063e19c3a158a889deec399d47b5269a0f3b6a2e28bc/multidict-6.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966", size = 120370 }, + { url = "https://files.pythonhosted.org/packages/db/46/d4416eb20176492d2258fbd47b4abe729ff3b6e9c829ea4236f93c865089/multidict-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305", size = 125178 }, + { url = "https://files.pythonhosted.org/packages/5b/46/73697ad7ec521df7de5531a32780bbfd908ded0643cbe457f981a701457c/multidict-6.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2", size = 119567 }, + { url = "https://files.pythonhosted.org/packages/cd/ed/51f060e2cb0e7635329fa6ff930aa5cffa17f4c7f5c6c3ddc3500708e2f2/multidict-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2", size = 129822 }, + { url = "https://files.pythonhosted.org/packages/df/9e/ee7d1954b1331da3eddea0c4e08d9142da5f14b1321c7301f5014f49d492/multidict-6.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6", size = 128656 }, + { url = "https://files.pythonhosted.org/packages/77/00/8538f11e3356b5d95fa4b024aa566cde7a38aa7a5f08f4912b32a037c5dc/multidict-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3", size = 125360 }, + { url = "https://files.pythonhosted.org/packages/be/05/5d334c1f2462d43fec2363cd00b1c44c93a78c3925d952e9a71caf662e96/multidict-6.1.0-cp312-cp312-win32.whl", hash = "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133", size = 26382 }, + { url = "https://files.pythonhosted.org/packages/a3/bf/f332a13486b1ed0496d624bcc7e8357bb8053823e8cd4b9a18edc1d97e73/multidict-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1", size = 28529 }, + { url = "https://files.pythonhosted.org/packages/22/67/1c7c0f39fe069aa4e5d794f323be24bf4d33d62d2a348acdb7991f8f30db/multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008", size = 48771 }, + { url = "https://files.pythonhosted.org/packages/3c/25/c186ee7b212bdf0df2519eacfb1981a017bda34392c67542c274651daf23/multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f", size = 29533 }, + { url = "https://files.pythonhosted.org/packages/67/5e/04575fd837e0958e324ca035b339cea174554f6f641d3fb2b4f2e7ff44a2/multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28", size = 29595 }, + { url = "https://files.pythonhosted.org/packages/d3/b2/e56388f86663810c07cfe4a3c3d87227f3811eeb2d08450b9e5d19d78876/multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b", size = 130094 }, + { url = "https://files.pythonhosted.org/packages/6c/ee/30ae9b4186a644d284543d55d491fbd4239b015d36b23fea43b4c94f7052/multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c", size = 134876 }, + { url = "https://files.pythonhosted.org/packages/84/c7/70461c13ba8ce3c779503c70ec9d0345ae84de04521c1f45a04d5f48943d/multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3", size = 133500 }, + { url = "https://files.pythonhosted.org/packages/4a/9f/002af221253f10f99959561123fae676148dd730e2daa2cd053846a58507/multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44", size = 131099 }, + { url = "https://files.pythonhosted.org/packages/82/42/d1c7a7301d52af79d88548a97e297f9d99c961ad76bbe6f67442bb77f097/multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2", size = 120403 }, + { url = "https://files.pythonhosted.org/packages/68/f3/471985c2c7ac707547553e8f37cff5158030d36bdec4414cb825fbaa5327/multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3", size = 125348 }, + { url = "https://files.pythonhosted.org/packages/67/2c/e6df05c77e0e433c214ec1d21ddd203d9a4770a1f2866a8ca40a545869a0/multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa", size = 119673 }, + { url = "https://files.pythonhosted.org/packages/c5/cd/bc8608fff06239c9fb333f9db7743a1b2eafe98c2666c9a196e867a3a0a4/multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa", size = 129927 }, + { url = "https://files.pythonhosted.org/packages/44/8e/281b69b7bc84fc963a44dc6e0bbcc7150e517b91df368a27834299a526ac/multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4", size = 128711 }, + { url = "https://files.pythonhosted.org/packages/12/a4/63e7cd38ed29dd9f1881d5119f272c898ca92536cdb53ffe0843197f6c85/multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6", size = 125519 }, + { url = "https://files.pythonhosted.org/packages/38/e0/4f5855037a72cd8a7a2f60a3952d9aa45feedb37ae7831642102604e8a37/multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81", size = 26426 }, + { url = "https://files.pythonhosted.org/packages/7e/a5/17ee3a4db1e310b7405f5d25834460073a8ccd86198ce044dfaf69eac073/multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774", size = 28531 }, + { url = "https://files.pythonhosted.org/packages/99/b7/b9e70fde2c0f0c9af4cc5277782a89b66d35948ea3369ec9f598358c3ac5/multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506", size = 10051 }, +] + +[[package]] +name = "munch" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/a1/ec48010724eedfe2add68eb7592a0d238590e14e08b95a4ffb3c7b2f0808/munch-2.5.0.tar.gz", hash = "sha256:2d735f6f24d4dba3417fa448cae40c6e896ec1fdab6cdb5e6510999758a4dbd2", size = 17015 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/ab/85d8da5c9a45e072301beb37ad7f833cd344e04c817d97e0cc75681d248f/munch-2.5.0-py2.py3-none-any.whl", hash = "sha256:6f44af89a2ce4ed04ff8de41f70b226b984db10a91dcc7b9ac2efc1c77022fdd", size = 10347 }, +] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195 }, +] + +[[package]] +name = "netaddr" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/90/188b2a69654f27b221fba92fda7217778208532c962509e959a9cee5229d/netaddr-1.3.0.tar.gz", hash = "sha256:5c3c3d9895b551b763779ba7db7a03487dc1f8e3b385af819af341ae9ef6e48a", size = 2260504 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/cc/f4fe2c7ce68b92cbf5b2d379ca366e1edae38cccaad00f69f529b460c3ef/netaddr-1.3.0-py3-none-any.whl", hash = "sha256:c2c6a8ebe5554ce33b7d5b3a306b71bbb373e000bbbf2350dd5213cc56e3dbbe", size = 2262023 }, +] + +[[package]] +name = "networkx" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263 }, +] + +[[package]] +name = "numpy" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/75/10dd1f8116a8b796cb2c737b674e02d02e80454bda953fa7e65d8c12b016/numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78", size = 18902015 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/cf/034500fb83041aa0286e0fb16e7c76e5c8b67c0711bb6e9e9737a717d5fe/numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448", size = 21169137 }, + { url = "https://files.pythonhosted.org/packages/4a/d9/32de45561811a4b87fbdee23b5797394e3d1504b4a7cf40c10199848893e/numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195", size = 13703552 }, + { url = "https://files.pythonhosted.org/packages/c1/ca/2f384720020c7b244d22508cb7ab23d95f179fcfff33c31a6eeba8d6c512/numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57", size = 5298957 }, + { url = "https://files.pythonhosted.org/packages/0e/78/a3e4f9fb6aa4e6fdca0c5428e8ba039408514388cf62d89651aade838269/numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a", size = 6905573 }, + { url = "https://files.pythonhosted.org/packages/a0/72/cfc3a1beb2caf4efc9d0b38a15fe34025230da27e1c08cc2eb9bfb1c7231/numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669", size = 13914330 }, + { url = "https://files.pythonhosted.org/packages/ba/a8/c17acf65a931ce551fee11b72e8de63bf7e8a6f0e21add4c937c83563538/numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951", size = 19534895 }, + { url = "https://files.pythonhosted.org/packages/ba/86/8767f3d54f6ae0165749f84648da9dcc8cd78ab65d415494962c86fac80f/numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9", size = 19937253 }, + { url = "https://files.pythonhosted.org/packages/df/87/f76450e6e1c14e5bb1eae6836478b1028e096fd02e85c1c37674606ab752/numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15", size = 14414074 }, + { url = "https://files.pythonhosted.org/packages/5c/ca/0f0f328e1e59f73754f06e1adfb909de43726d4f24c6a3f8805f34f2b0fa/numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4", size = 6470640 }, + { url = "https://files.pythonhosted.org/packages/eb/57/3a3f14d3a759dcf9bf6e9eda905794726b758819df4663f217d658a58695/numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc", size = 15910230 }, + { url = "https://files.pythonhosted.org/packages/45/40/2e117be60ec50d98fa08c2f8c48e09b3edea93cfcabd5a9ff6925d54b1c2/numpy-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b", size = 20895803 }, + { url = "https://files.pythonhosted.org/packages/46/92/1b8b8dee833f53cef3e0a3f69b2374467789e0bb7399689582314df02651/numpy-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e", size = 13471835 }, + { url = "https://files.pythonhosted.org/packages/7f/19/e2793bde475f1edaea6945be141aef6c8b4c669b90c90a300a8954d08f0a/numpy-2.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c", size = 5038499 }, + { url = "https://files.pythonhosted.org/packages/e3/ff/ddf6dac2ff0dd50a7327bcdba45cb0264d0e96bb44d33324853f781a8f3c/numpy-2.0.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c", size = 6633497 }, + { url = "https://files.pythonhosted.org/packages/72/21/67f36eac8e2d2cd652a2e69595a54128297cdcb1ff3931cfc87838874bd4/numpy-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692", size = 13621158 }, + { url = "https://files.pythonhosted.org/packages/39/68/e9f1126d757653496dbc096cb429014347a36b228f5a991dae2c6b6cfd40/numpy-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a", size = 19236173 }, + { url = "https://files.pythonhosted.org/packages/d1/e9/1f5333281e4ebf483ba1c888b1d61ba7e78d7e910fdd8e6499667041cc35/numpy-2.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c", size = 19634174 }, + { url = "https://files.pythonhosted.org/packages/71/af/a469674070c8d8408384e3012e064299f7a2de540738a8e414dcfd639996/numpy-2.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded", size = 14099701 }, + { url = "https://files.pythonhosted.org/packages/d0/3d/08ea9f239d0e0e939b6ca52ad403c84a2bce1bde301a8eb4888c1c1543f1/numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5", size = 6174313 }, + { url = "https://files.pythonhosted.org/packages/b2/b5/4ac39baebf1fdb2e72585c8352c56d063b6126be9fc95bd2bb5ef5770c20/numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a", size = 15606179 }, +] + +[[package]] +name = "nvidia-cublas-cu12" +version = "12.4.5.8" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/7f/7fbae15a3982dc9595e49ce0f19332423b260045d0a6afe93cdbe2f1f624/nvidia_cublas_cu12-12.4.5.8-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0f8aa1706812e00b9f19dfe0cdb3999b092ccb8ca168c0db5b8ea712456fd9b3", size = 363333771 }, + { url = "https://files.pythonhosted.org/packages/ae/71/1c91302526c45ab494c23f61c7a84aa568b8c1f9d196efa5993957faf906/nvidia_cublas_cu12-12.4.5.8-py3-none-manylinux2014_x86_64.whl", hash = "sha256:2fc8da60df463fdefa81e323eef2e36489e1c94335b5358bcb38360adf75ac9b", size = 363438805 }, +] + +[[package]] +name = "nvidia-cuda-cupti-cu12" +version = "12.4.127" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/b5/9fb3d00386d3361b03874246190dfec7b206fd74e6e287b26a8fcb359d95/nvidia_cuda_cupti_cu12-12.4.127-py3-none-manylinux2014_aarch64.whl", hash = "sha256:79279b35cf6f91da114182a5ce1864997fd52294a87a16179ce275773799458a", size = 12354556 }, + { url = "https://files.pythonhosted.org/packages/67/42/f4f60238e8194a3106d06a058d494b18e006c10bb2b915655bd9f6ea4cb1/nvidia_cuda_cupti_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:9dec60f5ac126f7bb551c055072b69d85392b13311fcc1bcda2202d172df30fb", size = 13813957 }, +] + +[[package]] +name = "nvidia-cuda-nvrtc-cu12" +version = "12.4.127" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/aa/083b01c427e963ad0b314040565ea396f914349914c298556484f799e61b/nvidia_cuda_nvrtc_cu12-12.4.127-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0eedf14185e04b76aa05b1fea04133e59f465b6f960c0cbf4e37c3cb6b0ea198", size = 24133372 }, + { url = "https://files.pythonhosted.org/packages/2c/14/91ae57cd4db3f9ef7aa99f4019cfa8d54cb4caa7e00975df6467e9725a9f/nvidia_cuda_nvrtc_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a178759ebb095827bd30ef56598ec182b85547f1508941a3d560eb7ea1fbf338", size = 24640306 }, +] + +[[package]] +name = "nvidia-cuda-runtime-cu12" +version = "12.4.127" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/aa/b656d755f474e2084971e9a297def515938d56b466ab39624012070cb773/nvidia_cuda_runtime_cu12-12.4.127-py3-none-manylinux2014_aarch64.whl", hash = "sha256:961fe0e2e716a2a1d967aab7caee97512f71767f852f67432d572e36cb3a11f3", size = 894177 }, + { url = "https://files.pythonhosted.org/packages/ea/27/1795d86fe88ef397885f2e580ac37628ed058a92ed2c39dc8eac3adf0619/nvidia_cuda_runtime_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:64403288fa2136ee8e467cdc9c9427e0434110899d07c779f25b5c068934faa5", size = 883737 }, +] + +[[package]] +name = "nvidia-cudnn-cu12" +version = "9.1.0.70" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/fd/713452cd72343f682b1c7b9321e23829f00b842ceaedcda96e742ea0b0b3/nvidia_cudnn_cu12-9.1.0.70-py3-none-manylinux2014_x86_64.whl", hash = "sha256:165764f44ef8c61fcdfdfdbe769d687e06374059fbb388b6c89ecb0e28793a6f", size = 664752741 }, +] + +[[package]] +name = "nvidia-cufft-cu12" +version = "11.2.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/8a/0e728f749baca3fbeffad762738276e5df60851958be7783af121a7221e7/nvidia_cufft_cu12-11.2.1.3-py3-none-manylinux2014_aarch64.whl", hash = "sha256:5dad8008fc7f92f5ddfa2101430917ce2ffacd86824914c82e28990ad7f00399", size = 211422548 }, + { url = "https://files.pythonhosted.org/packages/27/94/3266821f65b92b3138631e9c8e7fe1fb513804ac934485a8d05776e1dd43/nvidia_cufft_cu12-11.2.1.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f083fc24912aa410be21fa16d157fed2055dab1cc4b6934a0e03cba69eb242b9", size = 211459117 }, +] + +[[package]] +name = "nvidia-curand-cu12" +version = "10.3.5.147" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/9c/a79180e4d70995fdf030c6946991d0171555c6edf95c265c6b2bf7011112/nvidia_curand_cu12-10.3.5.147-py3-none-manylinux2014_aarch64.whl", hash = "sha256:1f173f09e3e3c76ab084aba0de819c49e56614feae5c12f69883f4ae9bb5fad9", size = 56314811 }, + { url = "https://files.pythonhosted.org/packages/8a/6d/44ad094874c6f1b9c654f8ed939590bdc408349f137f9b98a3a23ccec411/nvidia_curand_cu12-10.3.5.147-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a88f583d4e0bb643c49743469964103aa59f7f708d862c3ddb0fc07f851e3b8b", size = 56305206 }, +] + +[[package]] +name = "nvidia-cusolver-cu12" +version = "11.6.1.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12" }, + { name = "nvidia-cusparse-cu12" }, + { name = "nvidia-nvjitlink-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/6b/a5c33cf16af09166845345275c34ad2190944bcc6026797a39f8e0a282e0/nvidia_cusolver_cu12-11.6.1.9-py3-none-manylinux2014_aarch64.whl", hash = "sha256:d338f155f174f90724bbde3758b7ac375a70ce8e706d70b018dd3375545fc84e", size = 127634111 }, + { url = "https://files.pythonhosted.org/packages/3a/e1/5b9089a4b2a4790dfdea8b3a006052cfecff58139d5a4e34cb1a51df8d6f/nvidia_cusolver_cu12-11.6.1.9-py3-none-manylinux2014_x86_64.whl", hash = "sha256:19e33fa442bcfd085b3086c4ebf7e8debc07cfe01e11513cc6d332fd918ac260", size = 127936057 }, +] + +[[package]] +name = "nvidia-cusparse-cu12" +version = "12.3.1.170" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/a9/c0d2f83a53d40a4a41be14cea6a0bf9e668ffcf8b004bd65633f433050c0/nvidia_cusparse_cu12-12.3.1.170-py3-none-manylinux2014_aarch64.whl", hash = "sha256:9d32f62896231ebe0480efd8a7f702e143c98cfaa0e8a76df3386c1ba2b54df3", size = 207381987 }, + { url = "https://files.pythonhosted.org/packages/db/f7/97a9ea26ed4bbbfc2d470994b8b4f338ef663be97b8f677519ac195e113d/nvidia_cusparse_cu12-12.3.1.170-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ea4f11a2904e2a8dc4b1833cc1b5181cde564edd0d5cd33e3c168eff2d1863f1", size = 207454763 }, +] + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.21.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/99/12cd266d6233f47d00daf3a72739872bdc10267d0383508b0b9c84a18bb6/nvidia_nccl_cu12-2.21.5-py3-none-manylinux2014_x86_64.whl", hash = "sha256:8579076d30a8c24988834445f8d633c697d42397e92ffc3f63fa26766d25e0a0", size = 188654414 }, +] + +[[package]] +name = "nvidia-nvjitlink-cu12" +version = "12.4.127" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/45/239d52c05074898a80a900f49b1615d81c07fceadd5ad6c4f86a987c0bc4/nvidia_nvjitlink_cu12-12.4.127-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4abe7fef64914ccfa909bc2ba39739670ecc9e820c83ccc7a6ed414122599b83", size = 20552510 }, + { url = "https://files.pythonhosted.org/packages/ff/ff/847841bacfbefc97a00036e0fce5a0f086b640756dc38caea5e1bb002655/nvidia_nvjitlink_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:06b3b9b25bf3f8af351d664978ca26a16d2c5127dbd53c0497e28d1fb9611d57", size = 21066810 }, +] + +[[package]] +name = "nvidia-nvtx-cu12" +version = "12.4.127" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/39/471f581edbb7804b39e8063d92fc8305bdc7a80ae5c07dbe6ea5c50d14a5/nvidia_nvtx_cu12-12.4.127-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7959ad635db13edf4fc65c06a6e9f9e55fc2f92596db928d169c0bb031e88ef3", size = 100417 }, + { url = "https://files.pythonhosted.org/packages/87/20/199b8713428322a2f22b722c62b8cc278cc53dffa9705d744484b5035ee9/nvidia_nvtx_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:781e950d9b9f60d8241ccea575b32f5105a5baf4c2351cab5256a24869f12a1a", size = 99144 }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + +[[package]] +name = "password-strength" +version = "0.0.3.post2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/f1/6165ebcca27fca3f1d63f8c3a45805c2ed8568be4d09219a2aa45e792c14/password_strength-0.0.3.post2.tar.gz", hash = "sha256:bf4df10a58fcd3abfa182367307b4fd7b1cec518121dd83bf80c1c42ba796762", size = 12857 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/d6/08fd888c980589e4e27c2a4177e972481e8881600138e63afb785fe52630/password_strength-0.0.3.post2-py2.py3-none-any.whl", hash = "sha256:6739357c2863d707b7c7f247ff7c6882a70904a18d12c9aaf98f8b95da176fb9", size = 12167 }, +] + +[[package]] +name = "pip" +version = "24.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/b1/b422acd212ad7eedddaf7981eee6e5de085154ff726459cf2da7c5a184c1/pip-24.3.1.tar.gz", hash = "sha256:ebcb60557f2aefabc2e0f918751cd24ea0d56d8ec5445fe1807f1d2109660b99", size = 1931073 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/7d/500c9ad20238fcfcb4cb9243eede163594d7020ce87bd9610c9e02771876/pip-24.3.1-py3-none-any.whl", hash = "sha256:3790624780082365f47549d032f3770eeb2b1e8bd1f7b2e02dace1afa361b4ed", size = 1822182 }, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "propcache" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/c8/2a13f78d82211490855b2fb303b6721348d0787fdd9a12ac46d99d3acde1/propcache-0.2.1.tar.gz", hash = "sha256:3f77ce728b19cb537714499928fe800c3dda29e8d9428778fc7c186da4c09a64", size = 41735 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/0f/2913b6791ebefb2b25b4efd4bb2299c985e09786b9f5b19184a88e5778dd/propcache-0.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ffc3cca89bb438fb9c95c13fc874012f7b9466b89328c3c8b1aa93cdcfadd16", size = 79297 }, + { url = "https://files.pythonhosted.org/packages/cf/73/af2053aeccd40b05d6e19058419ac77674daecdd32478088b79375b9ab54/propcache-0.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f174bbd484294ed9fdf09437f889f95807e5f229d5d93588d34e92106fbf6717", size = 45611 }, + { url = "https://files.pythonhosted.org/packages/3c/09/8386115ba7775ea3b9537730e8cf718d83bbf95bffe30757ccf37ec4e5da/propcache-0.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:70693319e0b8fd35dd863e3e29513875eb15c51945bf32519ef52927ca883bc3", size = 45146 }, + { url = "https://files.pythonhosted.org/packages/03/7a/793aa12f0537b2e520bf09f4c6833706b63170a211ad042ca71cbf79d9cb/propcache-0.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b480c6a4e1138e1aa137c0079b9b6305ec6dcc1098a8ca5196283e8a49df95a9", size = 232136 }, + { url = "https://files.pythonhosted.org/packages/f1/38/b921b3168d72111769f648314100558c2ea1d52eb3d1ba7ea5c4aa6f9848/propcache-0.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d27b84d5880f6d8aa9ae3edb253c59d9f6642ffbb2c889b78b60361eed449787", size = 239706 }, + { url = "https://files.pythonhosted.org/packages/14/29/4636f500c69b5edea7786db3c34eb6166f3384b905665ce312a6e42c720c/propcache-0.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:857112b22acd417c40fa4595db2fe28ab900c8c5fe4670c7989b1c0230955465", size = 238531 }, + { url = "https://files.pythonhosted.org/packages/85/14/01fe53580a8e1734ebb704a3482b7829a0ef4ea68d356141cf0994d9659b/propcache-0.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf6c4150f8c0e32d241436526f3c3f9cbd34429492abddbada2ffcff506c51af", size = 231063 }, + { url = "https://files.pythonhosted.org/packages/33/5c/1d961299f3c3b8438301ccfbff0143b69afcc30c05fa28673cface692305/propcache-0.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66d4cfda1d8ed687daa4bc0274fcfd5267873db9a5bc0418c2da19273040eeb7", size = 220134 }, + { url = "https://files.pythonhosted.org/packages/00/d0/ed735e76db279ba67a7d3b45ba4c654e7b02bc2f8050671ec365d8665e21/propcache-0.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2f992c07c0fca81655066705beae35fc95a2fa7366467366db627d9f2ee097f", size = 220009 }, + { url = "https://files.pythonhosted.org/packages/75/90/ee8fab7304ad6533872fee982cfff5a53b63d095d78140827d93de22e2d4/propcache-0.2.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:4a571d97dbe66ef38e472703067021b1467025ec85707d57e78711c085984e54", size = 212199 }, + { url = "https://files.pythonhosted.org/packages/eb/ec/977ffaf1664f82e90737275873461695d4c9407d52abc2f3c3e24716da13/propcache-0.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bb6178c241278d5fe853b3de743087be7f5f4c6f7d6d22a3b524d323eecec505", size = 214827 }, + { url = "https://files.pythonhosted.org/packages/57/48/031fb87ab6081764054821a71b71942161619549396224cbb242922525e8/propcache-0.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ad1af54a62ffe39cf34db1aa6ed1a1873bd548f6401db39d8e7cd060b9211f82", size = 228009 }, + { url = "https://files.pythonhosted.org/packages/1a/06/ef1390f2524850838f2390421b23a8b298f6ce3396a7cc6d39dedd4047b0/propcache-0.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e7048abd75fe40712005bcfc06bb44b9dfcd8e101dda2ecf2f5aa46115ad07ca", size = 231638 }, + { url = "https://files.pythonhosted.org/packages/38/2a/101e6386d5a93358395da1d41642b79c1ee0f3b12e31727932b069282b1d/propcache-0.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:160291c60081f23ee43d44b08a7e5fb76681221a8e10b3139618c5a9a291b84e", size = 222788 }, + { url = "https://files.pythonhosted.org/packages/db/81/786f687951d0979007e05ad9346cd357e50e3d0b0f1a1d6074df334b1bbb/propcache-0.2.1-cp311-cp311-win32.whl", hash = "sha256:819ce3b883b7576ca28da3861c7e1a88afd08cc8c96908e08a3f4dd64a228034", size = 40170 }, + { url = "https://files.pythonhosted.org/packages/cf/59/7cc7037b295d5772eceb426358bb1b86e6cab4616d971bd74275395d100d/propcache-0.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:edc9fc7051e3350643ad929df55c451899bb9ae6d24998a949d2e4c87fb596d3", size = 44404 }, + { url = "https://files.pythonhosted.org/packages/4c/28/1d205fe49be8b1b4df4c50024e62480a442b1a7b818e734308bb0d17e7fb/propcache-0.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:081a430aa8d5e8876c6909b67bd2d937bfd531b0382d3fdedb82612c618bc41a", size = 79588 }, + { url = "https://files.pythonhosted.org/packages/21/ee/fc4d893f8d81cd4971affef2a6cb542b36617cd1d8ce56b406112cb80bf7/propcache-0.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2ccec9ac47cf4e04897619c0e0c1a48c54a71bdf045117d3a26f80d38ab1fb0", size = 45825 }, + { url = "https://files.pythonhosted.org/packages/4a/de/bbe712f94d088da1d237c35d735f675e494a816fd6f54e9db2f61ef4d03f/propcache-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:14d86fe14b7e04fa306e0c43cdbeebe6b2c2156a0c9ce56b815faacc193e320d", size = 45357 }, + { url = "https://files.pythonhosted.org/packages/7f/14/7ae06a6cf2a2f1cb382586d5a99efe66b0b3d0c6f9ac2f759e6f7af9d7cf/propcache-0.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:049324ee97bb67285b49632132db351b41e77833678432be52bdd0289c0e05e4", size = 241869 }, + { url = "https://files.pythonhosted.org/packages/cc/59/227a78be960b54a41124e639e2c39e8807ac0c751c735a900e21315f8c2b/propcache-0.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1cd9a1d071158de1cc1c71a26014dcdfa7dd3d5f4f88c298c7f90ad6f27bb46d", size = 247884 }, + { url = "https://files.pythonhosted.org/packages/84/58/f62b4ffaedf88dc1b17f04d57d8536601e4e030feb26617228ef930c3279/propcache-0.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98110aa363f1bb4c073e8dcfaefd3a5cea0f0834c2aab23dda657e4dab2f53b5", size = 248486 }, + { url = "https://files.pythonhosted.org/packages/1c/07/ebe102777a830bca91bbb93e3479cd34c2ca5d0361b83be9dbd93104865e/propcache-0.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:647894f5ae99c4cf6bb82a1bb3a796f6e06af3caa3d32e26d2350d0e3e3faf24", size = 243649 }, + { url = "https://files.pythonhosted.org/packages/ed/bc/4f7aba7f08f520376c4bb6a20b9a981a581b7f2e385fa0ec9f789bb2d362/propcache-0.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfd3223c15bebe26518d58ccf9a39b93948d3dcb3e57a20480dfdd315356baff", size = 229103 }, + { url = "https://files.pythonhosted.org/packages/fe/d5/04ac9cd4e51a57a96f78795e03c5a0ddb8f23ec098b86f92de028d7f2a6b/propcache-0.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d71264a80f3fcf512eb4f18f59423fe82d6e346ee97b90625f283df56aee103f", size = 226607 }, + { url = "https://files.pythonhosted.org/packages/e3/f0/24060d959ea41d7a7cc7fdbf68b31852331aabda914a0c63bdb0e22e96d6/propcache-0.2.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e73091191e4280403bde6c9a52a6999d69cdfde498f1fdf629105247599b57ec", size = 221153 }, + { url = "https://files.pythonhosted.org/packages/77/a7/3ac76045a077b3e4de4859a0753010765e45749bdf53bd02bc4d372da1a0/propcache-0.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3935bfa5fede35fb202c4b569bb9c042f337ca4ff7bd540a0aa5e37131659348", size = 222151 }, + { url = "https://files.pythonhosted.org/packages/e7/af/5e29da6f80cebab3f5a4dcd2a3240e7f56f2c4abf51cbfcc99be34e17f0b/propcache-0.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f508b0491767bb1f2b87fdfacaba5f7eddc2f867740ec69ece6d1946d29029a6", size = 233812 }, + { url = "https://files.pythonhosted.org/packages/8c/89/ebe3ad52642cc5509eaa453e9f4b94b374d81bae3265c59d5c2d98efa1b4/propcache-0.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1672137af7c46662a1c2be1e8dc78cb6d224319aaa40271c9257d886be4363a6", size = 238829 }, + { url = "https://files.pythonhosted.org/packages/e9/2f/6b32f273fa02e978b7577159eae7471b3cfb88b48563b1c2578b2d7ca0bb/propcache-0.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b74c261802d3d2b85c9df2dfb2fa81b6f90deeef63c2db9f0e029a3cac50b518", size = 230704 }, + { url = "https://files.pythonhosted.org/packages/5c/2e/f40ae6ff5624a5f77edd7b8359b208b5455ea113f68309e2b00a2e1426b6/propcache-0.2.1-cp312-cp312-win32.whl", hash = "sha256:d09c333d36c1409d56a9d29b3a1b800a42c76a57a5a8907eacdbce3f18768246", size = 40050 }, + { url = "https://files.pythonhosted.org/packages/3b/77/a92c3ef994e47180862b9d7d11e37624fb1c00a16d61faf55115d970628b/propcache-0.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:c214999039d4f2a5b2073ac506bba279945233da8c786e490d411dfc30f855c1", size = 44117 }, + { url = "https://files.pythonhosted.org/packages/0f/2a/329e0547cf2def8857157f9477669043e75524cc3e6251cef332b3ff256f/propcache-0.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aca405706e0b0a44cc6bfd41fbe89919a6a56999157f6de7e182a990c36e37bc", size = 77002 }, + { url = "https://files.pythonhosted.org/packages/12/2d/c4df5415e2382f840dc2ecbca0eeb2293024bc28e57a80392f2012b4708c/propcache-0.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:12d1083f001ace206fe34b6bdc2cb94be66d57a850866f0b908972f90996b3e9", size = 44639 }, + { url = "https://files.pythonhosted.org/packages/d0/5a/21aaa4ea2f326edaa4e240959ac8b8386ea31dedfdaa636a3544d9e7a408/propcache-0.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d93f3307ad32a27bda2e88ec81134b823c240aa3abb55821a8da553eed8d9439", size = 44049 }, + { url = "https://files.pythonhosted.org/packages/4e/3e/021b6cd86c0acc90d74784ccbb66808b0bd36067a1bf3e2deb0f3845f618/propcache-0.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba278acf14471d36316159c94a802933d10b6a1e117b8554fe0d0d9b75c9d536", size = 224819 }, + { url = "https://files.pythonhosted.org/packages/3c/57/c2fdeed1b3b8918b1770a133ba5c43ad3d78e18285b0c06364861ef5cc38/propcache-0.2.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4e6281aedfca15301c41f74d7005e6e3f4ca143584ba696ac69df4f02f40d629", size = 229625 }, + { url = "https://files.pythonhosted.org/packages/9d/81/70d4ff57bf2877b5780b466471bebf5892f851a7e2ca0ae7ffd728220281/propcache-0.2.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b750a8e5a1262434fb1517ddf64b5de58327f1adc3524a5e44c2ca43305eb0b", size = 232934 }, + { url = "https://files.pythonhosted.org/packages/3c/b9/bb51ea95d73b3fb4100cb95adbd4e1acaf2cbb1fd1083f5468eeb4a099a8/propcache-0.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf72af5e0fb40e9babf594308911436c8efde3cb5e75b6f206c34ad18be5c052", size = 227361 }, + { url = "https://files.pythonhosted.org/packages/f1/20/3c6d696cd6fd70b29445960cc803b1851a1131e7a2e4ee261ee48e002bcd/propcache-0.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2d0a12018b04f4cb820781ec0dffb5f7c7c1d2a5cd22bff7fb055a2cb19ebce", size = 213904 }, + { url = "https://files.pythonhosted.org/packages/a1/cb/1593bfc5ac6d40c010fa823f128056d6bc25b667f5393781e37d62f12005/propcache-0.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e800776a79a5aabdb17dcc2346a7d66d0777e942e4cd251defeb084762ecd17d", size = 212632 }, + { url = "https://files.pythonhosted.org/packages/6d/5c/e95617e222be14a34c709442a0ec179f3207f8a2b900273720501a70ec5e/propcache-0.2.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4160d9283bd382fa6c0c2b5e017acc95bc183570cd70968b9202ad6d8fc48dce", size = 207897 }, + { url = "https://files.pythonhosted.org/packages/8e/3b/56c5ab3dc00f6375fbcdeefdede5adf9bee94f1fab04adc8db118f0f9e25/propcache-0.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:30b43e74f1359353341a7adb783c8f1b1c676367b011709f466f42fda2045e95", size = 208118 }, + { url = "https://files.pythonhosted.org/packages/86/25/d7ef738323fbc6ebcbce33eb2a19c5e07a89a3df2fded206065bd5e868a9/propcache-0.2.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:58791550b27d5488b1bb52bc96328456095d96206a250d28d874fafe11b3dfaf", size = 217851 }, + { url = "https://files.pythonhosted.org/packages/b3/77/763e6cef1852cf1ba740590364ec50309b89d1c818e3256d3929eb92fabf/propcache-0.2.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0f022d381747f0dfe27e99d928e31bc51a18b65bb9e481ae0af1380a6725dd1f", size = 222630 }, + { url = "https://files.pythonhosted.org/packages/4f/e9/0f86be33602089c701696fbed8d8c4c07b6ee9605c5b7536fd27ed540c5b/propcache-0.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:297878dc9d0a334358f9b608b56d02e72899f3b8499fc6044133f0d319e2ec30", size = 216269 }, + { url = "https://files.pythonhosted.org/packages/cc/02/5ac83217d522394b6a2e81a2e888167e7ca629ef6569a3f09852d6dcb01a/propcache-0.2.1-cp313-cp313-win32.whl", hash = "sha256:ddfab44e4489bd79bda09d84c430677fc7f0a4939a73d2bba3073036f487a0a6", size = 39472 }, + { url = "https://files.pythonhosted.org/packages/f4/33/d6f5420252a36034bc8a3a01171bc55b4bff5df50d1c63d9caa50693662f/propcache-0.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:556fc6c10989f19a179e4321e5d678db8eb2924131e64652a51fe83e4c3db0e1", size = 43363 }, + { url = "https://files.pythonhosted.org/packages/41/b6/c5319caea262f4821995dca2107483b94a3345d4607ad797c76cb9c36bcc/propcache-0.2.1-py3-none-any.whl", hash = "sha256:52277518d6aae65536e9cea52d4e7fd2f7a66f4aa2d30ed3f2fcea620ace3c54", size = 11818 }, +] + +[[package]] +name = "protobuf" +version = "5.29.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d2/4f/1639b7b1633d8fd55f216ba01e21bf2c43384ab25ef3ddb35d85a52033e8/protobuf-5.29.1.tar.gz", hash = "sha256:683be02ca21a6ffe80db6dd02c0b5b2892322c59ca57fd6c872d652cb80549cb", size = 424965 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/c7/28669b04691a376cf7d0617d612f126aa0fff763d57df0142f9bf474c5b8/protobuf-5.29.1-cp310-abi3-win32.whl", hash = "sha256:22c1f539024241ee545cbcb00ee160ad1877975690b16656ff87dde107b5f110", size = 422706 }, + { url = "https://files.pythonhosted.org/packages/e3/33/dc7a7712f457456b7e0b16420ab8ba1cc8686751d3f28392eb43d0029ab9/protobuf-5.29.1-cp310-abi3-win_amd64.whl", hash = "sha256:1fc55267f086dd4050d18ef839d7bd69300d0d08c2a53ca7df3920cc271a3c34", size = 434505 }, + { url = "https://files.pythonhosted.org/packages/e5/39/44239fb1c6ec557e1731d996a5de89a9eb1ada7a92491fcf9c5d714052ed/protobuf-5.29.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:d473655e29c0c4bbf8b69e9a8fb54645bc289dead6d753b952e7aa660254ae18", size = 417822 }, + { url = "https://files.pythonhosted.org/packages/fb/4a/ec56f101d38d4bef2959a9750209809242d86cf8b897db00f2f98bfa360e/protobuf-5.29.1-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:b5ba1d0e4c8a40ae0496d0e2ecfdbb82e1776928a205106d14ad6985a09ec155", size = 319572 }, + { url = "https://files.pythonhosted.org/packages/04/52/c97c58a33b3d6c89a8138788576d372a90a6556f354799971c6b4d16d871/protobuf-5.29.1-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:8ee1461b3af56145aca2800e6a3e2f928108c749ba8feccc6f5dd0062c410c0d", size = 319671 }, + { url = "https://files.pythonhosted.org/packages/3b/24/c8c49df8f6587719e1d400109b16c10c6902d0c9adddc8fff82840146f99/protobuf-5.29.1-py3-none-any.whl", hash = "sha256:32600ddb9c2a53dedc25b8581ea0f1fd8ea04956373c0c07577ce58d312522e0", size = 172547 }, +] + +[[package]] +name = "psutil" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/10/2a30b13c61e7cf937f4adf90710776b7918ed0a9c434e2c38224732af310/psutil-6.1.0.tar.gz", hash = "sha256:353815f59a7f64cdaca1c0307ee13558a0512f6db064e92fe833784f08539c7a", size = 508565 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/2b/f4dea5d993d9cd22ad958eea828a41d5d225556123d372f02547c29c4f97/psutil-6.1.0-cp27-none-win32.whl", hash = "sha256:9118f27452b70bb1d9ab3198c1f626c2499384935aaf55388211ad982611407e", size = 246648 }, + { url = "https://files.pythonhosted.org/packages/9f/14/4aa97a7f2e0ac33a050d990ab31686d651ae4ef8c86661fef067f00437b9/psutil-6.1.0-cp27-none-win_amd64.whl", hash = "sha256:a8506f6119cff7015678e2bce904a4da21025cc70ad283a53b099e7620061d85", size = 249905 }, + { url = "https://files.pythonhosted.org/packages/01/9e/8be43078a171381953cfee33c07c0d628594b5dbfc5157847b85022c2c1b/psutil-6.1.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6e2dcd475ce8b80522e51d923d10c7871e45f20918e027ab682f94f1c6351688", size = 247762 }, + { url = "https://files.pythonhosted.org/packages/1d/cb/313e80644ea407f04f6602a9e23096540d9dc1878755f3952ea8d3d104be/psutil-6.1.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0895b8414afafc526712c498bd9de2b063deaac4021a3b3c34566283464aff8e", size = 248777 }, + { url = "https://files.pythonhosted.org/packages/65/8e/bcbe2025c587b5d703369b6a75b65d41d1367553da6e3f788aff91eaf5bd/psutil-6.1.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9dcbfce5d89f1d1f2546a2090f4fcf87c7f669d1d90aacb7d7582addece9fb38", size = 284259 }, + { url = "https://files.pythonhosted.org/packages/58/4d/8245e6f76a93c98aab285a43ea71ff1b171bcd90c9d238bf81f7021fb233/psutil-6.1.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:498c6979f9c6637ebc3a73b3f87f9eb1ec24e1ce53a7c5173b8508981614a90b", size = 287255 }, + { url = "https://files.pythonhosted.org/packages/27/c2/d034856ac47e3b3cdfa9720d0e113902e615f4190d5d1bdb8df4b2015fb2/psutil-6.1.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d905186d647b16755a800e7263d43df08b790d709d575105d419f8b6ef65423a", size = 288804 }, + { url = "https://files.pythonhosted.org/packages/ea/55/5389ed243c878725feffc0d6a3bc5ef6764312b6fc7c081faaa2cfa7ef37/psutil-6.1.0-cp37-abi3-win32.whl", hash = "sha256:1ad45a1f5d0b608253b11508f80940985d1d0c8f6111b5cb637533a0e6ddc13e", size = 250386 }, + { url = "https://files.pythonhosted.org/packages/11/91/87fa6f060e649b1e1a7b19a4f5869709fbf750b7c8c262ee776ec32f3028/psutil-6.1.0-cp37-abi3-win_amd64.whl", hash = "sha256:a8fb3752b491d246034fa4d279ff076501588ce8cbcdbb62c32fd7a377d996be", size = 254228 }, +] + +[[package]] +name = "py" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/ff/fec109ceb715d2a6b4c4a85a61af3b40c723a961e8828319fbcb15b868dc/py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", size = 207796 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378", size = 98708 }, +] + +[[package]] +name = "py-bip39-bindings" +version = "0.1.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/8a/5e22cbd00b799b33ce0a45ae3715c9ea3fcd263f877544819e7d03753c49/py_bip39_bindings-0.1.11.tar.gz", hash = "sha256:ebc128ccf3a0750d758557e094802f0975c3760a939f8a8b76392d7dbe6b52a1", size = 18103 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/b6/0bd5bf1c4cb00e000a9c909280aa7f8654208ee136e2cd1f3650a8de59ed/py_bip39_bindings-0.1.11-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:27cce22727e28705a660464689ade6d2cdad4e622bead5bde2ffa53c4f605ee5", size = 399429 }, + { url = "https://files.pythonhosted.org/packages/22/44/b6ffdc17cc499b72821a1d777fb465af842d5b7373f1825a45dce551fedc/py_bip39_bindings-0.1.11-cp311-cp311-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:cdf35d031587296dcbdb22dbc67f2eaf5b5df9d5036b77fbeb93affbb9eec8d3", size = 792364 }, + { url = "https://files.pythonhosted.org/packages/15/8d/0883d814a26f922b331218c777ecaec61919aebf9c54d4991f919b21ab8a/py_bip39_bindings-0.1.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2fd5b926686207752d5f2e2ff164a9489b3613239d0967362f10c2fbd64eb018", size = 413967 }, + { url = "https://files.pythonhosted.org/packages/72/30/e3c76035b83c9552bbeee90645411a3d52983067badbd8a5854a823701f9/py_bip39_bindings-0.1.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba84c38962bffdaea0e499245731d669cc21d1280f81ace8ff60ed3550024570", size = 1225281 }, + { url = "https://files.pythonhosted.org/packages/38/c9/3b73fe8ffd285387c4fe7b60ccd0072ee16d5153409619c472852ec88acc/py_bip39_bindings-0.1.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9024ec3c4a3db005b355f9a00602cede290dec5e9c7cf7dd06a26f620b0cf99", size = 1227054 }, + { url = "https://files.pythonhosted.org/packages/7e/2f/d096e6e08439e5b3c1f41e95c5828700012c130611a64fe9e82a43b0ca45/py_bip39_bindings-0.1.11-cp311-cp311-manylinux_2_28_armv7l.whl", hash = "sha256:ce028c8aef51dec2a85f298461b2988cca28740bf3cc23472c3469d3f853714e", size = 1180933 }, + { url = "https://files.pythonhosted.org/packages/0b/8f/00c2239452f26e180229d74acd63ac0c027f8eb9a5fb90b879c6c1192102/py_bip39_bindings-0.1.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:51882cd0fa7529173b3543c089c24c775f1876ddf48f10e60f2ed07ad2af5cae", size = 1264074 }, + { url = "https://files.pythonhosted.org/packages/1a/7a/524e38494a0ffb7ca211225acde0324cf216f081dffae7fd55446b009889/py_bip39_bindings-0.1.11-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4ee776f3b33b2d71fee48679951f117e3d1f052449ec2fcb184f3c64a4c77e4f", size = 1252039 }, + { url = "https://files.pythonhosted.org/packages/e9/a0/68bbb9e9326266a9acca2558d6556e22df31bcf4d2235ee1cdaf362add82/py_bip39_bindings-0.1.11-cp311-none-win32.whl", hash = "sha256:d8b722e49562810f94eb61c9efa172f327537c74c37da3e86b161f7f444c51bf", size = 296767 }, + { url = "https://files.pythonhosted.org/packages/0a/47/4c5d0ff9949b725696b1b10b5b87f6c6d3c333d8458b354b7c8536272eef/py_bip39_bindings-0.1.11-cp311-none-win_amd64.whl", hash = "sha256:be934052497f07605768e2c7184e4f4269b3e2e77930131dfc9bdbb791e6fdf4", size = 283550 }, + { url = "https://files.pythonhosted.org/packages/cb/a5/0d29c79ee79475ceca80ca19b5975917827af6ce4dd2711ed197822a12ea/py_bip39_bindings-0.1.11-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:afa9c5762cfaec01141f478a9c3132de01ec3890ff2e5a4013c79d3ba3aff8bb", size = 798236 }, + { url = "https://files.pythonhosted.org/packages/47/fd/a4baff5368ef8be569064e5aef1319c4e75b24a80c70c0f3a871727c6a38/py_bip39_bindings-0.1.11-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a3af7c1f955c6bbd613c6b38d022f7c73896acaf0ecc972ac0dee4b952e14568", size = 406227 }, + { url = "https://files.pythonhosted.org/packages/78/44/fe4a107204690d18691a2db7cacfd6043331f6982dc59962d9e220d46711/py_bip39_bindings-0.1.11-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6aed3e86f105a36676e8dd0c8bc3f611a81b7ba4309b22a77fdc0f63b260e094", size = 1215916 }, + { url = "https://files.pythonhosted.org/packages/0d/53/0cbfe92fde6925244280eaed3ede0f16cb498c8764023acc155225d5f9e4/py_bip39_bindings-0.1.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d202f051cf063abae3acd0b74454d9d7b1dbeaf466ef7cb47a34ccedac845b62", size = 451663 }, + { url = "https://files.pythonhosted.org/packages/44/9b/4c3c8c6decdc7472323a66e98e1d37c43dcbf798c944791eafeb63ff8411/py_bip39_bindings-0.1.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae120b5542fecf97aa3fdb6a526bac1004cb641bc9cc0d0030c6735dc2156072", size = 1206493 }, + { url = "https://files.pythonhosted.org/packages/94/47/71ed526077a4e58ac4ec5dbb43637faa33cc02a0ada912a3fd8f20c193b9/py_bip39_bindings-0.1.11-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:baf896aabb3bec42803015e010c121c8a3210b20184f37aaa6e400ae8e877e60", size = 483935 }, + { url = "https://files.pythonhosted.org/packages/be/e3/7da98b60d113334e2eb95028289410f8a1771e755fa7ad3de1ae2fa9d951/py_bip39_bindings-0.1.11-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e4d45324c598197dbddac10a0298197ca2587fa7b09d1450697517988a29d515", size = 481093 }, + { url = "https://files.pythonhosted.org/packages/c1/38/d54060bda276a062e2327e169b6660b27beb4f75ab7a9e216dd11b9ae703/py_bip39_bindings-0.1.11-cp312-none-win32.whl", hash = "sha256:92abce265b0f2d8c5830441aff06b7b4f9426088a3de39624b12f3f9ff9fc2eb", size = 296429 }, + { url = "https://files.pythonhosted.org/packages/86/12/256aa92f70a8bdf2a00dc84f6c75c86abadeca1c990e02c8345933889952/py_bip39_bindings-0.1.11-cp312-none-win_amd64.whl", hash = "sha256:6794187229eb0b04d0770f0fba936f0c5c598f552848a398ed5af9a61638cacb", size = 284888 }, +] + +[[package]] +name = "py-ed25519-zebra-bindings" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/f5/833c284f7d76e13d9520215b5731be2983f8f04cf3405f087267de89af37/py_ed25519_zebra_bindings-1.1.0.tar.gz", hash = "sha256:2977603b59cfc593fb01284465fe41062d6929b0d09edf0e1ade40709977014f", size = 12176 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/95/ae7129025ffc3954994b0bd72c83a091ec1a96a508da2b5a8f3e9e54ef93/py_ed25519_zebra_bindings-1.1.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:048e84121007b6ced32b70086e9bd710a825920f0715d73be4760c45f61847be", size = 287381 }, + { url = "https://files.pythonhosted.org/packages/46/9d/f41a6b8103697eca40c0bb9b22c8bd9f9593ed1941da340ce27b419c2d6a/py_ed25519_zebra_bindings-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8200479a222da9bab0abbe35d9a60e4f658a4039054c3b9f2e58a102a393a658", size = 263398 }, + { url = "https://files.pythonhosted.org/packages/fa/38/5edf90bea230aa9528f581b5540f994bba6bd517ffc1a902d44fdc46fcc5/py_ed25519_zebra_bindings-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ef039b7a7e4f1653e7f0c467d136a7e135061e53fdc74934d36489a8859f9e4", size = 295474 }, + { url = "https://files.pythonhosted.org/packages/ab/76/79e2a9e3873b7f99b3071a3f2473d7355d47adfa576843fa084bfd1e66e5/py_ed25519_zebra_bindings-1.1.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d6d035b7bd3dd998ef6030666e69cde95c34225187f53ebb9c2fa7298a65ffde", size = 321097 }, + { url = "https://files.pythonhosted.org/packages/12/de/5aa80345871578a23d548f03597cab77ec7269434136a0e1f716f7222355/py_ed25519_zebra_bindings-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f4a7bb72294f7f1f560a464832c0fc32bc0a20cf4d3b638f2428bf3dde6ebda", size = 334803 }, + { url = "https://files.pythonhosted.org/packages/a1/c9/a6819824ed0bea5139d3b85eb51343f2f00dcfc93cdf3829deeac8b63fc9/py_ed25519_zebra_bindings-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89735c2623bcf02177004eaa895250a3115214cd51df2ab561863f565aa06b1b", size = 316386 }, + { url = "https://files.pythonhosted.org/packages/49/c3/37d32d12e36226f6ffb3d5f68e2191e56b708bfc39662f67e1ff7c5244ee/py_ed25519_zebra_bindings-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4a7cccd8ab3156d1f397c6476583e78427e93cd01fa82173df78b96e15eb9f4d", size = 334029 }, + { url = "https://files.pythonhosted.org/packages/28/82/82e787a9899a02154b7db6f73c3fe9d5f414baa45e3f4dff00362750473f/py_ed25519_zebra_bindings-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:27483aa9262d0952e886d01ec174056503238064ce1f08a3fb17752db18071dd", size = 481082 }, + { url = "https://files.pythonhosted.org/packages/d8/a7/e37725ae7029482742ce77887636a1051c8ba4667c84dca53c78e631d2e2/py_ed25519_zebra_bindings-1.1.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:b461baeb4adb5c5d916f8cf31651142744f29b90f010a71bb22beafe0d803f40", size = 583309 }, + { url = "https://files.pythonhosted.org/packages/89/e4/568c2134ebe3b95ef10c0d00da28a8a667aaa2cf913f2b8436a7080f8036/py_ed25519_zebra_bindings-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1b79af368be80b5cd32b2a678c775f113c1d76c6f0e1ea5e66586c81c9e0ab5b", size = 506769 }, + { url = "https://files.pythonhosted.org/packages/08/06/1afb277b1021cc0669b56fe7cad81b5f26f6cded280f1b2f881ac6dd5f54/py_ed25519_zebra_bindings-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9555ccf645374e5282103416fe5cba60937d7bf12af676980bd4e18cfa2fab48", size = 485460 }, + { url = "https://files.pythonhosted.org/packages/21/82/576d2c725d6fb66090424ad30eae9b1a503cd07973b6bc10c1f28a508686/py_ed25519_zebra_bindings-1.1.0-cp311-none-win32.whl", hash = "sha256:1c55a32c2070aa68e0ed5a2930ba547fbf47617fd279462171d5c0f87b00df6d", size = 183764 }, + { url = "https://files.pythonhosted.org/packages/2f/ed/1da5f72b31eda77d6333756c6f4c542962ab6071a192e3bf4db0ca1e4ccb/py_ed25519_zebra_bindings-1.1.0-cp311-none-win_amd64.whl", hash = "sha256:c4a4dedb1b8edf7f68dd8015f9d8a20f2f0ecca90fac4432e5cbabfcc16ab13d", size = 186734 }, + { url = "https://files.pythonhosted.org/packages/01/e7/41d2c8c43d173e3f5421dbcff89ddec60f3b0df2111146a3469be8f0b19f/py_ed25519_zebra_bindings-1.1.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3ee9a0b7eb896547e539a27c4286461d58c6a99952ea27fa1b5f5e39e63019dc", size = 287118 }, + { url = "https://files.pythonhosted.org/packages/81/99/4dd0de0907eb44e3bb4240dd5daa002ab52c92a44f288cb8485e1cd42534/py_ed25519_zebra_bindings-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:93e2a12d0bbf58f4d1e5ae2a1c352e43302cadd747a1a5e88fea03ce7a78a562", size = 263119 }, + { url = "https://files.pythonhosted.org/packages/99/ea/46d2ffbe8205c02228106ff757bd1afebfe1258f8ded83303ed20c689499/py_ed25519_zebra_bindings-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33673e6047eba0a0a28e818fa0b36b703986347fc98e6f0f96e36af68756787", size = 294870 }, + { url = "https://files.pythonhosted.org/packages/ad/6c/767881f4917b7626a1a1e5ad80b031a692883e93076c361254f20bcf3964/py_ed25519_zebra_bindings-1.1.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:14399e6e8c5b9c193256a1b9aec16b9de719691de84ab23a690056cfe011d13b", size = 320685 }, + { url = "https://files.pythonhosted.org/packages/1a/4c/4e730ff1c965bacd6a6a065cd4e462d37ed938d172939609a5da01bc03f5/py_ed25519_zebra_bindings-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:85ea7a5632a1bf6713518bb56d4c8abe5128aee173a3c498b3a564cfb346ca72", size = 333754 }, + { url = "https://files.pythonhosted.org/packages/bd/5b/b889db7a23724ac1c559c7079c7a941e822849695109aac1d065251039ff/py_ed25519_zebra_bindings-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a01f58a1c980df68c8efd43892562b3224507bab83d880910fbb4a3c84fc965", size = 316045 }, + { url = "https://files.pythonhosted.org/packages/7f/3d/ba457c149c108d594adea964573f2b4809fd3b7ed887a7d38aa77ed7d11b/py_ed25519_zebra_bindings-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:655360cd74718d4efb8fbaf7afb2e4ce459af5f1d399479f577a63bd9177aa3b", size = 333567 }, + { url = "https://files.pythonhosted.org/packages/88/00/f4e05b36cc04212c4d00c6fcbfbf17a2b15f6d08f00f12c1ce499061b666/py_ed25519_zebra_bindings-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:650081644c6613fdf658456ed4b2a6580ec1b54084f318a31a924ce5cf536bb9", size = 481375 }, + { url = "https://files.pythonhosted.org/packages/be/7b/e96f39dfa9fc11e477256b3dec80b7afde51004ba97f87ac2eb3e68e44cf/py_ed25519_zebra_bindings-1.1.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5d56b81186fc75cbcf80d0549f83e98c62c4359460e512f9fb8d6c7be2a158dd", size = 583169 }, + { url = "https://files.pythonhosted.org/packages/ca/8f/9dbf2a43efdfd2b2452eb497b3a72bbac40c2fa57afc6262574cd05ed7ae/py_ed25519_zebra_bindings-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:072bf62421ad890c1849aaa19c7b6e6a890d337f0622e9bd09161b180a10496c", size = 506486 }, + { url = "https://files.pythonhosted.org/packages/28/ca/2bad513face5cce85773d9ecf92dfe8ddbeb8ef33d64b6c2a14565cc99b3/py_ed25519_zebra_bindings-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e09830c3672699f5f1f164fe92b102254ef68300ceaddc847d9a35bf4a2ec270", size = 485075 }, + { url = "https://files.pythonhosted.org/packages/f9/18/ebc4dcde6da7f6a5040bf4c53fd53a5942500e40da8ff3179b8e8462f62c/py_ed25519_zebra_bindings-1.1.0-cp312-none-win32.whl", hash = "sha256:33ca2a7ad10846c281a73450316b390c7343e62e40516389fc1b580241f3907f", size = 183761 }, + { url = "https://files.pythonhosted.org/packages/4f/17/e334d38d2ff14fab0722e03959472b6e24740376ae92bc30c9c076af2be8/py_ed25519_zebra_bindings-1.1.0-cp312-none-win_amd64.whl", hash = "sha256:4ba042528ddb81f8f025b1987bf8f19547f188efb7aa4c95d1a4e3e7f968e991", size = 186542 }, +] + +[[package]] +name = "py-sr25519-bindings" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/55/e5c27d1387f6cb3a6bf7714e1e0c4a62edc3b006710e2d081e8bdfa4123f/py_sr25519_bindings-0.2.1.tar.gz", hash = "sha256:1b96d3dde43adcf86ab427a9fd72b2c6291dca36eb40747df631588c16f01c1a", size = 18439 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/e5/62067ff055a940bcbb02467f7fb63fd85a89cc12153f8c78199ce5c71fb9/py_sr25519_bindings-0.2.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4941e6e0e180f7e72565043ed3ba7190455c9feaa2ab9ee6038904f2b4bb6c5b", size = 331203 }, + { url = "https://files.pythonhosted.org/packages/0a/6c/48a6e1289012b4ab704ccec5315a7c1f1694909b5cc332a36ec87ab03608/py_sr25519_bindings-0.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b63d7cf5bb4d9b986d7f7012c80b92be70311dc9b75862f7880e03b71a29543d", size = 306083 }, + { url = "https://files.pythonhosted.org/packages/e6/da/b7ab72a15e950779edf376b344b6de43aacc7250e319ff23996ef96cda5b/py_sr25519_bindings-0.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b6752bf3b109446d99f3a368e3ba805812fc5bc09e52ef1c82f5a47e43b19973", size = 340172 }, + { url = "https://files.pythonhosted.org/packages/15/7f/4defee54893a3947936f3b5b8b1fe8cb10bb6d01cf87240345f511636e8d/py_sr25519_bindings-0.2.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0368dcdf5ec8d2bb9c13273c78c3c5b033211d37a70a2f1d2080f29a7d118340", size = 368044 }, + { url = "https://files.pythonhosted.org/packages/44/a9/b6ddb161bb28f7da1b261d8e6d59d9669a15bdbfe8bfff0ff15f9a28f0a6/py_sr25519_bindings-0.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2618b02b4a3babac07b8bb61fe9550f911f038bb079665682ca76b2e664e5258", size = 384053 }, + { url = "https://files.pythonhosted.org/packages/7a/66/5d4c78ad9766cd46e5439e9fb84cb10bc47b9c4929c8ea99ee880f405f50/py_sr25519_bindings-0.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ab1bc4dc524efefaecf3a85f4a0ff05c1ca9509d4d64056199984550f3c98b3", size = 365700 }, + { url = "https://files.pythonhosted.org/packages/07/ef/f96d4e2472af62768ffd81df2170f643de87b0ab831e405a4572b9959379/py_sr25519_bindings-0.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7ccdc89d5e3ae0dd163c8150ec76b6bb3291c1cec9746eb79e9544b3423f35f9", size = 385360 }, + { url = "https://files.pythonhosted.org/packages/9e/91/ea5e750e5f2896412fcbbe32da3be8ffab50f4221df7fe3ab367c51a99ac/py_sr25519_bindings-0.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ae6545c414cfa5d7207c9c77aaa576bb374982fb2105a7a9c2764afa5621f6d4", size = 523867 }, + { url = "https://files.pythonhosted.org/packages/7c/d0/e56f6753b264dd4c3f40364879429af7127c8b235c7a2f6d5fbb69137004/py_sr25519_bindings-0.2.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:7046774e39e0166d3c12632969c9d1713e6ad9ca8206bbe82923ba6935b0a01f", size = 627828 }, + { url = "https://files.pythonhosted.org/packages/63/19/7a8d5cca0a498da55b0457be98f03e428e4981b563e5d1c8c92dfc7d136e/py_sr25519_bindings-0.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:cba9a8821176895b080ea761e5ab9cd8727660bf401478a6532a30ae3429573d", size = 551658 }, + { url = "https://files.pythonhosted.org/packages/58/4e/083694bded9ce2d8d598f086aa4ca67f2b9c5d9bfd79ca46f04c95e9322b/py_sr25519_bindings-0.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c31aba05819e5b6b26746dc1b078cf680bd471f135c55e376e95c7774e22e936", size = 529627 }, + { url = "https://files.pythonhosted.org/packages/3d/cc/837b57c938d2b1d0e6f296dc09a3e65b0d762b2387301f8a51452d679391/py_sr25519_bindings-0.2.1-cp311-none-win32.whl", hash = "sha256:d4bfb9c9a5c46563ccf12e74862ee95d2961556ba7aca62c9e4d6e4f7c37b4e0", size = 217894 }, + { url = "https://files.pythonhosted.org/packages/5e/43/3f91ccad4b8d96ddf9a26b00be11de6ad0d260ab26e17ad8f98088512c3a/py_sr25519_bindings-0.2.1-cp311-none-win_amd64.whl", hash = "sha256:4f0d5c065d5e6122e53e771035aa335534363b451358b408d211df1c46773617", size = 224191 }, + { url = "https://files.pythonhosted.org/packages/fa/6f/5dca831fe2617075237d49868d1bd4f025d0dbd23676d7dec3aaf39642cd/py_sr25519_bindings-0.2.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:01ef73c0b3d3f703b54ee69c0f5ff4aa54b4233212c466fd497c7a84d170963a", size = 330633 }, + { url = "https://files.pythonhosted.org/packages/3e/86/569b69e01a962e0c3cd63465e5faad589e54f0c27bfaed5436fef283d56c/py_sr25519_bindings-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7ce8ac85e5ea82825a863f3f6f071e5ead610d7675820eb8ffe772267445ec0b", size = 306030 }, + { url = "https://files.pythonhosted.org/packages/a1/ae/ad0d1fff92966b4ca020abc3ea12e3e1f34c3a937bab28fa0e6bf893d587/py_sr25519_bindings-0.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f59ac8c03c8ef819db063627f4a8247aab0db11d88b21562abbe371612cf66ab", size = 340266 }, + { url = "https://files.pythonhosted.org/packages/b0/7e/93903b1a0789fe1e7f2bb17f4992b55549dfbc8dd8dc3fa4d57c08b72250/py_sr25519_bindings-0.2.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d2c11fc77b57308e3ada9a40e7c343027129b582d3091ebd992c99b1832ac8c1", size = 367790 }, + { url = "https://files.pythonhosted.org/packages/f4/79/842a46cc48c33ff0d08f95db6b327fdd5972fd68d733634322762dd74702/py_sr25519_bindings-0.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92af2831d6896f0b3fef792d1f2da780fabf6c78dac12535b394cbdb51c0d257", size = 383790 }, + { url = "https://files.pythonhosted.org/packages/0d/33/aeeacf174483ae6163bfb8993c0dabdb15875272e59658123d2dcf55f39a/py_sr25519_bindings-0.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc99f7f310b7641e510810c1d6a6b51792ab2ccefac3ab288445a9fcbc9a8265", size = 365962 }, + { url = "https://files.pythonhosted.org/packages/85/bb/c41e0115115336acad5b05d577bf463fa69975ed84dcf50011ac4e07eb89/py_sr25519_bindings-0.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1dc4995a352a6e5851a41cb0ea37d8c9083d173515b7fd2f381b014f57dc1cda", size = 386028 }, + { url = "https://files.pythonhosted.org/packages/cd/d0/48744d7ec55853dc7ec6889f7b85b4f9d21349f09a9ccc8fd988a67f0a46/py_sr25519_bindings-0.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f103dc5c420057c4447bd6ebf28b2b68ff3ab8da85a5f7ff39c405293de80c78", size = 524320 }, + { url = "https://files.pythonhosted.org/packages/50/4f/9462c0525bd64417c56e788b9543a34c08583bf7eabf81797bf5545b924d/py_sr25519_bindings-0.2.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:902ee675497b8d356a2abe2abc4278cd76c503f76d06ef2bcd797c1df59e84b7", size = 628052 }, + { url = "https://files.pythonhosted.org/packages/a7/2a/873f8e7425fd424f9d4aa6eddbbe767889d2aee639372fd9516d6b352c93/py_sr25519_bindings-0.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5dd9748f4bd9a3bc4d5c1245f6edcc723075b1470b4c36add4474df4c53604e8", size = 552273 }, + { url = "https://files.pythonhosted.org/packages/0e/e2/bb29457851816c1637bdd7176ac419073faeecf452dcfae54b50ddb81bc1/py_sr25519_bindings-0.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8c24bc55699d12948571969c26e65138a942bdaca062171288c40c44b9a4f266", size = 530013 }, + { url = "https://files.pythonhosted.org/packages/4b/70/21d32090ca207738a3979620865e2a48ccbed64871cffafb24c6febe234d/py_sr25519_bindings-0.2.1-cp312-none-win32.whl", hash = "sha256:d4799c9a8f280abdfe564d397bad45da380275c8d22604e059bd7b3d5af404b5", size = 218181 }, + { url = "https://files.pythonhosted.org/packages/bb/df/06a61ef52a6889d6879bfa8a5877688f62854c8eab491ad7af60e797a3ef/py_sr25519_bindings-0.2.1-cp312-none-win_amd64.whl", hash = "sha256:0746befd71d1766d8747910cfeb2cec2be2c859c3b3618eda1dc3cb4a1b85175", size = 224095 }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, +] + +[[package]] +name = "pycryptodome" +version = "3.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/52/13b9db4a913eee948152a079fe58d035bd3d1a519584155da8e786f767e6/pycryptodome-3.21.0.tar.gz", hash = "sha256:f7787e0d469bdae763b876174cf2e6c0f7be79808af26b1da96f1a64bcf47297", size = 4818071 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/88/5e83de10450027c96c79dc65ac45e9d0d7a7fef334f39d3789a191f33602/pycryptodome-3.21.0-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:2480ec2c72438430da9f601ebc12c518c093c13111a5c1644c82cdfc2e50b1e4", size = 2495937 }, + { url = "https://files.pythonhosted.org/packages/66/e1/8f28cd8cf7f7563319819d1e172879ccce2333781ae38da61c28fe22d6ff/pycryptodome-3.21.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:de18954104667f565e2fbb4783b56667f30fb49c4d79b346f52a29cb198d5b6b", size = 1634629 }, + { url = "https://files.pythonhosted.org/packages/6a/c1/f75a1aaff0c20c11df8dc8e2bf8057e7f73296af7dfd8cbb40077d1c930d/pycryptodome-3.21.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de4b7263a33947ff440412339cb72b28a5a4c769b5c1ca19e33dd6cd1dcec6e", size = 2168708 }, + { url = "https://files.pythonhosted.org/packages/ea/66/6f2b7ddb457b19f73b82053ecc83ba768680609d56dd457dbc7e902c41aa/pycryptodome-3.21.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0714206d467fc911042d01ea3a1847c847bc10884cf674c82e12915cfe1649f8", size = 2254555 }, + { url = "https://files.pythonhosted.org/packages/2c/2b/152c330732a887a86cbf591ed69bd1b489439b5464806adb270f169ec139/pycryptodome-3.21.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d85c1b613121ed3dbaa5a97369b3b757909531a959d229406a75b912dd51dd1", size = 2294143 }, + { url = "https://files.pythonhosted.org/packages/55/92/517c5c498c2980c1b6d6b9965dffbe31f3cd7f20f40d00ec4069559c5902/pycryptodome-3.21.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:8898a66425a57bcf15e25fc19c12490b87bd939800f39a03ea2de2aea5e3611a", size = 2160509 }, + { url = "https://files.pythonhosted.org/packages/39/1f/c74288f54d80a20a78da87df1818c6464ac1041d10988bb7d982c4153fbc/pycryptodome-3.21.0-cp36-abi3-musllinux_1_2_i686.whl", hash = "sha256:932c905b71a56474bff8a9c014030bc3c882cee696b448af920399f730a650c2", size = 2329480 }, + { url = "https://files.pythonhosted.org/packages/39/1b/d0b013bf7d1af7cf0a6a4fce13f5fe5813ab225313755367b36e714a63f8/pycryptodome-3.21.0-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:18caa8cfbc676eaaf28613637a89980ad2fd96e00c564135bf90bc3f0b34dd93", size = 2254397 }, + { url = "https://files.pythonhosted.org/packages/14/71/4cbd3870d3e926c34706f705d6793159ac49d9a213e3ababcdade5864663/pycryptodome-3.21.0-cp36-abi3-win32.whl", hash = "sha256:280b67d20e33bb63171d55b1067f61fbd932e0b1ad976b3a184303a3dad22764", size = 1775641 }, + { url = "https://files.pythonhosted.org/packages/43/1d/81d59d228381576b92ecede5cd7239762c14001a828bdba30d64896e9778/pycryptodome-3.21.0-cp36-abi3-win_amd64.whl", hash = "sha256:b7aa25fc0baa5b1d95b7633af4f5f1838467f1815442b22487426f94e0d66c53", size = 1812863 }, + { url = "https://files.pythonhosted.org/packages/25/b3/09ff7072e6d96c9939c24cf51d3c389d7c345bf675420355c22402f71b68/pycryptodome-3.21.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:2cb635b67011bc147c257e61ce864879ffe6d03342dc74b6045059dfbdedafca", size = 1691593 }, + { url = "https://files.pythonhosted.org/packages/a8/91/38e43628148f68ba9b68dedbc323cf409e537fd11264031961fd7c744034/pycryptodome-3.21.0-pp27-pypy_73-win32.whl", hash = "sha256:4c26a2f0dc15f81ea3afa3b0c87b87e501f235d332b7f27e2225ecb80c0b1cdd", size = 1765997 }, +] + +[[package]] +name = "pydantic" +version = "2.10.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/0f/27908242621b14e649a84e62b133de45f84c255eecb350ab02979844a788/pydantic-2.10.3.tar.gz", hash = "sha256:cb5ac360ce894ceacd69c403187900a02c4b20b693a9dd1d643e1effab9eadf9", size = 786486 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/51/72c18c55cf2f46ff4f91ebcc8f75aa30f7305f3d726be3f4ebffb4ae972b/pydantic-2.10.3-py3-none-any.whl", hash = "sha256:be04d85bbc7b65651c5f8e6b9976ed9c6f41782a55524cef079a34a0bb82144d", size = 456997 }, +] + +[[package]] +name = "pydantic-core" +version = "2.27.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/9f/7de1f19b6aea45aeb441838782d68352e71bfa98ee6fa048d5041991b33e/pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235", size = 412785 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/39/46fe47f2ad4746b478ba89c561cafe4428e02b3573df882334bd2964f9cb/pydantic_core-2.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8", size = 1895553 }, + { url = "https://files.pythonhosted.org/packages/1c/00/0804e84a78b7fdb394fff4c4f429815a10e5e0993e6ae0e0b27dd20379ee/pydantic_core-2.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330", size = 1807220 }, + { url = "https://files.pythonhosted.org/packages/01/de/df51b3bac9820d38371f5a261020f505025df732ce566c2a2e7970b84c8c/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52", size = 1829727 }, + { url = "https://files.pythonhosted.org/packages/5f/d9/c01d19da8f9e9fbdb2bf99f8358d145a312590374d0dc9dd8dbe484a9cde/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4", size = 1854282 }, + { url = "https://files.pythonhosted.org/packages/5f/84/7db66eb12a0dc88c006abd6f3cbbf4232d26adfd827a28638c540d8f871d/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c", size = 2037437 }, + { url = "https://files.pythonhosted.org/packages/34/ac/a2537958db8299fbabed81167d58cc1506049dba4163433524e06a7d9f4c/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de", size = 2780899 }, + { url = "https://files.pythonhosted.org/packages/4a/c1/3e38cd777ef832c4fdce11d204592e135ddeedb6c6f525478a53d1c7d3e5/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025", size = 2135022 }, + { url = "https://files.pythonhosted.org/packages/7a/69/b9952829f80fd555fe04340539d90e000a146f2a003d3fcd1e7077c06c71/pydantic_core-2.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e", size = 1987969 }, + { url = "https://files.pythonhosted.org/packages/05/72/257b5824d7988af43460c4e22b63932ed651fe98804cc2793068de7ec554/pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919", size = 1994625 }, + { url = "https://files.pythonhosted.org/packages/73/c3/78ed6b7f3278a36589bcdd01243189ade7fc9b26852844938b4d7693895b/pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c", size = 2090089 }, + { url = "https://files.pythonhosted.org/packages/8d/c8/b4139b2f78579960353c4cd987e035108c93a78371bb19ba0dc1ac3b3220/pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc", size = 2142496 }, + { url = "https://files.pythonhosted.org/packages/3e/f8/171a03e97eb36c0b51981efe0f78460554a1d8311773d3d30e20c005164e/pydantic_core-2.27.1-cp311-none-win32.whl", hash = "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9", size = 1811758 }, + { url = "https://files.pythonhosted.org/packages/6a/fe/4e0e63c418c1c76e33974a05266e5633e879d4061f9533b1706a86f77d5b/pydantic_core-2.27.1-cp311-none-win_amd64.whl", hash = "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5", size = 1980864 }, + { url = "https://files.pythonhosted.org/packages/50/fc/93f7238a514c155a8ec02fc7ac6376177d449848115e4519b853820436c5/pydantic_core-2.27.1-cp311-none-win_arm64.whl", hash = "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89", size = 1864327 }, + { url = "https://files.pythonhosted.org/packages/be/51/2e9b3788feb2aebff2aa9dfbf060ec739b38c05c46847601134cc1fed2ea/pydantic_core-2.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f", size = 1895239 }, + { url = "https://files.pythonhosted.org/packages/7b/9e/f8063952e4a7d0127f5d1181addef9377505dcce3be224263b25c4f0bfd9/pydantic_core-2.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02", size = 1805070 }, + { url = "https://files.pythonhosted.org/packages/2c/9d/e1d6c4561d262b52e41b17a7ef8301e2ba80b61e32e94520271029feb5d8/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c", size = 1828096 }, + { url = "https://files.pythonhosted.org/packages/be/65/80ff46de4266560baa4332ae3181fffc4488ea7d37282da1a62d10ab89a4/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac", size = 1857708 }, + { url = "https://files.pythonhosted.org/packages/d5/ca/3370074ad758b04d9562b12ecdb088597f4d9d13893a48a583fb47682cdf/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb", size = 2037751 }, + { url = "https://files.pythonhosted.org/packages/b1/e2/4ab72d93367194317b99d051947c071aef6e3eb95f7553eaa4208ecf9ba4/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529", size = 2733863 }, + { url = "https://files.pythonhosted.org/packages/8a/c6/8ae0831bf77f356bb73127ce5a95fe115b10f820ea480abbd72d3cc7ccf3/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35", size = 2161161 }, + { url = "https://files.pythonhosted.org/packages/f1/f4/b2fe73241da2429400fc27ddeaa43e35562f96cf5b67499b2de52b528cad/pydantic_core-2.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089", size = 1993294 }, + { url = "https://files.pythonhosted.org/packages/77/29/4bb008823a7f4cc05828198153f9753b3bd4c104d93b8e0b1bfe4e187540/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381", size = 2001468 }, + { url = "https://files.pythonhosted.org/packages/f2/a9/0eaceeba41b9fad851a4107e0cf999a34ae8f0d0d1f829e2574f3d8897b0/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb", size = 2091413 }, + { url = "https://files.pythonhosted.org/packages/d8/36/eb8697729725bc610fd73940f0d860d791dc2ad557faaefcbb3edbd2b349/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae", size = 2154735 }, + { url = "https://files.pythonhosted.org/packages/52/e5/4f0fbd5c5995cc70d3afed1b5c754055bb67908f55b5cb8000f7112749bf/pydantic_core-2.27.1-cp312-none-win32.whl", hash = "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c", size = 1833633 }, + { url = "https://files.pythonhosted.org/packages/ee/f2/c61486eee27cae5ac781305658779b4a6b45f9cc9d02c90cb21b940e82cc/pydantic_core-2.27.1-cp312-none-win_amd64.whl", hash = "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16", size = 1986973 }, + { url = "https://files.pythonhosted.org/packages/df/a6/e3f12ff25f250b02f7c51be89a294689d175ac76e1096c32bf278f29ca1e/pydantic_core-2.27.1-cp312-none-win_arm64.whl", hash = "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e", size = 1883215 }, + { url = "https://files.pythonhosted.org/packages/0f/d6/91cb99a3c59d7b072bded9959fbeab0a9613d5a4935773c0801f1764c156/pydantic_core-2.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073", size = 1895033 }, + { url = "https://files.pythonhosted.org/packages/07/42/d35033f81a28b27dedcade9e967e8a40981a765795c9ebae2045bcef05d3/pydantic_core-2.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08", size = 1807542 }, + { url = "https://files.pythonhosted.org/packages/41/c2/491b59e222ec7e72236e512108ecad532c7f4391a14e971c963f624f7569/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf", size = 1827854 }, + { url = "https://files.pythonhosted.org/packages/e3/f3/363652651779113189cefdbbb619b7b07b7a67ebb6840325117cc8cc3460/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737", size = 1857389 }, + { url = "https://files.pythonhosted.org/packages/5f/97/be804aed6b479af5a945daec7538d8bf358d668bdadde4c7888a2506bdfb/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2", size = 2037934 }, + { url = "https://files.pythonhosted.org/packages/42/01/295f0bd4abf58902917e342ddfe5f76cf66ffabfc57c2e23c7681a1a1197/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107", size = 2735176 }, + { url = "https://files.pythonhosted.org/packages/9d/a0/cd8e9c940ead89cc37812a1a9f310fef59ba2f0b22b4e417d84ab09fa970/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51", size = 2160720 }, + { url = "https://files.pythonhosted.org/packages/73/ae/9d0980e286627e0aeca4c352a60bd760331622c12d576e5ea4441ac7e15e/pydantic_core-2.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a", size = 1992972 }, + { url = "https://files.pythonhosted.org/packages/bf/ba/ae4480bc0292d54b85cfb954e9d6bd226982949f8316338677d56541b85f/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc", size = 2001477 }, + { url = "https://files.pythonhosted.org/packages/55/b7/e26adf48c2f943092ce54ae14c3c08d0d221ad34ce80b18a50de8ed2cba8/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960", size = 2091186 }, + { url = "https://files.pythonhosted.org/packages/ba/cc/8491fff5b608b3862eb36e7d29d36a1af1c945463ca4c5040bf46cc73f40/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23", size = 2154429 }, + { url = "https://files.pythonhosted.org/packages/78/d8/c080592d80edd3441ab7f88f865f51dae94a157fc64283c680e9f32cf6da/pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05", size = 1833713 }, + { url = "https://files.pythonhosted.org/packages/83/84/5ab82a9ee2538ac95a66e51f6838d6aba6e0a03a42aa185ad2fe404a4e8f/pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337", size = 1987897 }, + { url = "https://files.pythonhosted.org/packages/df/c3/b15fb833926d91d982fde29c0624c9f225da743c7af801dace0d4e187e71/pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5", size = 1882983 }, +] + +[[package]] +name = "pygments" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 }, +] + +[[package]] +name = "pynacl" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/22/27582568be639dfe22ddb3902225f91f2f17ceff88ce80e4db396c8986da/PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba", size = 3392854 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/75/0b8ede18506041c0bf23ac4d8e2971b4161cd6ce630b177d0a08eb0d8857/PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1", size = 349920 }, + { url = "https://files.pythonhosted.org/packages/59/bb/fddf10acd09637327a97ef89d2a9d621328850a72f1fdc8c08bdf72e385f/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92", size = 601722 }, + { url = "https://files.pythonhosted.org/packages/5d/70/87a065c37cca41a75f2ce113a5a2c2aa7533be648b184ade58971b5f7ccc/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394", size = 680087 }, + { url = "https://files.pythonhosted.org/packages/ee/87/f1bb6a595f14a327e8285b9eb54d41fef76c585a0edef0a45f6fc95de125/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d", size = 856678 }, + { url = "https://files.pythonhosted.org/packages/66/28/ca86676b69bf9f90e710571b67450508484388bfce09acf8a46f0b8c785f/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858", size = 1133660 }, + { url = "https://files.pythonhosted.org/packages/3d/85/c262db650e86812585e2bc59e497a8f59948a005325a11bbbc9ecd3fe26b/PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b", size = 663824 }, + { url = "https://files.pythonhosted.org/packages/fd/1a/cc308a884bd299b651f1633acb978e8596c71c33ca85e9dc9fa33a5399b9/PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff", size = 1117912 }, + { url = "https://files.pythonhosted.org/packages/25/2d/b7df6ddb0c2a33afdb358f8af6ea3b8c4d1196ca45497dd37a56f0c122be/PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543", size = 204624 }, + { url = "https://files.pythonhosted.org/packages/5e/22/d3db169895faaf3e2eda892f005f433a62db2decbcfbc2f61e6517adfa87/PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93", size = 212141 }, +] + +[[package]] +name = "pytest" +version = "8.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, +] + +[[package]] +name = "python-dotenv" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, +] + +[[package]] +name = "python-levenshtein" +version = "0.26.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "levenshtein" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/31/72/58d77cb80b3c130d94f53a8204ffad9acfddb925b2fb5818ff9af0b3c832/python_levenshtein-0.26.1.tar.gz", hash = "sha256:24ba578e28058ebb4afa2700057e1678d7adf27e43cd1f17700c09a9009d5d3a", size = 12276 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/d7/03e0453719ed89724664f781f0255949408118093dbf77a2aa2a1198b38e/python_Levenshtein-0.26.1-py3-none-any.whl", hash = "sha256:8ef5e529dd640fb00f05ee62d998d2ee862f19566b641ace775d5ae16167b2ef", size = 9426 }, +] + +[[package]] +name = "python-statemachine" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/91/4f05f3931d1e9b1df71b17dc08c43feddf2bed7dbf13f95323df2cc8e340/python_statemachine-2.5.0.tar.gz", hash = "sha256:ae88cd22e47930b92b983a2176e61d811e571b69897be2568ec812c2885fb93a", size = 403718 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/2d/1c95ebe84df60d630f8e855d1df2c66368805444ac167e9b50f29eabe917/python_statemachine-2.5.0-py3-none-any.whl", hash = "sha256:0ed53846802c17037fcb2a92323f4bc0c833290fa9d17a3587c50886c1541e62", size = 50415 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, +] + +[[package]] +name = "rapidfuzz" +version = "3.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/39/e3bcb901c2746734cd70151253bf9e61c688d3c415227b08e6fbf7eb5d7f/rapidfuzz-3.10.1.tar.gz", hash = "sha256:5a15546d847a915b3f42dc79ef9b0c78b998b4e2c53b252e7166284066585979", size = 57982250 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/2c/62efddd64bcaf39c03b928784777bb614028c5975ec7465d34eded34a7f7/rapidfuzz-3.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:92958ae075c87fef393f835ed02d4fe8d5ee2059a0934c6c447ea3417dfbf0e8", size = 1954920 }, + { url = "https://files.pythonhosted.org/packages/41/a7/f8411b9b4037d1ea6707dee975e4ed6b5358192f5ba7aa544610df5c7522/rapidfuzz-3.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ba7521e072c53e33c384e78615d0718e645cab3c366ecd3cc8cb732befd94967", size = 1427745 }, + { url = "https://files.pythonhosted.org/packages/0d/69/ddd0192b64cb55bca40ebcae48480fab0148334b9995eb9d5bd78b7333f6/rapidfuzz-3.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00d02cbd75d283c287471b5b3738b3e05c9096150f93f2d2dfa10b3d700f2db9", size = 1409233 }, + { url = "https://files.pythonhosted.org/packages/18/7d/0655a52c31227bf2880f28d3c01cc4f20b584210f849a1953e4c734599e5/rapidfuzz-3.10.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:efa1582a397da038e2f2576c9cd49b842f56fde37d84a6b0200ffebc08d82350", size = 5609458 }, + { url = "https://files.pythonhosted.org/packages/0b/c5/5f18cd956fcf95cbdee054cd4f7b7042eacc1430f6682fae0859deb9694b/rapidfuzz-3.10.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f12912acee1f506f974f58de9fdc2e62eea5667377a7e9156de53241c05fdba8", size = 1675729 }, + { url = "https://files.pythonhosted.org/packages/82/67/cf9f25a2dc02f8170c1c0b7f6d41aa39b0f28c3cd54140ec3cab315cbdf0/rapidfuzz-3.10.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666d5d8b17becc3f53447bcb2b6b33ce6c2df78792495d1fa82b2924cd48701a", size = 1678147 }, + { url = "https://files.pythonhosted.org/packages/ac/3d/fa8444d7144129b1c67a2ba0660b44af03285fd641516ee294593d2acb91/rapidfuzz-3.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26f71582c0d62445067ee338ddad99b655a8f4e4ed517a90dcbfbb7d19310474", size = 3129309 }, + { url = "https://files.pythonhosted.org/packages/81/f6/a9fc68b776273282d6aeaadc6330740328bac29f8746fe8cceb9155e904a/rapidfuzz-3.10.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8a2ef08b27167bcff230ffbfeedd4c4fa6353563d6aaa015d725dd3632fc3de7", size = 2339967 }, + { url = "https://files.pythonhosted.org/packages/17/e5/f6c99fefbacef3676394b09ee66782d72710911c971c8730ef602e21fbd1/rapidfuzz-3.10.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:365e4fc1a2b95082c890f5e98489b894e6bf8c338c6ac89bb6523c2ca6e9f086", size = 6943002 }, + { url = "https://files.pythonhosted.org/packages/ee/ab/92c97b37ee24f68e2f904d8ef658bcfa47e3caf4d8491aa8bc5314704fc4/rapidfuzz-3.10.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1996feb7a61609fa842e6b5e0c549983222ffdedaf29644cc67e479902846dfe", size = 2717032 }, + { url = "https://files.pythonhosted.org/packages/20/f9/894a20e7856c9b29fd746ffca8f8360df8e4027b503ac5475439c043137f/rapidfuzz-3.10.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:cf654702f144beaa093103841a2ea6910d617d0bb3fccb1d1fd63c54dde2cd49", size = 3263149 }, + { url = "https://files.pythonhosted.org/packages/db/69/2a00d3c7d29d084311b1ab0fc83ba228ce81f78e9a60f901d64c74c0f31e/rapidfuzz-3.10.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec108bf25de674781d0a9a935030ba090c78d49def3d60f8724f3fc1e8e75024", size = 4176326 }, + { url = "https://files.pythonhosted.org/packages/bd/27/0cef6ddfd7b163b99b40a7fb1b1c15e0c9d25ec8f528b9f0af9dc2b980a2/rapidfuzz-3.10.1-cp311-cp311-win32.whl", hash = "sha256:031f8b367e5d92f7a1e27f7322012f3c321c3110137b43cc3bf678505583ef48", size = 1835384 }, + { url = "https://files.pythonhosted.org/packages/fc/0b/b15a8853672e6fca00d83b3a6c037c07ff16a73932a55e69488c46e6b9d7/rapidfuzz-3.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:f98f36c6a1bb9a6c8bbec99ad87c8c0e364f34761739b5ea9adf7b48129ae8cf", size = 1614933 }, + { url = "https://files.pythonhosted.org/packages/95/8a/6057b41a8a6a2245a699c1beff62baa1021543e953e05dbdb355b953f886/rapidfuzz-3.10.1-cp311-cp311-win_arm64.whl", hash = "sha256:f1da2028cb4e41be55ee797a82d6c1cf589442504244249dfeb32efc608edee7", size = 845810 }, + { url = "https://files.pythonhosted.org/packages/77/e9/a7981ad1a7fbe4d76aa4fbbc8f2d6aac289ab62e60173f92fd3e05619f25/rapidfuzz-3.10.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1340b56340896bede246f612b6ecf685f661a56aabef3d2512481bfe23ac5835", size = 1938706 }, + { url = "https://files.pythonhosted.org/packages/bd/2b/f343df6ae726d01aa31c5ff63f2a5807dfeffa671ebf2fb9be8f8b9b2026/rapidfuzz-3.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2316515169b7b5a453f0ce3adbc46c42aa332cae9f2edb668e24d1fc92b2f2bb", size = 1423814 }, + { url = "https://files.pythonhosted.org/packages/13/07/6accf77b78772de2a5590ef7965d3b7c9997c5a92e913e525765586aa261/rapidfuzz-3.10.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e06fe6a12241ec1b72c0566c6b28cda714d61965d86569595ad24793d1ab259", size = 1393680 }, + { url = "https://files.pythonhosted.org/packages/46/16/2a016051489f870d15f7cdcccf823ea5f474453dda5c20cf0044ed3122d5/rapidfuzz-3.10.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d99c1cd9443b19164ec185a7d752f4b4db19c066c136f028991a480720472e23", size = 5545438 }, + { url = "https://files.pythonhosted.org/packages/97/0b/2cdafff5dcb06ed6ede6f81a2f677c1f4cc08a47a6cf16862eb62903a84b/rapidfuzz-3.10.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1d9aa156ed52d3446388ba4c2f335e312191d1ca9d1f5762ee983cf23e4ecf6", size = 1646447 }, + { url = "https://files.pythonhosted.org/packages/97/65/20a859278192ca036ead255dda49f4eac63dd8d666b3a902d7be3afd38ac/rapidfuzz-3.10.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54bcf4efaaee8e015822be0c2c28214815f4f6b4f70d8362cfecbd58a71188ac", size = 1672282 }, + { url = "https://files.pythonhosted.org/packages/3c/05/b8dcfbdc8f4e3e84abf86ea13ec9595ebaf7e5375011e5d49642705704ad/rapidfuzz-3.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0c955e32afdbfdf6e9ee663d24afb25210152d98c26d22d399712d29a9b976b", size = 3126089 }, + { url = "https://files.pythonhosted.org/packages/3f/eb/e2f5b1643cf463b1b23c36875e711cae0091f6aaa1538c025a12cba32634/rapidfuzz-3.10.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:191633722203f5b7717efcb73a14f76f3b124877d0608c070b827c5226d0b972", size = 2300501 }, + { url = "https://files.pythonhosted.org/packages/7c/28/f3aa5d3a56cc978e73baff951549425d1a722ec3b58cacbc74c4faad2127/rapidfuzz-3.10.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:195baad28057ec9609e40385991004e470af9ef87401e24ebe72c064431524ab", size = 6903454 }, + { url = "https://files.pythonhosted.org/packages/b8/66/ba6de8c1fe5c50230d4e0adb87dde39b143ac2a4ce959a3f8076266b1767/rapidfuzz-3.10.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0fff4a6b87c07366662b62ae994ffbeadc472e72f725923f94b72a3db49f4671", size = 2681137 }, + { url = "https://files.pythonhosted.org/packages/e8/ca/4e9dbc9bca8cd1b933cfc6f961179f883cead90689619ec0aa1a5f075b0e/rapidfuzz-3.10.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4ffed25f9fdc0b287f30a98467493d1e1ce5b583f6317f70ec0263b3c97dbba6", size = 3230482 }, + { url = "https://files.pythonhosted.org/packages/14/50/6484ce7091b815757d6f0c434b78b568d3e7a80b6145a2d9aadc65b16132/rapidfuzz-3.10.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d02cf8e5af89a9ac8f53c438ddff6d773f62c25c6619b29db96f4aae248177c0", size = 4147386 }, + { url = "https://files.pythonhosted.org/packages/0b/27/9f7a0dbdd5b478790c68297b0678bc0088b9068e667878e5d1359c3ffba0/rapidfuzz-3.10.1-cp312-cp312-win32.whl", hash = "sha256:f3bb81d4fe6a5d20650f8c0afcc8f6e1941f6fecdb434f11b874c42467baded0", size = 1818115 }, + { url = "https://files.pythonhosted.org/packages/58/b6/c5f5e8043052fdbd4aa4f41d93b0e72d089172237ed5ec42118683a9833a/rapidfuzz-3.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:aaf83e9170cb1338922ae42d320699dccbbdca8ffed07faeb0b9257822c26e24", size = 1600653 }, + { url = "https://files.pythonhosted.org/packages/56/d3/dd84c7ed88cd4391e78b3579ecf7141ebf8b900097da2d6911db148d4bb6/rapidfuzz-3.10.1-cp312-cp312-win_arm64.whl", hash = "sha256:c5da802a0d085ad81b0f62828fb55557996c497b2d0b551bbdfeafd6d447892f", size = 840363 }, + { url = "https://files.pythonhosted.org/packages/2f/7a/18aa6a51345e46886784e90a2c5bba62e45ee3adc554c12c3b97c297c4c3/rapidfuzz-3.10.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fc22d69a1c9cccd560a5c434c0371b2df0f47c309c635a01a913e03bbf183710", size = 1931333 }, + { url = "https://files.pythonhosted.org/packages/6f/6a/7e34ddc3d6d751c4dba0d58b681c99f161225730e9a2fa71969d2fa1d281/rapidfuzz-3.10.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38b0dac2c8e057562b8f0d8ae5b663d2d6a28c5ab624de5b73cef9abb6129a24", size = 1417685 }, + { url = "https://files.pythonhosted.org/packages/ca/15/93a2eafbb4cc563d72112e4717b8c6f09e9de15f5a7709b832b8c9b81196/rapidfuzz-3.10.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fde3bbb14e92ce8fcb5c2edfff72e474d0080cadda1c97785bf4822f037a309", size = 1388805 }, + { url = "https://files.pythonhosted.org/packages/82/23/541da9279b21fc380e89e49e5acd863ba2e2b5d483eb5b6e0cfc427e4540/rapidfuzz-3.10.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9141fb0592e55f98fe9ac0f3ce883199b9c13e262e0bf40c5b18cdf926109d16", size = 5515246 }, + { url = "https://files.pythonhosted.org/packages/f3/a0/f0e43fdaf3c3c1907aa377d3610c70b31830e4d6915b8a61b51b064fcbce/rapidfuzz-3.10.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:237bec5dd1bfc9b40bbd786cd27949ef0c0eb5fab5eb491904c6b5df59d39d3c", size = 1642160 }, + { url = "https://files.pythonhosted.org/packages/a7/da/7091eef23291997e7c379a396eedbac66a50a145200cac86a0313286403d/rapidfuzz-3.10.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18123168cba156ab5794ea6de66db50f21bb3c66ae748d03316e71b27d907b95", size = 1664562 }, + { url = "https://files.pythonhosted.org/packages/bd/72/417ca8b5dde3c040c1cab1d5500fd24ffdf1a397cb86e36e958acb07cd65/rapidfuzz-3.10.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b75fe506c8e02769cc47f5ab21ce3e09b6211d3edaa8f8f27331cb6988779be", size = 3106304 }, + { url = "https://files.pythonhosted.org/packages/57/18/0877c12deb79cee67f6b8fbb662e2d038582d0e26d895ddbfdb88cea7e17/rapidfuzz-3.10.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9da82aa4b46973aaf9e03bb4c3d6977004648c8638febfc0f9d237e865761270", size = 2302688 }, + { url = "https://files.pythonhosted.org/packages/c2/71/ca9e092c6d904f9fabadac6361e52a484165ee5970f34e4dc70a647f36f3/rapidfuzz-3.10.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c34c022d5ad564f1a5a57a4a89793bd70d7bad428150fb8ff2760b223407cdcf", size = 6893082 }, + { url = "https://files.pythonhosted.org/packages/c3/4c/99004b6ae04ead73d1e91829a78d9708c3c707aa83c1e782ea89f1ade491/rapidfuzz-3.10.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1e96c84d6c2a0ca94e15acb5399118fff669f4306beb98a6d8ec6f5dccab4412", size = 2669909 }, + { url = "https://files.pythonhosted.org/packages/cb/7c/d5c93a0e497a0430b4f0bfea22e41317c22357cd557fa9aeeafb9e991d9b/rapidfuzz-3.10.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e8e154b84a311263e1aca86818c962e1fa9eefdd643d1d5d197fcd2738f88cb9", size = 3223759 }, + { url = "https://files.pythonhosted.org/packages/d6/77/2c22f438b643524b429731492665d91d9c654144e895f0051cee78d5928d/rapidfuzz-3.10.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:335fee93188f8cd585552bb8057228ce0111bd227fa81bfd40b7df6b75def8ab", size = 4148589 }, + { url = "https://files.pythonhosted.org/packages/bf/d3/51cc9f258b362fca8ced7c34046b66d8887551da0169c06c27ee8d2ce279/rapidfuzz-3.10.1-cp313-cp313-win32.whl", hash = "sha256:6729b856166a9e95c278410f73683957ea6100c8a9d0a8dbe434c49663689255", size = 1816180 }, + { url = "https://files.pythonhosted.org/packages/9d/9d/a69358047742dbc94516c71c07cfab4409d490578815c875949011e3f482/rapidfuzz-3.10.1-cp313-cp313-win_amd64.whl", hash = "sha256:0e06d99ad1ad97cb2ef7f51ec6b1fedd74a3a700e4949353871cf331d07b382a", size = 1598626 }, + { url = "https://files.pythonhosted.org/packages/48/3c/8213b3216b542f3bd43051dc5a1c44e0cd741abb97bde064e89f241c5a82/rapidfuzz-3.10.1-cp313-cp313-win_arm64.whl", hash = "sha256:8d1b7082104d596a3eb012e0549b2634ed15015b569f48879701e9d8db959dbb", size = 839138 }, +] + +[[package]] +name = "regex" +version = "2024.11.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/58/7e4d9493a66c88a7da6d205768119f51af0f684fe7be7bac8328e217a52c/regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638", size = 482669 }, + { url = "https://files.pythonhosted.org/packages/34/4c/8f8e631fcdc2ff978609eaeef1d6994bf2f028b59d9ac67640ed051f1218/regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7", size = 287684 }, + { url = "https://files.pythonhosted.org/packages/c5/1b/f0e4d13e6adf866ce9b069e191f303a30ab1277e037037a365c3aad5cc9c/regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20", size = 284589 }, + { url = "https://files.pythonhosted.org/packages/25/4d/ab21047f446693887f25510887e6820b93f791992994f6498b0318904d4a/regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114", size = 792121 }, + { url = "https://files.pythonhosted.org/packages/45/ee/c867e15cd894985cb32b731d89576c41a4642a57850c162490ea34b78c3b/regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3", size = 831275 }, + { url = "https://files.pythonhosted.org/packages/b3/12/b0f480726cf1c60f6536fa5e1c95275a77624f3ac8fdccf79e6727499e28/regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f", size = 818257 }, + { url = "https://files.pythonhosted.org/packages/bf/ce/0d0e61429f603bac433910d99ef1a02ce45a8967ffbe3cbee48599e62d88/regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0", size = 792727 }, + { url = "https://files.pythonhosted.org/packages/e4/c1/243c83c53d4a419c1556f43777ccb552bccdf79d08fda3980e4e77dd9137/regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55", size = 780667 }, + { url = "https://files.pythonhosted.org/packages/c5/f4/75eb0dd4ce4b37f04928987f1d22547ddaf6c4bae697623c1b05da67a8aa/regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89", size = 776963 }, + { url = "https://files.pythonhosted.org/packages/16/5d/95c568574e630e141a69ff8a254c2f188b4398e813c40d49228c9bbd9875/regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d", size = 784700 }, + { url = "https://files.pythonhosted.org/packages/8e/b5/f8495c7917f15cc6fee1e7f395e324ec3e00ab3c665a7dc9d27562fd5290/regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34", size = 848592 }, + { url = "https://files.pythonhosted.org/packages/1c/80/6dd7118e8cb212c3c60b191b932dc57db93fb2e36fb9e0e92f72a5909af9/regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d", size = 852929 }, + { url = "https://files.pythonhosted.org/packages/11/9b/5a05d2040297d2d254baf95eeeb6df83554e5e1df03bc1a6687fc4ba1f66/regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45", size = 781213 }, + { url = "https://files.pythonhosted.org/packages/26/b7/b14e2440156ab39e0177506c08c18accaf2b8932e39fb092074de733d868/regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9", size = 261734 }, + { url = "https://files.pythonhosted.org/packages/80/32/763a6cc01d21fb3819227a1cc3f60fd251c13c37c27a73b8ff4315433a8e/regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60", size = 274052 }, + { url = "https://files.pythonhosted.org/packages/ba/30/9a87ce8336b172cc232a0db89a3af97929d06c11ceaa19d97d84fa90a8f8/regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", size = 483781 }, + { url = "https://files.pythonhosted.org/packages/01/e8/00008ad4ff4be8b1844786ba6636035f7ef926db5686e4c0f98093612add/regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", size = 288455 }, + { url = "https://files.pythonhosted.org/packages/60/85/cebcc0aff603ea0a201667b203f13ba75d9fc8668fab917ac5b2de3967bc/regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", size = 284759 }, + { url = "https://files.pythonhosted.org/packages/94/2b/701a4b0585cb05472a4da28ee28fdfe155f3638f5e1ec92306d924e5faf0/regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", size = 794976 }, + { url = "https://files.pythonhosted.org/packages/4b/bf/fa87e563bf5fee75db8915f7352e1887b1249126a1be4813837f5dbec965/regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", size = 833077 }, + { url = "https://files.pythonhosted.org/packages/a1/56/7295e6bad94b047f4d0834e4779491b81216583c00c288252ef625c01d23/regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", size = 823160 }, + { url = "https://files.pythonhosted.org/packages/fb/13/e3b075031a738c9598c51cfbc4c7879e26729c53aa9cca59211c44235314/regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", size = 796896 }, + { url = "https://files.pythonhosted.org/packages/24/56/0b3f1b66d592be6efec23a795b37732682520b47c53da5a32c33ed7d84e3/regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", size = 783997 }, + { url = "https://files.pythonhosted.org/packages/f9/a1/eb378dada8b91c0e4c5f08ffb56f25fcae47bf52ad18f9b2f33b83e6d498/regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", size = 781725 }, + { url = "https://files.pythonhosted.org/packages/83/f2/033e7dec0cfd6dda93390089864732a3409246ffe8b042e9554afa9bff4e/regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", size = 789481 }, + { url = "https://files.pythonhosted.org/packages/83/23/15d4552ea28990a74e7696780c438aadd73a20318c47e527b47a4a5a596d/regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", size = 852896 }, + { url = "https://files.pythonhosted.org/packages/e3/39/ed4416bc90deedbfdada2568b2cb0bc1fdb98efe11f5378d9892b2a88f8f/regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", size = 860138 }, + { url = "https://files.pythonhosted.org/packages/93/2d/dd56bb76bd8e95bbce684326302f287455b56242a4f9c61f1bc76e28360e/regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", size = 787692 }, + { url = "https://files.pythonhosted.org/packages/0b/55/31877a249ab7a5156758246b9c59539abbeba22461b7d8adc9e8475ff73e/regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", size = 262135 }, + { url = "https://files.pythonhosted.org/packages/38/ec/ad2d7de49a600cdb8dd78434a1aeffe28b9d6fc42eb36afab4a27ad23384/regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", size = 273567 }, + { url = "https://files.pythonhosted.org/packages/90/73/bcb0e36614601016552fa9344544a3a2ae1809dc1401b100eab02e772e1f/regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84", size = 483525 }, + { url = "https://files.pythonhosted.org/packages/0f/3f/f1a082a46b31e25291d830b369b6b0c5576a6f7fb89d3053a354c24b8a83/regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4", size = 288324 }, + { url = "https://files.pythonhosted.org/packages/09/c9/4e68181a4a652fb3ef5099e077faf4fd2a694ea6e0f806a7737aff9e758a/regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0", size = 284617 }, + { url = "https://files.pythonhosted.org/packages/fc/fd/37868b75eaf63843165f1d2122ca6cb94bfc0271e4428cf58c0616786dce/regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0", size = 795023 }, + { url = "https://files.pythonhosted.org/packages/c4/7c/d4cd9c528502a3dedb5c13c146e7a7a539a3853dc20209c8e75d9ba9d1b2/regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7", size = 833072 }, + { url = "https://files.pythonhosted.org/packages/4f/db/46f563a08f969159c5a0f0e722260568425363bea43bb7ae370becb66a67/regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7", size = 823130 }, + { url = "https://files.pythonhosted.org/packages/db/60/1eeca2074f5b87df394fccaa432ae3fc06c9c9bfa97c5051aed70e6e00c2/regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c", size = 796857 }, + { url = "https://files.pythonhosted.org/packages/10/db/ac718a08fcee981554d2f7bb8402f1faa7e868c1345c16ab1ebec54b0d7b/regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3", size = 784006 }, + { url = "https://files.pythonhosted.org/packages/c2/41/7da3fe70216cea93144bf12da2b87367590bcf07db97604edeea55dac9ad/regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07", size = 781650 }, + { url = "https://files.pythonhosted.org/packages/a7/d5/880921ee4eec393a4752e6ab9f0fe28009435417c3102fc413f3fe81c4e5/regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e", size = 789545 }, + { url = "https://files.pythonhosted.org/packages/dc/96/53770115e507081122beca8899ab7f5ae28ae790bfcc82b5e38976df6a77/regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6", size = 853045 }, + { url = "https://files.pythonhosted.org/packages/31/d3/1372add5251cc2d44b451bd94f43b2ec78e15a6e82bff6a290ef9fd8f00a/regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4", size = 860182 }, + { url = "https://files.pythonhosted.org/packages/ed/e3/c446a64984ea9f69982ba1a69d4658d5014bc7a0ea468a07e1a1265db6e2/regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", size = 787733 }, + { url = "https://files.pythonhosted.org/packages/2b/f1/e40c8373e3480e4f29f2692bd21b3e05f296d3afebc7e5dcf21b9756ca1c/regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", size = 262122 }, + { url = "https://files.pythonhosted.org/packages/45/94/bc295babb3062a731f52621cdc992d123111282e291abaf23faa413443ea/regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", size = 273545 }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + +[[package]] +name = "retry" +version = "0.9.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "decorator" }, + { name = "py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/72/75d0b85443fbc8d9f38d08d2b1b67cc184ce35280e4a3813cda2f445f3a4/retry-0.9.2.tar.gz", hash = "sha256:f8bfa8b99b69c4506d6f5bd3b0aabf77f98cdb17f3c9fc3f5ca820033336fba4", size = 6448 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/0d/53aea75710af4528a25ed6837d71d117602b01946b307a3912cb3cfcbcba/retry-0.9.2-py2.py3-none-any.whl", hash = "sha256:ccddf89761fa2c726ab29391837d4327f819ea14d244c232a1d24c67a2f98606", size = 7986 }, +] + +[[package]] +name = "rich" +version = "13.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, +] + +[[package]] +name = "s3transfer" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/0a/1cdbabf9edd0ea7747efdf6c9ab4e7061b085aa7f9bfc36bb1601563b069/s3transfer-0.10.4.tar.gz", hash = "sha256:29edc09801743c21eb5ecbc617a152df41d3c287f67b615f73e5f750583666a7", size = 145287 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/05/7957af15543b8c9799209506df4660cba7afc4cf94bfb60513827e96bed6/s3transfer-0.10.4-py3-none-any.whl", hash = "sha256:244a76a24355363a68164241438de1b72f8781664920260c48465896b712a41e", size = 83175 }, +] + +[[package]] +name = "safetensors" +version = "0.4.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/46/a1c56ed856c6ac3b1a8b37abe5be0cac53219367af1331e721b04d122577/safetensors-0.4.5.tar.gz", hash = "sha256:d73de19682deabb02524b3d5d1f8b3aaba94c72f1bbfc7911b9b9d5d391c0310", size = 65702 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/a5/25bcf75e373412daf1fd88045ab3aa8140a0d804ef0e70712c4f2c5b94d8/safetensors-0.4.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:21f848d7aebd5954f92538552d6d75f7c1b4500f51664078b5b49720d180e47c", size = 392256 }, + { url = "https://files.pythonhosted.org/packages/08/8c/ece3bf8756506a890bd980eca02f47f9d98dfbf5ce16eda1368f53560f67/safetensors-0.4.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bb07000b19d41e35eecef9a454f31a8b4718a185293f0d0b1c4b61d6e4487971", size = 381490 }, + { url = "https://files.pythonhosted.org/packages/39/83/c4a7ce01d626e46ea2b45887f2e59b16441408031e2ce2f9fe01860c6946/safetensors-0.4.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09dedf7c2fda934ee68143202acff6e9e8eb0ddeeb4cfc24182bef999efa9f42", size = 441093 }, + { url = "https://files.pythonhosted.org/packages/47/26/cc52de647e71bd9a0b0d78ead0d31d9c462b35550a817aa9e0cab51d6db4/safetensors-0.4.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:59b77e4b7a708988d84f26de3ebead61ef1659c73dcbc9946c18f3b1786d2688", size = 438960 }, + { url = "https://files.pythonhosted.org/packages/06/78/332538546775ee97e749867df2d58f2282d9c48a1681e4891eed8b94ec94/safetensors-0.4.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d3bc83e14d67adc2e9387e511097f254bd1b43c3020440e708858c684cbac68", size = 478031 }, + { url = "https://files.pythonhosted.org/packages/d9/03/a3c8663f1ddda54e624ecf43fce651659b49e8e1603c52c3e464b442acfa/safetensors-0.4.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39371fc551c1072976073ab258c3119395294cf49cdc1f8476794627de3130df", size = 494754 }, + { url = "https://files.pythonhosted.org/packages/e6/ee/69e498a892f208bd1da4104d4b9be887f8611bf4942144718b6738482250/safetensors-0.4.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6c19feda32b931cae0acd42748a670bdf56bee6476a046af20181ad3fee4090", size = 435013 }, + { url = "https://files.pythonhosted.org/packages/a2/61/f0cfce984515b86d1260f556ba3b782158e2855e6a318446ac2613786fa9/safetensors-0.4.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a659467495de201e2f282063808a41170448c78bada1e62707b07a27b05e6943", size = 455984 }, + { url = "https://files.pythonhosted.org/packages/e7/a9/3e3b48fcaade3eb4e347d39ebf0bd44291db21a3e4507854b42a7cb910ac/safetensors-0.4.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bad5e4b2476949bcd638a89f71b6916fa9a5cae5c1ae7eede337aca2100435c0", size = 619513 }, + { url = "https://files.pythonhosted.org/packages/80/23/2a7a1be24258c0e44c1d356896fd63dc0545a98d2d0184925fa09cd3ec76/safetensors-0.4.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a3a315a6d0054bc6889a17f5668a73f94f7fe55121ff59e0a199e3519c08565f", size = 604841 }, + { url = "https://files.pythonhosted.org/packages/b4/5c/34d082ff1fffffd8545fb22cbae3285ab4236f1f0cfc64b7e58261c2363b/safetensors-0.4.5-cp311-none-win32.whl", hash = "sha256:a01e232e6d3d5cf8b1667bc3b657a77bdab73f0743c26c1d3c5dd7ce86bd3a92", size = 272602 }, + { url = "https://files.pythonhosted.org/packages/6d/41/948c96c8a7e9fef57c2e051f1871c108a6dbbc6d285598bdb1d89b98617c/safetensors-0.4.5-cp311-none-win_amd64.whl", hash = "sha256:cbd39cae1ad3e3ef6f63a6f07296b080c951f24cec60188378e43d3713000c04", size = 285973 }, + { url = "https://files.pythonhosted.org/packages/bf/ac/5a63082f931e99200db95fd46fb6734f050bb6e96bf02521904c6518b7aa/safetensors-0.4.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:473300314e026bd1043cef391bb16a8689453363381561b8a3e443870937cc1e", size = 392015 }, + { url = "https://files.pythonhosted.org/packages/73/95/ab32aa6e9bdc832ff87784cdf9da26192b93de3ef82b8d1ada8f345c5044/safetensors-0.4.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:801183a0f76dc647f51a2d9141ad341f9665602a7899a693207a82fb102cc53e", size = 381774 }, + { url = "https://files.pythonhosted.org/packages/d6/6c/7e04b7626809fc63f3698f4c50e43aff2864b40089aa4506c918a75b8eed/safetensors-0.4.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1524b54246e422ad6fb6aea1ac71edeeb77666efa67230e1faf6999df9b2e27f", size = 441134 }, + { url = "https://files.pythonhosted.org/packages/58/2b/ffe7c86a277e6c1595fbdf415cfe2903f253f574a5405e93fda8baaa582c/safetensors-0.4.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b3139098e3e8b2ad7afbca96d30ad29157b50c90861084e69fcb80dec7430461", size = 438467 }, + { url = "https://files.pythonhosted.org/packages/67/9c/f271bd804e08c7fda954d17b70ff281228a88077337a9e70feace4f4cc93/safetensors-0.4.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65573dc35be9059770808e276b017256fa30058802c29e1038eb1c00028502ea", size = 476566 }, + { url = "https://files.pythonhosted.org/packages/4c/ad/4cf76a3e430a8a26108407fa6cb93e6f80d996a5cb75d9540c8fe3862990/safetensors-0.4.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd33da8e9407559f8779c82a0448e2133737f922d71f884da27184549416bfed", size = 492253 }, + { url = "https://files.pythonhosted.org/packages/d9/40/a6f75ea449a9647423ec8b6f72c16998d35aa4b43cb38536ac060c5c7bf5/safetensors-0.4.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3685ce7ed036f916316b567152482b7e959dc754fcc4a8342333d222e05f407c", size = 434769 }, + { url = "https://files.pythonhosted.org/packages/52/47/d4b49b1231abf3131f7bb0bc60ebb94b27ee33e0a1f9569da05f8ac65dee/safetensors-0.4.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dde2bf390d25f67908278d6f5d59e46211ef98e44108727084d4637ee70ab4f1", size = 457166 }, + { url = "https://files.pythonhosted.org/packages/c3/cd/006468b03b0fa42ff82d795d47c4193e99001e96c3f08bd62ef1b5cab586/safetensors-0.4.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7469d70d3de970b1698d47c11ebbf296a308702cbaae7fcb993944751cf985f4", size = 619280 }, + { url = "https://files.pythonhosted.org/packages/22/4d/b6208d918e83daa84b424c0ac3191ae61b44b3191613a3a5a7b38f94b8ad/safetensors-0.4.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3a6ba28118636a130ccbb968bc33d4684c48678695dba2590169d5ab03a45646", size = 605390 }, + { url = "https://files.pythonhosted.org/packages/e8/20/bf0e01825dc01ed75538021a98b9a046e60ead63c6c6700764c821a8c873/safetensors-0.4.5-cp312-none-win32.whl", hash = "sha256:c859c7ed90b0047f58ee27751c8e56951452ed36a67afee1b0a87847d065eec6", size = 273250 }, + { url = "https://files.pythonhosted.org/packages/f1/5f/ab6b6cec85b40789801f35b7d2fb579ae242d8193929974a106d5ff5c835/safetensors-0.4.5-cp312-none-win_amd64.whl", hash = "sha256:b5a8810ad6a6f933fff6c276eae92c1da217b39b4d8b1bc1c0b8af2d270dc532", size = 286307 }, + { url = "https://files.pythonhosted.org/packages/90/61/0e27b1403e311cba0be20026bee4ee822d90eda7dad372179e7f18bb99f3/safetensors-0.4.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:25e5f8e2e92a74f05b4ca55686234c32aac19927903792b30ee6d7bd5653d54e", size = 392062 }, + { url = "https://files.pythonhosted.org/packages/b1/9f/cc31fafc9f5d79da10a83a820ca37f069bab0717895ad8cbcacf629dd1c5/safetensors-0.4.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:81efb124b58af39fcd684254c645e35692fea81c51627259cdf6d67ff4458916", size = 382517 }, + { url = "https://files.pythonhosted.org/packages/a4/c7/4fda8a0ebb96662550433378f4a74c677fa5fc4d0a43a7ec287d1df254a9/safetensors-0.4.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:585f1703a518b437f5103aa9cf70e9bd437cb78eea9c51024329e4fb8a3e3679", size = 441378 }, + { url = "https://files.pythonhosted.org/packages/14/31/9abb431f6209de9c80dab83e1112ebd769f1e32e7ab7ab228a02424a4693/safetensors-0.4.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4b99fbf72e3faf0b2f5f16e5e3458b93b7d0a83984fe8d5364c60aa169f2da89", size = 438831 }, + { url = "https://files.pythonhosted.org/packages/37/37/99bfb195578a808b8d045159ee9264f8da58d017ac0701853dcacda14d4e/safetensors-0.4.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b17b299ca9966ca983ecda1c0791a3f07f9ca6ab5ded8ef3d283fff45f6bcd5f", size = 477112 }, + { url = "https://files.pythonhosted.org/packages/7d/05/fac3ef107e60d2a78532bed171a91669d4bb259e1236f5ea8c67a6976c75/safetensors-0.4.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:76ded72f69209c9780fdb23ea89e56d35c54ae6abcdec67ccb22af8e696e449a", size = 493373 }, + { url = "https://files.pythonhosted.org/packages/cf/7a/825800ee8c68214b4fd3506d5e19209338c69b41e01c6e14dd13969cc8b9/safetensors-0.4.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2783956926303dcfeb1de91a4d1204cd4089ab441e622e7caee0642281109db3", size = 435422 }, + { url = "https://files.pythonhosted.org/packages/5e/6c/7a3233c08bde558d6c33a41219119866cb596139a4673cc6c24024710ffd/safetensors-0.4.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d94581aab8c6b204def4d7320f07534d6ee34cd4855688004a4354e63b639a35", size = 457382 }, + { url = "https://files.pythonhosted.org/packages/a0/58/0b7bcba3788ff503990cf9278d611b56c029400612ba93e772c987b5aa03/safetensors-0.4.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:67e1e7cb8678bb1b37ac48ec0df04faf689e2f4e9e81e566b5c63d9f23748523", size = 619301 }, + { url = "https://files.pythonhosted.org/packages/82/cc/9c2cf58611daf1c83ce5d37f9de66353e23fcda36008b13fd3409a760aa3/safetensors-0.4.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:dbd280b07e6054ea68b0cb4b16ad9703e7d63cd6890f577cb98acc5354780142", size = 605580 }, +] + +[[package]] +name = "scalecodec" +version = "1.2.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "base58" }, + { name = "more-itertools" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/7c/703893e7a8751318517a3dd8c0c060b2c30ffa33f4ab5dd6a4ed483f7967/scalecodec-1.2.11.tar.gz", hash = "sha256:99a2cdbfccdcaf22bd86b86da55a730a2855514ad2309faef4a4a93ac6cbeb8d", size = 150260 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/60/2a903fa9ed3dfc842240da22969a25b16ea213ed3ee25b7ba8ae1cba20c7/scalecodec-1.2.11-py3-none-any.whl", hash = "sha256:d15c94965f617caa25096f83a45f5f73031d05e6ee08d6039969f0a64fc35de1", size = 99164 }, +] + +[[package]] +name = "sentry-sdk" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2b/23/643a9958d1d14f5ba1f0204396d5953f926624b3f95b77af7904fb406d03/sentry_sdk-2.19.1.tar.gz", hash = "sha256:6ad8507457a379b72f832aca55787b21e7391751892faef1fd8bace350aa5e17", size = 298915 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/b1/b03f54c8b379d493bd19f9dca241efdd17f77a8f7a34b80c2d4417dfc7b7/sentry_sdk-2.19.1-py2.py3-none-any.whl", hash = "sha256:b056e04b766f805fdf0aa620482cafe2ff000c8fcb51cb266cdb90873e93837b", size = 322816 }, +] + +[[package]] +name = "setproctitle" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/4e/b09341b19b9ceb8b4c67298ab4a08ef7a4abdd3016c7bb152e9b6379031d/setproctitle-1.3.4.tar.gz", hash = "sha256:3b40d32a3e1f04e94231ed6dfee0da9e43b4f9c6b5450d53e6dd7754c34e0c50", size = 26456 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/1a/1fb7d622195bcb3ce7b04366a833e51cfa5ad632c5dafe32e0763cd3fdc9/setproctitle-1.3.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f0f749f07002c2d6fecf37cedc43207a88e6c651926a470a5f229070cf791879", size = 16851 }, + { url = "https://files.pythonhosted.org/packages/46/54/e3aa4f46eddf795f10452ea878ff85c3496d36409636530f9a37e2de3cbe/setproctitle-1.3.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:90ea8d302a5d30b948451d146e94674a3c5b020cc0ced9a1c28f8ddb0f203a5d", size = 11620 }, + { url = "https://files.pythonhosted.org/packages/61/47/80988221679dfd93c464248abb71c2a96338f2ca3f8e3288d0ecb7422f4d/setproctitle-1.3.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f859c88193ed466bee4eb9d45fbc29d2253e6aa3ccd9119c9a1d8d95f409a60d", size = 31519 }, + { url = "https://files.pythonhosted.org/packages/2c/72/14984c127f708597e412f1a8cf7cac809b9bca50a267a6b01b221b094330/setproctitle-1.3.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b3afa5a0ed08a477ded239c05db14c19af585975194a00adf594d48533b23701", size = 32860 }, + { url = "https://files.pythonhosted.org/packages/16/9d/34ea09295620fddae65cf7caeac81bbfc386a3ae6ce26a4dcadbb54c134d/setproctitle-1.3.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a78fce9018cc3e9a772b6537bbe3fe92380acf656c9f86db2f45e685af376e", size = 30029 }, + { url = "https://files.pythonhosted.org/packages/44/bf/a447a51054ceed23f69d4f7370289044b4508569f11da6db2eec087bc174/setproctitle-1.3.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d758e2eed2643afac5f2881542fbb5aa97640b54be20d0a5ed0691d02f0867d", size = 31017 }, + { url = "https://files.pythonhosted.org/packages/ec/46/adcffde6fb8d95458da0a568afdf0dabbbff6470299d94014676e1ab43c0/setproctitle-1.3.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ef133a1a2ee378d549048a12d56f4ef0e2b9113b0b25b6b77821e9af94d50634", size = 30762 }, + { url = "https://files.pythonhosted.org/packages/a3/cd/747a67ce1f6ef8fd1fa46b0b13ba0e007b80914bd549318830b8691ab9f6/setproctitle-1.3.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1d2a154b79d5fb42d1eff06e05e22f0e8091261d877dd47b37d31352b74ecc37", size = 29753 }, + { url = "https://files.pythonhosted.org/packages/3d/86/5939546e57238462a7839ae78399a635d1cfc5d125c7a12a28face111730/setproctitle-1.3.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:202eae632815571297833876a0f407d0d9c7ad9d843b38adbe687fe68c5192ee", size = 32161 }, + { url = "https://files.pythonhosted.org/packages/62/83/9194a4baed06e0e90a69e2e4a77a75e5a3ff008046870c79bc36a5c45e1c/setproctitle-1.3.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2b0080819859e80a7776ac47cf6accb4b7ad313baf55fabac89c000480dcd103", size = 30104 }, + { url = "https://files.pythonhosted.org/packages/ac/cd/08928fec23cbf4dae2a7b245b72d86e6458d64f4e7e6956cd80a9fda8c80/setproctitle-1.3.4-cp311-cp311-win32.whl", hash = "sha256:9c9d7d1267dee8c6627963d9376efa068858cfc8f573c083b1b6a2d297a8710f", size = 11349 }, + { url = "https://files.pythonhosted.org/packages/aa/19/240c4b99d57e045d3b2e2effa5924e810eabb18c56ef9c2336a7746dffe4/setproctitle-1.3.4-cp311-cp311-win_amd64.whl", hash = "sha256:475986ddf6df65d619acd52188336a20f616589403f5a5ceb3fc70cdc137037a", size = 12071 }, + { url = "https://files.pythonhosted.org/packages/94/1f/02fb3c6038c819d86765316d2a911281fc56c7dd3a9355dceb3f26a5bf7b/setproctitle-1.3.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d06990dcfcd41bb3543c18dd25c8476fbfe1f236757f42fef560f6aa03ac8dfc", size = 16842 }, + { url = "https://files.pythonhosted.org/packages/b8/0c/d69e1f91c8f3d3aa74394e9e6ebb818f7d323e2d138ce1127e9462d09ebc/setproctitle-1.3.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:317218c9d8b17a010ab2d2f0851e8ef584077a38b1ba2b7c55c9e44e79a61e73", size = 11614 }, + { url = "https://files.pythonhosted.org/packages/86/ed/8031871d275302054b2f1b94b7cf5e850212cc412fe968f0979e64c1b838/setproctitle-1.3.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb5fefb53b9d9f334a5d9ec518a36b92a10b936011ac8a6b6dffd60135f16459", size = 31840 }, + { url = "https://files.pythonhosted.org/packages/45/b7/04f5d221cbdcff35d6cdf74e2a852e69dc8d8e746eb1b314be6b57b79c41/setproctitle-1.3.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0855006261635e8669646c7c304b494b6df0a194d2626683520103153ad63cc9", size = 33271 }, + { url = "https://files.pythonhosted.org/packages/25/b2/8dff0d2a72076e5535f117f33458d520538b5a0900b90a9f59a278f0d3f6/setproctitle-1.3.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a88e466fcaee659679c1d64dcb2eddbcb4bfadffeb68ba834d9c173a25b6184", size = 30509 }, + { url = "https://files.pythonhosted.org/packages/4b/cf/4f19cdc7fdff3eaeb3064ce6eeb27c63081dba3123fbf904ac6bf0de440c/setproctitle-1.3.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f963b6ed8ba33eda374a98d979e8a0eaf21f891b6e334701693a2c9510613c4c", size = 31543 }, + { url = "https://files.pythonhosted.org/packages/9b/a7/5f9c3c70dc5573f660f978fb3bb4847cd26ede95a5fc294d3f1cf6779800/setproctitle-1.3.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:122c2e05697fa91f5d23f00bbe98a9da1bd457b32529192e934095fadb0853f1", size = 31268 }, + { url = "https://files.pythonhosted.org/packages/26/ab/bbde90ea0ed6a062ef94fe1c609b68077f7eb586133a62fa62d0c8dd9f8c/setproctitle-1.3.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:1bba0a866f5895d5b769d8c36b161271c7fd407e5065862ab80ff91c29fbe554", size = 30232 }, + { url = "https://files.pythonhosted.org/packages/36/0e/817be9934eda4cf63c96c694c3383cb0d2e5d019a2871af7dbd2202f7a58/setproctitle-1.3.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:97f1f861998e326e640708488c442519ad69046374b2c3fe9bcc9869b387f23c", size = 32739 }, + { url = "https://files.pythonhosted.org/packages/b0/76/9b4877850c9c5f41c4bacae441285dead7c192bebf4fcbf3b3eb0e8033cc/setproctitle-1.3.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:726aee40357d4bdb70115442cb85ccc8e8bc554fc0bbbaa3a57cbe81df42287d", size = 30778 }, + { url = "https://files.pythonhosted.org/packages/b2/fa/bbc7ab32f253b9700ac20d78ba0d5fbdc4ea5789d33e1adb236cdf20b23a/setproctitle-1.3.4-cp312-cp312-win32.whl", hash = "sha256:04d6ba8b816dbb0bfd62000b0c3e583160893e6e8c4233e1dca1a9ae4d95d924", size = 11355 }, + { url = "https://files.pythonhosted.org/packages/44/5c/6e6665b5fd800206a9e537ab0d2630d7b9b31b4697d931ed468837cc9cf5/setproctitle-1.3.4-cp312-cp312-win_amd64.whl", hash = "sha256:9c76e43cb351ba8887371240b599925cdf3ecececc5dfb7125c71678e7722c55", size = 12069 }, + { url = "https://files.pythonhosted.org/packages/d4/01/51d07ab1dbec8885ebad419d254c06b9e28f4363c163b737a89995a52b75/setproctitle-1.3.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d6e3b177e634aa6bbbfbf66d097b6d1cdb80fc60e912c7d8bace2e45699c07dd", size = 16831 }, + { url = "https://files.pythonhosted.org/packages/30/03/deff7089b525c0d8ec047e06661d2be67c87685a99be6a6aed2890b81c8f/setproctitle-1.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6b17655a5f245b416e127e02087ea6347a48821cc4626bc0fd57101bfcd88afc", size = 11607 }, + { url = "https://files.pythonhosted.org/packages/ea/be/cb2950b3f6ba460f530bda2c713828236c75d982d0aa0f62b33429a9b4d0/setproctitle-1.3.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa5057a86df920faab8ee83960b724bace01a3231eb8e3f2c93d78283504d598", size = 31881 }, + { url = "https://files.pythonhosted.org/packages/5c/b4/1f0dba7525a2fbefd08d4086e7e998d9c7581153807fb6b3083d06e0b8e2/setproctitle-1.3.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149fdfb8a26a555780c4ce53c92e6d3c990ef7b30f90a675eca02e83c6d5f76d", size = 33290 }, + { url = "https://files.pythonhosted.org/packages/2d/a8/07a160f9dcd1a7b1cad39ce6cbaf4425837502b0592a400c38cb21f0f247/setproctitle-1.3.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ded03546938a987f463c68ab98d683af87a83db7ac8093bbc179e77680be5ba2", size = 30489 }, + { url = "https://files.pythonhosted.org/packages/83/0c/3d972d9ea4165961a9764df5324d42bf2d059cb8a6ef516c67f068ed4d92/setproctitle-1.3.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ab9f5b7f2bbc1754bc6292d9a7312071058e5a891b0391e6d13b226133f36aa", size = 31576 }, + { url = "https://files.pythonhosted.org/packages/7a/c0/c12bdc2c91009defdd1b207ff156ccd691f5b9a6a0aae1ed9126d4ff9a0c/setproctitle-1.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0b19813c852566fa031902124336fa1f080c51e262fc90266a8c3d65ca47b74c", size = 31273 }, + { url = "https://files.pythonhosted.org/packages/4f/83/8d704bee57990b27537adf7c97540f32226ffa3922fb26bdd459da8a4470/setproctitle-1.3.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:db78b645dc63c0ccffca367a498f3b13492fb106a2243a1e998303ba79c996e2", size = 30236 }, + { url = "https://files.pythonhosted.org/packages/d8/42/94e31d1f515f831e1ae43f2405794257eb940a7972b2fbb6283790db2958/setproctitle-1.3.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b669aaac70bd9f03c070270b953f78d9ee56c4af6f0ff9f9cd3e6d1878c10b40", size = 32766 }, + { url = "https://files.pythonhosted.org/packages/83/53/01746ed8fb75239a001ee89d5eb8ad5a3022df510572d1cf60dd04567e13/setproctitle-1.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6dc3d656702791565994e64035a208be56b065675a5bc87b644c657d6d9e2232", size = 30812 }, + { url = "https://files.pythonhosted.org/packages/5f/ea/3ce61e70a6b898e95c0a1e393964c829103dc4ad4b0732cd70c8fc13e54c/setproctitle-1.3.4-cp313-cp313-win32.whl", hash = "sha256:091f682809a4d12291cf0205517619d2e7014986b7b00ebecfde3d76f8ae5a8f", size = 11349 }, + { url = "https://files.pythonhosted.org/packages/e7/1a/8149da1c19db6bd57164d62b1d91c188e7d77e695947cf1ac327c8aea513/setproctitle-1.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:adcd6ba863a315702184d92d3d3bbff290514f24a14695d310f02ae5e28bd1f7", size = 12062 }, +] + +[[package]] +name = "setuptools" +version = "70.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/60/5db2249526c9b453c5bb8b9f6965fcab0ddb7f40ad734420b3b421f7da44/setuptools-70.0.0.tar.gz", hash = "sha256:f211a66637b8fa059bb28183da127d4e86396c991a942b028c6650d4319c3fd0", size = 2265182 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/88/70c5767a0e43eb4451c2200f07d042a4bcd7639276003a9c54a68cfcc1f8/setuptools-70.0.0-py3-none-any.whl", hash = "sha256:54faa7f2e8d2d11bcd2c07bed282eef1046b5c080d1c32add737d7b5817b1ad4", size = 863432 }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, +] + +[[package]] +name = "smmap" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/04/b5bf6d21dc4041000ccba7eb17dd3055feb237e7ffc2c20d3fae3af62baa/smmap-5.0.1.tar.gz", hash = "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62", size = 22291 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/a5/10f97f73544edcdef54409f1d839f6049a0d79df68adbc1ceb24d1aaca42/smmap-5.0.1-py3-none-any.whl", hash = "sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da", size = 24282 }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, +] + +[[package]] +name = "starlette" +version = "0.37.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/b5/6bceb93ff20bd7ca36e6f7c540581abb18f53130fabb30ba526e26fd819b/starlette-0.37.2.tar.gz", hash = "sha256:9af890290133b79fc3db55474ade20f6220a364a0402e0b556e7cd5e1e093823", size = 2843736 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/18/31fa32ed6c68ba66220204ef0be798c349d0a20c1901f9d4a794e08c76d8/starlette-0.37.2-py3-none-any.whl", hash = "sha256:6fe59f29268538e5d0d182f2791a479a0c64638e6935d1c6989e63fb2699c6ee", size = 71908 }, +] + +[[package]] +name = "substrate-interface" +version = "1.7.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "base58" }, + { name = "certifi" }, + { name = "ecdsa" }, + { name = "eth-keys" }, + { name = "eth-utils" }, + { name = "idna" }, + { name = "py-bip39-bindings" }, + { name = "py-ed25519-zebra-bindings" }, + { name = "py-sr25519-bindings" }, + { name = "pycryptodome" }, + { name = "pynacl" }, + { name = "requests" }, + { name = "scalecodec" }, + { name = "websocket-client" }, + { name = "xxhash" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/44/825433c906bdb69ab66fd3967c11fcfbcd953241e9d6257fd6a21c4cdc76/substrate-interface-1.7.11.tar.gz", hash = "sha256:4caa5eacb9996edbe76ad12249521b3542bbd8d9d69b96734087201db1fef8f6", size = 79221 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/e1/37344b7acd260cbfed13563dcbab391c7c4b0c9eca5ec59aba138c5dca9e/substrate_interface-1.7.11-py3-none-any.whl", hash = "sha256:ce19bc97481769238ed23c752db985a3058637918693f2db6aeed2fab3756075", size = 60273 }, +] + +[[package]] +name = "sympy" +version = "1.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/99/5a5b6f19ff9f083671ddf7b9632028436167cd3d33e11015754e41b249a4/sympy-1.13.1.tar.gz", hash = "sha256:9cebf7e04ff162015ce31c9c6c9144daa34a93bd082f54fd8f12deca4f47515f", size = 7533040 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/fe/81695a1aa331a842b582453b605175f419fe8540355886031328089d840a/sympy-1.13.1-py3-none-any.whl", hash = "sha256:db36cdc64bf61b9b24578b6f7bab1ecdd2452cf008f34faa33776680c26d66f8", size = 6189177 }, +] + +[[package]] +name = "termcolor" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/72/88311445fd44c455c7d553e61f95412cf89054308a1aa2434ab835075fc5/termcolor-2.5.0.tar.gz", hash = "sha256:998d8d27da6d48442e8e1f016119076b690d962507531df4890fcd2db2ef8a6f", size = 13057 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/be/df630c387a0a054815d60be6a97eb4e8f17385d5d6fe660e1c02750062b4/termcolor-2.5.0-py3-none-any.whl", hash = "sha256:37b17b5fc1e604945c2642c872a3764b5d547a48009871aea3edd3afa180afb8", size = 7755 }, +] + +[[package]] +name = "tokenizers" +version = "0.20.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/25/b1681c1c30ea3ea6e584ae3fffd552430b12faa599b558c4c4783f56d7ff/tokenizers-0.20.3.tar.gz", hash = "sha256:2278b34c5d0dd78e087e1ca7f9b1dcbf129d80211afa645f214bd6e051037539", size = 340513 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/93/6742ef9206409d5ce1fdf44d5ca1687cdc3847ba0485424e2c731e6bcf67/tokenizers-0.20.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:585b51e06ca1f4839ce7759941e66766d7b060dccfdc57c4ca1e5b9a33013a90", size = 2674224 }, + { url = "https://files.pythonhosted.org/packages/aa/14/e75ece72e99f6ef9ae07777ca9fdd78608f69466a5cecf636e9bd2f25d5c/tokenizers-0.20.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:61cbf11954f3b481d08723ebd048ba4b11e582986f9be74d2c3bdd9293a4538d", size = 2558991 }, + { url = "https://files.pythonhosted.org/packages/46/54/033b5b2ba0c3ae01e026c6f7ced147d41a2fa1c573d00a66cb97f6d7f9b3/tokenizers-0.20.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef820880d5e4e8484e2fa54ff8d297bb32519eaa7815694dc835ace9130a3eea", size = 2892476 }, + { url = "https://files.pythonhosted.org/packages/e6/b0/cc369fb3297d61f3311cab523d16d48c869dc2f0ba32985dbf03ff811041/tokenizers-0.20.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:67ef4dcb8841a4988cd00dd288fb95dfc8e22ed021f01f37348fd51c2b055ba9", size = 2802775 }, + { url = "https://files.pythonhosted.org/packages/1a/74/62ad983e8ea6a63e04ed9c5be0b605056bf8aac2f0125f9b5e0b3e2b89fa/tokenizers-0.20.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff1ef8bd47a02b0dc191688ccb4da53600df5d4c9a05a4b68e1e3de4823e78eb", size = 3086138 }, + { url = "https://files.pythonhosted.org/packages/6b/ac/4637ba619db25094998523f9e6f5b456e1db1f8faa770a3d925d436db0c3/tokenizers-0.20.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:444d188186eab3148baf0615b522461b41b1f0cd58cd57b862ec94b6ac9780f1", size = 3098076 }, + { url = "https://files.pythonhosted.org/packages/58/ce/9793f2dc2ce529369807c9c74e42722b05034af411d60f5730b720388c7d/tokenizers-0.20.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37c04c032c1442740b2c2d925f1857885c07619224a533123ac7ea71ca5713da", size = 3379650 }, + { url = "https://files.pythonhosted.org/packages/50/f6/2841de926bc4118af996eaf0bdf0ea5b012245044766ffc0347e6c968e63/tokenizers-0.20.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:453c7769d22231960ee0e883d1005c93c68015025a5e4ae56275406d94a3c907", size = 2994005 }, + { url = "https://files.pythonhosted.org/packages/a3/b2/00915c4fed08e9505d37cf6eaab45b12b4bff8f6719d459abcb9ead86a4b/tokenizers-0.20.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4bb31f7b2847e439766aaa9cc7bccf7ac7088052deccdb2275c952d96f691c6a", size = 8977488 }, + { url = "https://files.pythonhosted.org/packages/e9/ac/1c069e7808181ff57bcf2d39e9b6fbee9133a55410e6ebdaa89f67c32e83/tokenizers-0.20.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:843729bf0f991b29655a069a2ff58a4c24375a553c70955e15e37a90dd4e045c", size = 9294935 }, + { url = "https://files.pythonhosted.org/packages/50/47/722feb70ee68d1c4412b12d0ea4acc2713179fd63f054913990f9e259492/tokenizers-0.20.3-cp311-none-win32.whl", hash = "sha256:efcce3a927b1e20ca694ba13f7a68c59b0bd859ef71e441db68ee42cf20c2442", size = 2197175 }, + { url = "https://files.pythonhosted.org/packages/75/68/1b4f928b15a36ed278332ac75d66d7eb65d865bf344d049c452c18447bf9/tokenizers-0.20.3-cp311-none-win_amd64.whl", hash = "sha256:88301aa0801f225725b6df5dea3d77c80365ff2362ca7e252583f2b4809c4cc0", size = 2381616 }, + { url = "https://files.pythonhosted.org/packages/07/00/92a08af2a6b0c88c50f1ab47d7189e695722ad9714b0ee78ea5e1e2e1def/tokenizers-0.20.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:49d12a32e190fad0e79e5bdb788d05da2f20d8e006b13a70859ac47fecf6ab2f", size = 2667951 }, + { url = "https://files.pythonhosted.org/packages/ec/9a/e17a352f0bffbf415cf7d73756f5c73a3219225fc5957bc2f39d52c61684/tokenizers-0.20.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:282848cacfb9c06d5e51489f38ec5aa0b3cd1e247a023061945f71f41d949d73", size = 2555167 }, + { url = "https://files.pythonhosted.org/packages/27/37/d108df55daf4f0fcf1f58554692ff71687c273d870a34693066f0847be96/tokenizers-0.20.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abe4e08c7d0cd6154c795deb5bf81d2122f36daf075e0c12a8b050d824ef0a64", size = 2898389 }, + { url = "https://files.pythonhosted.org/packages/b2/27/32f29da16d28f59472fa7fb38e7782069748c7e9ab9854522db20341624c/tokenizers-0.20.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ca94fc1b73b3883c98f0c88c77700b13d55b49f1071dfd57df2b06f3ff7afd64", size = 2795866 }, + { url = "https://files.pythonhosted.org/packages/29/4e/8a9a3c89e128c4a40f247b501c10279d2d7ade685953407c4d94c8c0f7a7/tokenizers-0.20.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef279c7e239f95c8bdd6ff319d9870f30f0d24915b04895f55b1adcf96d6c60d", size = 3085446 }, + { url = "https://files.pythonhosted.org/packages/b4/3b/a2a7962c496ebcd95860ca99e423254f760f382cd4bd376f8895783afaf5/tokenizers-0.20.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16384073973f6ccbde9852157a4fdfe632bb65208139c9d0c0bd0176a71fd67f", size = 3094378 }, + { url = "https://files.pythonhosted.org/packages/1f/f4/a8a33f0192a1629a3bd0afcad17d4d221bbf9276da4b95d226364208d5eb/tokenizers-0.20.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:312d522caeb8a1a42ebdec87118d99b22667782b67898a76c963c058a7e41d4f", size = 3385755 }, + { url = "https://files.pythonhosted.org/packages/9e/65/c83cb3545a65a9eaa2e13b22c93d5e00bd7624b354a44adbdc93d5d9bd91/tokenizers-0.20.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2b7cb962564785a83dafbba0144ecb7f579f1d57d8c406cdaa7f32fe32f18ad", size = 2997679 }, + { url = "https://files.pythonhosted.org/packages/55/e9/a80d4e592307688a67c7c59ab77e03687b6a8bd92eb5db763a2c80f93f57/tokenizers-0.20.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:124c5882ebb88dadae1fc788a582299fcd3a8bd84fc3e260b9918cf28b8751f5", size = 8989296 }, + { url = "https://files.pythonhosted.org/packages/90/af/60c957af8d2244321124e893828f1a4817cde1a2d08d09d423b73f19bd2f/tokenizers-0.20.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2b6e54e71f84c4202111a489879005cb14b92616a87417f6c102c833af961ea2", size = 9303621 }, + { url = "https://files.pythonhosted.org/packages/be/a9/96172310ee141009646d63a1ca267c099c462d747fe5ef7e33f74e27a683/tokenizers-0.20.3-cp312-none-win32.whl", hash = "sha256:83d9bfbe9af86f2d9df4833c22e94d94750f1d0cd9bfb22a7bb90a86f61cdb1c", size = 2188979 }, + { url = "https://files.pythonhosted.org/packages/bd/68/61d85ae7ae96dde7d0974ff3538db75d5cdc29be2e4329cd7fc51a283e22/tokenizers-0.20.3-cp312-none-win_amd64.whl", hash = "sha256:44def74cee574d609a36e17c8914311d1b5dbcfe37c55fd29369d42591b91cf2", size = 2380725 }, + { url = "https://files.pythonhosted.org/packages/07/19/36e9eaafb229616cb8502b42030fa7fe347550e76cb618de71b498fc3222/tokenizers-0.20.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e0b630e0b536ef0e3c8b42c685c1bc93bd19e98c0f1543db52911f8ede42cf84", size = 2666813 }, + { url = "https://files.pythonhosted.org/packages/b9/c7/e2ce1d4f756c8a62ef93fdb4df877c2185339b6d63667b015bf70ea9d34b/tokenizers-0.20.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a02d160d2b19bcbfdf28bd9a4bf11be4cb97d0499c000d95d4c4b1a4312740b6", size = 2555354 }, + { url = "https://files.pythonhosted.org/packages/7c/cf/5309c2d173a6a67f9ec8697d8e710ea32418de6fd8541778032c202a1c3e/tokenizers-0.20.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e3d80d89b068bc30034034b5319218c7c0a91b00af19679833f55f3becb6945", size = 2897745 }, + { url = "https://files.pythonhosted.org/packages/2c/e5/af3078e32f225e680e69d61f78855880edb8d53f5850a1834d519b2b103f/tokenizers-0.20.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:174a54910bed1b089226512b4458ea60d6d6fd93060254734d3bc3540953c51c", size = 2794385 }, + { url = "https://files.pythonhosted.org/packages/0b/a7/bc421fe46650cc4eb4a913a236b88c243204f32c7480684d2f138925899e/tokenizers-0.20.3-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:098b8a632b8656aa5802c46689462c5c48f02510f24029d71c208ec2c822e771", size = 3084580 }, + { url = "https://files.pythonhosted.org/packages/c6/22/97e1e95ee81f75922c9f569c23cb2b1fdc7f5a7a29c4c9fae17e63f751a6/tokenizers-0.20.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:78c8c143e3ae41e718588281eb3e212c2b31623c9d6d40410ec464d7d6221fb5", size = 3093581 }, + { url = "https://files.pythonhosted.org/packages/d5/14/f0df0ee3b9e516121e23c0099bccd7b9f086ba9150021a750e99b16ce56f/tokenizers-0.20.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b26b0aadb18cd8701077362ba359a06683662d5cafe3e8e8aba10eb05c037f1", size = 3385934 }, + { url = "https://files.pythonhosted.org/packages/66/52/7a171bd4929e3ffe61a29b4340fe5b73484709f92a8162a18946e124c34c/tokenizers-0.20.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07d7851a72717321022f3774e84aa9d595a041d643fafa2e87fbc9b18711dac0", size = 2997311 }, + { url = "https://files.pythonhosted.org/packages/7c/64/f1993bb8ebf775d56875ca0d50a50f2648bfbbb143da92fe2e6ceeb4abd5/tokenizers-0.20.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:bd44e48a430ada902c6266a8245f5036c4fe744fcb51f699999fbe82aa438797", size = 8988601 }, + { url = "https://files.pythonhosted.org/packages/d6/3f/49fa63422159bbc2f2a4ac5bfc597d04d4ec0ad3d2ef46649b5e9a340e37/tokenizers-0.20.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:a4c186bb006ccbe1f5cc4e0380d1ce7806f5955c244074fd96abc55e27b77f01", size = 9303950 }, + { url = "https://files.pythonhosted.org/packages/66/11/79d91aeb2817ad1993ef61c690afe73e6dbedbfb21918b302ef5a2ba9bfb/tokenizers-0.20.3-cp313-none-win32.whl", hash = "sha256:6e19e0f1d854d6ab7ea0c743d06e764d1d9a546932be0a67f33087645f00fe13", size = 2188941 }, + { url = "https://files.pythonhosted.org/packages/c2/ff/ac8410f868fb8b14b5e619efa304aa119cb8a40bd7df29fc81a898e64f99/tokenizers-0.20.3-cp313-none-win_amd64.whl", hash = "sha256:d50ede425c7e60966a9680d41b58b3a0950afa1bb570488e2972fa61662c4273", size = 2380269 }, +] + +[[package]] +name = "toml" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/19/5cbd78eac8b1783671c40e34bb0fa83133a06d340a38b55c645076d40094/toml-0.10.0.tar.gz", hash = "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", size = 16719 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/12/ced7105d2de62fa7c8fb5fce92cc4ce66b57c95fb875e9318dba7f8c5db0/toml-0.10.0-py2.py3-none-any.whl", hash = "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", size = 25796 }, +] + +[[package]] +name = "toolz" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/0b/d80dfa675bf592f636d1ea0b835eab4ec8df6e9415d8cfd766df54456123/toolz-1.0.0.tar.gz", hash = "sha256:2c86e3d9a04798ac556793bced838816296a2f085017664e4995cb40a1047a02", size = 66790 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/98/eb27cc78ad3af8e302c9d8ff4977f5026676e130d28dd7578132a457170c/toolz-1.0.0-py3-none-any.whl", hash = "sha256:292c8f1c4e7516bf9086f8850935c799a874039c8bcf959d47b600e4c44a6236", size = 56383 }, +] + +[[package]] +name = "torch" +version = "2.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx" }, + { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, + { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, + { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, + { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, + { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, + { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, + { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, + { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, + { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, + { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, + { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, + { name = "setuptools", marker = "python_full_version >= '3.12'" }, + { name = "sympy" }, + { name = "triton", marker = "python_full_version < '3.13' and platform_machine == 'x86_64' and platform_system == 'Linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/35/e8b2daf02ce933e4518e6f5682c72fd0ed66c15910ea1fb4168f442b71c4/torch-2.5.1-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:de5b7d6740c4b636ef4db92be922f0edc425b65ed78c5076c43c42d362a45457", size = 906474467 }, + { url = "https://files.pythonhosted.org/packages/40/04/bd91593a4ca178ece93ca55f27e2783aa524aaccbfda66831d59a054c31e/torch-2.5.1-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:340ce0432cad0d37f5a31be666896e16788f1adf8ad7be481196b503dad675b9", size = 91919450 }, + { url = "https://files.pythonhosted.org/packages/0d/4a/e51420d46cfc90562e85af2fee912237c662ab31140ab179e49bd69401d6/torch-2.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:603c52d2fe06433c18b747d25f5c333f9c1d58615620578c326d66f258686f9a", size = 203098237 }, + { url = "https://files.pythonhosted.org/packages/d0/db/5d9cbfbc7968d79c5c09a0bc0bc3735da079f2fd07cc10498a62b320a480/torch-2.5.1-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:31f8c39660962f9ae4eeec995e3049b5492eb7360dd4f07377658ef4d728fa4c", size = 63884466 }, + { url = "https://files.pythonhosted.org/packages/8b/5c/36c114d120bfe10f9323ed35061bc5878cc74f3f594003854b0ea298942f/torch-2.5.1-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:ed231a4b3a5952177fafb661213d690a72caaad97d5824dd4fc17ab9e15cec03", size = 906389343 }, + { url = "https://files.pythonhosted.org/packages/6d/69/d8ada8b6e0a4257556d5b4ddeb4345ea8eeaaef3c98b60d1cca197c7ad8e/torch-2.5.1-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:3f4b7f10a247e0dcd7ea97dc2d3bfbfc90302ed36d7f3952b0008d0df264e697", size = 91811673 }, + { url = "https://files.pythonhosted.org/packages/5f/ba/607d013b55b9fd805db2a5c2662ec7551f1910b4eef39653eeaba182c5b2/torch-2.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:73e58e78f7d220917c5dbfad1a40e09df9929d3b95d25e57d9f8558f84c9a11c", size = 203046841 }, + { url = "https://files.pythonhosted.org/packages/57/6c/bf52ff061da33deb9f94f4121fde7ff3058812cb7d2036c97bc167793bd1/torch-2.5.1-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:8c712df61101964eb11910a846514011f0b6f5920c55dbf567bff8a34163d5b1", size = 63858109 }, + { url = "https://files.pythonhosted.org/packages/69/72/20cb30f3b39a9face296491a86adb6ff8f1a47a897e4d14667e6cf89d5c3/torch-2.5.1-cp313-cp313-manylinux1_x86_64.whl", hash = "sha256:9b61edf3b4f6e3b0e0adda8b3960266b9009d02b37555971f4d1c8f7a05afed7", size = 906393265 }, +] + +[[package]] +name = "tplr" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "aioboto3" }, + { name = "aiobotocore" }, + { name = "aiofiles" }, + { name = "bittensor" }, + { name = "boto3" }, + { name = "bt-decode" }, + { name = "einops" }, + { name = "pip" }, + { name = "python-dotenv" }, + { name = "torch" }, + { name = "transformers" }, + { name = "wandb" }, +] + +[package.metadata] +requires-dist = [ + { name = "aioboto3" }, + { name = "aiobotocore" }, + { name = "aiofiles" }, + { name = "bittensor", specifier = "==8.5.1" }, + { name = "boto3" }, + { name = "bt-decode", specifier = "==0.4.0" }, + { name = "einops" }, + { name = "pip" }, + { name = "python-dotenv" }, + { name = "torch" }, + { name = "transformers" }, + { name = "wandb" }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "platform_system == 'Windows'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 }, +] + +[[package]] +name = "transformers" +version = "4.46.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "requests" }, + { name = "safetensors" }, + { name = "tokenizers" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/37/5a/58f96c83e566f907ae39f16d4401bbefd8bb85c60bd1e6a95c419752ab90/transformers-4.46.3.tar.gz", hash = "sha256:8ee4b3ae943fe33e82afff8e837f4b052058b07ca9be3cb5b729ed31295f72cc", size = 8627944 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/51/b87caa939fedf307496e4dbf412f4b909af3d9ca8b189fc3b65c1faa456f/transformers-4.46.3-py3-none-any.whl", hash = "sha256:a12ef6f52841fd190a3e5602145b542d03507222f2c64ebb7ee92e8788093aef", size = 10034536 }, +] + +[[package]] +name = "triton" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/17/d9a5cf4fcf46291856d1e90762e36cbabd2a56c7265da0d1d9508c8e3943/triton-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f34f6e7885d1bf0eaaf7ba875a5f0ce6f3c13ba98f9503651c1e6dc6757ed5c", size = 209506424 }, + { url = "https://files.pythonhosted.org/packages/78/eb/65f5ba83c2a123f6498a3097746607e5b2f16add29e36765305e4ac7fdd8/triton-3.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8182f42fd8080a7d39d666814fa36c5e30cc00ea7eeeb1a2983dbb4c99a0fdc", size = 209551444 }, +] + +[[package]] +name = "typer" +version = "0.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/dca7b219718afd37a0068f4f2530a727c2b74a8b6e8e0c0080a4c0de4fcd/typer-0.15.1.tar.gz", hash = "sha256:a0588c0a7fa68a1978a069818657778f86abe6ff5ea6abf472f940a08bfe4f0a", size = 99789 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/cc/0a838ba5ca64dc832aa43f727bd586309846b0ffb2ce52422543e6075e8a/typer-0.15.1-py3-none-any.whl", hash = "sha256:7994fb7b8155b64d3402518560648446072864beefd44aa2dc36972a5972e847", size = 44908 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] + +[[package]] +name = "urllib3" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 }, +] + +[[package]] +name = "uvicorn" +version = "0.32.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/3c/21dba3e7d76138725ef307e3d7ddd29b763119b3aa459d02cc05fefcff75/uvicorn-0.32.1.tar.gz", hash = "sha256:ee9519c246a72b1c084cea8d3b44ed6026e78a4a309cbedae9c37e4cb9fbb175", size = 77630 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/c1/2d27b0a15826c2b71dcf6e2f5402181ef85acf439617bb2f1453125ce1f3/uvicorn-0.32.1-py3-none-any.whl", hash = "sha256:82ad92fd58da0d12af7482ecdb5f2470a04c9c9a53ced65b9bbb4a205377602e", size = 63828 }, +] + +[[package]] +name = "wandb" +version = "0.19.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "docker-pycreds" }, + { name = "gitpython" }, + { name = "platformdirs" }, + { name = "protobuf" }, + { name = "psutil" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "sentry-sdk" }, + { name = "setproctitle" }, + { name = "setuptools" }, + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/cc/3322f2c4d85b84a18cb93e97ecad216fe6a59ec39118a82bdfed7872f660/wandb-0.19.0.tar.gz", hash = "sha256:cfacf2cc323561909e7572e772a4a5f849f28248a4529247b199466171cd84f8", size = 11821728 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/a0/5d8f6268a728fbf008797967587dfad29cd4837a1e3ea33ad2ded074a0a0/wandb-0.19.0-py3-none-any.whl", hash = "sha256:d4dab974f8fd5304ae5af961777d89ba4622d776b18882dc091098a7eace6ca3", size = 6247909 }, + { url = "https://files.pythonhosted.org/packages/3b/23/0e60bee6cf1e738e34204820d19dace42063620dba6786e4293bfabd2166/wandb-0.19.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:ec14280a833263ae828d181b853be38858f933f55ecb77a9040372bf2b09b5e3", size = 20015933 }, + { url = "https://files.pythonhosted.org/packages/59/6e/3bf8590ccbce0eb7c705c0f2b1e3700fad891d48524a93e86f52f7000f67/wandb-0.19.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3d2275ef9d97ce8203b56621d710276b2c023ab3f1a9837dccaf5d75b819ab38", size = 19241348 }, + { url = "https://files.pythonhosted.org/packages/15/93/6b6f4b163a20c70ce4f2792b6ac15c2322dc0cbd1b7377b5aca8c43526df/wandb-0.19.0-py3-none-macosx_11_0_x86_64.whl", hash = "sha256:65c4fc6fd537d554bcab31a74f28bba82782f83f735b6972702dbab31caaecf1", size = 20040571 }, + { url = "https://files.pythonhosted.org/packages/50/db/efdb733bb98038434ce097ae8178039e81aa4bbc182b3ebc3bb244b29a3c/wandb-0.19.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f0fec8825702ec4ac8453652f2af69b211ee73895272bbdb625bb2721da1f4", size = 18821680 }, + { url = "https://files.pythonhosted.org/packages/45/e0/7ba3b78a74413b7467300cb7a5d486b9871ee464a7cade98ea869d3ca3df/wandb-0.19.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:146b972a0d11442f6b5592e5b53ae37b5add5131206136e5bf0a8c3e3fb8fbd0", size = 20095363 }, + { url = "https://files.pythonhosted.org/packages/0d/9a/828968f7c6256a2440123f9602a403fe2f7730a3286e0e344a84cb9a0821/wandb-0.19.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:370d96c23217cd5a16c1f56e02cda9b0f1e2805f4dd6fa942645a726a0e9b549", size = 20169998 }, + { url = "https://files.pythonhosted.org/packages/43/5b/3f436aa647681bf1b6a3fd694c974a40d33be5c40b492277020da360a5cf/wandb-0.19.0-py3-none-win32.whl", hash = "sha256:ab50cc3233727765fbb7b9266cf824f53637c8de2be47ba107542e3ad21ba307", size = 19563293 }, + { url = "https://files.pythonhosted.org/packages/cd/c8/c778a55aeab47ea918c82023fad837cdf5e686dc2249b67a723d452da390/wandb-0.19.0-py3-none-win_amd64.whl", hash = "sha256:0fe8af679306b959b22260b4a67f22186829433809f76e48e70d25c04c2dcf94", size = 19563296 }, +] + +[[package]] +name = "websocket-client" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/30/fba0d96b4b5fbf5948ed3f4681f7da2f9f64512e1d303f94b4cc174c24a5/websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da", size = 54648 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/84/44687a29792a70e111c5c477230a72c4b957d88d16141199bf9acb7537a3/websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526", size = 58826 }, +] + +[[package]] +name = "websockets" +version = "14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/1b/380b883ce05bb5f45a905b61790319a28958a9ab1e4b6b95ff5464b60ca1/websockets-14.1.tar.gz", hash = "sha256:398b10c77d471c0aab20a845e7a60076b6390bfdaac7a6d2edb0d2c59d75e8d8", size = 162840 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/ed/c0d03cb607b7fe1f7ff45e2cd4bb5cd0f9e3299ced79c2c303a6fff44524/websockets-14.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:449d77d636f8d9c17952628cc7e3b8faf6e92a17ec581ec0c0256300717e1512", size = 161949 }, + { url = "https://files.pythonhosted.org/packages/06/91/bf0a44e238660d37a2dda1b4896235d20c29a2d0450f3a46cd688f43b239/websockets-14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a35f704be14768cea9790d921c2c1cc4fc52700410b1c10948511039be824aac", size = 159606 }, + { url = "https://files.pythonhosted.org/packages/ff/b8/7185212adad274c2b42b6a24e1ee6b916b7809ed611cbebc33b227e5c215/websockets-14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b1f3628a0510bd58968c0f60447e7a692933589b791a6b572fcef374053ca280", size = 159854 }, + { url = "https://files.pythonhosted.org/packages/5a/8a/0849968d83474be89c183d8ae8dcb7f7ada1a3c24f4d2a0d7333c231a2c3/websockets-14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c3deac3748ec73ef24fc7be0b68220d14d47d6647d2f85b2771cb35ea847aa1", size = 169402 }, + { url = "https://files.pythonhosted.org/packages/bd/4f/ef886e37245ff6b4a736a09b8468dae05d5d5c99de1357f840d54c6f297d/websockets-14.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7048eb4415d46368ef29d32133134c513f507fff7d953c18c91104738a68c3b3", size = 168406 }, + { url = "https://files.pythonhosted.org/packages/11/43/e2dbd4401a63e409cebddedc1b63b9834de42f51b3c84db885469e9bdcef/websockets-14.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6cf0ad281c979306a6a34242b371e90e891bce504509fb6bb5246bbbf31e7b6", size = 168776 }, + { url = "https://files.pythonhosted.org/packages/6d/d6/7063e3f5c1b612e9f70faae20ebaeb2e684ffa36cb959eb0862ee2809b32/websockets-14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cc1fc87428c1d18b643479caa7b15db7d544652e5bf610513d4a3478dbe823d0", size = 169083 }, + { url = "https://files.pythonhosted.org/packages/49/69/e6f3d953f2fa0f8a723cf18cd011d52733bd7f6e045122b24e0e7f49f9b0/websockets-14.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f95ba34d71e2fa0c5d225bde3b3bdb152e957150100e75c86bc7f3964c450d89", size = 168529 }, + { url = "https://files.pythonhosted.org/packages/70/ff/f31fa14561fc1d7b8663b0ed719996cf1f581abee32c8fb2f295a472f268/websockets-14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9481a6de29105d73cf4515f2bef8eb71e17ac184c19d0b9918a3701c6c9c4f23", size = 168475 }, + { url = "https://files.pythonhosted.org/packages/f1/15/b72be0e4bf32ff373aa5baef46a4c7521b8ea93ad8b49ca8c6e8e764c083/websockets-14.1-cp311-cp311-win32.whl", hash = "sha256:368a05465f49c5949e27afd6fbe0a77ce53082185bbb2ac096a3a8afaf4de52e", size = 162833 }, + { url = "https://files.pythonhosted.org/packages/bc/ef/2d81679acbe7057ffe2308d422f744497b52009ea8bab34b6d74a2657d1d/websockets-14.1-cp311-cp311-win_amd64.whl", hash = "sha256:6d24fc337fc055c9e83414c94e1ee0dee902a486d19d2a7f0929e49d7d604b09", size = 163263 }, + { url = "https://files.pythonhosted.org/packages/55/64/55698544ce29e877c9188f1aee9093712411a8fc9732cca14985e49a8e9c/websockets-14.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed907449fe5e021933e46a3e65d651f641975a768d0649fee59f10c2985529ed", size = 161957 }, + { url = "https://files.pythonhosted.org/packages/a2/b1/b088f67c2b365f2c86c7b48edb8848ac27e508caf910a9d9d831b2f343cb/websockets-14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:87e31011b5c14a33b29f17eb48932e63e1dcd3fa31d72209848652310d3d1f0d", size = 159620 }, + { url = "https://files.pythonhosted.org/packages/c1/89/2a09db1bbb40ba967a1b8225b07b7df89fea44f06de9365f17f684d0f7e6/websockets-14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bc6ccf7d54c02ae47a48ddf9414c54d48af9c01076a2e1023e3b486b6e72c707", size = 159852 }, + { url = "https://files.pythonhosted.org/packages/ca/c1/f983138cd56e7d3079f1966e81f77ce6643f230cd309f73aa156bb181749/websockets-14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9777564c0a72a1d457f0848977a1cbe15cfa75fa2f67ce267441e465717dcf1a", size = 169675 }, + { url = "https://files.pythonhosted.org/packages/c1/c8/84191455d8660e2a0bdb33878d4ee5dfa4a2cedbcdc88bbd097303b65bfa/websockets-14.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a655bde548ca98f55b43711b0ceefd2a88a71af6350b0c168aa77562104f3f45", size = 168619 }, + { url = "https://files.pythonhosted.org/packages/8d/a7/62e551fdcd7d44ea74a006dc193aba370505278ad76efd938664531ce9d6/websockets-14.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3dfff83ca578cada2d19e665e9c8368e1598d4e787422a460ec70e531dbdd58", size = 169042 }, + { url = "https://files.pythonhosted.org/packages/ad/ed/1532786f55922c1e9c4d329608e36a15fdab186def3ca9eb10d7465bc1cc/websockets-14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6a6c9bcf7cdc0fd41cc7b7944447982e8acfd9f0d560ea6d6845428ed0562058", size = 169345 }, + { url = "https://files.pythonhosted.org/packages/ea/fb/160f66960d495df3de63d9bcff78e1b42545b2a123cc611950ffe6468016/websockets-14.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4b6caec8576e760f2c7dd878ba817653144d5f369200b6ddf9771d64385b84d4", size = 168725 }, + { url = "https://files.pythonhosted.org/packages/cf/53/1bf0c06618b5ac35f1d7906444b9958f8485682ab0ea40dee7b17a32da1e/websockets-14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb6d38971c800ff02e4a6afd791bbe3b923a9a57ca9aeab7314c21c84bf9ff05", size = 168712 }, + { url = "https://files.pythonhosted.org/packages/e5/22/5ec2f39fff75f44aa626f86fa7f20594524a447d9c3be94d8482cd5572ef/websockets-14.1-cp312-cp312-win32.whl", hash = "sha256:1d045cbe1358d76b24d5e20e7b1878efe578d9897a25c24e6006eef788c0fdf0", size = 162838 }, + { url = "https://files.pythonhosted.org/packages/74/27/28f07df09f2983178db7bf6c9cccc847205d2b92ced986cd79565d68af4f/websockets-14.1-cp312-cp312-win_amd64.whl", hash = "sha256:90f4c7a069c733d95c308380aae314f2cb45bd8a904fb03eb36d1a4983a4993f", size = 163277 }, + { url = "https://files.pythonhosted.org/packages/34/77/812b3ba5110ed8726eddf9257ab55ce9e85d97d4aa016805fdbecc5e5d48/websockets-14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3630b670d5057cd9e08b9c4dab6493670e8e762a24c2c94ef312783870736ab9", size = 161966 }, + { url = "https://files.pythonhosted.org/packages/8d/24/4fcb7aa6986ae7d9f6d083d9d53d580af1483c5ec24bdec0978307a0f6ac/websockets-14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36ebd71db3b89e1f7b1a5deaa341a654852c3518ea7a8ddfdf69cc66acc2db1b", size = 159625 }, + { url = "https://files.pythonhosted.org/packages/f8/47/2a0a3a2fc4965ff5b9ce9324d63220156bd8bedf7f90824ab92a822e65fd/websockets-14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5b918d288958dc3fa1c5a0b9aa3256cb2b2b84c54407f4813c45d52267600cd3", size = 159857 }, + { url = "https://files.pythonhosted.org/packages/dd/c8/d7b425011a15e35e17757e4df75b25e1d0df64c0c315a44550454eaf88fc/websockets-14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00fe5da3f037041da1ee0cf8e308374e236883f9842c7c465aa65098b1c9af59", size = 169635 }, + { url = "https://files.pythonhosted.org/packages/93/39/6e3b5cffa11036c40bd2f13aba2e8e691ab2e01595532c46437b56575678/websockets-14.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8149a0f5a72ca36720981418eeffeb5c2729ea55fa179091c81a0910a114a5d2", size = 168578 }, + { url = "https://files.pythonhosted.org/packages/cf/03/8faa5c9576299b2adf34dcccf278fc6bbbcda8a3efcc4d817369026be421/websockets-14.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77569d19a13015e840b81550922056acabc25e3f52782625bc6843cfa034e1da", size = 169018 }, + { url = "https://files.pythonhosted.org/packages/8c/05/ea1fec05cc3a60defcdf0bb9f760c3c6bd2dd2710eff7ac7f891864a22ba/websockets-14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf5201a04550136ef870aa60ad3d29d2a59e452a7f96b94193bee6d73b8ad9a9", size = 169383 }, + { url = "https://files.pythonhosted.org/packages/21/1d/eac1d9ed787f80754e51228e78855f879ede1172c8b6185aca8cef494911/websockets-14.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:88cf9163ef674b5be5736a584c999e98daf3aabac6e536e43286eb74c126b9c7", size = 168773 }, + { url = "https://files.pythonhosted.org/packages/0e/1b/e808685530185915299740d82b3a4af3f2b44e56ccf4389397c7a5d95d39/websockets-14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:836bef7ae338a072e9d1863502026f01b14027250a4545672673057997d5c05a", size = 168757 }, + { url = "https://files.pythonhosted.org/packages/b6/19/6ab716d02a3b068fbbeb6face8a7423156e12c446975312f1c7c0f4badab/websockets-14.1-cp313-cp313-win32.whl", hash = "sha256:0d4290d559d68288da9f444089fd82490c8d2744309113fc26e2da6e48b65da6", size = 162834 }, + { url = "https://files.pythonhosted.org/packages/6c/fd/ab6b7676ba712f2fc89d1347a4b5bdc6aa130de10404071f2b2606450209/websockets-14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8621a07991add373c3c5c2cf89e1d277e49dc82ed72c75e3afc74bd0acc446f0", size = 163277 }, + { url = "https://files.pythonhosted.org/packages/b0/0b/c7e5d11020242984d9d37990310520ed663b942333b83a033c2f20191113/websockets-14.1-py3-none-any.whl", hash = "sha256:4d4fc827a20abe6d544a119896f6b78ee13fe81cbfef416f3f2ddf09a03f0e2e", size = 156277 }, +] + +[[package]] +name = "wheel" +version = "0.45.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/98/2d9906746cdc6a6ef809ae6338005b3f21bb568bea3165cfc6a243fdc25c/wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729", size = 107545 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/2c/87f3254fd8ffd29e4c02732eee68a83a1d3c346ae39bc6822dcbcb697f2b/wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248", size = 72494 }, +] + +[[package]] +name = "wrapt" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/a1/fc03dca9b0432725c2e8cdbf91a349d2194cf03d8523c124faebe581de09/wrapt-1.17.0.tar.gz", hash = "sha256:16187aa2317c731170a88ef35e8937ae0f533c402872c1ee5e6d079fcf320801", size = 55542 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/40/def56538acddc2f764c157d565b9f989072a1d2f2a8e384324e2e104fc7d/wrapt-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:74bf625b1b4caaa7bad51d9003f8b07a468a704e0644a700e936c357c17dd45a", size = 38766 }, + { url = "https://files.pythonhosted.org/packages/89/e2/8c299f384ae4364193724e2adad99f9504599d02a73ec9199bf3f406549d/wrapt-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f2a28eb35cf99d5f5bd12f5dd44a0f41d206db226535b37b0c60e9da162c3ed", size = 83730 }, + { url = "https://files.pythonhosted.org/packages/29/ef/fcdb776b12df5ea7180d065b28fa6bb27ac785dddcd7202a0b6962bbdb47/wrapt-1.17.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:81b1289e99cf4bad07c23393ab447e5e96db0ab50974a280f7954b071d41b489", size = 75470 }, + { url = "https://files.pythonhosted.org/packages/55/b5/698bd0bf9fbb3ddb3a2feefbb7ad0dea1205f5d7d05b9cbab54f5db731aa/wrapt-1.17.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f2939cd4a2a52ca32bc0b359015718472d7f6de870760342e7ba295be9ebaf9", size = 83168 }, + { url = "https://files.pythonhosted.org/packages/ce/07/701a5cee28cb4d5df030d4b2649319e36f3d9fdd8000ef1d84eb06b9860d/wrapt-1.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6a9653131bda68a1f029c52157fd81e11f07d485df55410401f745007bd6d339", size = 82307 }, + { url = "https://files.pythonhosted.org/packages/42/92/c48ba92cda6f74cb914dc3c5bba9650dc80b790e121c4b987f3a46b028f5/wrapt-1.17.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4e4b4385363de9052dac1a67bfb535c376f3d19c238b5f36bddc95efae15e12d", size = 75101 }, + { url = "https://files.pythonhosted.org/packages/8a/0a/9276d3269334138b88a2947efaaf6335f61d547698e50dff672ade24f2c6/wrapt-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bdf62d25234290db1837875d4dceb2151e4ea7f9fff2ed41c0fde23ed542eb5b", size = 81835 }, + { url = "https://files.pythonhosted.org/packages/b9/4c/39595e692753ef656ea94b51382cc9aea662fef59d7910128f5906486f0e/wrapt-1.17.0-cp311-cp311-win32.whl", hash = "sha256:5d8fd17635b262448ab8f99230fe4dac991af1dabdbb92f7a70a6afac8a7e346", size = 36412 }, + { url = "https://files.pythonhosted.org/packages/63/bb/c293a67fb765a2ada48f48cd0f2bb957da8161439da4c03ea123b9894c02/wrapt-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:92a3d214d5e53cb1db8b015f30d544bc9d3f7179a05feb8f16df713cecc2620a", size = 38744 }, + { url = "https://files.pythonhosted.org/packages/85/82/518605474beafff11f1a34759f6410ab429abff9f7881858a447e0d20712/wrapt-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:89fc28495896097622c3fc238915c79365dd0ede02f9a82ce436b13bd0ab7569", size = 38904 }, + { url = "https://files.pythonhosted.org/packages/80/6c/17c3b2fed28edfd96d8417c865ef0b4c955dc52c4e375d86f459f14340f1/wrapt-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:875d240fdbdbe9e11f9831901fb8719da0bd4e6131f83aa9f69b96d18fae7504", size = 88622 }, + { url = "https://files.pythonhosted.org/packages/4a/11/60ecdf3b0fd3dca18978d89acb5d095a05f23299216e925fcd2717c81d93/wrapt-1.17.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ed16d95fd142e9c72b6c10b06514ad30e846a0d0917ab406186541fe68b451", size = 80920 }, + { url = "https://files.pythonhosted.org/packages/d2/50/dbef1a651578a3520d4534c1e434989e3620380c1ad97e309576b47f0ada/wrapt-1.17.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18b956061b8db634120b58f668592a772e87e2e78bc1f6a906cfcaa0cc7991c1", size = 89170 }, + { url = "https://files.pythonhosted.org/packages/44/a2/78c5956bf39955288c9e0dd62e807b308c3aa15a0f611fbff52aa8d6b5ea/wrapt-1.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:daba396199399ccabafbfc509037ac635a6bc18510ad1add8fd16d4739cdd106", size = 86748 }, + { url = "https://files.pythonhosted.org/packages/99/49/2ee413c78fc0bdfebe5bee590bf3becdc1fab0096a7a9c3b5c9666b2415f/wrapt-1.17.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4d63f4d446e10ad19ed01188d6c1e1bb134cde8c18b0aa2acfd973d41fcc5ada", size = 79734 }, + { url = "https://files.pythonhosted.org/packages/c0/8c/4221b7b270e36be90f0930fe15a4755a6ea24093f90b510166e9ed7861ea/wrapt-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8a5e7cc39a45fc430af1aefc4d77ee6bad72c5bcdb1322cfde852c15192b8bd4", size = 87552 }, + { url = "https://files.pythonhosted.org/packages/4c/6b/1aaccf3efe58eb95e10ce8e77c8909b7a6b0da93449a92c4e6d6d10b3a3d/wrapt-1.17.0-cp312-cp312-win32.whl", hash = "sha256:0a0a1a1ec28b641f2a3a2c35cbe86c00051c04fffcfcc577ffcdd707df3f8635", size = 36647 }, + { url = "https://files.pythonhosted.org/packages/b3/4f/243f88ac49df005b9129194c6511b3642818b3e6271ddea47a15e2ee4934/wrapt-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:3c34f6896a01b84bab196f7119770fd8466c8ae3dfa73c59c0bb281e7b588ce7", size = 38830 }, + { url = "https://files.pythonhosted.org/packages/67/9c/38294e1bb92b055222d1b8b6591604ca4468b77b1250f59c15256437644f/wrapt-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:714c12485aa52efbc0fc0ade1e9ab3a70343db82627f90f2ecbc898fdf0bb181", size = 38904 }, + { url = "https://files.pythonhosted.org/packages/78/b6/76597fb362cbf8913a481d41b14b049a8813cd402a5d2f84e57957c813ae/wrapt-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da427d311782324a376cacb47c1a4adc43f99fd9d996ffc1b3e8529c4074d393", size = 88608 }, + { url = "https://files.pythonhosted.org/packages/bc/69/b500884e45b3881926b5f69188dc542fb5880019d15c8a0df1ab1dfda1f7/wrapt-1.17.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba1739fb38441a27a676f4de4123d3e858e494fac05868b7a281c0a383c098f4", size = 80879 }, + { url = "https://files.pythonhosted.org/packages/52/31/f4cc58afe29eab8a50ac5969963010c8b60987e719c478a5024bce39bc42/wrapt-1.17.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e711fc1acc7468463bc084d1b68561e40d1eaa135d8c509a65dd534403d83d7b", size = 89119 }, + { url = "https://files.pythonhosted.org/packages/aa/9c/05ab6bf75dbae7a9d34975fb6ee577e086c1c26cde3b6cf6051726d33c7c/wrapt-1.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:140ea00c87fafc42739bd74a94a5a9003f8e72c27c47cd4f61d8e05e6dec8721", size = 86778 }, + { url = "https://files.pythonhosted.org/packages/0e/6c/4b8d42e3db355603d35fe5c9db79c28f2472a6fd1ccf4dc25ae46739672a/wrapt-1.17.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:73a96fd11d2b2e77d623a7f26e004cc31f131a365add1ce1ce9a19e55a1eef90", size = 79793 }, + { url = "https://files.pythonhosted.org/packages/69/23/90e3a2ee210c0843b2c2a49b3b97ffcf9cad1387cb18cbeef9218631ed5a/wrapt-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0b48554952f0f387984da81ccfa73b62e52817a4386d070c75e4db7d43a28c4a", size = 87606 }, + { url = "https://files.pythonhosted.org/packages/5f/06/3683126491ca787d8d71d8d340e775d40767c5efedb35039d987203393b7/wrapt-1.17.0-cp313-cp313-win32.whl", hash = "sha256:498fec8da10e3e62edd1e7368f4b24aa362ac0ad931e678332d1b209aec93045", size = 36651 }, + { url = "https://files.pythonhosted.org/packages/f1/bc/3bf6d2ca0d2c030d324ef9272bea0a8fdaff68f3d1fa7be7a61da88e51f7/wrapt-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd136bb85f4568fffca995bd3c8d52080b1e5b225dbf1c2b17b66b4c5fa02838", size = 38835 }, + { url = "https://files.pythonhosted.org/packages/ce/b5/251165c232d87197a81cd362eeb5104d661a2dd3aa1f0b33e4bf61dda8b8/wrapt-1.17.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:17fcf043d0b4724858f25b8826c36e08f9fb2e475410bece0ec44a22d533da9b", size = 40146 }, + { url = "https://files.pythonhosted.org/packages/89/33/1e1bdd3e866eeb73d8c4755db1ceb8a80d5bd51ee4648b3f2247adec4e67/wrapt-1.17.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4a557d97f12813dc5e18dad9fa765ae44ddd56a672bb5de4825527c847d6379", size = 113444 }, + { url = "https://files.pythonhosted.org/packages/9f/7c/94f53b065a43f5dc1fbdd8b80fd8f41284315b543805c956619c0b8d92f0/wrapt-1.17.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0229b247b0fc7dee0d36176cbb79dbaf2a9eb7ecc50ec3121f40ef443155fb1d", size = 101246 }, + { url = "https://files.pythonhosted.org/packages/62/5d/640360baac6ea6018ed5e34e6e80e33cfbae2aefde24f117587cd5efd4b7/wrapt-1.17.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8425cfce27b8b20c9b89d77fb50e368d8306a90bf2b6eef2cdf5cd5083adf83f", size = 109320 }, + { url = "https://files.pythonhosted.org/packages/e3/cf/6c7a00ae86a2e9482c91170aefe93f4ccda06c1ac86c4de637c69133da59/wrapt-1.17.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9c900108df470060174108012de06d45f514aa4ec21a191e7ab42988ff42a86c", size = 110193 }, + { url = "https://files.pythonhosted.org/packages/cd/cc/aa718df0d20287e8f953ce0e2f70c0af0fba1d3c367db7ee8bdc46ea7003/wrapt-1.17.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4e547b447073fc0dbfcbff15154c1be8823d10dab4ad401bdb1575e3fdedff1b", size = 100460 }, + { url = "https://files.pythonhosted.org/packages/f7/16/9f3ac99fe1f6caaa789d67b4e3c562898b532c250769f5255fa8b8b93983/wrapt-1.17.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:914f66f3b6fc7b915d46c1cc424bc2441841083de01b90f9e81109c9759e43ab", size = 106347 }, + { url = "https://files.pythonhosted.org/packages/64/85/c77a331b2c06af49a687f8b926fc2d111047a51e6f0b0a4baa01ff3a673a/wrapt-1.17.0-cp313-cp313t-win32.whl", hash = "sha256:a4192b45dff127c7d69b3bdfb4d3e47b64179a0b9900b6351859f3001397dabf", size = 37971 }, + { url = "https://files.pythonhosted.org/packages/05/9b/b2469f8be9efed24283fd7b9eeb8e913e9bc0715cf919ea8645e428ab7af/wrapt-1.17.0-cp313-cp313t-win_amd64.whl", hash = "sha256:4f643df3d4419ea3f856c5c3f40fec1d65ea2e89ec812c83f7767c8730f9827a", size = 40755 }, + { url = "https://files.pythonhosted.org/packages/4b/d9/a8ba5e9507a9af1917285d118388c5eb7a81834873f45df213a6fe923774/wrapt-1.17.0-py3-none-any.whl", hash = "sha256:d2c63b93548eda58abf5188e505ffed0229bf675f7c3090f8e36ad55b8cbc371", size = 23592 }, +] + +[[package]] +name = "xxhash" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/5e/d6e5258d69df8b4ed8c83b6664f2b47d30d2dec551a29ad72a6c69eafd31/xxhash-3.5.0.tar.gz", hash = "sha256:84f2caddf951c9cbf8dc2e22a89d4ccf5d86391ac6418fe81e3c67d0cf60b45f", size = 84241 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/c7/afed0f131fbda960ff15eee7f304fa0eeb2d58770fade99897984852ef23/xxhash-3.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:02c2e816896dc6f85922ced60097bcf6f008dedfc5073dcba32f9c8dd786f3c1", size = 31969 }, + { url = "https://files.pythonhosted.org/packages/8c/0c/7c3bc6d87e5235672fcc2fb42fd5ad79fe1033925f71bf549ee068c7d1ca/xxhash-3.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6027dcd885e21581e46d3c7f682cfb2b870942feeed58a21c29583512c3f09f8", size = 30800 }, + { url = "https://files.pythonhosted.org/packages/04/9e/01067981d98069eec1c20201f8c145367698e9056f8bc295346e4ea32dd1/xxhash-3.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1308fa542bbdbf2fa85e9e66b1077eea3a88bef38ee8a06270b4298a7a62a166", size = 221566 }, + { url = "https://files.pythonhosted.org/packages/d4/09/d4996de4059c3ce5342b6e1e6a77c9d6c91acce31f6ed979891872dd162b/xxhash-3.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c28b2fdcee797e1c1961cd3bcd3d545cab22ad202c846235197935e1df2f8ef7", size = 201214 }, + { url = "https://files.pythonhosted.org/packages/62/f5/6d2dc9f8d55a7ce0f5e7bfef916e67536f01b85d32a9fbf137d4cadbee38/xxhash-3.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:924361811732ddad75ff23e90efd9ccfda4f664132feecb90895bade6a1b4623", size = 429433 }, + { url = "https://files.pythonhosted.org/packages/d9/72/9256303f10e41ab004799a4aa74b80b3c5977d6383ae4550548b24bd1971/xxhash-3.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89997aa1c4b6a5b1e5b588979d1da048a3c6f15e55c11d117a56b75c84531f5a", size = 194822 }, + { url = "https://files.pythonhosted.org/packages/34/92/1a3a29acd08248a34b0e6a94f4e0ed9b8379a4ff471f1668e4dce7bdbaa8/xxhash-3.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:685c4f4e8c59837de103344eb1c8a3851f670309eb5c361f746805c5471b8c88", size = 208538 }, + { url = "https://files.pythonhosted.org/packages/53/ad/7fa1a109663366de42f724a1cdb8e796a260dbac45047bce153bc1e18abf/xxhash-3.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dbd2ecfbfee70bc1a4acb7461fa6af7748ec2ab08ac0fa298f281c51518f982c", size = 216953 }, + { url = "https://files.pythonhosted.org/packages/35/02/137300e24203bf2b2a49b48ce898ecce6fd01789c0fcd9c686c0a002d129/xxhash-3.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:25b5a51dc3dfb20a10833c8eee25903fd2e14059e9afcd329c9da20609a307b2", size = 203594 }, + { url = "https://files.pythonhosted.org/packages/23/03/aeceb273933d7eee248c4322b98b8e971f06cc3880e5f7602c94e5578af5/xxhash-3.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a8fb786fb754ef6ff8c120cb96629fb518f8eb5a61a16aac3a979a9dbd40a084", size = 210971 }, + { url = "https://files.pythonhosted.org/packages/e3/64/ed82ec09489474cbb35c716b189ddc1521d8b3de12b1b5ab41ce7f70253c/xxhash-3.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a905ad00ad1e1c34fe4e9d7c1d949ab09c6fa90c919860c1534ff479f40fd12d", size = 415050 }, + { url = "https://files.pythonhosted.org/packages/71/43/6db4c02dcb488ad4e03bc86d70506c3d40a384ee73c9b5c93338eb1f3c23/xxhash-3.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:963be41bcd49f53af6d795f65c0da9b4cc518c0dd9c47145c98f61cb464f4839", size = 192216 }, + { url = "https://files.pythonhosted.org/packages/22/6d/db4abec29e7a567455344433d095fdb39c97db6955bb4a2c432e486b4d28/xxhash-3.5.0-cp311-cp311-win32.whl", hash = "sha256:109b436096d0a2dd039c355fa3414160ec4d843dfecc64a14077332a00aeb7da", size = 30120 }, + { url = "https://files.pythonhosted.org/packages/52/1c/fa3b61c0cf03e1da4767213672efe186b1dfa4fc901a4a694fb184a513d1/xxhash-3.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:b702f806693201ad6c0a05ddbbe4c8f359626d0b3305f766077d51388a6bac58", size = 30003 }, + { url = "https://files.pythonhosted.org/packages/6b/8e/9e6fc572acf6e1cc7ccb01973c213f895cb8668a9d4c2b58a99350da14b7/xxhash-3.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:c4dcb4120d0cc3cc448624147dba64e9021b278c63e34a38789b688fd0da9bf3", size = 26777 }, + { url = "https://files.pythonhosted.org/packages/07/0e/1bfce2502c57d7e2e787600b31c83535af83746885aa1a5f153d8c8059d6/xxhash-3.5.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:14470ace8bd3b5d51318782cd94e6f94431974f16cb3b8dc15d52f3b69df8e00", size = 31969 }, + { url = "https://files.pythonhosted.org/packages/3f/d6/8ca450d6fe5b71ce521b4e5db69622383d039e2b253e9b2f24f93265b52c/xxhash-3.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:59aa1203de1cb96dbeab595ded0ad0c0056bb2245ae11fac11c0ceea861382b9", size = 30787 }, + { url = "https://files.pythonhosted.org/packages/5b/84/de7c89bc6ef63d750159086a6ada6416cc4349eab23f76ab870407178b93/xxhash-3.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08424f6648526076e28fae6ea2806c0a7d504b9ef05ae61d196d571e5c879c84", size = 220959 }, + { url = "https://files.pythonhosted.org/packages/fe/86/51258d3e8a8545ff26468c977101964c14d56a8a37f5835bc0082426c672/xxhash-3.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:61a1ff00674879725b194695e17f23d3248998b843eb5e933007ca743310f793", size = 200006 }, + { url = "https://files.pythonhosted.org/packages/02/0a/96973bd325412feccf23cf3680fd2246aebf4b789122f938d5557c54a6b2/xxhash-3.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2f2c61bee5844d41c3eb015ac652a0229e901074951ae48581d58bfb2ba01be", size = 428326 }, + { url = "https://files.pythonhosted.org/packages/11/a7/81dba5010f7e733de88af9555725146fc133be97ce36533867f4c7e75066/xxhash-3.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d32a592cac88d18cc09a89172e1c32d7f2a6e516c3dfde1b9adb90ab5df54a6", size = 194380 }, + { url = "https://files.pythonhosted.org/packages/fb/7d/f29006ab398a173f4501c0e4977ba288f1c621d878ec217b4ff516810c04/xxhash-3.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70dabf941dede727cca579e8c205e61121afc9b28516752fd65724be1355cc90", size = 207934 }, + { url = "https://files.pythonhosted.org/packages/8a/6e/6e88b8f24612510e73d4d70d9b0c7dff62a2e78451b9f0d042a5462c8d03/xxhash-3.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e5d0ddaca65ecca9c10dcf01730165fd858533d0be84c75c327487c37a906a27", size = 216301 }, + { url = "https://files.pythonhosted.org/packages/af/51/7862f4fa4b75a25c3b4163c8a873f070532fe5f2d3f9b3fc869c8337a398/xxhash-3.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e5b5e16c5a480fe5f59f56c30abdeba09ffd75da8d13f6b9b6fd224d0b4d0a2", size = 203351 }, + { url = "https://files.pythonhosted.org/packages/22/61/8d6a40f288f791cf79ed5bb113159abf0c81d6efb86e734334f698eb4c59/xxhash-3.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149b7914451eb154b3dfaa721315117ea1dac2cc55a01bfbd4df7c68c5dd683d", size = 210294 }, + { url = "https://files.pythonhosted.org/packages/17/02/215c4698955762d45a8158117190261b2dbefe9ae7e5b906768c09d8bc74/xxhash-3.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:eade977f5c96c677035ff39c56ac74d851b1cca7d607ab3d8f23c6b859379cab", size = 414674 }, + { url = "https://files.pythonhosted.org/packages/31/5c/b7a8db8a3237cff3d535261325d95de509f6a8ae439a5a7a4ffcff478189/xxhash-3.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fa9f547bd98f5553d03160967866a71056a60960be00356a15ecc44efb40ba8e", size = 192022 }, + { url = "https://files.pythonhosted.org/packages/78/e3/dd76659b2811b3fd06892a8beb850e1996b63e9235af5a86ea348f053e9e/xxhash-3.5.0-cp312-cp312-win32.whl", hash = "sha256:f7b58d1fd3551b8c80a971199543379be1cee3d0d409e1f6d8b01c1a2eebf1f8", size = 30170 }, + { url = "https://files.pythonhosted.org/packages/d9/6b/1c443fe6cfeb4ad1dcf231cdec96eb94fb43d6498b4469ed8b51f8b59a37/xxhash-3.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:fa0cafd3a2af231b4e113fba24a65d7922af91aeb23774a8b78228e6cd785e3e", size = 30040 }, + { url = "https://files.pythonhosted.org/packages/0f/eb/04405305f290173acc0350eba6d2f1a794b57925df0398861a20fbafa415/xxhash-3.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:586886c7e89cb9828bcd8a5686b12e161368e0064d040e225e72607b43858ba2", size = 26796 }, + { url = "https://files.pythonhosted.org/packages/c9/b8/e4b3ad92d249be5c83fa72916c9091b0965cb0faeff05d9a0a3870ae6bff/xxhash-3.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:37889a0d13b0b7d739cfc128b1c902f04e32de17b33d74b637ad42f1c55101f6", size = 31795 }, + { url = "https://files.pythonhosted.org/packages/fc/d8/b3627a0aebfbfa4c12a41e22af3742cf08c8ea84f5cc3367b5de2d039cce/xxhash-3.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:97a662338797c660178e682f3bc180277b9569a59abfb5925e8620fba00b9fc5", size = 30792 }, + { url = "https://files.pythonhosted.org/packages/c3/cc/762312960691da989c7cd0545cb120ba2a4148741c6ba458aa723c00a3f8/xxhash-3.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f85e0108d51092bdda90672476c7d909c04ada6923c14ff9d913c4f7dc8a3bc", size = 220950 }, + { url = "https://files.pythonhosted.org/packages/fe/e9/cc266f1042c3c13750e86a535496b58beb12bf8c50a915c336136f6168dc/xxhash-3.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd2fd827b0ba763ac919440042302315c564fdb797294d86e8cdd4578e3bc7f3", size = 199980 }, + { url = "https://files.pythonhosted.org/packages/bf/85/a836cd0dc5cc20376de26b346858d0ac9656f8f730998ca4324921a010b9/xxhash-3.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82085c2abec437abebf457c1d12fccb30cc8b3774a0814872511f0f0562c768c", size = 428324 }, + { url = "https://files.pythonhosted.org/packages/b4/0e/15c243775342ce840b9ba34aceace06a1148fa1630cd8ca269e3223987f5/xxhash-3.5.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07fda5de378626e502b42b311b049848c2ef38784d0d67b6f30bb5008642f8eb", size = 194370 }, + { url = "https://files.pythonhosted.org/packages/87/a1/b028bb02636dfdc190da01951d0703b3d904301ed0ef6094d948983bef0e/xxhash-3.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c279f0d2b34ef15f922b77966640ade58b4ccdfef1c4d94b20f2a364617a493f", size = 207911 }, + { url = "https://files.pythonhosted.org/packages/80/d5/73c73b03fc0ac73dacf069fdf6036c9abad82de0a47549e9912c955ab449/xxhash-3.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:89e66ceed67b213dec5a773e2f7a9e8c58f64daeb38c7859d8815d2c89f39ad7", size = 216352 }, + { url = "https://files.pythonhosted.org/packages/b6/2a/5043dba5ddbe35b4fe6ea0a111280ad9c3d4ba477dd0f2d1fe1129bda9d0/xxhash-3.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bcd51708a633410737111e998ceb3b45d3dbc98c0931f743d9bb0a209033a326", size = 203410 }, + { url = "https://files.pythonhosted.org/packages/a2/b2/9a8ded888b7b190aed75b484eb5c853ddd48aa2896e7b59bbfbce442f0a1/xxhash-3.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ff2c0a34eae7df88c868be53a8dd56fbdf592109e21d4bfa092a27b0bf4a7bf", size = 210322 }, + { url = "https://files.pythonhosted.org/packages/98/62/440083fafbc917bf3e4b67c2ade621920dd905517e85631c10aac955c1d2/xxhash-3.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:4e28503dccc7d32e0b9817aa0cbfc1f45f563b2c995b7a66c4c8a0d232e840c7", size = 414725 }, + { url = "https://files.pythonhosted.org/packages/75/db/009206f7076ad60a517e016bb0058381d96a007ce3f79fa91d3010f49cc2/xxhash-3.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a6c50017518329ed65a9e4829154626f008916d36295b6a3ba336e2458824c8c", size = 192070 }, + { url = "https://files.pythonhosted.org/packages/1f/6d/c61e0668943a034abc3a569cdc5aeae37d686d9da7e39cf2ed621d533e36/xxhash-3.5.0-cp313-cp313-win32.whl", hash = "sha256:53a068fe70301ec30d868ece566ac90d873e3bb059cf83c32e76012c889b8637", size = 30172 }, + { url = "https://files.pythonhosted.org/packages/96/14/8416dce965f35e3d24722cdf79361ae154fa23e2ab730e5323aa98d7919e/xxhash-3.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:80babcc30e7a1a484eab952d76a4f4673ff601f54d5142c26826502740e70b43", size = 30041 }, + { url = "https://files.pythonhosted.org/packages/27/ee/518b72faa2073f5aa8e3262408d284892cb79cf2754ba0c3a5870645ef73/xxhash-3.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:4811336f1ce11cac89dcbd18f3a25c527c16311709a89313c3acaf771def2d4b", size = 26801 }, +] + +[[package]] +name = "yarl" +version = "1.18.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/9d/4b94a8e6d2b51b599516a5cb88e5bc99b4d8d4583e468057eaa29d5f0918/yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1", size = 181062 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/93/282b5f4898d8e8efaf0790ba6d10e2245d2c9f30e199d1a85cae9356098c/yarl-1.18.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8503ad47387b8ebd39cbbbdf0bf113e17330ffd339ba1144074da24c545f0069", size = 141555 }, + { url = "https://files.pythonhosted.org/packages/6d/9c/0a49af78df099c283ca3444560f10718fadb8a18dc8b3edf8c7bd9fd7d89/yarl-1.18.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:02ddb6756f8f4517a2d5e99d8b2f272488e18dd0bfbc802f31c16c6c20f22193", size = 94351 }, + { url = "https://files.pythonhosted.org/packages/5a/a1/205ab51e148fdcedad189ca8dd587794c6f119882437d04c33c01a75dece/yarl-1.18.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:67a283dd2882ac98cc6318384f565bffc751ab564605959df4752d42483ad889", size = 92286 }, + { url = "https://files.pythonhosted.org/packages/ed/fe/88b690b30f3f59275fb674f5f93ddd4a3ae796c2b62e5bb9ece8a4914b83/yarl-1.18.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d980e0325b6eddc81331d3f4551e2a333999fb176fd153e075c6d1c2530aa8a8", size = 340649 }, + { url = "https://files.pythonhosted.org/packages/07/eb/3b65499b568e01f36e847cebdc8d7ccb51fff716dbda1ae83c3cbb8ca1c9/yarl-1.18.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b643562c12680b01e17239be267bc306bbc6aac1f34f6444d1bded0c5ce438ca", size = 356623 }, + { url = "https://files.pythonhosted.org/packages/33/46/f559dc184280b745fc76ec6b1954de2c55595f0ec0a7614238b9ebf69618/yarl-1.18.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c017a3b6df3a1bd45b9fa49a0f54005e53fbcad16633870104b66fa1a30a29d8", size = 354007 }, + { url = "https://files.pythonhosted.org/packages/af/ba/1865d85212351ad160f19fb99808acf23aab9a0f8ff31c8c9f1b4d671fc9/yarl-1.18.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75674776d96d7b851b6498f17824ba17849d790a44d282929c42dbb77d4f17ae", size = 344145 }, + { url = "https://files.pythonhosted.org/packages/94/cb/5c3e975d77755d7b3d5193e92056b19d83752ea2da7ab394e22260a7b824/yarl-1.18.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ccaa3a4b521b780a7e771cc336a2dba389a0861592bbce09a476190bb0c8b4b3", size = 336133 }, + { url = "https://files.pythonhosted.org/packages/19/89/b77d3fd249ab52a5c40859815765d35c91425b6bb82e7427ab2f78f5ff55/yarl-1.18.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2d06d3005e668744e11ed80812e61efd77d70bb7f03e33c1598c301eea20efbb", size = 347967 }, + { url = "https://files.pythonhosted.org/packages/35/bd/f6b7630ba2cc06c319c3235634c582a6ab014d52311e7d7c22f9518189b5/yarl-1.18.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:9d41beda9dc97ca9ab0b9888cb71f7539124bc05df02c0cff6e5acc5a19dcc6e", size = 346397 }, + { url = "https://files.pythonhosted.org/packages/18/1a/0b4e367d5a72d1f095318344848e93ea70da728118221f84f1bf6c1e39e7/yarl-1.18.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ba23302c0c61a9999784e73809427c9dbedd79f66a13d84ad1b1943802eaaf59", size = 350206 }, + { url = "https://files.pythonhosted.org/packages/b5/cf/320fff4367341fb77809a2d8d7fe75b5d323a8e1b35710aafe41fdbf327b/yarl-1.18.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6748dbf9bfa5ba1afcc7556b71cda0d7ce5f24768043a02a58846e4a443d808d", size = 362089 }, + { url = "https://files.pythonhosted.org/packages/57/cf/aadba261d8b920253204085268bad5e8cdd86b50162fcb1b10c10834885a/yarl-1.18.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0b0cad37311123211dc91eadcb322ef4d4a66008d3e1bdc404808992260e1a0e", size = 366267 }, + { url = "https://files.pythonhosted.org/packages/54/58/fb4cadd81acdee6dafe14abeb258f876e4dd410518099ae9a35c88d8097c/yarl-1.18.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fb2171a4486bb075316ee754c6d8382ea6eb8b399d4ec62fde2b591f879778a", size = 359141 }, + { url = "https://files.pythonhosted.org/packages/9a/7a/4c571597589da4cd5c14ed2a0b17ac56ec9ee7ee615013f74653169e702d/yarl-1.18.3-cp311-cp311-win32.whl", hash = "sha256:61b1a825a13bef4a5f10b1885245377d3cd0bf87cba068e1d9a88c2ae36880e1", size = 84402 }, + { url = "https://files.pythonhosted.org/packages/ae/7b/8600250b3d89b625f1121d897062f629883c2f45339623b69b1747ec65fa/yarl-1.18.3-cp311-cp311-win_amd64.whl", hash = "sha256:b9d60031cf568c627d028239693fd718025719c02c9f55df0a53e587aab951b5", size = 91030 }, + { url = "https://files.pythonhosted.org/packages/33/85/bd2e2729752ff4c77338e0102914897512e92496375e079ce0150a6dc306/yarl-1.18.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1dd4bdd05407ced96fed3d7f25dbbf88d2ffb045a0db60dbc247f5b3c5c25d50", size = 142644 }, + { url = "https://files.pythonhosted.org/packages/ff/74/1178322cc0f10288d7eefa6e4a85d8d2e28187ccab13d5b844e8b5d7c88d/yarl-1.18.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7c33dd1931a95e5d9a772d0ac5e44cac8957eaf58e3c8da8c1414de7dd27c576", size = 94962 }, + { url = "https://files.pythonhosted.org/packages/be/75/79c6acc0261e2c2ae8a1c41cf12265e91628c8c58ae91f5ff59e29c0787f/yarl-1.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b411eddcfd56a2f0cd6a384e9f4f7aa3efee14b188de13048c25b5e91f1640", size = 92795 }, + { url = "https://files.pythonhosted.org/packages/6b/32/927b2d67a412c31199e83fefdce6e645247b4fb164aa1ecb35a0f9eb2058/yarl-1.18.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436c4fc0a4d66b2badc6c5fc5ef4e47bb10e4fd9bf0c79524ac719a01f3607c2", size = 332368 }, + { url = "https://files.pythonhosted.org/packages/19/e5/859fca07169d6eceeaa4fde1997c91d8abde4e9a7c018e371640c2da2b71/yarl-1.18.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e35ef8683211db69ffe129a25d5634319a677570ab6b2eba4afa860f54eeaf75", size = 342314 }, + { url = "https://files.pythonhosted.org/packages/08/75/76b63ccd91c9e03ab213ef27ae6add2e3400e77e5cdddf8ed2dbc36e3f21/yarl-1.18.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84b2deecba4a3f1a398df819151eb72d29bfeb3b69abb145a00ddc8d30094512", size = 341987 }, + { url = "https://files.pythonhosted.org/packages/1a/e1/a097d5755d3ea8479a42856f51d97eeff7a3a7160593332d98f2709b3580/yarl-1.18.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e5a1fea0fd4f5bfa7440a47eff01d9822a65b4488f7cff83155a0f31a2ecba", size = 336914 }, + { url = "https://files.pythonhosted.org/packages/0b/42/e1b4d0e396b7987feceebe565286c27bc085bf07d61a59508cdaf2d45e63/yarl-1.18.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0e883008013c0e4aef84dcfe2a0b172c4d23c2669412cf5b3371003941f72bb", size = 325765 }, + { url = "https://files.pythonhosted.org/packages/7e/18/03a5834ccc9177f97ca1bbb245b93c13e58e8225276f01eedc4cc98ab820/yarl-1.18.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a3f356548e34a70b0172d8890006c37be92995f62d95a07b4a42e90fba54272", size = 344444 }, + { url = "https://files.pythonhosted.org/packages/c8/03/a713633bdde0640b0472aa197b5b86e90fbc4c5bc05b727b714cd8a40e6d/yarl-1.18.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ccd17349166b1bee6e529b4add61727d3f55edb7babbe4069b5764c9587a8cc6", size = 340760 }, + { url = "https://files.pythonhosted.org/packages/eb/99/f6567e3f3bbad8fd101886ea0276c68ecb86a2b58be0f64077396cd4b95e/yarl-1.18.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b958ddd075ddba5b09bb0be8a6d9906d2ce933aee81100db289badbeb966f54e", size = 346484 }, + { url = "https://files.pythonhosted.org/packages/8e/a9/84717c896b2fc6cb15bd4eecd64e34a2f0a9fd6669e69170c73a8b46795a/yarl-1.18.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c7d79f7d9aabd6011004e33b22bc13056a3e3fb54794d138af57f5ee9d9032cb", size = 359864 }, + { url = "https://files.pythonhosted.org/packages/1e/2e/d0f5f1bef7ee93ed17e739ec8dbcb47794af891f7d165fa6014517b48169/yarl-1.18.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4891ed92157e5430874dad17b15eb1fda57627710756c27422200c52d8a4e393", size = 364537 }, + { url = "https://files.pythonhosted.org/packages/97/8a/568d07c5d4964da5b02621a517532adb8ec5ba181ad1687191fffeda0ab6/yarl-1.18.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ce1af883b94304f493698b00d0f006d56aea98aeb49d75ec7d98cd4a777e9285", size = 357861 }, + { url = "https://files.pythonhosted.org/packages/7d/e3/924c3f64b6b3077889df9a1ece1ed8947e7b61b0a933f2ec93041990a677/yarl-1.18.3-cp312-cp312-win32.whl", hash = "sha256:f91c4803173928a25e1a55b943c81f55b8872f0018be83e3ad4938adffb77dd2", size = 84097 }, + { url = "https://files.pythonhosted.org/packages/34/45/0e055320daaabfc169b21ff6174567b2c910c45617b0d79c68d7ab349b02/yarl-1.18.3-cp312-cp312-win_amd64.whl", hash = "sha256:7e2ee16578af3b52ac2f334c3b1f92262f47e02cc6193c598502bd46f5cd1477", size = 90399 }, + { url = "https://files.pythonhosted.org/packages/30/c7/c790513d5328a8390be8f47be5d52e141f78b66c6c48f48d241ca6bd5265/yarl-1.18.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90adb47ad432332d4f0bc28f83a5963f426ce9a1a8809f5e584e704b82685dcb", size = 140789 }, + { url = "https://files.pythonhosted.org/packages/30/aa/a2f84e93554a578463e2edaaf2300faa61c8701f0898725842c704ba5444/yarl-1.18.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:913829534200eb0f789d45349e55203a091f45c37a2674678744ae52fae23efa", size = 94144 }, + { url = "https://files.pythonhosted.org/packages/c6/fc/d68d8f83714b221a85ce7866832cba36d7c04a68fa6a960b908c2c84f325/yarl-1.18.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef9f7768395923c3039055c14334ba4d926f3baf7b776c923c93d80195624782", size = 91974 }, + { url = "https://files.pythonhosted.org/packages/56/4e/d2563d8323a7e9a414b5b25341b3942af5902a2263d36d20fb17c40411e2/yarl-1.18.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a19f62ff30117e706ebc9090b8ecc79aeb77d0b1f5ec10d2d27a12bc9f66d0", size = 333587 }, + { url = "https://files.pythonhosted.org/packages/25/c9/cfec0bc0cac8d054be223e9f2c7909d3e8442a856af9dbce7e3442a8ec8d/yarl-1.18.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e17c9361d46a4d5addf777c6dd5eab0715a7684c2f11b88c67ac37edfba6c482", size = 344386 }, + { url = "https://files.pythonhosted.org/packages/ab/5d/4c532190113b25f1364d25f4c319322e86232d69175b91f27e3ebc2caf9a/yarl-1.18.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a74a13a4c857a84a845505fd2d68e54826a2cd01935a96efb1e9d86c728e186", size = 345421 }, + { url = "https://files.pythonhosted.org/packages/23/d1/6cdd1632da013aa6ba18cee4d750d953104a5e7aac44e249d9410a972bf5/yarl-1.18.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41f7ce59d6ee7741af71d82020346af364949314ed3d87553763a2df1829cc58", size = 339384 }, + { url = "https://files.pythonhosted.org/packages/9a/c4/6b3c39bec352e441bd30f432cda6ba51681ab19bb8abe023f0d19777aad1/yarl-1.18.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f52a265001d830bc425f82ca9eabda94a64a4d753b07d623a9f2863fde532b53", size = 326689 }, + { url = "https://files.pythonhosted.org/packages/23/30/07fb088f2eefdc0aa4fc1af4e3ca4eb1a3aadd1ce7d866d74c0f124e6a85/yarl-1.18.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:82123d0c954dc58db301f5021a01854a85bf1f3bb7d12ae0c01afc414a882ca2", size = 345453 }, + { url = "https://files.pythonhosted.org/packages/63/09/d54befb48f9cd8eec43797f624ec37783a0266855f4930a91e3d5c7717f8/yarl-1.18.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2ec9bbba33b2d00999af4631a3397d1fd78290c48e2a3e52d8dd72db3a067ac8", size = 341872 }, + { url = "https://files.pythonhosted.org/packages/91/26/fd0ef9bf29dd906a84b59f0cd1281e65b0c3e08c6aa94b57f7d11f593518/yarl-1.18.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbd6748e8ab9b41171bb95c6142faf068f5ef1511935a0aa07025438dd9a9bc1", size = 347497 }, + { url = "https://files.pythonhosted.org/packages/d9/b5/14ac7a256d0511b2ac168d50d4b7d744aea1c1aa20c79f620d1059aab8b2/yarl-1.18.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:877d209b6aebeb5b16c42cbb377f5f94d9e556626b1bfff66d7b0d115be88d0a", size = 359981 }, + { url = "https://files.pythonhosted.org/packages/ca/b3/d493221ad5cbd18bc07e642894030437e405e1413c4236dd5db6e46bcec9/yarl-1.18.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b464c4ab4bfcb41e3bfd3f1c26600d038376c2de3297760dfe064d2cb7ea8e10", size = 366229 }, + { url = "https://files.pythonhosted.org/packages/04/56/6a3e2a5d9152c56c346df9b8fb8edd2c8888b1e03f96324d457e5cf06d34/yarl-1.18.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d39d351e7faf01483cc7ff7c0213c412e38e5a340238826be7e0e4da450fdc8", size = 360383 }, + { url = "https://files.pythonhosted.org/packages/fd/b7/4b3c7c7913a278d445cc6284e59b2e62fa25e72758f888b7a7a39eb8423f/yarl-1.18.3-cp313-cp313-win32.whl", hash = "sha256:61ee62ead9b68b9123ec24bc866cbef297dd266175d53296e2db5e7f797f902d", size = 310152 }, + { url = "https://files.pythonhosted.org/packages/f5/d5/688db678e987c3e0fb17867970700b92603cadf36c56e5fb08f23e822a0c/yarl-1.18.3-cp313-cp313-win_amd64.whl", hash = "sha256:578e281c393af575879990861823ef19d66e2b1d0098414855dd367e234f5b3c", size = 315723 }, + { url = "https://files.pythonhosted.org/packages/f5/4b/a06e0ec3d155924f77835ed2d167ebd3b211a7b0853da1cf8d8414d784ef/yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b", size = 45109 }, +] From 85f13226f08fcdef66e1baadd282c75d8a605beb Mon Sep 17 00:00:00 2001 From: distributedstatemachine! Date: Mon, 30 Dec 2024 10:35:14 +0000 Subject: [PATCH 02/15] fix: install rust --- docker/Dockerfile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docker/Dockerfile b/docker/Dockerfile index 8711fb7..1dcaccf 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -11,11 +11,17 @@ RUN apt-get update && apt-get install -y \ python3-venv \ git \ curl \ + build-essential \ && rm -rf /var/lib/apt/lists/* # Install uv RUN pip install uv +# Install Rust and Cargo +RUN curl https://sh.rustup.rs -sSf | sh -s -- -y + +ENV PATH="/root/.cargo/bin:${PATH}" + # Copy project files COPY . . From 8f71cc027bc66d63ed5e5d3af8aee04776ffcaf7 Mon Sep 17 00:00:00 2001 From: distributedstatemachine! Date: Mon, 30 Dec 2024 11:59:15 +0000 Subject: [PATCH 03/15] ci --- docker-compose-test.yml | 72 ++++++++++++++++++++++++++++++++++ docker/Dockerfile | 3 -- docker/docker-compose-test.yml | 66 +++++++++++++++++-------------- 3 files changed, 108 insertions(+), 33 deletions(-) create mode 100644 docker-compose-test.yml diff --git a/docker-compose-test.yml b/docker-compose-test.yml new file mode 100644 index 0000000..88d15c6 --- /dev/null +++ b/docker-compose-test.yml @@ -0,0 +1,72 @@ +services: + miner1: + build: + context: . + dockerfile: docker/Dockerfile + container_name: templar-miner-M111 + volumes: + - ~/.bittensor/wallets:/root/.bittensor/wallets + - ./logs:/app/logs + environment: + NODE_TYPE: miner + WALLET_NAME: Bistro + WALLET_HOTKEY: M111 + CUDA_DEVICE: cuda:0 + NETWORK: test + DEBUG: 'true' + WANDB_API_KEY: ${WANDB_API_KEY} + deploy: + resources: + reservations: + devices: + - driver: nvidia + device_ids: ['0', '1', '2'] + capabilities: [gpu] + + miner2: + build: + context: . + dockerfile: docker/Dockerfile + container_name: templar-miner-M222 + volumes: + - ~/.bittensor/wallets:/root/.bittensor/wallets + - ./logs:/app/logs + environment: + NODE_TYPE: miner + WALLET_NAME: Bistro + WALLET_HOTKEY: M222 + CUDA_DEVICE: cuda:1 + NETWORK: test + DEBUG: 'true' + WANDB_API_KEY: ${WANDB_API_KEY} + deploy: + resources: + reservations: + devices: + - driver: nvidia + device_ids: ['0', '1', '2'] + capabilities: [gpu] + + validator: + build: + context: . + dockerfile: docker/Dockerfile + container_name: templar-validator-V11 + volumes: + - ~/.bittensor/wallets:/root/.bittensor/wallets + - ./logs:/app/logs + environment: + NODE_TYPE: validator + WALLET_NAME: Bistro + WALLET_HOTKEY: V11 + CUDA_DEVICE: cuda:2 + NETWORK: test + DEBUG: 'true' + WANDB_API_KEY: ${WANDB_API_KEY} + deploy: + resources: + reservations: + devices: + - driver: nvidia + device_ids: ['0', '1', '2'] + capabilities: [gpu] \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile index 1dcaccf..8cbbae0 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -17,9 +17,6 @@ RUN apt-get update && apt-get install -y \ # Install uv RUN pip install uv -# Install Rust and Cargo -RUN curl https://sh.rustup.rs -sSf | sh -s -- -y - ENV PATH="/root/.cargo/bin:${PATH}" # Copy project files diff --git a/docker/docker-compose-test.yml b/docker/docker-compose-test.yml index 2509ccf..16295c1 100644 --- a/docker/docker-compose-test.yml +++ b/docker/docker-compose-test.yml @@ -1,66 +1,72 @@ services: miner1: - build: . + build: + context: . + dockerfile: Dockerfile container_name: templar-miner-M111 volumes: - ~/.bittensor/wallets:/root/.bittensor/wallets - ./logs:/app/logs environment: - - NODE_TYPE=miner - - WALLET_NAME=Bistro - - WALLET_HOTKEY=M111 - - CUDA_DEVICE=cuda:0 - - NETWORK=test - - DEBUG=true - - WANDB_API_KEY=${WANDB_API_KEY} + NODE_TYPE: miner + WALLET_NAME: Bistro + WALLET_HOTKEY: M111 + CUDA_DEVICE: cuda:0 + NETWORK: test + DEBUG: 'true' + WANDB_API_KEY: ${WANDB_API_KEY} deploy: resources: reservations: devices: - driver: nvidia - device_ids: [ '0', '1', '2' ] - capabilities: [ gpu ] + device_ids: ['0', '1', '2'] + capabilities: [gpu] miner2: - build: . + build: + context: . + dockerfile: Dockerfile container_name: templar-miner-M222 volumes: - ~/.bittensor/wallets:/root/.bittensor/wallets - ./logs:/app/logs environment: - - NODE_TYPE=miner - - WALLET_NAME=Bistro - - WALLET_HOTKEY=M222 - - CUDA_DEVICE=cuda:1 - - NETWORK=test - - DEBUG=true - - WANDB_API_KEY=${WANDB_API_KEY} + NODE_TYPE: miner + WALLET_NAME: Bistro + WALLET_HOTKEY: M222 + CUDA_DEVICE: cuda:1 + NETWORK: test + DEBUG: 'true' + WANDB_API_KEY: ${WANDB_API_KEY} deploy: resources: reservations: devices: - driver: nvidia - device_ids: [ '0', '1', '2' ] - capabilities: [ gpu ] + device_ids: ['0', '1', '2'] + capabilities: [gpu] validator: - build: . + build: + context: . + dockerfile: Dockerfile container_name: templar-validator-V11 volumes: - ~/.bittensor/wallets:/root/.bittensor/wallets - ./logs:/app/logs environment: - - NODE_TYPE=validator - - WALLET_NAME=Bistro - - WALLET_HOTKEY=V11 - - CUDA_DEVICE=cuda:2 - - NETWORK=test - - DEBUG=true - - WANDB_API_KEY=${WANDB_API_KEY} + NODE_TYPE: validator + WALLET_NAME: Bistro + WALLET_HOTKEY: V11 + CUDA_DEVICE: cuda:2 + NETWORK: test + DEBUG: 'true' + WANDB_API_KEY: ${WANDB_API_KEY} deploy: resources: reservations: devices: - driver: nvidia - device_ids: [ '0', '1', '2' ] - capabilities: [ gpu ] + device_ids: ['0', '1', '2'] + capabilities: [gpu] \ No newline at end of file From a67379b48994051a9f8cf9547d8039966328bcfa Mon Sep 17 00:00:00 2001 From: distributedstatemachine! Date: Mon, 30 Dec 2024 12:04:43 +0000 Subject: [PATCH 04/15] chore:m remove arm --- .github/workflows/docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 34c0585..4c6b797 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -50,7 +50,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - platforms: linux/amd64,linux/arm64 + platforms: linux/amd64 tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha From 00e328577aef3dcb41792b4ee034fa3a51abf3c5 Mon Sep 17 00:00:00 2001 From: distributedstatemachine! Date: Mon, 30 Dec 2024 13:04:00 +0000 Subject: [PATCH 05/15] fix: release latest , delete old templar dir --- .env-template | 17 + .github/workflows/docker.yml | 9 +- docker/compose.yml | 2 +- src/templar/__init__.py | 38 -- src/templar/autoupdate.py | 327 ---------- src/templar/chain.py | 118 ---- src/templar/checkpoint.py | 1103 --------------------------------- src/templar/commitment.py | 166 ----- src/templar/comms.py | 1102 -------------------------------- src/templar/config.py | 48 -- src/templar/constants.py | 1 - src/templar/dataset.py | 511 --------------- src/templar/hparams.py | 74 --- src/templar/learning_rates.py | 84 --- src/templar/logging.py | 98 --- src/templar/schemas.py | 24 - src/templar/wandb.py | 89 --- src/tplr/config.py | 46 +- 18 files changed, 59 insertions(+), 3798 deletions(-) create mode 100644 .env-template delete mode 100644 src/templar/__init__.py delete mode 100644 src/templar/autoupdate.py delete mode 100644 src/templar/chain.py delete mode 100644 src/templar/checkpoint.py delete mode 100644 src/templar/commitment.py delete mode 100644 src/templar/comms.py delete mode 100644 src/templar/config.py delete mode 100644 src/templar/constants.py delete mode 100644 src/templar/dataset.py delete mode 100644 src/templar/hparams.py delete mode 100644 src/templar/learning_rates.py delete mode 100644 src/templar/logging.py delete mode 100644 src/templar/schemas.py delete mode 100644 src/templar/wandb.py diff --git a/.env-template b/.env-template new file mode 100644 index 0000000..11e910c --- /dev/null +++ b/.env-template @@ -0,0 +1,17 @@ +WANDB_API_KEY= +NODE_TYPE= +WALLET_NAME= +WALLET_HOTKEY= +CUDA_DEVICE= +NETWORK= +DEBUG= +# R2 Configuration +R2_ACCOUNT_ID= + +# Read credentials +R2_READ_ACCESS_KEY_ID= +R2_READ_SECRET_ACCESS_KEY= + +# Write credentials +R2_WRITE_ACCESS_KEY_ID= +R2_WRITE_SECRET_ACCESS_KEY= \ No newline at end of file diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 4c6b797..72c7db0 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -39,10 +39,13 @@ jobs: with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | + # Release tags (v1.0.0, 1.0.0) type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }} - type=sha,prefix= + type=semver,pattern={{raw}} + # Latest tag on release or main branch + type=raw,value=latest,enable={{is_default_branch || startsWith(github.ref, 'refs/tags/v')}} + # SHA for every build + type=sha,prefix=sha-,format=short - name: Build and push Docker image uses: docker/build-push-action@v5 diff --git a/docker/compose.yml b/docker/compose.yml index f3fa1d9..7160ea5 100644 --- a/docker/compose.yml +++ b/docker/compose.yml @@ -1,6 +1,6 @@ services: node: - image: ghcr.io/tplr-ai/templar:latest + image: ghcr.io/tplr-ai/templar:a67379b container_name: templar-${NODE_TYPE:-miner}-${WALLET_HOTKEY} restart: unless-stopped volumes: diff --git a/src/templar/__init__.py b/src/templar/__init__.py deleted file mode 100644 index 898230d..0000000 --- a/src/templar/__init__.py +++ /dev/null @@ -1,38 +0,0 @@ -# The MIT License (MIT) -# © 2024 templar.tech - -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the “Software”), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of -# the Software. - -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -# ruff: noqa -# pylint: disable=all -# mypy: ignore-errors -# type: ignore - -__version__ = "0.1.33" -version_key = 4000 - -# Import package. -from .autoupdate import * -from .chain import * -from .checkpoint import * -from .commitment import * -from .comms import * -from .config import * -from .commitment import * -from .dataset import * -from .hparams import * -from .learning_rates import * -from .logging import * -from .wandb import * diff --git a/src/templar/autoupdate.py b/src/templar/autoupdate.py deleted file mode 100644 index 5c176f2..0000000 --- a/src/templar/autoupdate.py +++ /dev/null @@ -1,327 +0,0 @@ -# Global imports -import asyncio -import aiohttp -from packaging import version -import git -import os -import subprocess -import sys -import threading -import time -import json - -# Local imports -from .config import BUCKET_SECRETS -from .comms import delete_old_version_files -from .logging import logger - - -TARGET_BRANCH = "main" - - -class AutoUpdate(threading.Thread): - """ - Automatic update utility for templar neurons. - """ - - def __init__(self): - super().__init__() - self.daemon = True # Ensure thread exits when main program exits - try: - self.repo = git.Repo(search_parent_directories=True) - except Exception as e: - logger.exception("Failed to initialize the repository", exc_info=e) - sys.exit(1) # Terminate the thread/application - - async def get_remote_version(self): - """ - Asynchronously fetch the remote version string from a remote HTTP endpoint. - """ - try: - url = "https://raw.githubusercontent.com/tplr-ai/templar/main/src/templar/__init__.py" - async with aiohttp.ClientSession() as session: - async with session.get(url, timeout=5) as response: - response.raise_for_status() - content = await response.text() - - for line in content.split("\n"): - if line.startswith("__version__"): - version_info = line.split("=")[1].strip().strip(" \"'") - return version_info - - logger.error("Version string not found in remote __init__.py") - return None - - except Exception as e: - logger.exception( - "Failed to get remote version for version check", exc_info=e - ) - return None - - async def check_version_updated(self): - """ - Asynchronously compares local and remote versions and returns True if the remote version is higher. - """ - remote_version = await self.get_remote_version() - if not remote_version: - logger.error("Failed to get remote version, skipping version check") - return False - - local_version = self.get_local_version() - if not local_version: - logger.error("Failed to get local version, skipping version check") - return False - - local_version_obj = version.parse(local_version) - remote_version_obj = version.parse(remote_version) - logger.info( - f"Version check - remote_version: {remote_version}, local_version: {local_version}" - ) - - if remote_version_obj > local_version_obj: - logger.info( - f"Remote version ({remote_version}) is higher " - f"than local version ({local_version}), automatically updating..." - ) - return True - - return False - - def attempt_update(self): - """ - Attempts to update the local repository to match the remote. - """ - if self.repo.head.is_detached: - logger.error("Repository is in a detached HEAD state. Cannot update.") - return False - - if self.repo.is_dirty(untracked_files=True): - logger.error( - "Repository has uncommitted changes or untracked files. Cannot update." - ) - return False - - try: - origin = self.repo.remote(name="origin") - # Fetch latest changes from remote - origin.fetch() - # Get the current branch - current_branch = self.repo.active_branch - if current_branch.name != TARGET_BRANCH: - logger.error( - f"Current branch ({current_branch.name}) is not the target branch ({TARGET_BRANCH}). Cannot update." - ) - return False - - # Reset local branch to the remote branch - remote_ref = f"origin/{TARGET_BRANCH}" - logger.info( - f"Resetting local branch '{current_branch.name}' to '{remote_ref}'" - ) - self.repo.git.reset("--hard", remote_ref) - logger.info("Successfully reset to the latest commit from remote.") - - # Verify that local and remote commits match - local_commit = self.repo.commit(current_branch) - remote_commit = self.repo.commit(remote_ref) - if local_commit.hexsha != remote_commit.hexsha: - logger.error( - "Local commit does not match remote commit after reset. Rolling back." - ) - self.repo.git.reset("--hard", "HEAD@{1}") # Reset to previous HEAD - return False - - return True - except git.exc.GitCommandError as e: - logger.error(f"Git command failed: {e}") - # Rollback on failure - self.repo.git.reset("--hard", "HEAD@{1}") - return False - except Exception as e: - logger.exception("Failed to update repository.", exc_info=e) - return False - except git.exc.GitCommandError as e: - logger.error(f"Git command failed: {e}") - return False - except Exception as e: - logger.exception("Failed to update repository.", exc_info=e) - return False - - def handle_merge_conflicts(self): - """ - Attempt to automatically resolve any merge conflicts that may have arisen. - """ - try: - self.repo.git.reset("--merge") - origin = self.repo.remote(name="origin") - current_branch = self.repo.active_branch.name - origin.pull(current_branch) - - for item in self.repo.index.diff(None): - file_path = item.a_path - logger.info(f"Resolving conflict in file: {file_path}") - self.repo.git.checkout("--theirs", file_path) - self.repo.index.commit("Resolved merge conflicts automatically") - logger.info("Merge conflicts resolved, repository updated to remote state.") - logger.info("✅ Successfully updated") - return True - except git.GitCommandError as e: - logger.exception( - "Failed to resolve merge conflicts. Please manually pull and update.", - exc_info=e, - ) - return False - - def attempt_package_update(self): - """ - Synchronize dependencies using 'uv sync --extra all'. - """ - logger.info("Attempting to update packages using 'uv sync --extra all'...") - - try: - uv_executable = "uv" - # TODO: Allow specifying the path to 'uv' if it's not in PATH - - subprocess.check_call( - [uv_executable, "sync", "--extra", "all"], - timeout=300, - ) - logger.info("Successfully updated packages using 'uv sync --extra all'.") - except subprocess.CalledProcessError as e: - logger.exception("Failed to synchronize dependencies with uv", exc_info=e) - except FileNotFoundError: - logger.error( - "uv executable not found. Please ensure 'uv' is installed and in PATH." - ) - except Exception as e: - logger.exception( - "Unexpected error during package synchronization", exc_info=e - ) - - async def cleanup_old_versions(self): - """ - Cleans up old version slices from the S3 bucket. - """ - from templar import __version__ - - logger.info( - f"Cleaning up old versions from bucket {BUCKET_SECRETS['bucket_name']}" - ) - await delete_old_version_files(BUCKET_SECRETS["bucket_name"], __version__) - - def try_update(self): - """ - Automatic update entrypoint method. - """ - - if self.repo.head.is_detached or self.repo.active_branch.name != TARGET_BRANCH: - logger.info("Not on the target branch, skipping auto-update") - return - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - try: - logger.info("Checking for updates...") - # Check if remote version is newer - is_update_needed = loop.run_until_complete(self.check_version_updated()) - if not is_update_needed: - logger.info("Local version is up to date. No updates needed.") - return - - logger.info("Attempting auto update") - # Attempt to update code - update_applied = self.attempt_update() - if not update_applied: - logger.info("No updates were applied. Continuing without restart.") - return - - # Now read the local version - local_version = self.get_local_version() - logger.info(f"Local version after update: {local_version}") - - # Synchronize dependencies - self.attempt_package_update() - - # Clean up old versions from the bucket - loop.run_until_complete(self.cleanup_old_versions()) - - # Restart application - logger.info("Attempting to restart the application...") - self.restart_app() - except Exception as e: - logger.exception("Exception during autoupdate process", exc_info=e) - finally: - loop.close() - - def get_pm2_process_name(self): - """ - Attempt to find the current process's PM2 name by using `pm2 jlist` and matching the current PID. - """ - current_pid = os.getpid() - try: - result = subprocess.run( - ["pm2", "jlist"], check=True, capture_output=True, text=True - ) - pm2_data = json.loads(result.stdout) - except Exception as e: - logger.error(f"Error running `pm2 jlist`: {e}") - return None - for proc in pm2_data: - if proc.get("pid") == current_pid: - return proc.get("name") - - return None - - def restart_app(self): - """Restarts the current application appropriately based on the runtime environment.""" - logger.info("Restarting application...") - pm2_name = self.get_pm2_process_name() - if pm2_name: - logger.info( - f"Detected PM2 environment. Restarting PM2 process '{pm2_name}'..." - ) - try: - subprocess.run(["pm2", "restart", pm2_name], check=True) - logger.info(f"Successfully restarted PM2 process '{pm2_name}'.") - sys.exit(0) - except Exception as e: - logger.error(f"Failed to restart PM2 process '{pm2_name}': {e}") - sys.exit(1) - else: - try: - logger.info( - "PM2 process name not found. Performing regular restart using subprocess.Popen" - ) - subprocess.Popen([sys.executable] + sys.argv) - logger.info("New process started. Exiting current process.") - sys.exit(0) - except Exception as e: - logger.exception("Failed to restart application.", exc_info=e) - sys.exit(1) - - def run(self): - """Thread run method to periodically check for updates.""" - while True: - try: - logger.info("Running autoupdate") - self.try_update() - except Exception as e: - logger.exception("Exception during autoupdate check", exc_info=e) - time.sleep(60) - - def get_local_version(self): - """ - Reads the local __version__ from the __init__.py file. - """ - try: - init_py_path = os.path.join(os.path.dirname(__file__), "__init__.py") - with open(init_py_path, "r") as f: - content = f.read() - for line in content.split("\n"): - if line.startswith("__version__"): - local_version = line.split("=")[1].strip().strip(" \"'") - return local_version - logger.error("Could not find __version__ in local __init__.py") - return None - except Exception as e: - logger.exception("Failed to read local version", exc_info=e) - return None diff --git a/src/templar/chain.py b/src/templar/chain.py deleted file mode 100644 index 00015f6..0000000 --- a/src/templar/chain.py +++ /dev/null @@ -1,118 +0,0 @@ -# Global imports -import bittensor as bt -from pydantic import ValidationError - -# Local imports -import templar as tplr -from templar.schemas import Bucket - - -class ChainManager: - def __init__(self, subtensor: bt.Subtensor, wallet: bt.wallet, netuid: int): - """Class used to get commits from and send commits to the blockchain. - - Args: - subtensor: Subtensor network object. - wallet: The wallet associated with the neuron committing the data. - netuid: The unique identifier of the subnetwork. - """ - self.subtensor = subtensor - self.wallet = wallet - self.netuid = netuid - - def commit(self) -> None: - """Commits bucket configuration data to the subtensor network. - - This method prepares and commits bucket configuration data associated - with the wallet to the subtensor network. The data includes: - - Account ID: A string of fixed length 32 characters. - - Access key ID: A string of fixed length 32 characters. - - Secret access key: A string of variable length (up to 64 characters). - - The commitment process involves: - - Fetching the required configuration details from the `tplr.config.BUCKET_SECRETS` - dictionary. - - Concatenating the account ID, access key ID, and secret access key - into a single string, in this exact order. - - Committing the concatenated data to the subtensor network using the - configured `netuid` and wallet. - - **Note:** The order of concatenation (account ID, access key ID, secret - access key) is critical for correct parsing when the data is retrieved. - - Logs provide visibility into the data type and structure before - committing. - - Raises: - Any exceptions that might arise from the subtensor network - communication are propagated. - """ - concatenated = ( - tplr.config.BUCKET_SECRETS["account_id"] - + tplr.config.BUCKET_SECRETS["read"]["access_key_id"] - + tplr.config.BUCKET_SECRETS["read"]["secret_access_key"] - ) - self.subtensor.commit(self.wallet, self.netuid, concatenated) - tplr.logger.info( - f"Committed {type(concatenated)} data of type to the network: {concatenated}" - ) - - def get_commitment(self, uid: int) -> Bucket: - """Retrieves and parses committed bucket configuration data for a given - UID. - - This method fetches commitment data for a specific UID from the - subtensor network and decodes it into a structured format. The - retrieved data is split into the following fields: - - Account ID: A string of fixed length 32 characters. - - Access key ID: A string of fixed length 32 characters. - - Secret access key: A string of variable length (up to 64 characters). - - The parsed fields are then mapped to an instance of the `Bucket` class. - When initializing the Bucket object, the account ID is also used as the - bucket name. - - The retrieval process involves: - - Fetching the commitment data for the specified UID using the - configured `netuid` from the subtensor network. - - Splitting the concatenated string into individual fields based on - their expected lengths and order. - - Mapping the parsed fields to a `Bucket` instance. - - **Note:** The order of fields (bucket name, account ID, access key ID, - secret access key) in the concatenated string is critical for accurate - parsing. - - Args: - uid: The UID of the neuron whose commitment data is being - retrieved. - - Returns: - Bucket: An instance of the `Bucket` class containing the parsed - bucket configuration details. - - Raises: - ValueError: If the parsed data does not conform to the expected - format for the `Bucket` class. - Exception: If an error occurs while retrieving the commitment data - from the subtensor network. - """ - try: - concatenated = self.subtensor.get_commitment(self.netuid, uid) - tplr.logger.success(f"Commitment fetched: {concatenated}") - except Exception as e: - raise Exception(f"Couldn't get commitment from uid {uid} because {e}") - if len(concatenated) != 128: - raise ValueError( - f"Commitment '{concatenated}' is of length {len(concatenated)} but should be of length 128." - ) - - try: - return Bucket( - name=concatenated[:32], - account_id=concatenated[:32], - access_key_id=concatenated[32:64], - secret_access_key=concatenated[64:], - ) - except ValidationError as e: - raise ValueError(f"Invalid data in commitment: {e}") diff --git a/src/templar/checkpoint.py b/src/templar/checkpoint.py deleted file mode 100644 index cd44617..0000000 --- a/src/templar/checkpoint.py +++ /dev/null @@ -1,1103 +0,0 @@ -import asyncio -import time -import aiofiles -import torch -import os -import glob -import re -import shutil -from typing import List, Optional, Union -from aiobotocore.session import get_session -from tqdm import tqdm -from . import __version__ -from . import config # Import config module -from .config import client_config -from .constants import CF_REGION_NAME -from .logging import logger -from .schemas import Bucket -from .commitment import get_all_commitments - - -def get_base_url(account_id: str) -> str: - """Get base URL for R2 storage""" - return f"https://{account_id}.r2.cloudflarestorage.com" - - -async def save_checkpoint( - filename: str, - model: torch.nn.Module, - optimizer: torch.optim.Optimizer = None, - scheduler: torch.optim.lr_scheduler._LRScheduler = None, - global_step: int = 0, - **kwargs, -): - """ - Saves the checkpoint to the specified filename asynchronously. - Uses asyncio.to_thread to avoid blocking the main event loop. - """ - checkpoint = { - "global_step": global_step, - "model_state_dict": model.state_dict(), - } - if optimizer: - checkpoint["optimizer_state_dict"] = optimizer.state_dict() - if scheduler: - checkpoint["scheduler_state_dict"] = scheduler.state_dict() - # Include additional state variables - checkpoint.update(kwargs) - - try: - await asyncio.to_thread(torch.save, checkpoint, filename) - logger.info(f"Checkpoint saved at {filename}") - except Exception as e: - logger.error(f"Failed to save checkpoint at {filename}: {e}") - raise - - -async def load_checkpoint( - filename: str, - model: torch.nn.Module, - optimizer: torch.optim.Optimizer, - scheduler: torch.optim.lr_scheduler._LRScheduler, - device: str = "cpu", - is_validator: bool = False, - hparams=None, -) -> int: - """ - Loads the checkpoint from the specified filename asynchronously. - Adjusts optimizer and scheduler for miners. - """ - try: - logger.info(f"Loading checkpoint from {filename}") - checkpoint = await asyncio.to_thread( - torch.load, filename, map_location=device, weights_only=True - ) - - # Load the model state - model.load_state_dict(checkpoint["model_state_dict"]) - global_step = checkpoint.get("global_step", 0) - logger.info(f"Loaded model state at global step {global_step}") - - # Load optimizer state - optimizer.load_state_dict(checkpoint["optimizer_state_dict"]) - - # Adjust optimizer state if miner - if not is_validator: - # Retrieve validator's learning rate from optimizer state - validator_lr = optimizer.param_groups[0]["lr"] - miner_lr = hparams.learning_rate # Miner's learning rate - - # Compute scaling factor - scaling_factor = validator_lr / miner_lr - - # Scale optimizer's internal states - for state in optimizer.state.values(): - if "exp_avg" in state: - state["exp_avg"].mul_(scaling_factor) - if "exp_avg_sq" in state: - # Optionally adjust exp_avg_sq if needed - pass - - # Update optimizer's learning rate to miner's learning rate - for param_group in optimizer.param_groups: - param_group["lr"] = miner_lr - - logger.info("Adjusted optimizer states for miner.") - - else: - logger.info("Loaded optimizer states for validator.") - - return global_step - - except Exception as e: - logger.error(f"Failed to load checkpoint from {filename}: {e}") - return 0 - - -async def download_checkpoint_from_neuron( - bucket_info: Bucket, - neuron_hotkey: str, - checkpoint_dir: str, -) -> Optional[str]: - """ - Downloads the latest checkpoint file with parallel processing and progress tracking. - Handles multiple processes and provides detailed progress information. - """ - start_time = time.time() - regex_pattern = ( - rf"neuron_checkpoint_{neuron_hotkey}_b(\d+)_v({re.escape(__version__)})\.pth" - ) - local_checkpoint_path = None - chunk_size = 8 * 1024 * 1024 # 8MB chunks - max_concurrent_downloads = 4 - max_retries = 3 - retry_delay = 5 - - # Ensure checkpoint directory exists with absolute path - checkpoint_dir = os.path.abspath(checkpoint_dir) - os.makedirs(checkpoint_dir, exist_ok=True) - - def format_size(size_bytes): - """Convert bytes to human readable format""" - for unit in ["B", "KB", "MB", "GB", "TB"]: - if size_bytes < 1024.0: - return f"{size_bytes:.2f} {unit}" - size_bytes /= 1024.0 - - def create_progress_bar(progress, total_size): - """Create a progress bar with size information""" - width = 50 - filled = int(width * progress / 100) - bar = "█" * filled + "-" * (width - filled) - size_info = ( - f"{format_size(total_size * progress / 100)}/{format_size(total_size)}" - ) - return f"[{bar}] {progress:.1f}% {size_info}" - - session = get_session() - async with session.create_client( - "s3", - endpoint_url=get_base_url(bucket_info.account_id), - region_name=CF_REGION_NAME, - config=client_config, - aws_access_key_id=bucket_info.access_key_id, - aws_secret_access_key=bucket_info.secret_access_key, - ) as s3_client: - try: - # Find latest checkpoint - paginator = s3_client.get_paginator("list_objects_v2") - latest_block_number = -1 - latest_filename = None - file_size = None - - async for page in paginator.paginate( - Bucket=bucket_info.name, Prefix=f"neuron_checkpoint_{neuron_hotkey}_" - ): - for obj in page.get("Contents", []): - key = obj["Key"] - match = re.match(regex_pattern, key) - if match: - block_number = int(match.group(1)) - if block_number > latest_block_number: - latest_block_number = block_number - latest_filename = key - file_size = obj["Size"] - - if not latest_filename: - logger.info(f"No valid checkpoints found for neuron {neuron_hotkey}") - return None - - logger.info( - f"Found latest checkpoint: {latest_filename} ({format_size(file_size)})" - ) - local_checkpoint_path = os.path.join(checkpoint_dir, latest_filename) - temp_path = f"{local_checkpoint_path}.temp" - lock_path = f"{local_checkpoint_path}.lock" - - # Check if file already exists and is complete - if os.path.exists(local_checkpoint_path): - if os.path.getsize(local_checkpoint_path) == file_size: - logger.info( - f"Checkpoint already exists and is complete: {local_checkpoint_path}" - ) - return local_checkpoint_path - - # Try to acquire lock - try: - with open(lock_path, "x") as _: # Atomic file creation - logger.info(f"Acquired lock for downloading: {lock_path}") - except FileExistsError: - # Another process is downloading, wait for it - logger.info("Another process is downloading, waiting...") - for _ in range(30): # Wait up to 30 seconds - await asyncio.sleep(1) - if os.path.exists(local_checkpoint_path): - if os.path.getsize(local_checkpoint_path) == file_size: - logger.info("File downloaded by another process") - try: - os.remove(lock_path) # Try to clean up lock - except OSError as e: - logger.warning(f"Failed to remove lock file: {e}") - return local_checkpoint_path - logger.warning( - "Timeout waiting for other process, proceeding with download" - ) - - try: - # Download chunks - chunks_data = {} - downloaded_size = 0 - semaphore = asyncio.Semaphore(max_concurrent_downloads) - total_chunks = (file_size + chunk_size - 1) // chunk_size - - async def download_chunk(chunk_number: int): - start = chunk_number * chunk_size - end = min(start + chunk_size, file_size) - - for attempt in range(max_retries): - try: - async with semaphore: - response = await s3_client.get_object( - Bucket=bucket_info.name, - Key=latest_filename, - Range=f"bytes={start}-{end-1}", - ) - chunk_data = await response["Body"].read() - - nonlocal downloaded_size - downloaded_size += len(chunk_data) - progress = (downloaded_size / file_size) * 100 - - if chunk_number % 5 == 0 or progress >= 100: - elapsed_time = time.time() - start_time - speed = downloaded_size / ( - 1024 * 1024 * elapsed_time - ) # MB/s - progress_bar = create_progress_bar( - progress, file_size - ) - logger.info( - f"\nDownload Progress: {progress_bar} [{speed:.2f} MB/s]" - ) - - chunks_data[chunk_number] = chunk_data - return True - - except Exception as e: - if attempt == max_retries - 1: - logger.error( - f"Failed to download chunk {chunk_number}: {str(e)}" - ) - return False - await asyncio.sleep(retry_delay * (attempt + 1)) - - # Download all chunks - tasks = [download_chunk(i) for i in range(total_chunks)] - results = await asyncio.gather(*tasks) - - if not all(results): - raise Exception("Some chunks failed to download") - - # Write chunks to temp file - logger.info("Writing chunks to temp file...") - async with aiofiles.open(temp_path, "wb") as f: - for chunk_num in range(total_chunks): - if chunk_num in chunks_data: - await f.write(chunks_data[chunk_num]) - else: - raise Exception(f"Missing chunk {chunk_num}") - - await asyncio.sleep(0.5) # Short delay for file system - - # Verify the temp file - if not os.path.exists(temp_path): - raise Exception(f"Temp file not found at: {temp_path}") - - actual_size = os.path.getsize(temp_path) - if actual_size != file_size: - raise Exception( - f"Size mismatch in temp file: expected {file_size}, got {actual_size}" - ) - - # Move to final location with extra verification - logger.info( - f"Moving temp file to final location: {local_checkpoint_path}" - ) - - # Remove destination file if it exists - if os.path.exists(local_checkpoint_path): - logger.info( - f"Removing existing checkpoint file: {local_checkpoint_path}" - ) - os.remove(local_checkpoint_path) - - try: - # Use shutil.move for more reliable cross-device moves - shutil.move(temp_path, local_checkpoint_path) - - # Verify the move - if not os.path.exists(local_checkpoint_path): - raise Exception( - "Move operation failed - destination file doesn't exist" - ) - - # Double check the source file is gone - if os.path.exists(temp_path): - logger.warning( - "Temp file still exists after move, attempting cleanup" - ) - try: - os.remove(temp_path) - except Exception as e: - logger.warning(f"Failed to cleanup temp file: {e}") - - # Final size verification - final_size = os.path.getsize(local_checkpoint_path) - if final_size != file_size: - raise Exception( - f"Size mismatch in final file: expected {file_size}, got {final_size}" - ) - - # Extra verification - try to open the file - with open(local_checkpoint_path, "rb") as f: - # Read first few bytes to verify file is accessible - f.read(1024) - - logger.info("Move operation successful and verified") - - total_time = time.time() - start_time - avg_speed = (file_size / (1024 * 1024)) / total_time # MB/s - logger.info( - f"Successfully downloaded checkpoint to: {local_checkpoint_path}" - ) - logger.info( - f"Download completed in {total_time:.2f} seconds ({avg_speed:.2f} MB/s average)" - ) - - return local_checkpoint_path - - except Exception as move_e: - logger.error(f"Error during move operation: {str(move_e)}") - # Try to recover the temp file if move failed - if os.path.exists(temp_path) and not os.path.exists( - local_checkpoint_path - ): - try: - shutil.copy2(temp_path, local_checkpoint_path) - logger.info("Recovered file using copy operation") - except Exception as recover_e: - logger.error(f"Failed to recover file: {str(recover_e)}") - raise - - except Exception as e: - logger.error(f"Error during file operations: {str(e)}") - # Cleanup both temp and final files if they exist - for filepath in [temp_path, local_checkpoint_path]: - if filepath and os.path.exists(filepath): - try: - os.remove(filepath) - logger.info(f"Cleaned up file: {filepath}") - except Exception as rm_e: - logger.error( - f"Failed to cleanup file {filepath}: {str(rm_e)}" - ) - return None - - finally: - # Clean up lock file - try: - os.remove(lock_path) - except Exception as e: - logger.warning(f"Failed to remove lock file: {str(e)}") - - except Exception as e: - logger.error(f"Unexpected error: {str(e)}") - return None - - -def get_all_buckets( - netuid: int, - metagraph, - config, -) -> List[Optional[Union[str, Bucket]]]: - """ - Retrieves and parses all bucket commitments from the network. - """ - buckets = [] - commitments = get_all_commitments( - netuid=netuid, - metagraph=metagraph, - config=config, - ) - - for uid in metagraph.uids: - bucket = commitments.get(uid) - logger.debug(f"UID {uid} bucket: {bucket}") - - if bucket is not None: - logger.debug(f"Retrieved valid bucket for UID {uid}: {bucket}") - buckets.append(bucket) - else: - logger.debug(f"No valid bucket found for UID {uid}") - buckets.append(None) - - logger.debug(f"Final list of buckets: {buckets}") - return buckets - - -def get_neuron_with_highest_stake( - metagraph, buckets: List[Optional[Union[str, Bucket]]] -) -> Optional[str]: - """ - Get the hotkey of the neuron with highest stake that has a valid bucket. - """ - try: - highest_stake_uid = int(metagraph.S.argmax()) - if highest_stake_uid < len(buckets) and buckets[highest_stake_uid] is not None: - return metagraph.hotkeys[highest_stake_uid] - logger.warning("No valid bucket found for highest stake neuron") - return None - except Exception as e: - logger.error(f"Error finding highest stake neuron: {e}") - return None - - -async def load_highest_stake_checkpoint( - metagraph, - buckets: List[Optional[Union[str, Bucket]]], - model: torch.nn.Module, - checkpoint_path: str, - device: str = "cpu", - optimizer: Optional[torch.optim.Optimizer] = None, - scheduler: Optional[torch.optim.lr_scheduler._LRScheduler] = None, -) -> int: - """ - Attempts to load checkpoint from the highest stake neuron. - """ - try: - highest_stake_hotkey = get_neuron_with_highest_stake( - metagraph=metagraph, buckets=buckets - ) - - if highest_stake_hotkey: - uid = metagraph.hotkeys.index(highest_stake_hotkey) - bucket_info = buckets[uid] - - if bucket_info: - checkpoint_dir = os.path.dirname(checkpoint_path) - await asyncio.to_thread(os.makedirs, checkpoint_dir, exist_ok=True) - - checkpoint_file = await download_checkpoint_from_neuron( - bucket_info=bucket_info, - neuron_hotkey=highest_stake_hotkey, - checkpoint_dir=checkpoint_dir, - ) - - if checkpoint_file: - global_step, _ = await load_checkpoint( - filename=checkpoint_file, - model=model, - device=device, - optimizer=optimizer, - scheduler=scheduler, - ) - logger.info(f"Resumed from global step {global_step}") - return global_step if global_step is not None else 0 - - logger.warning( - "Failed to download neuron checkpoint. Starting from scratch." - ) - return 0 - - logger.warning( - f"No bucket info for neuron {highest_stake_hotkey}. Starting from scratch." - ) - return 0 - - logger.warning("No neurons found. Starting from scratch.") - return 0 - - except Exception as e: - logger.error(f"Error loading checkpoint: {e}") - return 0 - - -class CheckpointManager: - """ - Improved CheckpointManager that saves and uploads checkpoints asynchronously, - and can clean up old checkpoints, all without blocking the main thread. - """ - - def __init__( - self, - model: torch.nn.Module, - checkpoint_path: str, - wallet, - device: str = "cpu", - optimizer: Optional[torch.optim.Optimizer] = None, - scheduler: Optional[torch.optim.lr_scheduler._LRScheduler] = None, - ): - self.model = model - self.checkpoint_path = checkpoint_path - self.wallet = wallet - self.device = device - self.optimizer = optimizer - self.scheduler = scheduler - self.upload_task = None # Track the upload task - - self.checkpoint_dir = os.path.dirname(self.checkpoint_path) or os.getcwd() - os.makedirs(self.checkpoint_dir, exist_ok=True) - - self._shutdown = False - - async def _save_checkpoint_async( - self, global_step: int, block_number: int, **kwargs - ): - """Asynchronously save a checkpoint.""" - checkpoint = { - "global_step": global_step, - "block_number": block_number, - "model_state_dict": self.model.state_dict(), - } - - if self.optimizer: - checkpoint["optimizer_state_dict"] = self.optimizer.state_dict() - if self.scheduler: - checkpoint["scheduler_state_dict"] = self.scheduler.state_dict() - - checkpoint.update(kwargs) - - filename = f"neuron_checkpoint_{self.wallet.hotkey.ss58_address}_b{block_number}_v{__version__}.pth" - full_path = os.path.join(self.checkpoint_dir, filename) - - await asyncio.to_thread(torch.save, checkpoint, full_path) - self.checkpoint_path = full_path - logger.info(f"Checkpoint saved at {self.checkpoint_path}") - - async def _upload_checkpoint_async(self): - """Async checkpoint upload to S3 with verified parallel uploads.""" - try: - filename = os.path.basename(self.checkpoint_path) - logger.info(f"Starting checkpoint upload to S3: {filename}") - - bucket = config.BUCKET_SECRETS["bucket_name"].split("/")[-1] - chunk_size = 16 * 1024 * 1024 # 16MB chunks - max_concurrent_uploads = 50 - max_retries = 3 - retry_delay = 5 - - session = get_session() - async with session.create_client( - "s3", - endpoint_url=f"https://{config.BUCKET_SECRETS['account_id']}.r2.cloudflarestorage.com", - region_name=CF_REGION_NAME, - config=client_config, - aws_access_key_id=config.BUCKET_SECRETS["write"]["access_key_id"], - aws_secret_access_key=config.BUCKET_SECRETS["write"][ - "secret_access_key" - ], - ) as s3_client: - # Initialize multipart upload - response = await s3_client.create_multipart_upload( - Bucket=bucket, - Key=filename, - CacheControl="no-cache, no-store, must-revalidate", - ) - upload_id = response["UploadId"] - logger.info(f"Initiated multipart upload with ID: {upload_id}") - - try: - total_size = os.path.getsize(self.checkpoint_path) - total_parts = (total_size + chunk_size - 1) // chunk_size - parts = {} # Use dict to track parts by number - uploaded_size = 0 - semaphore = asyncio.Semaphore(max_concurrent_uploads) - upload_tasks = [] - failed_parts = set() - - # Initialize progress bar - pbar = tqdm( - total=total_size, - unit="B", - unit_scale=True, - desc="Uploading checkpoint", - ) - - async def upload_part_with_retry( - part_number: int, offset: int - ) -> dict: - """Upload a single part with retries and verification.""" - for attempt in range(max_retries): - try: - async with semaphore: - async with aiofiles.open( - self.checkpoint_path, "rb" - ) as f: - await f.seek(offset) - chunk = await f.read( - min(chunk_size, total_size - offset) - ) - - response = await s3_client.upload_part( - Bucket=bucket, - Key=filename, - PartNumber=part_number, - UploadId=upload_id, - Body=chunk, - ) - - # Verify part upload - part_size = len(chunk) - if part_size == 0: - raise ValueError( - f"Zero-size chunk for part {part_number}" - ) - - pbar.update(part_size) - - return { - "PartNumber": part_number, - "ETag": response["ETag"], - "Size": part_size, - } - except Exception as e: - if attempt < max_retries - 1: - logger.warning( - f"Retry {attempt + 1}/{max_retries} for part {part_number}: {str(e)}" - ) - await asyncio.sleep(retry_delay) - else: - failed_parts.add(part_number) - raise - - # Create upload tasks for all parts - for part_number in range(1, total_parts + 1): - offset = (part_number - 1) * chunk_size - task = asyncio.create_task( - upload_part_with_retry(part_number, offset) - ) - upload_tasks.append(task) - - # Wait for all uploads and collect results - completed_parts = await asyncio.gather( - *upload_tasks, return_exceptions=True - ) - - # Close progress bar - pbar.close() - - # Process results and check for failures - for part in completed_parts: - if isinstance(part, Exception): - raise Exception(f"Part upload failed: {str(part)}") - parts[part["PartNumber"]] = part - - # Verify all parts are present and ordered - if len(parts) != total_parts: - missing_parts = set(range(1, total_parts + 1)) - set( - parts.keys() - ) - raise Exception(f"Missing parts: {missing_parts}") - - # Sort parts for completion - ordered_parts = [parts[i] for i in range(1, total_parts + 1)] - - # Complete multipart upload - completion_response = await s3_client.complete_multipart_upload( - Bucket=bucket, - Key=filename, - UploadId=upload_id, - MultipartUpload={ - "Parts": [ - {"PartNumber": p["PartNumber"], "ETag": p["ETag"]} - for p in ordered_parts - ] - }, - ) - - # Verify upload completion - try: - head_response = await s3_client.head_object( - Bucket=bucket, Key=filename - ) - if head_response["ContentLength"] != total_size: - raise Exception( - f"Size mismatch: uploaded={head_response['ContentLength']}, expected={total_size}" - ) - - logger.info( - f"Successfully verified upload of {filename} ({total_size} bytes)" - ) - except Exception as e: - raise Exception(f"Upload verification failed: {str(e)}") - - except Exception as e: - logger.error(f"Error during upload: {str(e)}") - try: - await s3_client.abort_multipart_upload( - Bucket=bucket, Key=filename, UploadId=upload_id - ) - logger.info(f"Aborted multipart upload {upload_id}") - except Exception as abort_e: - logger.error( - f"Failed to abort multipart upload: {str(abort_e)}" - ) - raise - - except Exception as e: - logger.exception(f"Failed to upload checkpoint: {e}") - raise - - finally: - # Clean up any remaining tasks - if "upload_tasks" in locals(): - for task in upload_tasks: - if not task.done(): - task.cancel() - - async def _cleanup_old_checkpoints_async(self, max_checkpoints=3): - """ - Asynchronously deletes old checkpoints locally and in S3. - Keeps only the latest 'max_checkpoints'. - """ - logger.info( - f"Starting checkpoint cleanup, keeping latest {max_checkpoints} checkpoints" - ) - pattern = os.path.join( - self.checkpoint_dir, - f"neuron_checkpoint_{self.wallet.hotkey.ss58_address}_b*_v{__version__}.pth", - ) - logger.info(f"Looking for checkpoints matching pattern: {pattern}") - - checkpoint_files = await asyncio.to_thread(glob.glob, pattern) - logger.info(f"Found {len(checkpoint_files)} total checkpoint files") - if len(checkpoint_files) <= max_checkpoints: - logger.info("No cleanup needed - number of checkpoints below threshold") - return - - # Parse block numbers - logger.info("Parsing block numbers from checkpoint filenames") - checkpoints = [] - for filepath in checkpoint_files: - filename = os.path.basename(filepath) - logger.info(f"Processing checkpoint file: {filename}") - match = re.match( - rf"neuron_checkpoint_{self.wallet.hotkey.ss58_address}_b(\d+)_v{__version__}\.pth", - filename, - ) - if match: - block_number = int(match.group(1)) - logger.info(f"Found checkpoint for block {block_number}") - checkpoints.append((block_number, filepath)) - - # Sort by block number descending - checkpoints.sort(reverse=True) - old_checkpoints = checkpoints[max_checkpoints:] - logger.info(f"Identified {len(old_checkpoints)} checkpoints to delete") - - # Delete local files - logger.info("Starting deletion of local checkpoint files") - for block_num, filepath in old_checkpoints: - try: - logger.info( - f"Attempting to delete checkpoint from block {block_num} at {filepath}" - ) - await asyncio.to_thread(os.remove, filepath) - logger.info(f"Successfully deleted local checkpoint: {filepath}") - except Exception as e: - logger.warning(f"Failed to delete local checkpoint {filepath}: {e}") - logger.error(f"Error details: {str(e)}") - - # Delete old checkpoints from S3 - logger.info("Starting deletion of S3 checkpoint files") - await self._delete_old_checkpoints_from_s3(old_checkpoints) - - async def _delete_old_checkpoints_from_s3(self, old_checkpoints): - logger.info(f"Starting S3 checkpoint deletion for {len(old_checkpoints)} files") - bucket = config.BUCKET_SECRETS["bucket_name"].split("/")[-1] - logger.info(f"Using bucket: {bucket}") - - session = get_session() - logger.info("Created aiobotocore session") - - logger.info( - f"Connecting to S3 endpoint: {get_base_url(config.BUCKET_SECRETS['account_id'])}" - ) - async with session.create_client( - "s3", - endpoint_url=get_base_url(config.BUCKET_SECRETS["account_id"]), - region_name=CF_REGION_NAME, - config=client_config, - aws_access_key_id=config.BUCKET_SECRETS["write"]["access_key_id"], - aws_secret_access_key=config.BUCKET_SECRETS["write"]["secret_access_key"], - ) as s3_client: - logger.info("Successfully connected to S3") - - delete_objects = { - "Objects": [ - {"Key": os.path.basename(filepath)} - for _, filepath in old_checkpoints - ], - "Quiet": True, - } - logger.info( - f"Prepared delete request for {len(delete_objects['Objects'])} objects" - ) - - if delete_objects["Objects"]: - try: - logger.info( - f"Attempting to delete objects: {[obj['Key'] for obj in delete_objects['Objects']]}" - ) - response = await s3_client.delete_objects( - Bucket=bucket, Delete=delete_objects - ) - logger.info("Successfully initiated deletion request") - logger.info( - f"Deleted old checkpoints from S3: {delete_objects['Objects']}" - ) - logger.info(f"S3 deletion response: {response}") - - if "Deleted" in response: - logger.info( - f"Successfully deleted {len(response['Deleted'])} objects" - ) - if "Errors" in response: - logger.warning( - f"Failed to delete {len(response['Errors'])} objects: {response['Errors']}" - ) - - except Exception as e: - logger.error(f"Failed to delete old checkpoints from S3: {str(e)}") - logger.error( - f"Full error details: {e.__class__.__name__}: {str(e)}" - ) - - async def load_from_highest_stake( - self, - metagraph, - buckets, - optimizer, - scheduler, - is_validator: bool = False, - hparams=None, - ) -> int: - """ - Attempts to load checkpoint from the highest stake neuron. - """ - try: - await self.cleanup_old_version_checkpoints() - highest_stake_hotkey = get_neuron_with_highest_stake( - metagraph=metagraph, buckets=buckets - ) - - if highest_stake_hotkey: - uid = metagraph.hotkeys.index(highest_stake_hotkey) - bucket_info = buckets[uid] - - if bucket_info: - checkpoint_dir = os.path.dirname(self.checkpoint_path) - await asyncio.to_thread(os.makedirs, checkpoint_dir, exist_ok=True) - - checkpoint_file = await download_checkpoint_from_neuron( - bucket_info=bucket_info, - neuron_hotkey=highest_stake_hotkey, - checkpoint_dir=checkpoint_dir, - ) - - if checkpoint_file: - global_step, _ = await load_checkpoint( - filename=checkpoint_file, - model=self.model, - device=self.device, - optimizer=optimizer, - scheduler=scheduler, - is_validator=is_validator, - hparams=hparams, - ) - logger.info(f"Resumed from global step {global_step}") - return global_step if global_step is not None else 0 - - logger.warning( - "Failed to download neuron checkpoint. Starting from scratch." - ) - return 0 - - logger.warning( - f"No bucket info for neuron {highest_stake_hotkey}. Starting from scratch." - ) - return 0 - - logger.warning("No neurons found. Starting from scratch.") - return 0 - - except Exception as e: - logger.error(f"Error loading checkpoint: {e}") - return 0 - - async def cleanup_old_version_checkpoints(self, keep_latest: bool = True) -> None: - """ - Cleans up checkpoint files that don't match the current version number. - Handles non-existent directories and empty paths gracefully. - - Args: - keep_latest (bool): If True, keeps the latest checkpoint from old versions - as a backup. Defaults to True. - """ - try: - checkpoint_dir = os.path.dirname(self.checkpoint_path) - - # Check if directory exists - if not os.path.exists(checkpoint_dir): - logger.debug(f"Checkpoint directory does not exist: {checkpoint_dir}") - return - - pattern = os.path.join( - checkpoint_dir, - f"neuron_checkpoint_{self.wallet.hotkey.ss58_address}_b*_v*.pth", - ) - - # Get list of checkpoint files - checkpoint_files = await asyncio.to_thread(glob.glob, pattern) - if not checkpoint_files: - logger.debug(f"No checkpoint files found in {checkpoint_dir}") - return - - # Group checkpoints by version - version_groups = {} - for filepath in checkpoint_files: - if not os.path.exists(filepath): # Check if file still exists - continue - - filename = os.path.basename(filepath) - match = re.match( - rf"neuron_checkpoint_{self.wallet.hotkey.ss58_address}_b(\d+)_v(.+)\.pth", - filename, - ) - if match: - block_number = int(match.group(1)) - version = match.group(2) - if version not in version_groups: - version_groups[version] = [] - version_groups[version].append((block_number, filepath)) - - if not version_groups: - logger.debug("No valid checkpoint files found") - return - - # Identify files to delete - to_delete = [] - for version, checkpoints in version_groups.items(): - if version != __version__: # If not current version - if keep_latest: - # Sort by block number and keep only the latest - checkpoints.sort(key=lambda x: x[0], reverse=True) - to_delete.extend(filepath for _, filepath in checkpoints[1:]) - else: - # Delete all checkpoints of old versions - to_delete.extend(filepath for _, filepath in checkpoints) - - if not to_delete: - logger.debug("No old version checkpoints to clean up") - return - - # Delete files - deleted_count = 0 - for filepath in to_delete: - try: - if os.path.exists( - filepath - ): # Double check file exists before deletion - await asyncio.to_thread(os.remove, filepath) - deleted_count += 1 - logger.info(f"Deleted old version checkpoint: {filepath}") - except Exception as e: - logger.warning(f"Failed to delete checkpoint {filepath}: {e}") - - if deleted_count > 0: - logger.info(f"Cleaned up {deleted_count} old version checkpoint(s)") - - except Exception as e: - logger.error(f"Error during checkpoint cleanup: {e}") - - async def save_and_upload(self, global_step: int, block_number: int, **kwargs): - """Save and upload checkpoint asynchronously.""" - try: - start_time = asyncio.get_event_loop().time() - # Save checkpoint - await self._save_checkpoint_async(global_step, block_number, **kwargs) - save_time = asyncio.get_event_loop().time() - start_time - logger.info(f"Checkpoint save took {save_time:.2f} seconds") - - # Schedule new upload and cleanup without canceling existing ones - self.upload_task = asyncio.create_task(self._upload_and_cleanup()) - except Exception as e: - logger.error(f"Error in save_and_upload: {e}") - - async def _upload_and_cleanup(self): - """Uploads the checkpoint and cleans up old ones.""" - try: - start_time = asyncio.get_event_loop().time() - await self._upload_checkpoint_async() - upload_time = asyncio.get_event_loop().time() - start_time - logger.info(f"Checkpoint upload took {upload_time:.2f} seconds") - - cleanup_start = asyncio.get_event_loop().time() - await self._cleanup_old_checkpoints_async() - cleanup_time = asyncio.get_event_loop().time() - cleanup_start - logger.info(f"Checkpoint cleanup took {cleanup_time:.2f} seconds") - except Exception as e: - logger.exception(f"Exception in _upload_and_cleanup: {e}") - - def cleanup(self): - """Cleanup resources if needed.""" - self._shutdown = True - # Let any pending upload tasks complete - logger.info("CheckpointManager shutdown complete") - - -async def load_model_for_eval( - metagraph, - buckets: List[Optional[Union[str, Bucket]]], - model: torch.nn.Module, - checkpoint_path: str, - device: str = "cuda", -) -> tuple[int, int]: # Return (global_step, block_number) - """ - Simplified checkpoint loader that only loads model state for evaluation. - Returns tuple of (global_step, block_number). - """ - try: - # Get highest stake neuron - highest_stake_hotkey = get_neuron_with_highest_stake(metagraph, buckets) - if not highest_stake_hotkey: - logger.warning("No neurons found. Starting from scratch.") - return 0, 0 - - uid = metagraph.hotkeys.index(highest_stake_hotkey) - bucket_info = buckets[uid] - - if bucket_info: - # Download checkpoint - checkpoint_dir = os.path.dirname(checkpoint_path) - await asyncio.to_thread(os.makedirs, checkpoint_dir, exist_ok=True) - - checkpoint_file = await download_checkpoint_from_neuron( - bucket_info=bucket_info, - neuron_hotkey=highest_stake_hotkey, - checkpoint_dir=checkpoint_dir, - ) - - if checkpoint_file: - # Parse block number from filename - regex_pattern = rf"neuron_checkpoint_{highest_stake_hotkey}_b(\d+)_v({re.escape(__version__)})\.pth" - match = re.match(regex_pattern, os.path.basename(checkpoint_file)) - if not match: - logger.warning( - f"Could not parse block number from checkpoint filename: {checkpoint_file}" - ) - return 0, 0 - - block_number = int(match.group(1)) - - # Load only model state - checkpoint = torch.load( - checkpoint_file, map_location=device, weights_only=True - ) - if isinstance(checkpoint, dict) and "model_state_dict" in checkpoint: - model.load_state_dict(checkpoint["model_state_dict"]) - global_step = checkpoint.get("global_step", 0) - logger.info( - f"Loaded model state at global step {global_step} from block {block_number}" - ) - return global_step, block_number - - logger.warning("Failed to download or load checkpoint") - return 0, 0 - - logger.warning(f"No bucket info for neuron {highest_stake_hotkey}") - return 0, 0 - - except Exception as e: - logger.error(f"Error loading checkpoint: {e}") - return 0, 0 diff --git a/src/templar/commitment.py b/src/templar/commitment.py deleted file mode 100644 index cd90ab4..0000000 --- a/src/templar/commitment.py +++ /dev/null @@ -1,166 +0,0 @@ -# Global imports -import bittensor as bt -from retry import retry -from typing import Optional, Dict -from websockets.exceptions import ConnectionClosedOK, WebSocketException -import os -import toml - -# Local imports -from .logging import logger -import templar as tplr -from templar.schemas import Bucket - - -def commit(subtensor: bt.Subtensor, wallet, netuid: int) -> None: - """Commits bucket configuration data to the subtensor network. - - This method prepares and commits bucket configuration data to the subtensor network. - The data includes: - - Account ID: A string of fixed length 32 characters - - Access key ID: A string of fixed length 32 characters - - Secret access key: A string of variable length (up to 64 characters) - - The commitment process involves: - - Concatenating the account ID, access key ID, and secret access key into a single string - - Committing the concatenated data to the subtensor network using the provided netuid and wallet - - Args: - subtensor: The subtensor network interface - wallet: The wallet used to sign the commitment transaction - netuid: The network UID to commit the data to - - Raises: - Any exceptions from the subtensor network communication are propagated - """ - concatenated = ( - tplr.config.BUCKET_SECRETS["account_id"] - + tplr.config.BUCKET_SECRETS["read"]["access_key_id"] - + tplr.config.BUCKET_SECRETS["read"]["secret_access_key"] - ) - subtensor.commit(wallet, netuid, concatenated) - logger.info(f"Committed data to the network: {concatenated}") - - -def get_all_commitments( - netuid: int, - metagraph, - config, - block: Optional[int] = None, -) -> Dict[int, Bucket]: - """Retrieves and parses all commitment data from the network for a given netuid.""" - - @retry( - exceptions=(ConnectionClosedOK, WebSocketException, Exception), - delay=2, - tries=5, - backoff=2, - max_delay=8, - ) - def query_commitments(): - # Create a fresh subtensor using the same config as the original - fresh_subtensor = bt.subtensor(config=config) - - try: - block_hash = ( - None if block is None else fresh_subtensor.get_block_hash(block) - ) - logger.info( - f"Querying commitments for netuid {netuid} at block {'latest' if block_hash is None else block_hash}" - ) - - result = fresh_subtensor.substrate.query_map( - module="Commitments", - storage_function="CommitmentOf", - params=[netuid], - block_hash=block_hash, - ) - return list(result) - finally: - # Always clean up - if ( - hasattr(fresh_subtensor.substrate, "websocket") - and fresh_subtensor.substrate.websocket - ): - fresh_subtensor.substrate.close() - - try: - result = query_commitments() - hotkey_to_uid = dict(zip(metagraph.hotkeys, metagraph.uids)) - commitments = {} - - for key, value in result: - hotkey = key.value - - # Skip blacklisted hotkeys - if hotkey in BLACKLISTED_HOTKEYS: - logger.info(f"Skipping blacklisted hotkey: {hotkey}") - continue - - if hotkey not in hotkey_to_uid: - continue - - uid = hotkey_to_uid[hotkey] - commitment_info = value.value.get("info", {}) - fields = commitment_info.get("fields", []) - - if not fields or not isinstance(fields[0], dict): - continue - - field_value = next(iter(fields[0].values())) - if field_value.startswith("0x"): - field_value = field_value[2:] - - try: - concatenated = bytes.fromhex(field_value).decode("utf-8").strip() - - if len(concatenated) != 128: - logger.debug( - f"Commitment '{concatenated}' has length {len(concatenated)}, expected 128." - ) - continue - - bucket = Bucket( - name=concatenated[:32], - account_id=concatenated[:32], - access_key_id=concatenated[32:64], - secret_access_key=concatenated[64:], - ) - - commitments[uid] = bucket - logger.debug(f"Bucket fetched and parsed for UID {uid}: {bucket.name}") - - except Exception as e: - logger.error( - f"Failed to decode and parse commitment for UID {uid}: {e}" - ) - continue - - return commitments - - except Exception as e: - logger.error(f"Failed to query commitments: {e}") - raise - logger.error(f"Failed to query commitments: {e}") - raise - - -def load_blacklisted_hotkeys(): - blacklist_file = os.path.join( - os.path.dirname(os.path.abspath(__file__)), "..", "..", "blacklist.toml" - ) - blacklist_file = os.path.normpath(blacklist_file) - if os.path.exists(blacklist_file): - try: - config = toml.load(blacklist_file) - hotkeys = config.get("blacklist", {}).get("hotkeys", []) - return set(hotkeys) - except Exception as e: - logger.error(f"Error loading blacklist.toml: {e}") - return set() - else: - logger.warning("blacklist.toml not found.") - return set() - - -BLACKLISTED_HOTKEYS = load_blacklisted_hotkeys() diff --git a/src/templar/comms.py b/src/templar/comms.py deleted file mode 100644 index d0379b0..0000000 --- a/src/templar/comms.py +++ /dev/null @@ -1,1102 +0,0 @@ -# The MIT License (MIT) -# © 2024 templar.tech - -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the “Software”), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of -# the Software. - -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -# Global imports -import aiofiles -import asyncio -import hashlib -import numpy as np -import os -import re -import tempfile -import torch -import uvloop -from aiobotocore.session import get_session -import bittensor as bt -from collections import defaultdict -from filelock import FileLock, Timeout -from types import SimpleNamespace -from typing import List, Dict, Tuple - -# Local imports -from . import __version__ -from .config import ( - BUCKET_SECRETS, - client_config, -) -from templar.constants import CF_REGION_NAME -from templar.logging import logger -from templar.schemas import Bucket - - -def get_base_url(account_id: str) -> str: - """Gets the base URL for Cloudflare R2 storage. - - Args: - account_id (str): The Cloudflare account ID - - Returns: - str: The base URL for R2 storage in the format https://{account_id}.r2.cloudflarestorage.com - """ - return f"https://{account_id}.r2.cloudflarestorage.com" - - -def get_bucket(bucket_secrets: dict[str, str | dict[str, str]]) -> Bucket: - """Creates a Bucket object from bucket secrets configuration. - - Args: - bucket_secrets (dict[str, str | dict[str, str]]): Dictionary containing bucket configuration with: - - bucket_name: Name of the bucket - - account_id: Cloudflare account ID - - read: Dict containing read access credentials: - - access_key_id: Access key ID for read operations - - secret_access_key: Secret access key for read operations - - Returns: - Bucket: A Bucket object initialized with the provided configuration - - Example: - >>> secrets = { - ... "bucket_name": "my-bucket", - ... "account_id": "abc123", - ... "read": { - ... "access_key_id": "KEY123", - ... "secret_access_key": "SECRET123" - ... } - ... } - >>> bucket = get_bucket(secrets) - """ - return Bucket( - name=bucket_secrets["bucket_name"], - account_id=bucket_secrets["account_id"], - access_key_id=bucket_secrets["read"]["access_key_id"], - secret_access_key=bucket_secrets["read"]["secret_access_key"], - ) - - -# Set uvloop as the event loop policy -asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) - -# Define a semaphore to limit concurrent downloads (adjust as needed) -semaphore = asyncio.Semaphore(1000) - - -async def get_slices(filename: str, device: str) -> Dict[str, torch.Tensor]: - """ - Loads model parameter slices from a file with thread-safe locking. - Handles missing files gracefully. - """ - lock_path = f"{filename}.lock" - try: - # Check if file exists before trying to acquire lock - if not os.path.exists(filename): - logger.warning(f"Slice file not found: {filename}") - return {} - - lock = FileLock(lock_path) - with lock.acquire(timeout=1): - # Check again if file exists after acquiring lock - if not os.path.exists(filename): - logger.warning(f"Slice file not found after acquiring lock: {filename}") - return {} - try: - return torch.load( - filename, - map_location=torch.device(device), - weights_only=True, - ) - except ( - torch.serialization.pickle.UnpicklingError, - RuntimeError, - EOFError, - FileNotFoundError, - ) as e: - logger.warning(f"Failed to load slice file {filename}: {e}") - return {} - except Exception as e: - logger.warning(f"Error loading slice file {filename}: {e}") - return {} - except Timeout: - logger.warning(f"Timeout acquiring lock for {filename}") - return {} - except Exception as e: - logger.warning(f"Error during slice loading for {filename}: {e}") - return {} - finally: - # Cleanup lock file if it exists - try: - if os.path.exists(lock_path): - os.remove(lock_path) - except Exception as e: - logger.warning(f"Failed to remove lock file {lock_path}: {e}") - - -async def apply_slices_to_model( - model: torch.nn.Module, - window: int, - seed: str, - compression: int, - save_location: str, - key: str = "slice", -) -> Tuple[int, Dict[str, any]]: - """ - Applies downloaded model parameter slices to a model for a specific window, - weighting each contribution equally based on the norm of each miner's update - and preserving the overall parameter scale. - - Args: - model (torch.nn.Module): The PyTorch model to apply slices to - window (int): The window number to load slices for - seed (str): Seed used to determine which parameters to select - compression (int): Compression factor for parameter selection - save_location (str): Directory where slices are saved - key (str, optional): Prefix for the slice files. Defaults to 'slice'. - - Returns: - int: The maximum global step seen across all applied slices. - Dict: The metric dictionary aggregated participants' slices in a window - """ - max_global_step = 0 - window_metric = {} - - indices_dict = await get_indices_for_window(model, seed, compression) - slice_files = await load_files_for_window( - window=window, save_location=save_location, key=key - ) - - param_sums = { - name: torch.zeros( - len(indices_dict[name]), dtype=param.data.dtype, device=model.device - ) - for name, param in model.named_parameters() - if name in indices_dict - } - slice_norms = [] # Collect norms for computing median - num_files = 0 # Track the number of valid files - - for file_i in slice_files: - try: - filename = os.path.basename(file_i) - match = re.match( - rf"^{key}-{window}-(.+)-v{re.escape(__version__)}\.pt$", - filename, - ) - if not match: - logger.warning( - f"Skipping file {file_i} due to version mismatch in filename." - ) - continue - participant_hotkey = match.group(1) - - slice_i = await get_slices(file_i, model.device) - - # Handle both dictionary and tensor returns - if isinstance(slice_i, dict): - slice_global_step = slice_i.get("global_step") - slice_metric = slice_i.get("slice_metric") - # Remove non-tensor items from the dictionary - tensor_items = { - k: v for k, v in slice_i.items() if isinstance(v, torch.Tensor) - } - else: - # If it's not a dict, assume it's a tensor or tensor-like object - slice_global_step = None - slice_metric = None - tensor_items = slice_i - - if slice_global_step is not None: - max_global_step = max(max_global_step, slice_global_step) - - if slice_metric is not None: - window_metric[participant_hotkey] = slice_metric - - # Compute norm of the slice - slice_norm = 0.0 - slice_values = {} - - try: - for name, param in model.named_parameters(): - # Check if name exists in both dictionaries using dict methods - if ( - not isinstance(tensor_items, dict) - or name not in indices_dict.keys() - or name not in tensor_items.keys() - ): - continue - values = tensor_items[name].to(model.device) - slice_norm += torch.norm(values, p=2).item() ** 2 - slice_values[name] = values - - slice_norm = np.sqrt(slice_norm) + 1e-8 - slice_norms.append(slice_norm) - num_files += 1 - - # Normalize and accumulate - for name, values in slice_values.items(): - normalized_values = values / slice_norm - param_sums[name] += normalized_values - - except RuntimeError as e: - logger.warning(f"Skipping problematic slice {filename}: {str(e)}") - continue - - except Exception as e: - logger.error(f"Error applying slice from {file_i}: {str(e)}") - continue - - if not num_files or not slice_norms: - logger.warning(f"No valid slices found for window {window}") - return max_global_step, window_metric - - # Apply the average of normalized slices - median_norm = torch.median(torch.tensor(slice_norms)) - for name, param in model.named_parameters(): - if name not in indices_dict: - continue - param_indices = indices_dict[name].to(model.device) - avg_param = param_sums[name] / num_files - - avg_param = avg_param * median_norm - # Convert to the appropriate data type - avg_param = avg_param.to(param.data.dtype) - - # Apply the averaged and scaled parameter to the model - param.data.view(-1)[param_indices] = avg_param.clone() - - return max_global_step, window_metric - - -async def upload_slice_for_window( - bucket: str, - model: torch.nn.Module, - window: int, - seed: str, - wallet: "bt.wallet", - compression: int, - save_location: str, - key: str = "slice", - global_step: int = 0, - slice_metric: Dict[str, any] = None, -): - """ - Uploads a slice of model parameters to S3 for a specific window. - Handles concurrent file operations gracefully. - """ - filename = f"{key}-{window}-{wallet.hotkey.ss58_address}-v{__version__}.pt" - logger.debug(f"Uploading slice to S3: {filename}") - - # Prepare the slice data - indices = await get_indices_for_window(model, seed, compression) - - # Create the slice dictionary with global_step - slice_data = {"global_step": global_step} - if slice_metric is not None: - slice_data["slice_metric"] = slice_metric - # Create the slice dictionary with global_step - for name, param in model.named_parameters(): - slice_data[name] = param.data.view(-1)[indices[name].to(model.device)].cpu() - - # Use save_location for temporary file - temp_file_name = os.path.join(save_location, filename) - - try: - # Save the file - torch.save(slice_data, temp_file_name) - - # Upload the file to S3 - session = get_session() - async with session.create_client( - "s3", - endpoint_url=get_base_url(BUCKET_SECRETS["account_id"]), - region_name=CF_REGION_NAME, - config=client_config, - aws_access_key_id=BUCKET_SECRETS["write"]["access_key_id"], - aws_secret_access_key=BUCKET_SECRETS["write"]["secret_access_key"], - ) as s3_client: - try: - with open(temp_file_name, "rb") as f: - await s3_client.put_object(Bucket=bucket, Key=filename, Body=f) - logger.debug(f"Successfully uploaded slice to S3: {filename}") - except Exception as e: - logger.warning(f"Failed to upload slice {filename} to S3: {str(e)}") - # Don't raise, allow process to continue - except Exception as e: - logger.warning( - f"Error during slice preparation/upload for {filename}: {str(e)}" - ) - # Don't raise, allow process to continue - finally: - # Clean up the temporary file if it exists - try: - if os.path.exists(temp_file_name): - os.remove(temp_file_name) - logger.debug(f"Temporary file {temp_file_name} removed") - except Exception as e: - logger.warning( - f"Failed to remove temporary file {temp_file_name}: {str(e)}" - ) - # Don't raise, allow process to continue - - -async def upload_master(bucket: str, model: torch.nn.Module, wallet: "bt.wallet"): - """ - Uploads the master PyTorch model to an S3 bucket. - - Args: - bucket (str): Name of the S3 bucket. - model (torch.nn.Module): The PyTorch model to be uploaded. - wallet (bt.wallet): The wallet object containing the hotkey. - """ - upload_filename = f"master-{wallet.hotkey.ss58_address}.pt" - logger.debug(f"Uploading master model to S3: {upload_filename}") - - session = get_session() - async with session.create_client( - "s3", - endpoint_url=get_base_url(BUCKET_SECRETS["account_id"]), - region_name=CF_REGION_NAME, - config=client_config, - aws_access_key_id=BUCKET_SECRETS["write"]["access_key_id"], - aws_secret_access_key=BUCKET_SECRETS["write"]["secret_access_key"], - ) as s3_client: - try: - # Create a temporary file and write the model state dictionary to it - with tempfile.NamedTemporaryFile(delete=False) as temp_file: - torch.save(model.state_dict(), temp_file) - temp_file_name = temp_file.name - - # Upload the file to S3 - with open(temp_file_name, "rb") as f: - await s3_client.put_object(Bucket=bucket, Key=upload_filename, Body=f) - logger.debug(f"Successfully uploaded master model to S3: {upload_filename}") - except Exception: - logger.exception(f"Failed to upload master model {upload_filename} to S3") - finally: - # Clean up the temporary file - os.remove(temp_file_name) - logger.debug(f"Temporary file {temp_file_name} removed") - - -async def get_indices_for_window( - model: torch.nn.Module, seed: str, compression: int -) -> Dict[str, torch.LongTensor]: - """ - Computes the indices for the given window and compression factor. - - Args: - model (torch.nn.Module): The PyTorch model. - seed (str): The window seed identifier. - compression (int): The compression factor. - - Returns: - Dict[str, torch.LongTensor]: A dictionary mapping parameter names to index tensors. - """ - logger.debug( - f"Computing indices for window seed {seed} with compression {compression}" - ) - result = {} - # Seed the random number generator with the seed - seed = int(hashlib.md5(str(seed).encode("utf-8")).hexdigest(), 16) % (2**32) - rng = np.random.default_rng(seed) - for name, param in model.named_parameters(): - # Randomly select indices based on the compression factor - num_indices = max(1, int(param.numel() // compression)) - indices = rng.choice(param.numel(), size=num_indices, replace=False) - result[name] = torch.from_numpy(indices).long().cpu() - return result - - -async def download_file( - s3_client, bucket: str, filename: str, save_location: str -) -> str: - """ - Downloads a file from S3, using parallel downloads for large files. - - Args: - s3_client: The S3 client. - bucket (str): Name of the S3 bucket. - filename (str): The S3 object key (filename). - - Returns: - str: The path to the downloaded file in the temporary directory. - """ - async with semaphore: - temp_file = os.path.join(save_location, filename) - # Check if the file exists. - if os.path.exists(temp_file): - logger.debug(f"File {temp_file} already exists, skipping download.") - return temp_file - lock_file = f"{temp_file}.lock" - lock = FileLock(lock_file) - try: - # Try to acquire both locks with a timeout - with lock.acquire(timeout=1): - # Proceed to download the file - logger.debug(f"Downloading file {filename} to {temp_file}") - CHUNK_SIZE = 1 * 1024 * 1024 # 1 MB - - response = await s3_client.get_object(Bucket=bucket, Key=filename) - async with aiofiles.open(temp_file, "wb") as outfile: - while True: - chunk = await response["Body"].read(CHUNK_SIZE) - if not chunk: - break - await outfile.write(chunk) - - logger.debug(f"Successfully downloaded file {filename} to {temp_file}") - return temp_file - - except Timeout: - logger.error( - f"Timeout occurred while trying to acquire lock on {lock_file}" - ) - return None - except Exception as e: - logger.exception( - f"Failed to download file {filename} from bucket {bucket}: {e}" - ) - return None - finally: - # The lock is automatically released when exiting the 'with' block - pass - - -async def handle_file( - s3_client, - bucket: str, - filename: str, - hotkey: str, - window: int, - version: str, - save_location: str, -): - """ - Handles downloading a single file from S3. - - Args: - s3_client: The S3 client. - bucket (str): Name of the S3 bucket. - filename (str): The S3 object key (filename). - hotkey (str): The hotkey identifier. - window (int): The window identifier. - version (str): The version extracted from the filename. - - Returns: - SimpleNamespace: An object containing file metadata and the path to the downloaded file, - including the version. - """ - logger.debug( - f"Handling file '{filename}' for window {window} and hotkey '{hotkey}'" - ) - temp_file = await download_file(s3_client, bucket, filename, save_location) - if temp_file: - return SimpleNamespace( - bucket=bucket, - hotkey=hotkey, - filename=filename, - window=window, - temp_file=temp_file, - version=version, - ) - return None - - -async def validate_slice_data(slice_file: str, save_location: str) -> bool: - """ - Validates a slice file and moves it to appropriate directory based on validity. - - Args: - slice_file (str): Path to the slice file - save_location (str): Base directory for organizing slices - - Returns: - bool: True if slice is valid, False otherwise - """ - try: - # Load the slice data - slice_data = torch.load(slice_file, weights_only=True) - - # Basic validation checks - if not isinstance(slice_data, dict): - raise ValueError("Slice data is not a dictionary") - - # Check for required tensor data - has_tensors = False - for key, value in slice_data.items(): - if isinstance(value, torch.Tensor): - has_tensors = True - break - - if not has_tensors: - raise ValueError("No tensor data found in slice") - - return True - - except Exception as e: - # Handle invalid slice - - filename = os.path.basename(slice_file) - logger.warning(f"Invalid slice {filename}: {str(e)}") - - return False - - -async def process_bucket( - s3_client, bucket: str, windows: List[int], key: str, save_location: str -): - """ - Processes a single S3 bucket to download files for specified windows. - - Args: - s3_client: The S3 client to use for operations. - bucket (str): Name of the S3 bucket to process. - windows (List[int]): List of window IDs to download files for. - key (str): Prefix to filter files by. - save_location (str): Base directory for saving and organizing slices. - - Returns: - List[SimpleNamespace]: List of downloaded file metadata objects for valid slices. - """ - # Import the required modules - import re - from templar import __version__ # Ensure __version__ is imported - - logger.debug(f"Processing bucket '{bucket}' for windows {windows}") - files = [] - paginator = s3_client.get_paginator("list_objects_v2") - - for window in windows: - prefix = f"{key}-{window}" - logger.debug(f"Listing objects with prefix '{prefix}' in bucket '{bucket}'") - try: - async for page in paginator.paginate(Bucket=bucket, Prefix=prefix): - logger.trace( - f"Processing page for prefix '{prefix}' in bucket '{bucket}'" - ) - if "Contents" not in page: - logger.trace( - f"No contents found for prefix '{prefix}' in bucket '{bucket}'" - ) - continue - - download_tasks = [] - for obj in page.get("Contents", []): - filename = obj["Key"] - logger.trace( - f"Processing object with key '{filename}' in bucket '{bucket}'" - ) - try: - # Extract hotkey and version from the filename using non-greedy matching - match = re.match(rf"^{key}-{window}-(.+?)-v(.+)\.pt$", filename) - if not match: - logger.error( - f"Filename '{filename}' does not conform to the expected format." - ) - continue - slice_hotkey = match.group(1) - slice_version = match.group(2) - - # Compare version with the expected version - if slice_version != __version__: - logger.warning( - f"Skipping file '{filename}' due to version mismatch " - f"(expected {__version__}, got {slice_version})." - ) - continue - - logger.trace( - f"Parsed filename '{filename}' into window '{window}', " - f"hotkey '{slice_hotkey}', and version '{slice_version}'" - ) - - # Add the download task - download_tasks.append( - handle_file( - s3_client, - bucket, - filename, - slice_hotkey, - window, - slice_version, - save_location, - ) - ) - except ValueError: - logger.exception(f"Error parsing filename '{filename}'") - continue - except Exception as e: - logger.exception( - f"Unexpected error processing filename '{filename}': {e}" - ) - continue - - # Download and validate files concurrently - try: - results = await asyncio.gather( - *download_tasks, return_exceptions=True - ) - for res in results: - if isinstance(res, Exception): - logger.error(f"Download task failed: {res}") - continue - - if not res: - continue - - # Validate the downloaded slice - is_valid = await validate_slice_data( - res.temp_file, save_location - ) - if is_valid: - files.append(res) - - logger.trace( - f"Completed processing page for prefix '{prefix}' in bucket '{bucket}'" - ) - except Exception as e: - logger.exception( - f"Error during asyncio.gather for prefix '{prefix}': {e}" - ) - - except Exception as e: - logger.error( - f"Error listing objects in bucket '{bucket}' with prefix '{prefix}': {e}" - ) - - logger.trace(f"Completed processing bucket '{bucket}' for windows {windows}") - return files - - -async def download_slices_for_buckets_and_windows( - buckets: List[Bucket], windows: List[int], key: str, save_location: str -) -> Dict[int, List[SimpleNamespace]]: - """Downloads model slices from multiple S3 buckets for specified windows. - - This function downloads model slice files from a list of S3 buckets for the given window IDs. - It processes the buckets concurrently and combines the results into a dictionary mapping - window IDs to lists of downloaded slices. - - Args: - buckets (List[Bucket]): List of Bucket objects containing S3 credentials and configuration - windows (List[int]): List of window IDs to download slices for - key (str, optional): Prefix to filter files by. Defaults to "slice" - - Returns: - Dict[int, List[SimpleNamespace]]: Dictionary mapping window IDs to lists of downloaded slices. - Each slice is represented as a SimpleNamespace object containing metadata and file path. - - Example: - >>> buckets = [Bucket(...), Bucket(...)] # List of bucket configs - >>> windows = [1, 2, 3] # Window IDs to download - >>> slices = await download_slices_for_buckets_and_windows(buckets, windows) - >>> print(slices[1]) # Get all slices for window 1 - [Slice(path='/tmp/slice-1-abc.pt'), Slice(path='/tmp/slice-1-def.pt')] - - Note: - - Filters out None buckets from input list - - Downloads files concurrently across buckets - - Uses CloudFront for downloads if configured - - Handles S3 authentication using bucket credentials - - Returns empty dict if no valid buckets provided - """ - # Filter out None buckets - # Filter out None buckets - valid_buckets = [] - for b in buckets: - if b is None: - continue - if isinstance(b, str): - logger.warning(f"Received string instead of Bucket object: {b}") - continue - if not isinstance(b, Bucket): - logger.warning(f"Invalid bucket type: {type(b)}") - continue - valid_buckets.append(b) - - if not valid_buckets: - logger.warning("No valid buckets provided") - return {} - - try: - logger.debug( - f"Downloading files for buckets {[b.name for b in valid_buckets]} and windows {windows}" - ) - except Exception as e: - logger.error(f"Error logging bucket names: {e}") - return {} - - session = get_session() - tasks = [] - for bucket in set(valid_buckets): - async with session.create_client( - "s3", - endpoint_url=get_base_url(bucket.account_id), - region_name=CF_REGION_NAME, - config=client_config, - aws_access_key_id=bucket.access_key_id, - aws_secret_access_key=bucket.secret_access_key, - ) as s3_client: - logger.debug(f"Processing bucket: {bucket.name}") - tasks.append( - process_bucket(s3_client, bucket.name, windows, key, save_location) - ) - - results = await asyncio.gather(*tasks) - # Combine results into a dictionary mapping window IDs to lists of slices - slices = defaultdict(list) - for result in results: - for item in result: - slices[item.window].append(item) - return slices - - -async def load_files_for_window( - window: int, save_location: str, key: str = "slice" -) -> List[str]: - """ - Loads files for a specific window from the temporary directory. - - Args: - window (int): The window identifier to load files for - key (str, optional): The prefix to filter files by. Defaults to 'slice'. - - Returns: - List[str]: A list of full file paths matching the window and key pattern - - Example: - >>> files = await load_files_for_window(123, 'state') - >>> print(files) - ['/tmp/state-123-abc-v1.0.0.pt', '/tmp/state-123-def-v1.0.0.pt'] - - Note: - - Only returns files matching pattern: {key}-{window}-*-v{version}.pt - - Files must be in the system temp directory - - Version number is pulled from templar.__version__ - """ - - logger.debug(f"Retrieving files for window {window} from temporary directory") - temp_dir = save_location - window_files = [] - pattern = re.compile(rf"^{key}-{window}-.+-v{__version__}\.pt$") - for filename in os.listdir(temp_dir): - if pattern.match(filename): - window_files.append(os.path.join(temp_dir, filename)) - logger.debug(f"Found file {filename} for window {window}") - return window_files - - -async def delete_files_before_window( - window_max: int, save_location: str, key: str = "slice" -): - """ - Deletes temporary files with window IDs less than the specified maximum. - - Args: - window_max (int): Maximum window ID to keep. Files with window IDs less than this will be deleted - key (str, optional): The prefix to filter files by. Defaults to 'slice' - - Example: - >>> await delete_files_before_window(100, 'state') - # Deletes all state-*.pt files with window < 100 - - Note: - - Deletes both .pt and .pt.lock files - - Only deletes files matching pattern: {key}-{window}-*-v{version}.pt - - Files must be in system temp directory - - Version number is pulled from templar.__version__ - """ - - logger.debug(f"Deleting files with window id before {window_max}") - temp_dir = save_location - pattern = re.compile(rf"^{re.escape(key)}-(\d+)-.+-v{__version__}\.(pt|pt\.lock)$") - for filename in os.listdir(temp_dir): - match = pattern.match(filename) - if match: - try: - window_id = int(match.group(1)) - if window_id < window_max: - file_path = os.path.join(temp_dir, filename) - if os.path.exists(file_path): - os.remove(file_path) - logger.debug(f"Deleted file {file_path}") - except Exception as e: - logger.error(f"Error deleting file {filename}: {e}") - - -async def delete_files_from_bucket_before_window( - bucket: str, window_max: int, key: str = "slice" -): - """ - Deletes files from an S3 bucket with window IDs less than the specified maximum. - - Args: - bucket (str): Name of the S3 bucket to delete files from - window_max (int): Maximum window ID to keep. Files with window IDs less than this will be deleted - key (str, optional): The prefix to filter files by. Defaults to 'slice' - - Example: - >>> await delete_files_from_bucket_before_window('my-bucket', 100, 'state') - # Deletes all state-*.pt files with window < 100 from my-bucket - - Note: - - Deletes both .pt and .pt.lock files - - Only deletes files matching pattern: {key}-{window}-*-v{version}.pt - - Version number is pulled from templar.__version__ - - Requires valid AWS credentials and bucket permissions - """ - - logger.debug( - f"Deleting files in bucket {bucket} with window id before {window_max}" - ) - session = get_session() - async with session.create_client( - "s3", - endpoint_url=get_base_url(BUCKET_SECRETS["account_id"]), - region_name=CF_REGION_NAME, - config=client_config, - aws_access_key_id=BUCKET_SECRETS["write"]["access_key_id"], - aws_secret_access_key=BUCKET_SECRETS["write"]["secret_access_key"], - ) as s3_client: - try: - response = await s3_client.list_objects_v2(Bucket=bucket) - if "Contents" in response: - for obj in response["Contents"]: - filename = obj["Key"] - match = re.match( - rf"^{re.escape(key)}-(\d+)-.+-v{__version__}\.(pt|pt\.lock)$", - filename, - ) - if match: - try: - window_id = int(match.group(1)) - if window_id < window_max: - await s3_client.delete_object( - Bucket=bucket, Key=filename - ) - logger.debug( - f"Deleted file {filename} from bucket {bucket}" - ) - except Exception as e: - logger.error( - f"Error deleting file {filename} from bucket {bucket}: {e}" - ) - except Exception as e: - logger.error(f"Error listing objects in bucket {bucket}: {e}") - - -BUCKET_REGEX = re.compile( - r"^(?=.{3,63}$)(?!.*\.\.)(?!\-)(?!\.)(?!.*\.$)[a-z0-9]+(?:[\.-][a-z0-9]+)*$" -) - -ARN_REGEX = re.compile( - r"^arn:(aws|aws-cn|aws-us-gov):s3-object-lambda:[a-z0-9\-]+:\d{12}:accesspoint[/:][a-zA-Z0-9.\-_]{1,63}$" - r"|^arn:(aws|aws-cn|aws-us-gov):s3-outposts:[a-z0-9\-]+:\d{12}:outpost[/:][a-zA-Z0-9.\-_]{1,63}[/:]accesspoint[/:][a-zA-Z0-9\-]{1,63}$" -) - - -async def delete_old_version_files(bucket_name: str, current_version: str): - """ - Deletes files from the S3 bucket that do not match the current version. - - Args: - bucket_name (str): The name of the S3 bucket. - current_version (str): The current version string. - """ - session = get_session() - async with session.create_client( - "s3", - endpoint_url=get_base_url(BUCKET_SECRETS["account_id"]), - region_name=CF_REGION_NAME, - config=client_config, - aws_access_key_id=BUCKET_SECRETS["write"]["access_key_id"], - aws_secret_access_key=BUCKET_SECRETS["write"]["secret_access_key"], - ) as s3_client: - paginator = s3_client.get_paginator("list_objects_v2") - async for page in paginator.paginate(Bucket=bucket_name): - to_delete = [] - for obj in page.get("Contents", []): - filename = obj["Key"] - # Check if the file version matches the current version - match = re.match(r".+-v(.+)\.pt$", filename) - if match: - file_version = match.group(1) - if file_version != current_version: - to_delete.append({"Key": filename}) - logger.debug(f"Scheduled for deletion: {filename}") - # Delete old versions in batches of 1000 (S3 limit for delete_objects) - if to_delete: - response = await s3_client.delete_objects( - Bucket=bucket_name, Delete={"Objects": to_delete} - ) - deleted = response.get("Deleted", []) - logger.info( - f"Deleted {len(deleted)} old version files from bucket {bucket_name}" - ) - - -# def is_valid_bucket(bucket_name: str) -> bool: -# """ -# Validates if the bucket name matches AWS S3 bucket naming rules -# and checks if the bucket exists and is accessible. - -# Args: -# bucket_name (str): The bucket name to validate. - -# Returns: -# bool: True if valid and accessible, False otherwise. -# """ -# # Ensure bucket_name is a string -# if isinstance(bucket_name, bytes): -# bucket_name = bucket_name.decode('utf-8') - -# # # Check if the bucket name matches the regex -# # if not (BUCKET_REGEX.match(bucket_name) or ARN_REGEX.match(bucket_name)): -# # logger.debug(f"Invalid bucket name format: {bucket_name}") -# # return False - -# # Create S3 client -# s3_client = boto3.client( -# 's3', -# region_name=CF_REGION_NAME, -# aws_access_key_id=AWS_ACCESS_KEY_ID, -# aws_secret_access_key=AWS_SECRET_ACCESS_KEY, -# config=client_config -# ) - -# # Check if the bucket exists and is accessible -# try: -# # Try to list objects in the bucket -# s3_client.list_objects_v2(Bucket=bucket_name, MaxKeys=1) -# logger.debug(f"Bucket '{bucket_name}' exists and is accessible.") -# return True # Bucket exists and is accessible -# except ClientError as e: -# error_code = e.response['Error']['Code'] -# if error_code in ['NoSuchBucket', '404']: -# logger.debug(f"Bucket '{bucket_name}' does not exist.") -# elif error_code in ['AccessDenied', '403']: -# logger.debug(f"Access denied for bucket '{bucket_name}'.") -# elif error_code == 'AllAccessDisabled': -# logger.debug(f"All access disabled for bucket '{bucket_name}'.") -# else: -# logger.debug(f"Error accessing bucket '{bucket_name}': {e}") -# return False -# except Exception as e: -# logger.debug(f"Unexpected error when accessing bucket '{bucket_name}': {e}") -# return False - -# def validate_bucket_or_exit(bucket_name: str): -# """ -# Validates the bucket name and exits the program if invalid. - -# Args: -# bucket_name (str): The name of the S3 bucket. -# """ -# logger.debug("Validating Bucket name") -# if not is_valid_bucket(bucket_name): -# logger.error(f"Bucket name {bucket_name} is invalid. Please refer to the AWS documentation on naming conventions ") -# sys.exit(1) - - -async def save_checkpoint( - filename, model, optimizer=None, scheduler=None, global_step=0, **kwargs -): - """ - Saves the checkpoint to the specified filename asynchronously. - - Args: - filename (str): Path to save the checkpoint. - model (torch.nn.Module): The model to save. - optimizer (torch.optim.Optimizer, optional): Optimizer to save. - scheduler (torch.optim.lr_scheduler._LRScheduler, optional): Scheduler to save. - global_step (int): The current global step. - **kwargs: Additional state variables to save. - """ - # Gather the checkpoint data - checkpoint = { - "global_step": global_step, - "model_state_dict": model.state_dict(), - } - if optimizer: - checkpoint["optimizer_state_dict"] = optimizer.state_dict() - if scheduler: - checkpoint["scheduler_state_dict"] = scheduler.state_dict() - # Include additional state variables - for key, value in kwargs.items(): - checkpoint[key] = value - - # Save the checkpoint asynchronously to avoid blocking the main thread - loop = asyncio.get_event_loop() - await loop.run_in_executor(None, torch.save, checkpoint, filename) - torch.save(checkpoint, filename) - - -async def load_checkpoint( - filename, model, optimizer=None, scheduler=None, device="cpu" -): - """ - Loads the checkpoint from the specified filename. - - Args: - filename (str): Path to the checkpoint file. - model (torch.nn.Module): The model to load the state into. - optimizer (torch.optim.Optimizer, optional): Optimizer to load the state into. - scheduler (torch.optim.lr_scheduler._LRScheduler, optional): Scheduler to load the state into. - device (str): Device to map the checkpoint. - - Returns: - global_step (int): The global step at which the checkpoint was saved. - additional_state (dict): Dictionary of additional state variables. - """ - try: - checkpoint = torch.load(filename, map_location=device, weights_only=True) - model.load_state_dict(checkpoint["model_state_dict"]) - if optimizer and "optimizer_state_dict" in checkpoint: - optimizer.load_state_dict(checkpoint["optimizer_state_dict"]) - if scheduler and "scheduler_state_dict" in checkpoint: - scheduler.load_state_dict(checkpoint["scheduler_state_dict"]) - global_step = checkpoint.get("global_step", 0) - additional_state = { - k: checkpoint[k] - for k in checkpoint - if k - not in [ - "global_step", - "model_state_dict", - "optimizer_state_dict", - "scheduler_state_dict", - ] - } - return global_step, additional_state - except (torch.serialization.pickle.UnpicklingError, RuntimeError, EOFError) as e: - logger.error(f"Checkpoint at {filename} is corrupt: {e}") - # Return global_step as 0 and an empty additional_state - return 0, {} - except Exception as e: - logger.error(f"Failed to load checkpoint from {filename}: {e}") - return 0, {} - - -def get_neuron_temp_dir(wallet) -> str: - """ - Returns a unique temporary directory for the neuron based on its wallet hotkey. - """ - - temp_dir = os.path.join( - tempfile.gettempdir(), f"neuron_{wallet.hotkey.ss58_address}" - ) - os.makedirs(temp_dir, exist_ok=True) - return temp_dir diff --git a/src/templar/config.py b/src/templar/config.py deleted file mode 100644 index 6153c2e..0000000 --- a/src/templar/config.py +++ /dev/null @@ -1,48 +0,0 @@ -# The MIT License (MIT) -# © 2024 templar.tech - -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the “Software”), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of -# the Software. - -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -# Global imports -import os -import sys -import yaml -from pathlib import Path - -# Local imports -import botocore.config -from dotenv import dotenv_values -from loguru import logger - -# Load environment variables -env_config = {**dotenv_values(".env"), **os.environ} -AWS_ACCESS_KEY_ID = env_config.get("AWS_ACCESS_KEY_ID") -AWS_SECRET_ACCESS_KEY = env_config.get("AWS_SECRET_ACCESS_KEY") - -envfile_path = Path(__file__).parents[2] / ".env.yaml" -try: - with open(envfile_path, "r") as file: - BUCKET_SECRETS = yaml.safe_load(file) -except FileNotFoundError: - logger.error( - f"{envfile_path} not found. Please create it with the help of `.env-template.yaml`." - ) - sys.exit() -BUCKET_SECRETS["bucket_name"] = BUCKET_SECRETS["account_id"] - -# Configure the S3 client -client_config = botocore.config.Config( - max_pool_connections=256, -) diff --git a/src/templar/constants.py b/src/templar/constants.py deleted file mode 100644 index 5a7250e..0000000 --- a/src/templar/constants.py +++ /dev/null @@ -1 +0,0 @@ -CF_REGION_NAME: str = "enam" diff --git a/src/templar/dataset.py b/src/templar/dataset.py deleted file mode 100644 index 208e233..0000000 --- a/src/templar/dataset.py +++ /dev/null @@ -1,511 +0,0 @@ -# The MIT License (MIT) -# © 2024 templar.tech - -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the “Software”), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of -# the Software. - -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -# Global imports -import asyncio -import aiohttp -import numpy as np -import random -import typing -from torch.utils.data import IterableDataset -from transformers import AutoTokenizer - - -class SubsetLoader(IterableDataset): - """ - Base class for data-specific subset loader classes. - - # TODO: Make this class abstract - """ - - def __init__( - self, - batch_size=None, - sequence_length=None, - num_pages=None, - tokenizer: AutoTokenizer = None, - pack_samples: bool = False, - ): - self.batch_size = batch_size - self.sequence_length = sequence_length - self.num_pages = num_pages - self.tokenizer = tokenizer - self.pack_samples = pack_samples - - self.num_rows_per_page = 100 - - # Buffer to hold pages loaded from the api - self.buffer = [] - - # Buffer to hold pages already loaded into a batch - self.used_buffer = [] - - # Buffer to hold padded pages - self.padded_buffer = [] - - self.lock = asyncio.Lock() # For thread-safe operations - - async def fetch_data_for_pages(self, pages): - """ - Set the pages to be used to fill the buffer. Then fetch the page data - to the buffer. - """ - - self.pages = pages - - # Empty the buffer if it is not. - self.buffer = [] - - async with aiohttp.ClientSession() as session: - tasks = [self._fetch_data_for_page(page, session) for page in self.pages] - await asyncio.gather(*tasks) - - async def _fetch_data_for_page(self, page, session): - retry_limit = 10 - attempt = 0 - while attempt < retry_limit: - config_name, page_number, split = page - - # Create the request parameters - params = dict( - dataset=self.name, - config=config_name, - split=split, - offset=page_number, - limit=self.num_rows_per_page, - ) - - try: - async with session.get(self.rows_base_url, params=params) as response: - response.raise_for_status() - data = await response.json() - - # Prepare the data to append - buffer_to_append = [] - for row in data["rows"]: - content = row["row"]["text"] - input_ids = self.tokenizer(content, truncation=True)[ - "input_ids" - ] - buffer_to_append.extend(input_ids) - buffer_to_append.append(self.tokenizer.eos_token_id) - - async with self.lock: - self.buffer.extend(buffer_to_append) - self.pages.append((config_name, page_number, split)) - break # Success, exit retry loop - - except aiohttp.ClientResponseError: - attempt += 1 - if attempt < retry_limit: - await asyncio.sleep(5) - else: - raise - - def _get_pad_size(self, input_ids): - """ - Get the number of tokens to be padded to the sample to match - the max allowed sequence length. - If sample packing is activated, then return 1 - """ - - if self.pack_samples: - return 1 - - sample_size = len(input_ids) - - remainder = sample_size % self.sequence_length - pad_size = self.sequence_length - remainder - - # Apply modulo again to guarantee a pad size of 0 if remainder is 0 - pad_size = pad_size % self.sequence_length - - return pad_size - - def _refill_padded_buffer(self): - """ - This methods pulls one page from `self.buffer`, pads it and pushs - it to the `self.padded_buffer`. - """ - - while self.buffer and len(self.padded_buffer) < self.sequence_length: - input_ids = [] - - # search for EOS token index and cut the buffer at it. - EOS_index = self.buffer.index(self.tokenizer.eos_token_id) - input_ids = self.buffer[: EOS_index + 1] - self.buffer = self.buffer[EOS_index + 1 :] - - self.used_buffer += input_ids - - # Add to padded buffer without the EOS token. - self.padded_buffer += input_ids[:-1] - - # Pad - self.padded_buffer += [self.tokenizer.eos_token_id] * self._get_pad_size( - input_ids=input_ids[:-1] - ) - - def __iter__(self): - self.buffer = self.used_buffer + self.buffer - self.padded_buffer = [] - - # Pad and prepare one page for batching - self._refill_padded_buffer() - - return self - - def __next__(self): - batch = [] - - while len(self.padded_buffer) >= self.sequence_length: - batch.append(self.padded_buffer[: self.sequence_length]) - self.padded_buffer = self.padded_buffer[self.sequence_length :] - self._refill_padded_buffer() - - if len(batch) == self.batch_size: - return np.stack(batch) - - raise StopIteration - - -class DatasetLoader(SubsetLoader): - name: str = "airtrain-ai/fineweb-edu-fortified" - rows_base_url: str = "https://datasets-server.huggingface.co/rows" - size_base_url: str = "https://datasets-server.huggingface.co/size" - - retry_limit: int = 10 # Number of retries - retry_delay: int = 5 # Seconds to wait between retries - num_rows_per_page: int = 100 - - @staticmethod - async def next_pages( - offset: int, n_pages: int, seed: str, num_rows_per_page: int = 100 - ): - configs_data = await DatasetLoader.fetch_dataset_configs() - rng = np.random.default_rng( - hash(seed) & 0xFFFFFFFF - ) # Create a generator with a seed - rng.bit_generator.advance(offset) # Efficiently skip ahead `n` steps - result = [] - for _ in range(n_pages): - config = rng.choice(list(configs_data.keys())) - choice = rng.integers( - 0, configs_data[config]["num_rows"] - 1 - num_rows_per_page - ) - result.append((str(config), int(choice), configs_data[config]["split"])) - return result - - def __init__( - self, - batch_size=None, - sequence_length=None, - num_pages=None, - pages_info=None, - tokenizer: AutoTokenizer = None, - pack_samples: bool = False, - ): - super().__init__( - batch_size, sequence_length, num_pages, tokenizer, pack_samples - ) - - # Initialize properties - self.configs_data = None - self.pages = [] - self.buffer = [] - self.lock = asyncio.Lock() # For thread-safe operations - - @classmethod - async def create( - cls, - batch_size=None, - sequence_length=None, - num_pages=None, - pages_info=None, - tokenizer: AutoTokenizer = None, - pack_samples: bool = False, - ): - self = cls( - batch_size=batch_size, - sequence_length=sequence_length, - num_pages=num_pages, - tokenizer=tokenizer, - pack_samples=pack_samples, - ) - - # Fetch dataset configs asynchronously - self.configs_data = await cls.fetch_dataset_configs() - - if pages_info is not None: - await self._fetch(pages_info) - elif self.num_pages: - await self._fetch_data_to_buffer(self.num_pages) - - return self - - async def _fetch(self, page_info: typing.Tuple[str, int, str]): - self.pages = list(page_info) - async with aiohttp.ClientSession() as session: - tasks = [ - self._fetch_data_for_page((config_name, page, split), session) - for (config_name, page, split) in self.pages - ] - await asyncio.gather(*tasks) - - async def _fetch_data_to_buffer(self, num_pages): - """ - Randomly sample pages and add their data to the buffer. - If a page is inaccessible, another one is sampled. - This method sets the `pages` property. - """ - self.pages = [] - pages_to_fetch = self.get_random_pages(num_pages) - - async with aiohttp.ClientSession() as session: - tasks = [ - self._fetch_data_for_page(page, session) for page in pages_to_fetch - ] - await asyncio.gather(*tasks) - - async def fetch_data_to_rows(self, num_pages): - rows = [] - pages_to_fetch = self.get_random_pages(num_pages) - - async with aiohttp.ClientSession() as session: - tasks = [ - self._fetch_rows_for_page(page, session) for page in pages_to_fetch - ] - results = await asyncio.gather(*tasks) - for page_rows in results: - rows.extend(page_rows) - - return rows - - async def _fetch_data_for_page(self, page, session): - """ - Fetches data asynchronously for a single page, processes it without blocking the event loop, - and appends the tokenized data to the buffer. - - Args: - page: A tuple containing the config name, page number, and split. - session: The HTTP session used for making requests. - - Raises: - Exception: If the maximum number of retry attempts is exceeded. - """ - retry_limit = self.retry_limit - attempt = 0 - while attempt < retry_limit: - config_name, page_number, split = page - - # Create the request parameters - params = { - "dataset": self.name, - "config": config_name, - "split": split, - "offset": page_number, - "limit": self.num_rows_per_page, - } - - try: - # Make an asynchronous HTTP GET request to fetch the data - async with session.get(self.rows_base_url, params=params) as response: - response.raise_for_status() # Raise an exception for HTTP errors - data = await response.json() - - # Prepare the data to append - buffer_to_append = [] - - # Asynchronously process each row without blocking the event loop - tasks = [ - self._tokenize_content(row["row"]["text"]) - for row in data["rows"] - ] - - # Gather the tokenized results concurrently - row_input_ids = await asyncio.gather(*tasks) - - # Flatten the list of input IDs and append them to the buffer - for input_ids in row_input_ids: - buffer_to_append.extend(input_ids) - - # Safely append the processed data to the shared buffer - async with self.lock: - self.buffer.extend(buffer_to_append) - self.pages.append((config_name, page_number, split)) - break # Success, exit retry loop - - except aiohttp.ClientResponseError as e: - # Handle HTTP client errors with a retry mechanism - attempt += 1 - if attempt < retry_limit: - await asyncio.sleep(self.retry_delay) # Wait before retrying - else: - raise Exception( - f"Maximum retry attempts exceeded for page {page}" - ) from e - - async def _tokenize_content(self, content): - """ - Asynchronously tokenizes a string of content using the tokenizer in a separate thread. - - Args: - content: The text content to be tokenized. - - Returns: - The list of token IDs for the content, including the EOS token. - """ - # Offload the CPU-bound tokenization to a thread executor to prevent blocking the event loop - input_ids = await asyncio.to_thread( - self.tokenizer.encode, - content, - truncation=True, - max_length=self.sequence_length, - ) - input_ids.append(self.tokenizer.eos_token_id) - return input_ids - - async def _fetch_rows_for_page(self, page, session): - retry_limit = self.retry_limit - attempt = 0 - while attempt < retry_limit: - config_name, page_number, split = page - - # Create the request parameters - params = dict( - dataset=self.name, - config=config_name, - split=split, - offset=page_number, - limit=self.num_rows_per_page, - ) - - try: - async with session.get(self.rows_base_url, params=params) as response: - response.raise_for_status() - data = await response.json() - - # Collect the rows - return [row["row"]["text"] for row in data["rows"]] - - except aiohttp.ClientResponseError: - attempt += 1 - if attempt < retry_limit: - await asyncio.sleep(self.retry_delay) - else: - raise - - def get_random_pages(self, num_pages): - """ - Randomly sample pages. - A page is a row number of a given split of a given dataset dump. - """ - pages = [] - - for _ in range(num_pages): - # Choose a random config - config_name = random.choice(list(self.configs_data.keys())) - - # Choose a random page (row) - page = random.randint( - 0, - self.configs_data[config_name]["num_rows"] - 1 - self.num_rows_per_page, - ) - - split = self.configs_data[config_name]["split"] - - pages.append((config_name, page, split)) - - return pages - - def get_page_names(self): - """ - This is a utility function that returns the page names that were used. - Each page as a single string instead of a tuple. - """ - page_names = [] - - if hasattr(self, "pages"): - page_names = [ - f"{cfg_name}_{num_rows}_{split}" - for cfg_name, num_rows, split in self.pages - ] - - return page_names - - @staticmethod - async def fetch_dataset_configs() -> typing.Dict[str, typing.Dict]: - """ - Fetch the different dump names, aka configs, aka samples, of the - dataset. - The returned value is a dictionary with dump names as keys and - a dict of the number of rows and the split as values. - """ - # Request parameters - params = dict(dataset=DatasetLoader.name) - - attempt = 0 - while attempt < DatasetLoader.retry_limit: - try: - async with aiohttp.ClientSession() as session: - async with session.get( - DatasetLoader.size_base_url, params=params - ) as response: - response.raise_for_status() - - data = await response.json() - - # Extract the configs dict - configs_dict = data["size"]["splits"] - - # Now create a dict with config names (except 'default') as - # keys, and the number of rows as values - configs_data = { - entry["config"]: { - "num_rows": entry["num_rows"], - "split": entry["split"], - } - for entry in configs_dict - if entry["config"] != "default" - } - - return configs_data - - except aiohttp.ClientResponseError: - attempt += 1 - if attempt < DatasetLoader.retry_limit: - await asyncio.sleep(DatasetLoader.retry_delay) - else: - raise - - @staticmethod - async def next_pages_async( - offset: int, n_pages: int, seed: str, num_rows_per_page: int = 100 - ): - configs_data = await DatasetLoader.fetch_dataset_configs() - rng = np.random.default_rng( - hash(seed) & 0xFFFFFFFF - ) # Create a generator with a seed - rng.bit_generator.advance(offset) # Efficiently skip ahead `n` steps - result = [] - for _ in range(n_pages): - config = rng.choice(list(configs_data.keys())) - choice = rng.integers( - 0, configs_data[config]["num_rows"] - 1 - num_rows_per_page - ) - result.append((str(config), int(choice), configs_data[config]["split"])) - return result diff --git a/src/templar/hparams.py b/src/templar/hparams.py deleted file mode 100644 index babcc17..0000000 --- a/src/templar/hparams.py +++ /dev/null @@ -1,74 +0,0 @@ -# The MIT License (MIT) -# © 2024 templar.tech - -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the “Software”), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of -# the Software. - -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -# Global imports -import json -from types import SimpleNamespace - -# Local imports -from transformers import AutoTokenizer, LlamaConfig - -# Cache file path -HPARAMS_FILE = "hparams.json" - - -def create_namespace(hparams: dict) -> SimpleNamespace: - """ - Create a SimpleNamespace from the hyperparameters and add model configuration. - - Args: - hparams (dict): Hyperparameters dictionary. - - Returns: - SimpleNamespace: Namespace containing hyperparameters and model configuration. - """ - hparams_ns = SimpleNamespace(**hparams) - - hparams_ns.tokenizer = AutoTokenizer.from_pretrained( - hparams_ns.tokenizer_name, verbose=False, clean_up_tokenization_spaces=True - ) - hparams_ns.tokenizer.pad_token = hparams_ns.tokenizer.eos_token - - hparams_ns.model_config = LlamaConfig( - vocab_size=hparams_ns.tokenizer.vocab_size, - hidden_size=hparams_ns.hidden_size, - num_hidden_layers=hparams_ns.num_hidden_layers, - num_attention_heads=hparams_ns.num_attention_heads, - intermediate_size=hparams_ns.intermediate_size, - num_key_value_heads=hparams_ns.num_key_value_heads, - activation_function=hparams_ns.activation_function, - max_position_embeddings=hparams_ns.max_position_embeddings, - ) - - return hparams_ns - - -def load_hparams() -> SimpleNamespace: - """ - Load hyperparameters from local hparams.json file. - - Returns: - SimpleNamespace: A namespace containing the hyperparameters and model configuration. - - Example: - hparams = load_hparams() - print(hparams.hidden_size) - print(hparams.model_config) - """ - with open(HPARAMS_FILE, "r") as f: - hparams = json.load(f) - return create_namespace(hparams) diff --git a/src/templar/learning_rates.py b/src/templar/learning_rates.py deleted file mode 100644 index c383868..0000000 --- a/src/templar/learning_rates.py +++ /dev/null @@ -1,84 +0,0 @@ -# Global imports -import math -from functools import partial -from torch.optim.lr_scheduler import LambdaLR - - -def _get_wsd_lr_lambda( - current_step: int, - *, - num_warmup_steps: int, - num_stable_steps: int, - num_decay_steps: int, -) -> float: - """ - Calculates the learning rate multiplier for Warmup-Stable-Decay (WSD) schedule. - - The schedule consists of three phases: - 1. Warmup: Linear increase from 0 to 1 over num_warmup_steps - 2. Stable: Constant learning rate of 1.0 for num_stable_steps - 3. Decay: Square root decay from 1.0 to 0.0 over num_decay_steps - - Args: - current_step (int): Current training step - num_warmup_steps (int): Number of warmup steps - num_stable_steps (int): Number of steps at constant learning rate - num_decay_steps (int): Number of decay steps - - Returns: - float: Learning rate multiplier between 0.0 and 1.0 - """ - if current_step < num_warmup_steps: - # Warmup phase: increase linearly from 0 to 1 - return float(current_step) / float(max(1, num_warmup_steps)) - elif current_step < num_warmup_steps + num_stable_steps: - # Stable phase: keep learning rate constant at 1.0 - return 1.0 - else: - # Decay phase: decrease following a 1 - sqrt(x) schedule - decay_step = current_step - num_warmup_steps - num_stable_steps - decay_progress = float(decay_step) / float(max(1, num_decay_steps)) - return max(0.0, 1 - math.sqrt(decay_progress)) - - -def get_wsd_scheduler( - optimizer, - num_warmup_steps: int, - num_stable_steps: int, - num_decay_steps: int, - last_epoch: int = -1, -) -> LambdaLR: - """ - Creates a learning rate scheduler with Warmup-Stable-Decay schedule. - - This scheduler adjusts the learning rate according to three phases: - 1. Linear warmup for num_warmup_steps - 2. Constant learning rate for num_stable_steps - 3. Square root decay for num_decay_steps - - Args: - optimizer: PyTorch optimizer - num_warmup_steps (int): Number of warmup steps - num_stable_steps (int): Number of steps at constant learning rate - num_decay_steps (int): Number of decay steps - last_epoch (int, optional): The index of last epoch. Default: -1 - - Returns: - LambdaLR: PyTorch learning rate scheduler - - Example: - >>> optimizer = AdamW(model.parameters(), lr=1e-3) - >>> scheduler = get_wsd_scheduler( - ... optimizer, - ... num_warmup_steps=1000, - ... num_stable_steps=10000, - ... num_decay_steps=5000 - ... ) - """ - lr_lambda = partial( - _get_wsd_lr_lambda, - num_warmup_steps=num_warmup_steps, - num_stable_steps=num_stable_steps, - num_decay_steps=num_decay_steps, - ) - return LambdaLR(optimizer, lr_lambda, last_epoch) diff --git a/src/templar/logging.py b/src/templar/logging.py deleted file mode 100644 index e0b231b..0000000 --- a/src/templar/logging.py +++ /dev/null @@ -1,98 +0,0 @@ -# The MIT License (MIT) -# © 2024 templar.tech - -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the “Software”), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of -# the Software. - -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -# Global imports -import logging -import time -from rich.highlighter import NullHighlighter -from rich.logging import RichHandler - - -def T() -> float: - """ - Returns the current time in seconds since the epoch. - - Returns: - float: Current time in seconds. - """ - return time.time() - - -def P(window: int, duration: float) -> str: - """ - Formats a log prefix with the window number and duration. - - Args: - window (int): The current window index. - duration (float): The duration in seconds. - - Returns: - str: A formatted string for log messages. - """ - return f"[steel_blue]{window}[/steel_blue] ([grey63]{duration:.2f}s[/grey63])" - - -# Configure the root logger -FORMAT = "%(message)s" -logging.basicConfig( - level=logging.INFO, - format=FORMAT, - datefmt="[%X]", - handlers=[ - RichHandler( - markup=True, # Enable markup parsing to allow color rendering - rich_tracebacks=True, - highlighter=NullHighlighter(), - show_level=False, - show_time=False, - show_path=False, - ) - ], -) - -# Create a logger instance -logger = logging.getLogger("templar") -logger.setLevel(logging.INFO) - - -def debug() -> None: - """ - Sets the logger level to DEBUG. - """ - logger.setLevel(logging.DEBUG) - - -def trace() -> None: - """ - Sets the logger level to TRACE. - - Note: - The TRACE level is not standard in the logging module. - You may need to add it explicitly if required. - """ - TRACE_LEVEL_NUM = 5 - logging.addLevelName(TRACE_LEVEL_NUM, "TRACE") - - def trace_method(self, message, *args, **kws) -> None: - if self.isEnabledFor(TRACE_LEVEL_NUM): - self._log(TRACE_LEVEL_NUM, message, args, **kws) - - logging.Logger.trace = trace_method - logger.setLevel(TRACE_LEVEL_NUM) - - -__all__ = ["logger", "debug", "trace", "P", "T"] diff --git a/src/templar/schemas.py b/src/templar/schemas.py deleted file mode 100644 index 406d22f..0000000 --- a/src/templar/schemas.py +++ /dev/null @@ -1,24 +0,0 @@ -from pydantic import BaseModel, ConfigDict - - -class Bucket(BaseModel): - """Configuration for a bucket, including name and access credentials.""" - - model_config = ConfigDict(str_min_length=1, str_strip_whitespace=True) - - def __hash__(self): - # Use all fields to generate a unique hash - return hash( - (self.name, self.account_id, self.access_key_id, self.secret_access_key) - ) - - def __eq__(self, other): - # Compare all fields to determine equality - if isinstance(other, Bucket): - return self.dict() == other.dict() - return False - - name: str - account_id: str - access_key_id: str - secret_access_key: str diff --git a/src/templar/wandb.py b/src/templar/wandb.py deleted file mode 100644 index 095ac76..0000000 --- a/src/templar/wandb.py +++ /dev/null @@ -1,89 +0,0 @@ -# The MIT License (MIT) -# © 2024 templar.tech - -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the “Software”), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of -# the Software. - -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. -# fmt: off - - -# Global imports -import os -import wandb - -# Local imports -from templar import __version__, logger - -def initialize_wandb(run_prefix, uid, config, group, job_type): - # Ensure the wandb directory exists - wandb_dir = os.path.join(os.getcwd(), 'wandb') - os.makedirs(wandb_dir, exist_ok=True) - - # Define the run ID file path inside the wandb directory - run_id_file = os.path.join( - wandb_dir, f"wandb_run_id_{run_prefix}{uid}_{__version__}.txt" - ) - - # Check for existing run and verify it still exists in wandb - run_id = None - if os.path.exists(run_id_file): - with open(run_id_file, 'r') as f: - run_id = f.read().strip() - - # Verify if run still exists in wandb - try: - api = wandb.Api() - api.run(f"tplr/{config.project}-v{__version__}/{run_id}") - logger.info(f"Found existing run ID: {run_id}") - except Exception: - # Run doesn't exist anymore, clear the run_id - logger.info(f"Previous run {run_id} not found in WandB, starting new run") - run_id = None - os.remove(run_id_file) - - # Initialize WandB - run = wandb.init( - project=f"{config.project}-v{__version__}", - entity='tplr', - id=run_id, - resume='must' if run_id else 'never', - name=f'{run_prefix}{uid}', - config=config, - group=group, - job_type=job_type, - dir=wandb_dir, - settings=wandb.Settings( - init_timeout=300, - _disable_stats=True, - ) - ) - - # Special handling for evaluator - if run_prefix == "E": - tasks = config.tasks.split(',') - for task in tasks: - metric_name = f"eval/{task}" - # Set up x/y plot configuration - wandb.define_metric( - name=metric_name, - step_metric="global_step", # This sets global_step as x-axis - plot=True, # Ensure it creates a line plot - summary="max" - ) - - # Save run ID for future resumption - if not run_id: - with open(run_id_file, 'w') as f: - f.write(run.id) - - return run diff --git a/src/tplr/config.py b/src/tplr/config.py index 0cbb8d0..71f9567 100644 --- a/src/tplr/config.py +++ b/src/tplr/config.py @@ -19,26 +19,50 @@ # Global imports import os import sys -import yaml from pathlib import Path # Local imports import botocore.config -from dotenv import dotenv_values +from dotenv import load_dotenv from .logging import logger # Load environment variables -env_config = {**dotenv_values(".env"), **os.environ} -envfile_path = Path(__file__).parents[2] / ".env.yaml" -try: - with open(envfile_path, "r") as file: - BUCKET_SECRETS = yaml.safe_load(file) -except FileNotFoundError: +env_path = Path(__file__).parents[2] / ".env" +if not env_path.exists(): logger.error( - f"{envfile_path} not found. Please create it with the help of `.env-template.yaml`." + f"{env_path} not found. Please create it with the required R2 configuration." ) - sys.exit() -BUCKET_SECRETS["bucket_name"] = BUCKET_SECRETS["account_id"] + sys.exit(1) + +load_dotenv(env_path) + +# Configure bucket secrets +BUCKET_SECRETS = { + "account_id": os.getenv("R2_ACCOUNT_ID"), + "bucket_name": os.getenv("R2_ACCOUNT_ID"), # Using account_id as bucket name + "read": { + "access_key_id": os.getenv("R2_READ_ACCESS_KEY_ID"), + "secret_access_key": os.getenv("R2_READ_SECRET_ACCESS_KEY") + }, + "write": { + "access_key_id": os.getenv("R2_WRITE_ACCESS_KEY_ID"), + "secret_access_key": os.getenv("R2_WRITE_SECRET_ACCESS_KEY") + } +} + +# Validate required environment variables +required_vars = [ + "R2_ACCOUNT_ID", + "R2_READ_ACCESS_KEY_ID", + "R2_READ_SECRET_ACCESS_KEY", + "R2_WRITE_ACCESS_KEY_ID", + "R2_WRITE_SECRET_ACCESS_KEY" +] + +missing_vars = [var for var in required_vars if not os.getenv(var)] +if missing_vars: + logger.error(f"Missing required environment variables: {', '.join(missing_vars)}") + sys.exit(1) # Configure the S3 client client_config = botocore.config.Config( From 7a05d4c039b189ef3730b0b07a4bfbb05569b4c7 Mon Sep 17 00:00:00 2001 From: distributedstatemachine! Date: Mon, 30 Dec 2024 13:06:20 +0000 Subject: [PATCH 06/15] fix: ci --- .github/workflows/docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 72c7db0..8eef53f 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -43,7 +43,7 @@ jobs: type=semver,pattern={{version}} type=semver,pattern={{raw}} # Latest tag on release or main branch - type=raw,value=latest,enable={{is_default_branch || startsWith(github.ref, 'refs/tags/v')}} + type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') }} # SHA for every build type=sha,prefix=sha-,format=short From 248f7c4a363b33bda0d10cef860846379218d8fd Mon Sep 17 00:00:00 2001 From: distributedstatemachine! Date: Mon, 30 Dec 2024 13:30:29 +0000 Subject: [PATCH 07/15] chore: update config, fix watch tower --- docker/compose.yml | 7 +++++-- src/tplr/config.py | 28 ++++++++-------------------- 2 files changed, 13 insertions(+), 22 deletions(-) diff --git a/docker/compose.yml b/docker/compose.yml index 7160ea5..e1f7c70 100644 --- a/docker/compose.yml +++ b/docker/compose.yml @@ -1,6 +1,6 @@ services: node: - image: ghcr.io/tplr-ai/templar:a67379b + image: ghcr.io/tplr-ai/templar:v0.0.2 container_name: templar-${NODE_TYPE:-miner}-${WALLET_HOTKEY} restart: unless-stopped volumes: @@ -14,6 +14,7 @@ services: - NETWORK=${NETWORK:-finney} - DEBUG=${DEBUG:-false} - WANDB_API_KEY=${WANDB_API_KEY} + - HOST_CUDA_VERSION=12.6 deploy: resources: reservations: @@ -28,10 +29,12 @@ services: image: containrrr/watchtower volumes: - /var/run/docker.sock:/var/run/docker.sock - - /root/.docker/config.json:/config.json + - ${HOME}/.docker/config.json:/config.json:ro command: --interval 30 --cleanup --label-enable restart: unless-stopped environment: - WATCHTOWER_CLEANUP=true - WATCHTOWER_LABEL_ENABLE=true - WATCHTOWER_INCLUDE_RESTARTING=true + - REPO_USER=${GITHUB_USER} + - REPO_PASSWORD=${GITHUB_TOKEN} diff --git a/src/tplr/config.py b/src/tplr/config.py index 71f9567..c628095 100644 --- a/src/tplr/config.py +++ b/src/tplr/config.py @@ -19,34 +19,22 @@ # Global imports import os import sys -from pathlib import Path # Local imports import botocore.config -from dotenv import load_dotenv from .logging import logger -# Load environment variables -env_path = Path(__file__).parents[2] / ".env" -if not env_path.exists(): - logger.error( - f"{env_path} not found. Please create it with the required R2 configuration." - ) - sys.exit(1) - -load_dotenv(env_path) - -# Configure bucket secrets +# Configure bucket secrets from environment variables BUCKET_SECRETS = { - "account_id": os.getenv("R2_ACCOUNT_ID"), - "bucket_name": os.getenv("R2_ACCOUNT_ID"), # Using account_id as bucket name + "account_id": os.environ.get("R2_ACCOUNT_ID"), + "bucket_name": os.environ.get("R2_ACCOUNT_ID"), # Using account_id as bucket name "read": { - "access_key_id": os.getenv("R2_READ_ACCESS_KEY_ID"), - "secret_access_key": os.getenv("R2_READ_SECRET_ACCESS_KEY") + "access_key_id": os.environ.get("R2_READ_ACCESS_KEY_ID"), + "secret_access_key": os.environ.get("R2_READ_SECRET_ACCESS_KEY") }, "write": { - "access_key_id": os.getenv("R2_WRITE_ACCESS_KEY_ID"), - "secret_access_key": os.getenv("R2_WRITE_SECRET_ACCESS_KEY") + "access_key_id": os.environ.get("R2_WRITE_ACCESS_KEY_ID"), + "secret_access_key": os.environ.get("R2_WRITE_SECRET_ACCESS_KEY") } } @@ -59,7 +47,7 @@ "R2_WRITE_SECRET_ACCESS_KEY" ] -missing_vars = [var for var in required_vars if not os.getenv(var)] +missing_vars = [var for var in required_vars if not os.environ.get(var)] if missing_vars: logger.error(f"Missing required environment variables: {', '.join(missing_vars)}") sys.exit(1) From 01d4e24a55b6f5b119d89afcf2c2dcc3c51ebf69 Mon Sep 17 00:00:00 2001 From: distributedstatemachine! Date: Mon, 30 Dec 2024 13:45:59 +0000 Subject: [PATCH 08/15] fix: comms --- docker/compose.yml | 7 ++++++- src/tplr/comms.py | 37 +++++++++++++------------------------ 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/docker/compose.yml b/docker/compose.yml index e1f7c70..5ced794 100644 --- a/docker/compose.yml +++ b/docker/compose.yml @@ -1,6 +1,6 @@ services: node: - image: ghcr.io/tplr-ai/templar:v0.0.2 + image: ghcr.io/tplr-ai/templar:v0.0.4 container_name: templar-${NODE_TYPE:-miner}-${WALLET_HOTKEY} restart: unless-stopped volumes: @@ -15,6 +15,11 @@ services: - DEBUG=${DEBUG:-false} - WANDB_API_KEY=${WANDB_API_KEY} - HOST_CUDA_VERSION=12.6 + - R2_ACCOUNT_ID=${R2_ACCOUNT_ID} + - R2_READ_ACCESS_KEY_ID=${R2_READ_ACCESS_KEY_ID} + - R2_READ_SECRET_ACCESS_KEY=${R2_READ_SECRET_ACCESS_KEY} + - R2_WRITE_ACCESS_KEY_ID=${R2_WRITE_ACCESS_KEY_ID} + - R2_WRITE_SECRET_ACCESS_KEY=${R2_WRITE_SECRET_ACCESS_KEY} deploy: resources: reservations: diff --git a/src/tplr/comms.py b/src/tplr/comms.py index 60109f6..a0685f5 100644 --- a/src/tplr/comms.py +++ b/src/tplr/comms.py @@ -73,36 +73,25 @@ def __init__( self.bucket_secrets = BUCKET_SECRETS def get_own_bucket(self) -> Bucket: - """Parses the credentials from .env.yaml to create a Bucket object.""" - env_file = ".env.yaml" - if not os.path.isfile(env_file): - logger.error(f"The {env_file} file was not found.") - raise FileNotFoundError(f"The {env_file} file was not found.") - - try: - with open(env_file, "r") as file: - credentials = yaml.safe_load(file) - except yaml.YAMLError as e: - logger.error(f"Error parsing {env_file}: {e}") - raise e - + """Gets bucket configuration from environment variables via config.BUCKET_SECRETS.""" try: - account_id = credentials["account_id"] - read_access_key_id = credentials["read"]["access_key_id"] - read_secret_access_key = credentials["read"]["secret_access_key"] - # Create a Bucket object + # Create a Bucket object using write credentials from BUCKET_SECRETS bucket = Bucket( - name=account_id, - account_id=account_id, - access_key_id=read_access_key_id, - secret_access_key=read_secret_access_key, + name=BUCKET_SECRETS["account_id"], + account_id=BUCKET_SECRETS["account_id"], + access_key_id=BUCKET_SECRETS["write"]["access_key_id"], + secret_access_key=BUCKET_SECRETS["write"]["secret_access_key"], ) - logger.debug(f"Parsed bucket from {env_file}: {bucket}") + logger.debug(f"Created bucket from environment: {bucket}") return bucket + except KeyError as e: - logger.error(f"Missing key in {env_file}: {e}") - raise e + logger.error(f"Missing required R2 configuration: {e}") + raise + except Exception as e: + logger.error(f"Error creating bucket: {e}") + raise async def put( self, From bacb695ed4ca47646799485afa615acbc43bdf42 Mon Sep 17 00:00:00 2001 From: distributedstatemachine! Date: Mon, 30 Dec 2024 14:24:22 +0000 Subject: [PATCH 09/15] fix: pass netuid --- .env-template | 2 ++ docker/compose.yml | 3 ++- scripts/entrypoint.sh | 4 +++- src/tplr/chain.py | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.env-template b/.env-template index 11e910c..fdc2cde 100644 --- a/.env-template +++ b/.env-template @@ -5,6 +5,8 @@ WALLET_HOTKEY= CUDA_DEVICE= NETWORK= DEBUG= +NETUID=268 + # R2 Configuration R2_ACCOUNT_ID= diff --git a/docker/compose.yml b/docker/compose.yml index 5ced794..42b86b1 100644 --- a/docker/compose.yml +++ b/docker/compose.yml @@ -1,6 +1,6 @@ services: node: - image: ghcr.io/tplr-ai/templar:v0.0.4 + image: ghcr.io/tplr-ai/templar:v0.0.6 container_name: templar-${NODE_TYPE:-miner}-${WALLET_HOTKEY} restart: unless-stopped volumes: @@ -13,6 +13,7 @@ services: - CUDA_DEVICE=${CUDA_DEVICE:-cuda:0} - NETWORK=${NETWORK:-finney} - DEBUG=${DEBUG:-false} + - NETUID=${NETUID:-268} - WANDB_API_KEY=${WANDB_API_KEY} - HOST_CUDA_VERSION=12.6 - R2_ACCOUNT_ID=${R2_ACCOUNT_ID} diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh index f53f56f..6d10d67 100644 --- a/scripts/entrypoint.sh +++ b/scripts/entrypoint.sh @@ -2,7 +2,7 @@ set -e # Check required environment variables -for var in WALLET_NAME WALLET_HOTKEY NODE_TYPE WANDB_API_KEY; do +for var in WALLET_NAME WALLET_HOTKEY NODE_TYPE WANDB_API_KEY NETUID; do if [ -z "${!var}" ]; then echo "Error: $var environment variable is required" exit 1 @@ -42,6 +42,7 @@ if [ "$NODE_TYPE" = "miner" ]; then exec python3 neurons/miner.py \ --wallet.name ${WALLET_NAME} \ --wallet.hotkey ${WALLET_HOTKEY} \ + --netuid ${NETUID} \ --device ${CUDA_DEVICE} \ --subtensor.network ${NETWORK} \ --use_wandb \ @@ -51,6 +52,7 @@ elif [ "$NODE_TYPE" = "validator" ]; then exec python3 neurons/validator.py \ --wallet.name ${WALLET_NAME} \ --wallet.hotkey ${WALLET_HOTKEY} \ + --netuid ${NETUID} \ --device ${CUDA_DEVICE} \ --subtensor.network ${NETWORK} \ --use_wandb \ diff --git a/src/tplr/chain.py b/src/tplr/chain.py index 4677360..72fdce2 100644 --- a/src/tplr/chain.py +++ b/src/tplr/chain.py @@ -188,7 +188,7 @@ async def commit(self, wallet: "bt.wallet", bucket: Bucket) -> None: bucket (Bucket): Bucket configuration to commit """ if self.netuid: - raise ValueError("Subtensor and netuid must be set for chain operations") + raise ValueError("netuid must be set for chain operations") concatenated = ( bucket.account_id + bucket.access_key_id + bucket.secret_access_key From 6ba656423bad1be634854dea044bc3c77ba2838a Mon Sep 17 00:00:00 2001 From: distributedstatemachine! Date: Mon, 30 Dec 2024 14:38:47 +0000 Subject: [PATCH 10/15] fix: chain --- src/tplr/chain.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/tplr/chain.py b/src/tplr/chain.py index 72fdce2..a3f5598 100644 --- a/src/tplr/chain.py +++ b/src/tplr/chain.py @@ -187,9 +187,6 @@ async def commit(self, wallet: "bt.wallet", bucket: Bucket) -> None: wallet (bt.wallet): Wallet to sign the commitment bucket (Bucket): Bucket configuration to commit """ - if self.netuid: - raise ValueError("netuid must be set for chain operations") - concatenated = ( bucket.account_id + bucket.access_key_id + bucket.secret_access_key ) From ea07a3a3e52f1e2f9b654efebd269914f78e8c40 Mon Sep 17 00:00:00 2001 From: distributedstatemachine! Date: Mon, 30 Dec 2024 14:52:27 +0000 Subject: [PATCH 11/15] fix: chain --- docker/compose.yml | 2 +- src/tplr/chain.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docker/compose.yml b/docker/compose.yml index 42b86b1..d6b427e 100644 --- a/docker/compose.yml +++ b/docker/compose.yml @@ -1,6 +1,6 @@ services: node: - image: ghcr.io/tplr-ai/templar:v0.0.6 + image: ghcr.io/tplr-ai/templar:v0.0.9 container_name: templar-${NODE_TYPE:-miner}-${WALLET_HOTKEY} restart: unless-stopped volumes: diff --git a/src/tplr/chain.py b/src/tplr/chain.py index a3f5598..7aaef82 100644 --- a/src/tplr/chain.py +++ b/src/tplr/chain.py @@ -187,10 +187,11 @@ async def commit(self, wallet: "bt.wallet", bucket: Bucket) -> None: wallet (bt.wallet): Wallet to sign the commitment bucket (Bucket): Bucket configuration to commit """ + subtensor = bt.subtensor(config=self.config) concatenated = ( bucket.account_id + bucket.access_key_id + bucket.secret_access_key ) - self.subtensor.commit(wallet, self.netuid, concatenated) + subtensor.commit(wallet, self.netuid, concatenated) logger.info( f"Committed bucket configuration to chain for hotkey {wallet.hotkey.ss58_address}" ) From 0590790500cd418d5593300e8471ee76455f939c Mon Sep 17 00:00:00 2001 From: distributedstatemachine! Date: Mon, 30 Dec 2024 15:15:25 +0000 Subject: [PATCH 12/15] fix: list objects first --- docker/compose.yml | 2 +- src/tplr/comms.py | 47 +++++++++++++++++++++++++++------------------- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/docker/compose.yml b/docker/compose.yml index d6b427e..c4fe3ba 100644 --- a/docker/compose.yml +++ b/docker/compose.yml @@ -1,6 +1,6 @@ services: node: - image: ghcr.io/tplr-ai/templar:v0.0.9 + image: ghcr.io/tplr-ai/templar:v0.0.10 container_name: templar-${NODE_TYPE:-miner}-${WALLET_HOTKEY} restart: unless-stopped volumes: diff --git a/src/tplr/comms.py b/src/tplr/comms.py index a0685f5..e5e20f2 100644 --- a/src/tplr/comms.py +++ b/src/tplr/comms.py @@ -264,16 +264,6 @@ async def get( ) -> Optional[Dict[str, torch.Tensor]]: """ Downloads data from the R2 bucket. Handles both model state dicts and large checkpoint files. - - Args: - uid (str): Unique identifier for the download. - window (int): The window number for synchronization. - key (str, optional): Custom key for the filename. - timeout (int): Timeout in seconds for the download operation. - local (bool): If True, keeps the downloaded file on disk. - - Returns: - Optional[Dict[str, torch.Tensor]]: The downloaded state dictionary. """ key = key or self.key_prefix hotkey = self.get_hotkey(int(uid)) @@ -298,15 +288,34 @@ async def get( aws_access_key_id=bucket.access_key_id, aws_secret_access_key=bucket.secret_access_key, ) as s3_client: - # Check file size first - response = await s3_client.head_object( - Bucket=bucket.name, - Key=filename - ) - file_size = response['ContentLength'] - - # Use multipart download for files larger than 100MB - if file_size > 100 * 1024 * 1024: + # Check if file exists first + try: + # List objects with the prefix to check existence + paginator = s3_client.get_paginator('list_objects_v2') + file_exists = False + + async for page in paginator.paginate( + Bucket=bucket.name, + Prefix=filename + ): + for obj in page.get('Contents', []): + if obj['Key'] == filename: + file_exists = True + file_size = obj['Size'] + break + if file_exists: + break + + if not file_exists: + logger.debug(f"File {filename} not found in bucket. Skipping...") + return None + + except Exception as e: + logger.debug(f"Error checking file existence: {e}") + return None + + # File exists, proceed with download + if file_size > 100 * 1024 * 1024: # 100MB success = await self._download_large_file(s3_client, filename, temp_file_path) if not success: return None From 05948d747a5cc1bfd093282b78d6b2be753a5e2a Mon Sep 17 00:00:00 2001 From: distributedstatemachine! Date: Tue, 31 Dec 2024 15:46:52 +0000 Subject: [PATCH 13/15] feat: works --- .env-template | 19 - .env-template.yaml | 9 - .github/workflows/ci.yml | 15 +- .gitignore | 48 +- README.md | 304 ++++++++- assets/acl_perms.png | Bin 239272 -> 0 bytes assets/allow_public_access.png | Bin 29659 -> 0 bytes assets/object_owner_preferred.png | Bin 316516 -> 0 bytes blacklist.toml | 28 - docker-compose-test.yml | 72 --- docker/Dockerfile | 3 +- docker/compose.yml | 2 +- docker/docker-compose-test.yml | 51 +- docs/checkpointing.md | 124 ---- docs/global_sync.md | 225 ------- docs/incentive_design.md | 203 ------ docs/miner.md | 417 ++++++++---- docs/validator.md | 428 +++++++----- hparams.json | 7 +- justfile | 73 --- neurons/miner.py | 161 +++-- neurons/validator.py | 163 ++++- run.py | 395 ------------ scripts/clean.py | 69 -- scripts/clean_testnet.py | 87 --- scripts/decay.py | 345 ---------- scripts/eval.py | 17 +- scripts/release_notes.rs | 173 ----- scripts/run.sh | 750 --------------------- scripts/start.sh | 39 +- src/tplr/__init__.py | 7 +- src/tplr/autoupdate.py | 347 ---------- src/tplr/comms.py | 1005 ++++++++++++++++------------- src/tplr/compress.py | 2 +- src/tplr/config.py | 2 +- src/tplr/dataset.py | 3 +- src/tplr/hparams.py | 2 +- src/tplr/logging.py | 1 - src/tplr/schemas.py | 1 - start.sh | 6 - tests/test_autoupdater.py | 260 -------- tests/test_checkpoints.py | 216 ------- 42 files changed, 1756 insertions(+), 4323 deletions(-) delete mode 100644 .env-template delete mode 100644 .env-template.yaml delete mode 100644 assets/acl_perms.png delete mode 100644 assets/allow_public_access.png delete mode 100644 assets/object_owner_preferred.png delete mode 100644 blacklist.toml delete mode 100644 docker-compose-test.yml delete mode 100644 docs/checkpointing.md delete mode 100644 docs/global_sync.md delete mode 100644 docs/incentive_design.md delete mode 100644 justfile delete mode 100644 run.py delete mode 100644 scripts/clean.py delete mode 100644 scripts/clean_testnet.py delete mode 100644 scripts/decay.py delete mode 100755 scripts/release_notes.rs delete mode 100755 scripts/run.sh delete mode 100644 src/tplr/autoupdate.py delete mode 100755 start.sh delete mode 100644 tests/test_autoupdater.py delete mode 100644 tests/test_checkpoints.py diff --git a/.env-template b/.env-template deleted file mode 100644 index fdc2cde..0000000 --- a/.env-template +++ /dev/null @@ -1,19 +0,0 @@ -WANDB_API_KEY= -NODE_TYPE= -WALLET_NAME= -WALLET_HOTKEY= -CUDA_DEVICE= -NETWORK= -DEBUG= -NETUID=268 - -# R2 Configuration -R2_ACCOUNT_ID= - -# Read credentials -R2_READ_ACCESS_KEY_ID= -R2_READ_SECRET_ACCESS_KEY= - -# Write credentials -R2_WRITE_ACCESS_KEY_ID= -R2_WRITE_SECRET_ACCESS_KEY= \ No newline at end of file diff --git a/.env-template.yaml b/.env-template.yaml deleted file mode 100644 index a074b39..0000000 --- a/.env-template.yaml +++ /dev/null @@ -1,9 +0,0 @@ -account_id: "value" - -read: - access_key_id: "value" - secret_access_key: "value" - -write: - access_key_id: "value" - secret_access_key: "value" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4b3481d..472a798 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,16 +22,6 @@ jobs: - name: Install dependencies run: uv sync --extra all - - name: Create .env.yaml - run: | - echo "account_id: \"$ACCOUNT_ID\"" > .env.yaml - echo "read:" >> .env.yaml - echo " access_key_id: \"$READ_ACCESS_KEY_ID\"" >> .env.yaml - echo " secret_access_key: \"$READ_SECRET_ACCESS_KEY\"" >> .env.yaml - echo "write:" >> .env.yaml - echo " access_key_id: \"$WRITE_ACCESS_KEY_ID\"" >> .env.yaml - echo " secret_access_key: \"$WRITE_SECRET_ACCESS_KEY\"" >> .env.yaml - - name: Run Ruff Lint uses: astral-sh/ruff-action@v1 with: @@ -40,7 +30,4 @@ jobs: - name: Run Ruff Format uses: astral-sh/ruff-action@v1 with: - args: format --check - - - name: Run Tests - run: uv run pytest \ No newline at end of file + args: format --check \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1443233..29d513a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,18 +1,46 @@ -# Python-generated files +.env.yaml +.env +*.pth + +# Python __pycache__/ -*.py[oc] +*.py[cod] +*$py.class +*.so +.Python build/ +develop-eggs/ dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ wheels/ -wandb/ -wandb/* -*.egg-info +*.egg-info/ +.installed.cfg +*.egg -# Virtual environments -.venv +# UV specific +.uv/ +.venv/ +venv/ +.env/ +uv.lock -.env.yaml +# Ruff sepcific +.ruff* -.env +# IDE +.idea/ +.vscode/ +*.swp +*.swo +.DS_Store -logs \ No newline at end of file +# Project specific +wandb +*ipynb \ No newline at end of file diff --git a/README.md b/README.md index ce1433e..c7b61ae 100644 --- a/README.md +++ b/README.md @@ -11,14 +11,308 @@ ___ _ _ _ _ | _ _ | | +
-Documentation: Incentive DesignMinerValidator +Documentation: MinerValidator
- \ No newline at end of file +### Formal Definitions + +- **Model Parameters** at time \( t \): \( \theta^t \). +- **Local Gradients**: \( g_i^t \), computed by miner \( i \) at time \( t \). +- **Momentum Buffer Update**: + \[ + m_i^{t+1} = \gamma m_i^t + \eta g_i^t + \] +- **Weight Decay**: + \[ + \theta^{t+1} = (1 - \lambda)\theta^t + \] +- **Compressed Gradient**: \( \tilde{g}_i^t \), the top-k compressed version of the transformed momentum buffer. +- **Aggregated Gradient**: + \[ + \delta_{\text{agg}} = \sum_{i \in \mathcal{P}} \tilde{g}_i^t + \] + - \( \mathcal{P} \): Set of peer miners. + +--- + +## Validators + +### Operations + +1. **Model Synchronization**: + - Synchronize their model with the latest global state. + - Attempt to load the latest model checkpoint from the validator with the highest stake or start from scratch. + +2. **Data Acquisition**: + - Select a miner to evaluate. + - Retrieve the same data subset assigned to that miner using the same deterministic seeding mechanism. + +3. **Gradient Gathering**: + - Gather the compressed gradients submitted by miners for the current window. + - Decompress and apply these gradients to their local model to maintain consistency. + +4. **Evaluation of Miners**: + - For the selected miner \( i \): + - **Compute Loss Before** applying the miner's gradient: + \[ + L_{\text{before}} = \mathcal{L}(\theta^t; D_i) + \] + - \( D_i \): Dataset assigned to miner \( i \). + - **Apply** the miner's gradient: + \[ + \theta^{t+1} = \theta^t + \delta_i + \] + - \( \delta_i \): Decompressed gradient from miner \( i \). + - **Compute Loss After** applying the gradient: + \[ + L_{\text{after}} = \mathcal{L}(\theta^{t+1}; D_i) + \] + - **Compute Improvement**: + \[ + s_i = L_{\text{before}} - L_{\text{after}} + \] + +5. **Score Calculation**: + - The score \( s_i \) reflects the miner's contribution to reducing the loss. + +6. **Weight Assignment and Update**: + - Update the moving average of the miner's score: + \[ + \bar{s}_i = \alpha s_i + (1 - \alpha)\bar{s}_i + \] + - \( \alpha \): Smoothing factor. + - Compute weights using a softmax function over the moving average scores: + \[ + w_i = \frac{e^{\bar{s}_i}}{\sum_{j \in \mathcal{M}} e^{\bar{s}_j}} + \] + - \( \mathcal{M} \): Set of miners. + +7. **Blockchain Update**: + - Validators set these weights on the blockchain, influencing reward distribution and miner reputation. + +8. **Optimizer Step and Learning Rate Scheduling**: + - Apply optimizer steps and adjust learning rates to keep the model updated. + +--- + +### Formal Definitions + +- **Model Loss**: + - Before update: \( L_{\text{before}} = \mathcal{L}(\theta^t; D_i) \) + - After update: \( L_{\text{after}} = \mathcal{L}(\theta^{t+1}; D_i) \) +- **Miner's Score**: + \[ + s_i = L_{\text{before}} - L_{\text{after}} + \] +- **Moving Average Score**: + \[ + \bar{s}_i = \alpha s_i + (1 - \alpha)\bar{s}_i + \] +- **Assigned Weight**: + \[ + w_i = \frac{e^{\bar{s}_i}}{\sum_{j} e^{\bar{s}_j}} + \] + +--- + +## Incentive Mechanism + +### Objective + +The incentive mechanism in **τemplar** aims to: + +- **Encourage Honest Participation**: Motivate miners to perform genuine training and provide updates that improve model performance. +- **Promote Model Improvement**: Reward updates that lead to a reduction in loss. +- **Discourage Malicious Behavior**: Penalize updates that do not improve or degrade model performance. + +### Detailed Explanation + +#### Score Calculation and Weight Assignment + +1. **Compute Loss Improvement**: + - Validators measure the effectiveness of a miner's update by the reduction in loss on the assigned dataset. + - The score \( s_i = L_{\text{before}} - L_{\text{after}} \) quantifies this improvement. + +2. **Interpretation of Scores**: + - **Positive \( s_i \)**: Indicates the miner's update improved the model. + - **Zero \( s_i \)**: No change in model performance. + - **Negative \( s_i \)**: The miner's update worsened the model. + +3. **Moving Average for Stability**: + - Using a moving average \( \bar{s}_i \) smooths out fluctuations in individual scores. + - Helps in maintaining stable weights over time. + +4. **Weight Computation**: + - Apply the softmax function to the moving average scores to compute the weights: + \[ + w_i = \frac{e^{\bar{s}_i}}{\sum_{j} e^{\bar{s}_j}} + \] + - Ensures that miners with higher contributions receive proportionally higher weights. + +5. **Blockchain Update**: + - Validators set these weights on the blockchain, which influences reward distribution and miner reputation. + +### Formal Guarantees + +1. **Alignment Incentive**: + - Miners are incentivized to produce updates that reduce the loss, aligning individual goals with the collective objective. + +2. **Discouraging Malicious Actions**: + - Miners submitting harmful updates receive lower or negative scores, resulting in minimal or no rewards. + +3. **Fair Reward Distribution**: + - Weights are computed based on the actual performance improvements contributed by miners. + +4. **Convergence Assurance**: + - By aggregating beneficial updates, the model is guided towards convergence and improved performance. + +5. **Sybil Resistance**: + - Since rewards are based on contribution quality, creating fake identities without meaningful contributions offers no advantage. + +--- + +## Formal Analysis + +### Miner Utility Maximization + +Miners aim to maximize their expected reward, which is proportional to their assigned weight \( w_i \): + +\[ +\max_{\delta_i} \quad w_i = \frac{e^{\bar{s}_i}}{\sum_{j} e^{\bar{s}_j}} +\] + +Subject to: + +- **Update Rule**: + \[ + \delta_i = \text{Compress}(\gamma m_i^{t} + \eta g_i^t) + \] +- **Model Update**: + \[ + \theta^{t+1} = \theta^{t} + \delta_{\text{agg}} + \] +- **Score Function**: + \[ + s_i = L(\theta^{t}; D_i) - L(\theta^{t} + \delta_i; D_i) + \] + +The optimal strategy for miners is to compute accurate gradients that lead to a reduction in loss on their assigned data. + +### Validator Consistency + +Validators ensure: + +- **Fair Evaluation**: + - Use the same datasets as miners to compute losses. + - Apply the miners' updates accurately. + +- **Transparency**: + - Evaluation procedures are deterministic and replicable. + +### Security Considerations + +1. **Data Integrity**: + - Deterministic data assignments prevent miners from manipulating their datasets. + +2. **Gradient Compression and Privacy**: + - Compression reduces the risk of exposing sensitive information. + - Only significant components are shared. + +3. **Preventing Free Riding**: + - Miners gain rewards only if their updates lead to performance improvements. + +--- + +## Conclusion + +The incentive mechanism in **τemplar** effectively encourages miners to contribute meaningful updates that enhance the global model. By tying rewards to the actual improvement in model loss resulting from miners' gradients, the system ensures that only beneficial contributions are rewarded. This approach aligns individual miner incentives with the collective goal of training an effective model. + +The careful design of data assignment, gradient evaluation, and weight distribution fosters a self-regulating ecosystem where honest participation is the most profitable strategy. Formal guarantees provide robustness against malicious actors, promoting the overall improvement of the model through collaborative effort. + +Thus, **τemplar** creates a sustainable and efficient framework for decentralized collaborative learning, leveraging incentives to drive positive contributions and advance the shared model's performance. + diff --git a/assets/acl_perms.png b/assets/acl_perms.png deleted file mode 100644 index 9c9a87e500033827422846dd1769ea26b6610912..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 239272 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 BmDd0O diff --git a/assets/allow_public_access.png b/assets/allow_public_access.png deleted file mode 100644 index 063bacaa2414b988c3e6dfad7c11149fc740b7b4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29659 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 MYwBr~-Fp)B9|W&8wEzGB diff --git a/blacklist.toml b/blacklist.toml deleted file mode 100644 index e37cc5b..0000000 --- a/blacklist.toml +++ /dev/null @@ -1,28 +0,0 @@ -[blacklist] -hotkeys = [ - "5F1AvW1YMA3wycLPDfvC6FgEeu4cXaicZ3nxsu51WQsu7ZzY", - "5CkrU6o7eDNbf3SJdyzZu4fK96eHr4KFhjUjFRtbsgZHEXAC", - "5G7AawrGnYhPgS8nHC9W93MsWan3R3i8VJ3qHaetMBDRf9zx", - "5GTRujZS7JoCHEYapRD32o2iK26z9nsM7u1ZEmnc1BmGQTLb", - "5EAJxoyP1QrUkCyyDf5zx89uVtuZ2cxJjoA1eixN1gwT4xvT", - "5F28AhQcDSoMPrnoAgQc1TmvBTBS5HPXLovuA3dgXqJK9kRn", - "5CdTAxpZcnCJP5MZ7qNncgbFSddbyKf9dyzM5WSbkXYkDmAQ", - "5ERxpTnQBHQSFSHxQfBwncyg2nX6fwd9zuAgWEc9zhKSS3o4", - "5ENnzZUHbMqjeZw4nMffCfaRBVRX3wPcN4qNd9g1dSzdQ3AS", - "5CVD4y13Lo1k9mN46d39i8StB5ecMz4sA3YqMaHjUKg5MNE1", - "5EHd7FxG2sPqTNbmEjDU9SiMkPGdbiPNw2PXgf6QBPjG3Sge", - "5CMk8v2x8DSeKhggxTTwdXpiTDaogFdFh34vrvawgCn62Ri3", - "5E9t5M8YaetNoXbwdb16zUfhrtJGLTjqUZLSxtSAjBquMYk8", - "5EFPzb95KBbKZWFraUqLgf93uatCKz11eGKfF1mHN6zN5hix", - "5HgBkh5nYPvJwfWwyZdPTZepx1Ta3s7VvqqyK4ycmB38MASW", - "5CP6EzFLcWZZ5CCZGCi96EhbgxMTQ3Ragq1ziGyXXjMEywpX", - "5DfeE7AzhPtcSQ7HfkLuBb3o3EHRsHwMkirkW1DHeS2576zD", - "5EUwLvCGQGWVBzuKCmVig3z2WnPScmToPvXPd4AJKBKJ9oAB", - "5CfjjV4xaxJ1Bg82ysU9TFEwu4Bt85skz3MKhAdUyKKmqoNp", - "5Gbj4KaQPgDMdVgaaMXrzNqSYQPBXz7mCT7XfESLyLW7F3pP", - "5DHXoVMi6VWigqdeCwGuxUKPJuCcJrRdTnbN85T4vgEdTcn9", - "5GgX9NvYVcp1to4kyUDXyxyc8zEVBK51s4m8geEjfrzpmbzt", - "5FcPNdTGmVMAmimvkUkFACfZQprhrPSzcwx1sQNUCfDghxe2", - "5F1K7TEV5a7bK2bt7zT3K7wLPWrq4QcBeq9ooTWWWLuVyWbo", - "5Ev5oSnyb2QazBzXG5WdzonB6v2wKMLM6DTps7sLRuHC84Mp" -] \ No newline at end of file diff --git a/docker-compose-test.yml b/docker-compose-test.yml deleted file mode 100644 index 88d15c6..0000000 --- a/docker-compose-test.yml +++ /dev/null @@ -1,72 +0,0 @@ -services: - miner1: - build: - context: . - dockerfile: docker/Dockerfile - container_name: templar-miner-M111 - volumes: - - ~/.bittensor/wallets:/root/.bittensor/wallets - - ./logs:/app/logs - environment: - NODE_TYPE: miner - WALLET_NAME: Bistro - WALLET_HOTKEY: M111 - CUDA_DEVICE: cuda:0 - NETWORK: test - DEBUG: 'true' - WANDB_API_KEY: ${WANDB_API_KEY} - deploy: - resources: - reservations: - devices: - - driver: nvidia - device_ids: ['0', '1', '2'] - capabilities: [gpu] - - miner2: - build: - context: . - dockerfile: docker/Dockerfile - container_name: templar-miner-M222 - volumes: - - ~/.bittensor/wallets:/root/.bittensor/wallets - - ./logs:/app/logs - environment: - NODE_TYPE: miner - WALLET_NAME: Bistro - WALLET_HOTKEY: M222 - CUDA_DEVICE: cuda:1 - NETWORK: test - DEBUG: 'true' - WANDB_API_KEY: ${WANDB_API_KEY} - deploy: - resources: - reservations: - devices: - - driver: nvidia - device_ids: ['0', '1', '2'] - capabilities: [gpu] - - validator: - build: - context: . - dockerfile: docker/Dockerfile - container_name: templar-validator-V11 - volumes: - - ~/.bittensor/wallets:/root/.bittensor/wallets - - ./logs:/app/logs - environment: - NODE_TYPE: validator - WALLET_NAME: Bistro - WALLET_HOTKEY: V11 - CUDA_DEVICE: cuda:2 - NETWORK: test - DEBUG: 'true' - WANDB_API_KEY: ${WANDB_API_KEY} - deploy: - resources: - reservations: - devices: - - driver: nvidia - device_ids: ['0', '1', '2'] - capabilities: [gpu] \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile index 8cbbae0..40c4eb7 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -19,7 +19,8 @@ RUN pip install uv ENV PATH="/root/.cargo/bin:${PATH}" -# Copy project files +# Copy pyproject.toml first for better caching +COPY pyproject.toml . COPY . . # Install dependencies using uv diff --git a/docker/compose.yml b/docker/compose.yml index c4fe3ba..e32b9bd 100644 --- a/docker/compose.yml +++ b/docker/compose.yml @@ -1,6 +1,6 @@ services: node: - image: ghcr.io/tplr-ai/templar:v0.0.10 + image: ghcr.io/tplr-ai/templar:latest container_name: templar-${NODE_TYPE:-miner}-${WALLET_HOTKEY} restart: unless-stopped volumes: diff --git a/docker/docker-compose-test.yml b/docker/docker-compose-test.yml index 16295c1..84eb20f 100644 --- a/docker/docker-compose-test.yml +++ b/docker/docker-compose-test.yml @@ -1,12 +1,12 @@ services: miner1: build: - context: . - dockerfile: Dockerfile + context: .. + dockerfile: docker/Dockerfile container_name: templar-miner-M111 volumes: - ~/.bittensor/wallets:/root/.bittensor/wallets - - ./logs:/app/logs + - ../logs:/app/logs environment: NODE_TYPE: miner WALLET_NAME: Bistro @@ -15,22 +15,29 @@ services: NETWORK: test DEBUG: 'true' WANDB_API_KEY: ${WANDB_API_KEY} + NETUID: 268 + HOST_CUDA_VERSION: 12.6 + R2_ACCOUNT_ID: ${R2_ACCOUNT_ID} + R2_READ_ACCESS_KEY_ID: ${R2_READ_ACCESS_KEY_ID} + R2_READ_SECRET_ACCESS_KEY: ${R2_READ_SECRET_ACCESS_KEY} + R2_WRITE_ACCESS_KEY_ID: ${R2_WRITE_ACCESS_KEY_ID} + R2_WRITE_SECRET_ACCESS_KEY: ${R2_WRITE_SECRET_ACCESS_KEY} deploy: resources: reservations: devices: - driver: nvidia - device_ids: ['0', '1', '2'] - capabilities: [gpu] + device_ids: [ '0', '1', '2' ] + capabilities: [ gpu ] miner2: build: - context: . - dockerfile: Dockerfile + context: .. + dockerfile: docker/Dockerfile container_name: templar-miner-M222 volumes: - ~/.bittensor/wallets:/root/.bittensor/wallets - - ./logs:/app/logs + - ../logs:/app/logs environment: NODE_TYPE: miner WALLET_NAME: Bistro @@ -39,22 +46,29 @@ services: NETWORK: test DEBUG: 'true' WANDB_API_KEY: ${WANDB_API_KEY} + NETUID: 268 + HOST_CUDA_VERSION: 12.6 + R2_ACCOUNT_ID: ${R2_ACCOUNT_ID} + R2_READ_ACCESS_KEY_ID: ${R2_READ_ACCESS_KEY_ID} + R2_READ_SECRET_ACCESS_KEY: ${R2_READ_SECRET_ACCESS_KEY} + R2_WRITE_ACCESS_KEY_ID: ${R2_WRITE_ACCESS_KEY_ID} + R2_WRITE_SECRET_ACCESS_KEY: ${R2_WRITE_SECRET_ACCESS_KEY} deploy: resources: reservations: devices: - driver: nvidia - device_ids: ['0', '1', '2'] - capabilities: [gpu] + device_ids: [ '0', '1', '2' ] + capabilities: [ gpu ] validator: build: - context: . - dockerfile: Dockerfile + context: .. + dockerfile: docker/Dockerfile container_name: templar-validator-V11 volumes: - ~/.bittensor/wallets:/root/.bittensor/wallets - - ./logs:/app/logs + - ../logs:/app/logs environment: NODE_TYPE: validator WALLET_NAME: Bistro @@ -63,10 +77,17 @@ services: NETWORK: test DEBUG: 'true' WANDB_API_KEY: ${WANDB_API_KEY} + NETUID: 268 + HOST_CUDA_VERSION : 12.6 + R2_ACCOUNT_ID: ${R2_ACCOUNT_ID} + R2_READ_ACCESS_KEY_ID: ${R2_READ_ACCESS_KEY_ID} + R2_READ_SECRET_ACCESS_KEY: ${R2_READ_SECRET_ACCESS_KEY} + R2_WRITE_ACCESS_KEY_ID : ${R2_WRITE_ACCESS_KEY_ID} + R2_WRITE_SECRET_ACCESS_KEY : ${R2_WRITE_SECRET_ACCESS_KEY} deploy: resources: reservations: devices: - driver: nvidia - device_ids: ['0', '1', '2'] - capabilities: [gpu] \ No newline at end of file + device_ids: [ '0', '1', '2' ] + capabilities: [ gpu ] diff --git a/docs/checkpointing.md b/docs/checkpointing.md deleted file mode 100644 index 79d379f..0000000 --- a/docs/checkpointing.md +++ /dev/null @@ -1,124 +0,0 @@ -# Checkpointing in Miners and Validators - -This document explains the checkpointing mechanism used in both miners and validators, highlighting the process and key differences between them. - -## Overview - -Checkpointing is a crucial feature that allows miners and validators to save their current state periodically. This enables them to resume operations seamlessly after interruptions, such as crashes or restarts, without losing significant progress. - -- **Miners** save their training state, including the model parameters, optimizer state, scheduler state, and current training step. -- **Validators** save their evaluation state, including the model parameters, evaluation scores, weights, and current evaluation step. - -## Asynchronous Checkpoint Saving - -Both miners and validators utilize asynchronous checkpoint saving to prevent blocking the main training or evaluation loops. By saving checkpoints asynchronously, the processes can continue their operations without waiting for the checkpoint to be saved, enhancing performance and efficiency. - -### Key Features - -- **Non-Blocking**: Checkpoint saving runs in the background, allowing the main loop to proceed without delays. -- **Regular Intervals**: - - **Miners**: Save checkpoints every **500 training steps** (`global_step`). - - **Validators**: Save checkpoints every **500 blocks** based on the blockchain's block number. - -## Checkpointing in Miners - -### What is Saved - -- **Model State**: The current state of the model's parameters. -- **Optimizer State**: State of the optimizer to resume training seamlessly. -- **Scheduler State**: If a learning rate scheduler is used. -- **Global Step**: The current training step (`global_step`). -- **Additional State**: Any other variables necessary for training. - -### Saving Mechanism - -- Checkpoints are saved asynchronously every 500 training steps. -- The saving process uses asynchronous tasks to offload the I/O operations. -- Default checkpoint file is `checkpoint-M.pth`. - -### Restoring from Checkpoint - -- On startup, the miner checks for the existence of the checkpoint file. -- If found, it loads the saved states and resumes training from the saved `global_step`. -- No additional flags are required for checkpoint loading. - -## Checkpointing in Validators - -### What is Saved - -- **Model State**: The current state of the model's parameters. -- **Global Step**: The current evaluation step (`global_step`). -- **Scores**: Evaluation scores for different miners. -- **Weights**: Assigned weights based on evaluation. -- **Additional State**: Any other variables necessary for evaluation. - -### Saving Mechanism - -- Checkpoints are saved asynchronously every 500 blocks. -- The checkpointing is triggered based on the blockchain's block number. -- Uses asynchronous tasks to prevent blocking the evaluation loop. -- Default checkpoint file is `checkpoint-V.pth`. - -### Restoring from Checkpoint - -- On startup, the validator checks for the existence of the checkpoint file. -- If found, it loads the saved states and resumes evaluation from the saved `global_step`. -- No additional flags are required for checkpoint loading. - -## Differences Between Miner and Validator Checkpoints - -| Aspect | Miner | Validator | -|----------------------|------------------------------------------------------|--------------------------------------------------------| -| **Saving Frequency** | Every 500 **training steps** (`global_step`) | Every 500 **blocks** (blockchain block number) | -| **Trigger Condition**| `global_step % 500 == 0` | `current_block % 500 == 0` | -| **Saved States** | Model state, optimizer, scheduler, global step, etc. | Model state, global step, scores, weights, etc. | -| **Checkpoint File** | `miner_checkpoint.pth` | `validator_checkpoint.pth` | -| **Restoration** | Resumes training from saved `global_step` | Resumes evaluation from saved `global_step` | - -## Configuration - -### Setting the Checkpoint Path - -By default, the checkpoint files are saved with the names `checkpoint-M.pth` and `checkpoint-V.pth`. You can customize the checkpoint path using the `--checkpoint_path` argument when running the miner or validator. - -**Example**: - -```bash -# For Miner -python neurons/miner.py --checkpoint_path /path/to/custom_miner_checkpoint.pth - -# For Validator -python neurons/validator.py --checkpoint_path /path/to/custom_validator_checkpoint.pth -``` - -### Ensure Write Permissions - -Make sure that the process has read and write permissions to the directory where the checkpoint files are stored. - -## Best Practices - -- **Regular Monitoring**: Check logs to ensure that checkpoints are being saved and loaded correctly. -- **Avoid Overwriting**: Ensure that `global_step` is not being unintentionally reset after loading from a checkpoint. -- **Backup Checkpoints**: Periodically back up checkpoint files to prevent data loss. -- **Consistent Paths**: Use consistent checkpoint paths when running multiple processes to avoid confusion. - -## Troubleshooting - -- **Checkpoint Not Saving**: - - Verify that the checkpoint path is correct. - - Ensure the process has write permissions to the checkpoint location. - - Check for any errors in the logs during the checkpoint saving steps. -- **Global Step Reset to Zero**: - - Check that `global_step` is not being reinitialized after loading the checkpoint. - - Remove any code that sets `global_step = 0` after loading. -- **Checkpoint Not Loading**: - - Ensure the checkpoint file exists at the specified path. - - Verify that the file is not corrupted. - - Check logs for any exceptions during the loading process. -- **Asynchronous Saving Issues**: - - Ensure that the event loop is running correctly. - - Check for exceptions in the asynchronous tasks. - -## Conclusion - -Checkpointing is essential for maintaining continuity in mining and validation operations. By understanding the differences and properly configuring your setup, you can ensure efficient and reliable performance of your miner and validator nodes. diff --git a/docs/global_sync.md b/docs/global_sync.md deleted file mode 100644 index 5e78f02..0000000 --- a/docs/global_sync.md +++ /dev/null @@ -1,225 +0,0 @@ -# Global Step Synchronization - - -## Introduction - -This document explains how the system achieves **global step synchronization** among miners and validators. Synchronizing the `global_step` is crucial to ensure consistent training progression, learning rate scheduling, and coordinated model updates across all nodes participating in the decentralized training process. - -## Motivation - -Without global step synchronization, several issues can arise: - -- **Learning Rate Inconsistency**: New nodes or restarted nodes may start with `global_step` at zero, causing them to be in the warm-up phase of the learning rate scheduler while others are in stable or decay phases. -- **Training Progress Discrepancies**: Nodes operating at different `global_step` values may apply updates that are out of sync, leading to conflicts and suboptimal model performance. -- **Optimization Conflicts**: Asynchronous steps can cause conflicting gradients and hinder convergence. - -To address these issues, the system implements a mechanism to synchronize `global_step` across all nodes, ensuring cohesive training and optimal model updates. - -## Synchronization Mechanism Overview - -The synchronization of `global_step` is achieved through the following steps: - -1. **Embedding `global_step` in Model Slices**: Whenever a miner or validator uploads a model slice (state or delta), they include their current `global_step` in the data. -2. **Extracting `global_step` from Received Slices**: When nodes receive slices from others, they extract the `global_step` and keep track of the maximum value. -3. **Updating Local `global_step`**: Nodes update their local `global_step` to be the maximum between their current value and the maximum `global_step` from the received slices. -4. **Adjusting Learning Rate Schedulers**: After updating `global_step`, nodes adjust their learning rate schedulers to align with the new `global_step`. - -## Detailed Implementation - -### Including `global_step` in Uploaded Slices - -When miners and validators upload their model slices, they include the `global_step` as metadata. This is implemented in the `upload_slice_for_window` function. - -```python -# In src/templar/comms.py - -async def upload_slice_for_window( - bucket: str, - model: torch.nn.Module, - window: int, - seed: str, - wallet: 'bt.wallet', - compression: int, - key: str = 'slice', - global_step: int = 0 -): - filename = f'{key}-{window}-{wallet.hotkey.ss58_address}.pt' - logger.debug(f"Uploading slice to S3: {filename}") - - # Get indices for slicing - indices = await get_indices_for_window(model, seed, compression) - - # Create the slice data with global_step - slice_data = {'global_step': global_step} - for name, param in model.named_parameters(): - slice_data[name] = param.data.view(-1)[indices[name].to(model.device)].cpu() - - # Save and upload the slice_data - # ... existing code to save and upload to S3 ... -``` - -### Extracting and Updating `global_step` from Received Slices - -When applying slices from other nodes, the system extracts `global_step` and updates the local `global_step` accordingly. - -```python -# In src/templar/comms.py - -async def apply_slices_to_model( - model: torch.nn.Module, - window: int, - seed: str, - compression: int, - key: str = 'slice' -) -> int: - indices_dict = await get_indices_for_window(model, seed, compression) - slice_files = await load_files_for_window(window=window, key=key) - - max_global_step = 0 # Initialize max_global_step - - # Iterate over each slice file - for file_i in slice_files: - try: - slice_i = await get_slices(file_i, model.device) - slice_global_step = slice_i.get('global_step', 0) # Default to 0 if not present - max_global_step = max(max_global_step, slice_global_step) - - # Apply the slice to the model - # ... existing code to apply parameter slices ... - except Exception as e: - logger.exception(f"Error applying slice from {file_i}: {e}") - - # Return the maximum global_step found - return max_global_step -``` - -### Updating Local `global_step` and Adjusting the Scheduler - -After applying slices, nodes update their local `global_step` and adjust their learning rate schedulers to reflect the new training progress. - -```python -# In neurons/miner.py or neurons/validator.py - -# Apply slices and get max_global_step -max_global_step = await apply_slices_to_model( - model=self.model, - window=window, - seed=window, - compression=self.hparams.compression, - key='state' -) - -# Update local global_step -self.global_step = max(self.global_step, max_global_step) -self.scheduler.last_epoch = self.global_step - 1 # Update scheduler to match global_step -tplr.logger.info(f"Updated global step to {self.global_step}") -``` - -### Initializing or Loading `global_step` from Checkpoints - -When nodes start or restart, they load the `global_step` from saved checkpoints if available. - -```python -# In neurons/miner.py or neurons/validator.py - -# Load checkpoint if it exists -if os.path.exists(self.checkpoint_path): - tplr.logger.info(f"Loading checkpoint from {self.checkpoint_path}") - global_step, _ = asyncio.run(load_checkpoint( - filename=self.checkpoint_path, - model=self.model, - optimizer=self.optimizer, # For miners - scheduler=None, # Scheduler will be initialized later - device=self.config.device - )) - self.global_step = global_step - tplr.logger.info(f"Resumed from global step {self.global_step}") -else: - tplr.logger.info("No checkpoint found. Starting from scratch.") - self.global_step = 0 -``` - -### Adjusting the Learning Rate Scheduler - -When initializing the learning rate scheduler, the `last_epoch` parameter is set to `self.global_step - 1` to ensure the learning rate matches the current training stage. - -```python -# In neurons/miner.py or neurons/validator.py - -self.scheduler = get_wsd_scheduler( - optimizer=self.optimizer, - num_warmup_steps=self.hparams.num_warmup_steps, - num_stable_steps=self.hparams.num_stable_steps, - num_decay_steps=self.hparams.num_decay_steps, - last_epoch=self.global_step - 1 # Set to global_step - 1 -) -``` - -### Saving Checkpoints with `global_step` - -Nodes save their `global_step` along with other state information in checkpoints. - -```python -# In src/templar/comms.py - -async def save_checkpoint( - filename, - model, - optimizer=None, - scheduler=None, - global_step=0, - **kwargs -): - checkpoint = { - 'global_step': global_step, - 'model_state_dict': model.state_dict(), - # Include optimizer and scheduler states if available - # ... existing code ... - } - # Save the checkpoint asynchronously - await loop.run_in_executor(None, torch.save, checkpoint, filename) -``` - -## Handling Possible Scenarios - -### New Nodes Joining the Network - -- **Scenario**: A new miner or validator joins the network without prior checkpoints. -- **Handling**: - - The node starts with `global_step = 0`. - - Upon applying slices from other nodes, it updates its `global_step` to match the network. - - The learning rate scheduler is adjusted accordingly. - -### Node Restarts - -- **Scenario**: A node restarts due to a crash or manual restart. -- **Handling**: - - The node loads its saved `global_step` from the checkpoint. - - After applying new slices, it updates `global_step` if higher steps are found. - - The learning rate scheduler is realigned. - -### Missing `global_step` in Slices - -- **Scenario**: Some slices do not contain `global_step` (e.g., due to older software versions). -- **Handling**: - - Slices without `global_step` default to zero. - - The system uses the maximum `global_step` from all slices. - - Nodes avoid regressing `global_step` to a lower value. - -## Benefits of Global Step Synchronization - -- **Consistent Learning Rate Scheduling**: Ensures all nodes are in the same phase (warm-up, stable, decay) of the learning rate schedule. -- **Aligned Training Progress**: Nodes update and apply model parameters coherently. -- **Improved Model Convergence**: Synchronization reduces conflicting updates and promotes efficient training. -- **Enhanced Collaboration**: Facilitates smoother integration of contributions from various nodes. - -## Conclusion - -By embedding `global_step` within model slices and updating the local `global_step` based on the maximum received value, the system achieves effective synchronization across miners and validators. This mechanism ensures consistent training progression, coordinated updates, and optimal performance of the decentralized model training process. - ---- - -**Note**: For more details on checkpointing and learning rate scheduling, refer to the following documents: - -- [Checkpointing in Miners and Validators](checkpointing.md) -- [Learning Rate Scheduler Implementation](../src/templar/learning_rates.py) \ No newline at end of file diff --git a/docs/incentive_design.md b/docs/incentive_design.md deleted file mode 100644 index b8d6a94..0000000 --- a/docs/incentive_design.md +++ /dev/null @@ -1,203 +0,0 @@ -# Incentive Design - -## Introduction - -This document provides a detailed explanation of the incentive mechanism employed in the protocol, focusing on how miners and validators interact to collaboratively train a model while ensuring honest participation. The protocol is designed to encourage miners to contribute genuine model updates that improve the overall model performance, with validators evaluating these contributions to assign rewards accordingly. - -## Overview of the Protocol - -The protocol involves two main participants: - -- **Miners**: Nodes responsible for training the model on assigned data subsets and uploading their updates. -- **Validators**: Nodes that evaluate the miners' contributions by comparing the uploaded updates with locally computed gradients. - -The coordination between miners and validators ensures that only beneficial updates are integrated into the model, and participants are incentivized to act honestly. - -## Miners - -### Operations - -1. **Model Synchronization**: - - Miners start by synchronizing their model with the latest global state. - - They download **state slices** from other miners, ensuring their model parameters are up-to-date. - -2. **Data Acquisition**: - - Each miner receives a specific subset of the dataset (pages) for the current window. - - The data assignment is deterministic, based on a seed derived from the window number and the miner's UID. - -3. **Training**: - - Miners train their local model on the assigned data, performing gradient updates. - - The training is conducted for a specific number of steps, determined by the batch size and sequence length. - -4. **Delta Computation and Upload**: - - After training, miners compute the **delta** (the difference between the updated and initial model parameters). - - These deltas are compressed and uploaded to a designated S3 bucket associated with the miner. - -5. **Window Progression**: - - Miners proceed to the next window and repeat the process, ensuring continuous contribution to model training. - -### Formal Definitions - -- **Model Parameters**: $\theta^t$ at window $t$. -- **Updated Parameters**: $\theta^{t+1}$ after training. -- **Delta**: $\delta^t = \theta^{t+1} - \theta^t$. - -## Validators - -### Operations - -1. **Model Synchronization**: - - Validators synchronize their model to match the state at the beginning of the evaluation window. - - They download and apply **state slices** to ensure consistency. - -2. **Delta Acquisition**: - - Validators download the deltas uploaded by miners for the evaluation window. - -3. **Local Gradient Computation**: - - For each miner, the validator computes the local gradient $\hat{g}_i$ on the same data subset the miner was assigned. - -4. **Scoring**: - - Validators calculate the **cosine similarity** between each miner's delta $\delta_i$ and the validator's local gradient $\hat{g}_i$. - - This similarity score reflects how well the miner's update aligns with the true gradient. - -5. **Reward Assignment**: - - Based on the similarity scores, validators assign weights (rewards) to miners. - - These weights are normalized and set on the chain to influence the global model updates. - -### Formal Definitions - -- **Local Gradient**: $\hat{g}_i$, the gradient computed by the validator for miner $i$. -- **Miner's Delta**: $\delta_i$, uploaded by miner $i$. -- **Cosine Similarity**: - -$$ -s_i = \frac{\delta_i \cdot \hat{g}_i}{|\delta_i| |\hat{g}_i|} -$$ - -- **Assigned Weight**: $w_i$, proportional to $s_i$. - -## Incentive Mechanism - -### Objective - -The incentive mechanism aims to: - -- **Encourage Honest Participation**: Miners are motivated to perform genuine training and provide truthful updates. -- **Promote Model Improvement**: Only updates that positively contribute to the model are rewarded. -- **Discourage Malicious Behavior**: Malicious or random updates yield low or negative rewards, making dishonest behavior unprofitable. - -### Detailed Explanation - -#### Cosine Similarity Scoring - -For each miner $i$: - -1. **Compute Cosine Similarity**: - -$$ -s_i = \frac{\delta_i \cdot \hat{g}_i}{|\delta_i| |\hat{g}_i|} -$$ - - - Measures the alignment between the miner's update and the true gradient. - -2. **Interpretation of $s_i$**: - - **$s_i > 0$**: Miner’s update is in the same general direction as the true gradient, contributing positively. - - **$s_i \approx 0$**: Miner’s update is orthogonal to the true gradient, offering little to no benefit. - - **$s_i < 0$**: Miner’s update opposes the true gradient, potentially harmful. - -#### Weight Assignment - -1. **Initial Weight Calculation**: - - Assign initial weights proportional to the similarity scores: - -$$ -w_i\prime = \max(s_i, 0) -$$ - -2. **Normalization**: - - Normalize the weights to ensure they sum up to 1: - -$$ -w_i = \frac{w_i'}{\sum_j w_j'} -$$ - - This ensures the distribution of rewards is fair and proportional to positive contributions. - -#### Reward Distribution - -- **Total Reward Pool**: Determined by network parameters and available tokens. -- **Individual Reward**: - -$$ -R_i = R_{\text{total}} \times w_i -$$ - -- Miners receive rewards based on their normalized weights. - -### Formal Guarantees - -1. **Alignment Incentive**: - - Miners maximize rewards by aligning their updates with the true gradient. - - Honest training naturally leads to higher cosine similarity scores. - -2. **Robustness Against Malicious Behavior**: - - Malicious updates yield low or negative similarity scores. - - Negative scores are set to zero in weight assignment, nullifying rewards for harmful contributions. - -3. **Fair Reward Distribution**: - - Normalization ensures that rewards are proportionally distributed among positive contributors. - - Miners contributing more effectively to the model receive higher rewards. - -4. **Convergence Assurance**: - - By aggregating updates that align with the true gradients, the model is guaranteed to improve or converge under standard optimization assumptions. - -5. **Data Subset Specialization**: - - Miners focus on specific data subsets, promoting specialization and efficient coverage of the entire dataset. - -6. **Sybil Resistance**: - - Rewards are tied to the quality of contributions, not the number of identities. - - Multiple identities with low-quality updates do not gain an advantage. - -## Formal Analysis - -### Miner Utility Maximization - -Each miner seeks to maximize their expected reward \( R_i \): - -$$ -\max_{\delta_i} \quad R_i = R_{\text{total}} \times \frac{\max(s_i, 0)}{\sum_j \max(s_j, 0)} -$$ - -Subject to: - -- **Update Constraint**: $\delta_i = \theta^{t+1}_i - \theta^t$ -- **Training Dynamics**: $\theta^{t+1}_i = \theta^t - \eta \hat{g}_i$ (using learning rate $\eta$) - -The miner's optimal strategy is to set $\( \delta_i \)$ proportional to $\( -\hat{g}_i \)$, aligning with the negative gradient descent direction. - -### Validator Consistency - -Validators ensure that: - -- The evaluation is done fairly using consistent data subsets. -- The local gradients $\( \hat{g}_i \)$ are computed accurately. - -### Security Considerations - -1. **Data Integrity**: - - Data subsets are determined by deterministic functions, preventing miners from choosing favorable data. - -2. **Parameter Confidentiality**: - - Only parameter slices are shared, and the indices are not revealed in advance, reducing the risk of targeted attacks. - -3. **Resistance to Free Riders**: - - Miners not contributing meaningful updates do not receive rewards. - - Validators' scoring mechanism filters out non-beneficial contributions. - -## Conclusion - -The protocol's incentive mechanism effectively encourages miners to contribute authentic, high-quality updates to the global model. By tying rewards to the cosine similarity between miners' updates and validators' local gradients, the system ensures that only beneficial contributions are rewarded. Formal guarantees provide robustness against malicious actors and promote the overall improvement of the model through collaborative effort. - -The careful design of data assignment, update evaluation, and reward distribution creates a self-regulating ecosystem where honest participation is the most profitable strategy for miners, aligning individual incentives with the collective goal of training an effective model. diff --git a/docs/miner.md b/docs/miner.md index 285fb34..ad428b1 100644 --- a/docs/miner.md +++ b/docs/miner.md @@ -1,179 +1,308 @@ +Sure, I'll update the documentation to reflect the new recommended method using Docker Compose and ensure it's coherent following the Diátaxis framework. I'll also populate the `.env.example` file with the required variables and provide instructions for running without Docker. + +--- # Miner Setup -This document provides a guide on how to set up and run a miner using `miner.py`. It explains the workflow, configuration options, and step-by-step instructions to get a miner up and running. +This document provides a comprehensive guide on how to set up and run a miner using `miner.py`. Miners are crucial components of **τemplar**, responsible for training the model on assigned data subsets and sharing their gradients with peers. ## Table of Contents - [Miner Setup](#miner-setup) - - [Table of Contents](#table-of-contents) + - [Introduction](#introduction) - [Prerequisites](#prerequisites) - [Installation](#installation) - - [Automated Installation (WIP](#automated-installation-recommended) + - [Using Docker Compose (Recommended)](#using-docker-compose-recommended) - [Manual Installation](#manual-installation) - [Running the Miner](#running-the-miner) - - [Using PM2 (Recommended)](#using-pm2-recommended) - - [Important Flags](#important-flags) + - [Using Docker Compose](#using-docker-compose) + - [Running Without Docker](#running-without-docker) - [Configuration](#configuration) + - [Environment Variables](#environment-variables) - [Hardware Requirements](#hardware-requirements) - [Network Options](#network-options) - - [AWS Setup](#aws-setup) - [Monitoring](#monitoring) - [Logs](#logs) - [Performance](#performance) - [Troubleshooting](#troubleshooting) + - [Miner Operations](#miner-operations) + - [Model Synchronization](#model-synchronization) + - [Training Process](#training-process) + - [Gradient Sharing](#gradient-sharing) + +--- + +## Introduction + +This guide will help you set up and run a miner for **τemplar**. We'll cover both the recommended Docker Compose method and manual installation for environments where Docker is not preferred. + +--- ## Prerequisites - **NVIDIA GPU** with CUDA support - - Minimum 80GB VRAM recommended + - Minimum 24GB VRAM recommended - **Ubuntu** (or Ubuntu-based Linux distribution) -- **Python 3.12** -- **CUDA-compatible drivers** +- **Docker** and **Docker Compose** +- **Git** +- **Cloudflare R2 Bucket Configuration**: + - Permissions remain the same as before. + - **Bucket Setup**: + 1. **Create a Bucket**: Name it the same as your **account ID** and set the **region** to **ENAM**. + 2. **Generate Tokens**: + - **Read Token**: Admin Read permissions. + - **Write Token**: Admin Read & Write permissions. + 3. **Store Credentials**: You'll need these for the `.env` file. + +--- +## Installation -## Cloudflare R2 Bucket Configuration - To use buckets for sharing model slices, do the following: - 1. **Navigate to R2 Object Storage and Create a Bucket**: - - Name the bucket the same as your CloudFlare **account ID**. This can be found on the your [Cloudflare Dashboard](https://dash.cloudflare.com) in the lower right corner or the right side of the R2 Object Storage Overview page. Account IDs are not sensitive and are safe to share. - - Set the **region** to **ENAM** (Eastern North America). +### Using Docker Compose (Recommended) - 2. **Generate Tokens**: - - Navigate to the R2 Object Storage Overview page, on the left side, click "Manage R2 API Tokens". - - Create seperate **read** and **read/write** tokens. - - Note down the access key IDs and secret access keys for each token. These can also be retrieved at any time from your R2 API Token Management page - - ***Heads up***: The access key id and secret access key for your *read* token will be shared - with other neurons through commits to the network. The secrets for your write - token will stay secret. +1. **Install Docker and Docker Compose**: - 3. **Update `.env.yaml`**: - - Create the file `.env.yaml` by copying [`.env-template.yaml`](../.env-template.yaml) - and populate it with values from the previous steps: - ``` - cp .env-template.yaml .env.yaml - ``` - + ```bash + # Update package list + sudo apt-get update + # Install prerequisites + sudo apt-get install \ + ca-certificates \ + curl \ + gnupg \ + lsb-release + # Add Docker’s official GPG key + sudo mkdir -p /etc/apt/keyrings + curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg -## Installation + # Set up the repository + echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ + $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null - + # Install Docker Engine + sudo apt-get update + sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin -### Manual Installation + # Install Docker Compose + sudo apt-get install docker-compose + ``` -If you prefer to install manually, follow these steps: +2. **Clone the Repository**: -1. **Install System Dependencies**: -```bash -# Add Python 3.12 repository -sudo add-apt-repository ppa:deadsnakes/ppa -sudo apt-get update + ```bash + git clone https://github.com/tplr-ai/templar.git + cd templar + ``` -# Install required packages -sudo apt-get install git python3-pip jq npm -``` +3. **Navigate to the Docker Directory**: -2. **Install Node.js and PM2**: -```bash -npm install pm2 -g && pm2 update -``` + ```bash + cd docker + ``` -3. **Install Rust and uv**: -```bash -# Install Rust -curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -. "$HOME/.cargo/env" - -# Install uv and set python version to 3.12 -curl -LsSf https://astral.sh/uv/install.sh | sh -source $HOME/.local/bin/env -uv python install 3.12 && uv python pin 3.12 -``` +4. **Create and Populate the `.env` File**: -4. **Clone Repo**: -```bash -# Git Clone -git clone https://github.com/tplr-ai/templar.git -cd templar -``` + Create a `.env` file in the `docker` directory by copying the `.env.example`: + ```bash + cp .env.example .env + ``` -5. **Set Up Python Environment**: -```bash -# Create virtual environment -uv venv .venv -source .venv/bin/activate + Populate the `.env` file with your configuration. The variables that need to be set are: -# Install PyTorch -uv pip install torch --index-url https://download.pytorch.org/whl/cu118 + ```dotenv:docker/.env + WANDB_API_KEY=your_wandb_api_key -# Install requirements -uv sync --extra all -``` + # Cloudflare R2 Credentials + R2_ACCOUNT_ID=your_r2_account_id + R2_READ_ACCESS_KEY_ID=your_r2_read_access_key_id + R2_READ_SECRET_ACCESS_KEY=your_r2_read_secret_access_key -6. **Create and Register Wallets**: -```bash -# Create coldkey -btcli wallet new_coldkey --wallet.name default --n-words 12 + R2_WRITE_ACCESS_KEY_ID=your_r2_write_access_key_id + R2_WRITE_SECRET_ACCESS_KEY=your_r2_write_secret_access_key + # Wallet Configuration + WALLET_NAME=default + WALLET_HOTKEY=your_miner_hotkey_name -# Create and register hotkey -btcli wallet new_hotkey --wallet.name default --wallet.hotkey --n-words 12 -btcli subnet pow_register --wallet.name default --wallet.hotkey --netuid --subtensor.network -``` + # Network Configuration + NETWORK=finney + NETUID=3 -7. **Log into Weights & Biases (WandB)** -```bash -# Log into WandB -wandb login -``` + # GPU Configuration + CUDA_DEVICE=cuda:0 + + # Additional Settings + DEBUG=false + ``` + + Replace the placeholders with your actual values. + +5. **Update `docker-compose.yml`**: + + Ensure that the `docker-compose.yml` file is correctly configured for your setup (usually no changes are needed). + +6. **Run Docker Compose**: + + Start the miner using Docker Compose: + + ```bash + docker-compose up -d node + ``` + + This will start the miner in detached mode. + +### Manual Installation + +If you prefer to run the miner without Docker, follow the instructions in the [Running Without Docker](#running-without-docker) section. + +--- ## Running the Miner -### Using PM2 (Recommended) +### Using Docker Compose -PM2 automatically manages your miner processes and restarts them if they crash: +Assuming you've completed the installation steps above, your miner should now be running. You can verify this by listing running containers: ```bash -# Start a miner on each GPU - pm2 start neurons/miner.py --interpreter python3 --name miner -- \ - --actual_batch_size \ - --wallet.name default \ - --wallet.hotkey "name" \ - --device "cuda" \ - --use_wandb \ - --netuid \ - --subtensor.network \ - --process_name miner \ # Must match PM2's --name - --sync_state - - -# Monitor logs -pm2 logs - -# Check status -pm2 list +docker ps ``` -> **Important**: When using PM2, the `--process_name` argument must match the PM2 process name specified by `--name`. For example, if PM2 process is named `miner_C0`, use `--process_name miner_C0`. +You should see a container named `templar-miner-`. + +### Running Without Docker + +1. **Install System Dependencies**: + + ```bash + # Add Python 3.12 repository + sudo add-apt-repository ppa:deadsnakes/ppa + sudo apt-get update + + # Install required packages + sudo apt-get install python3.12 python3.12-venv git + ``` + +2. **Install NVIDIA CUDA Drivers**: + + Install the appropriate NVIDIA CUDA drivers for your GPU. + +3. **Clone the Repository**: + + ```bash + git clone https://github.com/tplr-ai/templar.git + cd templar + ``` -### Important Flags -- **`--process_name`**: (Required) Must match the PM2 process name when using PM2 -- **`--sync_state`**: Synchronizes model state with network history -- **`--actual_batch_size`**: Set based on GPU memory: - - 80GB+ VRAM: batch size 6 -- **`--netuid`**: Network subnet ID (e.g., 223 for testnet) -- **`--subtensor.network`**: Network name (finney/test/local) -- **`--no_autoupdate`**: Disable automatic code updates +4. **Set Up Python Environment**: + + ```bash + # Create virtual environment + python3.12 -m venv .venv + source .venv/bin/activate + + # Upgrade pip + pip install --upgrade pip + + # Install PyTorch with CUDA support + pip install torch --index-url https://download.pytorch.org/whl/cu118 + + # Install other requirements + pip install -r requirements.txt + + # Install uv tool (if needed) + pip install uv + ``` + +5. **Create and Register Wallets**: + + ```bash + # Create coldkey + btcli wallet new_coldkey --wallet.name default --n-words 12 + + # Create and register hotkey + btcli wallet new_hotkey --wallet.name default --wallet.hotkey miner --n-words 12 + btcli subnet pow_register --wallet.name default --wallet.hotkey miner --netuid --subtensor.network + ``` + +6. **Log into Weights & Biases (WandB)**: + + ```bash + wandb login your_wandb_api_key + ``` + +7. **Set Environment Variables**: + + Export necessary environment variables or create a `.env` file in the project root. + + ```bash + export WANDB_API_KEY=your_wandb_api_key + export R2_ACCOUNT_ID=your_r2_account_id + export R2_READ_ACCESS_KEY_ID=your_r2_read_access_key_id + export R2_READ_SECRET_ACCESS_KEY=your_r2_read_secret_access_key + export R2_WRITE_ACCESS_KEY_ID=your_r2_write_access_key_id + export R2_WRITE_SECRET_ACCESS_KEY=your_r2_write_secret_access_key + ``` + +8. **Run the Miner**: + + ```bash + python neurons/miner.py \ + --actual_batch_size 6 \ + --wallet.name default \ + --wallet.hotkey miner \ + --device cuda \ + --use_wandb \ + --netuid \ + --subtensor.network \ + --sync_state + ``` + +--- ## Configuration +### Environment Variables + +When using Docker Compose, set the following variables in the `docker/.env` file: + +```dotenv:docker/.env +WANDB_API_KEY=your_wandb_api_key + +# Cloudflare R2 Credentials +R2_ACCOUNT_ID=your_r2_account_id + +R2_READ_ACCESS_KEY_ID=your_r2_read_access_key_id +R2_READ_SECRET_ACCESS_KEY=your_r2_read_secret_access_key + +R2_WRITE_ACCESS_KEY_ID=your_r2_write_access_key_id +R2_WRITE_SECRET_ACCESS_KEY=your_r2_write_secret_access_key + +# Wallet Configuration +WALLET_NAME=default +WALLET_HOTKEY=your_miner_hotkey_name + +# Network Configuration +NETWORK=finney +NETUID=3 + +# GPU Configuration +CUDA_DEVICE=cuda:0 + +# Additional Settings +DEBUG=false +``` + +**Note**: The R2 permissions remain unchanged from previous configurations. + ### Hardware Requirements -- **GPU Memory Requirements**: - - Recommended: 80GB+ VRAM +- **GPU Requirements**: + - Minimum: NVIDIA H100 with 80GB VRAM - **Storage**: 100GB+ recommended for model and data - **RAM**: 32GB+ recommended - **Network**: Stable internet connection with good bandwidth @@ -182,36 +311,66 @@ pm2 list - **Mainnet (Finney)**: - Network: `finney` - - Netuid: 3 + - Netuid: `3` - **Testnet**: - Network: `test` - - Netuid: 223 - - Endpoint: `wss://test.finney.opentensor.ai:443/` + - Netuid: `223` - **Local**: - Network: `local` - - Netuid: 3 - - Endpoint: `wss://localhost:9944` + - Netuid: `1` + +--- ## Monitoring ### Logs -- **PM2 Logs**: `pm2 logs [miner_name]` -- **System Monitoring**: `pm2 monit` -- **Weights & Biases**: Enable with `--use_wandb` +- **Docker Logs**: + + ```bash + docker logs -f templar-miner-${WALLET_HOTKEY} + ``` + +- **Weights & Biases**: + + - Ensure `--use_wandb` is enabled + - Monitor training metrics and performance on your WandB dashboard ### Performance -Monitor key metrics: +Keep an eye on: + - GPU utilization - Memory usage - Network bandwidth - Training progress - Rewards and weights - +--- diff --git a/docs/validator.md b/docs/validator.md index 7f00fd7..6df5147 100644 --- a/docs/validator.md +++ b/docs/validator.md @@ -1,198 +1,250 @@ # Validator Setup -This document provides a guide on how to set up and run a validator using `validator.py`. Validators are crucial components of the protocol, responsible for evaluating miners' contributions by comparing their uploaded deltas with locally computed gradients. +This document provides a comprehensive guide on how to set up and run a validator using `validator.py`. Validators are crucial components of **τemplar**, responsible for evaluating miners' contributions by assessing their uploaded gradients. ## Table of Contents - [Validator Setup](#validator-setup) - - [Table of Contents](#table-of-contents) + - [Introduction](#introduction) - [Prerequisites](#prerequisites) - [Installation](#installation) - - [Automated Installation (Recommended)](#automated-installation-recommended) + - [Using Docker Compose (Recommended)](#using-docker-compose-recommended) - [Manual Installation](#manual-installation) - [Running the Validator](#running-the-validator) - - [Using PM2 (Recommended)](#using-pm2-recommended) - - [Important Flags](#important-flags) + - [Using Docker Compose](#using-docker-compose) + - [Running Without Docker](#running-without-docker) - [Configuration](#configuration) + - [Environment Variables](#environment-variables) - [Hardware Requirements](#hardware-requirements) - [Network Options](#network-options) - - [AWS Setup](#aws-setup) - [Monitoring](#monitoring) - [Logs](#logs) - [Performance](#performance) + - [Troubleshooting](#troubleshooting) - [Validator Operations](#validator-operations) - [State Synchronization](#state-synchronization) - [Evaluation Process](#evaluation-process) - [Weight Setting](#weight-setting) - - [Troubleshooting](#troubleshooting) + +--- + +## Introduction + +This guide will help you set up and run a validator for **τemplar**. Validators play a critical role in maintaining the integrity of the network by evaluating miners' contributions and updating weights accordingly. + +--- ## Prerequisites - **NVIDIA GPU** with CUDA support - Minimum 24GB VRAM recommended - - Single GPU typically sufficient - **Ubuntu** (or Ubuntu-based Linux distribution) -- **Python 3.12** -- **CUDA-compatible drivers** +- **Docker** and **Docker Compose** +- **Git** - **Cloudflare R2 Bucket Configuration**: - - To use buckets for sharing model slices, do the following: - 1. **Navigate to R2 Object Storage and Create a Bucket**: - - Name the bucket the same as your **account ID**. - - Set the **region** to **ENAM**. - - - 2. **Generate Tokens**: - - Create a **read token** with **Admin Read** permissions and a **write token** with **Admin Read & Write** permissions. - - Note down the access key IDs and secret access keys for each token. - - 3. **Update `.env.yaml`**: - - Create the file `.env.yaml` by copying [`.env-template.yaml`](../.env-template.yaml) - and populate it with values from the previous steps: - ``` - cp .env-template.yaml .env.yaml - ``` - - The access key id and secret access key for your *read* token will be shared - with other neurons through commits to the network. The secrets for your write - token will stay secret. + - Permissions remain the same as before. + - **Bucket Setup**: + 1. **Create a Bucket**: Name it the same as your **account ID** and set the **region** to **ENAM**. + 2. **Generate Tokens**: + - **Read Token**: Admin Read permissions. + - **Write Token**: Admin Read & Write permissions. + 3. **Store Credentials**: You'll need these for the `.env` file. -- **Git** +--- ## Installation - + # GPU Configuration + CUDA_DEVICE=cuda:0 + + # Node Type + NODE_TYPE=validator + + # Additional Settings + DEBUG=false + ``` + + **Note**: Set `NODE_TYPE` to `validator`. + +5. **Update `docker-compose.yml`**: + + Ensure that the `docker-compose.yml` file is correctly configured for your setup. + +6. **Run Docker Compose**: + + Start the validator using Docker Compose: + + ```bash + docker-compose up -d node + ``` ### Manual Installation -If you prefer to install manually, follow these steps: +If you prefer to run the validator without Docker, follow the instructions in the [Running Without Docker](#running-without-docker) section. -1. **Install System Dependencies**: -```bash -# Add Python 3.12 repository -sudo add-apt-repository ppa:deadsnakes/ppa -sudo apt-get update +--- -# Install required packages -sudo apt-get install python3.12 python3.12-venv git npm -``` +## Running the Validator + +### Using Docker Compose + +After completing the installation steps, your validator should be running. Check it with: -2. **Install Node.js and PM2**: ```bash -curl -fsSL https://deb.nodesource.com/setup_18.x | sudo bash -sudo apt-get install -y nodejs -npm install pm2 -g +docker ps ``` -3. **Install Rust and uv**: -```bash -# Install Rust -curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -source $HOME/.cargo/env +You should see a container named `templar-validator-`. -# Install uv -curl -LsSf https://astral.sh/uv/install.sh | sh -``` +### Running Without Docker -4. **Set Up Python Environment**: -```bash -# Create virtual environment -uv venv .venv -source .venv/bin/activate +1. **Install System Dependencies**: -# Install PyTorch -uv pip install torch --index-url https://download.pytorch.org/whl/cu118 + Same as in the miner setup. -# Install requirements -uv sync --extra all -``` +2. **Install NVIDIA CUDA Drivers**: + + Install the appropriate NVIDIA CUDA drivers. + +3. **Clone the Repository**: + + ```bash + git clone https://github.com/tplr-ai/templar.git + cd templar + ``` + +4. **Set Up Python Environment**: + + Same as in the miner setup. 5. **Create and Register Validator Wallet**: -```bash -# Create coldkey -btcli wallet new_coldkey --wallet.name default --n-words 12 -# Create and register validator hotkey -btcli wallet new_hotkey --wallet.name default --wallet.hotkey validator --n-words 12 -btcli subnet pow_register --wallet.name default --wallet.hotkey validator --netuid --subtensor.network -``` + ```bash + # Create coldkey if not already created + btcli wallet new_coldkey --wallet.name default --n-words 12 -6. **Log into Weights & Biases (WandB)** -```bash -# Log into WandB -wandb login -``` + # Create and register validator hotkey + btcli wallet new_hotkey --wallet.name default --wallet.hotkey validator --n-words 12 + btcli subnet pow_register --wallet.name default --wallet.hotkey validator --netuid --subtensor.network + ``` -## Running the Validator +6. **Log into Weights & Biases (WandB)**: -### Using PM2 (Recommended) + ```bash + wandb login your_wandb_api_key + ``` -PM2 automatically manages your validator process and restarts it if it crashes: +7. **Set Environment Variables**: -```bash -pm2 start neurons/validator.py --interpreter python3 --name validator -- \ - --actual_batch_size 6 \ - --wallet.name default \ - --wallet.hotkey validator \ - --use_wandb \ - --netuid \ - --subtensor.network \ - --process_name validator \ # Must match PM2's --name - --sync_state - -> **Important**: When using PM2, the `--process_name` argument must match the PM2 process name specified by `--name`. In this example, PM2 process is named `validator`, so we use `--process_name validator`. - -# Monitor logs -pm2 logs validator - -# Check status -pm2 list -``` + Export necessary environment variables as in the miner setup. + +8. **Run the Validator**: -### Important Flags + ```bash + python neurons/validator.py \ + --actual_batch_size 6 \ + --wallet.name default \ + --wallet.hotkey validator \ + --device cuda \ + --use_wandb \ + --netuid \ + --subtensor.network \ + --sync_state + ``` -- **`--process_name`**: (Required) Must match the PM2 process name when using PM2 -- **`--sync_state`**: Synchronizes model state with network history (crucial) -- **`--actual_batch_size`**: Set based on GPU memory: - - 80GB+ VRAM: batch size 6 - - 40GB VRAM: batch size 3 - - 24GB VRAM: batch size 1 -- **`--netuid`**: Network subnet ID (e.g., 223 for testnet) -- **`--subtensor.network`**: Network name (finney/test/local) -- **`--no_autoupdate`**: Disable automatic code updates +--- ## Configuration +### Environment Variables + +Set the following in the `docker/.env` file when using Docker Compose: + +```dotenv:docker/.env +WANDB_API_KEY=your_wandb_api_key + +# Cloudflare R2 Credentials +R2_ACCOUNT_ID=your_r2_account_id + +R2_READ_ACCESS_KEY_ID=your_r2_read_access_key_id +R2_READ_SECRET_ACCESS_KEY=your_r2_read_secret_access_key + +R2_WRITE_ACCESS_KEY_ID=your_r2_write_access_key_id +R2_WRITE_SECRET_ACCESS_KEY=your_r2_write_secret_access_key + +# Wallet Configuration +WALLET_NAME=default +WALLET_HOTKEY=your_validator_hotkey_name + +# Network Configuration +NETWORK=finney +NETUID=3 + +# GPU Configuration +CUDA_DEVICE=cuda:0 + +# Node Type +NODE_TYPE=validator + +# Additional Settings +DEBUG=false +``` + +**Note**: The R2 permissions remain unchanged. + ### Hardware Requirements -- **GPU Memory Requirements**: - - Minimum: 24GB VRAM - - Recommended: 40GB VRAM - - Optimal: 80GB VRAM +- **GPU Requirements**: + - Minimum: NVIDIA H100 with 80GB VRAM - **Storage**: 200GB+ recommended for model and evaluation data - **RAM**: 32GB+ recommended - **Network**: High-bandwidth, stable connection for state synchronization @@ -201,65 +253,113 @@ pm2 list - **Mainnet (Finney)**: - Network: `finney` - - Netuid: 3 + - Netuid: `3` - **Testnet**: - Network: `test` - - Netuid: 223 - - Endpoint: `wss://test.finney.opentensor.ai:443/` + - Netuid: `223` - **Local**: - Network: `local` - - Netuid: 1 - - Endpoint: `wss://localhost:9944` + - Netuid: `1` + +--- ## Monitoring ### Logs -- **PM2 Logs**: `pm2 logs validator` -- **System Monitoring**: `pm2 monit` -- **Weights & Biases**: Enable with `--use_wandb` - - Training metrics - - Evaluation scores - - Network statistics +- **Docker Logs**: + + ```bash + docker logs -f templar-validator-${WALLET_HOTKEY} + ``` + +- **Weights & Biases**: + + - Ensure `--use_wandb` is enabled + - Monitor evaluation metrics and network statistics ### Performance -Monitor key metrics: +Key metrics to monitor: + - GPU utilization - Memory usage - Network bandwidth - Evaluation throughput - Weight setting frequency +--- + +## Troubleshooting + +- **State Synchronization Failures**: Check network settings and ensure the validator is properly registered and connected. +- **Out of Memory Errors**: Reduce `--actual_batch_size`. +- **Network Connectivity Issues**: Verify firewall settings and network configurations. + +--- + ## Validator Operations ### State Synchronization -- Initial sync downloads full model state -- Continuous sync with new updates -- Delta application and verification -- State consistency checks +- The validator synchronizes its model with the latest global state. +- It gathers and applies gradients from miners to maintain consistency. ### Evaluation Process -1. Download miner deltas -2. Compute local gradients -3. Compare and score contributions -4. Update weights based on quality +1. **Collect Miner Gradients**: Gathers compressed gradients submitted by miners. +2. **Evaluate Contributions**: Assesses the impact of each miner's gradient on model performance. +3. **Compute Scores**: Calculates scores based on loss improvement. +4. **Update Weights**: Adjusts miners' weights on the blockchain accordingly. ### Weight Setting -- Scoring mechanisms -- Weight update frequency -- Impact on network consensus -- Optimization strategies +- **Scoring Mechanism**: Based on the performance improvement contributed by miners. +- **Update Frequency**: Weights are periodically updated on the blockchain. +- **Impact**: Influences reward distribution and miner reputation in the network. + +--- + +# `.env.example` + +Here's the updated `.env.example` file populated with all the required variables: + +```dotenv:docker/.env.example +# Weights & Biases API Key +WANDB_API_KEY=your_wandb_api_key + +# Cloudflare R2 Credentials +R2_ACCOUNT_ID=your_r2_account_id + +# Read Token (Shared with other neurons) +R2_READ_ACCESS_KEY_ID=your_r2_read_access_key_id +R2_READ_SECRET_ACCESS_KEY=your_r2_read_secret_access_key + +# Write Token (Keep Secret) +R2_WRITE_ACCESS_KEY_ID=your_r2_write_access_key_id +R2_WRITE_SECRET_ACCESS_KEY=your_r2_write_secret_access_key + +# Wallet Configuration +WALLET_NAME=default +WALLET_HOTKEY=your_hotkey_name + +# Network Configuration +NETWORK=finney +NETUID=3 + +# GPU Configuration +CUDA_DEVICE=cuda:0 + +# Node Type (miner or validator) +NODE_TYPE=miner + +# Additional Settings +DEBUG=false +``` - +- Replace all placeholders with your actual credentials. +- The `.env` file should be placed in the `docker` directory (`templar/docker/.env`). +- Permissions for R2 remain the same; read credentials are shared, write credentials are kept secret. diff --git a/hparams.json b/hparams.json index 520eb88..64c717c 100644 --- a/hparams.json +++ b/hparams.json @@ -5,7 +5,7 @@ "pages_per_window": 2, "batch_size": 6, "learning_rate": 4e-4, - "blocks_per_window": 2, + "blocks_per_window": 3, "windows_per_sync": 100, "windows_per_weights": 10, "momentum_decay": 0.999, @@ -22,6 +22,7 @@ "max_position_embeddings": 2048, "weight_decay": 0.1, "warmup_steps": 250, - "alpha_f": 0.1, - "t_max": 20000 + "alpha_f": 0.1, + "t_max": 20000, + "validator_offset": 4 } \ No newline at end of file diff --git a/justfile b/justfile deleted file mode 100644 index 5dff88e..0000000 --- a/justfile +++ /dev/null @@ -1,73 +0,0 @@ -# Start all miners -start: - pm2 start neurons/miner.py --interpreter python3 --name M1 -- --actual_batch_size 6 --wallet.name temple_run --wallet.hotkey temple_runner --device cuda:6 --use_wandb --autoupdate --process_name M1 --sync - pm2 start neurons/miner.py --interpreter python3 --name M2 -- --actual_batch_size 6 --wallet.name noa-d --wallet.hotkey C0 --device cuda:5 --use_wandb --autoupdate --process_name M2 --sync - pm2 start neurons/miner.py --interpreter python3 --name M3 -- --actual_batch_size 6 --wallet.name noa-d --wallet.hotkey C1 --device cuda:4 --use_wandb --autoupdate --process_name M3 --sync - - -# Stop all miners -stop: - pm2 delete M1 M2 M3 - -# Stop all miners -restart: - pm2 restart M1 M2 M3 - -# Restart all miners with clean state -restart-clean: stop restart - - -# Start testnet miners and validator -start-testnet project='gonso-2': - pm2 start neurons/miner.py --interpreter python3 --name TM1 -- --actual_batch_size 6 --wallet.name Bistro --wallet.hotkey M111 --device cuda:1 --use_wandb --autoupdate --process_name TM1 --project {{project}} --netuid 223 --test - pm2 start neurons/miner.py --interpreter python3 --name TM2 -- --actual_batch_size 6 --wallet.name Bistro --wallet.hotkey M222 --device cuda:2 --use_wandb --autoupdate --process_name TM2 --project {{project}} --netuid 223 --test - pm2 start neurons/validator.py --interpreter python3 --name TV1 -- --actual_batch_size 6 --wallet.name Bistro --wallet.hotkey V11 --device cuda:3 --use_wandb --autoupdate --process_name TV1 --project {{project}} --netuid 223 --test - -# Stop testnet miners and validator -stop-testnet: - pm2 delete TM1 TM2 TV1 - -# Restart testnet miners and validator -restart-testnet: - pm2 restart TM1 TM2 TV1 - -# Remove checkpoint folder -clean-checkpoints: - rm -rf checkpoints/ - -# Clean testnet data (R2 bucket and local folders) -clean-testnet: - python3 scripts/clean_testnet.py - -# Restart testnet with clean state -restart-testnet-clean project='gonso-2': stop-testnet clean-testnet - just start-testnet {{project}} - -# Run tests with uv -test: - export NEST_ASYNCIO=1 && uv run pytest - -# Run ruff linting -lint: - ruff check --fix - -# Run code formatting -format: - ruff format - -# Run all code quality checks and tests -check: format lint test - -# Stop evaluator -stop-eval: - pm2 delete Eval - -# Start evaluator -start-eval: - pm2 start scripts/eval.py --interpreter python3 --name Eval -- --actual_batch_size 6 --device cuda:0 --use_wandb --process_name Eval - -# Restart evaluator -restart-eval: - pm2 restart Eval - - diff --git a/neurons/miner.py b/neurons/miner.py index e17d42a..d0fdb03 100644 --- a/neurons/miner.py +++ b/neurons/miner.py @@ -17,14 +17,12 @@ # fmt: off # Standard library -import os import sys import time import random import asyncio import argparse import threading -from typing import Dict # Third party import torch @@ -60,11 +58,10 @@ class Miner: def config(): parser = argparse.ArgumentParser(description='Miner script') parser.add_argument('--netuid', type=int, default=268, help='Bittensor network UID.') - parser.add_argument('--project', type=str, default='llama-demo-1', help='Wandb project.') + parser.add_argument('--project', type=str, default='templar-1', help='Wandb project.') parser.add_argument('--device', type=str, default='cuda', help='Device to use for training') parser.add_argument('--debug', action='store_true', help='Enable debug logging') parser.add_argument('--trace', action='store_true', help='Enable trace logging') - parser.add_argument('--use_wandb', action='store_true', help='Use Weights and Biases for logging') parser.add_argument('--peers', type=int, nargs='+', default=[], help='List of UIDs to peer with') bt.subtensor.add_args(parser) bt.logging.add_args(parser) @@ -108,7 +105,7 @@ def __init__(self): self.optimizer, start_factor=0.1, end_factor=1.0, - total_iters=250, + total_iters=10, ) cosine_scheduler = CosineAnnealingWarmRestarts( self.optimizer, @@ -119,7 +116,7 @@ def __init__(self): self.scheduler = SequentialLR( self.optimizer, schedulers=[warmup_scheduler, cosine_scheduler], - milestones=[250], + milestones=[10], ) # Init compression @@ -153,14 +150,24 @@ def __init__(self): self.stop_event = asyncio.Event() self.current_block = self.subtensor.block self.current_window = int(self.current_block / self.hparams.blocks_per_window) + self.step_counter = 0 - # Init wandb - if self.config.use_wandb: - self.wandb = tplr.WandbManager( - uid=self.uid, - config=self.config, - is_validator=False, - ).run + # Add step tracking + self.global_step = 0 + self.window_step = 0 + + # Track additional metrics + self.total_tokens_processed = 0 + self.batch_times = [] # For tracking processing speed + + # Initialize WandB + self.wandb = tplr.initialize_wandb( + run_prefix='M', + uid=self.uid, + config=self.config, + group='miner', + job_type='mining' + ) # Main training loop. async def run(self): @@ -172,7 +179,9 @@ async def run(self): uid=str(validator_uid), window=self.current_window, key='checkpoint', - timeout=240 + timeout=240, + local=False, + stale_retention=10 ) if state_dict is not None: self.model.load_state_dict(state_dict) @@ -208,36 +217,87 @@ async def run(self): pages_info = pages, tokenizer = self.tokenizer ) - tplr.logger.info(f"Pages: {[p[1] for p in pages]} for UID: {self.config.uid} and Window: {step_window}") + tplr.logger.info(f"Pages: {[p[1] for p in pages]} for Window: {step_window}") - # Accumulate gradient. + # Accumulate gradient start_time = time.time() tplr.logger.info("Start accumulating...") self.optimizer.zero_grad() self.model.zero_grad() total_loss = 0 + batch_tokens = 0 + for i, batch in enumerate(loader): input_ids = torch.tensor(batch, dtype=torch.long).to(self.model.device) labels = input_ids.clone() labels = torch.where(labels == self.tokenizer.pad_token_id, -100, labels) + with torch.amp.autocast(device_type=self.model.device.type, dtype=torch.bfloat16): outputs = self.model(input_ids=input_ids, labels=labels) + total_loss += outputs.loss.item() outputs.loss.backward() + + # Track tokens + batch_tokens += (labels != -100).sum().item() + tplr.logger.info(f'loss: {outputs.loss.item()}') if self.current_window != step_window: tplr.logger.info('') break tplr.logger.info(f"Stopped accumulating: {i+1} batches with {(i+1) * self.hparams.batch_size * self.hparams.sequence_length} tokens") + + # Calculate processing metrics duration = time.time() - start_time - - # Log metrics - if self.config.use_wandb: + self.batch_times.append(duration) + self.total_tokens_processed += batch_tokens + + # Enhanced wandb logging with both existing and new metrics + self.wandb.log({ + # Training metrics + "miner/loss": total_loss/(i+1), + "miner/tokens_per_sec": ((i+1) * self.hparams.batch_size * self.hparams.sequence_length)/duration, + "miner/batch_duration": duration, + "miner/total_tokens": self.total_tokens_processed, + "miner/batch_tokens": batch_tokens, + "miner/global_step": self.global_step, + + # Resource metrics + "miner/gpu_memory_allocated": torch.cuda.memory_allocated() / 1024**2, # MB + "miner/gpu_memory_cached": torch.cuda.memory_reserved() / 1024**2, # MB + + # Network metrics + "miner/active_peers": len(self.peers), + "miner/effective_batch_size": len(self.peers) * self.hparams.batch_size, + + # Optimization metrics + "miner/learning_rate": self.scheduler.get_last_lr()[0], + }, step=self.global_step) + + # Log gradient metrics + grad_norms = [p.grad.norm().item() for p in self.model.parameters() if p.grad is not None] + weight_norms = [p.norm().item() for p in self.model.parameters()] + momentum_norms = [m.norm().item() for m in self.momentum.values()] + + self.wandb.log({ + # Gradient metrics + "miner/mean_grad_norm": sum(grad_norms) / len(grad_norms) if grad_norms else 0, + "miner/max_grad_norm": max(grad_norms) if grad_norms else 0, + "miner/mean_weight_norm": sum(weight_norms) / len(weight_norms), + "miner/mean_momentum_norm": sum(momentum_norms) / len(momentum_norms), + + # Distribution metrics + "miner/grad_norm_distribution": self.wandb.Histogram(grad_norms), + "miner/weight_norm_distribution": self.wandb.Histogram(weight_norms), + "miner/momentum_norm_distribution": self.wandb.Histogram(momentum_norms), + }, step=self.global_step) + + # Log per-peer metrics + for peer_uid in self.peers: self.wandb.log({ - "loss": total_loss/(i+1), - "tokens_per_sec": ((i+1) * self.hparams.batch_size * self.hparams.sequence_length)/duration - }) - + f"miner/peer_stake/{peer_uid}": self.metagraph.S[peer_uid].item(), + }, step=self.global_step) + # Reduce gradient using DeMo. gradient = {} xshapes = {} @@ -277,37 +337,56 @@ async def run(self): window=step_window, key='gradient', timeout=5, - device=self.config.device + device=self.config.device, + local=False, + stale_retention=10 ) - # Decompress state and apply grad + # Decompress state and apply to grad. for n, p in self.model.named_parameters(): - # Decompress all gradients in batch form - new_grad = self.transformer.decode( - self.compressor.batch_decompress( - p, gather_result[n + 'idxs'], gather_result[n + 'vals'], - xshapes[n], totalks[n] + idxs_key = n + 'idxs' + vals_key = n + 'vals' + idxs = gather_result.state_dict.get(idxs_key) + vals = gather_result.state_dict.get(vals_key) + if idxs is not None and vals is not None: + # Ensure idx and val are lists of tensors + if not isinstance(idxs, (list, tuple)): + idxs = [idxs] + if not isinstance(vals, (list, tuple)): + vals = [vals] + + new_grad = self.transformer.decode( + self.compressor.batch_decompress( + p.to(self.config.device), + idxs, + vals, + xshapes[n], + totalks[n] + ) ) - ) - # Set recomputed gathered gradient - if p.grad is None: - p.grad = new_grad + # Set recomputed gathered gradient. + if p.grad is None: + p.grad = new_grad + else: + p.grad.copy_(new_grad) + # Sign-SGD + p.grad.sign_() else: - p.grad.copy_(new_grad) - # Sign-SGD - p.grad.sign_() - + tplr.logger.info(f"Gradient data missing for parameter {n}, skipping.") + # Apply optimizer step tplr.logger.info("Finish and step.") self.optimizer.step() self.scheduler.step() - if self.config.use_wandb: - self.wandb.log({"lr": self.scheduler.get_last_lr()[0]}) + self.global_step += 1 + self.window_step += 1 + tplr.logger.info(f"Total optimization steps: {self.global_step}") - # Wait for end of window + # Wait for next window tplr.logger.info("Wait for next window...") while self.current_window == step_window: await asyncio.sleep(0.1) + self.window_step = 0 # Listens for new blocks and sets self.current_block and self.current_window def block_listener(self, loop): diff --git a/neurons/validator.py b/neurons/validator.py index 1183c8d..5d4f6f2 100644 --- a/neurons/validator.py +++ b/neurons/validator.py @@ -54,7 +54,7 @@ class Validator: def config(): parser = argparse.ArgumentParser(description='Validator script') parser.add_argument('--netuid', type=int, default=268, help='Bittensor network UID.') - parser.add_argument('--project', type=str, default='llama-demo-1', help='Wandb project.') + parser.add_argument('--project', type=str, default='templar-1', help='Wandb project.') parser.add_argument('--device', type=str, default='cuda', help='Device to use for training') parser.add_argument('--debug', action='store_true', help='Enable debug logging') parser.add_argument('--trace', action='store_true', help='Enable trace logging') @@ -129,7 +129,7 @@ def __init__(self): milestones=[250] ) - # Init comms + # Init comms with required chain management args self.comms = tplr.comms.Comms( wallet=self.wallet, save_location='/tmp', @@ -155,14 +155,22 @@ def __init__(self): # Init scores self.scores = torch.zeros(self.metagraph.n, dtype=torch.float32) + self.moving_avg_scores = torch.zeros(self.metagraph.n, dtype=torch.float32) # Add moving average tracking + self.ma_alpha = 0.95 # Moving average decay factor - # Init wandb - if self.config.use_wandb: - self.wandb = tplr.WandbManager( - uid=self.uid, - config=self.config, - is_validator=True - ).run + # Add step tracking + self.global_step = 0 + self.window_step = 0 + self.eval_count = 0 # Track number of evaluations + + # Initialize WandB + self.wandb = tplr.initialize_wandb( + run_prefix='V', + uid=self.uid, + config=self.config, + group='validator', + job_type='validation' + ) async def run(self): # Try to load latest checkpoint @@ -173,7 +181,8 @@ async def run(self): uid=str(validator_uid), window=self.current_window, key='checkpoint', - timeout=240 + timeout=240, + local=False ) if state_dict is not None: self.model.load_state_dict(state_dict) @@ -193,6 +202,7 @@ async def run(self): ).start() while True: + step_window = self.current_window # Wait for validator offset while self.sync_window >= (self.current_window - self.hparams.validator_offset): tplr.logger.info(f'Waiting for validator window offset, synced: {self.sync_window}, current:{self.current_window}, offset:{self.hparams.validator_offset}') @@ -205,15 +215,23 @@ async def run(self): try: # Upload the model state directly using put await self.comms.put( - state_dict_or_path=self.model.state_dict(), + state_dict=self.model.state_dict(), uid=self.uid, - window_or_block=self.current_window, - key='checkpoint' + window=self.current_window, + key='checkpoint', + local=False ) tplr.logger.info(f"Successfully created checkpoint at window {self.current_window}") except Exception as e: tplr.logger.error(f"Failed to create checkpoint: {e}") + # Log checkpoint creation + if self.current_window % 500 == 0: + self.wandb.log({ + "checkpoint_window": self.current_window, + "global_step": self.global_step, + }, step=self.global_step) + # Catch up to current - validator_offset while self.sync_window < (self.current_window - self.hparams.validator_offset): self.sync_window += 1 @@ -228,8 +246,14 @@ async def run(self): key='gradient', timeout=5, device=self.config.device, - local=True, + local=False ) + # Log gradient stats + tplr.logger.info(f"Gradient stats - Window: {self.sync_window}") + # Check if any gradients were gathered + if not step_grads == 0: + tplr.logger.info("No gradients received, waiting for next window.") + continue # Decompress state and apply to gradients for n, p in self.model.named_parameters(): @@ -251,8 +275,8 @@ async def run(self): # Apply the optimizer step self.optimizer.step() self.scheduler.step() - if self.config.use_wandb: - self.wandb.log({"lr": self.scheduler.get_last_lr()[0]}) + + self.wandb.log({"lr": self.scheduler.get_last_lr()[0]}, step=self.global_step) # Get a random peer to eval on their gradient at self.sync_window + 1 eval_uid = random.choice(self.peers) @@ -270,22 +294,27 @@ async def run(self): ) tplr.logger.info(f'Evaluating uid: {eval_uid} on window: {self.sync_window + 1} with state from: {self.sync_window} and pages: {[p[1] for p in pages]}') - # Get loss on all samples from this window + # Compute and log loss before gradient application loss_before = 0 + n_tokens = 0 for i, batch in enumerate(loader): input_ids = torch.tensor(batch, dtype=torch.long).to(self.model.device) labels = input_ids.clone() labels = torch.where(labels == self.tokenizer.pad_token_id, -100, labels) loss_before += self.model(input_ids=input_ids, labels=labels).loss.item() - tplr.logger.info(f'Computed total loss before: {loss_before}') - + n_tokens += (labels != -100).sum().item() + + loss_before_per_token = loss_before / n_tokens if n_tokens > 0 else 0 + tplr.logger.info(f'Computed total loss before: {loss_before} ({loss_before_per_token:.4f} per token)') + # Get the gradients from this miner on this window eval_grad = await self.comms.get( - uid=eval_uid, - window=self.sync_window + 1, - key='gradient', - timeout=5, - local=True, + uid=eval_uid, + window=self.sync_window + 1, + key='gradient', + timeout=5, + local=False, + stale_retention=10 ) if eval_grad is None: score = 0 @@ -306,14 +335,21 @@ async def run(self): # Apply this grad to the param of the model using the learning rate of the scheduler p.data.sub_(decompressed_grad, alpha=self.scheduler.get_last_lr()[0]) - # Get loss after we apply the gradient + # Compute loss after gradient application loss_after = 0 + n_tokens = 0 for i, batch in enumerate(loader): input_ids = torch.tensor(batch, dtype=torch.long).to(self.model.device) labels = input_ids.clone() labels = torch.where(labels == self.tokenizer.pad_token_id, -100, labels) loss_after += self.model(input_ids=input_ids, labels=labels).loss.item() - tplr.logger.info(f'Computed total loss after: {loss_after}') + n_tokens += (labels != -100).sum().item() + if self.current_window != step_window: + tplr.logger.info('') + break + + loss_after_per_token = loss_after / n_tokens if n_tokens > 0 else 0 + tplr.logger.info(f'Computed total loss after: {loss_after} ({loss_after_per_token:.4f} per token)') # Remove gradient from the model for n, p in self.model.named_parameters(): @@ -329,17 +365,69 @@ async def run(self): # Apply this grad to the param of the model using the learning rate of the scheduler p.data.add_(decompressed_grad, alpha=self.scheduler.get_last_lr()[0]) + # Compute improvement metrics + loss_improvement = loss_before - loss_after + improvement_percentage = ((loss_before - loss_after) / loss_before * 100) if loss_before != 0 else 0 + # Compute score score = loss_before - loss_after tplr.logger.info(f'score: {score}') - + + # Log comprehensive metrics + self.wandb.log({ + "validator/loss_before": loss_before_per_token, + "validator/loss_after": loss_after_per_token, + "validator/loss_improvement": loss_improvement, + "validator/improvement_percentage": improvement_percentage, + "validator/eval_count": self.eval_count, + "validator/tokens_evaluated": n_tokens, + "validator/learning_rate": self.scheduler.get_last_lr()[0], + "validator/window": self.current_window, + "validator/global_step": self.global_step, + "validator/current_score": score, + }, step=self.global_step) + + # Update counters + self.global_step += 1 + self.eval_count += 1 + # Set weights if needed if self.sync_window % self.hparams.windows_per_weights == 0: # Update scores with new score self.scores[eval_uid] = self.hparams.scores_alpha * score + (1 - self.hparams.scores_alpha) * self.scores[eval_uid] - # Compute weights from scores - weights = torch.softmax(self.scores, dim=0) - + # Update moving average scores + self.moving_avg_scores[eval_uid] = self.ma_alpha * self.moving_avg_scores[eval_uid] + (1 - self.ma_alpha) * score + # Compute weights from moving average scores + weights = torch.softmax(self.moving_avg_scores, dim=0) + + # Log per-UID metrics + valid_score_indices = torch.nonzero(self.scores > 0).squeeze().view(-1) + for uid_i in valid_score_indices: + uid = uid_i.item() + self.wandb.log({ + f"validator/scores/{uid}": self.scores[uid_i].item(), + f"validator/moving_avg_scores/{uid}": self.moving_avg_scores[uid_i].item(), + f"validator/weights/{uid}": weights[uid_i].item(), + f"validator/stakes/{uid}": self.metagraph.S[uid_i].item(), + f"validator/current_score/{uid}": score if uid == eval_uid else 0, + }, step=self.global_step) + + # Log aggregate network statistics + self.wandb.log({ + "validator/active_miners": len(valid_score_indices), + "validator/mean_score": self.scores[valid_score_indices].mean().item(), + "validator/mean_moving_avg_score": self.moving_avg_scores[valid_score_indices].mean().item(), + "validator/max_score": self.scores.max().item(), + "validator/max_moving_avg_score": self.moving_avg_scores.max().item(), + "validator/mean_weight": weights[valid_score_indices].mean().item(), + "validator/weight_std": weights[valid_score_indices].std().item(), + + # Histograms + "validator/scores_distribution": self.wandb.Histogram(self.scores[valid_score_indices].cpu().numpy()), + "validator/moving_avg_scores_distribution": self.wandb.Histogram(self.moving_avg_scores[valid_score_indices].cpu().numpy()), + "validator/weights_distribution": self.wandb.Histogram(weights[valid_score_indices].cpu().numpy()), + }, step=self.global_step) + # Set weights on chain self.subtensor.set_weights( wallet=self.wallet, @@ -351,6 +439,21 @@ async def run(self): ) tplr.logger.info(f'Set weights on chain for window {self.sync_window}') + # Log weight update metrics + self.wandb.log({ + "validator/weight_update_window": self.sync_window, + "validator/mean_weight": weights.mean().item(), + "validator/max_weight": weights.max().item(), + "validator/min_weight": weights.min().item(), + "validator/weight_std": weights.std().item(), + }, step=self.global_step) + + # Apply the optimizer step + tplr.logger.info("Finish and step.") + self.optimizer.step() + self.scheduler.step() + tplr.logger.info(f"Total optimization steps: {self.global_step}") + def block_listener(self, loop): def handler(event, _u, _s): self.current_block = int(event['header']['number']) diff --git a/run.py b/run.py deleted file mode 100644 index 93c9574..0000000 --- a/run.py +++ /dev/null @@ -1,395 +0,0 @@ -# The MIT License (MIT) -# © 2024 templar.tech - -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the "Software"), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of -# the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. -# fmt: off - -# Global imports -import os -import sys -import time -import torch -import random -import asyncio -import argparse -import threading -import bittensor as bt -import torch.optim as optim -from transformers import LlamaForCausalLM -from torch.optim.lr_scheduler import CosineAnnealingWarmRestarts - -# Local imports -import tplr -import tplr.checkpoint - -# GPU optimizations. -torch.backends.cudnn.benchmark = True -torch.backends.cuda.matmul.allow_tf32 = True -torch.backends.cudnn.allow_tf32 = True - -class Neuron: - - # Command line config items. - @staticmethod - def config(): - parser = argparse.ArgumentParser(description='Miner / Validator script') - parser.add_argument('--netuid', type=int, default=268, help='Bittensor network UID.') - parser.add_argument('--device', type=str, default='cuda', help='Device to use for training (e.g., cpu or cuda)') - parser.add_argument('--debug', action='store_true', help='Enable debug logging') - parser.add_argument('--trace', action='store_true', help='Enable trace logging') - parser.add_argument('--use_wandb', action='store_true', help='Use Weights and Biases for logging') - parser.add_argument('--is_validator', action='store_true', help='If validator, turn on to run evals rather than train for incentive.') - parser.add_argument('--random', action='store_true', help='Trains on a random page instead of correctly assigned.') - parser.add_argument('--peers', type=int, nargs='+', default=[], help='List of UIDs to peer with. e.g., --uids 1 2 3') - parser.add_argument('--checkpoint_path', type=str, default=None, help='Path to save/load the checkpoint. If None, the path is set to checkpoint-M.pth.') - parser.add_argument('--save-location', type=str, default=None, help='Directory to save/load slice files') - bt.wallet.add_args( parser ) - bt.subtensor.add_args( parser ) - bt.logging.add_args( parser ) - config = bt.config( parser ) - if config.debug: - tplr.debug() - if config.trace: - tplr.trace() - return config - - def __init__(self): - tplr.logger.debug("Starting initialization...") - - # Init config from command line - self.config = Neuron.config() - - # # Init AutoUpdate - # self.autoupdate = tplr.autoupdate.AutoUpdate() - - # Load hyperparameters - self.hparams = tplr.load_hparams() - - # Init bittensor objects. - self.wallet = bt.wallet( config = self.config ) - self.subtensor = bt.subtensor( config = self.config ) - self.metagraph = self.subtensor.metagraph( self.config.netuid ) - if self.wallet.hotkey.ss58_address not in self.metagraph.hotkeys: - tplr.logger.error(f'\n\t[bold]The wallet {self.wallet} is not registered on subnet: {self.metagraph.netuid}[/bold]. You need to register first with: [blue]`btcli subnet register`[/blue]\n') - sys.exit() - self.uid = self.metagraph.hotkeys.index(self.wallet.hotkey.ss58_address) - tplr.logger.info('\n' + '-' * 40 + ' Objects ' + '-' * 40) - tplr.logger.info(f'\n{self.wallet}\n{self.subtensor}\n{self.metagraph}\nuid: {self.uid}') - tplr.logger.debug("Initialized bittensor objects...") - tplr.logger.debug("Initializing buckets...") - # Buckets must - self.buckets = {} # Initialize empty dict first - - - # Initialize the model with config from hparams - self.model = LlamaForCausalLM(self.hparams.model_config) - self.model.to(self.config.device) - # Print model parameters - total_params = sum(p.numel() for p in self.model.parameters()) - tplr.logger.info(f"Total parameters: {total_params:,}") - tplr.logger.debug("Initialized model...") - - # Init tokenizer. - self.tokenizer = self.hparams.tokenizer - - # Init optimizer. - self.momentum = {} - self.optimizer = optim.SGD(self.model.parameters(), lr = self.hparams.learning_rate) - for n, p in self.model.named_parameters(): - self.momentum[n] = torch.zeros_like(p) - self.scheduler = tplr.CosineWarmupScheduler( - optimizer=self.optimizer, - warmup_steps=self.hparams.warmup_steps, - alpha_f=self.hparams.alpha_f, - t_max=self.hparams.t_max - ) - - # Init compression. - self.transformer = tplr.compress.TransformDCT( self.model, target_chunk = self.hparams.target_chunk ) - self.compressor = tplr.compress.CompressDCT() - - # # Set checkpoint path => root dir as argumnet and pass root dir, in the init - # if self.config.checkpoint_path is None: - # # Default path if none provided - # self.checkpoint_path = f"checkpoints/checkpoint-{self.uid}.pth" - # else: - # self.checkpoint_path = self.config.checkpoint_path - - # # Create checkpoint directory if it doesn't exist - # os.makedirs(os.path.dirname(self.checkpoint_path), exist_ok=True) - - - # # Initialize checkpoint manager - # self.checkpoint_manager = tplr.checkpoint.CheckpointManager( - # model=self.model, - # checkpoint_path=self.checkpoint_path, - # wallet=self.wallet, - # device=self.config.device, - # optimizer=self.optimizer, - # scheduler=self.scheduler - # ) - - # # Load initial checkpoint - # tplr.logger.debug("Loading checkpoint...") - # self.global_step = asyncio.run( - # self.checkpoint_manager.load_from_highest_stake( - # metagraph=self.metagraph, - # buckets=self.buckets, - # optimizer=self.optimizer, - # scheduler=self.scheduler, - # is_validator=False, - # hparams=self.hparams - # ) - # ) - - - # Initialize Comms - self.comms = tplr.comms.Comms( - wallet=self.wallet, - save_location='/tmp', - key_prefix='model', - config=self.config, - netuid=self.config.netuid, - metagraph=self.metagraph, - hparams=self.hparams, - ) - - # Initialize peers with buckets - if not self.config.peers: - # Use peers with buckets from ChainManager - self.peers = self.comms.peers - tplr.logger.info(f'Filtered peers with buckets: {self.peers}') - else: - self.peers = self.config.peers # Use specified peers - - # Ensure we have at least one peer - if self.config.is_validator and not self.peers: - tplr.logger.error( - "No peers available for validation. Ensure there are miners with buckets registered." - ) - sys.exit(1) - - # Add self to peers if not already included - if self.uid not in self.peers: - self.peers.append(self.uid) - - tplr.logger.info(f'Active peers: {self.peers}') - - # Init state params. - self.stop_event = asyncio.Event() - self.current_block = self.subtensor.block - self.current_window = int( self.current_block / self.hparams.blocks_per_window ) - - # Init scores. - self.scores = torch.zeros(self.metagraph.n, dtype=torch.float32) - - # Init wandb. - if self.config.use_wandb: - self.wandb = tplr.wandb.WandbManager( - uid=self.uid, - config=self.config, - is_validator=self.config.is_validator - ).run - - # Main training loop. - async def run( self ): - - # Start background block listener. - self.loop = asyncio.get_running_loop() - self.listener = threading.Thread(target=self.block_listener, args=(self.loop,), daemon=True).start() - - # Run until stopped. - while True: - - # Record the window we are on. - step_window = self.current_window - # Get the uid to seed data (if validator, take random from peers.) - step_uid = self.uid if not self.config.is_validator else random.choice(self.peers) - tplr.logger.info('\n' + '-' * 40 + f' Window: {step_window} ' + '-' * 40) - - # Checkpoint: every X windows , the validators with the highest stake will comms.put into s3, if model is None - # wait until until next window that % 100 == 0, just gather validator with max stake - - # Optionally sync state. Take this out - if step_window % self.hparams.windows_per_sync == 0: - tplr.logger.info("Sync globally") - # This gather op is way too slow - # When a miner joins the the network , wait until a new checkpoint has being put up by the validator - gather_result = await self.comms.gather( - state_dict = self.model.state_dict(), - my_uid = self.uid, - uids = self.peers, - window = int(self.current_window/self.hparams.windows_per_sync), - key = 'model', - timeout = 30, - device = self.config.device - ) - # Take mean of all peers state - state_dict = {name: torch.mean(torch.stack(gather_result[name]), dim=0) for name in gather_result} - # Load state into model. - self.model.load_state_dict(state_dict) - tplr.logger.info("Done global sync.") - - # Get the pages for this window. - pages = await tplr.dataset.DatasetLoader.next_pages( - offset = step_window, - n_pages = self.hparams.pages_per_window, - seed = self.metagraph.hotkeys[ step_uid ] if not self.config.random else random.randint(10000) # Select seed from step_uid. - ) - loader = await tplr.dataset.DatasetLoader.create( - batch_size = self.hparams.batch_size, - sequence_length = self.hparams.sequence_length, - pages_info = pages, - tokenizer = self.tokenizer - ) - tplr.logger.info(f"Pages: {[p[1] for p in pages]} for UID: {step_uid} and Window: {step_window}") - - # Accumulate gradient. - tplr.logger.info("Start accumulating...") - self.optimizer.zero_grad() - self.model.zero_grad() - total_loss = 0 - for i, batch in enumerate(loader): - input_ids = torch.tensor(batch, dtype=torch.long).to(self.model.device) - labels = input_ids.clone() - labels = torch.where(labels == self.tokenizer.pad_token_id, -100, labels) - with torch.amp.autocast(device_type=self.model.device.type, dtype=torch.bfloat16): - outputs = self.model(input_ids=input_ids, labels=labels) - total_loss += outputs.loss.item() - outputs.loss.backward() - print('loss:', outputs.loss.item()) - if self.current_window != step_window: - break - tplr.logger.info(f"Stopped accumulating: {i+1} batches with {(i+1) * self.hparams.batch_size * self.hparams.sequence_length} tokens ") - # Log to wandb. - if self.config.use_wandb: - self.wandb.log({"loss": outputs.loss.item()}) - - # Reduce gradient using DeMo. - gradient = {} - xshapes = {} - totalks = {} - transmitted = {} - for n, p in self.model.named_parameters(): - # Step-Weight decay - p.data.mul_( 1.0 - self.scheduler.get_last_lr()[0] * self.hparams.weight_decay ) - # Momentum decay - self.momentum[n].mul_( self.hparams.momentum_decay ) - # Add the grad to the momentum. - self.momentum[n].add_( p.grad, alpha=self.scheduler.get_last_lr()[0] ) - # Compress gradient. - idxs, vals, xshape, totalk = self.compressor.compress( - self.transformer.encode(self.momentum[n]), self.hparams.topk_compression - ) - # Estimate transmitted gradient. - transmit_grad = self.transformer.decode( - self.compressor.decompress(p, idxs, vals, xshape, totalk) - ) - # Remove the transmitted from delta (double counting) - self.momentum[n].sub_(transmit_grad) - # Add to share_state - transmitted[ n ] = transmit_grad - gradient[ n + 'idxs'] = idxs - gradient[ n + 'vals'] = vals - xshapes[ n ] = xshape - totalks[ n ] = totalk - - # All-gather share state from all peers with timeout. - tplr.logger.info(f"Start gather: {self.peers}") - gather_result = await self.comms.gather( - state_dict = gradient, - my_uid = self.uid, - uids = self.peers, - window = step_window, - key = 'gradient', - timeout = 5, - device = self.config.device - ) - - # Decompress state and apply to grad. - for n, p in self.model.named_parameters(): - # Decode grad from all nodes - if self.config.is_validator: - # Get gradient for step uid we are evaluating. - eval_idx = gather_result[n + 'idxs'][ self.peers.index(step_uid) ] - eval_val = gather_result[n + 'vals'][ self.peers.index(step_uid) ] - # Decompress their gradient. - their_grad = self.transformer.decode( - self.compressor.decompress(p, eval_idx, eval_val, xshapes[ n ], totalks[ n ]) - ) - # Get my recreated gradient. - my_grad = transmitted[ n ] - # Compute cosine sim score. - score = torch.nn.functional.cosine_similarity(their_grad.flatten(), my_grad.flatten(), dim=0) - # Compute moving scores and weights. - self.scores[step_uid] = self.hparams.scores_alpha * score + (1 - self.hparams.scores_alpha) * self.scores[step_uid].expand_as(score) - self.weights = torch.softmax(self.scores, dim=0) - # Log scores and weights to wandb. - if self.config.use_wandb: - for uid in self.peers: - self.wandb.log({f"s{uid}": self.scores[uid], f"w{uid}": self.weights[uid] }) - - # Decompress all gradients in batch form to produce shared gradient. - new_grad = self.transformer.decode( - self.compressor.batch_decompress( - p, gather_result[n + 'idxs'], gather_result[n + 'vals'], xshapes[ n ], totalks[ n ] - ) - ) - # Set recomputed gathered gradient. - if p.grad is None: - p.grad = new_grad - else: - p.grad.copy_(new_grad) - # Sign-SGD - p.grad.sign_() - - # Apply the optimizer step - tplr.logger.info("Finish and step.") - self.optimizer.step() - self.scheduler.step() - # Set weights on the chain based on current weights. - if self.config.is_validator and step_window % self.hparams.windows_per_weights == 0: - - # Set weights on chain. - self.subtensor.set_weights( - wallet = self.wallet, - netuid = self.config.netuid, - uids = self.metagraph.uids, - weights = self.weights, - wait_for_inclusion = False, # Dont wait, fire and forget. - wait_for_finalization = False, - ) - - - # Wait for end of window (if not already done.) - while self.current_window == step_window: - time.sleep(0.1) - - # Listens for new blocks and sets self.current_block and self.current_window - def block_listener(self, loop): - def handler(event, _u, _s): - self.current_block = int(event['header']['number']) - if int( self.current_block / self.hparams.blocks_per_window ) != self.current_window: - self.current_window = int( self.current_block / self.hparams.blocks_per_window ) - while not self.stop_event.is_set(): - try: - bt.subtensor(config=self.config).substrate.subscribe_block_headers(handler) - break - except Exception: - time.sleep(1) - -# Start miner/validator. -if __name__ == "__main__": - asyncio.run( Neuron().run() ) diff --git a/scripts/clean.py b/scripts/clean.py deleted file mode 100644 index 745acea..0000000 --- a/scripts/clean.py +++ /dev/null @@ -1,69 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2024 Chakana.tech - -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the “Software”), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of -# the Software. - -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -import os -import boto3 -import typer -from dotenv import dotenv_values -from templar.constants import CF_REGION_NAME - -env_config = {**dotenv_values(".env"), **os.environ} -AWS_ACCESS_KEY_ID = env_config.get("AWS_ACCESS_KEY_ID") -AWS_SECRET_ACCESS_KEY = env_config.get("AWS_SECRET_ACCESS_KEY") -CLIENT: boto3.client = boto3.client( - "s3", - region_name=CF_REGION_NAME, - aws_access_key_id=AWS_ACCESS_KEY_ID, - aws_secret_access_key=AWS_SECRET_ACCESS_KEY, -) - - -def main( - bucket: str = "decis", -): - # Create your S3 connection. - client: boto3.client = boto3.client( - "s3", - region_name=CF_REGION_NAME, - aws_access_key_id=AWS_ACCESS_KEY_ID, - aws_secret_access_key=AWS_SECRET_ACCESS_KEY, - ) - continuation_token = None - while True: - if continuation_token: - response = client.list_objects_v2( - Bucket=bucket, ContinuationToken=continuation_token - ) - else: - response = client.list_objects_v2(Bucket=bucket) - - file_names = [content["Key"] for content in response.get("Contents", [])] - - # Delete all the filenames - for file_name in file_names: - client.delete_object(Bucket=bucket, Key=file_name) - print(f"Deleted {file_name}") - - # Check if there are more files to delete - continuation_token = response.get("NextContinuationToken") - if not continuation_token: - break - - -# Main function. -if __name__ == "__main__": - typer.run(main) diff --git a/scripts/clean_testnet.py b/scripts/clean_testnet.py deleted file mode 100644 index 8391e37..0000000 --- a/scripts/clean_testnet.py +++ /dev/null @@ -1,87 +0,0 @@ -import yaml -from pathlib import Path -import boto3 -import shutil - -import templar as tplr - - -def clean_r2_bucket(): - """Clean all checkpoint files from R2 bucket""" - try: - # Load credentials from .env.yaml - env_path = Path(__file__).parent.parent / ".env.yaml" - with open(env_path, "r") as f: - config = yaml.safe_load(f) - - # Get R2 credentials - account_id = config.get("account_id") - write_creds = config.get("write", {}) - access_key_id = write_creds.get("access_key_id") - secret_access_key = write_creds.get("secret_access_key") - - if not all([account_id, access_key_id, secret_access_key]): - raise ValueError("Missing required R2 credentials in .env.yaml") - - # R2 connection settings - session = boto3.Session( - aws_access_key_id=access_key_id, aws_secret_access_key=secret_access_key - ) - - s3 = session.client( - service_name="s3", - endpoint_url=f"https://{account_id}.r2.cloudflarestorage.com", - ) - - # List and delete all objects - paginator = s3.get_paginator("list_objects_v2") - - deleted_count = 0 - for page in paginator.paginate(Bucket=account_id): - if "Contents" in page: - for obj in page["Contents"]: - s3.delete_object(Bucket=account_id, Key=obj["Key"]) - deleted_count += 1 - - tplr.logger.success(f"Deleted {deleted_count} files from R2 bucket") - - except FileNotFoundError: - tplr.logger.error("Could not find .env.yaml file") - except yaml.YAMLError: - tplr.logger.error("Error parsing .env.yaml file") - except Exception as e: - tplr.logger.error(f"Error cleaning R2 bucket: {str(e)}") - - -def clean_local_folders(): - """Clean local wandb and checkpoints folders""" - try: - # Get project root directory - root_dir = Path(__file__).parent.parent - - # Clean wandb - wandb_dir = root_dir / "wandb" - if wandb_dir.exists(): - shutil.rmtree(wandb_dir) - tplr.logger.success("Cleaned wandb folder") - - # Clean checkpoints - checkpoints_dir = root_dir / "checkpoints" - if checkpoints_dir.exists(): - shutil.rmtree(checkpoints_dir) - tplr.logger.success("Cleaned checkpoints folder") - - except Exception as e: - tplr.logger.error(f"Error cleaning local folders: {str(e)}") - - -def main(): - """Main function to clean both R2 bucket and local folders""" - tplr.logger.info("Starting cleanup process...") - clean_r2_bucket() - clean_local_folders() - tplr.logger.success("Cleanup completed!") - - -if __name__ == "__main__": - main() diff --git a/scripts/decay.py b/scripts/decay.py deleted file mode 100644 index aa69776..0000000 --- a/scripts/decay.py +++ /dev/null @@ -1,345 +0,0 @@ -import os -import shutil -import sys -import json -import torch -import random -import asyncio -import numpy as np -from typing import Optional -from transformers import LlamaForCausalLM -import bittensor as bt -import argparse -import torch.optim as optim - -import templar as tplr - - -class DecayAgent: - @staticmethod - def config(): - parser = argparse.ArgumentParser(description="Decay agent script") - parser.add_argument( - "--project", type=str, default="templar", help="Optional wandb project name" - ) - parser.add_argument( - "--netuid", type=int, default=3, help="Bittensor network UID." - ) - parser.add_argument( - "--actual_batch_size", - type=int, - default=8, - help="Training batch size per accumulation.", - ) - parser.add_argument( - "--device", type=str, default="cuda", help="Device to use for training" - ) - parser.add_argument("--debug", action="store_true", help="Enable debug logging") - parser.add_argument("--trace", action="store_true", help="Enable trace logging") - parser.add_argument("--test", action="store_true", help="Run on test network") - parser.add_argument("--local", action="store_true", help="Run on local network") - parser.add_argument( - "--checkpoint_path", - type=str, - default=None, - help="Path to save/load the checkpoint", - ) - parser.add_argument( - "--save-location", - type=str, - default=None, - help="Directory to save/load slice files", - ) - bt.wallet.add_args(parser) - bt.subtensor.add_args(parser) - config = bt.config(parser) - if config.test: - config.subtensor.network = "test" - config.subtensor.chain_endpoint = "wss://test.finney.opentensor.ai:443/" - elif config.local: - config.subtensor.network = "local" - config.subtensor.chain_endpoint = "ws://127.0.0.1:9944" - if config.debug: - tplr.debug() - if config.trace: - tplr.trace() - return config - - def __init__(self): - # Init config - self.config = DecayAgent.config() - tplr.logger.info("\n" + "-" * 40 + " Config " + "-" * 40) - tplr.logger.info(self.config) - - # Init bittensor objects - self.wallet = bt.wallet(config=self.config) - self.subtensor = bt.subtensor(config=self.config) - self.metagraph = self.subtensor.metagraph(netuid=self.config.netuid) - self.chain_manager = tplr.chain.ChainManager( - subtensor=self.subtensor, wallet=self.wallet, netuid=self.config.netuid - ) - if self.wallet.hotkey.ss58_address not in self.metagraph.hotkeys: - tplr.logger.error( - f"\n\t[bold]The wallet {self.wallet} is not registered on subnet: {self.metagraph.netuid}[/bold]. You need to register first with: [blue]`btcli subnet register`[/blue]\n" - ) - sys.exit() - self.uid = self.metagraph.hotkeys.index(self.wallet.hotkey.ss58_address) - tplr.logger.info("\n" + "-" * 40 + " Objects " + "-" * 40) - tplr.logger.info( - f"\nWallet: {self.wallet}\nSubtensor: {self.subtensor}\nMetagraph: {self.metagraph}\nUID: {self.uid}" - ) - - # Set up paths - self.checkpoint_path = os.path.join( - "checkpoints", - "decay", - f"neuron_checkpoint_{self.wallet.hotkey.ss58_address}", - ) - os.makedirs(os.path.dirname(self.checkpoint_path), exist_ok=True) - - # Initialize wandb - wandb_dir = os.path.join(os.getcwd(), "wandb") - os.makedirs(wandb_dir, exist_ok=True) - run_id_file = os.path.join( - wandb_dir, f"wandb_run_id_D{self.uid}_{tplr.__version__}.txt" - ) - - if os.path.exists(run_id_file): - with open(run_id_file, "r") as f: - run_id = f.read().strip() - tplr.logger.info(f"Resuming WandB run with id {run_id}") - else: - run_id = None - tplr.logger.info("Starting new WandB run") - - self.wandb = tplr.initialize_wandb( - run_prefix="D", - uid=self.uid, - config=self.config, - group="decay", - job_type="decay_training", - ) - - # Load model and configuration - self.hparams = tplr.load_hparams() - torch.manual_seed(42) - np.random.seed(42) - random.seed(42) - - self.model = LlamaForCausalLM(config=self.hparams.model_config) - self.model.to(self.config.device) - - # Initialize optimizer with decay schedule - self.optimizer = optim.AdamW( - self.model.parameters(), - lr=self.hparams.learning_rate, - betas=(self.hparams.optimizer_beta1, self.hparams.optimizer_beta2), - weight_decay=self.hparams.optimizer_weight_decay, - foreach=True, - ) - - # Initialize decay scheduler - self.scheduler = tplr.get_wsd_scheduler( - optimizer=self.optimizer, - num_warmup_steps=0, # No warmup for decay - num_stable_steps=0, # No stable phase - num_decay_steps=self.hparams.num_decay_steps, - ) - - # Initialize checkpoint manager - self.checkpoint_manager = tplr.CheckpointManager( - model=self.model, - checkpoint_path=self.checkpoint_path, - wallet=self.wallet, - device=self.config.device, - optimizer=self.optimizer, - scheduler=self.scheduler, - ) - - # Load initial checkpoint with optimizer state - self.global_step = asyncio.run( - self.checkpoint_manager.load_from_highest_stake( - metagraph=self.metagraph, - buckets=self.buckets, - optimizer=self.optimizer, - scheduler=self.scheduler, - is_validator=False, - hparams=self.hparams, - ) - ) - - # Get buckets for all neurons - self.buckets = tplr.get_all_buckets( - subtensor=self.subtensor, - netuid=self.config.netuid, - metagraph=self.metagraph, - ) - - # Initialize state - self.last_eval_step = 0 - self.last_block_number = 0 - self.global_step = 0 - - async def decay_and_evaluate(self) -> Optional[int]: - """Performs decay training and evaluation""" - try: - if self.global_step == 0: - tplr.logger.error("Failed to load checkpoint from highest stake neuron") - return None - - # Start decay training - tplr.logger.info(f"Starting decay training from step {self.global_step}") - - for step in range(self.hparams.num_decay_steps): - # Download training data - pages = await tplr.dataset.DatasetLoader.next_pages( - offset=self.block_number, n_pages=1, seed=self.uid - ) - - dataset = await tplr.dataset.DatasetLoader.create( - batch_size=self.config.actual_batch_size, - sequence_length=self.hparams.sequence_length, - pages_info=pages, - tokenizer=self.hparams.tokenizer, - ) - - # Training step - self.model.train() - total_loss = 0.0 - - for batch in dataset: - input_ids = torch.tensor(batch, dtype=torch.long).to( - self.model.device - ) - labels = input_ids.clone() - labels = torch.where( - labels == self.hparams.tokenizer.pad_token_id, -100, labels - ) - - with torch.amp.autocast( - device_type=self.model.device.type, dtype=torch.bfloat16 - ): - outputs = self.model(input_ids=input_ids, labels=labels) - - total_loss += outputs.loss.item() - outputs.loss.backward() - - if self.hparams.grad_clip: - torch.nn.utils.clip_grad_norm_( - self.model.parameters(), self.hparams.grad_clip - ) - - self.optimizer.step() - self.scheduler.step() - self.optimizer.zero_grad() - - current_lr = self.scheduler.get_last_lr()[0] - step_loss = total_loss / len(dataset) - - # Log training metrics - self.wandb.log( - { - "decay/loss": step_loss, - "decay/learning_rate": current_lr, - "decay/progress": step / self.hparams.num_decay_steps, - "global_step": self.global_step + step, - } - ) - - # Run evaluation periodically - if (step + 1) % self.config.eval_interval == 0: - await self.evaluate(self.global_step + step) - - tplr.logger.info( - f"Decay step {step}/{self.hparams.num_decay_steps}, Loss: {step_loss:.4f}, LR: {current_lr:.2e}" - ) - - tplr.logger.info("Decay training completed") - return self.global_step + self.hparams.num_decay_steps - - except Exception as e: - tplr.logger.error(f"Error during decay training: {str(e)}") - return None - - async def evaluate(self, global_step: int) -> None: - """Runs evaluation on the current model state""" - try: - # Save model and tokenizer for evaluation - model_path = "models/eval" - os.makedirs(model_path, exist_ok=True) - self.model.save_pretrained(model_path) - self.hparams.tokenizer.save_pretrained(model_path) - - # Create results directory - results_dir = f"{model_path}/results" - os.makedirs(results_dir, exist_ok=True) - - # Run evaluation - lm_eval_command = ( - f"lm-eval " - f"--model hf " - f"--model_args pretrained=models/eval,tokenizer=models/eval " - f"--tasks arc_challenge,arc_easy,hellaswag,openbookqa,piqa,winogrande " - f"--device {self.config.device} " - f"--batch_size 6 " - f"--output_path {results_dir}" - ) - - process = await asyncio.create_subprocess_shell( - lm_eval_command, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - - stdout, stderr = await process.communicate() - - if process.returncode != 0: - tplr.logger.error(f"Evaluation failed: {stderr.decode()}") - return - - # Process and log results - eval_results_dir = os.path.join(results_dir, "models__eval") - if not os.path.exists(eval_results_dir): - tplr.logger.error(f"Results directory not found: {eval_results_dir}") - return - - latest_file = max( - [ - os.path.join(eval_results_dir, f) - for f in os.listdir(eval_results_dir) - ], - key=os.path.getctime, - ) - - with open(latest_file, "r") as f: - results = json.load(f) - - # Log results to wandb - for task_name, task_results in results["results"].items(): - metric_name = ( - "acc_norm,none" if task_name != "winogrande" else "acc,none" - ) - if metric_value := task_results.get(metric_name): - tplr.logger.info(f"{task_name}: {metric_value}") - self.wandb.log( - { - f"eval/{task_name}": metric_value, - }, - step=global_step, - ) - - # Cleanup - shutil.rmtree(model_path) - torch.cuda.empty_cache() - - except Exception as e: - tplr.logger.error(f"Error during evaluation: {str(e)}") - - -async def main(): - decay_agent = DecayAgent() - await decay_agent.decay_and_evaluate() - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/scripts/eval.py b/scripts/eval.py index b3cc9b0..93e405a 100644 --- a/scripts/eval.py +++ b/scripts/eval.py @@ -1,11 +1,26 @@ # The MIT License (MIT) # © 2024 templar.tech +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# fmt: off + import os import json -import asyncio import shutil import torch +import asyncio import argparse import bittensor as bt from transformers import LlamaForCausalLM diff --git a/scripts/release_notes.rs b/scripts/release_notes.rs deleted file mode 100755 index 73cbde4..0000000 --- a/scripts/release_notes.rs +++ /dev/null @@ -1,173 +0,0 @@ -#!/usr/bin/env rust-script -// ^ `cargo install rust-script` to be able to run this script - -use core::{fmt::Display, str::FromStr}; -use std::{env, process::Command}; - -fn eval(cmd: impl Display, print: bool) -> Result { - if print { - println!("$ {}", cmd); - } - let output = Command::new("sh") - .arg("-c") - .arg(cmd.to_string()) - .output() - .expect("failed to execute process"); - if print { - println!("{}", String::from_utf8(output.stdout.clone()).unwrap()); - eprintln!("{}", String::from_utf8(output.stderr.clone()).unwrap()); - } - if !output.status.success() { - return Err(String::from_utf8(output.stderr).unwrap()); - } - Ok(String::from_utf8(output.stdout).unwrap().trim().to_string()) -} - -#[derive(Copy, Clone, PartialEq, Eq, Debug)] -enum Network { - Mainnet, - Testnet, -} - -impl FromStr for Network { - type Err = (); - - fn from_str(s: &str) -> Result { - match s { - "mainnet" => Ok(Network::Mainnet), - "testnet" => Ok(Network::Testnet), - _ => Err(()), - } - } -} - -fn main() { - let network = env::var("NETWORK") - .unwrap_or_else(|_| "mainnet".to_string()) - .parse::() - .unwrap_or_else(|_| panic!("Invalid NETWORK value")); - println!("Network: {:?}", network); - - let all_tags = env::var("PREVIOUS_TAG") - .unwrap_or_else(|_| eval("git tag --sort=-creatordate", false).unwrap()) - .split("\n") - .map(|s| s.trim().to_string()) - .collect::>(); - - let previous_tag = match network { - Network::Mainnet => all_tags - .iter() - .find(|tag| tag.starts_with("v") && !tag.ends_with("-pre-release")) - .expect("could not find a valid mainnet tag!"), - Network::Testnet => all_tags - .iter() - .find(|tag| tag.starts_with("v") && tag.ends_with("-pre-release")) - .expect("could not find a valid testnet tag!"), - }; - println!("Previous release tag: {}", previous_tag); - - let branch = env::var("BRANCH").unwrap_or( - match network { - Network::Mainnet => "testnet", - Network::Testnet => "devnet", - } - .to_string(), - ); - println!("Branch: {}", branch); - - println!( - "Generating release notes for all merges since {}...", - previous_tag, - ); - let merges = eval( - format!( - "git log --merges --pretty=format:'%s' {}..{}", - branch, previous_tag, - ), - false, - ) - .unwrap() - .split("\n") - .map(|s| s.trim().to_string()) - .filter(|s| { - !s.is_empty() - && s.starts_with("Merge pull request #") - && !s.ends_with("from opentensor/devnet-ready") - && !s.ends_with("from opentensor/testnet-ready") - && !s.ends_with("from opentensor/devnet") - && !s.ends_with("from opentensor/testnet") - }) - .collect::>(); - - println!(""); - println!("Filtered merges:\n{}", merges.join("\n")); - - println!(""); - let pr_numbers = merges - .iter() - .map(|s| s.split(" ").collect::>()[3].trim_start_matches("#")) - .collect::>(); - println!("PR numbers:\n{:?}", pr_numbers); - - println!(""); - println!("Fetching PR titles..."); - let pr_titles = pr_numbers - .iter() - .map(|pr_number| { - print!("#{}: ", pr_number); - let title = eval(format!("gh pr view {} --json title", pr_number), false) - .unwrap() - .trim() - .to_string(); - if !title.starts_with("{\"title\":\"") { - panic!("Malformed PR title: {}", title); - } - let title = title - .trim_start_matches("{\"title\":\"") - .trim_end_matches("\"}") - .trim() - .to_string(); - println!("{}", title); - title - }) - .collect::>(); - - println!(""); - println!("Fetching PR authors..."); - let pr_authors = pr_numbers - .iter() - .map(|pr_number| { - print!("#{}: ", pr_number); - let author = eval( - format!("gh pr view {} --json author | jq .author.login", pr_number), - false, - ) - .unwrap() - .trim() - .trim_start_matches("\"") - .trim_end_matches("\"") - .to_string(); - println!("{}", author); - author - }) - .collect::>(); - - println!(""); - println!("generated release notes:"); - let release_notes = "## What's Changed\n".to_string(); - let release_notes = release_notes - + &pr_numbers - .iter() - .zip(pr_titles.iter()) - .zip(pr_authors.iter()) - .map(|((pr_number, pr_title), pr_author)| { - format!("- {} in #{} by @{}\n", pr_title, pr_number, pr_author) - }) - .collect::(); - println!("{}", release_notes); - - println!(""); - println!("writing release notes to /tmp/release_notes.md"); - std::fs::write("/tmp/release_notes.md", release_notes).unwrap(); - println!("done!"); -} diff --git a/scripts/run.sh b/scripts/run.sh deleted file mode 100755 index c916200..0000000 --- a/scripts/run.sh +++ /dev/null @@ -1,750 +0,0 @@ -#!/usr/bin/env bash - -# The MIT License (MIT) -# © 2024 templar.tech - -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the “Software”), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of -# the Software. - -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -set -euo pipefail - -# Initialize default values -DEBUG=false -PROJECT="templar" -AWS_ACCESS_KEY_ID="" -AWS_SECRET_ACCESS_KEY="" -BUCKET="" -NETWORK="" -NEURON_TYPE="" - -# Function to display help message -display_help() { - cat << EOF -Usage: $0 [options] - -Options: - --debug Enable debug mode - --project Set the project name (default: templar) - --aws-access-key-id Set AWS Access Key ID - --aws-secret-access-key Set AWS Secret Access Key - --bucket Set the S3 bucket name - --network Set the network (options: finney, test, local) - --neuron Set the neuron type (options: miner, validator) - -h, --help Display this help message - -Description: - Installs and runs a τemplar neuron on your GPU. If the --network option is not provided, you will be prompted to select a network. -EOF -} - -# Parse command-line arguments -while [[ $# -gt 0 ]]; do - key="$1" - case $key in - --debug) - DEBUG=true - shift - ;; - --project) - PROJECT="$2" - shift 2 - ;; - --aws-access-key-id) - AWS_ACCESS_KEY_ID="$2" - shift 2 - ;; - --aws-secret-access-key) - AWS_SECRET_ACCESS_KEY="$2" - shift 2 - ;; - --bucket) - BUCKET="$2" - shift 2 - ;; - --network) - NETWORK="$2" - shift 2 - ;; - --neuron) - NEURON_TYPE="$2" - shift 2 - ;; - -h|--help|-help|--h) - display_help - exit 0 - ;; - *) - # Only error if not a network argument - if [[ "$1" != "--network" ]]; then - echo "Unknown option: $1" - display_help - exit 1 - fi - shift - ;; - esac -done - - - -# Set up colors and styles for terminal output -if [[ -t 1 ]]; then - tty_escape() { printf "\033[%sm" "$1"; } -else - tty_escape() { :; } -fi -tty_mkbold() { tty_escape "1;$1"; } -tty_blue="$(tty_mkbold 34)" -tty_red="$(tty_mkbold 31)" -tty_green="$(tty_mkbold 32)" -tty_yellow="$(tty_mkbold 33)" -tty_bold="$(tty_mkbold 39)" -tty_reset="$(tty_escape 0)" - -# Logging functions for standardized output -ohai() { - printf "${tty_blue}==>${tty_bold} %s${tty_reset}\n" "$*" -} - -pdone() { - printf " ${tty_green}[✔]${tty_bold} %s${tty_reset}\n" "$*" -} - -info() { - printf "${tty_green}%s${tty_reset}\n" "$*" -} - -warn() { - printf "${tty_yellow}Warning${tty_reset}: %s\n" "$*" >&2 -} - -error() { - printf "${tty_red}Error${tty_reset}: %s\n" "$*" >&2 -} - -abort() { - error "$@" - exit 1 -} - -trap 'abort "An unexpected error occurred."' ERR - -# Function to get a single character input from the user -getc() { - local save_state - save_state="$(/bin/stty -g)" - /bin/stty raw -echo - IFS='' read -r -n 1 -d '' "$@" - /bin/stty "${save_state}" -} - -# Function to pause execution and wait for user confirmation -wait_for_user() { - local c - echo - echo -e "${tty_bold}Press ${tty_green}RETURN/ENTER${tty_reset} ${tty_bold}to continue or any other key to abort:${tty_reset}" - getc c - if ! [[ "${c}" == $'\r' || "${c}" == $'\n' ]] - then - exit 1 - fi -} - -# Function to execute a command with logging -execute() { - ohai "Running: $*" - if ! "$@"; then - abort "Failed during: $*" - fi -} - -# Function to check for sudo access -have_sudo_access() { - if ! command -v sudo &> /dev/null; then - warn "sudo command not found. Please install sudo or run as root." - return 1 - fi - if [ "$EUID" -ne 0 ]; then - if ! sudo -n true 2>/dev/null; then - warn "This script requires sudo access to install packages. Please run as root or ensure your user has sudo privileges." - return 1 - fi - fi - return 0 -} - -# Function to execute commands with sudo if necessary -execute_sudo() { - if have_sudo_access; then - ohai "sudo $*" - if ! sudo "$@"; then - abort "Failed to execute: sudo $*" - fi - else - warn "Sudo access is required, attempting to run without sudo" - ohai "$*" - if ! "$@"; then - abort "Failed to execute: $*" - fi - fi -} - -# Function to set or replace environment variables in bash_profile -set_or_replace_env_var() { - local var_name="$1" - local var_value="$2" - local profile_file="$3" - - # Escape special characters for sed - local escaped_var_value=$(printf '%s\n' "$var_value" | sed -e 's/[\/&]/\\&/g') - - if grep -q "^export $var_name=" "$profile_file"; then - # Variable exists, replace it - sed -i.bak "s/^export $var_name=.*/export $var_name=\"$escaped_var_value\"/" "$profile_file" - else - # Variable does not exist, append it - echo "export $var_name=\"$var_value\"" >> "$profile_file" - fi -} - -# Clear screen -clear - -# Display the logo -printf '%s\n' "___ _ _ _ _ | _ _ " -printf '%s\n' " | (/_| | ||_)|(_|| " -printf '%s\n' " | | " -echo "" - -echo -e "\nWelcome to the τemplar Installation Script\n" - -echo -e "This script will:\n" -echo -e "1. Install required software (Git, npm, pm2, rust, uv, Python 3.12)" -echo -e "2. Set up AWS credentials" -echo -e "3. Clone and set up the τemplar repository" -echo -e "4. Create and register Bittensor wallets" -echo -e "5. Configure wandb for logging" -echo -e "6. Clean the specified S3 bucket" -echo -e "7. Start τemplar neurons on available GPUs on your chosen network\n" - -echo -e "⚠️ Please ensure you have:" -echo -e " ✓ A stable internet connection" -echo -e " ✓ Sufficient permissions to install software\n" - - - -wait_for_user - -# If network not provided, prompt user to select one -if [[ -z "$NETWORK" ]]; then - echo "Please select a network:" - echo "1) finney" - echo "2) testnet" - echo "3) local" - read -p "Enter selection [1-3]: " network_choice - - case $network_choice in - 1) NETWORK="finney" ;; - 2) NETWORK="testnet" ;; - 3) NETWORK="local" ;; - *) - echo "Invalid selection" - exit 1 - ;; - esac -fi - -# Set network-specific variables based on the selected network -case "$NETWORK" in - finney|FINNEY|Finney) - SUBTENSOR_NETWORK="finney" - NETUID=3 - SUBTENSOR_CHAIN_ENDPOINT="" - PM2_NETWORK_OPTIONS="" - ;; - test|testnet|TEST|TESTNET|Testnet) - SUBTENSOR_NETWORK="test" - NETUID=223 - SUBTENSOR_CHAIN_ENDPOINT="wss://test.finney.opentensor.ai:443/" - PM2_NETWORK_OPTIONS="--test" - ;; - local|LOCAL|Local) - SUBTENSOR_NETWORK="local" - NETUID=3 - SUBTENSOR_CHAIN_ENDPOINT="wss://localhost:9944" - PM2_NETWORK_OPTIONS="" - ;; - *) - echo "Unknown network: $NETWORK" - display_help - exit 1 - ;; -esac - - -if [[ -z "$NEURON_TYPE" ]]; then - echo "Please select a neuron type:" - echo "1) miner" - echo "2) validator" - read -p "Enter selection [1-2]: " neuron_choice - - case $neuron_choice in - 1) NEURON_TYPE="miner" ;; - 2) NEURON_TYPE="validator" ;; - *) - echo "Invalid selection" - exit 1 - ;; - esac -fi - -# Validate neuron type -case "$NEURON_TYPE" in - miner|validator) - ;; - *) - echo "Invalid neuron type: $NEURON_TYPE" - display_help - exit 1 - ;; -esac - -# Ensure ~/.bash_profile exists -touch ~/.bash_profile -source ~/.bash_profile - -# Backup the bash_profile -cp ~/.bash_profile ~/.bash_profile.bak - -# Prompt the user for AWS credentials if not supplied via command-line -ohai "Getting AWS credentials ..." -if [[ -z "$AWS_ACCESS_KEY_ID" ]] || [[ -z "$AWS_SECRET_ACCESS_KEY" ]] || [[ -z "$BUCKET" ]]; then - warn "This script will store your AWS credentials in your ~/.bash_profile file." - warn "This is not secure and is not recommended." - read -p "Do you want to proceed? [y/N]: " proceed - if [[ "$proceed" != "y" && "$proceed" != "Y" ]]; then - abort "Aborted by user." - fi - - if [[ -z "$AWS_ACCESS_KEY_ID" ]]; then - read -p "Enter your AWS Access Key ID: " AWS_ACCESS_KEY_ID - fi - if [[ -z "$AWS_SECRET_ACCESS_KEY" ]]; then - read -p "Enter your AWS Secret Access Key: " AWS_SECRET_ACCESS_KEY - fi - if [[ -z "$BUCKET" ]]; then - read -p "Enter your S3 Bucket Name: " BUCKET - fi -fi - -# Overwrite or add the AWS credentials in the bash_profile -set_or_replace_env_var "AWS_ACCESS_KEY_ID" "$AWS_ACCESS_KEY_ID" ~/.bash_profile -set_or_replace_env_var "AWS_SECRET_ACCESS_KEY" "$AWS_SECRET_ACCESS_KEY" ~/.bash_profile -set_or_replace_env_var "BUCKET" "$BUCKET" ~/.bash_profile - -# Source the bash_profile to apply the changes -source ~/.bash_profile -pdone "AWS credentials set in ~/.bash_profile" - -ohai "Installing requirements ..." -# Install Git if not present -if ! command -v git &> /dev/null; then - ohai "Git not found. Installing git ..." - if [[ "$OSTYPE" == "linux-gnu"* ]]; then - ohai "Detected Linux" - if [ -f /etc/os-release ]; then - . /etc/os-release - if [[ "$ID" == "ubuntu" || "$ID_LIKE" == *"ubuntu"* ]]; then - ohai "Detected Ubuntu, installing Git..." - if [[ "$DEBUG" == "true" ]]; then - execute_sudo apt-get update -y - execute_sudo apt-get install git -y - else - execute_sudo apt-get update -y > /dev/null 2>&1 - execute_sudo apt-get install git -y > /dev/null 2>&1 - fi - else - warn "Unsupported Linux distribution: $ID" - abort "Cannot install Git automatically" - fi - else - warn "Cannot detect Linux distribution" - abort "Cannot install Git automatically" - fi - else - abort "Unsupported OS type: $OSTYPE" - fi -else - pdone "Git is already installed" -fi - -# Check for Rust installation -if ! command -v rustc &> /dev/null; then - ohai "Installing Rust ..." - if [[ "$DEBUG" == "true" ]]; then - execute curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y - else - execute curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y > /dev/null 2>&1 - fi - # Add Rust to the PATH for the current session - source $HOME/.cargo/env -fi -pdone "Rust is installed" - -# Install uv if not present -if ! command -v uv &> /dev/null; then - ohai "Installing uv ..." - if [[ "$DEBUG" == "true" ]]; then - execute curl -LsSf https://astral.sh/uv/install.sh | sh - else - execute curl -LsSf https://astral.sh/uv/install.sh | sh > /dev/null 2>&1 - fi - # Add uv to the PATH for the current session - export PATH="$HOME/.cargo/bin:$PATH" -fi -pdone "uv is installed" - -# Check if npm is installed -if ! command -v npm &> /dev/null; then - ohai "Installing npm ..." - if ! command -v node &> /dev/null; then - ohai "Node.js could not be found, installing..." - if ! curl -fsSL https://deb.nodesource.com/setup_18.x | bash; then - abort "Failed to download Node.js setup script" - fi - if ! execute_sudo apt-get install -y nodejs; then - abort "Failed to install Node.js" - fi - fi - if ! curl -L https://www.npmjs.com/install.sh | sh; then - abort "Failed to install npm" - fi -fi -pdone "npm is installed" - -# Install pm2 -if ! command -v pm2 &> /dev/null; then - ohai "Installing pm2 ..." - if [[ "$DEBUG" == "true" ]]; then - execute npm install pm2 -g - else - execute npm install pm2 -g > /dev/null 2>&1 - fi -fi -pdone "pm2 is installed" - -ohai "Installing τemplar ..." -# Check if we are inside the τemplar repository -if git rev-parse --is-inside-work-tree > /dev/null 2>&1; then - REPO_PATH="." -else - if [ ! -d "τemplar" ]; then - ohai "Cloning τemplar ..." - execute git clone https://github.com/RaoFoundation/templar - REPO_PATH="templar" - else - REPO_PATH="templar" - fi -fi -pdone "τemplar repository is ready at $REPO_PATH" - -# Install Python 3.12 if not installed -if ! command -v python3.12 &> /dev/null; then - ohai "Installing python3.12 ..." - if [[ "$OSTYPE" == "linux-gnu"* ]]; then - ohai "Detected Linux" - if [ -f /etc/os-release ]; then - . /etc/os-release - if [[ "$ID" == "ubuntu" || "$ID_LIKE" == *"ubuntu"* ]]; then - ohai "Detected Ubuntu, installing Python 3.12..." - if [[ "$DEBUG" == "true" ]]; then - if have_sudo_access; then - execute_sudo add-apt-repository ppa:deadsnakes/ppa -y - else - warn "Skipping add-apt-repository due to lack of sudo access" - fi - execute_sudo apt-get update -y - else - if have_sudo_access; then - execute_sudo add-apt-repository ppa:deadsnakes/ppa -y > /dev/null 2>&1 - else - warn "Skipping add-apt-repository due to lack of sudo access" - fi - execute_sudo apt-get update -y > /dev/null 2>&1 - execute_sudo apt-get install --reinstall python3-apt > /dev/null 2>&1 - execute_sudo apt-get install python3.12 -y > /dev/null 2>&1 - execute_sudo apt-get install python3.12-venv > /dev/null 2>&1 - fi - else - warn "Unsupported Linux distribution: $ID" - abort "Cannot install Python 3.12 automatically" - fi - else - warn "Cannot detect Linux distribution" - abort "Cannot install Python 3.12 automatically" - fi - else - abort "Unsupported OS type: $OSTYPE" - fi -fi -pdone "Python 3.12 is installed" - -# Create a virtual environment if it does not exist -if [ ! -d "$REPO_PATH/venv" ]; then - ohai "Creating virtual environment at $REPO_PATH..." - if [[ "$DEBUG" == "true" ]]; then - execute uv venv "$REPO_PATH/.venv" - else - execute uv venv "$REPO_PATH/.venv" > /dev/null 2>&1 - fi -fi -pdone "Virtual environment is set up at $REPO_PATH" - - -# Activate the virtual environment -ohai "Activating virtual environment ..." -source $REPO_PATH/.venv/bin/activate -pdone "Virtual environment activated" - -ohai "Installing Python requirements ..." -cd "$REPO_PATH" - -# First, ensure uv is properly set up -if [[ "$DEBUG" == "true" ]]; then - execute uv pip install --upgrade pip -else - execute uv pip install --upgrade pip > /dev/null 2>&1 -fi - -# Install PyTorch first -if [[ "$DEBUG" == "true" ]]; then - execute uv pip install torch --index-url https://download.pytorch.org/whl/cu118 -else - execute uv pip install torch --index-url https://download.pytorch.org/whl/cu118 > /dev/null 2>&1 -fi - -# Now run uv sync -if [[ "$DEBUG" == "true" ]]; then - # remove prerelease once bt decode is released - execute uv sync --extra all --prerelease=allow -else - execute uv sync --extra all --prerelease=allow > /dev/null 2>&1 -fi - -# Install flash-attn separately due to its special requirements -if [[ "$DEBUG" == "true" ]]; then - execute uv pip install flash-attn --no-build-isolation -else - execute uv pip install flash-attn --no-build-isolation > /dev/null 2>&1 -fi - -pdone "Python requirements installed" - -# Check for GPUs -ohai "Checking for GPUs..." -if ! command -v nvidia-smi &> /dev/null; then - warn "nvidia-smi command not found. Please ensure NVIDIA drivers are installed." - NUM_GPUS=0 -else - NUM_GPUS=$(nvidia-smi --query-gpu=name --format=csv,noheader | wc -l) - - if [ "$NUM_GPUS" -gt 0 ]; then - nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits | while read -r memory; do - pdone "Found GPU with $((memory / 1024)) GB of memory" - done - else - warn "No GPUs found on this machine." - fi -fi - -# Check system RAM -if command -v free &> /dev/null; then - TOTAL_RAM=$(free -g | awk '/^Mem:/{print $2}') - pdone "System RAM: ${TOTAL_RAM} GB" -else - warn "Cannot determine system RAM. 'free' command not found." -fi - -ohai "Creating wallets ..." - -# Create coldkey if it doesn't exist -exists_on_device=$(python3 -c "import bittensor as bt; w = bt.wallet(); print(w.coldkey_file.exists_on_device())" 2>/dev/null) -if [ "$exists_on_device" != "True" ]; then - if [[ "$DEBUG" == "true" ]]; then - echo "n" | btcli wallet new_coldkey --wallet.name default --n-words 12 - else - echo "n" | btcli wallet new_coldkey --wallet.name default --n-words 12 > /dev/null 2>&1 - fi -fi -pdone "Wallet 'default' is ready" - -# Create hotkeys based on neuron type -if [ "$NEURON_TYPE" = "validator" ]; then - # Create single hotkey for validator - HOTKEY_NAME="validator" - - # Check if hotkey exists - exists_on_device=$(python3 -c "import bittensor as bt; w = bt.wallet(hotkey='$HOTKEY_NAME'); print(w.hotkey_file.exists_on_device())" 2>/dev/null) - - if [ "$exists_on_device" != "True" ]; then - if [[ "$DEBUG" == "true" ]]; then - echo "n" | btcli wallet new_hotkey --wallet.name default --wallet.hotkey "$HOTKEY_NAME" --n-words 12 - else - echo "n" | btcli wallet new_hotkey --wallet.name default --wallet.hotkey "$HOTKEY_NAME" --n-words 12 > /dev/null 2>&1 - fi - pdone "Created Validator Hotkey '$HOTKEY_NAME'" - fi - - # Check registration status - is_registered=$(python3 -c "import bittensor as bt; w = bt.wallet(hotkey='$HOTKEY_NAME'); sub = bt.subtensor('$SUBTENSOR_NETWORK'); print(sub.is_hotkey_registered_on_subnet(hotkey_ss58=w.hotkey.ss58_address, netuid=$NETUID))") - - if [[ "$is_registered" != *"True"* ]]; then - ohai "Registering validator hotkey on netuid $NETUID" - if [[ "$DEBUG" == "true" ]]; then - btcli subnet pow_register --wallet.name default --wallet.hotkey "$HOTKEY_NAME" --netuid "$NETUID" --subtensor.network "$SUBTENSOR_NETWORK" --no_prompt - else - btcli subnet pow_register --wallet.name default --wallet.hotkey "$HOTKEY_NAME" --netuid "$NETUID" --subtensor.network "$SUBTENSOR_NETWORK" --no_prompt > /dev/null 2>&1 - fi - pdone "Registered Validator Hotkey on netuid $NETUID" - else - pdone "Validator Hotkey already registered on netuid $NETUID" - fi -else - # Create miner hotkeys - if [ "$NUM_GPUS" -gt 0 ]; then - for i in $(seq 0 $((NUM_GPUS - 1))); do - HOTKEY_NAME="C$i" - - # Check if hotkey exists - exists_on_device=$(python3 -c "import bittensor as bt; w = bt.wallet(hotkey='$HOTKEY_NAME'); print(w.hotkey_file.exists_on_device())" 2>/dev/null) - - if [ "$exists_on_device" != "True" ]; then - if [[ "$DEBUG" == "true" ]]; then - echo "n" | btcli wallet new_hotkey --wallet.name default --wallet.hotkey "$HOTKEY_NAME" --n-words 12 - else - echo "n" | btcli wallet new_hotkey --wallet.name default --wallet.hotkey "$HOTKEY_NAME" --n-words 12 > /dev/null 2>&1 - fi - pdone "Created Miner Hotkey '$HOTKEY_NAME'" - fi - - # Check registration status - is_registered=$(python3 -c "import bittensor as bt; w = bt.wallet(hotkey='$HOTKEY_NAME'); sub = bt.subtensor('$SUBTENSOR_NETWORK'); print(sub.is_hotkey_registered_on_subnet(hotkey_ss58=w.hotkey.ss58_address, netuid=$NETUID))") - - if [[ "$is_registered" != *"True"* ]]; then - ohai "Registering miner hotkey $HOTKEY_NAME on netuid $NETUID" - if [[ "$DEBUG" == "true" ]]; then - btcli subnet pow_register --wallet.name default --wallet.hotkey "$HOTKEY_NAME" --netuid "$NETUID" --subtensor.network "$SUBTENSOR_NETWORK" --no_prompt - else - btcli subnet pow_register --wallet.name default --wallet.hotkey "$HOTKEY_NAME" --netuid "$NETUID" --subtensor.network "$SUBTENSOR_NETWORK" --no_prompt > /dev/null 2>&1 - fi - pdone "Registered Miner Hotkey $HOTKEY_NAME on netuid $NETUID" - else - pdone "Miner Hotkey $HOTKEY_NAME already registered on netuid $NETUID" - fi - done - else - warn "No GPUs found. Exiting" - exit 1 - fi -fi -pdone "All hotkeys registered" - -# Initialize PM2 -ohai "Stopping old pm2 processes..." -if pm2 list | grep -q 'online'; then - pm2 delete all - pdone "Old processes stopped" -fi - -# Start neurons based on type -if [ "$NEURON_TYPE" = "validator" ] -then - ohai "Starting validator on network '$NETWORK' ..." - VALIDATOR_ARGS="--actual_batch_size 6 --wallet.name default --wallet.hotkey validator --bucket $BUCKET --use_wandb --project $PROJECT --netuid $NETUID --autoupdate --remote" - - # Add network options - [ -n "$PM2_NETWORK_OPTIONS" ] && VALIDATOR_ARGS="$VALIDATOR_ARGS $PM2_NETWORK_OPTIONS" - [ -n "$SUBTENSOR_NETWORK" ] && VALIDATOR_ARGS="$VALIDATOR_ARGS --subtensor.network $SUBTENSOR_NETWORK" - [ -n "$SUBTENSOR_CHAIN_ENDPOINT" ] && VALIDATOR_ARGS="$VALIDATOR_ARGS --subtensor.chain_endpoint $SUBTENSOR_CHAIN_ENDPOINT" - - # Start validator - if [ "$DEBUG" = "true" ] - then - execute pm2 start neurons/validator.py --interpreter python3 --name ${NETWORK}_validator -- $VALIDATOR_ARGS - else - execute pm2 start neurons/validator.py --interpreter python3 --name ${NETWORK}_validator -- $VALIDATOR_ARGS > /dev/null 2>&1 - fi - pdone "Validator started" - LOGGING_TARGET="${NETWORK}_validator" -fi - -if [ "$NEURON_TYPE" = "miner" ] -then - ohai "Starting miners on network '$NETWORK' ..." - if [ "$NUM_GPUS" -gt 0 ] - then - for i in $(seq 0 $((NUM_GPUS - 1))) - do - GPU_INDEX=$i - HOTKEY_NAME="C$i" - GPU_MEMORY=$(nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits | sed -n "$((i + 1))p") - - if [ -z "$GPU_MEMORY" ] - then - warn "Could not get GPU memory for GPU $i" - continue - fi - - # Set batch size - if [ "$GPU_MEMORY" -ge 80000 ] - then - BATCH_SIZE=6 - elif [ "$GPU_MEMORY" -ge 40000 ] - then - BATCH_SIZE=3 - else - BATCH_SIZE=1 - fi - - ohai "Starting miner on GPU $GPU_INDEX with batch size $BATCH_SIZE..." - MINER_ARGS="--actual_batch_size $BATCH_SIZE --wallet.name default --wallet.hotkey $HOTKEY_NAME --bucket $BUCKET --device cuda:$GPU_INDEX --use_wandb --project $PROJECT --netuid $NETUID --remote" - - # Add network options - [ -n "$PM2_NETWORK_OPTIONS" ] && MINER_ARGS="$MINER_ARGS $PM2_NETWORK_OPTIONS" - [ -n "$SUBTENSOR_NETWORK" ] && MINER_ARGS="$MINER_ARGS --subtensor.network $SUBTENSOR_NETWORK" - [ -n "$SUBTENSOR_CHAIN_ENDPOINT" ] && MINER_ARGS="$MINER_ARGS --subtensor.chain_endpoint $SUBTENSOR_CHAIN_ENDPOINT" - - # Start miner - if [ "$DEBUG" = "true" ] - then - execute pm2 start neurons/miner.py --interpreter python3 --name ${NETWORK}_$HOTKEY_NAME -- $MINER_ARGS - else - execute pm2 start neurons/miner.py --interpreter python3 --name ${NETWORK}_$HOTKEY_NAME -- $MINER_ARGS > /dev/null 2>&1 - fi - done - LOGGING_TARGET="${NETWORK}_C0" - else - warn "No GPUs found. Skipping miner startup." - fi - pdone "All miners started" -fi - -# Display status -pm2 list -echo "" -pdone "SUCCESS" -echo "" - -# Start logs -pm2 logs $LOGGING_TARGET \ No newline at end of file diff --git a/scripts/start.sh b/scripts/start.sh index 73ed692..592748c 100755 --- a/scripts/start.sh +++ b/scripts/start.sh @@ -1,35 +1,4 @@ -# The MIT License (MIT) -# © 2024 templar.tech - -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the “Software”), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of -# the Software. - -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -# Close down all previous processes and restart them. -pm2 sendSignal SIGINT all -pm2 delete all -# Delete items from bucket -BUCKET=${1:-cont2} -PROJECT=${2:-templar} -python3 tools/clean.py --bucket $BUCKET - -# Start all the processes again. -pm2 start neurons/validator.py --interpreter python3 --name V2 -- --actual_batch_size 1 --wallet.name Bistro --wallet.hotkey V1 --bucket $BUCKET --device cuda:0 --use_wandb --project $PROJECT --test --netuid 223 --autoupdate --process_name V2 -pm2 start neurons/miner.py --interpreter python3 --name M1 -- --actual_batch_size 1 --wallet.name Bistro --wallet.hotkey M111 --bucket $BUCKET --device cuda:1 --use_wandb --project $PROJECT --test --netuid 223 --autoupdate --process_name M1 -pm2 start neurons/miner.py --interpreter python3 --name M2 -- --actual_batch_size 1 --wallet.name Bistro --wallet.hotkey M222 --bucket $BUCKET --device cuda:2 --use_wandb --project $PROJECT --test --netuid 223 --autoupdate --process_name M2 -pm2 start neurons/miner.py --interpreter python3 --name M3 -- --actual_batch_size 1 --wallet.name Bistro --wallet.hotkey M333 --bucket $BUCKET --device cuda:3 --use_wandb --project $PROJECT --test --netuid 223 --autoupdate --process_name M3 -pm2 start neurons/miner.py --interpreter python3 --name M4 -- --actual_batch_size 1 --wallet.name Bistro --wallet.hotkey M444 --bucket $BUCKET --device cuda:4 --use_wandb --project $PROJECT --test --netuid 223 --autoupdate --process_name M4 -pm2 start neurons/miner.py --interpreter python3 --name M5 -- --actual_batch_size 1 --wallet.name Bistro --wallet.hotkey M555 --bucket $BUCKET --device cuda:5 --use_wandb --project $PROJECT --test --netuid 223 --autoupdate --process_name M5 -pm2 start neurons/miner.py --interpreter python3 --name M6 -- --actual_batch_size 1 --wallet.name Bistro --wallet.hotkey M666 --bucket $BUCKET --device cuda:6 --use_wandb --project $PROJECT --test --netuid 223 --autoupdate --process_name M6 - -pm2 start neurons/validator.py --interpreter python3 --name V1 -- --actual_batch_size 6 --wallet.name templar --wallet.hotkey templar_validator --device cuda:7 --use_wandb --netuid 3 --autoupdate --process_name V1 --sync +# pm2 delete all +pm2 start neurons/miner.py --interpreter python3 --name TM0 -- --wallet.name Bistro --wallet.hotkey M111 --device cuda:0 --subtensor.network test --use_wandb --debug --netuid 268 --project ${1:-default} +pm2 start neurons/miner.py --interpreter python3 --name TM1 -- --wallet.name Bistro --wallet.hotkey M222 --device cuda:1 --subtensor.network test --use_wandb --debug --netuid 268 --project ${1:-default} +pm2 start neurons/validator.py --interpreter python3 --name TV1 -- --wallet.name Bistro --wallet.hotkey V11 --device cuda:3 --subtensor.network test --use_wandb --debug --netuid 268 --project ${1:-default} diff --git a/src/tplr/__init__.py b/src/tplr/__init__.py index 2ae7d44..c7c5803 100644 --- a/src/tplr/__init__.py +++ b/src/tplr/__init__.py @@ -2,14 +2,14 @@ # © 2024 templar.tech # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# documentation files (the "Software"), to deal in the Software without restriction, including without limitation # the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, # and to permit persons to whom the Software is furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in all copies or substantial portions of # the Software. -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO # THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER @@ -23,12 +23,11 @@ __version__ = "0.2.0" # Import package. -from .autoupdate import * from .chain import * from .comms import * from .compress import * from .dataset import * from .hparams import * from .logging import * +from .schemas import * from .wandb import * - diff --git a/src/tplr/autoupdate.py b/src/tplr/autoupdate.py deleted file mode 100644 index 8be6591..0000000 --- a/src/tplr/autoupdate.py +++ /dev/null @@ -1,347 +0,0 @@ -# The MIT License (MIT) -# © 2024 templar.tech - -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the “Software”), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of -# the Software. - -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. -# fmt: off - -# Global imports -import os -import git -import sys -import json -import time -import aiohttp -import asyncio -import threading -import subprocess -from packaging import version - -# Local imports -from .logging import logger -from .config import BUCKET_SECRETS -from .comms import delete_old_version_files - - -TARGET_BRANCH = "main" - - -class AutoUpdate(threading.Thread): - """ - Automatic update utility for templar neurons. - """ - - def __init__(self): - super().__init__() - self.daemon = True # Ensure thread exits when main program exits - - try: - self.repo = git.Repo(search_parent_directories=True) - except Exception as e: - logger.exception("Failed to initialize the repository", exc_info=e) - sys.exit(1) # Terminate the thread/application - self.start() - - async def get_remote_version(self): - """ - Asynchronously fetch the remote version string from a remote HTTP endpoint. - """ - try: - url = "https://raw.githubusercontent.com/tplr-ai/templar/main/src/templar/__init__.py" - async with aiohttp.ClientSession() as session: - async with session.get(url, timeout=5) as response: - response.raise_for_status() - content = await response.text() - - for line in content.split("\n"): - if line.startswith("__version__"): - version_info = line.split("=")[1].strip().strip(" \"'") - return version_info - - logger.error("Version string not found in remote __init__.py") - return None - - except Exception as e: - logger.exception( - "Failed to get remote version for version check", exc_info=e - ) - return None - - async def check_version_updated(self): - """ - Asynchronously compares local and remote versions and returns True if the remote version is higher. - """ - remote_version = await self.get_remote_version() - if not remote_version: - logger.error("Failed to get remote version, skipping version check") - return False - - local_version = self.get_local_version() - if not local_version: - logger.error("Failed to get local version, skipping version check") - return False - - local_version_obj = version.parse(local_version) - remote_version_obj = version.parse(remote_version) - logger.info( - f"Version check - remote_version: {remote_version}, local_version: {local_version}" - ) - - if remote_version_obj > local_version_obj: - logger.info( - f"Remote version ({remote_version}) is higher " - f"than local version ({local_version}), automatically updating..." - ) - return True - - return False - - def attempt_update(self): - """ - Attempts to update the local repository to match the remote. - """ - if self.repo.head.is_detached: - logger.error("Repository is in a detached HEAD state. Cannot update.") - return False - - if self.repo.is_dirty(untracked_files=True): - logger.error( - "Repository has uncommitted changes or untracked files. Cannot update." - ) - return False - - try: - origin = self.repo.remote(name="origin") - # Fetch latest changes from remote - origin.fetch() - # Get the current branch - current_branch = self.repo.active_branch - if current_branch.name != TARGET_BRANCH: - logger.error( - f"Current branch ({current_branch.name}) is not the target branch ({TARGET_BRANCH}). Cannot update." - ) - return False - - # Reset local branch to the remote branch - remote_ref = f"origin/{TARGET_BRANCH}" - logger.info( - f"Resetting local branch '{current_branch.name}' to '{remote_ref}'" - ) - self.repo.git.reset("--hard", remote_ref) - logger.info("Successfully reset to the latest commit from remote.") - - # Verify that local and remote commits match - local_commit = self.repo.commit(current_branch) - remote_commit = self.repo.commit(remote_ref) - if local_commit.hexsha != remote_commit.hexsha: - logger.error( - "Local commit does not match remote commit after reset. Rolling back." - ) - self.repo.git.reset("--hard", "HEAD@{1}") # Reset to previous HEAD - return False - - return True - except git.exc.GitCommandError as e: - logger.error(f"Git command failed: {e}") - # Rollback on failure - self.repo.git.reset("--hard", "HEAD@{1}") - return False - except Exception as e: - logger.exception("Failed to update repository.", exc_info=e) - return False - except git.exc.GitCommandError as e: - logger.error(f"Git command failed: {e}") - return False - except Exception as e: - logger.exception("Failed to update repository.", exc_info=e) - return False - - def handle_merge_conflicts(self): - """ - Attempt to automatically resolve any merge conflicts that may have arisen. - """ - try: - self.repo.git.reset("--merge") - origin = self.repo.remote(name="origin") - current_branch = self.repo.active_branch.name - origin.pull(current_branch) - - for item in self.repo.index.diff(None): - file_path = item.a_path - logger.info(f"Resolving conflict in file: {file_path}") - self.repo.git.checkout("--theirs", file_path) - self.repo.index.commit("Resolved merge conflicts automatically") - logger.info("Merge conflicts resolved, repository updated to remote state.") - logger.info("✅ Successfully updated") - return True - except git.GitCommandError as e: - logger.exception( - "Failed to resolve merge conflicts. Please manually pull and update.", - exc_info=e, - ) - return False - - def attempt_package_update(self): - """ - Synchronize dependencies using 'uv sync --extra all'. - """ - logger.info("Attempting to update packages using 'uv sync --extra all'...") - - try: - uv_executable = "uv" - # TODO: Allow specifying the path to 'uv' if it's not in PATH - - subprocess.check_call( - [uv_executable, "sync", "--extra", "all"], - timeout=300, - ) - logger.info("Successfully updated packages using 'uv sync --extra all'.") - except subprocess.CalledProcessError as e: - logger.exception("Failed to synchronize dependencies with uv", exc_info=e) - except FileNotFoundError: - logger.error( - "uv executable not found. Please ensure 'uv' is installed and in PATH." - ) - except Exception as e: - logger.exception( - "Unexpected error during package synchronization", exc_info=e - ) - - async def cleanup_old_versions(self): - """ - Cleans up old version slices from the S3 bucket. - """ - from templar import __version__ - - logger.info( - f"Cleaning up old versions from bucket {BUCKET_SECRETS['bucket_name']}" - ) - await delete_old_version_files(BUCKET_SECRETS["bucket_name"], __version__) - - def try_update(self): - """ - Automatic update entrypoint method. - """ - - if self.repo.head.is_detached or self.repo.active_branch.name != TARGET_BRANCH: - logger.info("Not on the target branch, skipping auto-update") - return - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - try: - logger.info("Checking for updates...") - # Check if remote version is newer - is_update_needed = loop.run_until_complete(self.check_version_updated()) - if not is_update_needed: - logger.info("Local version is up to date. No updates needed.") - return - - logger.info("Attempting auto update") - # Attempt to update code - update_applied = self.attempt_update() - if not update_applied: - logger.info("No updates were applied. Continuing without restart.") - return - - # Now read the local version - local_version = self.get_local_version() - logger.info(f"Local version after update: {local_version}") - - # Synchronize dependencies - self.attempt_package_update() - - # Clean up old versions from the bucket - loop.run_until_complete(self.cleanup_old_versions()) - - # Restart application - logger.info("Attempting to restart the application...") - self.restart_app() - except Exception as e: - logger.exception("Exception during autoupdate process", exc_info=e) - finally: - loop.close() - - def get_pm2_process_name(self): - """ - Attempt to find the current process's PM2 name by using `pm2 jlist` and matching the current PID. - """ - current_pid = os.getpid() - try: - result = subprocess.run( - ["pm2", "jlist"], check=True, capture_output=True, text=True - ) - pm2_data = json.loads(result.stdout) - except Exception as e: - logger.error(f"Error running `pm2 jlist`: {e}") - return None - for proc in pm2_data: - if proc.get("pid") == current_pid: - return proc.get("name") - - return None - - def restart_app(self): - """Restarts the current application appropriately based on the runtime environment.""" - logger.info("Restarting application...") - pm2_name = self.get_pm2_process_name() - if pm2_name: - logger.info( - f"Detected PM2 environment. Restarting PM2 process '{pm2_name}'..." - ) - try: - subprocess.run(["pm2", "restart", pm2_name], check=True) - logger.info(f"Successfully restarted PM2 process '{pm2_name}'.") - sys.exit(0) - except Exception as e: - logger.error(f"Failed to restart PM2 process '{pm2_name}': {e}") - sys.exit(1) - else: - try: - logger.info( - "PM2 process name not found. Performing regular restart using subprocess.Popen" - ) - subprocess.Popen([sys.executable] + sys.argv) - logger.info("New process started. Exiting current process.") - sys.exit(0) - except Exception as e: - logger.exception("Failed to restart application.", exc_info=e) - sys.exit(1) - - def run(self): - """Thread run method to periodically check for updates.""" - while True: - try: - logger.info("Running autoupdate") - self.try_update() - except Exception as e: - logger.exception("Exception during autoupdate check", exc_info=e) - time.sleep(60) - - def get_local_version(self): - """ - Reads the local __version__ from the __init__.py file. - """ - try: - init_py_path = os.path.join(os.path.dirname(__file__), "__init__.py") - with open(init_py_path, "r") as f: - content = f.read() - for line in content.split("\n"): - if line.startswith("__version__"): - local_version = line.split("=")[1].strip().strip(" \"'") - return local_version - logger.error("Could not find __version__ in local __init__.py") - return None - except Exception as e: - logger.exception("Failed to read local version", exc_info=e) - return None diff --git a/src/tplr/comms.py b/src/tplr/comms.py index e5e20f2..a9aed5c 100644 --- a/src/tplr/comms.py +++ b/src/tplr/comms.py @@ -1,47 +1,25 @@ -# The MIT License (MIT) -# © 2024 templar.tech - -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the "Software"), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of -# the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. -# fmt: off - -# Global imports import os -import re import time -import yaml import torch import asyncio import aiofiles +import tempfile import numpy as np import bittensor as bt from typing import List, Dict, Optional, Tuple +from types import SimpleNamespace from aiobotocore.session import get_session - -# Local imports from . import __version__ -from .schemas import Bucket -from .logging import logger -from .chain import ChainManager from .config import client_config, BUCKET_SECRETS +from .chain import ChainManager +from .schemas import Bucket -CF_REGION_NAME: str = "enam" - +import tplr as tplr +import botocore -def get_base_url(account_id): - """Constructs the base URL for the R2 storage endpoint.""" - return f"https://{account_id}.r2.cloudflarestorage.com" +# Constants +CF_REGION_NAME: str = "enam" +LOCAL_TMP_DIR = "/tmp/local_store" class Comms(ChainManager): @@ -49,19 +27,22 @@ def __init__( self, wallet: "bt.wallet", save_location: str = "/tmp", - key_prefix: str = "slice", - **kwargs + key_prefix: str = "model", + **kwargs, ): self.wallet = wallet + # Get the bucket directly self.bucket = self.get_own_bucket() + # Now initialize ChainManager with the bucket super().__init__( - config=kwargs.get('config'), - netuid=kwargs.get('netuid'), - metagraph=kwargs.get('metagraph'), - hparams=kwargs.get('hparams'), + config=kwargs.get("config"), + netuid=kwargs.get("netuid"), + metagraph=kwargs.get("metagraph"), + hparams=kwargs.get("hparams"), wallet=self.wallet, bucket=self.bucket, ) + # Use the hotkey directly in the save_location hotkey = self.wallet.hotkey.ss58_address self.save_location = os.path.join("/tmp", f"hotkey_{hotkey}") @@ -69,13 +50,10 @@ def __init__( self.key_prefix = key_prefix self.session = get_session() self.lock = asyncio.Lock() - # Load bucket secrets - self.bucket_secrets = BUCKET_SECRETS def get_own_bucket(self) -> Bucket: """Gets bucket configuration from environment variables via config.BUCKET_SECRETS.""" try: - # Create a Bucket object using write credentials from BUCKET_SECRETS bucket = Bucket( name=BUCKET_SECRETS["account_id"], @@ -83,303 +61,367 @@ def get_own_bucket(self) -> Bucket: access_key_id=BUCKET_SECRETS["write"]["access_key_id"], secret_access_key=BUCKET_SECRETS["write"]["secret_access_key"], ) - logger.debug(f"Created bucket from environment: {bucket}") + tplr.logger.debug(f"Created bucket from environment: {bucket}") return bucket except KeyError as e: - logger.error(f"Missing required R2 configuration: {e}") + tplr.logger.error(f"Missing required R2 configuration: {e}") raise except Exception as e: - logger.error(f"Error creating bucket: {e}") + tplr.logger.error(f"Error creating bucket: {e}") raise - async def put( - self, - state_dict_or_path, - uid: str, - window_or_block: int, - key: Optional[str] = None, + def get_base_url(self, account_id): + """Constructs the base URL for the R2 storage endpoint.""" + return f"https://{account_id}.r2.cloudflarestorage.com" + + def delete_local_directory(self, path: str): + """Safely remove a local directory and all its contents.""" + if not os.path.exists(path): + return + for root, dirs, files in os.walk(path, topdown=False): + for name in files: + os.remove(os.path.join(root, name)) + for name in dirs: + os.rmdir(os.path.join(root, name)) + os.rmdir(path) + + # Convert all the existing functions to methods + async def cleanup_local_data( + self, uid: str, current_window: int, stale_retention: int ): - """ - Uploads data to the R2 bucket. Handles both small state_dicts and large checkpoint files. + """Clean up stale local data for a given uid.""" + user_dir = os.path.join(LOCAL_TMP_DIR, str(uid)) + if not os.path.exists(user_dir): + return + + min_allowed_window = current_window - stale_retention + for wdir in os.listdir(user_dir): + if wdir.isdigit(): + w = int(wdir) + if w < min_allowed_window: + old_path = os.path.join(user_dir, wdir) + tplr.logger.debug(f"Removing stale local directory: {old_path}") + try: + self.delete_local_directory(old_path) + except Exception as e: + tplr.logger.debug( + f"Error removing stale directory {old_path}: {e}" + ) - Args: - state_dict_or_path (dict or str): The state dictionary to upload or the path to the checkpoint file. - uid (str): Unique identifier for the upload (e.g., hotkey or user ID). - window_or_block (int): The window number or block number. - key (str, optional): Custom key for the filename. Defaults to self.key_prefix. - """ - key = key or self.key_prefix - hotkey = self.wallet.hotkey.ss58_address + async def cleanup_s3_data( + self, uid: str, current_window: int, stale_retention: int + ): + """Clean up stale S3 data for a given uid.""" + min_allowed_window = current_window - stale_retention + prefix = f"{uid}/" + + session = get_session() + async with session.create_client( + "s3", + endpoint_url=self.get_base_url(BUCKET_SECRETS["account_id"]), + region_name=CF_REGION_NAME, + config=client_config, + aws_access_key_id=BUCKET_SECRETS["write"]["access_key_id"], + aws_secret_access_key=BUCKET_SECRETS["write"]["secret_access_key"], + ) as s3_client: + continuation_token = None + + while True: + list_args = { + "Bucket": BUCKET_SECRETS["bucket_name"], + "Prefix": prefix, + "MaxKeys": 1000, + } + if continuation_token: + list_args["ContinuationToken"] = continuation_token + + response = await s3_client.list_objects_v2(**list_args) + contents = response.get("Contents", []) + + # Identify stale objects to delete + stale_objects = [] + for obj in contents: + key = obj["Key"] + # Key format: uid/window/key + parts = key.split("/") + if len(parts) < 2: + continue + try: + w = int(parts[1]) + except ValueError: + continue - if isinstance(state_dict_or_path, dict): - # Handle state_dict upload - filename = f"{key}-{window_or_block}-{hotkey}-v{__version__}.pt" - temp_file_path = os.path.join(self.save_location, filename) + if w < min_allowed_window: + stale_objects.append({"Key": key}) + + # Batch delete stale objects + if stale_objects: + tplr.logger.debug( + f"Removing stale S3 objects for {uid}: {stale_objects}" + ) + await s3_client.delete_objects( + Bucket=BUCKET_SECRETS["bucket_name"], + Delete={"Objects": stale_objects}, + ) - # Ensure the save directory exists - os.makedirs(self.save_location, exist_ok=True) + if response.get("IsTruncated"): + continuation_token = response.get("NextContinuationToken") + else: + break + + async def s3_put_object(self, key: str, data: bytes): + """Upload object to S3.""" + session = get_session() + async with session.create_client( + "s3", + endpoint_url=self.get_base_url(BUCKET_SECRETS["account_id"]), + region_name=CF_REGION_NAME, + config=client_config, + aws_access_key_id=BUCKET_SECRETS["write"]["access_key_id"], + aws_secret_access_key=BUCKET_SECRETS["write"]["secret_access_key"], + ) as s3_client: + await s3_client.put_object( + Bucket=BUCKET_SECRETS["bucket_name"], Key=key, Body=data + ) + async def s3_get_object(self, key: str, timeout: int) -> Optional[dict]: + """Download object from S3.""" + session = get_session() + async with session.create_client( + "s3", + endpoint_url=self.get_base_url(BUCKET_SECRETS["account_id"]), + region_name=CF_REGION_NAME, + config=client_config, + aws_access_key_id=BUCKET_SECRETS["write"]["access_key_id"], + aws_secret_access_key=BUCKET_SECRETS["write"]["secret_access_key"], + ) as s3_client: try: - # Save the state_dict to a temporary file - torch.save(state_dict_or_path, temp_file_path) - logger.debug(f"Temporary file saved at {temp_file_path}") + # Check if file exists first + try: + await s3_client.head_object( + Bucket=BUCKET_SECRETS["bucket_name"], Key=key + ) + except ( + botocore.exceptions.ClientError, + botocore.exceptions.BotoCoreError, + ) as e: + tplr.logger.debug(f"Object not found or access denied: {e}") + return None + + response = await asyncio.wait_for( + s3_client.get_object(Bucket=BUCKET_SECRETS["bucket_name"], Key=key), + timeout=timeout, + ) + except asyncio.TimeoutError: + tplr.logger.debug(f"Timeout occurred while downloading {key}.") + return None except Exception as e: - logger.error(f"Error saving temporary file: {e}") - raise - - file_path = temp_file_path - elif isinstance(state_dict_or_path, str): - # Handle checkpoint file upload - file_path = state_dict_or_path - filename = os.path.basename(file_path) - else: - raise ValueError("state_dict_or_path must be a state_dict or a file path.") - - # Determine if multipart upload is needed based on file size - file_size = os.path.getsize(file_path) - use_multipart = file_size > 5 * 1024 * 1024 * 1024 # 5 GB threshold - - # Upload the file to R2 bucket - try: - async with self.session.create_client( - "s3", - endpoint_url=get_base_url(BUCKET_SECRETS["account_id"]), - region_name=CF_REGION_NAME, - config=client_config, - aws_access_key_id=BUCKET_SECRETS["write"]["access_key_id"], - aws_secret_access_key=BUCKET_SECRETS["write"]["secret_access_key"], - ) as s3_client: - if use_multipart: - await self._multipart_upload(s3_client, filename, file_path) - else: - async with aiofiles.open(file_path, "rb") as f: - data = await f.read() - await s3_client.put_object( - Bucket=self.bucket.name, Key=filename, Body=data - ) - logger.debug(f"Successfully uploaded {filename} to R2 bucket.") - except Exception as e: - logger.error(f"Failed to upload {filename} to R2 bucket: {e}") - raise - finally: - # Clean up the temporary file if it exists and was created - if isinstance(state_dict_or_path, dict): - logger.debug(f"Attempting to delete temporary file at {temp_file_path}") + tplr.logger.debug(f"An error occurred during GET {key}: {e}") + return None + + # Save to a temporary file and load + with tempfile.NamedTemporaryFile(delete=True, suffix=".pt") as temp_file: + temp_file_path = temp_file.name + async with aiofiles.open(temp_file_path, "wb") as outfile: + while True: + chunk = await response["Body"].read(1 * 1024 * 1024) + if not chunk: + break + await outfile.write(chunk) + + # Load the object try: - if os.path.exists(temp_file_path): - os.remove(temp_file_path) - logger.debug(f"Deleted temporary file at {temp_file_path}") - else: - logger.debug(f"Temporary file does not exist at {temp_file_path}") + with open(temp_file_path, "rb") as f: + state_dict = torch.load(f, weights_only=True) + return state_dict except Exception as e: - logger.error(f"Error during cleanup of temporary file: {e}") - - async def _multipart_upload(self, s3_client, filename, file_path): - """Handles multipart upload for large files.""" - bucket = BUCKET_SECRETS["bucket_name"].split("/")[-1] - chunk_size = 16 * 1024 * 1024 # 16MB chunks - max_concurrent_uploads = 10 - max_retries = 3 - retry_delay = 5 - - # Initialize multipart upload - response = await s3_client.create_multipart_upload( - Bucket=bucket, - Key=filename, - CacheControl="no-cache, no-store, must-revalidate", - ) - upload_id = response["UploadId"] - logger.info(f"Initiated multipart upload with ID: {upload_id}") + tplr.logger.debug(f"Error loading state_dict from {key}: {e}") + return None + + async def put( + self, + state_dict: dict, + uid: str, + window: int, + key: str, + local: bool = True, + stale_retention: int = 10, + ): + """PUT operation: Store the state_dict either locally or in R2.""" + tplr.logger.debug(f"PUT {uid}/{window}/{key} -->") + + # Create versioned filename + filename = f"{key}-{window}-{uid}-v{__version__}.pt" + + # Create a temporary file path + temp_file_path = tempfile.mktemp(suffix=".pt") try: - total_size = os.path.getsize(file_path) - total_parts = (total_size + chunk_size - 1) // chunk_size - parts = {} - semaphore = asyncio.Semaphore(max_concurrent_uploads) - upload_tasks = [] - - async def upload_part(part_number: int, offset: int): - """Upload a single part with retries.""" - for attempt in range(max_retries): - try: - async with semaphore: - async with aiofiles.open(file_path, "rb") as f: - await f.seek(offset) - chunk = await f.read(min(chunk_size, total_size - offset)) - - response = await s3_client.upload_part( - Bucket=bucket, - Key=filename, - PartNumber=part_number, - UploadId=upload_id, - Body=chunk, - ) - - return { - "PartNumber": part_number, - "ETag": response["ETag"], - } - except Exception as e: - if attempt < max_retries - 1: - logger.warning(f"Retry {attempt + 1}/{max_retries} for part {part_number}: {str(e)}") - await asyncio.sleep(retry_delay) - else: - raise - - # Create upload tasks for all parts - for part_number in range(1, total_parts + 1): - offset = (part_number - 1) * chunk_size - task = asyncio.create_task(upload_part(part_number, offset)) - upload_tasks.append(task) - - # Wait for all uploads and collect results - completed_parts = await asyncio.gather(*upload_tasks) - parts = [part for part in completed_parts if part is not None] - parts.sort(key=lambda x: x["PartNumber"]) - - # Complete multipart upload - await s3_client.complete_multipart_upload( - Bucket=bucket, - Key=filename, - UploadId=upload_id, - MultipartUpload={"Parts": parts}, - ) - logger.info(f"Successfully uploaded checkpoint {filename}") + # Save state_dict to the temporary file + torch.save(state_dict, temp_file_path) + + if local: + # Local storage logic remains unchanged + await self.cleanup_local_data( + uid=uid, current_window=window, stale_retention=stale_retention + ) + local_dir = os.path.join(LOCAL_TMP_DIR, str(uid), str(window)) + os.makedirs(local_dir, exist_ok=True) + final_path = os.path.join(local_dir, filename) + os.replace(temp_file_path, final_path) + else: + # Cleanup old S3 data + await self.cleanup_s3_data( + uid=uid, current_window=window, stale_retention=stale_retention + ) + + # Check file size + file_size = os.path.getsize(temp_file_path) + object_key = f"{uid}/{window}/{filename}" + + if file_size > 5 * 1024 * 1024 * 1024: # 5GB + # Use multipart upload for large files + success = await self.upload_large_file(temp_file_path, object_key) + if not success: + raise Exception("Large file upload failed") + else: + # Use regular upload for smaller files + async with aiofiles.open(temp_file_path, "rb") as f: + data = await f.read() + await self.s3_put_object(object_key, data) + + # Remove temporary file after successful upload + if os.path.exists(temp_file_path): + os.remove(temp_file_path) + except Exception as e: - logger.error(f"Error during multipart upload: {str(e)}") - await s3_client.abort_multipart_upload( - Bucket=bucket, Key=filename, UploadId=upload_id - ) - logger.info(f"Aborted multipart upload {upload_id}") - raise + tplr.logger.debug(f"PUT error {uid}/{window}/{key}: {e}") + if os.path.exists(temp_file_path): + os.remove(temp_file_path) + + tplr.logger.debug(f"PUT {uid}/{window}/{key} <--") async def get( self, uid: str, window: int, - key: Optional[str] = None, + key: str, timeout: int = 30, - local: bool = False - ) -> Optional[Dict[str, torch.Tensor]]: - """ - Downloads data from the R2 bucket. Handles both model state dicts and large checkpoint files. - """ - key = key or self.key_prefix - hotkey = self.get_hotkey(int(uid)) - if hotkey is None: - logger.error(f"No hotkey found for uid {uid}") - return None - - filename = f"{key}-{window}-{hotkey}-v{__version__}.pt" - temp_file_path = os.path.join(self.save_location, filename) - - bucket = self.get_bucket(int(uid)) - if bucket is None: - logger.debug(f"Bucket for uid {uid} not found. Skipping...") - return None + local: bool = True, + stale_retention: int = 10, + ) -> Optional[dict]: + """GET operation: Retrieve state_dict from local or R2 storage.""" + filename = f"{key}-{window}-{uid}-v{__version__}.pt" + full_key = f"{uid}/{window}/{filename}" + tplr.logger.debug(f"GET {full_key} -->") try: - async with self.session.create_client( - "s3", - endpoint_url=get_base_url(bucket.account_id), - region_name=CF_REGION_NAME, - config=client_config, - aws_access_key_id=bucket.access_key_id, - aws_secret_access_key=bucket.secret_access_key, - ) as s3_client: - # Check if file exists first - try: - # List objects with the prefix to check existence - paginator = s3_client.get_paginator('list_objects_v2') - file_exists = False - - async for page in paginator.paginate( - Bucket=bucket.name, - Prefix=filename - ): - for obj in page.get('Contents', []): - if obj['Key'] == filename: - file_exists = True - file_size = obj['Size'] - break - if file_exists: - break + if local: + # Local storage logic remains unchanged + await self.cleanup_local_data( + uid=uid, current_window=window, stale_retention=stale_retention + ) + local_path = os.path.join( + LOCAL_TMP_DIR, str(uid), str(window), filename + ) + if not os.path.exists(local_path): + tplr.logger.debug(f"Local file not found: {local_path}") + return None + state_dict = torch.load(local_path, weights_only=True) + return state_dict + else: + # Cleanup old S3 data + await self.cleanup_s3_data( + uid=uid, current_window=window, stale_retention=stale_retention + ) - if not file_exists: - logger.debug(f"File {filename} not found in bucket. Skipping...") + # Check file size first + async with self.session.create_client( + "s3", + endpoint_url=self.get_base_url(self.bucket.account_id), + region_name=CF_REGION_NAME, + config=client_config, + aws_access_key_id=self.bucket.access_key_id, + aws_secret_access_key=self.bucket.secret_access_key, + ) as s3_client: + try: + response = await s3_client.head_object( + Bucket=self.bucket.name, Key=full_key + ) + file_size = response["ContentLength"] + except ( + botocore.exceptions.ClientError, + botocore.exceptions.BotoCoreError, + ) as e: + tplr.logger.debug(f"Failed to get object metadata: {e}") return None - except Exception as e: - logger.debug(f"Error checking file existence: {e}") - return None + # Create a temporary file for download + with tempfile.NamedTemporaryFile( + delete=False, suffix=".pt" + ) as temp_file: + temp_file_path = temp_file.name - # File exists, proceed with download - if file_size > 100 * 1024 * 1024: # 100MB - success = await self._download_large_file(s3_client, filename, temp_file_path) - if not success: - return None - else: - async def download(): - response = await s3_client.get_object( - Bucket=bucket.name, - Key=filename + try: + if file_size > 5 * 1024 * 1024 * 1024: # 5GB + # Use multipart download for large files + success = await self.download_large_file( + full_key, temp_file_path ) - async with aiofiles.open(temp_file_path, "wb") as f: - await f.write(await response["Body"].read()) - - await asyncio.wait_for(download(), timeout=timeout) - - # Load the state_dict - state_dict = torch.load(temp_file_path, map_location="cpu", weights_only=True) - logger.debug(f"Successfully downloaded {filename} from R2 bucket.") - - # Clean up unless local=True - if not local: - os.remove(temp_file_path) - - return state_dict + if not success: + raise Exception("Large file download failed") + else: + # Use regular download for smaller files + state_dict = await self.s3_get_object(full_key, timeout) + return state_dict + + # Load the state dict from the temporary file + state_dict = torch.load(temp_file_path, weights_only=True) + return state_dict + + finally: + # Clean up temporary file + if os.path.exists(temp_file_path): + os.remove(temp_file_path) - except asyncio.TimeoutError: - logger.error(f"Timeout while downloading {filename} from R2 bucket.") except Exception as e: - logger.error(f"Failed to download {filename} from R2 bucket: {e}") + tplr.logger.debug(f"GET error {full_key}: {e}") + return None + finally: - if not local and os.path.exists(temp_file_path): - os.remove(temp_file_path) - - return None + tplr.logger.debug(f"GET {full_key} <--") async def get_with_retry( self, uid: str, window: int, - key: Optional[str] = None, - timeout: int = 30, - retry_interval: float = 0.1, - ): - """ - Attempts to download data from the R2 bucket, retrying until success or timeout. - - Args: - uid (str): Unique identifier for the download. - window (int): The window number for synchronization. - key (str, optional): Custom key for the filename. - timeout (int): Total timeout duration for retries. - retry_interval (float): Time to wait between retries. - - Returns: - dict: The state dictionary downloaded from the bucket. - """ + key: str, + timeout: int, + local: bool = True, + stale_retention: int = 10, + ) -> Optional[dict]: + """GET with retry operation.""" start_time = time.time() + end_time = start_time + timeout + while True: - state_dict = await self.get(uid, window, key, timeout) + if time.time() >= end_time: + tplr.logger.debug(f"GET {uid}/{window}/{key} timed out.") + return None + + state_dict = await self.get( + uid=uid, + window=window, + key=key, + local=local, + stale_retention=stale_retention, + ) if state_dict is not None: return state_dict - if time.time() - start_time > timeout: - logger.error(f"Exceeded timeout while downloading data for UID {uid}.") - return None - await asyncio.sleep(retry_interval) + + # Retry after a short delay + await asyncio.sleep(0.1) async def gather( self, @@ -387,156 +429,214 @@ async def gather( my_uid: str, uids: List[str], window: int, - key: Optional[str] = None, - timeout: int = 30, - device: str = "cpu", - ) -> Dict[str, List[torch.Tensor]]: + key: str, + timeout: int, + device: str, + local: bool = True, + stale_retention: int = 10, + ) -> SimpleNamespace: + """Gather operation.""" + start_time = time.time() + metrics = {"upload_bytes": 0, "download_bytes": 0, "successes": []} + + # Put own state_dict if available + if state_dict is not None: + await self.put( + state_dict=state_dict, + uid=str(my_uid), + window=window, + key=key, + local=local, + stale_retention=stale_retention, + ) + metrics["upload_bytes"] += sum( + tensor.element_size() * tensor.nelement() + for tensor in state_dict.values() + ) + + # Small delay to ensure data propagation + await asyncio.sleep(0.1) + + # Prepare gather tasks + gather_tasks = [ + self.get_with_retry( + uid=uid, + window=window, + key=key, + timeout=timeout, + local=local, + stale_retention=stale_retention, + ) + for uid in uids + ] + + # Initialize the aggregated state dict + aggregated_state_dict = {} + successes = [] + + # Process responses + responses = await asyncio.gather(*gather_tasks) + for idx, resp in enumerate(responses): + if resp is None: + successes.append(False) + continue + + successes.append(True) + + # Initialize aggregated_state_dict if empty + if not aggregated_state_dict: + aggregated_state_dict = { + param_name: [torch.zeros_like(tensor).to(device) for _ in uids] + for param_name, tensor in resp.items() + } + + # Fill in data from this response + for param_name, tensor in resp.items(): + aggregated_state_dict[param_name][idx] = tensor.to(device) + metrics["download_bytes"] += tensor.element_size() * tensor.nelement() + + # Calculate success metrics + success_rate = sum(successes) / len(successes) if successes else 0 + total_time = time.time() - start_time + + return SimpleNamespace( + time=total_time, + upload_bytes=metrics["upload_bytes"], + download_bytes=metrics["download_bytes"], + success_rate=success_rate, + successes=successes, + state_dict=aggregated_state_dict, + ) + + async def upload_large_file(self, file_path: str, filename: str) -> bool: """ - Gathers slices from multiple peers and assembles them for aggregation. + Uploads a large file to R2 using multipart upload. Args: - state_dict (Dict[str, torch.Tensor]): Local state dictionary. - my_uid (str): This node's unique identifier. - uids (List[str]): List of peer UIDs to gather data from. - window (int): The window number for synchronization. - key (str, optional): Custom key for filenames. - timeout (int): Timeout for gathering data from each peer. - device (str): Device to map tensors onto. + file_path (str): Path to the local file to upload + filename (str): Destination filename in R2 Returns: - Dict[str, List[torch.Tensor]]: Aggregated state dictionaries from all peers. + bool: True if successful, False otherwise """ - key = key or self.key_prefix - # Put own state_dict to the bucket - await self.put(state_dict, my_uid, window, key) + try: + # Use 16MB chunks for multipart upload + chunk_size = 16 * 1024 * 1024 - time.sleep(5) + async with self.session.create_client( + "s3", + endpoint_url=self.get_base_url(self.bucket.account_id), + region_name=CF_REGION_NAME, + config=client_config, + aws_access_key_id=self.bucket.access_key_id, + aws_secret_access_key=self.bucket.secret_access_key, + ) as s3_client: + # Initialize multipart upload + response = await s3_client.create_multipart_upload( + Bucket=self.bucket.name, Key=filename + ) + upload_id = response["UploadId"] - # Gather state_dicts from peers - gather_tasks = [ - self.get_with_retry(uid=uid, window=window, key=key, timeout=timeout) - for uid in uids - ] + # Upload parts + parts = [] + part_number = 1 - responses = await asyncio.gather(*gather_tasks) - # Initialize the gather_result dictionary - gather_result = {param_name: [] for param_name in state_dict.keys()} - # Assemble the results - for idx, peer_state in enumerate(responses): - if peer_state is None: - # Handle missing peer data, e.g., fill with zeros or skip - for param_name in state_dict.keys(): - gather_result[param_name].append( - torch.zeros_like(state_dict[param_name]).to(device) - ) - else: - for param_name in state_dict.keys(): - gather_result[param_name].append(peer_state[param_name].to(device)) + async with aiofiles.open(file_path, "rb") as f: + while True: + data = await f.read(chunk_size) + if not data: + break - return gather_result + response = await s3_client.upload_part( + Bucket=self.bucket.name, + Key=filename, + PartNumber=part_number, + UploadId=upload_id, + Body=data, + ) - def get_highest_stake_validator(self) -> Tuple[Optional[int], float]: - """Returns the UID and stake of the neuron with the highest stake.""" - stakes = self.metagraph.S - logger.info(stakes) - - # Convert numpy array to torch tensor if needed - if isinstance(stakes, np.ndarray): - stakes = torch.from_numpy(stakes) - - # Check if any stakes are non-zero - if torch.all(stakes == 0): - return None, 0.0 - - highest_stake_uid = torch.argmax(stakes).item() - stake = stakes[highest_stake_uid].item() + parts.append( + {"PartNumber": part_number, "ETag": response["ETag"]} + ) + part_number += 1 - # Validate the stake is actually non-zero - if stake == 0: - return None, 0.0 + # Complete multipart upload + await s3_client.complete_multipart_upload( + Bucket=self.bucket.name, + Key=filename, + UploadId=upload_id, + MultipartUpload={"Parts": parts}, + ) - return highest_stake_uid, stake + tplr.logger.debug(f"Successfully uploaded large file {filename}") + return True - async def get_latest_checkpoint(self) -> Optional[str]: - """ - Attempts to get the latest checkpoint from the highest stake validator. - Returns the checkpoint path if successful, None otherwise. + except Exception as e: + tplr.logger.error(f"Error uploading large file {filename}: {e}") + return False + + async def download_large_file(self, filename: str, destination_path: str) -> bool: """ - validator_uid, stake = self.get_highest_stake_validator() - if stake == 0: - logger.warning("No active validators found") - return None + Downloads a large file from R2 using multipart download. - # Get the current block and calculate window - current_block = self.subtensor.block - current_window = int(current_block / self.hparams.blocks_per_window) - - # Try last 5 windows to find most recent checkpoint - for window in range(current_window, max(0, current_window - 5), -1): - try: - checkpoint = await self.get( - uid=str(validator_uid), - window=window, - key='checkpoint', - timeout=30 # Longer timeout for checkpoint downloads - ) - if checkpoint is not None: - # Save checkpoint to disk - checkpoint_path = os.path.join(self.save_location, f'checkpoint-{window}.pt') - torch.save(checkpoint, checkpoint_path) - logger.info(f"Downloaded checkpoint from validator {validator_uid} at window {window}") - return checkpoint_path - except Exception as e: - logger.warning(f"Failed to get checkpoint from window {window}: {e}") - continue - - return None + Args: + filename (str): File to download from R2 + destination_path (str): Local path to save the file - async def _download_large_file(self, s3_client, filename: str, temp_file_path: str): - """Handles downloading large files using multipart download.""" + Returns: + bool: True if successful, False otherwise + """ try: - # Get file size - response = await s3_client.head_object( - Bucket=self.bucket.name, - Key=filename - ) - file_size = response['ContentLength'] - - # Use 16MB chunks for multipart download - chunk_size = 16 * 1024 * 1024 - total_parts = (file_size + chunk_size - 1) // chunk_size - - async with aiofiles.open(temp_file_path, 'wb') as f: - for part in range(total_parts): - start = part * chunk_size - end = min(start + chunk_size, file_size) - - response = await s3_client.get_object( - Bucket=self.bucket.name, - Key=filename, - Range=f'bytes={start}-{end-1}' - ) - - chunk = await response['Body'].read() - await f.write(chunk) - - logger.debug(f"Successfully downloaded large file {filename}") - return True + async with self.session.create_client( + "s3", + endpoint_url=self.get_base_url(self.bucket.account_id), + region_name=CF_REGION_NAME, + config=client_config, + aws_access_key_id=self.bucket.access_key_id, + aws_secret_access_key=self.bucket.secret_access_key, + ) as s3_client: + # Get file size + response = await s3_client.head_object( + Bucket=self.bucket.name, Key=filename + ) + file_size = response["ContentLength"] + + # Use 16MB chunks for multipart download + chunk_size = 16 * 1024 * 1024 + total_parts = (file_size + chunk_size - 1) // chunk_size + + async with aiofiles.open(destination_path, "wb") as f: + for part in range(total_parts): + start = part * chunk_size + end = min(start + chunk_size, file_size) + + response = await s3_client.get_object( + Bucket=self.bucket.name, + Key=filename, + Range=f"bytes={start}-{end-1}", + ) + + chunk = await response["Body"].read() + await f.write(chunk) + + tplr.logger.debug(f"Successfully downloaded large file {filename}") + return True + except Exception as e: - logger.error(f"Error downloading large file {filename}: {e}") + tplr.logger.error(f"Error downloading large file {filename}: {e}") return False async def cleanup_old_checkpoints(self, keep_last: int = 3): """ Removes old checkpoints from storage, keeping only the most recent ones. - + Args: keep_last (int): Number of most recent checkpoints to keep """ try: async with self.session.create_client( "s3", - endpoint_url=get_base_url(self.bucket.account_id), + endpoint_url=self.get_base_url(self.bucket.account_id), region_name=CF_REGION_NAME, config=client_config, aws_access_key_id=self.bucket.access_key_id, @@ -545,66 +645,47 @@ async def cleanup_old_checkpoints(self, keep_last: int = 3): # List all checkpoint files paginator = s3_client.get_paginator("list_objects_v2") checkpoint_files = [] - + async for page in paginator.paginate( - Bucket=self.bucket.name, - Prefix='checkpoint' + Bucket=self.bucket.name, Prefix="checkpoint" ): for obj in page.get("Contents", []): if obj["Key"].startswith("checkpoint"): checkpoint_files.append(obj) - + # Sort by last modified time checkpoint_files.sort(key=lambda x: x["LastModified"], reverse=True) - + # Delete older checkpoints if len(checkpoint_files) > keep_last: to_delete = checkpoint_files[keep_last:] await s3_client.delete_objects( Bucket=self.bucket.name, - Delete={"Objects": [{"Key": obj["Key"]} for obj in to_delete]} + Delete={"Objects": [{"Key": obj["Key"]} for obj in to_delete]}, ) - logger.info(f"Deleted {len(to_delete)} old checkpoints") - + tplr.logger.info(f"Deleted {len(to_delete)} old checkpoints") + except Exception as e: - logger.error(f"Error cleaning up old checkpoints: {e}") - - -async def delete_old_version_files(bucket_name: str, current_version: str): - """ - Deletes files from the S3 bucket that do not match the current version. - - Args: - bucket_name (str): The name of the S3 bucket. - current_version (str): The current version string. - """ - session = get_session() - async with session.create_client( - "s3", - endpoint_url=get_base_url(BUCKET_SECRETS["account_id"]), - region_name=CF_REGION_NAME, - config=client_config, - aws_access_key_id=BUCKET_SECRETS["write"]["access_key_id"], - aws_secret_access_key=BUCKET_SECRETS["write"]["secret_access_key"], - ) as s3_client: - paginator = s3_client.get_paginator("list_objects_v2") - async for page in paginator.paginate(Bucket=bucket_name): - to_delete = [] - for obj in page.get("Contents", []): - filename = obj["Key"] - # Check if the file version matches the current version - match = re.match(r".+-v(.+)\.pt$", filename) - if match: - file_version = match.group(1) - if file_version != current_version: - to_delete.append({"Key": filename}) - logger.debug(f"Scheduled for deletion: {filename}") - # Delete old versions in batches of 1000 (S3 limit for delete_objects) - if to_delete: - response = await s3_client.delete_objects( - Bucket=bucket_name, Delete={"Objects": to_delete} - ) - deleted = response.get("Deleted", []) - logger.info( - f"Deleted {len(deleted)} old version files from bucket {bucket_name}" - ) + tplr.logger.error(f"Error cleaning up old checkpoints: {e}") + + def get_highest_stake_validator(self) -> Tuple[Optional[int], float]: + """Returns the UID and stake of the neuron with the highest stake.""" + stakes = self.metagraph.S + tplr.logger.info(stakes) + + # Convert numpy array to torch tensor if needed + if isinstance(stakes, np.ndarray): + stakes = torch.from_numpy(stakes) + + # Check if any stakes are non-zero + if torch.all(stakes == 0): + return None, 0.0 + + highest_stake_uid = torch.argmax(stakes).item() + stake = stakes[highest_stake_uid].item() + + # Validate the stake is actually non-zero + if stake == 0: + return None, 0.0 + + return highest_stake_uid, stake diff --git a/src/tplr/compress.py b/src/tplr/compress.py index 13cb6e7..4955f42 100644 --- a/src/tplr/compress.py +++ b/src/tplr/compress.py @@ -303,4 +303,4 @@ def _get_smaller_split(n, close_to): if ix == 0: return val return all_divisors[ix - 1] - return n \ No newline at end of file + return n diff --git a/src/tplr/config.py b/src/tplr/config.py index c628095..ddd5cc2 100644 --- a/src/tplr/config.py +++ b/src/tplr/config.py @@ -19,9 +19,9 @@ # Global imports import os import sys +import botocore.config # Local imports -import botocore.config from .logging import logger # Configure bucket secrets from environment variables diff --git a/src/tplr/dataset.py b/src/tplr/dataset.py index 96e7d0c..efffc19 100644 --- a/src/tplr/dataset.py +++ b/src/tplr/dataset.py @@ -22,9 +22,8 @@ import asyncio import aiohttp import numpy as np -from torch.utils.data import IterableDataset from transformers import AutoTokenizer - +from torch.utils.data import IterableDataset class SubsetLoader(IterableDataset): """ diff --git a/src/tplr/hparams.py b/src/tplr/hparams.py index 3ed2900..34a29fd 100644 --- a/src/tplr/hparams.py +++ b/src/tplr/hparams.py @@ -136,4 +136,4 @@ def load_hparams(hparams_file: str = "hparams.json") -> SimpleNamespace: raise except Exception as e: logger.error(f"Error loading hyperparameters: {e}") - raise \ No newline at end of file + raise diff --git a/src/tplr/logging.py b/src/tplr/logging.py index e125285..89fbe17 100644 --- a/src/tplr/logging.py +++ b/src/tplr/logging.py @@ -22,7 +22,6 @@ from rich.logging import RichHandler from rich.highlighter import NullHighlighter - def T() -> float: """ Returns the current time in seconds since the epoch. diff --git a/src/tplr/schemas.py b/src/tplr/schemas.py index aaf2028..f1338fc 100644 --- a/src/tplr/schemas.py +++ b/src/tplr/schemas.py @@ -39,7 +39,6 @@ def __eq__(self, other): account_id: str access_key_id: str secret_access_key: str - class Config: str_min_length = 1 str_strip_whitespace = True diff --git a/start.sh b/start.sh deleted file mode 100755 index 7b0fc0c..0000000 --- a/start.sh +++ /dev/null @@ -1,6 +0,0 @@ - - -# pm2 delete all -pm2 start neurons/miner.py --interpreter python3 --name TM0 -- --wallet.name Bistro --wallet.hotkey M111 --device cuda:0 --subtensor.network test --use_wandb --debug -pm2 start neurons/miner.py --interpreter python3 --name TM1 -- --wallet.name Bistro --wallet.hotkey M222 --device cuda:1 --subtensor.network test --use_wandb --debug -pm2 start neurons/validator.py --interpreter python3 --name TV1 -- --wallet.name Bistro --wallet.hotkey V11 --device cuda:3 --subtensor.network test --use_wandb --debug diff --git a/tests/test_autoupdater.py b/tests/test_autoupdater.py deleted file mode 100644 index dcf2fca..0000000 --- a/tests/test_autoupdater.py +++ /dev/null @@ -1,260 +0,0 @@ -# ruff: noqa -# pylint: disable=all -# mypy: ignore-errors - -import os -import tempfile -from unittest.mock import MagicMock, patch, PropertyMock - -import git -import pytest -import asyncio -import subprocess -import json - -# Import the AutoUpdate class -from templar.autoupdate import AutoUpdate, TARGET_BRANCH - - -@pytest.mark.asyncio -async def test_autoupdate_cleanup_old_versions(): - """Test cleanup of old versions.""" - autoupdater = AutoUpdate() - - with patch("templar.autoupdate.BUCKET_SECRETS", {"bucket_name": "test-bucket"}): - with patch("templar.autoupdate.delete_old_version_files") as mock_cleanup: - # Mock the templar.__version__ to a known value - with patch("templar.__version__", "0.1.29"): - await autoupdater.cleanup_old_versions() - mock_cleanup.assert_called_with("test-bucket", "0.1.29") - - -def test_autoupdate_run_method(): - """Test the run method's loop.""" - autoupdater = AutoUpdate() - - with patch.object(autoupdater, "try_update") as mock_try_update: - # Create a side effect function to stop the loop after first iteration - def side_effect_sleep(duration): - raise KeyboardInterrupt # Breaking the infinite loop - - with patch("time.sleep", side_effect=side_effect_sleep) as mock_sleep: - try: - autoupdater.run() - except KeyboardInterrupt: - pass # Expected to break the loop - - mock_try_update.assert_called_once() - mock_sleep.assert_called_once_with(60) - - -def test_autoupdate_restart_app_pm2_no_process_name_failure(): - """Test that the application handles failure to get PM2 process name.""" - autoupdater = AutoUpdate() - - # Mock get_pm2_process_name to return None - with patch.object(autoupdater, "get_pm2_process_name", return_value=None): - with patch("templar.autoupdate.logger") as mock_logger: - with patch("templar.autoupdate.sys.exit", side_effect=SystemExit): - # Mock PM2 environment - with patch.dict(os.environ, {"PM2_HOME": "/path/to/pm2"}): - with pytest.raises(SystemExit): - autoupdater.restart_app() - # Check the correct info message is logged - mock_logger.info.assert_any_call( - "PM2 process name not found. Performing regular restart using subprocess.Popen" - ) - - -def test_autoupdate_restart_app_pm2_success(): - """Test that the application restarts successfully in a PM2 environment.""" - autoupdater = AutoUpdate() - - mock_pm2_process_name = "test_process" - # Mock get_pm2_process_name to return a test process name - with patch.object( - autoupdater, "get_pm2_process_name", return_value=mock_pm2_process_name - ): - with patch("templar.autoupdate.subprocess.run") as mock_run: - with patch("templar.autoupdate.sys.exit", side_effect=SystemExit): - # Mock PM2 environment - with patch.dict(os.environ, {"PM2_HOME": "/path/to/pm2"}): - with pytest.raises(SystemExit): - autoupdater.restart_app() - mock_run.assert_called_with( - ["pm2", "restart", mock_pm2_process_name], check=True - ) - - -@pytest.mark.asyncio -async def test_autoupdate_check_version_updated(): - """Test that check_version_updated works correctly when update is needed.""" - autoupdater = AutoUpdate() - - # Mock get_remote_version to return a higher version - async def mock_get_remote_version(self): - return "0.2.0" - - with patch.object(AutoUpdate, "get_remote_version", new=mock_get_remote_version): - # Mock templar.__version__ to a lower version - with patch("templar.__version__", "0.1.0"): - is_updated = await autoupdater.check_version_updated() - assert is_updated is True - - -@pytest.mark.asyncio -async def test_autoupdate_check_version_not_updated(): - """Test that check_version_updated works correctly when no update is needed.""" - autoupdater = AutoUpdate() - - # Mock get_remote_version to return the same version - async def mock_get_remote_version(self): - return "0.1.0" - - with patch.object(AutoUpdate, "get_remote_version", new=mock_get_remote_version): - # Mock templar.__version__ to the same version - with patch("templar.__version__", "0.1.0"): - is_updated = await autoupdater.check_version_updated() - assert is_updated is False - - -def test_autoupdate_attempt_update_success(): - """Test that attempt_update succeeds when repo is clean.""" - autoupdater = AutoUpdate() - - # Patch 'is_detached' property - with patch.object( - type(autoupdater.repo.head), "is_detached", new_callable=PropertyMock - ) as mock_is_detached: - mock_is_detached.return_value = False - - # Patch 'active_branch' property to return a mock branch - mock_branch = MagicMock() - mock_branch.name = TARGET_BRANCH - with patch.object( - type(autoupdater.repo), "active_branch", new_callable=PropertyMock - ) as mock_active_branch: - mock_active_branch.return_value = mock_branch - - with patch.object(autoupdater.repo, "is_dirty", return_value=False): - # Mock the 'remote' method to return a mock 'origin' - mock_origin = MagicMock() - mock_origin.fetch.return_value = None # Simulate successful fetch - with patch.object(autoupdater.repo, "remote", return_value=mock_origin): - # Mock 'git' attribute - mock_git = MagicMock() - autoupdater.repo.git = mock_git - - # Mock commits for local and remote to match - mock_commit = MagicMock() - mock_commit.hexsha = "abcdef" - with patch.object( - autoupdater.repo, "commit", return_value=mock_commit - ): - result = autoupdater.attempt_update() - mock_origin.fetch.assert_called_once() - mock_git.reset.assert_called_with( - "--hard", f"origin/{TARGET_BRANCH}" - ) - assert result is True - - -def test_autoupdate_attempt_update_dirty_repo(): - """Test that attempt_update fails when repo is dirty.""" - autoupdater = AutoUpdate() - - with patch.object(autoupdater.repo, "is_dirty", return_value=True): - with patch("templar.autoupdate.logger") as mock_logger: - result = autoupdater.attempt_update() - mock_logger.error.assert_called_with( - "Repository has uncommitted changes or untracked files. Cannot update." - ) - assert result is False - - -def test_autoupdate_attempt_update_pull_failure(): - """Test that attempt_update handles fetch failure.""" - autoupdater = AutoUpdate() - - # Patch 'is_detached' property - with patch.object( - type(autoupdater.repo.head), "is_detached", new_callable=PropertyMock - ) as mock_is_detached: - mock_is_detached.return_value = False - - # Patch 'active_branch' property - mock_branch = MagicMock() - mock_branch.name = TARGET_BRANCH - with patch.object( - type(autoupdater.repo), "active_branch", new_callable=PropertyMock - ) as mock_active_branch: - mock_active_branch.return_value = mock_branch - - with patch.object(autoupdater.repo, "is_dirty", return_value=False): - mock_origin = MagicMock() - mock_origin.fetch.side_effect = git.exc.GitCommandError( - "fetch", "Failed to fetch" - ) - with patch.object(autoupdater.repo, "remote", return_value=mock_origin): - # Mock 'git' attribute - mock_git = MagicMock() - autoupdater.repo.git = mock_git - - with patch("templar.autoupdate.logger") as mock_logger: - result = autoupdater.attempt_update() - # Confirm that an error was logged - mock_logger.error.assert_called_once() - error_msg = mock_logger.error.call_args[0][0] - assert error_msg.startswith("Git command failed:") - assert "Failed to fetch" in error_msg - assert result is False - - -def test_autoupdate_attempt_package_update(): - """Test that attempt_package_update calls the correct subprocess.""" - autoupdater = AutoUpdate() - - with patch("templar.autoupdate.subprocess.check_call") as mock_check_call: - autoupdater.attempt_package_update() - mock_check_call.assert_called_with( - ["uv", "sync", "--extra", "all"], - timeout=300, - ) - - -def test_autoupdate_attempt_package_update_failure(): - """Test that package update handles failures gracefully.""" - autoupdater = AutoUpdate() - - with patch( - "templar.autoupdate.subprocess.check_call", - side_effect=subprocess.CalledProcessError(1, "uv"), - ): - with patch("templar.autoupdate.logger") as mock_logger: - autoupdater.attempt_package_update() - mock_logger.exception.assert_called_once() - - -def test_autoupdate_get_pm2_process_name(): - """Test getting the PM2 process name.""" - autoupdater = AutoUpdate() - - # Mock os.getpid() - with patch("os.getpid", return_value=12345): - # Mock subprocess.run() - mock_pm2_output = json.dumps( - [ - {"name": "test_process", "pid": 12345}, - {"name": "other_process", "pid": 67890}, - ] - ) - - mock_completed_process = subprocess.CompletedProcess( - args=["pm2", "jlist"], - returncode=0, - stdout=mock_pm2_output, - ) - - with patch("subprocess.run", return_value=mock_completed_process): - process_name = autoupdater.get_pm2_process_name() - assert process_name == "test_process" diff --git a/tests/test_checkpoints.py b/tests/test_checkpoints.py deleted file mode 100644 index 70b9603..0000000 --- a/tests/test_checkpoints.py +++ /dev/null @@ -1,216 +0,0 @@ -# ruff: noqa -# pylint: disable=all -# mypy: ignore-errors -# type: ignore - -import asyncio -import os -import torch -import unittest -from unittest import mock -import tempfile -import glob -from aiobotocore.session import get_session - -from templar.checkpoint import ( - CheckpointManager, - get_base_url, - load_checkpoint, - download_checkpoint_from_neuron, -) -from templar import __version__ -from templar.config import BUCKET_SECRETS -from templar.constants import CF_REGION_NAME - - -class DummyModel(torch.nn.Module): - def __init__(self): - super().__init__() - self.linear = torch.nn.Linear(10, 1) - - -async def upload_exists_in_s3(bucket_name, key, access_key, secret_key, endpoint_url): - session = get_session() - async with session.create_client( - "s3", - endpoint_url=endpoint_url, - region_name=CF_REGION_NAME, - aws_access_key_id=access_key, - aws_secret_access_key=secret_key, - ) as s3_client: - try: - await s3_client.head_object(Bucket=bucket_name, Key=key) - return True - except Exception: - return False - - -async def async_delete_from_s3(bucket_name, key, access_key, secret_key, endpoint_url): - session = get_session() - async with session.create_client( - "s3", - endpoint_url=endpoint_url, - region_name=CF_REGION_NAME, - aws_access_key_id=access_key, - aws_secret_access_key=secret_key, - ) as s3_client: - try: - await s3_client.delete_object(Bucket=bucket_name, Key=key) - except Exception as e: - print(f"Error deleting S3 object: {e}") - - -class TestCheckpointManager(unittest.IsolatedAsyncioTestCase): - def setUp(self): - self.temp_dir = tempfile.TemporaryDirectory() - self.checkpoint_path = os.path.join(self.temp_dir.name, "checkpoint.pth") - self.model = DummyModel() - self.wallet = mock.Mock() - self.wallet.hotkey.ss58_address = "dummy_hotkey_address" - self.endpoint_url = get_base_url(BUCKET_SECRETS["account_id"]) - self.bucket_name = BUCKET_SECRETS["bucket_name"].split("/")[-1] - self.access_key = BUCKET_SECRETS["write"]["access_key_id"] - self.secret_key = BUCKET_SECRETS["write"]["secret_access_key"] - self.original_bucket_secrets = BUCKET_SECRETS.copy() - - def tearDown(self): - self.temp_dir.cleanup() - for key in self.original_bucket_secrets: - BUCKET_SECRETS[key] = self.original_bucket_secrets[key] - - async def test_async_behavior(self): - # Example test that ensures `save_and_upload` doesn't block - checkpoint_manager = CheckpointManager( - model=self.model, - checkpoint_path=self.checkpoint_path, - wallet=self.wallet, - device="cpu", - ) - - await checkpoint_manager.save_and_upload(global_step=1, block_number=100) - self.assertTrue(os.path.exists(checkpoint_manager.checkpoint_path)) - checkpoint_manager.cleanup() - - async def test_checkpoint_cleanup(self): - checkpoint_manager = CheckpointManager( - model=self.model, - checkpoint_path=self.checkpoint_path, - wallet=self.wallet, - device="cpu", - ) - - # Save multiple checkpoints - for i in range(5): - await checkpoint_manager.save_and_upload( - global_step=i, block_number=100 + i - ) - - # Wait a moment for async tasks to finish - await asyncio.sleep(5) - - # Check that only the latest 3 checkpoints remain locally - pattern = os.path.join( - checkpoint_manager.checkpoint_dir, - f"neuron_checkpoint_{self.wallet.hotkey.ss58_address}_b*_v{__version__}.pth", - ) - files = glob.glob(pattern) - self.assertEqual(len(files), 3) - checkpoint_manager.cleanup() - - async def test_checkpoint_local_save(self): - # If you previously tested `save_checkpoint` directly, now test `save_and_upload` - # to ensure a checkpoint is saved locally without error. - checkpoint_manager = CheckpointManager( - model=self.model, - checkpoint_path=self.checkpoint_path, - wallet=self.wallet, - device="cpu", - ) - await checkpoint_manager.save_and_upload(global_step=1, block_number=100) - self.assertTrue(os.path.exists(checkpoint_manager.checkpoint_path)) - checkpoint_manager.cleanup() - - async def test_checkpoint_s3_upload(self): - checkpoint_manager = CheckpointManager( - model=self.model, - checkpoint_path=self.checkpoint_path, - wallet=self.wallet, - device="cpu", - ) - await checkpoint_manager.save_and_upload(global_step=1, block_number=100) - - # Wait for the upload to complete - if checkpoint_manager.upload_task: - await checkpoint_manager.upload_task - - filename = os.path.basename(checkpoint_manager.checkpoint_path) - s3_exists = await upload_exists_in_s3( - self.bucket_name, - filename, - self.access_key, - self.secret_key, - self.endpoint_url, - ) - self.assertTrue(s3_exists) - - # Cleanup - await async_delete_from_s3( - self.bucket_name, - filename, - self.access_key, - self.secret_key, - self.endpoint_url, - ) - checkpoint_manager.cleanup() - - async def test_checkpoint_upload_with_invalid_credentials(self): - faulty_access_key = "invalid_access_key" - faulty_secret_key = "invalid_secret_key" - - with mock.patch.dict( - "templar.config.BUCKET_SECRETS", - { - "bucket_name": BUCKET_SECRETS["bucket_name"], - "account_id": BUCKET_SECRETS["account_id"], - "write": { - "access_key_id": faulty_access_key, - "secret_access_key": faulty_secret_key, - }, - }, - clear=False, - ): - checkpoint_manager = CheckpointManager( - model=self.model, - checkpoint_path=self.checkpoint_path, - wallet=self.wallet, - device="cpu", - ) - await checkpoint_manager.save_and_upload(global_step=1, block_number=100) - - # Check if NOT uploaded with correct creds - filename = os.path.basename(checkpoint_manager.checkpoint_path) - s3_exists = await upload_exists_in_s3( - self.bucket_name, - filename, - self.access_key, - self.secret_key, - self.endpoint_url, - ) - self.assertFalse(s3_exists) - checkpoint_manager.cleanup() - - async def test_invalid_checkpoint_path(self): - # Test what happens if the checkpoint directory is invalid - invalid_path = "/invalid_dir/checkpoint.pth" - - with self.assertRaises(PermissionError): - checkpoint_manager = CheckpointManager( - model=self.model, - checkpoint_path=invalid_path, - wallet=self.wallet, - device="cpu", - ) - # No need to proceed if initialization fails - await checkpoint_manager.save_and_upload(global_step=1, block_number=100) - - # No cleanup needed since initialization failed From 2b0a4ba66f89133b1108c58415bc36e3a88a0f27 Mon Sep 17 00:00:00 2001 From: distributedstatemachine! Date: Tue, 31 Dec 2024 16:03:02 +0000 Subject: [PATCH 14/15] fix: wandb --- neurons/miner.py | 27 ++++---- neurons/validator.py | 11 ++-- src/tplr/__init__.py | 2 +- src/tplr/wandb.py | 153 +++++++++++++++++++++---------------------- 4 files changed, 91 insertions(+), 102 deletions(-) diff --git a/neurons/miner.py b/neurons/miner.py index d0fdb03..78f4b07 100644 --- a/neurons/miner.py +++ b/neurons/miner.py @@ -58,7 +58,7 @@ class Miner: def config(): parser = argparse.ArgumentParser(description='Miner script') parser.add_argument('--netuid', type=int, default=268, help='Bittensor network UID.') - parser.add_argument('--project', type=str, default='templar-1', help='Wandb project.') + parser.add_argument('--project', type=str, default='templar', help='Wandb project.') parser.add_argument('--device', type=str, default='cuda', help='Device to use for training') parser.add_argument('--debug', action='store_true', help='Enable debug logging') parser.add_argument('--trace', action='store_true', help='Enable trace logging') @@ -252,7 +252,12 @@ async def run(self): self.batch_times.append(duration) self.total_tokens_processed += batch_tokens - # Enhanced wandb logging with both existing and new metrics + # Log gradient metrics + grad_norms = [p.grad.norm().item() for p in self.model.parameters() if p.grad is not None] + weight_norms = [p.norm().item() for p in self.model.parameters()] + momentum_norms = [m.norm().item() for m in self.momentum.values()] + + # Enhanced wandb logging with all metrics self.wandb.log({ # Training metrics "miner/loss": total_loss/(i+1), @@ -272,24 +277,14 @@ async def run(self): # Optimization metrics "miner/learning_rate": self.scheduler.get_last_lr()[0], - }, step=self.global_step) - - # Log gradient metrics - grad_norms = [p.grad.norm().item() for p in self.model.parameters() if p.grad is not None] - weight_norms = [p.norm().item() for p in self.model.parameters()] - momentum_norms = [m.norm().item() for m in self.momentum.values()] - - self.wandb.log({ - # Gradient metrics + + # Gradient statistics as points "miner/mean_grad_norm": sum(grad_norms) / len(grad_norms) if grad_norms else 0, "miner/max_grad_norm": max(grad_norms) if grad_norms else 0, + "miner/min_grad_norm": min(grad_norms) if grad_norms else 0, + "miner/grad_norm_std": torch.tensor(grad_norms).std().item() if grad_norms else 0, "miner/mean_weight_norm": sum(weight_norms) / len(weight_norms), "miner/mean_momentum_norm": sum(momentum_norms) / len(momentum_norms), - - # Distribution metrics - "miner/grad_norm_distribution": self.wandb.Histogram(grad_norms), - "miner/weight_norm_distribution": self.wandb.Histogram(weight_norms), - "miner/momentum_norm_distribution": self.wandb.Histogram(momentum_norms), }, step=self.global_step) # Log per-peer metrics diff --git a/neurons/validator.py b/neurons/validator.py index 5d4f6f2..8d1710a 100644 --- a/neurons/validator.py +++ b/neurons/validator.py @@ -54,7 +54,7 @@ class Validator: def config(): parser = argparse.ArgumentParser(description='Validator script') parser.add_argument('--netuid', type=int, default=268, help='Bittensor network UID.') - parser.add_argument('--project', type=str, default='templar-1', help='Wandb project.') + parser.add_argument('--project', type=str, default='templar', help='Wandb project.') parser.add_argument('--device', type=str, default='cuda', help='Device to use for training') parser.add_argument('--debug', action='store_true', help='Enable debug logging') parser.add_argument('--trace', action='store_true', help='Enable trace logging') @@ -418,14 +418,13 @@ async def run(self): "validator/mean_score": self.scores[valid_score_indices].mean().item(), "validator/mean_moving_avg_score": self.moving_avg_scores[valid_score_indices].mean().item(), "validator/max_score": self.scores.max().item(), + "validator/min_score": self.scores.min().item(), "validator/max_moving_avg_score": self.moving_avg_scores.max().item(), + "validator/min_moving_avg_score": self.moving_avg_scores.min().item(), "validator/mean_weight": weights[valid_score_indices].mean().item(), "validator/weight_std": weights[valid_score_indices].std().item(), - - # Histograms - "validator/scores_distribution": self.wandb.Histogram(self.scores[valid_score_indices].cpu().numpy()), - "validator/moving_avg_scores_distribution": self.wandb.Histogram(self.moving_avg_scores[valid_score_indices].cpu().numpy()), - "validator/weights_distribution": self.wandb.Histogram(weights[valid_score_indices].cpu().numpy()), + "validator/score_std": self.scores[valid_score_indices].std().item(), + "validator/moving_avg_score_std": self.moving_avg_scores[valid_score_indices].std().item(), }, step=self.global_step) # Set weights on chain diff --git a/src/tplr/__init__.py b/src/tplr/__init__.py index c7c5803..ec3cb93 100644 --- a/src/tplr/__init__.py +++ b/src/tplr/__init__.py @@ -30,4 +30,4 @@ from .hparams import * from .logging import * from .schemas import * -from .wandb import * +from .wandb import initialize_wandb diff --git a/src/tplr/wandb.py b/src/tplr/wandb.py index 9f4756c..5d0f1ed 100644 --- a/src/tplr/wandb.py +++ b/src/tplr/wandb.py @@ -1,92 +1,87 @@ # The MIT License (MIT) # © 2024 templar.tech -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the "Software"), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of -# the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. -# fmt: off - -# Global imports import os -import wandb as wandbm +import wandb +from wandb.sdk.wandb_run import Run +from . import __version__ +from .logging import logger + +def initialize_wandb(run_prefix: str, uid: str, config: any, group: str, job_type: str) -> Run: + """Initialize WandB run with persistence and resumption capabilities. + + Args: + run_prefix (str): Prefix for the run name (e.g., 'V' for validator, 'M' for miner) + uid (str): Unique identifier for the run + config (any): Configuration object containing project and other settings + group (str): Group name for organizing runs + job_type (str): Type of job (e.g., 'validation', 'training') + + Returns: + Run: Initialized WandB run object + """ + # Ensure the wandb directory exists + wandb_dir = os.path.join(os.getcwd(), 'wandb') + os.makedirs(wandb_dir, exist_ok=True) -# Local imports -from . import __version__, logger + # Define the run ID file path inside the wandb directory + run_id_file = os.path.join( + wandb_dir, f"wandb_run_id_{run_prefix}{uid}_{__version__}.txt" + ) -class WandbManager: - def __init__(self, run_prefix=None, uid=None, config=None, group=None, job_type=None, is_validator=False): - """Initialize WandB manager with proper run management and resumability. + # Check for existing run and verify it still exists in wandb + run_id = None + if os.path.exists(run_id_file): + with open(run_id_file, 'r') as f: + run_id = f.read().strip() - Args: - run_prefix: Optional prefix for the run name (if None, determined by is_validator) - uid: User ID - config: Config object containing wandb settings - group: Group name (if None, determined by is_validator) - job_type: Job type (if None, determined by is_validator) - is_validator: Boolean indicating if this is a validator run - """ - self.wandb_dir = os.path.join(os.getcwd(), 'wandb') - os.makedirs(self.wandb_dir, exist_ok=True) - self.run = None + # Verify if run still exists in wandb + try: + api = wandb.Api() + api.run(f"tplr/{config.project}-v{__version__}/{run_id}") + logger.info(f"Found existing run ID: {run_id}") + except Exception: + logger.info(f"Previous run {run_id} not found in WandB, starting new run") + run_id = None + os.remove(run_id_file) - if all(x is not None for x in [uid, config]): - # Set defaults based on validator status if not provided - if run_prefix is None: - run_prefix = 'V' if is_validator else 'M' - if group is None: - group = 'validator' if is_validator else 'miner' - if job_type is None: - job_type = 'validation' if is_validator else 'training' + # Initialize WandB + run = wandb.init( + project=f"{config.project}-v{__version__}", + entity='tplr', + id=run_id, + resume='must' if run_id else 'never', + name=f'{run_prefix}{uid}', + config=config, + group=group, + job_type=job_type, + dir=wandb_dir, + settings=wandb.Settings( + init_timeout=300, + _disable_stats=True, + ) + ) - # Define the run ID file path inside the wandb directory - run_id_file = os.path.join( - self.wandb_dir, f"wandb_run_id_{run_prefix}{uid}_{__version__}.txt" + # Special handling for evaluator + if run_prefix == "E": + tasks = config.tasks.split(',') + for task in tasks: + metric_name = f"eval/{task}" + wandb.define_metric( + name=metric_name, + step_metric="global_step", + plot=True, + summary="max" ) - # Check for existing run and verify it still exists in wandb - run_id = None - if os.path.exists(run_id_file): - with open(run_id_file, 'r') as f: - run_id = f.read().strip() - - # Verify if run still exists in wandb - try: - api = wandbm.Api() - api.run(f"tplr/{config.project}-v{__version__}/{run_id}") - logger.info(f"Found existing run ID: {run_id}") - except Exception: - logger.info(f"Previous run {run_id} not found in WandB, starting new run") - run_id = None - os.remove(run_id_file) + # Save run ID for future resumption + if not run_id: + with open(run_id_file, 'w') as f: + f.write(run.id) - # Initialize WandB - self.run = wandbm.init( - project=f"{config.project}-v{__version__}", - entity='tplr', - id=run_id, - resume='allow', - name=f'{run_prefix}{uid}', - config=config, - group=group, - job_type=job_type, - dir=self.wandb_dir, - settings=wandbm.Settings( - init_timeout=300, - _disable_stats=True, - ) - ) + return run - # Save run ID for future resumption - if not run_id: - with open(run_id_file, 'w') as f: - f.write(self.run.id) +# TODO: Add error handling for network issues +# TODO: Add retry mechanism for wandb initialization +# TODO: Add cleanup mechanism for old run ID files +# TODO: Add support for custom wandb settings From 5cf61ee6537ad55fe0ae65c14abb594357e5ceb7 Mon Sep 17 00:00:00 2001 From: distributedstatemachine! Date: Tue, 31 Dec 2024 16:13:03 +0000 Subject: [PATCH 15/15] chore: ruff --- src/tplr/wandb.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/tplr/wandb.py b/src/tplr/wandb.py index 5d0f1ed..4f4a4bc 100644 --- a/src/tplr/wandb.py +++ b/src/tplr/wandb.py @@ -7,21 +7,24 @@ from . import __version__ from .logging import logger -def initialize_wandb(run_prefix: str, uid: str, config: any, group: str, job_type: str) -> Run: + +def initialize_wandb( + run_prefix: str, uid: str, config: any, group: str, job_type: str +) -> Run: """Initialize WandB run with persistence and resumption capabilities. - + Args: run_prefix (str): Prefix for the run name (e.g., 'V' for validator, 'M' for miner) uid (str): Unique identifier for the run config (any): Configuration object containing project and other settings group (str): Group name for organizing runs job_type (str): Type of job (e.g., 'validation', 'training') - + Returns: Run: Initialized WandB run object """ # Ensure the wandb directory exists - wandb_dir = os.path.join(os.getcwd(), 'wandb') + wandb_dir = os.path.join(os.getcwd(), "wandb") os.makedirs(wandb_dir, exist_ok=True) # Define the run ID file path inside the wandb directory @@ -32,9 +35,9 @@ def initialize_wandb(run_prefix: str, uid: str, config: any, group: str, job_typ # Check for existing run and verify it still exists in wandb run_id = None if os.path.exists(run_id_file): - with open(run_id_file, 'r') as f: + with open(run_id_file, "r") as f: run_id = f.read().strip() - + # Verify if run still exists in wandb try: api = wandb.Api() @@ -48,10 +51,10 @@ def initialize_wandb(run_prefix: str, uid: str, config: any, group: str, job_typ # Initialize WandB run = wandb.init( project=f"{config.project}-v{__version__}", - entity='tplr', + entity="tplr", id=run_id, - resume='must' if run_id else 'never', - name=f'{run_prefix}{uid}', + resume="must" if run_id else "never", + name=f"{run_prefix}{uid}", config=config, group=group, job_type=job_type, @@ -59,28 +62,26 @@ def initialize_wandb(run_prefix: str, uid: str, config: any, group: str, job_typ settings=wandb.Settings( init_timeout=300, _disable_stats=True, - ) + ), ) # Special handling for evaluator if run_prefix == "E": - tasks = config.tasks.split(',') + tasks = config.tasks.split(",") for task in tasks: metric_name = f"eval/{task}" wandb.define_metric( - name=metric_name, - step_metric="global_step", - plot=True, - summary="max" + name=metric_name, step_metric="global_step", plot=True, summary="max" ) # Save run ID for future resumption if not run_id: - with open(run_id_file, 'w') as f: + with open(run_id_file, "w") as f: f.write(run.id) return run + # TODO: Add error handling for network issues # TODO: Add retry mechanism for wandb initialization # TODO: Add cleanup mechanism for old run ID files