Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement web panel to manage hyperparams #1

Merged
merged 23 commits into from
Jul 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
dd0c33f
Use custom user model from users app
olzhasar-reef May 3, 2024
2776c35
Clean & lint
olzhasar-reef May 3, 2024
0abfedd
Add HyperParameter model, admin
olzhasar-reef May 3, 2024
8ddbc05
Add bittensor dependency
olzhasar-reef May 3, 2024
2a0fda8
Add pytest-mock, faker
olzhasar-reef May 3, 2024
d1540eb
Add subtensor variables to settings and .env
olzhasar-reef May 4, 2024
6597133
Implement bittensor interaction layer
olzhasar-reef May 4, 2024
3d5f4c3
Implement update_hyperparam service
olzhasar-reef May 4, 2024
e318dbe
Implement service for syncing hyperparams
olzhasar-reef May 4, 2024
296a9cf
Catch SystemExit raised by bittensor in services
olzhasar-reef May 4, 2024
990b01d
Use update_hyperparam service in admin
olzhasar-reef May 4, 2024
fa36730
Rename sync_hyperparams to refresh_hyperparams
olzhasar-reef May 4, 2024
dad9bd2
Add docstrings to services
olzhasar-reef May 4, 2024
c3db9db
Implement admin button for refreshing hyperparams
olzhasar-reef May 4, 2024
cf9b3bb
Use BigIntegerField for HyperParameter values
olzhasar-reef May 4, 2024
d5ee696
Format with ruff
olzhasar-reef May 4, 2024
e79abc9
Redesign error handling moving api errors to bt
olzhasar-reef May 5, 2024
09dbcd9
Format with ruff
olzhasar-reef May 5, 2024
7ed0f44
Use DecimalField for hyperparameter values
olzhasar-reef May 6, 2024
4d42faf
Use Charfield for hyperparameter values
olzhasar-reef May 6, 2024
63bf076
Remove unused hotkey setting and env var
olzhasar-reef May 6, 2024
b0d35f2
Fix typing
olzhasar-reef May 6, 2024
5bb5fec
Bump bittensor to 7.2.0
olzhasar-reef Jul 8, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions app/envs/prod/gunicorn.conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,3 @@
bind = "0.0.0.0:8000"
wsgi_app = "bittensor_panel.wsgi:application"
access_logfile = "-"



3 changes: 0 additions & 3 deletions app/src/bittensor_panel/asgi.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import os


from django.core.asgi import get_asgi_application



os.environ.setdefault("DJANGO_SETTINGS_MODULE", "bittensor_panel.settings")
application = get_asgi_application()
5 changes: 4 additions & 1 deletion app/src/bittensor_panel/celery.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import logging
import os

from celery import Celeryfrom django.conf import settings
from celery import Celery
from celery.signals import setup_logging
from django.conf import settings
from django_structlog.celery.steps import DjangoStructLogInitStep

from .settings import configure_structlog

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "bittensor_panel.settings")
Expand Down
52 changes: 50 additions & 2 deletions app/src/bittensor_panel/core/admin.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,53 @@
from django.contrib import admin # noqa
from django.contrib.admin import register # noqa
from django.contrib import admin, messages
from django.contrib.admin import register
from django.http.response import (
HttpResponseRedirect,
)
from django.urls import path, reverse

from .exceptions import BittensorAPIError, HyperParameterUpdateFailed
from .models import HyperParameter
from .services import (
refresh_hyperparams,
update_hyperparam,
)


@register(HyperParameter)
class HyperParameterAdmin(admin.ModelAdmin):
list_display = ["name", "value", "created_at", "updated_at"]

def has_add_permission(self, request):
return False

def has_delete_permission(self, request, obj=None):
return False

def save_model(self, request, obj, form, change):
try:
update_hyperparam(obj)
except (BittensorAPIError, HyperParameterUpdateFailed) as e:
messages.error(request, str(e))

def get_urls(self):
urls = [
path(
"refresh-hyperparams/",
self.admin_site.admin_view(self.refresh_hyperparams_view),
name="refresh_hyperparams",
),
]
urls += super().get_urls()
return urls

def refresh_hyperparams_view(self, request):
if request.method == "POST":
try:
refresh_hyperparams()
except BittensorAPIError as e:
messages.error(request, str(e))

return HttpResponseRedirect(reverse("admin:core_hyperparameter_changelist"))


admin.site.site_header = "Bittensor Administration Panel"
Expand Down
55 changes: 55 additions & 0 deletions app/src/bittensor_panel/core/bt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from dataclasses import asdict

import bittensor
from django.conf import settings

from .exceptions import (
SubtensorConnectionError,
SubtensorServerError,
)


def get_subtensor() -> bittensor.subtensor:
try:
return bittensor.subtensor(settings.SUBTENSOR_ADDRESS)
except (SystemExit, Exception) as e:
raise SubtensorConnectionError(f"Failed to connect to subtensor at: {settings.SUBTENSOR_ADDRESS}") from e


def get_wallet() -> bittensor.wallet:
return bittensor.wallet(
name=settings.WALLET_NAME,
path=settings.WALLET_PATH,
)


def load_hyperparams() -> dict[str, int] | None:
st = get_subtensor()

try:
hyperparams = st.get_subnet_hyperparameters(settings.SUBNET_UID)
except Exception as e:
raise SubtensorServerError(f"Failed to load hyperparameters from subtensor\n{e}") from e

if not hyperparams:
return None

return asdict(hyperparams)


def update_remote_hyperparam(name: str, value: str) -> bool:
st = get_subtensor()
wallet = get_wallet()

try:
return st.set_hyperparameter(
wallet=wallet,
netuid=settings.SUBNET_UID,
parameter=name,
value=value,
wait_for_inclusion=False,
wait_for_finalization=True,
prompt=False,
)
except Exception as exc:
raise SubtensorServerError(f"Failed to update hyperparameter in subtensor\n{exc}") from exc
14 changes: 14 additions & 0 deletions app/src/bittensor_panel/core/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
class BittensorAPIError(Exception):
"""Base class for all Bittensor API errors."""


class SubtensorConnectionError(BittensorAPIError):
"""Failed to connect to subtensor instance."""


class SubtensorServerError(BittensorAPIError):
"""Subtensor network returned an error."""


class HyperParameterUpdateFailed(Exception):
"""Failed to update hyperparameter in subtensor."""
30 changes: 30 additions & 0 deletions app/src/bittensor_panel/core/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Generated by Django 4.2.11 on 2024-05-06 18:02

from django.db import migrations, models


class Migration(migrations.Migration):
initial = True

dependencies = []

operations = [
migrations.CreateModel(
name="HyperParameter",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("name", models.CharField(max_length=255, unique=True)),
("value", models.CharField(max_length=255)),
],
),
]
Empty file.
7 changes: 7 additions & 0 deletions app/src/bittensor_panel/core/models.py
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
from django.db import models # noqa


class HyperParameter(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
name = models.CharField(max_length=255, unique=True)
value = models.CharField(max_length=255)
34 changes: 34 additions & 0 deletions app/src/bittensor_panel/core/services.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from bittensor_panel.core.bt import load_hyperparams, update_remote_hyperparam
from bittensor_panel.core.exceptions import HyperParameterUpdateFailed
from bittensor_panel.core.models import HyperParameter


def update_hyperparam(instance: HyperParameter) -> None:
"""
Update hyperparameter in the subtensor with the new value and save changes to the database.
"""

result = update_remote_hyperparam(instance.name, instance.value)

if not result:
raise HyperParameterUpdateFailed("Failed to update remote hyperparameter. Subtensor returned False.")

instance.save(update_fields=["value"])


def refresh_hyperparams() -> None:
"""
Load hyperparameters from the subtensor and overwrite corresponding records in the database.
"""

hyperparam_dict = load_hyperparams()

if not hyperparam_dict:
return

objs: list[HyperParameter] = []

for name, value in hyperparam_dict.items():
objs.append(HyperParameter(name=name, value=value))

HyperParameter.objects.bulk_create(objs, update_conflicts=True, update_fields=["value"], unique_fields=["name"])
6 changes: 5 additions & 1 deletion app/src/bittensor_panel/core/tests/settings.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import os

os.environ["DEBUG_TOOLBAR"] = "False"
os.environ["SUBTENSOR_ADDRESS"] = "local"
os.environ["SUBNET_UID"] = "1"
os.environ["WALLET_NAME"] = "default"
os.environ["WALLET_HOTKEY"] = "default"
os.environ["WALLET_PATH"] = ""

from bittensor_panel.settings import * # noqa: E402,F403

30 changes: 30 additions & 0 deletions app/src/bittensor_panel/core/tests/test_admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from unittest.mock import MagicMock

import pytest
from django.test import Client
from pytest_mock import MockerFixture

from bittensor_panel.core.exceptions import SubtensorConnectionError


@pytest.fixture
def mock_refresh_hyperparams(mocker: MockerFixture):
return mocker.patch("bittensor_panel.core.admin.refresh_hyperparams")


def test_refresh_hyperparams_view(admin_client: Client, mock_refresh_hyperparams: MagicMock):
response = admin_client.post("/admin/core/hyperparameter/refresh-hyperparams/")

mock_refresh_hyperparams.assert_called_once()

assert response.status_code == 302
assert response.url == "/admin/core/hyperparameter/" # type: ignore


def test_refresh_hyperparams_view_exception(admin_client: Client, mock_refresh_hyperparams: MagicMock):
mock_refresh_hyperparams.side_effect = SubtensorConnectionError

response = admin_client.post("/admin/core/hyperparameter/refresh-hyperparams/")

assert response.status_code == 302
assert response.url == "/admin/core/hyperparameter/" # type: ignore
Loading
Loading