Skip to content

Commit

Permalink
Bugfixes/fix provider gpus UI (#143)
Browse files Browse the repository at this point in the history
* Return gpus in provider endpoints

* Display gpus from feature discovery instead of capabilities

* Add missing indexes

* Change to inner join

* .

* Code improvements

* .

* Add array helper

* Add createFilterUnique to deploy-web project
  • Loading branch information
Redm4x authored Apr 4, 2024
1 parent 88fdd8f commit ec66e7a
Show file tree
Hide file tree
Showing 16 changed files with 154 additions and 34 deletions.
8 changes: 8 additions & 0 deletions api/src/routes/v1/providers/byAddress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ const route = createRoute({
memory: z.number(),
storage: z.number()
}),
gpuModels: z.array(
z.object({
vendor: z.string(),
model: z.string(),
ram: z.string(),
interface: z.string()
})
),
attributes: z.array(
z.object({
key: z.string(),
Expand Down
8 changes: 8 additions & 0 deletions api/src/routes/v1/providers/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ const route = createRoute({
memory: z.number(),
storage: z.number()
}),
gpuModels: z.array(
z.object({
vendor: z.string(),
model: z.string(),
ram: z.string(),
interface: z.string()
})
),
attributes: z.array(
z.object({
key: z.string(),
Expand Down
74 changes: 56 additions & 18 deletions api/src/services/db/providerStatusService.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Provider, ProviderAttribute, ProviderAttributeSignature } from "@shared/dbSchemas/akash";
import { Provider, ProviderAttribute, ProviderAttributeSignature, ProviderSnapshotNode, ProviderSnapshotNodeGPU } from "@shared/dbSchemas/akash";
import { ProviderSnapshot } from "@shared/dbSchemas/akash/providerSnapshot";
import { toUTC } from "@src/utils";
import { add } from "date-fns";
Expand Down Expand Up @@ -42,7 +42,7 @@ export async function getNetworkCapacity() {
}

export const getProviderList = async () => {
const providers = await Provider.findAll({
const providersWithAttributesAndAuditors = await Provider.findAll({
where: {
deletedHeight: null
},
Expand All @@ -56,13 +56,40 @@ export const getProviderList = async () => {
}
]
});
const filteredProviders = providers.filter((value, index, self) => self.map((x) => x.hostUri).lastIndexOf(value.hostUri) === index);

const providerWithNodes = await Provider.findAll({
attributes: ["owner"],
where: {
deletedHeight: null
},
include: [
{
model: ProviderSnapshot,
attributes: ["id"],
required: true,
as: "lastSnapshot",
include: [
{
model: ProviderSnapshotNode,
attributes: ["id"],
required: true,
include: [{ model: ProviderSnapshotNodeGPU, required: true }]
}
]
}
]
});

const distinctProviders = providersWithAttributesAndAuditors.filter((value, index, self) => self.map((x) => x.hostUri).lastIndexOf(value.hostUri) === index);
const providerAttributeSchemaQuery = getProviderAttributesSchema();
const auditorsQuery = getAuditors();

const [auditors, providerAttributeSchema] = await Promise.all([auditorsQuery, providerAttributeSchemaQuery]);

return filteredProviders.map((x) => mapProviderToList(x, providerAttributeSchema, auditors));
return distinctProviders.map((x) => {
const nodes = providerWithNodes.find((p) => p.owner === x.owner)?.lastSnapshot?.nodes;
return mapProviderToList(x, providerAttributeSchema, auditors, nodes);
});
};

export const getProviderDetail = async (address: string): Promise<ProviderDetail> => {
Expand All @@ -78,32 +105,43 @@ export const getProviderDetail = async (address: string): Promise<ProviderDetail
},
{
model: ProviderAttributeSignature
},
{
model: ProviderSnapshot,
as: "providerSnapshots",
attributes: ["isOnline", "id", "checkDate"],
required: false,
separate: true,
where: {
checkDate: {
[Op.gte]: add(nowUtc, { days: -1 })
}
}
}
]
});

if (!provider) return null;

const uptimeSnapshots = await ProviderSnapshot.findAll({
attributes: ["isOnline", "id", "checkDate"],
where: {
owner: provider.owner,
checkDate: {
[Op.gte]: add(nowUtc, { days: -1 })
}
}
});

const lastSnapshot = await ProviderSnapshot.findOne({
where: {
id: provider.lastSnapshotId
},
order: [["checkDate", "DESC"]],
include: [
{
model: ProviderSnapshotNode,
include: [{ model: ProviderSnapshotNodeGPU }]
}
]
});

const providerAttributeSchemaQuery = getProviderAttributesSchema();
const auditorsQuery = getAuditors();

const [auditors, providerAttributeSchema] = await Promise.all([auditorsQuery, providerAttributeSchemaQuery]);

return {
...mapProviderToList(provider, providerAttributeSchema, auditors),
uptime: provider.providerSnapshots.map((ps) => ({
...mapProviderToList(provider, providerAttributeSchema, auditors, lastSnapshot?.nodes),
uptime: uptimeSnapshots.map((ps) => ({
id: ps.id,
isOnline: ps.isOnline,
checkDate: ps.checkDate
Expand Down
1 change: 1 addition & 0 deletions api/src/types/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface ProviderList {
isValidVersion: boolean;
isOnline: boolean;
isAudited: boolean;
gpuModels: { vendor: string; model: string; ram: string; interface: string }[];
activeStats: {
cpu: number;
gpu: number;
Expand Down
19 changes: 19 additions & 0 deletions api/src/utils/array/array.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { createFilterUnique } from "./array";

describe("array helpers", () => {
describe("createFilterUnique", () => {
it("should return a functionning unique filter with default equality matcher", () => {
const arrayWithDuplicate = [1, 2, 2, 3, 3, 3];
const expected = [1, 2, 3];

expect(arrayWithDuplicate.filter(createFilterUnique())).toEqual(expected);
});

it("should return a functionning unique filter with custom matcher", () => {
const arrayWithDuplicate = [{ v: 1 }, { v: 2 }, { v: 2 }, { v: 3 }, { v: 3 }, { v: 3 }];
const expected = [{ v: 1 }, { v: 2 }, { v: 3 }];

expect(arrayWithDuplicate.filter(createFilterUnique((a, b) => a.v === b.v))).toEqual(expected);
});
});
});
7 changes: 7 additions & 0 deletions api/src/utils/array/array.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
type Matcher<T> = (a: T, b: T) => boolean;

export function createFilterUnique<T>(matcher: Matcher<T> = (a, b) => a === b): (value: T, index: number, array: T[]) => boolean {
return (value, index, array) => {
return array.findIndex((other) => matcher(value, other)) === index;
};
}
21 changes: 19 additions & 2 deletions api/src/utils/map/provider.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { Provider } from "@shared/dbSchemas/akash";
import { Provider, ProviderSnapshotNode } from "@shared/dbSchemas/akash";
import { Auditor, ProviderAttributesSchema, ProviderList } from "@src/types/provider";
import { createFilterUnique } from "../array/array";
import semver from "semver";

export const mapProviderToList = (provider: Provider, providerAttributeSchema: ProviderAttributesSchema, auditors: Array<Auditor>): ProviderList => {
export const mapProviderToList = (
provider: Provider,
providerAttributeSchema: ProviderAttributesSchema,
auditors: Array<Auditor>,
nodes?: ProviderSnapshotNode[]
): ProviderList => {
const isValidVersion = provider.cosmosSdkVersion ? semver.gte(provider.cosmosSdkVersion, "v0.45.9") : false;
const name = provider.isOnline ? new URL(provider.hostUri).hostname : null;
const gpuModels = getDistinctGpuModelsFromNodes(nodes || []);

return {
owner: provider.owner,
Expand Down Expand Up @@ -42,6 +49,7 @@ export const mapProviderToList = (provider: Provider, providerAttributeSchema: P
memory: isValidVersion ? provider.availableMemory : 0,
storage: isValidVersion ? provider.availableStorage : 0
},
gpuModels: gpuModels,
uptime1d: provider.uptime1d,
uptime7d: provider.uptime7d,
uptime30d: provider.uptime30d,
Expand Down Expand Up @@ -83,6 +91,15 @@ export const mapProviderToList = (provider: Provider, providerAttributeSchema: P
} as ProviderList;
};

function getDistinctGpuModelsFromNodes(nodes: ProviderSnapshotNode[]) {
const gpuModels = nodes.flatMap((x) => x.gpus).map((x) => ({ vendor: x.vendor, model: x.name, ram: x.memorySize, interface: x.interface }));
const distinctGpuModels = gpuModels.filter(
createFilterUnique((a, b) => a.vendor === b.vendor && a.model === b.model && a.ram === b.ram && a.interface === b.interface)
);

return distinctGpuModels;
}

export const getProviderAttributeValue = (
key: keyof ProviderAttributesSchema,
provider: Provider,
Expand Down
4 changes: 2 additions & 2 deletions deploy-web/src/components/providers/ProviderListRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import { Uptime } from "./Uptime";
import React from "react";
import { hasSomeParentTheClass } from "@src/utils/domUtils";
import { cx } from "@emotion/css";
import CheckIcon from "@mui/icons-material/Check";
import WarningIcon from "@mui/icons-material/Warning";
import { createFilterUnique } from "@src/utils/array";

const useStyles = makeStyles()(theme => ({
root: {
Expand Down Expand Up @@ -62,7 +62,7 @@ export const ProviderListRow: React.FunctionComponent<Props> = ({ provider }) =>
const _totalStorage = provider.isOnline
? bytesToShrink(provider.availableStats.storage + provider.pendingStats.storage + provider.activeStats.storage)
: null;
const gpuModels = provider.hardwareGpuModels.map(gpu => gpu.substring(gpu.lastIndexOf(" ") + 1, gpu.length));
const gpuModels = provider.gpuModels.map(x => x.model).filter(createFilterUnique());

const onStarClick = event => {
event.preventDefault();
Expand Down
15 changes: 9 additions & 6 deletions deploy-web/src/components/providers/ProviderSpecs.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { Box, Chip, Paper } from "@mui/material";
import { makeStyles } from "tss-react/mui";
import { useRouter } from "next/router";
import { ClientProviderDetailWithStatus } from "@src/types/provider";
import { LabelValue } from "../shared/LabelValue";
import CheckIcon from "@mui/icons-material/Check";
import { ProviderAttributesSchema } from "@src/types/providerAttributes";
import { createFilterUnique } from "@src/utils/array";

const useStyles = makeStyles()(theme => ({
root: {
Expand All @@ -20,12 +19,16 @@ const useStyles = makeStyles()(theme => ({

type Props = {
provider: Partial<ClientProviderDetailWithStatus>;
providerAttributesSchema: ProviderAttributesSchema;
};

export const ProviderSpecs: React.FunctionComponent<Props> = ({ provider, providerAttributesSchema }) => {
export const ProviderSpecs: React.FunctionComponent<Props> = ({ provider }) => {
const { classes } = useStyles();
const router = useRouter();

const gpuModels =
provider?.gpuModels
?.map(x => x.model + " " + x.ram)
.filter(createFilterUnique())
.sort((a, b) => a.localeCompare(b)) || [];

return (
<Paper className={classes.root}>
Expand All @@ -41,7 +44,7 @@ export const ProviderSpecs: React.FunctionComponent<Props> = ({ provider, provid
<Box>
<LabelValue
label="GPU Models"
value={provider.hardwareGpuModels.map(x => (
value={gpuModels.map(x => (
<Chip key={x} label={x} size="small" sx={{ marginRight: ".5rem" }} />
))}
/>
Expand Down
2 changes: 1 addition & 1 deletion deploy-web/src/pages/providers/[owner]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ const ProviderDetailPage: React.FunctionComponent<Props> = ({ owner, _provider }
<Typography variant="body2" sx={{ marginBottom: "1rem" }}>
Specs
</Typography>
<ProviderSpecs provider={provider} providerAttributesSchema={providerAttributesSchema} />
<ProviderSpecs provider={provider} />

<Typography variant="body2" sx={{ marginBottom: "1rem", marginTop: "1rem" }}>
Features
Expand Down
1 change: 1 addition & 0 deletions deploy-web/src/types/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ export interface ApiProviderList {
isValidVersion: boolean;
isOnline: boolean;
isAudited: boolean;
gpuModels: { vendor: string; model: string; ram: string; interface: string }[];
activeStats: {
cpu: number;
gpu: number;
Expand Down
7 changes: 7 additions & 0 deletions deploy-web/src/utils/array.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
type Matcher<T> = (a: T, b: T) => boolean;

export function createFilterUnique<T>(matcher: Matcher<T> = (a, b) => a === b): (value: T, index: number, array: T[]) => boolean {
return (value, index, array) => {
return array.findIndex(other => matcher(value, other)) === index;
};
}
5 changes: 4 additions & 1 deletion shared/dbSchemas/akash/providerSnapshot.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Column, Default, Model, PrimaryKey, Table } from "sequelize-typescript";
import { Column, Default, HasMany, Model, PrimaryKey, Table } from "sequelize-typescript";
import { DataTypes } from "sequelize";
import { Required } from "../decorators/requiredDecorator";
import { ProviderSnapshotNode } from "./providerSnapshotNode";

@Table({
modelName: "providerSnapshot",
Expand Down Expand Up @@ -33,4 +34,6 @@ export class ProviderSnapshot extends Model {
@Column(DataTypes.BIGINT) availableGPU?: number;
@Column(DataTypes.BIGINT) availableMemory?: number;
@Column(DataTypes.BIGINT) availableStorage?: number;

@HasMany(() => ProviderSnapshotNode, "snapshotId") nodes: ProviderSnapshotNode[];
}
10 changes: 8 additions & 2 deletions shared/dbSchemas/akash/providerSnapshotNode.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { Column, Default, Model, PrimaryKey, Table } from "sequelize-typescript";
import { Column, Default, HasMany, Model, PrimaryKey, Table } from "sequelize-typescript";
import { DataTypes } from "sequelize";
import { Required } from "../decorators/requiredDecorator";
import { ProviderSnapshotNodeGPU } from "./providerSnapshotNodeGPU";
import { ProviderSnapshotNodeCPU } from "./providerSnapshotNodeCPU";

@Table({
modelName: "providerSnapshotNode"
modelName: "providerSnapshotNode",
indexes: [{ unique: false, fields: ["snapshotId"] }]
})
export class ProviderSnapshotNode extends Model {
@Required @PrimaryKey @Default(DataTypes.UUIDV4) @Column(DataTypes.UUID) id: string;
Expand All @@ -26,4 +29,7 @@ export class ProviderSnapshotNode extends Model {

@Column(DataTypes.BIGINT) gpuAllocatable: number;
@Column(DataTypes.BIGINT) gpuAllocated: number;

@HasMany(() => ProviderSnapshotNodeGPU, "snapshotNodeId") gpus: ProviderSnapshotNodeGPU[];
@HasMany(() => ProviderSnapshotNodeCPU, "snapshotNodeId") cpus: ProviderSnapshotNodeCPU[];
}
3 changes: 2 additions & 1 deletion shared/dbSchemas/akash/providerSnapshotNodeCPU.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { DataTypes } from "sequelize";
import { Required } from "../decorators/requiredDecorator";

@Table({
modelName: "providerSnapshotNodeCPU"
modelName: "providerSnapshotNodeCPU",
indexes: [{ unique: false, fields: ["snapshotNodeId"] }]
})
export class ProviderSnapshotNodeCPU extends Model {
@Required @PrimaryKey @Default(DataTypes.UUIDV4) @Column(DataTypes.UUID) id: string;
Expand Down
3 changes: 2 additions & 1 deletion shared/dbSchemas/akash/providerSnapshotNodeGPU.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { DataTypes } from "sequelize";
import { Required } from "../decorators/requiredDecorator";

@Table({
modelName: "providerSnapshotNodeGPU"
modelName: "providerSnapshotNodeGPU",
indexes: [{ unique: false, fields: ["snapshotNodeId"] }]
})
export class ProviderSnapshotNodeGPU extends Model {
@Required @PrimaryKey @Default(DataTypes.UUIDV4) @Column(DataTypes.UUID) id: string;
Expand Down

0 comments on commit ec66e7a

Please sign in to comment.