Skip to content

Commit 1392c89

Browse files
feat: Use Pylon client in the Facilitator
Facilitator now uses the pylon client to communicate with the chain wherever possible. Issue: COM-807 Impacts: facilitator
1 parent 5aa12c5 commit 1392c89

File tree

19 files changed

+139
-107
lines changed

19 files changed

+139
-107
lines changed

compute_horde/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ dependencies = [
1818
"compute-horde-sdk",
1919
"rich~=13.9.4",
2020
"web3==7.11.0",
21-
"bittensor-pylon-client==1.3.0",
21+
"bittensor-pylon-client==1.4.0",
2222
]
2323

2424
[build-system]

compute_horde/uv.lock

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

executor/uv.lock

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from django.conf import settings
2+
from pylon_client.v1 import DEFAULT_RETRIES, Config, PylonClient
3+
from tenacity import Retrying
4+
5+
6+
def pylon_client(retries: Retrying = DEFAULT_RETRIES) -> PylonClient:
7+
return PylonClient(
8+
Config(
9+
address=settings.PYLON_ADDRESS,
10+
open_access_token=settings.PYLON_OPEN_ACCESS_TOKEN,
11+
retry=retries,
12+
)
13+
)

facilitator/app/src/project/core/tasks.py

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
from collections import defaultdict
22
from datetime import timedelta
3+
from ipaddress import IPv4Address
34

45
import requests
56
import structlog
67
from asgiref.sync import async_to_sync
78
from celery.utils.log import get_task_logger
89
from channels.layers import get_channel_layer
9-
from compute_horde.utils import get_validators
1010
from django.conf import settings
1111
from django.db import connection
1212
from django.utils.timezone import now
1313
from pydantic import BaseModel, ValidationError, parse_obj_as
14+
from pylon_client.v1 import NetUid
1415
from requests import RequestException
1516

1617
from project.celery import app
@@ -26,6 +27,7 @@
2627
Validator,
2728
)
2829
from .models import MinerVersion as MinerVersionDTO
30+
from .pylon import pylon_client
2931
from .schemas import ForceDisconnect, HardwareSpec
3032
from .specs import normalize_gpu_name
3133
from .utils import fetch_compute_subnet_hardware
@@ -38,14 +40,18 @@
3840
@app.task
3941
def sync_metagraph() -> None:
4042
"""Fetch current validators and miners from the network and store them in the database"""
41-
import bittensor
42-
43-
with bittensor.subtensor(network=settings.BITTENSOR_NETWORK) as subtensor:
44-
metagraph = subtensor.metagraph(netuid=settings.BITTENSOR_NETUID)
45-
validators = get_validators(metagraph=metagraph)
46-
47-
sync_validators.delay([v.hotkey for v in validators])
48-
sync_miners.delay([neuron.hotkey for neuron in metagraph.neurons if neuron.axon_info.is_serving])
43+
netuid = NetUid(settings.BITTENSOR_NETUID)
44+
with pylon_client() as client:
45+
validators_response = client.open_access.get_latest_validators(netuid)
46+
block_number = validators_response.block.number
47+
neurons_response = client.open_access.get_neurons(netuid, block_number)
48+
49+
validator_hotkeys = [v.hotkey for v in validators_response.validators]
50+
serving_miner_hotkeys = [
51+
hotkey for hotkey, neuron in neurons_response.neurons.items() if neuron.axon_info.is_serving
52+
]
53+
sync_validators.delay(validator_hotkeys)
54+
sync_miners.delay(serving_miner_hotkeys)
4955

5056

5157
@app.task
@@ -191,14 +197,18 @@ def fetch_miner_version(hotkey: str, ip: str, port: int) -> None:
191197
def fetch_miner_versions() -> None:
192198
"""
193199
Fetch miner & miner runner versions for every active miner.
194-
The list of active miners is retrieved from the metagraph.
200+
The list of active miners is retrieved from the metagraph via Pylon.
195201
"""
196-
import bittensor
197-
198-
metagraph = bittensor.metagraph(netuid=settings.BITTENSOR_NETUID, network=settings.BITTENSOR_NETWORK)
199-
miners = [neuron for neuron in metagraph.neurons if neuron.axon_info.is_serving]
200-
for miner in miners:
201-
fetch_miner_version.delay(miner.hotkey, miner.axon_info.ip, miner.axon_info.port)
202+
netuid = NetUid(settings.BITTENSOR_NETUID)
203+
with pylon_client() as client:
204+
response = client.open_access.get_latest_neurons(netuid)
205+
206+
for hotkey, neuron in response.neurons.items():
207+
if neuron.axon_info.is_serving:
208+
if not isinstance(neuron.axon_info.ip, IPv4Address):
209+
log.warning("skipping non-IPv4 miner", miner_hotkey=hotkey, ip=neuron.axon_info.ip)
210+
continue
211+
fetch_miner_version.delay(hotkey, str(neuron.axon_info.ip), neuron.axon_info.port)
202212

203213

204214
@app.task

facilitator/app/src/project/core/tests/conftest.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
from ...asgi import application
2121
from ..models import Channel, Job, Validator
2222

23+
pytest_plugins = ["compute_horde.test_base.pylon"]
24+
2325

2426
@pytest_asyncio.fixture
2527
async def communicator():
File renamed without changes.

facilitator/app/src/project/core/tests/test_tasks.py

Lines changed: 48 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
from ipaddress import IPv4Address
12
from typing import NamedTuple
3+
from unittest.mock import MagicMock, patch
24

35
import pytest
46
from asgiref.sync import sync_to_async
@@ -9,56 +11,38 @@
911

1012
class MockedAxonInfo(NamedTuple):
1113
is_serving: bool
12-
ip: str = ""
13-
port: int = 0
14+
ip: IPv4Address = IPv4Address("0.0.0.0")
15+
port: int = 8000
1416

1517

1618
class MockedNeuron(NamedTuple):
17-
uid: int
1819
hotkey: str
1920
axon_info: MockedAxonInfo
20-
stake: float
2121

2222

23-
class MockedMetagraph:
24-
def __init__(self, neurons):
25-
self.neurons = neurons
26-
self.total_stake = [n.stake for n in neurons]
27-
28-
29-
class MockedSubtensor:
30-
def __init__(self, metagraph: MockedMetagraph):
31-
self._metagraph = metagraph
32-
33-
def __call__(self, *args, **kwargs):
34-
return self
35-
36-
def __enter__(self):
37-
return self
38-
39-
def __exit__(self, *args, **kwargs):
40-
pass
23+
class MockedValidator(NamedTuple):
24+
hotkey: str
4125

42-
def metagraph(self, *args, **kwargs):
43-
return self._metagraph
4426

27+
def configure_mock_pylon_client(
28+
mock_client: MagicMock,
29+
validators: list[str],
30+
neurons: dict[str, MockedNeuron],
31+
) -> None:
32+
validators_response = MagicMock()
33+
validators_response.validators = [MockedValidator(hotkey=v) for v in validators]
34+
validators_response.block.number = 12345
4535

46-
validator_params = dict(
47-
axon_info=MockedAxonInfo(is_serving=False),
48-
stake=1000.0,
49-
)
36+
neurons_response = MagicMock()
37+
neurons_response.neurons = neurons
5038

51-
miner_params = dict(
52-
axon_info=MockedAxonInfo(is_serving=True),
53-
stake=0.0,
54-
)
39+
mock_client.open_access.get_latest_validators.return_value = validators_response
40+
mock_client.open_access.get_neurons.return_value = neurons_response
5541

5642

5743
@pytest.mark.django_db(transaction=True)
58-
def test__sync_metagraph__activation(monkeypatch):
59-
import bittensor
60-
61-
validators = Validator.objects.bulk_create(
44+
def test__sync_metagraph__activation(mock_pylon_client):
45+
Validator.objects.bulk_create(
6246
[
6347
Validator(ss58_address="remains_active", is_active=True),
6448
Validator(ss58_address="is_deactivated", is_active=True),
@@ -67,56 +51,55 @@ def test__sync_metagraph__activation(monkeypatch):
6751
]
6852
)
6953

70-
metagraph = MockedMetagraph(
71-
neurons=[
72-
MockedNeuron(uid=0, hotkey="remains_active", **validator_params),
73-
MockedNeuron(uid=1, hotkey="is_deactivated", **miner_params),
74-
MockedNeuron(uid=2, hotkey="remains_inactive", **miner_params),
75-
MockedNeuron(uid=3, hotkey="is_activated", **validator_params),
76-
MockedNeuron(uid=4, hotkey="new_validator", **validator_params),
77-
MockedNeuron(uid=5, hotkey="new_miner", **miner_params),
78-
]
79-
)
80-
subtensor = MockedSubtensor(metagraph=metagraph)
54+
validator_hotkeys = ["remains_active", "is_activated", "new_validator"]
55+
neurons = {
56+
"remains_active": MockedNeuron(hotkey="remains_active", axon_info=MockedAxonInfo(is_serving=False)),
57+
"is_deactivated": MockedNeuron(hotkey="is_deactivated", axon_info=MockedAxonInfo(is_serving=True)),
58+
"remains_inactive": MockedNeuron(hotkey="remains_inactive", axon_info=MockedAxonInfo(is_serving=True)),
59+
"is_activated": MockedNeuron(hotkey="is_activated", axon_info=MockedAxonInfo(is_serving=False)),
60+
"new_validator": MockedNeuron(hotkey="new_validator", axon_info=MockedAxonInfo(is_serving=False)),
61+
"new_miner": MockedNeuron(hotkey="new_miner", axon_info=MockedAxonInfo(is_serving=True)),
62+
}
63+
64+
configure_mock_pylon_client(mock_pylon_client, validator_hotkeys, neurons)
8165

82-
with monkeypatch.context() as mp:
83-
mp.setattr(bittensor, "subtensor", subtensor)
66+
with patch("project.core.tasks.pylon_client", return_value=mock_pylon_client):
8467
sync_metagraph()
8568

8669
validators = Validator.objects.order_by("id").values_list("ss58_address", "is_active")
8770
assert list(validators) == [
88-
tuple(d.values())
89-
for d in [
90-
dict(ss58_address="remains_active", is_active=True),
91-
dict(ss58_address="is_deactivated", is_active=False),
92-
dict(ss58_address="remains_inactive", is_active=False),
93-
dict(ss58_address="is_activated", is_active=True),
94-
dict(ss58_address="new_validator", is_active=True),
95-
]
71+
("remains_active", True),
72+
("is_deactivated", False),
73+
("remains_inactive", False),
74+
("is_activated", True),
75+
("new_validator", True),
9676
]
9777

9878

9979
@pytest.mark.asyncio
10080
@pytest.mark.django_db(transaction=True)
10181
async def test__websocket__disconnect_validator_if_become_inactive(
102-
monkeypatch,
82+
mock_pylon_client,
10383
communicator,
10484
authenticated,
10585
validator,
10686
job,
10787
dummy_job_params,
10888
):
109-
"""Check that validator is disconnected if it becomes inactive"""
110-
import bittensor
111-
11289
await communicator.receive_json_from()
11390
assert await Channel.objects.filter(validator=validator).aexists()
11491

115-
metagraph = MockedMetagraph(neurons=[MockedNeuron(uid=0, hotkey=validator.ss58_address, **miner_params)])
116-
subtensor = MockedSubtensor(metagraph=metagraph)
92+
validator_hotkeys = []
93+
neurons = {
94+
validator.ss58_address: MockedNeuron(
95+
hotkey=validator.ss58_address,
96+
axon_info=MockedAxonInfo(is_serving=True),
97+
),
98+
}
99+
100+
configure_mock_pylon_client(mock_pylon_client, validator_hotkeys, neurons)
117101

118-
with monkeypatch.context() as mp:
119-
mp.setattr(bittensor, "subtensor", subtensor)
102+
with patch("project.core.tasks.pylon_client", return_value=mock_pylon_client):
120103
await sync_to_async(sync_metagraph)()
121104

122105
assert (await communicator.receive_output())["type"] == "websocket.close"

facilitator/app/src/project/settings.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ def wrapped(*args, **kwargs):
137137

138138
BITTENSOR_NETUID = env.int("BITTENSOR_NETUID")
139139
BITTENSOR_NETWORK = env.str("BITTENSOR_NETWORK")
140+
PYLON_ADDRESS = env.str("PYLON_ADDRESS", "http://pylon:8000")
141+
PYLON_OPEN_ACCESS_TOKEN = env.str("PYLON_OPEN_ACCESS_TOKEN", "facilitator_token")
140142
SIGNATURE_EXPIRE_DURATION = env("SIGNATURE_EXPIRE_DURATION", default="300")
141143

142144
WANDB_API_KEY = env("WANDB_API_KEY")

facilitator/envs/dev/.env.template

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ CHANNELS_BACKEND_URL=redis://localhost:7379/0
3636
BITTENSOR_NETUID=12
3737
BITTENSOR_NETWORK=test
3838

39+
PYLON_OPEN_ACCESS_TOKEN=facilitator_token
40+
3941
WANDB_API_KEY=
4042

4143
EMAIL_BACKEND=django.core.mail.backends.filebased.EmailBackend

0 commit comments

Comments
 (0)