diff --git a/api/src/caching/helpers.ts b/api/src/caching/helpers.ts index 88dec565b..1cd7608ab 100644 --- a/api/src/caching/helpers.ts +++ b/api/src/caching/helpers.ts @@ -60,5 +60,6 @@ export const cacheKeys = { getSandboxNodes: "getSandboxNodes", getMainnetVersion: "getMainnetVersion", getTestnetVersion: "getTestnetVersion", - getSandboxVersion: "getSandboxVersion" + getSandboxVersion: "getSandboxVersion", + getGpuModels: "getGpuModels", }; diff --git a/api/src/routers/internalRouter.ts b/api/src/routers/internalRouter.ts index 7ceebd94c..35dbac270 100644 --- a/api/src/routers/internalRouter.ts +++ b/api/src/routers/internalRouter.ts @@ -1,8 +1,12 @@ 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"; @@ -232,3 +236,49 @@ internalRouter.get("leases-duration/:owner", async (c) => { leases }); }); + +internalRouter.get("gpu-models", async (c) => { + const response = await cacheResponse(60 * 2, cacheKeys.getGpuModels, async () => { + const res = await axios.get("https://raw.githubusercontent.com/akash-network/provider-configs/main/devices/pcie/gpus.json"); + return res.data; + }); + + const gpuModels: GpuVendor[] = []; + + // 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); +}); diff --git a/api/src/services/db/providerDataService.ts b/api/src/services/db/providerDataService.ts index 0d6d36d69..058805731 100644 --- a/api/src/services/db/providerDataService.ts +++ b/api/src/services/db/providerDataService.ts @@ -10,7 +10,7 @@ export async function getProviderRegions() { include: [{ model: ProviderAttribute, attributes: ["value"], where: { key: "location-region" } }] }); - console.log(JSON.stringify(providers, null, 2)); + // console.log(JSON.stringify(providers, null, 2)); const result = regions.map((region) => { const filteredProviders = providers.filter((p) => p.providerAttributes.some((attr) => attr.value === region.key)).map((x) => x.owner); return { ...region, providers: filteredProviders }; diff --git a/api/src/types/gpu.ts b/api/src/types/gpu.ts new file mode 100644 index 000000000..250a8da43 --- /dev/null +++ b/api/src/types/gpu.ts @@ -0,0 +1,23 @@ +export interface GpuVendor { + name: string; + models: GpuModel[]; +} + +export interface GpuModel { + name: string; + memory: string[]; + interface: string[]; +} + +export type ProviderConfigGpusType = { + [key: string]: { + name: string; + devices: { + [key: string]: { + name: string; + memory_size: string; + interface: string; + }; + }; + }; +}; \ No newline at end of file diff --git a/api/src/utils/gpu.ts b/api/src/utils/gpu.ts new file mode 100644 index 000000000..ae1231bf1 --- /dev/null +++ b/api/src/utils/gpu.ts @@ -0,0 +1,4 @@ +export function getGpuInterface(gpuInterface: string) { + const _formatted = gpuInterface.toLowerCase(); + return _formatted.startsWith("sxm") ? "sxm" : _formatted; +} diff --git a/deploy-web/package-lock.json b/deploy-web/package-lock.json index d73863ed7..2c80bc799 100644 --- a/deploy-web/package-lock.json +++ b/deploy-web/package-lock.json @@ -10,7 +10,7 @@ "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@akashnetwork/akashjs": "0.5.10", + "@akashnetwork/akashjs": "^0.5.11", "@auth0/nextjs-auth0": "^3.1.0", "@cosmjs/encoding": "^0.29.5", "@cosmjs/stargate": "^0.29.5", @@ -115,9 +115,9 @@ "integrity": "sha512-nA9XHtlAkYfJxY7bce8DcN7eKxWWCWkU+1GR9d+U6MbNpfwQp8TI7vqOsBsMcHoT4mBu2kypKoSKnghEzOOq5Q==" }, "node_modules/@akashnetwork/akashjs": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/@akashnetwork/akashjs/-/akashjs-0.5.10.tgz", - "integrity": "sha512-iMBpDWPIAKmQLrqnwpgkanDD0by2oxA/FT0lDZ3IKRqDLXl9v/kMEN18X3/Ab/BD/nhygOQxGNXWqzGEQDNeqA==", + "version": "0.5.11", + "resolved": "https://registry.npmjs.org/@akashnetwork/akashjs/-/akashjs-0.5.11.tgz", + "integrity": "sha512-bqX1bOlgskqq1wUk92RyoBoPLycd++dvq6co1i+7Y7/zsckEaqbbWh7dQB5jKl4WMzGgooovXqrmH65Fkk9DXQ==", "dependencies": { "@cosmjs/launchpad": "^0.27.0", "@cosmjs/proto-signing": "^0.28.11", @@ -24391,9 +24391,9 @@ "integrity": "sha512-nA9XHtlAkYfJxY7bce8DcN7eKxWWCWkU+1GR9d+U6MbNpfwQp8TI7vqOsBsMcHoT4mBu2kypKoSKnghEzOOq5Q==" }, "@akashnetwork/akashjs": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/@akashnetwork/akashjs/-/akashjs-0.5.10.tgz", - "integrity": "sha512-iMBpDWPIAKmQLrqnwpgkanDD0by2oxA/FT0lDZ3IKRqDLXl9v/kMEN18X3/Ab/BD/nhygOQxGNXWqzGEQDNeqA==", + "version": "0.5.11", + "resolved": "https://registry.npmjs.org/@akashnetwork/akashjs/-/akashjs-0.5.11.tgz", + "integrity": "sha512-bqX1bOlgskqq1wUk92RyoBoPLycd++dvq6co1i+7Y7/zsckEaqbbWh7dQB5jKl4WMzGgooovXqrmH65Fkk9DXQ==", "requires": { "@cosmjs/launchpad": "^0.27.0", "@cosmjs/proto-signing": "^0.28.11", diff --git a/deploy-web/package.json b/deploy-web/package.json index e24118daf..8adeca2d6 100644 --- a/deploy-web/package.json +++ b/deploy-web/package.json @@ -15,7 +15,7 @@ "postinstall": "patch-package" }, "dependencies": { - "@akashnetwork/akashjs": "0.5.10", + "@akashnetwork/akashjs": "0.5.11", "@auth0/nextjs-auth0": "^3.1.0", "@cosmjs/encoding": "^0.29.5", "@cosmjs/stargate": "^0.29.5", diff --git a/deploy-web/src/components/newDeploymentWizard/SdlBuilder.tsx b/deploy-web/src/components/newDeploymentWizard/SdlBuilder.tsx index 2af318ef4..9bcc326a8 100644 --- a/deploy-web/src/components/newDeploymentWizard/SdlBuilder.tsx +++ b/deploy-web/src/components/newDeploymentWizard/SdlBuilder.tsx @@ -6,8 +6,8 @@ import { nanoid } from "nanoid"; import { generateSdl } from "@src/utils/sdl/sdlGenerator"; import { Alert, Box, Button, CircularProgress } from "@mui/material"; import { SimpleServiceFormControl } from "../sdl/SimpleServiceFormControl"; -import { useProviderAttributesSchema } from "@src/queries/useProvidersQuery"; import { importSimpleSdl } from "@src/utils/sdl/sdlImport"; +import { useGpuModels } from "@src/queries/useGpuQuery"; interface Props { sdlString: string; @@ -38,7 +38,7 @@ export const SdlBuilder = React.forwardRef(({ sdlStrin keyName: "id" }); const { services: _services } = watch(); - const { data: providerAttributesSchema } = useProviderAttributesSchema(); + const { data: gpuModels } = useGpuModels(); const [serviceCollapsed, setServiceCollapsed] = useState([]); React.useImperativeHandle(ref, () => ({ @@ -78,7 +78,7 @@ export const SdlBuilder = React.forwardRef(({ sdlStrin try { if (!yamlStr) return []; - const services = importSimpleSdl(yamlStr, providerAttributesSchema); + const services = importSimpleSdl(yamlStr); setError(null); @@ -117,7 +117,8 @@ export const SdlBuilder = React.forwardRef(({ sdlStrin key={service.id} serviceIndex={serviceIndex} _services={_services} - providerAttributesSchema={providerAttributesSchema} + gpuModels={gpuModels} + setValue={setValue} control={control} trigger={trigger} onRemoveService={onRemoveService} diff --git a/deploy-web/src/components/sdl/AdvancedConfig.tsx b/deploy-web/src/components/sdl/AdvancedConfig.tsx index 94972c3b4..ff335dc33 100644 --- a/deploy-web/src/components/sdl/AdvancedConfig.tsx +++ b/deploy-web/src/components/sdl/AdvancedConfig.tsx @@ -13,13 +13,12 @@ import { ProviderAttributesSchema } from "@src/types/providerAttributes"; import { PersistentStorage } from "./PersistentStorage"; type Props = { - providerAttributesSchema: ProviderAttributesSchema; currentService: Service; control: Control; children?: ReactNode; }; -export const AdvancedConfig: React.FunctionComponent = ({ control, currentService, providerAttributesSchema }) => { +export const AdvancedConfig: React.FunctionComponent = ({ control, currentService }) => { const theme = useTheme(); const [expanded, setIsAdvancedOpen] = useState(false); const [isEditingCommands, setIsEditingCommands] = useState(false); @@ -42,7 +41,6 @@ export const AdvancedConfig: React.FunctionComponent = ({ control, curren serviceIndex={0} expose={currentService.expose} services={[currentService]} - providerAttributesSchema={providerAttributesSchema} /> )} diff --git a/deploy-web/src/components/sdl/ExposeFormModal.tsx b/deploy-web/src/components/sdl/ExposeFormModal.tsx index e79651ce7..0230801c4 100644 --- a/deploy-web/src/components/sdl/ExposeFormModal.tsx +++ b/deploy-web/src/components/sdl/ExposeFormModal.tsx @@ -13,7 +13,6 @@ import { CustomTooltip } from "../shared/CustomTooltip"; import InfoIcon from "@mui/icons-material/Info"; import { endpointNameValidationRegex } from "@src/utils/deploymentData/v1beta3"; import { HttpOptionsFormControl } from "./HttpOptionsFormControl"; -import { ProviderAttributesSchema } from "@src/types/providerAttributes"; type Props = { serviceIndex: number; @@ -22,10 +21,9 @@ type Props = { children?: ReactNode; services: Service[]; expose: Expose[]; - providerAttributesSchema: ProviderAttributesSchema; }; -export const ExposeFormModal: React.FunctionComponent = ({ control, serviceIndex, onClose, expose: _expose, services, providerAttributesSchema }) => { +export const ExposeFormModal: React.FunctionComponent = ({ control, serviceIndex, onClose, expose: _expose, services }) => { const acceptRef = useRef(); const toRef = useRef(); const { @@ -308,13 +306,7 @@ export const ExposeFormModal: React.FunctionComponent = ({ control, servi - + diff --git a/deploy-web/src/components/sdl/FormSelect.tsx b/deploy-web/src/components/sdl/FormSelect.tsx deleted file mode 100644 index aecc65277..000000000 --- a/deploy-web/src/components/sdl/FormSelect.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { Autocomplete, Box, ClickAwayListener, TextField } from "@mui/material"; -import { RentGpusFormValues, SdlBuilderFormValues } from "@src/types"; -import { ProviderAttributeSchemaDetailValue, ProviderAttributesSchema } from "@src/types/providerAttributes"; -import { useState } from "react"; -import { Control, Controller, FieldPath } from "react-hook-form"; - -type FormSelectProps = { - control: Control; - providerAttributesSchema: ProviderAttributesSchema; - optionName?: keyof ProviderAttributesSchema; - name: FieldPath; - className?: string; - requiredMessage?: string; - label: string; - multiple?: boolean; - required?: boolean; - disabled?: boolean; - valueType?: "key" | "description "; -}; - -export const FormSelect: React.FunctionComponent = ({ - control, - providerAttributesSchema, - optionName, - name, - className, - requiredMessage, - label, - required = providerAttributesSchema[optionName]?.required || false, - multiple, - disabled, - valueType = "description" -}) => { - const [isOpen, setIsOpen] = useState(false); - const options = providerAttributesSchema[optionName]?.values || []; - - return ( - ( - - (valueType === "key" ? option?.key : option?.description) || ""} - defaultValue={multiple ? [] : null} - isOptionEqualToValue={(option, value) => option.key === value.key} - filterSelectedOptions - fullWidth - multiple={multiple} - ChipProps={{ size: "small" }} - onChange={(event, newValue: string[] | null | ProviderAttributeSchemaDetailValue[]) => { - field.onChange(newValue); - }} - renderInput={params => ( - setIsOpen(false)}> - setIsOpen(prev => !prev)} - sx={{ minHeight: "42px" }} - /> - - )} - renderOption={(props, option) => { - return ( - -
{valueType === "key" ? option.key : option.description}
-
- ); - }} - /> -
- )} - /> - ); -}; diff --git a/deploy-web/src/components/sdl/GpuFormControl.tsx b/deploy-web/src/components/sdl/GpuFormControl.tsx index 73642268f..6db392ea5 100644 --- a/deploy-web/src/components/sdl/GpuFormControl.tsx +++ b/deploy-web/src/components/sdl/GpuFormControl.tsx @@ -1,17 +1,33 @@ import { ReactNode } from "react"; import { makeStyles } from "tss-react/mui"; -import { Box, Checkbox, CircularProgress, FormControl, FormHelperText, MenuItem, Select, Slider, TextField, Typography, useTheme } from "@mui/material"; +import { + Box, + Button, + Checkbox, + CircularProgress, + FormControl, + FormHelperText, + Grid, + IconButton, + InputLabel, + MenuItem, + Select, + Slider, + TextField, + Typography, + useTheme +} from "@mui/material"; import { RentGpusFormValues, SdlBuilderFormValues, Service } from "@src/types"; import { CustomTooltip } from "../shared/CustomTooltip"; import InfoIcon from "@mui/icons-material/Info"; import { FormPaper } from "./FormPaper"; -import { Control, Controller } from "react-hook-form"; +import { Control, Controller, UseFormSetValue, useFieldArray } from "react-hook-form"; import SpeedIcon from "@mui/icons-material/Speed"; -import { cx } from "@emotion/css"; -import { ProviderAttributesSchema } from "@src/types/providerAttributes"; import { gpuVendors } from "../shared/akash/gpu"; -import { FormSelect } from "./FormSelect"; import { validationConfig } from "../shared/akash/units"; +import { GpuVendor } from "@src/types/gpu"; +import DeleteIcon from "@mui/icons-material/Delete"; +import ClearIcon from "@mui/icons-material/Clear"; type Props = { serviceIndex: number; @@ -19,169 +35,357 @@ type Props = { hideHasGpu?: boolean; children?: ReactNode; control: Control; - providerAttributesSchema: ProviderAttributesSchema; + gpuModels: GpuVendor[]; currentService: Service; + setValue: UseFormSetValue; }; -const useStyles = makeStyles()(theme => ({ - formControl: { - marginBottom: theme.spacing(1.5) - }, - textField: { - width: "100%" - } -})); - -export const GpuFormControl: React.FunctionComponent = ({ providerAttributesSchema, control, serviceIndex, hasGpu, currentService, hideHasGpu }) => { - const { classes } = useStyles(); +export const GpuFormControl: React.FunctionComponent = ({ gpuModels, control, serviceIndex, hasGpu, currentService, setValue, hideHasGpu }) => { + const { + fields: formGpuModels, + remove: removeFormGpuModel, + append: appendFormGpuModel + } = useFieldArray({ + control, + name: `services.${serviceIndex}.profile.gpuModels`, + keyName: "id" + }); const theme = useTheme(); + const onAddGpuModel = () => { + appendFormGpuModel({ vendor: "nvidia", name: "", memory: "", interface: "" }); + }; + return ( - - { - if (!v) return "GPU amount is required."; - - const _value = v || 0; - - if (_value < 1) return "GPU amount must be greater than 0."; - else if (currentService.count === 1 && _value > validationConfig.maxGpuAmount) { - return `Maximum amount of GPU for a single service instance is ${validationConfig.maxGpuAmount}.`; - } else if (currentService.count > 1 && currentService.count * _value > validationConfig.maxGroupGpuCount) { - return `Maximum total amount of GPU for a single service instance group is ${validationConfig.maxGroupGpuCount}.`; + + + { + if (!v) return "GPU amount is required."; + + const _value = v || 0; + + if (_value < 1) return "GPU amount must be greater than 0."; + else if (currentService.count === 1 && _value > validationConfig.maxGpuAmount) { + return `Maximum amount of GPU for a single service instance is ${validationConfig.maxGpuAmount}.`; + } else if (currentService.count > 1 && currentService.count * _value > validationConfig.maxGroupGpuCount) { + return `Maximum total amount of GPU for a single service instance group is ${validationConfig.maxGroupGpuCount}.`; + } + return true; } - return true; - } - }} - render={({ field, fieldState }) => ( - - - - - - GPU - - - The amount of GPUs required for this workload. -
-
- You can also specify the GPU vendor and model you want specifically. If you don't specify any model, providers with any GPU model will - bid on your workload. -
-
- - View official documentation. - - - } - > - -
-
- - {!hideHasGpu && ( - ( - - )} - /> + }} + render={({ field, fieldState }) => ( + + + + + + GPU + + + The amount of GPUs required for this workload. +
+
+ You can also specify the GPU vendor and model you want specifically. If you don't specify any model, providers with any GPU model will + bid on your workload. +
+
+ + View official documentation. + + + } + > + +
+
+ + {!hideHasGpu && ( + ( + { + field.onChange(event); + if (event.target.checked && formGpuModels.length === 0) { + onAddGpuModel(); + } + }} + color="secondary" + size="small" + sx={{ marginLeft: ".5rem" }} + /> + )} + /> + )} +
+ + {hasGpu && ( + + field.onChange(parseFloat(event.target.value))} + inputProps={{ min: 1, step: 1, max: validationConfig.maxGpuAmount }} + size="small" + sx={{ width: "100px" }} + /> + )}
{hasGpu && ( - - field.onChange(parseFloat(event.target.value))} - inputProps={{ min: 1, step: 1, max: validationConfig.maxGpuAmount }} - size="small" - sx={{ width: "100px" }} - /> - + field.onChange(newValue)} + /> )} -
- - {hasGpu && ( - field.onChange(newValue)} - /> - )} - - {!!fieldState.error && {fieldState.error.message}} -
- )} - /> + + {!!fieldState.error && {fieldState.error.message}} + + )} + /> +
{hasGpu && ( -
- - ( - - )} - /> + <> + + + Picking specific GPU models below, filters out providers that don't have those GPUs and may reduce the number of bids you receive. + - - {providerAttributesSchema ? ( - - ) : ( - - - - Loading GPU models... - + {formGpuModels.map((formGpu, formGpuIndex) => { + const currentGpu = currentService.profile.gpuModels[formGpuIndex]; + const models = gpuModels?.find(u => u.name === currentGpu.vendor)?.models || []; + const interfaces = models.find(m => m.name === currentGpu.name)?.interface || []; + const memorySizes = models.find(m => m.name === currentGpu.name)?.memory || []; + + return ( + + + + ( + + + Vendor + + + + )} + /> + + {gpuModels ? ( + <> + + ( + + + Model + + + + )} + /> + + + ( + + + Memory + + + + )} + /> + + + ( + + + Interface + + + + )} + /> + + + + {formGpuIndex !== 0 && ( + removeFormGpuModel(formGpuIndex)} size="small"> + + + )} + + + ) : ( + + + + Loading GPU models... + + + )} + - )} - -
+ ); + })} + + )} + + {gpuModels && hasGpu && ( + + + )}
); diff --git a/deploy-web/src/components/sdl/HttpOptionsFormControl.tsx b/deploy-web/src/components/sdl/HttpOptionsFormControl.tsx index 880f3727f..99ddc19b5 100644 --- a/deploy-web/src/components/sdl/HttpOptionsFormControl.tsx +++ b/deploy-web/src/components/sdl/HttpOptionsFormControl.tsx @@ -5,7 +5,6 @@ import { Box, Checkbox, FormControlLabel, InputAdornment, MenuItem, Paper, Selec import { SdlBuilderFormValues, Service } from "@src/types"; import InfoIcon from "@mui/icons-material/Info"; import { CustomTooltip } from "../shared/CustomTooltip"; -import { ProviderAttributesSchema } from "@src/types/providerAttributes"; import { nextCases } from "@src/utils/sdl/data"; type Props = { @@ -13,7 +12,6 @@ type Props = { exposeIndex: number; services: Service[]; control: Control; - providerAttributesSchema: ProviderAttributesSchema; children?: ReactNode; }; @@ -35,7 +33,7 @@ const useStyles = makeStyles()(theme => ({ } })); -export const HttpOptionsFormControl: React.FunctionComponent = ({ control, serviceIndex, exposeIndex, services, providerAttributesSchema }) => { +export const HttpOptionsFormControl: React.FunctionComponent = ({ control, serviceIndex, exposeIndex, services }) => { const { classes } = useStyles(); const theme = useTheme(); const currentService = services[serviceIndex]; diff --git a/deploy-web/src/components/sdl/ImageSelect.tsx b/deploy-web/src/components/sdl/ImageSelect.tsx index ab4342976..3da9046de 100644 --- a/deploy-web/src/components/sdl/ImageSelect.tsx +++ b/deploy-web/src/components/sdl/ImageSelect.tsx @@ -1,5 +1,4 @@ import { ReactNode, useEffect, useLayoutEffect, useRef, useState } from "react"; -import { makeStyles } from "tss-react/mui"; import { Box, ClickAwayListener, IconButton, InputAdornment, Paper, Popper, TextField, useTheme } from "@mui/material"; import { ApiTemplate, RentGpusFormValues, SdlBuilderFormValues, Service } from "@src/types"; import { CustomTooltip } from "../shared/CustomTooltip"; @@ -17,15 +16,6 @@ type Props = { onSelectTemplate: (template: ApiTemplate) => void; }; -const useStyles = makeStyles()(theme => ({ - formControl: { - marginBottom: theme.spacing(1.5) - }, - textField: { - width: "100%" - } -})); - export const ImageSelect: React.FunctionComponent = ({ control, currentService, onSelectTemplate }) => { const theme = useTheme(); const { gpuTemplates } = useGpuTemplates(); diff --git a/deploy-web/src/components/sdl/ImportSdlModal.tsx b/deploy-web/src/components/sdl/ImportSdlModal.tsx index 26558c503..4f276a669 100644 --- a/deploy-web/src/components/sdl/ImportSdlModal.tsx +++ b/deploy-web/src/components/sdl/ImportSdlModal.tsx @@ -11,7 +11,6 @@ import { Snackbar } from "../shared/Snackbar"; import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward"; import { event } from "nextjs-google-analytics"; import { AnalyticsEvents } from "@src/utils/analytics"; -import { useProviderAttributesSchema } from "@src/queries/useProvidersQuery"; type Props = { setValue: UseFormSetValue; @@ -24,7 +23,6 @@ export const ImportSdlModal: React.FunctionComponent = ({ onClose, setVal const [sdl, setSdl] = useState(""); const [parsingError, setParsingError] = useState(null); const { enqueueSnackbar } = useSnackbar(); - const { data: providerAttributesSchema } = useProviderAttributesSchema(); useEffect(() => { const timer = Timer(500); @@ -45,7 +43,7 @@ export const ImportSdlModal: React.FunctionComponent = ({ onClose, setVal try { if (!yamlStr) return null; - const services = importSimpleSdl(yamlStr, providerAttributesSchema); + const services = importSimpleSdl(yamlStr); setParsingError(null); diff --git a/deploy-web/src/components/sdl/RentGpusForm.tsx b/deploy-web/src/components/sdl/RentGpusForm.tsx index 7e833123f..1d7da95ef 100644 --- a/deploy-web/src/components/sdl/RentGpusForm.tsx +++ b/deploy-web/src/components/sdl/RentGpusForm.tsx @@ -1,12 +1,11 @@ import { Alert, Box, Button, CircularProgress, Grid, Paper, Typography } from "@mui/material"; import { useForm } from "react-hook-form"; import { useEffect, useRef, useState } from "react"; -import { ApiTemplate, RentGpusFormValues, Service } from "@src/types"; +import { ApiTemplate, ProfileGpuModel, RentGpusFormValues, Service } from "@src/types"; import { defaultAnyRegion, defaultRentGpuService } from "@src/utils/sdl/data"; import { useRouter } from "next/router"; import sdlStore from "@src/store/sdlStore"; import { useAtom } from "jotai"; -import { useProviderAttributesSchema } from "@src/queries/useProvidersQuery"; import { RegionSelect } from "./RegionSelect"; import { AdvancedConfig } from "./AdvancedConfig"; import { GpuFormControl } from "./GpuFormControl"; @@ -36,19 +35,21 @@ import RocketLaunchIcon from "@mui/icons-material/RocketLaunch"; import { event } from "nextjs-google-analytics"; import { AnalyticsEvents } from "@src/utils/analytics"; import { useChainParam } from "@src/context/ChainParamProvider"; +import { useGpuModels } from "@src/queries/useGpuQuery"; type Props = {}; export const RentGpusForm: React.FunctionComponent = ({}) => { const [error, setError] = useState(null); // const [templateMetadata, setTemplateMetadata] = useState(null); + const [isQueryInit, setIsQuertInit] = useState(false); const [isCreatingDeployment, setIsCreatingDeployment] = useState(false); const [isDepositingDeployment, setIsDepositingDeployment] = useState(false); const [isCheckingPrerequisites, setIsCheckingPrerequisites] = useState(false); const formRef = useRef(); const [, setDeploySdl] = useAtom(sdlStore.deploySdl); const [rentGpuSdl, setRentGpuSdl] = useAtom(sdlStore.rentGpuSdl); - const { data: providerAttributesSchema } = useProviderAttributesSchema(); + const { data: gpuModels } = useGpuModels(); const { handleSubmit, control, watch, setValue, trigger } = useForm({ defaultValues: { services: [{ ...defaultRentGpuService }], @@ -80,6 +81,32 @@ export const RentGpusForm: React.FunctionComponent = ({}) => { return () => subscription.unsubscribe(); }, []); + useEffect(() => { + if (router.query.vendor && router.query.gpu && gpuModels && !isQueryInit) { + // Example query: ?vendor=nvidia&gpu=h100&vram=80Gi&interface=sxm + const vendorQuery = router.query.vendor as string; + const gpuQuery = router.query.gpu as string; + const gpuModel = gpuModels.find(x => x.name === vendorQuery)?.models.find(x => x.name === gpuQuery); + + if (gpuModel) { + const memoryQuery = router.query.vram as string; + const interfaceQuery = router.query.interface as string; + + const model: ProfileGpuModel = { + vendor: vendorQuery, + name: gpuModel.name, + memory: gpuModel.memory.find(x => x === memoryQuery) || "", + interface: gpuModel.interface.find(x => x === interfaceQuery) || "" + }; + setValue("services.0.profile.gpuModels", [model]); + } else { + console.log("GPU model not found", gpuQuery); + } + + setIsQuertInit(true); + } + }, [router.query, gpuModels, isQueryInit]); + async function createAndValidateDeploymentData(yamlStr: string, dseq = null, deposit = defaultInitialDeposit, depositorAddress = null) { try { if (!yamlStr) return null; @@ -99,7 +126,7 @@ export const RentGpusForm: React.FunctionComponent = ({}) => { try { if (!yamlStr) return null; - const services = importSimpleSdl(yamlStr, providerAttributesSchema); + const services = importSimpleSdl(yamlStr); setError(null); @@ -122,7 +149,19 @@ export const RentGpusForm: React.FunctionComponent = ({}) => { if (!result) return; + // Filter out invalid gpu models + const _gpuModels = (result[0].profile.gpuModels || []).map(templateModel => { + const isValid = gpuModels?.find(x => x.name === templateModel.vendor)?.models.some(x => x.name === templateModel.name); + return { + vendor: isValid ? templateModel.vendor : "nvidia", + name: isValid ? templateModel.name : "", + memory: isValid ? templateModel.memory : "", + interface: isValid ? templateModel.interface : "" + }; + }); + setValue("services", result as Service[]); + setValue("services.0.profile.gpuModels", _gpuModels); trigger(); }; @@ -236,10 +275,11 @@ export const RentGpusForm: React.FunctionComponent = ({}) => { @@ -266,7 +306,7 @@ export const RentGpusForm: React.FunctionComponent = ({}) => { - + {error && ( diff --git a/deploy-web/src/components/sdl/SimpleSdlBuilderForm.tsx b/deploy-web/src/components/sdl/SimpleSdlBuilderForm.tsx index dfdf53f03..721d0014c 100644 --- a/deploy-web/src/components/sdl/SimpleSdlBuilderForm.tsx +++ b/deploy-web/src/components/sdl/SimpleSdlBuilderForm.tsx @@ -22,8 +22,8 @@ import { memoryUnits, storageUnits } from "../shared/akash/units"; import sdlStore from "@src/store/sdlStore"; import { RouteStepKeys } from "@src/utils/constants"; import { useAtom } from "jotai"; -import { useProviderAttributesSchema } from "@src/queries/useProvidersQuery"; import { PreviewSdl } from "./PreviewSdl"; +import { useGpuModels } from "@src/queries/useGpuQuery"; type Props = {}; @@ -39,7 +39,7 @@ export const SimpleSDLBuilderForm: React.FunctionComponent = ({}) => { const formRef = useRef(); const [, setDeploySdl] = useAtom(sdlStore.deploySdl); const [sdlBuilderSdl, setSdlBuilderSdl] = useAtom(sdlStore.sdlBuilderSdl); - const { data: providerAttributesSchema } = useProviderAttributesSchema(); + const { data: gpuModels } = useGpuModels(); const { enqueueSnackbar } = useSnackbar(); const { handleSubmit, @@ -95,7 +95,7 @@ export const SimpleSDLBuilderForm: React.FunctionComponent = ({}) => { const response = await axios.get(`/api/proxy/user/template/${id}`); const template: ITemplate = response.data; - const services = importSimpleSdl(template.sdl, providerAttributesSchema); + const services = importSimpleSdl(template.sdl); setIsLoadingTemplate(false); @@ -296,12 +296,13 @@ export const SimpleSDLBuilderForm: React.FunctionComponent = ({}) => { key={service.id} serviceIndex={serviceIndex} _services={_services} - providerAttributesSchema={providerAttributesSchema} + setValue={setValue} control={control} trigger={trigger} onRemoveService={onRemoveService} serviceCollapsed={serviceCollapsed} setServiceCollapsed={setServiceCollapsed} + gpuModels={gpuModels} /> ))} diff --git a/deploy-web/src/components/sdl/SimpleServiceFormControl.tsx b/deploy-web/src/components/sdl/SimpleServiceFormControl.tsx index 82a0a8534..0587a0aa4 100644 --- a/deploy-web/src/components/sdl/SimpleServiceFormControl.tsx +++ b/deploy-web/src/components/sdl/SimpleServiceFormControl.tsx @@ -1,6 +1,6 @@ import { useTheme } from "@mui/material/styles"; import { Box, Collapse, Grid, IconButton, InputAdornment, Paper, TextField, Typography, useMediaQuery } from "@mui/material"; -import { Controller, Control, UseFormTrigger } from "react-hook-form"; +import { Controller, Control, UseFormTrigger, UseFormSetValue } from "react-hook-form"; import { makeStyles } from "tss-react/mui"; import { Dispatch, SetStateAction, useState } from "react"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; @@ -22,7 +22,6 @@ import { PriceValue } from "../shared/PriceValue"; import { getAvgCostPerMonth, toReadableDenom } from "@src/utils/priceUtils"; import Image from "next/legacy/image"; import { uAktDenom } from "@src/utils/constants"; -import { ProviderAttributesSchema } from "@src/types/providerAttributes"; import { EnvVarList } from "./EnvVarList"; import { CommandList } from "./CommandList"; import { ExposeList } from "./ExposeList"; @@ -32,16 +31,18 @@ import { CpuFormControl } from "./CpuFormControl"; import { MemoryFormControl } from "./MemoryFormControl"; import { StorageFormControl } from "./StorageFormControl"; import { TokenFormControl } from "./TokenFormControl"; +import { GpuVendor } from "@src/types/gpu"; type Props = { _services: Service[]; serviceIndex: number; control: Control; - providerAttributesSchema: ProviderAttributesSchema; trigger: UseFormTrigger; onRemoveService: (index: number) => void; serviceCollapsed: number[]; setServiceCollapsed: Dispatch>; + setValue: UseFormSetValue; + gpuModels: GpuVendor[]; }; const useStyles = makeStyles()(theme => ({ @@ -72,11 +73,12 @@ export const SimpleServiceFormControl: React.FunctionComponent = ({ serviceIndex, control, _services, - providerAttributesSchema, onRemoveService, trigger, serviceCollapsed, - setServiceCollapsed + setServiceCollapsed, + setValue, + gpuModels }) => { const { classes } = useStyles(); const theme = useTheme(); @@ -116,7 +118,6 @@ export const SimpleServiceFormControl: React.FunctionComponent = ({ serviceIndex={serviceIndex} expose={currentService.expose} services={_services} - providerAttributesSchema={providerAttributesSchema} /> )} {/** Edit Placement */} @@ -298,10 +299,11 @@ export const SimpleServiceFormControl: React.FunctionComponent = ({ diff --git a/deploy-web/src/queries/queryKeys.ts b/deploy-web/src/queries/queryKeys.ts index 1af21baeb..84fd26d1c 100644 --- a/deploy-web/src/queries/queryKeys.ts +++ b/deploy-web/src/queries/queryKeys.ts @@ -45,4 +45,5 @@ export class QueryKeys { static getTemplatesKey = () => ["TEMPLATES"]; static getProviderAttributesSchema = () => ["PROVIDER_ATTRIBUTES_SCHEMA"]; static getDepositParamsKey = () => ["DEPOSIT_PARAMS"]; + static getGpuModelsKey = () => ["GPU_MODELS"]; } diff --git a/deploy-web/src/queries/useGpuQuery.ts b/deploy-web/src/queries/useGpuQuery.ts new file mode 100644 index 000000000..95fdccad3 --- /dev/null +++ b/deploy-web/src/queries/useGpuQuery.ts @@ -0,0 +1,21 @@ +import { GpuVendor } from "@src/types/gpu"; +import { ApiUrlService } from "@src/utils/apiUtils"; +import axios from "axios"; +import { useQuery } from "react-query"; +import { QueryKeys } from "./queryKeys"; + +async function getGpuModels() { + const response = await axios.get(ApiUrlService.gpuModels()); + + return response.data as GpuVendor[]; +} + +export function useGpuModels(options = {}) { + return useQuery(QueryKeys.getGpuModelsKey(), () => getGpuModels(), { + ...options, + refetchInterval: false, + refetchIntervalInBackground: false, + refetchOnWindowFocus: false, + refetchOnReconnect: false + }); +} diff --git a/deploy-web/src/types/gpu.ts b/deploy-web/src/types/gpu.ts new file mode 100644 index 000000000..b60364109 --- /dev/null +++ b/deploy-web/src/types/gpu.ts @@ -0,0 +1,10 @@ +export interface GpuVendor { + name: string; + models: GpuModel[]; +} + +export interface GpuModel { + name: string; + memory: string[]; + interface: string[]; +} \ No newline at end of file diff --git a/deploy-web/src/types/sdlBuilder.ts b/deploy-web/src/types/sdlBuilder.ts index 6af22dfb6..1f51ea54d 100644 --- a/deploy-web/src/types/sdlBuilder.ts +++ b/deploy-web/src/types/sdlBuilder.ts @@ -1,4 +1,4 @@ -import { ProviderAttributeSchemaDetailValue, ProviderRegionValue } from "./providerAttributes"; +import { ProviderRegionValue } from "./providerAttributes"; export type Service = { id: string; @@ -28,8 +28,7 @@ export type Profile = { cpu: number; hasGpu?: boolean; gpu?: number; - gpuVendor?: string; - gpuModels?: ProviderAttributeSchemaDetailValue[]; + gpuModels?: ProfileGpuModel[]; ram: number; ramUnit: string; storage: number; @@ -40,6 +39,13 @@ export type Profile = { persistentStorageParam?: ServicePersistentStorage; }; +export type ProfileGpuModel = { + vendor: string; + name?: string; + memory?: string; + interface?: string; +}; + export type ServicePersistentStorage = { name: string; type: string; @@ -145,5 +151,5 @@ export type SdlSaveTemplateFormValues = { export type RentGpusFormValues = { services: Service[]; - region: Partial; + region?: Partial; }; diff --git a/deploy-web/src/utils/apiUtils.ts b/deploy-web/src/utils/apiUtils.ts index 8c1b96cf8..d52ad9ae8 100644 --- a/deploy-web/src/utils/apiUtils.ts +++ b/deploy-web/src/utils/apiUtils.ts @@ -108,6 +108,9 @@ export class ApiUrlService { static networkCapacity() { return `${BASE_API_URL}/v1/network-capacity`; } + static gpuModels() { + return `${BASE_API_URL}/internal/gpu-models`; + } // Github static auditors() { return `${BASE_API_URL}/v1/auditors`; diff --git a/deploy-web/src/utils/sdl/data.ts b/deploy-web/src/utils/sdl/data.ts index 38f1df88e..7ca05051a 100644 --- a/deploy-web/src/utils/sdl/data.ts +++ b/deploy-web/src/utils/sdl/data.ts @@ -23,8 +23,7 @@ export const defaultService: Service = { profile: { cpu: 0.1, gpu: 1, - gpuVendor: "nvidia", - gpuModels: [], + gpuModels: [{ vendor: "nvidia" }], hasGpu: false, ram: 512, ramUnit: "Mi", @@ -83,8 +82,7 @@ export const defaultRentGpuService: Service = { profile: { cpu: 0.1, gpu: 1, - gpuVendor: "nvidia", - gpuModels: [], + gpuModels: [{ vendor: "nvidia" }], hasGpu: true, ram: 512, ramUnit: "Mi", @@ -140,7 +138,7 @@ export const defaultAnyRegion = { key: "any", value: "any", description: "Any region" -} +}; export const nextCases = [ { id: 1, value: "error" }, diff --git a/deploy-web/src/utils/sdl/sdlGenerator.ts b/deploy-web/src/utils/sdl/sdlGenerator.ts index 4d410787e..127134fbb 100644 --- a/deploy-web/src/utils/sdl/sdlGenerator.ts +++ b/deploy-web/src/utils/sdl/sdlGenerator.ts @@ -1,4 +1,4 @@ -import { Expose, Service } from "@src/types"; +import { Expose, ProfileGpuModel, Service } from "@src/types"; import yaml from "js-yaml"; import { defaultHttpOptions } from "./data"; @@ -90,18 +90,43 @@ export const generateSdl = (services: Service[], region?: string) => { sdl.profiles.compute[service.title].resources.gpu = { units: service.profile.gpu, attributes: { - vendor: { - [service.profile.gpuVendor]: - service.profile.gpuModels.length > 0 - ? service.profile.gpuModels.map(x => { - const modelKey = x.key.split("/"); - // capabilities/gpu/vendor/nvidia/model/h100 -> h100 - return { model: modelKey[modelKey.length - 1] }; - }) - : null - } + vendor: {} } }; + + // Group models by vendor + const vendors = service.profile.gpuModels.reduce((group, model) => { + const { vendor } = model; + group[vendor] = group[vendor] ?? []; + group[vendor].push(model); + return group; + }, {}) as { [key: string]: ProfileGpuModel[] }; + + for (const [vendor, models] of Object.entries(vendors)) { + const mappedModels = models + .map(x => { + let model: { model?: string; ram?: string; interface?: string } = null; + + if (x.name) { + model = { + model: x.name + }; + } + + if (x.memory) { + model.ram = x.memory; + } + + if (x.interface) { + model.interface = x.interface; + } + + return model; + }) + .filter(x => x); + + sdl.profiles.compute[service.title].resources.gpu.attributes.vendor[vendor] = mappedModels.length > 0 ? mappedModels : null; + } } // Persistent Storage diff --git a/deploy-web/src/utils/sdl/sdlImport.ts b/deploy-web/src/utils/sdl/sdlImport.ts index 22852350e..04693da2e 100644 --- a/deploy-web/src/utils/sdl/sdlImport.ts +++ b/deploy-web/src/utils/sdl/sdlImport.ts @@ -1,12 +1,11 @@ -import { Expose, ImportService } from "@src/types"; +import { Expose, ImportService, ProfileGpuModel } from "@src/types"; import { nanoid } from "nanoid"; import { capitalizeFirstLetter } from "../stringUtils"; import yaml from "js-yaml"; import { CustomValidationError } from "../deploymentData"; -import { ProviderAttributeSchemaDetailValue, ProviderAttributesSchema } from "@src/types/providerAttributes"; import { defaultHttpOptions } from "./data"; -export const importSimpleSdl = (yamlStr: string, providerAttributesSchema: ProviderAttributesSchema) => { +export const importSimpleSdl = (yamlStr: string) => { try { const yamlJson = yaml.load(yamlStr) as any; const services: ImportService[] = []; @@ -33,13 +32,12 @@ export const importSimpleSdl = (yamlStr: string, providerAttributesSchema: Provi service.profile = { cpu: compute.resources.cpu.units, gpu: compute.resources.gpu ? compute.resources.gpu.units : 1, - gpuVendor: compute.resources.gpu ? getGpuVendor(compute.resources.gpu.attributes.vendor) : "nvidia", - gpuModels: compute.resources.gpu ? getGpuModels(compute.resources.gpu.attributes.vendor, providerAttributesSchema) : [], + gpuModels: compute.resources.gpu ? getGpuModels(compute.resources.gpu.attributes.vendor) : [], hasGpu: !!compute.resources.gpu, ram: getResourceDigit(compute.resources.memory.size), ramUnit: getResourceUnit(compute.resources.memory.size), - storage: getResourceDigit(ephStorage.size), - storageUnit: getResourceUnit(ephStorage.size), + storage: getResourceDigit(ephStorage?.size || "1Gi"), + storageUnit: getResourceUnit(ephStorage?.size || "1Gi"), hasPersistentStorage, persistentStorage: hasPersistentStorage ? getResourceDigit(persistentStorage?.size) : 10, persistentStorageUnit: hasPersistentStorage ? getResourceUnit(persistentStorage?.size) : "Gi", @@ -145,29 +143,28 @@ const getResourceUnit = (size: string): string => { return capitalizeFirstLetter(size.match(/[a-zA-Z]+/g)[0]); }; -const getGpuVendor = (vendorKey: { [key: string]: any }): string => { - const vendor = Object.keys(vendorKey)[0]; - - // For now only nvidia is supported - return vendor || "nvidia"; -}; - -const getGpuModels = ( - vendor: { [key: string]: { model: string }[] }, - providerAttributesSchema: ProviderAttributesSchema -): ProviderAttributeSchemaDetailValue[] => { - const models = vendor.nvidia - ? vendor.nvidia - .map(m => { - const model = providerAttributesSchema["hardware-gpu-model"].values.find(v => { - const modelKey = v.key.split("/"); - // capabilities/gpu/vendor/nvidia/model/h100 -> h100 - return m.model === modelKey[modelKey.length - 1]; - }) as ProviderAttributeSchemaDetailValue; - return model; - }) - .filter(m => m) - : []; +const getGpuModels = (vendor: { [key: string]: { model: string; ram: string; interface: string }[] }): ProfileGpuModel[] => { + const models: ProfileGpuModel[] = []; + + for (const [vendorName, vendorModels] of Object.entries(vendor)) { + if (vendorModels) { + vendorModels.forEach(m => { + models.push({ + vendor: vendorName, + name: m.model, + memory: m.ram || "", + interface: m.interface || "" + }); + }); + } else { + models.push({ + vendor: vendorName, + name: "", + memory: "", + interface: "" + }); + } + } return models; };