From 39f89a61bc6268f94a80684014ca5a135c30473e Mon Sep 17 00:00:00 2001 From: Redm4x <2829180+Redm4x@users.noreply.github.com> Date: Tue, 9 Apr 2024 19:12:11 -0400 Subject: [PATCH 1/3] Cap allocated to allocatable --- api/src/routes/internal/gpuPrices.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/routes/internal/gpuPrices.ts b/api/src/routes/internal/gpuPrices.ts index e45036466..1ba0b5255 100644 --- a/api/src/routes/internal/gpuPrices.ts +++ b/api/src/routes/internal/gpuPrices.ts @@ -328,7 +328,7 @@ async function getGpus() { WHERE p."isOnline" IS TRUE ORDER BY p."hostUri", p."createdHeight" DESC ) - SELECT s."hostUri", s."owner", n."name", n."gpuAllocatable" AS allocatable, n."gpuAllocated" AS allocated, gpu."modelId", gpu.vendor, gpu.name AS "modelName", gpu.interface, gpu."memorySize" + SELECT s."hostUri", s."owner", n."name", n."gpuAllocatable" AS allocatable, LEAST(n."gpuAllocated", n."gpuAllocatable") AS allocated, gpu."modelId", gpu.vendor, gpu.name AS "modelName", gpu.interface, gpu."memorySize" FROM snapshots s INNER JOIN "providerSnapshotNode" n ON n."snapshotId"=s.id AND n."gpuAllocatable" > 0 LEFT JOIN ( From 479d608e49551648a3d4e1a66980f54f1e336c10 Mon Sep 17 00:00:00 2001 From: Redm4x <2829180+Redm4x@users.noreply.github.com> Date: Thu, 11 Apr 2024 14:29:10 -0400 Subject: [PATCH 2/3] Add weighted average + fix median --- api/src/routes/internal/gpuPrices.ts | 101 ++++++++++++++++++--------- api/src/utils/math.ts | 30 ++++++++ 2 files changed, 97 insertions(+), 34 deletions(-) diff --git a/api/src/routes/internal/gpuPrices.ts b/api/src/routes/internal/gpuPrices.ts index 1ba0b5255..a2437cc0d 100644 --- a/api/src/routes/internal/gpuPrices.ts +++ b/api/src/routes/internal/gpuPrices.ts @@ -6,7 +6,7 @@ import { cacheResponse } from "@src/caching/helpers"; import { chainDb } from "@src/db/dbConnection"; import { MsgCreateBid } from "@src/proto/akash/v1beta4"; import { averageBlockCountInAMonth, averageBlockCountInAnHour } from "@src/utils/constants"; -import { round } from "@src/utils/math"; +import { average, median, round, weightedAverage } from "@src/utils/math"; import { decodeMsg, uint8arrayToString } from "@src/utils/protobuf"; import { addDays } from "date-fns"; import { Op, QueryTypes } from "sequelize"; @@ -44,6 +44,7 @@ const route = createRoute({ min: z.number(), max: z.number(), avg: z.number(), + weightedAverage: z.number(), med: z.number() }) }) @@ -204,8 +205,6 @@ async function getGpuPrices(debug: boolean) { available: totalAllocatable - totalAllocated }, models: gpuModels.map((x) => { - x.prices.sort((a, b) => a.hourlyPrice - b.hourlyPrice); - /* For each providers get their most relevent bid based on this order of priority: 1- Most recent bid from the pricing bot (those deployment have tiny cpu/ram/storage specs to improve gpu price accuracy) @@ -214,23 +213,27 @@ async function getGpuPrices(debug: boolean) { 4- Cheapest remaining bid 5- If no bids are found, increase search range from 14 to 31 days and repeat steps 2-4 */ - const bestProviderBids = x.providers + const providersWithBestBid = x.providers .map((p) => { const providerBids = x.prices.filter((b) => b.provider === p.owner); const providerBidsLast14d = providerBids.filter((x) => x.datetime > addDays(new Date(), -14)); const pricingBotAddress = "akash1pas6v0905jgyznpvnjhg7tsthuyqek60gkz7uf"; const bidsFromPricingBot = providerBids.filter((x) => x.deployment.owner === pricingBotAddress && x.deployment.cpuUnits === 100); - - if (bidsFromPricingBot.length > 0) return bidsFromPricingBot.sort((a, b) => b.height - a.height)[0]; - return findBestProviderBid(providerBidsLast14d, x) ?? findBestProviderBid(providerBids, x); + let bestBid = null; + if (bidsFromPricingBot.length > 0) { + bestBid = bidsFromPricingBot.sort((a, b) => b.height - a.height)[0]; + } else { + bestBid = findBestProviderBid(providerBidsLast14d, x) ?? findBestProviderBid(providerBids, x); + } + + return { + provider: p, + bestBid: bestBid + }; }) - .filter((x) => x) - .sort((a, b) => a.hourlyPrice - b.hourlyPrice); - - // Sort provider bids by price for the median calculation - const sortedPrices = bestProviderBids.map((x) => x.hourlyPrice); + .filter((x) => x.bestBid); return { vendor: x.vendor, @@ -246,21 +249,39 @@ async function getGpuPrices(debug: boolean) { available: x.availableProviders.length, providers: debug ? x.providers : undefined }, - price: { - currency: "USD", - min: Math.min(...sortedPrices), - max: Math.max(...sortedPrices), - avg: round(sortedPrices.reduce((a, b) => a + b, 0) / sortedPrices.length, 2), - med: sortedPrices[Math.floor(sortedPrices.length / 2)] - }, + price: getPricing(providersWithBestBid), bidCount: debug ? x.prices.length : undefined, - bids: debug ? x.prices : undefined, - bestProviderBids: debug ? bestProviderBids : undefined + providersWithBestBid: debug ? providersWithBestBid : undefined }; }) }; } +function getPricing( + providersWithBestBid: { + provider: GpuProviderType; + bestBid: GpuBidType; + }[] +) { + try { + if (!providersWithBestBid || providersWithBestBid.length === 0) return null; + + const prices = providersWithBestBid.map((x) => x.bestBid.hourlyPrice); + + return { + currency: "USD", + min: Math.min(...prices), + max: Math.max(...prices), + avg: round(average(prices), 2), + weightedAverage: round(weightedAverage(providersWithBestBid.map((p) => ({ value: p.bestBid.hourlyPrice, weight: p.provider.allocatable }))), 2), + med: round(median(prices), 2) + }; + } catch (e) { + console.error("Error calculating pricing", e); + return null; + } +} + function findBestProviderBid(providerBids: GpuBidType[], gpuModel: GpuWithPricesType) { const providerBidsWithRamAndInterface = providerBids.filter( (b) => b.deployment.gpus[0].ram === gpuModel.ram && isInterfaceMatching(gpuModel.interface, b.deployment.gpus[0].interface) @@ -346,20 +367,25 @@ async function getGpus() { const gpus: GpuType[] = []; for (const gpuNode of gpuNodes) { - const existing = gpus.find( + const nodeInfo = { owner: gpuNode.owner, hostUri: gpuNode.hostUri, allocated: gpuNode.allocated, allocatable: gpuNode.allocatable }; + + const existingGpu = gpus.find( (x) => x.vendor === gpuNode.vendor && x.model === gpuNode.modelName && x.interface === gpuNode.interface && x.ram === gpuNode.memorySize ); - if (existing) { - existing.allocatable += gpuNode.allocatable; - existing.allocated += gpuNode.allocated; + if (existingGpu) { + existingGpu.allocatable += gpuNode.allocatable; + existingGpu.allocated += gpuNode.allocated; - if (!existing.providers.some((p) => p.hostUri === gpuNode.hostUri)) { - existing.providers.push({ owner: gpuNode.owner, hostUri: gpuNode.hostUri }); - } - if (gpuNode.allocated < gpuNode.allocatable && !existing.availableProviders.some((p) => p.hostUri === gpuNode.hostUri)) { - existing.availableProviders.push({ owner: gpuNode.owner, hostUri: gpuNode.hostUri }); + const existingProvider = existingGpu.providers.find((p) => p.hostUri === gpuNode.hostUri); + if (!existingProvider) { + existingGpu.providers.push(nodeInfo); + } else { + existingProvider.allocated += gpuNode.allocated; + existingProvider.allocatable += gpuNode.allocatable; } + + existingGpu.availableProviders = existingGpu.providers.filter((p) => p.allocated < p.allocatable); } else { gpus.push({ vendor: gpuNode.vendor, @@ -368,8 +394,8 @@ async function getGpus() { interface: gpuNode.interface, allocatable: gpuNode.allocatable, allocated: gpuNode.allocated, - providers: [{ owner: gpuNode.owner, hostUri: gpuNode.hostUri }], - availableProviders: gpuNode.allocated < gpuNode.allocatable ? [{ owner: gpuNode.owner, hostUri: gpuNode.hostUri }] : [] + providers: [nodeInfo], + availableProviders: gpuNode.allocated < gpuNode.allocatable ? [nodeInfo] : [] }); } } @@ -384,6 +410,13 @@ type GpuType = { ram: string; allocatable: number; allocated: number; - providers: { owner: string; hostUri: string }[]; - availableProviders: { owner: string; hostUri: string }[]; + providers: GpuProviderType[]; + availableProviders: GpuProviderType[]; +}; + +type GpuProviderType = { + owner: string; + hostUri: string; + allocated: number; + allocatable: number; }; diff --git a/api/src/utils/math.ts b/api/src/utils/math.ts index fe370b99d..a9a00e844 100644 --- a/api/src/utils/math.ts +++ b/api/src/utils/math.ts @@ -13,3 +13,33 @@ export function uaktToAKT(amount: number, precision = 2) { export function udenomToDenom(amount: number, precision = 2) { return round(amount / 1_000_000, precision); } + +export function median(values: number[]): number { + if (values.length === 0) { + throw new Error("Input array is empty"); + } + + values = [...values].sort((a, b) => a - b); + + const half = Math.floor(values.length / 2); + + return values.length % 2 === 0 ? (values[half - 1] + values[half]) / 2 : values[half]; +} + +export function average(values: number[]): number { + if (values.length === 0) { + throw new Error("Input array is empty"); + } + + return values.reduce((acc, x) => acc + x, 0) / values.length; +} + +export function weightedAverage(values: { value: number; weight: number }[]): number { + if (values.length === 0) { + throw new Error("Input array is empty"); + } + + const totalWeight = values.map((x) => x.weight).reduce((acc, x) => acc + x, 0); + + return values.map((x) => x.value * x.weight).reduce((acc, x) => acc + x, 0) / totalWeight; +} From de7ddc52f04d4cd79c5f784f24ae277052dd4fa1 Mon Sep 17 00:00:00 2001 From: Redm4x <2829180+Redm4x@users.noreply.github.com> Date: Thu, 11 Apr 2024 14:38:24 -0400 Subject: [PATCH 3/3] Change var name in median calculation --- api/src/utils/math.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/src/utils/math.ts b/api/src/utils/math.ts index a9a00e844..8bee95831 100644 --- a/api/src/utils/math.ts +++ b/api/src/utils/math.ts @@ -19,11 +19,11 @@ export function median(values: number[]): number { throw new Error("Input array is empty"); } - values = [...values].sort((a, b) => a - b); + const sortedValues = [...values].sort((a, b) => a - b); - const half = Math.floor(values.length / 2); + const half = Math.floor(sortedValues.length / 2); - return values.length % 2 === 0 ? (values[half - 1] + values[half]) / 2 : values[half]; + return sortedValues.length % 2 === 0 ? (sortedValues[half - 1] + sortedValues[half]) / 2 : sortedValues[half]; } export function average(values: number[]): number {