Skip to content

BUG: Severity Low: Non-initial model group cap compares N-1 post-collateral weight against pre-collateral contribution #2217

Open @maria-mitina opened 2026-11-04 19:18 UTC 2 comments Updated 2026-11-04 19:51 UTC
Priority: Low

Summary

The group cap for non-initial models compares values in two different units. The cap budget is cap_factor × the previous epoch's final weight, which is after collateral adjustment, the 30% power cap and the confirmation discount. The value being capped is the group's pre-collateral contribution (ConsensusKoeff × PoC weight). Collateral adjustment then runs after the cap and reduces the group's weight a second time.

When participants are not fully collateralized, a non-initial model's weight share converges to about cap_factor × base_weight_ratio, no matter how much compute it contributes.

Where

  • ComputeGroupCap: cap = CapFactor × TotalNetworkWeight. TotalNetworkWeight is the sum of ConsensusWeights.
  • ConsensusWeights come from buildPreviousConfirmedWeightCaps. They are the N-1 confirmed effective weights, i.e. final ActiveParticipant.Weight after collateral and power capping, times the confirmation ratio.
  • ComputeConsensusWeights compares that cap against rawTotal = Σ ConsensusKoeff × MemberPocWeights, which is before collateral.
  • runWeightPipeline in module.go then calls AdjustWeightsByCollateral (L764) and applyEpochPowerCapping (L765) on the already-capped weights. The cap is applied at L725.

The calculator uses the name "consensus weight" for both the N-1 post-collateral value and the N pre-collateral value.

Observed on testnet

The testnet runs v0.2.17-testnet-1 with DECODE PoC; the coefficient is 1 for both models.

  • Models: Qwen3-4B is the initial model; Qwen2.5-7B is non-initial.
  • Params: cap_factor = 0.75, base_weight_ratio = 0.2, grace period ended. Only one participant has collateral.
  • Raw PoC weight: 7B ≈ 8,877, 4B ≈ 12,655, so 7B supplies about 41% of raw PoC weight.
  • One epoch:
  • Cap = 0.75 × N-1 total 1,730 = 1,297, so 7B is scaled to 14.6% of its raw contribution.
  • Collateral (×0.2) then reduces 7B to about 259; the chain shows 257.
  • The 30% power cap trims the large 4B participants. The final total is 1,704.
  • Result: 7B's weight share settles at 0.75 × 0.2 = 15%. Epochs 877–879 show 14.9%, 15.2% and 15.0%.
  • With the cap applied in post-collateral units, a rough estimate puts 7B at about 33%.

This also moves the share closer to the eligibility threshold (w_threshold = 0.1). If it drops below that, the model gets weight 0 and cannot become eligible again. See "Related" below.

Mainnet impact today

None at the moment. As of epoch 447: - All 25 active participants are fully collateralized (lowest coverage 1.1×). - No participant hits the 30% power cap. - Neither non-initial model (DeepSeek-V4-Flash and GLM-5.3-Flash, at about 118k–659k and 173k–482k against a cap of about 1.07M) reaches the cap.

The issue becomes live on mainnet if a non-initial model grows enough for the cap to bind and, at the same time, weight is reduced by a slash or collateral withdrawal, or by the confirmation discount.

Why the tests don't catch it

The ComputeGroupCap and ComputeConsensusWeights tests in weight_calculator_test.go set TotalNetworkWeight equal to the PoC weights, with no collateral or power cap, so the two units coincide.

Design doc mismatch

proposals/multi-model-poc/design-2.md and README.md define the cap as cap_factor * sum(member's prior-epoch consensus weight from other eligible groups), i.e. the group's own members. The code uses the whole network's N-1 weight. Under the doc's formula, hosts that only serve a non-initial model would always get a cap of 0. Either the doc or the code should be updated, and the units need to be defined explicitly.

Possible fixes

  1. Persist each participant's N-1 pre-collateral weight (the ComputeConsensusWeights output) and compute the cap against that.
  2. Or apply the group cap after AdjustWeightsByCollateral, so both sides are in final-weight units.
  3. Add a test with base_weight_ratio < 1 and partial collateral, plus one where the power cap binds.

A non-initial group whose members all had zero N-1 weight, for example after every member dropped out for one epoch, fails both MeetsWeightThreshold and the v_min established-member check in IsGroupEligible. It then stays at weight 0 permanently. On testnet the only way out was a governance change to w_threshold/v_min and back. This is worth a separate issue or a recovery path.


💬 Comments (2)

@tcharchian commented 2026-11-04 19:32 UTC

@GLiberman, let's fix it in 0.2.18, please.

@maria-mitina commented 2026-11-04 19:44 UTC

Chain logs from testnet (epoch 881 formation)

These are from v0.2.17-testnet-1, and the same pattern repeats in epochs 879, 880 and 882. Addresses are shortened.

INF Collateral grace period has ended. Adjusting weights by collateral. current_epoch=881
INF Universal power capping applied to epoch powers originalTotalPower=6943 cappedTotalPower=1714 participantCount=7
INF weight_group epoch=881 model=Qwen/Qwen2.5-7B-Instruct    coeff=1 raw_total=8858  cap=1280 scale=0.144502144953714157
INF weight_group epoch=881 model=Qwen/Qwen3-4B-Instruct-2507 coeff=1 raw_total=12638 cap=-1   scale=1
INF weight_pipeline epoch=881 addr=…pcgs0y modes=7B:DIRECT,4B:NONE     consensus=492  before_collateral=492  final=98
INF weight_pipeline epoch=881 addr=…32vrcn modes=7B:DIRECT,4B:NONE     consensus=255  before_collateral=255  final=51
INF weight_pipeline epoch=881 addr=…q60cwt modes=7B:DIRECT,4B:NONE     consensus=265  before_collateral=265  final=53
INF weight_pipeline epoch=881 addr=…4sgtd5 modes=7B:DIRECT,4B:NONE     consensus=266  before_collateral=266  final=53
INF weight_pipeline epoch=881 addr=…u4txhy modes=7B:DELEGATE,4B:DIRECT consensus=5277 before_collateral=5277 final=514
INF weight_pipeline epoch=881 addr=…kyc325 modes=7B:NONE,4B:DIRECT     consensus=2159 before_collateral=2159 final=431
INF weight_pipeline epoch=881 addr=…2f3a2y modes=7B:DELEGATE,4B:DIRECT consensus=5202 before_collateral=5202 final=514

What the logs show:

  • weight_group:
  • The 7B cap of 1280 is 0.75 × 1707, and 1707 is epoch 880's final total (cappedTotalPower).
  • It is compared against raw_total=8858, which is in pre-collateral units, so 7B is scaled to 14.45%.
  • weight_pipeline, 7B hosts: none of them have collateral. After the cap, collateral cuts each one again to 20%, e.g. pcgs0y 492 → 98.
  • The 7B final total is 255 of 1714, i.e. 14.9%, which matches cap_factor × base_weight_ratio = 0.75 × 0.2.
  • Power capping: originalTotalPower=6943 → cappedTotalPower=1714 combines collateral and the 30% power cap. That 1714 then becomes the next epoch's cap budget.
  • Code comment: the one on emitWeightPipelineLogs says the gap between before_collateral and final is "collateral adjustment and power-cap (both rarely active)". Here both are active every epoch.

The per-participant collateral breakdown (Adjusted participant weight by collateral) is logged at debug level, so it is not visible at the default log level. Raising it to info would make this issue easier to spot on mainnet.

Simulated fix

I fed epoch 881's per-node PoC weights through the chain's own ComputeConsensusWeights and ApplyPowerCappingForWeights. The current pipeline reproduces the on-chain finals exactly. With the cap computed in matching units, either by applying it after collateral or by comparing against N-1 pre-collateral weights:

7B final 4B final 7B share
current (on chain) 255 1459 14.9%
fixed, first epoch (cap after collateral) 1278 2670 32.4%
fixed, steady state (both fixes) 1770 2881 38.1%

At steady state the 7B cap no longer binds (1770 < 0.75 × 4651).


🔄 Auto-synced from Issue #2217 every hour.