Preserved nodes carry stale weight indefinitely and ignore node changes (v0.2.17) #2106
bug
Summary
On upgrade-v0.2.17 (checked at fc65e8ec0), a preserved ML node is never re-measured or re-checked once it is preserved. Its PocWeight is copied forward from the current epoch, the same node can be preserved in consecutive episodes, and changes to the node (status, hardware, fresh PoC result) are ignored. A single PoC measurement can therefore be carried indefinitely.
Mechanism (as implemented)
- Sampling:
SamplePreservedForEpisode(x/inference/module/model_assignment.go), at each PoC start and each cPoC trigger. - Candidates are the current epoch's subgroup
ValidationWeights, minus reserved (training) nodes. - Participant gates: live member, not a guardian,
EpochPerformanceSummary.RewardedCoins > 0in the previous epoch, per-participant and IQR weight thresholds. - Selection: seeded shuffle keeps N/2+1 participants, nodes are allocated smallest-first up to
PocSlotAllocation(default 50%), and non-voting power is capped at 34%. - The result is stored in
PreservedNodesSnapshot. - Carry:
PreservedParticipantsFromCurrentEpoch/GetPreservedNodesByParticipant(chainvalidation.go). - The preserved node's
PocWeightis copied from the current epoch's subgroupValidationWeights. - Its PoC distributions are dropped by
filterStoreCommitsFromInferenceNodes. - In
mergeByModelthe preserved entry is added first, so it wins over a PoC entry with the sameNodeId. - cPoC:
evaluateConfirmation(confirmation_poc.go). - The reading is
preserved + measured, wherepreservedis the stored weight of the snapshot's nodes (preservedWeightByParticipant). - Preserved nodes are never tested.
- Only node-level check:
setModelsForParticipantsdrops a carried node at epoch formation only if it is missing fromHardwareNodesor the inventory no longer lists the model.
Problems
- Weight is never re-measured. The value carried into epoch N+1 is whatever epoch N held. If the node was also preserved in N, that value is itself a carry. Nothing limits consecutive preservations or forces a fresh PoC every K epochs. cPoC credits the stored weight, so it cannot catch this either.
- Node state changes are ignored.
HardwareNode.status(STOPPED/FAILED/TRAINING) is not read by the sampler or the carry.- Hardware changes (GPU type or count) have no effect.
- The sampler does not consult
HardwareNodes, so a node removed from the inventory can still be written into the episode snapshot; it is dropped only later, at seating. Related: #310. - A fresh PoC result cannot replace the carried weight. If a preserved node also mined PoC, its distribution is filtered out and the preserved entry wins the merge. A host that upgraded the node is stuck at the old weight, and one whose node degraded keeps it.
- Eligibility is participant-level, not node-level. The only history gate is that the participant was rewarded last epoch. Nothing checks whether the specific node served inference or missed requests. A preserved host is rewarded, so it stays eligible.
Observed on testnet
Over epochs 379–419, every readable snapshot (21 PoC and cPoC episodes) preserved exactly two single-node hosts, rotating among the same four participants (…32vrcn, …kyc325, …pcgs0y, …4sgtd5), always node1. With one node per host, their weight cannot change while they keep being preserved.
Suggested fixes
- Disallow preserving the same node in consecutive episodes, or require a fresh PoC at least every K epochs (track the last measured epoch per node).
- At sampling time, exclude nodes that are missing from
HardwareNodes, have a status other thanINFERENCE/POC, or no longer list the model. - When a preserved node also has a valid fresh PoC distribution, use the fresh weight (or
min(carried, fresh)) instead of always preferring the preserved entry. - Optionally, include preserved nodes in cPoC measurement instead of crediting their stored weight.
Related: #310, #1805.
🔄 Auto-synced from Issue #2106 every hour.