-
Notifications
You must be signed in to change notification settings - Fork 56
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Features/gpu pricing endpoint (#142)
* POC * Improve algo * Ignore bids for days without aktPrice * Add monthly price to debug * Move internal routes to their own files + add swagger doc * Remove todo * Add currency property to prices * Add comments to explain gpu pricing logic * Change decodeMsg return type to unknown * Improve doc * Fix lint errors
- Loading branch information
Showing
10 changed files
with
825 additions
and
281 deletions.
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,284 +1,27 @@ | ||
import { Block } from "@shared/dbSchemas"; | ||
import { Lease, Provider } from "@shared/dbSchemas/akash"; | ||
import { cacheKeys, cacheResponse } from "@src/caching/helpers"; | ||
import { chainDb } from "@src/db/dbConnection"; | ||
import { GpuVendor, ProviderConfigGpusType } from "@src/types/gpu"; | ||
import { isValidBech32Address } from "@src/utils/addresses"; | ||
import { getGpuInterface } from "@src/utils/gpu"; | ||
import { round } from "@src/utils/math"; | ||
import axios from "axios"; | ||
import { differenceInSeconds } from "date-fns"; | ||
import { Hono } from "hono"; | ||
import * as semver from "semver"; | ||
import { Op, QueryTypes } from "sequelize"; | ||
|
||
export const internalRouter = new Hono(); | ||
|
||
internalRouter.get("/provider-versions", async (c) => { | ||
const providers = await Provider.findAll({ | ||
attributes: ["hostUri", "akashVersion"], | ||
where: { | ||
isOnline: true | ||
}, | ||
group: ["hostUri", "akashVersion"] | ||
}); | ||
|
||
const grouped: { version: string; providers: string[] }[] = []; | ||
|
||
for (const provider of providers) { | ||
const existing = grouped.find((x) => x.version === provider.akashVersion); | ||
|
||
if (existing) { | ||
existing.providers.push(provider.hostUri); | ||
} else { | ||
grouped.push({ | ||
version: provider.akashVersion, | ||
providers: [provider.hostUri] | ||
}); | ||
} | ||
} | ||
|
||
const nullVersionName = "<UNKNOWN>"; | ||
const results = grouped.map((x) => ({ | ||
version: x.version ?? nullVersionName, | ||
count: x.providers.length, | ||
ratio: round(x.providers.length / providers.length, 2), | ||
providers: Array.from(new Set(x.providers)) | ||
})); | ||
|
||
const sorted = results | ||
.filter((x) => x.version !== nullVersionName) // Remove <UNKNOWN> version for sorting | ||
.sort((a, b) => semver.compare(b.version, a.version)) | ||
.concat(results.filter((x) => x.version === nullVersionName)) // Add back <UNKNOWN> version at the end | ||
.reduce( | ||
(acc, x) => { | ||
acc[x.version] = x; | ||
return acc; | ||
}, | ||
{} as { [key: string]: (typeof results)[number] } | ||
); | ||
|
||
return c.json(sorted); | ||
}); | ||
|
||
internalRouter.get("/gpu", async (c) => { | ||
const provider = c.req.query("provider"); | ||
const vendor = c.req.query("vendor"); | ||
const model = c.req.query("model"); | ||
const memory_size = c.req.query("memory_size"); | ||
|
||
let provider_address = null; | ||
let provider_hosturi = null; | ||
|
||
if (provider) { | ||
if (isValidBech32Address(provider)) { | ||
provider_address = provider; | ||
} else if (URL.canParse(provider)) { | ||
provider_hosturi = provider; | ||
} else { | ||
return c.json({ error: "Invalid provider parameter, should be a valid akash address or host uri" }, 400); | ||
} | ||
} | ||
|
||
const gpuNodes = (await chainDb.query( | ||
` | ||
WITH snapshots AS ( | ||
SELECT DISTINCT ON("hostUri") | ||
ps.id AS id, | ||
"hostUri", | ||
p."owner" | ||
FROM provider p | ||
INNER JOIN "providerSnapshot" ps ON ps.id=p."lastSnapshotId" | ||
WHERE p."isOnline" IS TRUE | ||
) | ||
SELECT s."hostUri", n."name", n."gpuAllocatable" AS allocatable, n."gpuAllocated" 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 ( | ||
SELECT DISTINCT ON (gpu."snapshotNodeId") gpu.* | ||
FROM "providerSnapshotNodeGPU" gpu | ||
) gpu ON gpu."snapshotNodeId" = n.id | ||
WHERE | ||
(:vendor IS NULL OR gpu.vendor = :vendor) | ||
AND (:model IS NULL OR gpu.name = :model) | ||
AND (:memory_size IS NULL OR gpu."memorySize" = :memory_size) | ||
AND (:provider_address IS NULL OR s."owner" = :provider_address) | ||
AND (:provider_hosturi IS NULL OR s."hostUri" = :provider_hosturi) | ||
`, | ||
{ | ||
type: QueryTypes.SELECT, | ||
replacements: { | ||
vendor: vendor ?? null, | ||
model: model ?? null, | ||
memory_size: memory_size ?? null, | ||
provider_address: provider_address ?? null, | ||
provider_hosturi: provider_hosturi ?? null | ||
} | ||
} | ||
)) as { | ||
hostUri: string; | ||
name: string; | ||
allocatable: number; | ||
allocated: number; | ||
modelId: string; | ||
vendor: string; | ||
modelName: string; | ||
interface: string; | ||
memorySize: string; | ||
}[]; | ||
|
||
const response = { | ||
gpus: { | ||
total: { | ||
allocatable: gpuNodes.map((x) => x.allocatable).reduce((acc, x) => acc + x, 0), | ||
allocated: gpuNodes.map((x) => x.allocated).reduce((acc, x) => acc + x, 0) | ||
}, | ||
details: {} as { [key: string]: { model: string; ram: string; interface: string; allocatable: number; allocated: number }[] } | ||
} | ||
}; | ||
|
||
for (const gpuNode of gpuNodes) { | ||
const vendorName = gpuNode.vendor ?? "<UNKNOWN>"; | ||
if (!(vendorName in response.gpus.details)) { | ||
response.gpus.details[vendorName] = []; | ||
} | ||
|
||
const existing = response.gpus.details[vendorName].find( | ||
(x) => x.model === gpuNode.modelName && x.interface === gpuNode.interface && x.ram === gpuNode.memorySize | ||
); | ||
|
||
if (existing) { | ||
existing.allocatable += gpuNode.allocatable; | ||
existing.allocated += gpuNode.allocated; | ||
} else { | ||
response.gpus.details[vendorName].push({ | ||
model: gpuNode.modelName, | ||
ram: gpuNode.memorySize, | ||
interface: gpuNode.interface, | ||
allocatable: gpuNode.allocatable, | ||
allocated: gpuNode.allocated | ||
}); | ||
} | ||
} | ||
|
||
return c.json(response); | ||
}); | ||
|
||
internalRouter.get("leases-duration/:owner", async (c) => { | ||
const dateFormat = /^\d{4}-\d{2}-\d{2}$/; | ||
|
||
let startTime: Date = new Date("2000-01-01"); | ||
let endTime: Date = new Date("2100-01-01"); | ||
|
||
const { dseq, startDate, endDate } = c.req.query(); | ||
|
||
if (dseq && isNaN(parseInt(dseq))) { | ||
return c.text("Invalid dseq", 400); | ||
} | ||
|
||
if (startDate) { | ||
if (!startDate.match(dateFormat)) return c.text("Invalid start date, must be in the following format: YYYY-MM-DD", 400); | ||
|
||
const startMs = Date.parse(startDate); | ||
|
||
if (isNaN(startMs)) return c.text("Invalid start date", 400); | ||
|
||
startTime = new Date(startMs); | ||
} | ||
|
||
if (endDate) { | ||
if (!endDate.match(dateFormat)) return c.text("Invalid end date, must be in the following format: YYYY-MM-DD", 400); | ||
|
||
const endMs = Date.parse(endDate); | ||
|
||
if (isNaN(endMs)) return c.text("Invalid end date", 400); | ||
|
||
endTime = new Date(endMs); | ||
import { isProd } from "@src/utils/constants"; | ||
import { OpenAPIHono } from "@hono/zod-openapi"; | ||
import { swaggerUI } from "@hono/swagger-ui"; | ||
import routes from "../routes/internal"; | ||
|
||
export const internalRouter = new OpenAPIHono(); | ||
|
||
const servers = [{ url: `https://api.cloudmos.io/internal`, description: "Production" }]; | ||
if (!isProd) { | ||
servers.unshift({ url: `http://localhost:3080/internal`, description: "Localhost" }); | ||
} | ||
|
||
internalRouter.doc(`/doc`, { | ||
openapi: "3.0.0", | ||
servers: servers, | ||
info: { | ||
title: "Cloudmos Internal API", | ||
description: "APIs for internal use that are not part of the public API. There is no garantees of stability or backward compatibility.", | ||
version: "test" | ||
} | ||
|
||
if (endTime <= startTime) { | ||
return c.text("End time must be greater than start time", 400); | ||
} | ||
|
||
const closedLeases = await Lease.findAll({ | ||
where: { | ||
owner: c.req.param("owner"), | ||
closedHeight: { [Op.not]: null }, | ||
"$closedBlock.datetime$": { [Op.gte]: startTime, [Op.lte]: endTime }, | ||
...(dseq ? { dseq: dseq } : {}) | ||
}, | ||
include: [ | ||
{ model: Block, as: "createdBlock" }, | ||
{ model: Block, as: "closedBlock" } | ||
] | ||
}); | ||
|
||
const leases = closedLeases.map((x) => ({ | ||
dseq: x.dseq, | ||
oseq: x.oseq, | ||
gseq: x.gseq, | ||
provider: x.providerAddress, | ||
startHeight: x.createdHeight, | ||
startDate: x.createdBlock.datetime, | ||
closedHeight: x.closedHeight, | ||
closedDate: x.closedBlock.datetime, | ||
durationInBlocks: x.closedHeight - x.createdHeight, | ||
durationInSeconds: differenceInSeconds(x.closedBlock.datetime, x.createdBlock.datetime), | ||
durationInHours: differenceInSeconds(x.closedBlock.datetime, x.createdBlock.datetime) / 3600 | ||
})); | ||
|
||
const totalSeconds = leases.map((x) => x.durationInSeconds).reduce((a, b) => a + b, 0); | ||
|
||
return c.json({ | ||
leaseCount: leases.length, | ||
totalDurationInSeconds: totalSeconds, | ||
totalDurationInHours: totalSeconds / 3600, | ||
leases | ||
}); | ||
}); | ||
|
||
internalRouter.get("gpu-models", async (c) => { | ||
const response = await cacheResponse(60 * 2, cacheKeys.getGpuModels, async () => { | ||
const res = await axios.get<ProviderConfigGpusType>("https://raw.githubusercontent.com/akash-network/provider-configs/main/devices/pcie/gpus.json"); | ||
return res.data; | ||
}); | ||
const swaggerInstance = swaggerUI({ url: `/internal/doc` }); | ||
|
||
const gpuModels: GpuVendor[] = []; | ||
internalRouter.get(`/swagger`, swaggerInstance); | ||
|
||
// Loop over vendors | ||
for (const [, vendorValue] of Object.entries(response)) { | ||
const vendor: GpuVendor = { | ||
name: vendorValue.name, | ||
models: [] | ||
}; | ||
|
||
// Loop over models | ||
for (const [, modelValue] of Object.entries(vendorValue.devices)) { | ||
const _modelValue = modelValue as { | ||
name: string; | ||
memory_size: string; | ||
interface: string; | ||
}; | ||
const existingModel = vendor.models.find((x) => x.name === _modelValue.name); | ||
|
||
if (existingModel) { | ||
if (!existingModel.memory.includes(_modelValue.memory_size)) { | ||
existingModel.memory.push(_modelValue.memory_size); | ||
} | ||
if (!existingModel.interface.includes(getGpuInterface(_modelValue.interface))) { | ||
existingModel.interface.push(getGpuInterface(_modelValue.interface)); | ||
} | ||
} else { | ||
vendor.models.push({ | ||
name: _modelValue.name, | ||
memory: [_modelValue.memory_size], | ||
interface: [getGpuInterface(_modelValue.interface)] | ||
}); | ||
} | ||
} | ||
|
||
gpuModels.push(vendor); | ||
} | ||
|
||
return c.json(gpuModels); | ||
}); | ||
routes.forEach((route) => internalRouter.route(`/`, route)); |
Oops, something went wrong.