From 8cd7dc3e2beeda21f06e95a478a734ff253d0a32 Mon Sep 17 00:00:00 2001 From: dazi321 <161010156+dazi321@users.noreply.github.com> Date: Tue, 13 Jan 2026 15:22:25 -0700 Subject: [PATCH 1/2] Update README for inference subnet --- README.md | 36 ++++++++++++++++++-- neurons/miner.py | 41 +++++++++++++++-------- template/protocol.py | 62 ++++++++++++++++++----------------- template/validator/forward.py | 14 +++++--- template/validator/reward.py | 29 +++++++++------- template/validator/tasks.py | 48 +++++++++++++++++++++++++++ 6 files changed, 168 insertions(+), 62 deletions(-) create mode 100644 template/validator/tasks.py diff --git a/README.md b/README.md index ba69bdaee..d217d6ca2 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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. @@ -84,8 +89,8 @@ As described in [Quickstarter template](#quickstarter-template) section above, w - `template/protocol.py`: Contains the definition of the wire-protocol used by miners and validators. - `neurons/miner.py`: Script that defines the miner's behavior, i.e., how the miner responds to requests from validators. - `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. @@ -98,6 +103,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 --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 --wallet.name miner --wallet.hotkey default --logging.debug +``` + +### Run a validator + +```bash +python neurons/validator.py --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. diff --git a/neurons/miner.py b/neurons/miner.py index e42deeb1f..c6057c151 100644 --- a/neurons/miner.py +++ b/neurons/miner.py @@ -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 @@ -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 @@ -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, @@ -126,7 +139,7 @@ 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. @@ -134,7 +147,7 @@ async def priority(self, synapse: template.protocol.Dummy) -> float: 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. diff --git a/template/protocol.py b/template/protocol.py index c601e58af..aa1436041 100644 --- a/template/protocol.py +++ b/template/protocol.py @@ -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 "", + } diff --git a/template/validator/forward.py b/template/validator/forward.py index af5e7ee01..e79dea36b 100644 --- a/template/validator/forward.py +++ b/template/validator/forward.py @@ -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 @@ -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, @@ -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. diff --git a/template/validator/reward.py b/template/validator/reward.py index 96cd02120..35252a0b4 100644 --- a/template/validator/reward.py +++ b/template/validator/reward.py @@ -17,39 +17,44 @@ # 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 numpy as np -from typing import List +from typing import Dict, List import bittensor as bt -def reward(query: int, response: int) -> float: +def reward(task: Dict[str, str], response_payload: Dict[str, str]) -> float: """ - Reward the miner response to the dummy request. This method returns a reward - value for the miner, which is used to update the miner's score. + Reward the miner response to the inference request. This method returns a + reward value for the miner, which is used to update the miner's score. Returns: - float: The reward value for the miner. """ + expected = task["expected"].strip().lower() + normalized_response = ( + response_payload.get("response", "").strip().lower() + ) bt.logging.info( - f"In rewards, query val: {query}, response val: {response}, rewards val: {1.0 if response == query * 2 else 0}" + "In rewards, task id: " + f"{task['id']}, response: {normalized_response}, expected: {expected}" ) - return 1.0 if response == query * 2 else 0 + return 1.0 if normalized_response == expected else 0.0 def get_rewards( self, - query: int, - responses: List[float], + task: Dict[str, str], + responses: List[Dict[str, str]], ) -> np.ndarray: """ - Returns an array of rewards for the given query and responses. + Returns an array of rewards for the given task and responses. Args: - - query (int): The query sent to the miner. - - responses (List[float]): A list of responses from the miner. + - task (Dict[str, str]): The task sent to the miner, including the expected answer. + - responses (List[Dict[str, str]]): A list of miner payloads. Returns: - np.ndarray: An array of rewards for the given query and responses. """ # Get all the reward results by iteratively calling your reward() function. - return np.array([reward(query, response) for response in responses]) + return np.array([reward(task, response) for response in responses]) diff --git a/template/validator/tasks.py b/template/validator/tasks.py new file mode 100644 index 000000000..e9f00da2c --- /dev/null +++ b/template/validator/tasks.py @@ -0,0 +1,48 @@ +# The MIT License (MIT) +# Copyright © 2023 Yuma Rao +# TODO(developer): Set your name +# Copyright © 2023 + +# 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. + +from typing import Dict, List + +TASKS: List[Dict[str, str]] = [ + { + "id": "translate_hello_fr", + "prompt": "Translate 'hello' to French", + "expected": "bonjour", + }, + { + "id": "math_addition", + "prompt": "What is 2 + 2?", + "expected": "4", + }, + { + "id": "summary_bittensor", + "prompt": "Summarize: Bittensor is a decentralized network.", + "expected": "bittensor is decentralized.", + }, + { + "id": "sentiment_positive", + "prompt": "Classify sentiment: I love this product.", + "expected": "positive", + }, + { + "id": "capital_france", + "prompt": "Answer: The capital of France is?", + "expected": "paris", + }, +] From 5055c4c07ef56c9524cf685db93d582704787edc Mon Sep 17 00:00:00 2001 From: dazi321 <161010156+dazi321@users.noreply.github.com> Date: Tue, 13 Jan 2026 15:36:03 -0700 Subject: [PATCH 2/2] Update README.md --- README.md | 90 ++----------------------------------------------------- 1 file changed, 2 insertions(+), 88 deletions(-) diff --git a/README.md b/README.md index d217d6ca2..7efcce02a 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,8 @@ As described in [Quickstarter template](#quickstarter-template) section above, w - `template/protocol.py`: Contains the definition of the wire-protocol used by miners and validators. - `neurons/miner.py`: Script that defines the miner's behavior, i.e., how the miner responds to requests from validators. - `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. @@ -155,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. -```