Skip to content

Commit

Permalink
Features/gpu pricing endpoint (#142)
Browse files Browse the repository at this point in the history
* 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
Redm4x authored Apr 1, 2024
1 parent bea8819 commit 728416d
Show file tree
Hide file tree
Showing 10 changed files with 825 additions and 281 deletions.
301 changes: 22 additions & 279 deletions api/src/routers/internalRouter.ts
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));
Loading

0 comments on commit 728416d

Please sign in to comment.