Skip to content
This repository was archived by the owner on Jul 10, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
122 changes: 34 additions & 88 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
- [Before you proceed](#before-you-proceed)
- [Install](#install)
- [Writing your own incentive mechanism](#writing-your-own-incentive-mechanism)
- [Running this subnet (validator + miner)](#running-this-subnet-validator--miner)
- [Writing your own subnet API](#writing-your-own-subnet-api)
- [Subnet Links](#subnet-links)
- [License](#license)
Expand Down Expand Up @@ -48,6 +49,10 @@ Each subnet consists of:
- A protocol using which the subnet miners and subnet validators interact with one another. This protocol is part of the incentive mechanism.
- The Bittensor API using which the subnet miners and subnet validators interact with Bittensor's onchain consensus engine [Yuma Consensus](https://bittensor.com/documentation/validating/yuma-consensus). The Yuma Consensus is designed to drive these actors: subnet validators and subnet miners, into agreement on who is creating value and what that value is worth.

### This subnet: Inference-as-a-Service

This subnet is designed for model inference. Validators send prompts, miners return responses, and validators score responses against expected outputs. Miners can choose any model they want to run (including fine-tuned variants) and report the model name back to validators. By default, miners use placeholder responses, but you can replace the miner logic with a real model or inference service. The incentive mechanism rewards correct responses and can be extended to include latency or quality metrics.

This starter template is split into three primary files. To write your own incentive mechanism, you should edit these files. These files are:
1. `template/protocol.py`: Contains the definition of the protocol used by subnet miners and subnet validators.
2. `neurons/miner.py`: Script that defines the subnet miner's behavior, i.e., how the subnet miner responds to requests from subnet validators.
Expand Down Expand Up @@ -86,6 +91,8 @@ As described in [Quickstarter template](#quickstarter-template) section above, w
- `neurons/validator.py`: This script defines the validator's behavior, i.e., how the validator requests information from the miners and determines the scores.
- `template/forward.py`: Contains the definition of the validator's forward pass.
- `template/reward.py`: Contains the definition of how validators reward miner responses.
- `template/validator/forward.py`: Contains the definition of the validator's forward pass.
- `template/validator/reward.py`: Contains the definition of how validators reward miner responses.

In addition to the above files, you should also update the following files:
- `README.md`: This file contains the documentation for your project. Update this file to reflect your project's documentation.
Expand All @@ -98,6 +105,33 @@ __Note__
The `template` directory should also be renamed to your project name.
---

## Running this subnet (validator + miner)

Follow the full local/testnet/mainnet setup instructions in the docs linked above. Once your wallets are created and registered, you can run a validator and miner for this inference subnet.

### Run a miner

```bash
python neurons/miner.py --netuid <NETUID> --wallet.name miner --wallet.hotkey default --logging.debug
```

Optional: report the model you are running. This is not enforced but helps validators understand miner behavior:

```bash
export MINER_MODEL_NAME="my-llama3-8b-finetune"
python neurons/miner.py --netuid <NETUID> --wallet.name miner --wallet.hotkey default --logging.debug
```

### Run a validator

```bash
python neurons/validator.py --netuid <NETUID> --wallet.name validator --wallet.hotkey default --logging.debug
```

### How scoring works

Validators sample inference tasks, send prompts to miners, and compare responses to expected outputs. Correct responses receive higher rewards. You can expand this logic in `template/validator/reward.py` to include latency, safety checks, or more complex grading.

# Writing your own subnet API
To leverage the abstract `SubnetsAPI` in Bittensor, you can implement a standardized interface. This interface is used to interact with the Bittensor network and can be used by a client to interact with the subnet through its exposed axons.

Expand All @@ -123,91 +157,3 @@ class SubnetsAPI(ABC):
@abstractmethod
def prepare_synapse(self, *args, **kwargs) -> Any:
"""
Prepare the synapse-specific payload.
"""
...

@abstractmethod
def process_responses(self, responses: List[Union["bt.Synapse", Any]]) -> Any:
"""
Process the responses from the network.
"""
...

```


Here is a toy example:

```python
from bittensor.subnets import SubnetsAPI
from MySubnet import MySynapse

class MySynapseAPI(SubnetsAPI):
def __init__(self, wallet: "bt.wallet"):
super().__init__(wallet)
self.netuid = 99

def prepare_synapse(self, prompt: str) -> MySynapse:
# Do any preparatory work to fill the synapse
data = do_prompt_injection(prompt)

# Fill the synapse for transit
synapse = StoreUser(
messages=[data],
)
# Send it along
return synapse

def process_responses(self, responses: List[Union["bt.Synapse", Any]]) -> str:
# Look through the responses for information required by your application
for response in responses:
if response.dendrite.status_code != 200:
continue
# potentially apply post processing
result_data = postprocess_data_from_response(response)
# return data to the client
return result_data
```

You can use a subnet API to the registry by doing the following:
1. Download and install the specific repo you want
1. Import the appropriate API handler from bespoke subnets
1. Make the query given the subnet specific API



# Subnet Links
In order to see real-world examples of subnets in-action, see the `subnet_links.py` document or access them from inside the `template` package by:
```python
import template
template.SUBNET_LINKS
[{'name': 'sn0', 'url': ''},
{'name': 'sn1', 'url': 'https://github.com/opentensor/prompting/'},
{'name': 'sn2', 'url': 'https://github.com/bittranslateio/bittranslate/'},
{'name': 'sn3', 'url': 'https://github.com/gitphantomman/scraping_subnet/'},
{'name': 'sn4', 'url': 'https://github.com/manifold-inc/targon/'},
...
]
```

## License
This repository is licensed under the MIT License.
```text
# The MIT License (MIT)
# Copyright © 2024 Opentensor Foundation

# 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.
```
41 changes: 27 additions & 14 deletions neurons/miner.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
# 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 time
import typing
import bittensor as bt
Expand All @@ -43,27 +44,39 @@ def __init__(self, config=None):
# TODO(developer): Anything specific to your use case you can do here

async def forward(
self, synapse: template.protocol.Dummy
) -> template.protocol.Dummy:
self, synapse: template.protocol.Inference
) -> template.protocol.Inference:
"""
Processes the incoming 'Dummy' synapse by performing a predefined operation on the input data.
This method should be replaced with actual logic relevant to the miner's purpose.
Processes the incoming inference synapse by returning a response string.

Args:
synapse (template.protocol.Dummy): The synapse object containing the 'dummy_input' data.
synapse (template.protocol.Inference): The synapse object containing the prompt.

Returns:
template.protocol.Dummy: The synapse object with the 'dummy_output' field set to twice the 'dummy_input' value.
template.protocol.Inference: The synapse object with the 'response' field filled.

The 'forward' function is a placeholder and should be overridden with logic that is appropriate for
the miner's intended operation. This method demonstrates a basic transformation of input data.
This is a placeholder for model inference. Replace the simple rule-based
responses with a call to your model or inference service.
"""
# TODO(developer): Replace with actual implementation logic.
synapse.dummy_output = synapse.dummy_input * 2
# TODO(developer): Replace with actual model inference logic.
synapse.model_name = os.getenv(
"MINER_MODEL_NAME", "unspecified"
)
normalized_prompt = synapse.prompt.strip().lower()
canned_responses = {
"translate 'hello' to french": "bonjour",
"what is 2 + 2?": "4",
"summarize: bittensor is a decentralized network.": "bittensor is decentralized.",
"classify sentiment: i love this product.": "positive",
"answer: the capital of france is?": "paris",
}
synapse.response = canned_responses.get(
normalized_prompt, "unknown"
)
return synapse

async def blacklist(
self, synapse: template.protocol.Dummy
self, synapse: template.protocol.Inference
) -> typing.Tuple[bool, str]:
"""
Determines whether an incoming request should be blacklisted and thus ignored. Your implementation should
Expand All @@ -74,7 +87,7 @@ async def blacklist(
requests before they are deserialized to avoid wasting resources on requests that will be ignored.

Args:
synapse (template.protocol.Dummy): A synapse object constructed from the headers of the incoming request.
synapse (template.protocol.Inference): A synapse object constructed from the headers of the incoming request.

Returns:
Tuple[bool, str]: A tuple containing a boolean indicating whether the synapse's hotkey is blacklisted,
Expand Down Expand Up @@ -126,15 +139,15 @@ async def blacklist(
)
return False, "Hotkey recognized!"

async def priority(self, synapse: template.protocol.Dummy) -> float:
async def priority(self, synapse: template.protocol.Inference) -> float:
"""
The priority function determines the order in which requests are handled. More valuable or higher-priority
requests are processed before others. You should design your own priority mechanism with care.

This implementation assigns priority to incoming requests based on the calling entity's stake in the metagraph.

Args:
synapse (template.protocol.Dummy): The synapse object that contains metadata about the incoming request.
synapse (template.protocol.Inference): The synapse object that contains metadata about the incoming request.

Returns:
float: A priority score derived from the stake of the calling entity.
Expand Down
62 changes: 32 additions & 30 deletions template/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,59 +18,61 @@
# DEALINGS IN THE SOFTWARE.

import typing

import bittensor as bt

# TODO(developer): Rewrite with your protocol definition.

# This is the protocol for the dummy miner and validator.
# It is a simple request-response protocol where the validator sends a request
# to the miner, and the miner responds with a dummy response.
# This is the protocol for an inference-focused subnet.
# Validators send a prompt and miners return a response string.

# ---- miner ----
# Example usage:
# def dummy( synapse: Dummy ) -> Dummy:
# synapse.dummy_output = synapse.dummy_input + 1
# def generate( synapse: Inference ) -> Inference:
# synapse.response = run_model(synapse.prompt)
# return synapse
# axon = bt.axon().attach( dummy ).serve(netuid=...).start()
# axon = bt.axon().attach( generate ).serve(netuid=...).start()

# ---- validator ---
# Example usage:
# dendrite = bt.dendrite()
# dummy_output = dendrite.query( Dummy( dummy_input = 1 ) )
# assert dummy_output == 2
# response = dendrite.query( Inference( prompt="hello" ) )
# assert response == "bonjour"


class Dummy(bt.Synapse):
class Inference(bt.Synapse):
"""
A simple dummy protocol representation which uses bt.Synapse as its base.
This protocol helps in handling dummy request and response communication between
the miner and the validator.
A protocol representation for inference request/response.

Attributes:
- dummy_input: An integer value representing the input request sent by the validator.
- dummy_output: An optional integer value which, when filled, represents the response from the miner.
- prompt: The input prompt or task request sent by the validator.
- task_id: Optional task identifier for tracking.
- response: Optional model response text filled by the miner.
- model_name: Optional model identifier reported by the miner.
"""

# Required request input, filled by sending dendrite caller.
dummy_input: int
prompt: str

# Optional task identifier set by the validator.
task_id: typing.Optional[str] = None

# Optional response output, filled by receiving axon.
response: typing.Optional[str] = None

# Optional request output, filled by receiving axon.
dummy_output: typing.Optional[int] = None
# Optional model name reported by the miner.
model_name: typing.Optional[str] = None

def deserialize(self) -> int:
def deserialize(self) -> typing.Dict[str, str]:
"""
Deserialize the dummy output. This method retrieves the response from
the miner in the form of dummy_output, deserializes it and returns it
as the output of the dendrite.query() call.
Deserialize the response text. This method retrieves the response from
the miner and returns a structured payload from the dendrite.query()
call.

Returns:
- int: The deserialized response, which in this case is the value of dummy_output.

Example:
Assuming a Dummy instance has a dummy_output value of 5:
>>> dummy_instance = Dummy(dummy_input=4)
>>> dummy_instance.dummy_output = 5
>>> dummy_instance.deserialize()
5
- Dict[str, str]: A dictionary with the response text and model name.
"""
return self.dummy_output
return {
"response": self.response or "",
"model_name": self.model_name or "",
}
14 changes: 10 additions & 4 deletions template/validator/forward.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@
# 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 random
import time

import bittensor as bt

from template.protocol import Dummy
from template.protocol import Inference
from template.validator.reward import get_rewards
from template.validator.tasks import TASKS
from template.utils.uids import get_random_uids


Expand All @@ -39,12 +42,15 @@ async def forward(self):
# get_random_uids is an example method, but you can replace it with your own.
miner_uids = get_random_uids(self, k=self.config.neuron.sample_size)

# Select an inference task for this step.
task = random.choice(TASKS)

# The dendrite client queries the network.
responses = await self.dendrite(
# Send the query to selected miner axons in the network.
axons=[self.metagraph.axons[uid] for uid in miner_uids],
# Construct a dummy query. This simply contains a single integer.
synapse=Dummy(dummy_input=self.step),
# Construct an inference query. This contains the prompt to answer.
synapse=Inference(prompt=task["prompt"], task_id=task["id"]),
# All responses have the deserialize function called on them before returning.
# You are encouraged to define your own deserialization function.
deserialize=True,
Expand All @@ -55,7 +61,7 @@ async def forward(self):

# TODO(developer): Define how the validator scores responses.
# Adjust the scores based on responses from miners.
rewards = get_rewards(self, query=self.step, responses=responses)
rewards = get_rewards(self, task=task, responses=responses)

bt.logging.info(f"Scored responses: {rewards}")
# Update the scores based on the rewards. You may want to define your own update_scores function for custom behavior.
Expand Down
Loading