fix(x/inference): derive pricing capacity from per-model throughput per tokenomics-v2 spec#1452
Open
len5ky wants to merge 1 commit into
Open
fix(x/inference): derive pricing capacity from per-model throughput per tokenomics-v2 spec#1452len5ky wants to merge 1 commit into
len5ky wants to merge 1 commit into
Conversation
…er tokenomics-v2 spec Defect: CacheAllModelCapacities used TotalWeight (raw PoC nonce weight) as tokens/s capacity, calibrated once against QwQ-32B. Per-nonce throughput is strongly model-dependent (registry throughput_per_nonce), so the 0.40–0.60 stability band meant different true utilization per model. EpochGroupData.total_throughput and the sum over MLNodeInfo.Throughput already existed, but Throughput was never assigned so the sum stayed 0. Spec: proposals/tokenomics-v2/dynamic-pricing.md §2.2.3 — cache capacity from each model subgroup's total_throughput at epoch activation. Population site (analysis A): epochgroup.updateEpochGroupWithNewMember, via populateNodeThroughputs immediately before the TotalThroughput sum. - Has ModelKeeper.GetGovernanceModel (ThroughputPerNonce, UnitsOfComputePerToken). - Runs after final model assignment and immediately before the existing sum. - Least plumbing: no new keeper wiring; chainvalidation lacks Model access and would bake the wrong model if nodes are later reassigned; model_assignment's TODO is earlier and would need dual paths (with/without hardware) plus risk of affecting interim Throughput readers before subgroup formation. UNITS ASSUMPTION: throughput_per_nonce = compute-units/s per nonce of PoC weight; units_of_compute_per_token converts to tokens/s: Throughput = PocWeight * ThroughputPerNonce / UnitsOfComputePerToken (integer, overflow-safe via math/big; zero params → Throughput 0). Fallback/rollout: if TotalThroughput == 0, keep TotalWeight proxy including default-1000. Capacities re-cache each epoch; node throughput populates at next formation — self-activates one epoch after coordinated upgrade; pre-upgrade and unregistered models behave identically. Consensus-breaking: EpochGroupData.TotalThroughput and MLNodeInfo.Throughput become non-zero for models with registry params, changing cached pricing capacity and thus utilization/price trajectories. Requires coordinated upgrade. Reader impact (verified): the only non-pricing readers of MLNodeInfo.Throughput are the dedup sort tiebreaker compareMLNodePreference in module/model_assignment.go and log-only helpers. compareMLNodePreference breaks ties on Throughput only after PocWeight is equal. Its dedupMLNodesById call site (model_assignment.go:404) runs on originalMLNodes, a per-participant slice built by flattening p.MlNodes across ALL of that participant's models, so two entries can share a NodeId with equal raw PocWeight but different per-model k — hence now-different Throughput where they previously tied at 0. Selection and ordering are still unchanged: two such duplicate copies differ only in Throughput (overwritten at formation by populateNodeThroughputs) and TimeslotAllocation (reset to [true,false] at model_assignment.go:416), so whichever copy the tiebreaker keeps is functionally identical downstream. EpochGroupData. TotalThroughput has no consumers besides the epoch_group writer and the new capacity read. Populated Throughput also persists in ValidationWeights and is copied into next-epoch node rebuilds — the same reasoning applies there, so ordering is likewise unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fix(x/inference): derive pricing capacity from per-model throughput per tokenomics-v2 spec
Part 2 of 2 for #1450 — replace the raw
TotalWeightpricing-capacity proxywith per-model throughput derived from the model registry.
Dynamic pricing utilization =
load / (capacity × 5s).CacheAllModelCapacities(
inference-chain/x/inference/keeper/dynamic_pricing.go) setscapacity := modelEpochData.TotalWeight— raw PoC nonce weight treated astokens/second, calibrated once against QwQ-32B. Per-nonce throughput varies
strongly by model (~7× across the three currently served), so a single
weight × 1constant puts the 0.40–0.60 stability band at a different (andunknown) true utilization for every model. This PR derives capacity from the
registry's per-model throughput instead, per
tokenomics-v2/dynamic-pricing.md§2.2.3.
Why this is needed — analysis excerpt
The formula. Per model, per block:
The numerator is a rolling ~60s average of real
prompt + completiontokens ofcompleted inferences of that model — fine. The denominator is a proxy the code
itself flags as temporary:
So the ratio assumes 1 PoC nonce ≈ 1 token/s, calibrated on QwQ-32B, applied
uniformly to every model. No per-model normalization enters this weight upstream:
a sub-group's member weight is the sum of raw
PocWeight(the code comment says"no coefficient"), and the per-model
weight_scale_factorapplies only to rewardsand consensus voting weight, never to the pricing denominator.
The on-chain registry says the constant can't be right. Each model carries a
per-model
throughput_per_nonce(mainnet values via.../inference/models_all):throughput_per_nonceMiniMaxAI/MiniMax-M2.7moonshotai/Kimi-K2.6zai-org/GLM-5.2-FP8A 7× spread across the three served models. The same nonce buys very different
token throughput per model, so the
weight × 1constant makes the stability bandsit at a different true hardware utilization for each.
A second on-chain conversion disagrees by ~12–16×. The devshard gateway's
admission control converts the same raw weight into serving capacity at 5
concurrent requests per 10,000 weight per devshard instance
(
devshard/cmd/devshardctl/gateway_limiter.go); across uncoordinated shards theworkable network total is roughly 32 concurrent per 10,000 weight (a rough Gonka-
team figure, not on-chain). At ~20–26 tok/s decode per Kimi stream that is
~640–830 tok/s per 10,000 weight, while the pricing keeper assumes 10,000 tok/s
for the same weight — a ~12–16× disagreement, both conversions model-uniform.
Kimi K2.6 concretely (live, epoch 326, 2026-07-14): sub-group
TotalWeightis95,298, so the keeper assumes ~95k tok/s. The price stops falling only above
~38k tok/s (U = 0.4) and rises only above ~57k tok/s (U = 0.6). Two independent
estimates of the fleet's real sustainable throughput land far below: a hardware
estimate (~65 B200-class GPUs, ~12–36k tok/s decode, overstatement ~3–8×) and a
concurrency estimate (~300 admittable streams × ~20–26 tok/s ≈ 6–8k tok/s,
overstatement ~12–16×). Either way the decode ceiling sits below the 0.40 lower
bound, so under organic decode-bound demand Kimi's utilization can never reach the
stability zone — the mechanism can only lower its price. (Sub-group weight is also
volatile, 38k–95k across recent epochs, so the thresholds swing ~2.5× per epoch.)
Caveat (both directions). Load counts
prompt + completionequally and prefillis far cheaper than decode, so a prompt-heavy workload can post completed-token
rates above decode throughput — the thresholds are unreachable for real demand yet
reachable by deliberate prompt-stuffing, making the signal manipulable upward.
Separately, rejected demand (429s under load) never becomes a completed inference,
so genuine congestion is invisible to this utilization signal.
Units are unpinned. The spec does not pin the units of
throughput_per_nonce(tokens/s per nonce? per 1000 nonces?). Whatever they are, the same nonce clearly
buys different throughput per model, so the fix must carry the per-model factor
through; this PR's specific unit reading is flagged for confirmation below.
Spec alignment
proposals/tokenomics-v2/dynamic-pricing.md§2.2.3: at epoch activation thesystem copies each model subgroup's
total_throughputfield into thecapacity/{model_id}KV store. This PR implements exactly that (and the twoin-tree TODOs at
model_assignment.goanddynamic_pricing.go), nothing more.The fix is half-built already:
EpochGroupData.TotalThroughputexists andepoch_group.goalready sumsTotalThroughput += node.Throughput— butMLNodeInfo.Throughputis never assigned, so the sum is always 0 and thecapacity path falls back to
TotalWeight.Implementation
epochgroup/throughput.go, new):populateNodeThroughputsis called inepochgroup.updateEpochGroupWithNewMember, right after node selection andbefore the existing per-model
TotalThroughputaccumulation. This site hasModelKeeper.GetGovernanceModelaccess, runs after final model assignment,and needs no new keeper plumbing (chainvalidation lacks Model access;
model_assignment's TODO is earlier and would bake the wrong model on
reassignment).
computeMLNodeThroughput, pure integer,math/big):Throughput = PocWeight × ThroughputPerNonce / UnitsOfComputePerToken.Zero/missing params → 0; products over int64 saturate to
math.MaxInt64instead of wrapping.
Throughputis overwritten eachformation, including back to 0 for zero-param models. A node preserved from a
prior epoch carries last epoch's computed value on its pointer; without an
unconditional overwrite that stale nonzero would leak into
TotalThroughputand silently defeat the
TotalWeightfallback for legacy/zero-param models.(This is guarded by a dedicated test.)
CacheAllModelCapacities):TotalThroughput > 0→ elseTotalWeightproxy → else default1000. TheQwQ context in the comment is compressed, not deleted.
Units assumption — please confirm
The spec does not pin the units of
throughput_per_nonce. This PR reads it ascompute-units/second per nonce of PoC weight, with
units_of_compute_per_tokenconverting to tokens/second. This uses onlyexisting fields, is dimensionally consistent, and yields sane magnitudes:
Kimi K2.6 at 95,298 subgroup weight → 14,294 tokens/s (vs 95,298 under the
weight proxy). If the intended units differ, this is a one-line change in
computeMLNodeThroughput; the rest of the wiring is unaffected. Flaggingprominently because the whole calibration depends on this reading.
Reader impact
Non-pricing readers of
MLNodeInfo.Throughputare only the dedup tiebreakercompareMLNodePreference(module/model_assignment.go) and log-only helpers.The tiebreaker breaks ties on
Throughputonly afterPocWeightis equal;its
dedupMLNodesByIdcall operates on per-participant duplicates that differonly in
Throughput(overwritten at formation) andTimeslotAllocation(resetdownstream), so whichever copy it keeps is functionally identical — selection
and ordering are unchanged.
EpochGroupData.TotalThroughputhas no consumerbesides the epoch_group writer and the new capacity read. Populated Throughput
also persists in
ValidationWeightsand is copied into next-epoch rebuilds; thesame reasoning holds there.
Rollout safety
Capacities re-cache each epoch; node throughput populates at the next formation.
The fix self-activates one epoch after a coordinated upgrade. Until then,
and for any model without registry data, the
TotalWeightfallback preservescurrent behavior exactly.
Tests
throughput_test.go(normal case 95298×1500/10000=14294, each zero-param path,negative weight, int64-bound saturation, large-in-range), the single-member
subgroup sum (
TotalThroughputnow nonzero after formation), the stale-valueoverwrite guard, and
dynamic_pricing_test.gocapacity-selection(TotalThroughput > TotalWeight > default 1000).
Consensus impact
TotalThroughputandMLNodeInfo.Throughputbecome non-zero for models withregistry params, changing cached capacity and thus utilization/price
trajectories. Consensus-breaking; requires a coordinated upgrade.
Out of scope (disclosed)
TotalThroughputaccumulation across successiveupdateEpochGroupWithNewMembercalls is not directly unit-tested (asingle-member, two-node sum plus capacity selection are tested separately).
int64TotalThroughput +=sum is not overflow-saturated(only the per-node compute is); bounded far below int64 in practice.
throughput data is absent — inherent to the spec's fallback design.
moveUpcomingToEffectiveGroup— pre-existing timing semantics, unchanged.🤖 Generated with Claude Code