Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/frontend/compute/cloud/hyperstack/flavor.ts
Views: 687
/*1For hyperstack the flavor_name is a string of the form23[n1|n2|n3]-[RTX-A6000|A100|H100|L40|...]x[1,2,4,8,10][-NVLink-v2|-NVLink-K8s|-K8s|-a]45We define this as67[model]x[count]89where the count may include -NVLink-v2 or -a, i.e., count is not always just a number.1011The code in this module is for parsing and manipulating this flavor string.1213The actual flavors are1415Object.values(priceData.options).map((x)=>x.flavor_name)1617and are a small subset of all possibilities according to18the above format, of course.1920NOTES:2122- Presumably the actual flavors will just grow over time in unpredictable ways.23So below we make a list of known/tested data. We could explicitly list new things24that pop up via the API as "advanced".2526- I have no clue what the [n1|n2|n3]- part of the flavor actually "means". It is unique27in a region. It doesn't determine the region.2829- We won't offer the -K8s ones, since that likely conflicts with whatever30we are configuring. So we assume not an -K8s one.3132- The NVLink-v2 A100 and normal A100 are the same price, but the NVLink-v2 has33more than 2x the RAM, and as I check now, way more availability. So it's34worth offering both, though this will complicate our UI a little.3536*/3738import { field_cmp } from "@cocalc/util/misc";3940// These are the supported flavors that we want to allow and include at the time of writing.41// We explicitly exclude the K8s one as of right now (April 2024), but nothing else.42// When new flavors pop up, we will explicitly update our code to support them after doing43// full testing.44export const SUPPORTED_FLAVORS = [45"n1-A100x1",46"n1-A100x2",47"n1-A100x4",48"n2-A100x4",49"n2-A100x8",50"n2-A100x8-NVLink-v2",51"n2-A100x1",52"n2-H100x4",53"n2-H100x8",54"n2-H100x1",55"n2-H100x2",56"n2-L40x1",57"n2-L40x2",58"n2-L40x4",59"n2-L40x8",60"n2-RTX-A4000x1",61"n2-RTX-A4000x2",62"n2-RTX-A4000x4",63"n2-RTX-A4000x8",64"n2-RTX-A4000x10",65"n2-RTX-A5000x1",66"n2-RTX-A5000x2",67"n2-RTX-A5000x4",68"n2-RTX-A5000x8",69"n1-RTX-A6000x1",70"n1-RTX-A6000x2",71"n1-RTX-A6000x4",72"n1-RTX-A6000x1",73"n1-RTX-A6000x2",74"n1-RTX-A6000x4",75"n1-RTX-A6000x8",76"n1-RTX-A6000x8-a",77"n1-RTX-A6000-ADAx1",78"n1-RTX-A6000-ADAx2",79"n1-RTX-A6000-ADAx4",80] as const;8182const SUPPORTED_FLAVORS_SET = new Set(SUPPORTED_FLAVORS);83function isSupportedFlavor(flavor_name: string): boolean {84return SUPPORTED_FLAVORS_SET.has(flavor_name as any);85}8687interface Flavor {88model: string;89count: string;90}9192export function parseFlavor(flavor_name): Flavor {93const i = flavor_name.lastIndexOf("x"); // assumes "-NVLink-v2" and "-a" don't contain an "x".94const model = flavor_name.slice(0, i);95const count = flavor_name.slice(i + 1);96return { model, count };97}9899function countToNumber(count: string): number {100return parseFloat(count.split("-")[0]);101}102103export function encodeFlavor(flavor: Flavor): string {104const { model, count } = flavor;105return `${model}x${count}`;106}107108export function getModelOptions(priceData): {109region: string;110model: string;111available: number;112cost_per_hour: number;113gpu: string;114}[] {115const seen = new Set<string>([]);116const options: {117region: string;118model: string;119available: number;120cost_per_hour: number;121gpu: string;122}[] = [];123for (const key in priceData.options) {124const [region, flavor] = key.split("|");125if (!isSupportedFlavor(flavor)) {126continue;127}128const { model, count } = parseFlavor(flavor);129if (count != "1") {130continue;131}132const x = `${region}|${model}`;133if (seen.has(x)) {134continue;135}136seen.add(x);137const { cost_per_hour, gpu } = priceData.options[key];138const n = countToNumber(count);139const available = modelAvailability({ region, model, priceData });140options.push({141region,142model,143cost_per_hour: cost_per_hour / n,144available,145gpu,146});147}148return options.sort(field_cmp("cost_per_hour"));149}150151export function getCountOptions({ flavor_name, region_name, priceData }): {152count: string;153quantity: number;154available: number;155cost_per_hour: number;156gpu: string;157}[] {158const { model } = parseFlavor(flavor_name);159const options: {160count: string;161quantity: number;162available: number;163cost_per_hour: number;164gpu: string;165}[] = [];166for (const key in priceData.options) {167const [region, flavor] = key.split("|");168if (region != region_name || !isSupportedFlavor(flavor)) {169continue;170}171const x = parseFlavor(flavor);172const { cost_per_hour, gpu } = priceData.options[key];173if (x.model == model) {174options.push({175count: x.count,176quantity: parseFloat(x.count.split("-")[0]),177available: priceData.options[key]?.available ?? 0,178cost_per_hour,179gpu,180});181}182}183options.sort(field_cmp("quantity"));184return options;185}186187// return total number of GPUs of the given model that are currently available188export function modelAvailability({ region, model, priceData }) {189let available = 0;190for (const key in priceData.options) {191const [region0, flavor] = key.split("|");192if (region != region0) {193continue;194}195const x = parseFlavor(flavor);196if (x.model != model) {197continue;198}199available += priceData.options[key]?.available ?? 0;200}201return available;202}203204export function bestCount({ model, region, count, priceData }): string {205const opts = getCountOptions({206flavor_name: encodeFlavor({ model, count }),207region_name: region,208priceData,209});210for (const x of opts) {211if (x.count == count && x.available) {212return x.count;213}214}215for (const x of opts) {216if (x.available) {217return x.count;218}219}220return opts[0]?.count ?? "1";221}222223224