CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/compute/cloud/hyperstack/flavor.ts
Views: 687
1
/*
2
For hyperstack the flavor_name is a string of the form
3
4
[n1|n2|n3]-[RTX-A6000|A100|H100|L40|...]x[1,2,4,8,10][-NVLink-v2|-NVLink-K8s|-K8s|-a]
5
6
We define this as
7
8
[model]x[count]
9
10
where the count may include -NVLink-v2 or -a, i.e., count is not always just a number.
11
12
The code in this module is for parsing and manipulating this flavor string.
13
14
The actual flavors are
15
16
Object.values(priceData.options).map((x)=>x.flavor_name)
17
18
and are a small subset of all possibilities according to
19
the above format, of course.
20
21
NOTES:
22
23
- Presumably the actual flavors will just grow over time in unpredictable ways.
24
So below we make a list of known/tested data. We could explicitly list new things
25
that pop up via the API as "advanced".
26
27
- I have no clue what the [n1|n2|n3]- part of the flavor actually "means". It is unique
28
in a region. It doesn't determine the region.
29
30
- We won't offer the -K8s ones, since that likely conflicts with whatever
31
we are configuring. So we assume not an -K8s one.
32
33
- The NVLink-v2 A100 and normal A100 are the same price, but the NVLink-v2 has
34
more than 2x the RAM, and as I check now, way more availability. So it's
35
worth offering both, though this will complicate our UI a little.
36
37
*/
38
39
import { field_cmp } from "@cocalc/util/misc";
40
41
// These are the supported flavors that we want to allow and include at the time of writing.
42
// We explicitly exclude the K8s one as of right now (April 2024), but nothing else.
43
// When new flavors pop up, we will explicitly update our code to support them after doing
44
// full testing.
45
export const SUPPORTED_FLAVORS = [
46
"n1-A100x1",
47
"n1-A100x2",
48
"n1-A100x4",
49
"n2-A100x4",
50
"n2-A100x8",
51
"n2-A100x8-NVLink-v2",
52
"n2-A100x1",
53
"n2-H100x4",
54
"n2-H100x8",
55
"n2-H100x1",
56
"n2-H100x2",
57
"n2-L40x1",
58
"n2-L40x2",
59
"n2-L40x4",
60
"n2-L40x8",
61
"n2-RTX-A4000x1",
62
"n2-RTX-A4000x2",
63
"n2-RTX-A4000x4",
64
"n2-RTX-A4000x8",
65
"n2-RTX-A4000x10",
66
"n2-RTX-A5000x1",
67
"n2-RTX-A5000x2",
68
"n2-RTX-A5000x4",
69
"n2-RTX-A5000x8",
70
"n1-RTX-A6000x1",
71
"n1-RTX-A6000x2",
72
"n1-RTX-A6000x4",
73
"n1-RTX-A6000x1",
74
"n1-RTX-A6000x2",
75
"n1-RTX-A6000x4",
76
"n1-RTX-A6000x8",
77
"n1-RTX-A6000x8-a",
78
"n1-RTX-A6000-ADAx1",
79
"n1-RTX-A6000-ADAx2",
80
"n1-RTX-A6000-ADAx4",
81
] as const;
82
83
const SUPPORTED_FLAVORS_SET = new Set(SUPPORTED_FLAVORS);
84
function isSupportedFlavor(flavor_name: string): boolean {
85
return SUPPORTED_FLAVORS_SET.has(flavor_name as any);
86
}
87
88
interface Flavor {
89
model: string;
90
count: string;
91
}
92
93
export function parseFlavor(flavor_name): Flavor {
94
const i = flavor_name.lastIndexOf("x"); // assumes "-NVLink-v2" and "-a" don't contain an "x".
95
const model = flavor_name.slice(0, i);
96
const count = flavor_name.slice(i + 1);
97
return { model, count };
98
}
99
100
function countToNumber(count: string): number {
101
return parseFloat(count.split("-")[0]);
102
}
103
104
export function encodeFlavor(flavor: Flavor): string {
105
const { model, count } = flavor;
106
return `${model}x${count}`;
107
}
108
109
export function getModelOptions(priceData): {
110
region: string;
111
model: string;
112
available: number;
113
cost_per_hour: number;
114
gpu: string;
115
}[] {
116
const seen = new Set<string>([]);
117
const options: {
118
region: string;
119
model: string;
120
available: number;
121
cost_per_hour: number;
122
gpu: string;
123
}[] = [];
124
for (const key in priceData.options) {
125
const [region, flavor] = key.split("|");
126
if (!isSupportedFlavor(flavor)) {
127
continue;
128
}
129
const { model, count } = parseFlavor(flavor);
130
if (count != "1") {
131
continue;
132
}
133
const x = `${region}|${model}`;
134
if (seen.has(x)) {
135
continue;
136
}
137
seen.add(x);
138
const { cost_per_hour, gpu } = priceData.options[key];
139
const n = countToNumber(count);
140
const available = modelAvailability({ region, model, priceData });
141
options.push({
142
region,
143
model,
144
cost_per_hour: cost_per_hour / n,
145
available,
146
gpu,
147
});
148
}
149
return options.sort(field_cmp("cost_per_hour"));
150
}
151
152
export function getCountOptions({ flavor_name, region_name, priceData }): {
153
count: string;
154
quantity: number;
155
available: number;
156
cost_per_hour: number;
157
gpu: string;
158
}[] {
159
const { model } = parseFlavor(flavor_name);
160
const options: {
161
count: string;
162
quantity: number;
163
available: number;
164
cost_per_hour: number;
165
gpu: string;
166
}[] = [];
167
for (const key in priceData.options) {
168
const [region, flavor] = key.split("|");
169
if (region != region_name || !isSupportedFlavor(flavor)) {
170
continue;
171
}
172
const x = parseFlavor(flavor);
173
const { cost_per_hour, gpu } = priceData.options[key];
174
if (x.model == model) {
175
options.push({
176
count: x.count,
177
quantity: parseFloat(x.count.split("-")[0]),
178
available: priceData.options[key]?.available ?? 0,
179
cost_per_hour,
180
gpu,
181
});
182
}
183
}
184
options.sort(field_cmp("quantity"));
185
return options;
186
}
187
188
// return total number of GPUs of the given model that are currently available
189
export function modelAvailability({ region, model, priceData }) {
190
let available = 0;
191
for (const key in priceData.options) {
192
const [region0, flavor] = key.split("|");
193
if (region != region0) {
194
continue;
195
}
196
const x = parseFlavor(flavor);
197
if (x.model != model) {
198
continue;
199
}
200
available += priceData.options[key]?.available ?? 0;
201
}
202
return available;
203
}
204
205
export function bestCount({ model, region, count, priceData }): string {
206
const opts = getCountOptions({
207
flavor_name: encodeFlavor({ model, count }),
208
region_name: region,
209
priceData,
210
});
211
for (const x of opts) {
212
if (x.count == count && x.available) {
213
return x.count;
214
}
215
}
216
for (const x of opts) {
217
if (x.available) {
218
return x.count;
219
}
220
}
221
return opts[0]?.count ?? "1";
222
}
223
224