Skip to content

fix(x/inference): ceil dynamic price increases so sub-50 prices can rise#1451

Open
len5ky wants to merge 1 commit into
gonka-ai:mainfrom
len5ky:fix/pricing-increase-truncation
Open

fix(x/inference): ceil dynamic price increases so sub-50 prices can rise#1451
len5ky wants to merge 1 commit into
gonka-ai:mainfrom
len5ky:fix/pricing-increase-truncation

Conversation

@len5ky

@len5ky len5ky commented Jul 14, 2026

Copy link
Copy Markdown

fix(x/inference): ceil price increases so sub-50 prices can rise

Part 1 of 2 for #1450 — ceil the dynamic-pricing increase branch so a price
that has decayed below 50 ngonka can rise again under load.

Fix

Increase branch only (CalculateModelDynamicPrice,
inference-chain/x/inference/keeper/dynamic_pricing.go):

newPrice = uint64(newPriceDec.Ceil().IntPart())

Any block above the stability zone now raises the price by at least the integer
quantum (1). For p ≥ 50 behavior is nearly identical (ceil vs floor differ by
≤1); exact cap multiples are unchanged (1000 × 1.02 = 1020.0 → 1020). The
decrease branch stays floored: its floor guarantees downward progress
(floor(2 × 0.98) = 1), and ceiling it would create a mirror-image downward
trap where a small price could never fall (ceil(2 × 0.98) = 2).

Why this is needed — analysis excerpt

CalculateModelDynamicPrice computes the increased price as
uint64(newPriceDec.IntPart()). IntPart() truncates toward zero. The maximum
increase per block is ×1.02 (elasticity 0.05 × max excess 0.40), so
floor(p × 1.02) > p requires p ≥ 50:

p = 1:   1 × 1.02 = 1.02  → IntPart → 1
p = 49: 49 × 1.02 = 49.98 → IntPart → 49
p = 50: 50 × 1.02 = 51.0  → IntPart → 51   (first price that can move up)

So the increase branch is a no-op at any utilization once a price decays below
50. The MinPerTokenPrice clamp runs after truncation, so with
min_per_token_price = 1 it does not rescue anything. Any stored price in 1–49
is a one-way trap: it can fall in but never climb out through utilization alone.
The only other writers of the price map (the grace-period initializer and the
missing-price → base_per_token_price fallback) don't apply to an already-priced
model after the grace boundary, so escaping the trap otherwise requires a param
change, a code change, or a state migration.

How mainnet got there. After the grace boundary, with
base_per_token_price = 100, stability_zone_lower_bound = 0.40 and demand below
the stability zone, the price decays at up to −2%/block — and truncation makes it
faster than the continuous math suggests: 100 falls below the p=50 trap line in
26 blocks (≈2 min) and reaches the floor of 1 within 74 blocks (≈6 min; below 50,
floor(0.98p) loses a full 1 every block). grace_period_end_epoch = 90 and the
chain is past epoch 300, so this happened long ago.

Mainnet evidence. GET .../inference/all_model_per_token_prices on
node3.gonka.ai returns "price": "1" for all 8 models (incl.
moonshotai/Kimi-K2.6, zai-org/GLM-5.2-FP8, MiniMaxAI/MiniMax-M2.7), verified
live 2026-07-14. RecordInferencePrice locks this price onto every inference at
start/finish, so all mainnet inference settles at 1/100th of the intended base
price and cannot recover under any demand until intervention.

Why ceil, not round-half-up: round(1 × 1.02) = round(1.02) = 1. Rounding
only shrinks the trap to p < 25; it does not eliminate it. Only ceil guarantees
a strict +1 for every sub-cap increase.

Tests

dynamic_pricing_test.go, table-driven, added cases:

  • currentPrice=1, U=1.0 → 2 (trap escape; was 1 pre-fix)
  • 49 → 50, 50 → 51, 1000 → 1020 (cap intact), 1000 @ U=0.61 → 1001
    (sub-quantum rounds up), 0 → 1 (literal-zero stored price clamps to min).
  • A monotonicity guard (newPrice > oldPrice on every increase case) and an
    independent quantum property (any 0 < oldPrice < 50 under the 2% cap must
    rise by exactly one quantum) — both derived from the trap arithmetic, not from
    the keeper's own expression, so a regression to plain floor or round-half-up
    fails the suite.

Consensus impact

UpdateDynamicPricing runs for every model in BeginBlock, so this changes
on-chain price evolution and is consensus-breaking. It must ship in a
coordinated network upgrade. After this fix, prices climb from their current
floor values under demand (≈+1/block initially at 1 ngonka); if the team prefers
a reset to base_per_token_price at upgrade time, that would be a separate
param/state decision outside this PR.

Out of scope (disclosed)

  • uint64 conversion wrap at astronomical pricesuint64(...IntPart())
    is undefined once the decimal exceeds int64. Pre-existing; unchanged by this
    patch (the cast was already there).
  • No utilization > 1.0 clamp test — at U = 1.0 the factor is exactly the
    cap (1.02), so the > maxIncreasePerBlock clamp binds only above U = 1.0;
    that strict-clamp path is not separately exercised.
  • Decrease-branch truncation exceeding the −2% cap at tiny prices
    floor(2 × 0.98) = 1 is a −50% step, far past the relative −2% cap.
    Pre-existing; the decrease branch is intentionally left floored (see above).

🤖 Generated with Claude Code

CalculateModelDynamicPrice computed the increased price with
uint64(newPriceDec.IntPart()), which truncates toward zero. At the
max +2%/block cap, floor(p*1.02) = p for every currentPrice <= 49, so
the increase branch is a no-op at any utilization once a price decays
below 50 — the price can never rise again. Mainnet evidence: all 8
models are pinned at 1 ngonka (the min price), verified live 2026-07-14.

Fix: quantize upward in the increase branch only, using
newPriceDec.Ceil().IntPart(). Any block above the stability zone now
raises the price by at least the integer quantum (1). For prices >= 50
behavior is nearly identical (ceil vs floor differ by <=1), and exact
cap multiples (e.g. 1000*1.02 = 1020.0) are unchanged.

Ceil, not round-half-up: round(1*1.02) = 1 would only shrink the trap
to p < 25, not eliminate it. The decrease branch is left untouched —
its floor guarantees downward progress, and ceiling it would create a
mirror-image downward trap.

This changes on-chain price evolution and is consensus-breaking; it
must ship in a coordinated network upgrade.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@qdanik

qdanik commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

@len5ky we're currently handling an inference through the gateway. This way is deprecated for now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants