opentensor / subtensor

Bittensor Blockchain Layer
The Unlicense
155 stars 155 forks source link

Tiered Emissions #727

Open distributedstatemachine opened 3 months ago

distributedstatemachine commented 3 months ago

Description

Currently, miner emissions within subnets is based on a peer-to-peer competition model, with underperforming miners still earning emissions. To provide more flexibility, we propose implementing an optional tiered performance-based rewards system that allows subnet owners to define performance criteria and distribute rewards based on predefined tiers. This system will enable more targeted incentivization and efficient resource allocation for subnets that choose to use it.

Acceptance Criteria

  1. Subnet owners should be able to opt-in to the tiered reward system for their subnet.
  2. Subnet owners should be able to define performance tiers (e.g., S, A, B, C, D) with specific criteria for each subnet that opts in.
  3. For subnets using the tiered system, the reward distribution mechanism should allocate rewards only to miners/validators in the top tiers (e.g., S and A).
  4. The system should recycle undistributed rewards, adding them to the next epoch's reward pool for tiered subnets.
  5. The tiered system should be flexible enough to accommodate different performance metrics for various types of subnets.
  6. Subnets not opting into the tiered system should continue to use the existing reward distribution mechanism.
  7. The subnet information should include whether the tiered reward system is enabled for each subnet.

Tasks

#[pallet::storage]
pub type TierDefinitions<T: Config> = StorageDoubleMap<
    _,
    Twox64Concat,
    u16, // netuid
    Twox64Concat,
    u8,  // tier level
    Vec<(u16, u16)>, // (lower_bound, upper_bound)
>;

#[pallet::storage]
pub type TieredRewardEnabled<T: Config> = StorageMap<_, Twox64Concat, u16, bool>;
#[pallet::call_index(99)]
#[pallet::weight((10_000, DispatchClass::Normal, Pays::No))]
pub fn set_tiered_reward_status(
    origin: OriginFor<T>,
    netuid: u16,
    enabled: bool,
) -> DispatchResult {
    Self::ensure_subnet_owner_or_root(origin, netuid)?;
    TieredRewardEnabled::<T>::insert(netuid, enabled);
    Self::deposit_event(Event::TieredRewardStatusSet(netuid, enabled));
    Ok(())
}
fn calculate_tiers(netuid: u16, ranks: &Vec<I32F32>) -> Vec<u8> {
    if !TieredRewardEnabled::<T>::get(netuid).unwrap_or(false) {
        return vec![0; ranks.len()]; // All in top tier if not enabled
    }
    let tier_defs = TierDefinitions::<T>::get(netuid).unwrap_or_default();
    ranks.iter().map(|&rank| {
        let rank_u16 = (rank * I32F32::from_num(u16::MAX)).to_num::<u16>();
        tier_defs.iter()
            .position(|&(lower, upper)| rank_u16 >= lower && rank_u16 <= upper)
            .map(|pos| pos as u8)
            .unwrap_or(tier_defs.len() as u8) // Default to lowest tier
    }).collect()
}
let tiers = Self::calculate_tiers(netuid, &ranks);
let mut total_emission = 0;
if TieredRewardEnabled::<T>::get(netuid).unwrap_or(false) {
    for (i, tier) in tiers.iter().enumerate() {
        if *tier <= 1 { // Assuming tiers 0 and 1 are the top tiers
            let neuron_emission = emission_u64[i];
            Self::accumulate_hotkey_emission(&hotkeys[i], neuron_emission);
            total_emission += neuron_emission;
        }
    }
    let remaining_emission = subnet_emission.saturating_sub(total_emission);
    PendingEmission::<T>::mutate(netuid, |pending_emission| {
        *pending_emission = pending_emission.saturating_add(remaining_emission);
    });
} else {
    // Use existing distribution mechanism for non-tiered subnets
    for (i, _) in tiers.iter().enumerate() {
        let neuron_emission = emission_u64[i];
        Self::accumulate_hotkey_emission(&hotkeys[i], neuron_emission);
    }
}
#[pallet::call_index(98)]
#[pallet::weight((10_000, DispatchClass::Normal, Pays::No))]
pub fn set_tier_definitions(
    origin: OriginFor<T>,
    netuid: u16,
    tier_definitions: Vec<(u16, u16)>,
) -> DispatchResult {
    Self::ensure_subnet_owner_or_root(origin, netuid)?;
    ensure!(TieredRewardEnabled::<T>::get(netuid).unwrap_or(false), Error::<T>::TieredRewardNotEnabled);
    ensure!(tier_definitions.len() <= 5, Error::<T>::TooManyTiers);
    for (i, (lower, upper)) in tier_definitions.iter().enumerate() {
        ensure!(lower < upper, Error::<T>::InvalidTierBounds);
        if i > 0 {
            ensure!(lower > &tier_definitions[i-1].1, Error::<T>::OverlappingTiers);
        }
    }
    TierDefinitions::<T>::insert(netuid, tier_definitions);
    Self::deposit_event(Event::TierDefinitionsSet(netuid));
    Ok(())
}
// In subnet_info.rs

#[freeze_struct("fe79d58173da662a")]
#[derive(Decode, Encode, PartialEq, Eq, Clone, Debug)]
pub struct SubnetInfo<T: Config> {
    // ... (snip)
    tier_enabled: bool, // Add this line
}

#[freeze_struct("55b472510f10e76a")]
#[derive(Decode, Encode, PartialEq, Eq, Clone, Debug)]
pub struct SubnetHyperparams {
    // ... (snip)
    tier_enabled: bool, // Add this line
}

impl<T: Config> Pallet<T> {
    pub fn get_subnet_info(netuid: u16) -> Option<SubnetInfo<T>> {
    // ... (snip)
        Some(SubnetInfo {
    // ... (snip)
            tier_enabled: Self::get_tiered_reward_enabled(netuid), // Add this line
        })
    }

    pub fn get_subnet_hyperparams(netuid: u16) -> Option<SubnetHyperparams> {
            // ... (snip)
        Some(SubnetHyperparams {
               // ... (snip)
            tier_enabled: Self::get_tiered_reward_enabled(netuid), // Add this line
        })
    }

    // Add this new function to get the tiered reward status
    pub fn get_tiered_reward_enabled(netuid: u16) -> bool {
        TieredRewardEnabled::<T>::get(netuid).unwrap_or(false)
    }
}

Additional Considerations

ppolewicz commented 2 months ago

Perspective of a subnet owner is that in order to configure the subnet one has to set hyperparameters. Is there a very good reason to add a new extrinsic to manage a boolean flag? I think that's what subnet hyperparameters are for.

As for the tier parameter, I think a string hyperparameter matching a regex would be more practical to manage than a new extrinsic with new syntax, scale codec, cli etc. (\d)(?:,(\d))+ (+the length of the string should not be larger than something in case someone gets a funny idea).

The way I see it used is competitons on who can achieve some level of performance first, gets a reward which builds up until someone gets to that level. It can be fun.

Corner case to think about: what happens if some rewards accumulate, someone turns it off and then subnet gets deregged/dissolved?

Generally though, the tier system will lead to an incentive curve that isn't as steep as we'd like it to be, unless there is a high number of tiers I guess? Can you please elaborate on how you see this used in practice?

trdougherty commented 2 months ago

Hey @ppolewicz, a lot of the tiering can actually be managed by the incentive mechanism, as long as we have a way to direct the flow of emissions to capacitor / recycle. In practice, we will want to set minimum criteria of performance on our subnet (hit certain performance thresholds like sharpe ratio / returns). If a miner doesn't hit these, then the miner is never useful. We would prefer to store the incentive in an intermediary to make our downstream payouts more attractive for new miners, or recycle so the tokens aren't being wasted if we don't believe any of our miners are providing genuine value to the network.