Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/compute/google-cloud-config.tsx
5837 views
1
import {
2
Alert,
3
Button,
4
Checkbox,
5
Divider,
6
Popconfirm,
7
Radio,
8
Select,
9
Spin,
10
Switch,
11
Table,
12
Tooltip,
13
Typography,
14
} from "antd";
15
import { isEqual } from "lodash";
16
import { useEffect, useState } from "react";
17
18
import { useTypedRedux } from "@cocalc/frontend/app-framework";
19
import { A } from "@cocalc/frontend/components/A";
20
import ShowError from "@cocalc/frontend/components/error";
21
import { Icon } from "@cocalc/frontend/components/icon";
22
import Disk from "@cocalc/frontend/compute/cloud/common/disk";
23
import DNS from "@cocalc/frontend/compute/cloud/common/dns";
24
import Template from "@cocalc/frontend/compute/cloud/common/template";
25
import { displayAcceleratorType } from "@cocalc/frontend/compute/cloud/google-cloud/accelerator";
26
import Specs, {
27
RamAndCpu,
28
} from "@cocalc/frontend/compute/cloud/google-cloud/specs";
29
import ExcludeFromSync from "@cocalc/frontend/compute/exclude-from-sync";
30
import { filterOption } from "@cocalc/frontend/compute/util";
31
import computeCost, {
32
computeAcceleratorCost,
33
computeDiskCost,
34
computeInstanceCost,
35
DATA_TRANSFER_OUT_COST_PER_GiB,
36
EXTERNAL_IP_COST,
37
GoogleCloudData,
38
markup,
39
} from "@cocalc/util/compute/cloud/google-cloud/compute-cost";
40
import type {
41
Architecture,
42
ComputeServerTemplate,
43
GoogleCloudAcceleratorType,
44
GoogleCloudConfiguration as GoogleCloudConfigurationType,
45
Images,
46
State,
47
} from "@cocalc/util/db-schema/compute-servers";
48
import {
49
getArchitecture,
50
getMachineTypeArchitecture,
51
getMinDiskSizeGb,
52
GOOGLE_CLOUD_DEFAULTS,
53
} from "@cocalc/util/db-schema/compute-servers";
54
import { cmp, currency, plural } from "@cocalc/util/misc";
55
import AllowCollaboratorControl from "./allow-collaborator-control";
56
import {
57
getGoogleCloudPriceData,
58
setImageTested,
59
setServerConfiguration,
60
} from "./api";
61
import AutoRestart from "./auto-restart";
62
import { availableClouds } from "./config";
63
import CostOverview from "./cost-overview";
64
import Ephemeral from "./ephemeral";
65
import { reloadImages, useGoogleImages, useImages } from "./images-hook";
66
import NestedVirtualization from "./nested-virtualization";
67
import Proxy from "./proxy";
68
import SelectImage, { ImageDescription, ImageLinks } from "./select-image";
69
70
export const SELECTOR_WIDTH = "350px";
71
72
export const DEFAULT_GPU_CONFIG = GOOGLE_CLOUD_DEFAULTS.gpu2;
73
74
// {
75
// acceleratorType: "nvidia-l4",
76
// acceleratorCount: 1,
77
// machineType: "g2-standard-4",
78
// region: "us-central1",
79
// zone: "us-central1-b",
80
// image: "pytorch",
81
// };
82
83
const FALLBACK_INSTANCE = "n2-standard-4";
84
// an n1-standard-1 is SO dinky it causes huge trouble
85
// with downloading/processing models.
86
const DEFAULT_GPU_INSTANCE = "n1-highmem-2";
87
88
interface ConfigurationType extends GoogleCloudConfigurationType {
89
valid?: boolean;
90
}
91
92
interface Props {
93
configuration: ConfigurationType;
94
editable?: boolean;
95
// if id not set, then doesn't try to save anything to the backend
96
id?: number;
97
project_id?: string;
98
// called whenever changes are made.
99
onChange?: (configuration: ConfigurationType) => void;
100
disabled?: boolean;
101
state?: State;
102
data?;
103
setCloud?;
104
template?: ComputeServerTemplate;
105
}
106
107
export default function GoogleCloudConfiguration({
108
configuration: configuration0,
109
editable,
110
id,
111
project_id,
112
onChange,
113
disabled,
114
state,
115
data,
116
setCloud,
117
template,
118
}: Props) {
119
const [IMAGES, ImagesError] = useImages();
120
const [googleImages, ImagesErrorGoogle] = useGoogleImages();
121
const [loading, setLoading] = useState<boolean>(false);
122
const [cost, setCost] = useState<number | null>(null);
123
const [priceData, setPriceData] = useState<GoogleCloudData | null>(null);
124
const [error, setError0] = useState<string>("");
125
const [configuration, setLocalConfiguration] =
126
useState<ConfigurationType>(configuration0);
127
const setError = (error) => {
128
setError0(error);
129
const valid = !error;
130
if (onChange != null && configuration.valid != valid) {
131
onChange({ ...configuration, valid });
132
}
133
};
134
135
useEffect(() => {
136
if (!editable) {
137
setLocalConfiguration(configuration0);
138
}
139
}, [configuration0]);
140
141
useEffect(() => {
142
(async () => {
143
try {
144
setLoading(true);
145
const data = await getGoogleCloudPriceData();
146
setPriceData(data);
147
} catch (err) {
148
setError(`${err}`);
149
} finally {
150
setLoading(false);
151
}
152
})();
153
}, []);
154
155
useEffect(() => {
156
if (!editable || configuration == null || priceData == null) {
157
return;
158
}
159
try {
160
const cost = computeCost({ configuration, priceData });
161
setCost(cost);
162
} catch (err) {
163
setError(`${err}`);
164
setCost(null);
165
}
166
}, [configuration, priceData]);
167
168
if (ImagesError != null) {
169
return ImagesError;
170
}
171
if (ImagesErrorGoogle != null) {
172
return ImagesErrorGoogle;
173
}
174
175
if (IMAGES == null || googleImages == null) {
176
return <Spin />;
177
}
178
179
const summary = (
180
<Specs
181
configuration={configuration}
182
priceData={priceData}
183
IMAGES={IMAGES}
184
/>
185
);
186
187
if (!editable || !project_id) {
188
// short summary only
189
return summary;
190
}
191
192
if (priceData == null) {
193
return <Spin />;
194
}
195
196
const setConfig = async (changes) => {
197
let changed = false;
198
for (const key in changes) {
199
if (!isEqual(changes[key], configuration[key])) {
200
changed = true;
201
break;
202
}
203
}
204
if (!changed) {
205
// nothing at all changed
206
return;
207
}
208
209
changes = ensureConsistentConfiguration(
210
priceData,
211
configuration,
212
changes,
213
IMAGES,
214
);
215
const newConfiguration = { ...configuration, ...changes };
216
217
if (
218
(state ?? "deprovisioned") != "deprovisioned" &&
219
(configuration.region != newConfiguration.region ||
220
configuration.zone != newConfiguration.zone)
221
) {
222
setError(
223
"Can't change the region or zone without first deprovisioning the VM",
224
);
225
// make copy so config gets reset -- i.e., whatever change you just tried to make is reverted.
226
setLocalConfiguration({ ...configuration });
227
return;
228
}
229
230
if (Object.keys(changes).length == 0) {
231
// nothing going to change
232
return;
233
}
234
235
try {
236
setLoading(true);
237
if (onChange != null) {
238
onChange(newConfiguration);
239
}
240
setLocalConfiguration(newConfiguration);
241
if (id != null) {
242
await setServerConfiguration({ id, configuration: changes });
243
}
244
} catch (err) {
245
setError(`${err}`);
246
} finally {
247
setLoading(false);
248
}
249
};
250
251
const columns = [
252
{
253
dataIndex: "value",
254
key: "value",
255
},
256
{ dataIndex: "label", key: "label", width: 130 },
257
];
258
259
const dataSource = [
260
{
261
key: "provisioning",
262
label: (
263
<A href="https://cloud.google.com/compute/docs/instances/spot">
264
<Icon name="external-link" /> Provisioning
265
</A>
266
),
267
value: (
268
<Provisioning
269
disabled={loading || disabled}
270
priceData={priceData}
271
setConfig={setConfig}
272
configuration={configuration}
273
/>
274
),
275
},
276
{
277
key: "gpu",
278
label: (
279
<A href="https://cloud.google.com/compute/docs/gpus">
280
<Icon name="external-link" /> GPUs
281
</A>
282
),
283
value: (
284
<GPU
285
state={state}
286
disabled={loading || disabled}
287
priceData={priceData}
288
setConfig={setConfig}
289
configuration={configuration}
290
IMAGES={IMAGES}
291
setCloud={setCloud}
292
/>
293
),
294
},
295
{
296
key: "image",
297
label: (
298
<ImageLinks image={configuration.image} style={{ height: "90px" }} />
299
),
300
value: (
301
<Image
302
state={state}
303
disabled={loading || disabled}
304
setConfig={setConfig}
305
configuration={configuration}
306
gpu={
307
!!(configuration.acceleratorType && configuration.acceleratorCount)
308
}
309
googleImages={googleImages}
310
arch={getArchitecture(configuration)}
311
/>
312
),
313
},
314
315
{
316
key: "machineType",
317
label: (
318
<A href="https://cloud.google.com/compute/docs/machine-resource#recommendations_for_machine_types">
319
<Icon name="external-link" /> Machine Types
320
</A>
321
),
322
value: (
323
<MachineType
324
state={state}
325
disabled={loading || disabled}
326
priceData={priceData}
327
setConfig={setConfig}
328
configuration={configuration}
329
/>
330
),
331
},
332
333
{
334
key: "region",
335
label: (
336
<A href="https://cloud.google.com/about/locations">
337
<Icon name="external-link" /> Regions
338
</A>
339
),
340
value: (
341
<Region
342
disabled={
343
loading || disabled || (state ?? "deprovisioned") != "deprovisioned"
344
}
345
priceData={priceData}
346
setConfig={setConfig}
347
configuration={configuration}
348
/>
349
),
350
},
351
{
352
key: "zone",
353
label: (
354
<A href="https://cloud.google.com/about/locations">
355
<Icon name="external-link" /> Zones
356
</A>
357
),
358
value: (
359
<Zone
360
disabled={
361
loading || disabled || (state ?? "deprovisioned") != "deprovisioned"
362
}
363
priceData={priceData}
364
setConfig={setConfig}
365
configuration={configuration}
366
/>
367
),
368
},
369
370
{
371
key: "disk",
372
label: (
373
<A href="https://cloud.google.com/compute/docs/disks/performance">
374
<Icon name="external-link" /> Disks
375
</A>
376
),
377
value: (
378
<BootDisk
379
id={id}
380
disabled={loading}
381
setConfig={setConfig}
382
configuration={configuration}
383
priceData={priceData}
384
state={state}
385
IMAGES={IMAGES}
386
/>
387
),
388
},
389
{
390
key: "exclude",
391
value: (
392
<ExcludeFromSync
393
id={id}
394
disabled={loading}
395
setConfig={setConfig}
396
configuration={configuration}
397
state={state}
398
style={{ marginTop: "10px", color: "#666" }}
399
/>
400
),
401
},
402
{
403
key: "network",
404
label: (
405
<A href="https://cloud.google.com/compute/docs/network-bandwidth">
406
<Icon name="external-link" /> Network
407
</A>
408
),
409
value: (
410
<Network
411
setConfig={setConfig}
412
configuration={configuration}
413
loading={loading}
414
priceData={priceData}
415
/>
416
),
417
},
418
{
419
key: "proxy",
420
label: <></>,
421
value: (
422
<Proxy
423
setConfig={setConfig}
424
configuration={configuration}
425
data={data}
426
state={state}
427
IMAGES={IMAGES}
428
project_id={project_id}
429
id={id}
430
/>
431
),
432
},
433
434
{
435
key: "ephemeral",
436
label: <></>,
437
value: (
438
<Ephemeral
439
setConfig={setConfig}
440
configuration={configuration}
441
loading={loading}
442
/>
443
),
444
},
445
{
446
key: "auto-restart",
447
label: <></>,
448
value: (
449
<AutoRestart
450
setConfig={setConfig}
451
configuration={configuration}
452
loading={loading}
453
/>
454
),
455
},
456
{
457
key: "allow-collaborator-control",
458
label: <></>,
459
value: (
460
<AllowCollaboratorControl
461
setConfig={setConfig}
462
configuration={configuration}
463
loading={loading}
464
/>
465
),
466
},
467
{
468
key: "nested-virtualization",
469
label: <></>,
470
value: (
471
<NestedVirtualization
472
setConfig={setConfig}
473
configuration={configuration}
474
loading={loading}
475
/>
476
),
477
},
478
{
479
key: "admin",
480
label: <></>,
481
value: (
482
<Admin
483
id={id}
484
configuration={configuration}
485
loading={loading}
486
template={template}
487
/>
488
),
489
},
490
];
491
492
const errDisplay = error ? (
493
<div
494
style={{
495
/*minHeight: "35px", */
496
padding: "5px 10px",
497
background: error ? "red" : undefined,
498
color: "white",
499
borderRadius: "5px",
500
}}
501
>
502
{error}
503
<Button
504
size="small"
505
onClick={() => {
506
setError("");
507
setLocalConfiguration(configuration0);
508
}}
509
style={{ float: "right" }}
510
>
511
Close
512
</Button>
513
</div>
514
) : undefined;
515
516
return (
517
<div>
518
{loading && (
519
<div style={{ float: "right" }}>
520
<Spin delay={1000} />
521
</div>
522
)}
523
{errDisplay}
524
{cost != null && (
525
<CostOverview
526
cost={cost}
527
description={
528
<>
529
You pay <b>{currency(cost)}/hour</b> while the computer server is
530
running. The rate is{" "}
531
<b>
532
{currency(
533
computeCost({ configuration, priceData, state: "off" }),
534
)}
535
/hour
536
</b>{" "}
537
when the server is off, and there is no cost when it is
538
deprovisioned. Network data transfer out charges are not included
539
in the above cost, and depend on how much data leaves the server
540
(see the Network section below). Incoming networking is free.
541
</>
542
}
543
/>
544
)}
545
<Divider />
546
<div style={{ textAlign: "center", margin: "10px 80px" }}>{summary}</div>
547
<Divider />
548
<Table
549
showHeader={false}
550
style={{ marginTop: "5px" }}
551
columns={columns}
552
dataSource={dataSource}
553
pagination={false}
554
/>
555
{errDisplay}
556
</div>
557
);
558
}
559
560
function Region({ priceData, setConfig, configuration, disabled }) {
561
const [sortByPrice, setSortByPrice] = useState<boolean>(true);
562
const [newRegion, setNewRegion] = useState<string>(configuration.region);
563
useEffect(() => {
564
setNewRegion(configuration.region);
565
}, [configuration.region]);
566
567
const regions = getRegions(priceData, configuration);
568
if (sortByPrice) {
569
regions.sort((a, b) => cmp(a.cost, b.cost));
570
}
571
const options = regions.map(({ region, location, lowCO2, cost }) => {
572
const price = <CostPerHour cost={cost} extra={" (total)"} />;
573
return {
574
value: region,
575
search: `${region} ${location} ${lowCO2 ? " co2 " : ""}`,
576
label: (
577
<div key={region} style={{ display: "flex" }}>
578
<div style={{ flex: 1 }}> {region}</div>
579
<div style={{ flex: 1 }}>{price}</div>
580
<div style={{ flex: 0.7 }}> {lowCO2 ? "🍃 Low CO2" : ""}</div>
581
<div style={{ flex: 0.8 }}> {location?.split(",")[1].trim()}</div>
582
</div>
583
),
584
};
585
});
586
587
return (
588
<div>
589
{configuration.machineType ? (
590
<div style={{ color: "#666", marginBottom: "5px" }}>
591
<b>
592
<Icon name="global" /> Region
593
</b>
594
</div>
595
) : undefined}
596
<div>
597
<Select
598
disabled={disabled}
599
style={{ width: "100%" }}
600
options={options as any}
601
value={newRegion}
602
onChange={(region) => {
603
setNewRegion(region);
604
setConfig({ region });
605
}}
606
showSearch
607
optionFilterProp="children"
608
filterOption={filterOption}
609
/>
610
</div>
611
<div>
612
<Checkbox
613
disabled={disabled}
614
style={{ marginTop: "5px" }}
615
checked={sortByPrice}
616
onChange={() => setSortByPrice(!sortByPrice)}
617
>
618
Sort by price
619
</Checkbox>
620
<div style={{ color: "#666", marginTop: "5px" }}>
621
Price above is total price in this region for the machine, disk and
622
GPU.
623
</div>
624
</div>
625
</div>
626
);
627
}
628
629
// Gets the regions where the given VM type is available.
630
// Ignores the currently selected zone.
631
function getRegions(priceData, configuration) {
632
const lowCO2 = new Set<string>();
633
const regions = new Set<string>();
634
const location: { [region: string]: string } = {};
635
const cost: { [region: string]: number } = {};
636
const { machineType, spot } = configuration ?? {};
637
for (const zone in priceData.zones) {
638
const i = zone.lastIndexOf("-");
639
const region = zone.slice(0, i);
640
const zoneData = priceData.zones[zone];
641
if (machineType) {
642
if (!zoneData.machineTypes.includes(machineType.split("-")[0])) {
643
continue;
644
}
645
if (spot) {
646
if (priceData.machineTypes[machineType]?.spot?.[region] == null) {
647
continue;
648
}
649
}
650
}
651
if (cost[region] == null) {
652
try {
653
cost[region] = computeCost({
654
priceData,
655
configuration: { ...configuration, region, zone },
656
});
657
} catch (_) {
658
continue;
659
// console.warn({ ...configuration, region, zone }, err);
660
}
661
}
662
if (zoneData.lowCO2 || zoneData.lowC02) {
663
// C02 above because of typo in data.
664
lowCO2.add(region);
665
}
666
regions.add(region);
667
location[region] = zoneData.location;
668
}
669
const v = Array.from(regions);
670
v.sort((a, b) => {
671
for (const g of [
672
"us",
673
"northamerica",
674
"europe",
675
"asia",
676
"southamerica",
677
"australia",
678
]) {
679
if (a.startsWith(g) && !b.startsWith(g)) {
680
return -1;
681
}
682
if (!a.startsWith(g) && b.startsWith(g)) {
683
return 1;
684
}
685
}
686
return cmp(a, b);
687
});
688
const data: {
689
region: string;
690
location: string;
691
lowCO2: boolean;
692
cost?: number;
693
}[] = [];
694
for (const region of v) {
695
data.push({
696
region,
697
location: location[region],
698
lowCO2: lowCO2.has(region),
699
cost: cost[region],
700
});
701
}
702
return data;
703
}
704
705
// Gets the zones compatible with the other configuration
706
function getZones(priceData, configuration) {
707
const lowCO2 = new Set<string>();
708
const zones = new Set<string>();
709
const { region, machineType, acceleratorType, spot } = configuration;
710
const prefix = machineType.split("-")[0];
711
for (const zone in priceData.zones) {
712
if (region != zoneToRegion(zone)) {
713
// this zone isn't in the chosen region.
714
continue;
715
}
716
const zoneData = priceData.zones[zone];
717
if (machineType) {
718
if (!zoneData.machineTypes.includes(prefix)) {
719
continue;
720
}
721
if (spot != null) {
722
if (priceData.machineTypes[machineType]?.spot?.[region] == null) {
723
continue;
724
}
725
}
726
}
727
if (acceleratorType) {
728
if (priceData.accelerators[acceleratorType]?.prices?.[zone] == null) {
729
// not in this zone.
730
continue;
731
}
732
}
733
if (zoneData.lowCO2 || zoneData.lowC02) {
734
// C02 above because of typo in data.
735
lowCO2.add(zone);
736
}
737
zones.add(zone);
738
}
739
const v = Array.from(zones);
740
v.sort();
741
const data: {
742
zone: string;
743
lowCO2: boolean;
744
}[] = [];
745
for (const zone of v) {
746
data.push({
747
zone,
748
lowCO2: lowCO2.has(zone),
749
});
750
}
751
return data;
752
}
753
754
function Provisioning({ priceData, setConfig, configuration, disabled }) {
755
const [newSpot, setNewSpot] = useState<boolean>(!!configuration.spot);
756
const [prices, setPrices] = useState<{
757
spot: number | null;
758
standard: number;
759
discount: number;
760
} | null>(getSpotAndStandardPrices(priceData, configuration));
761
762
useEffect(() => {
763
setNewSpot(!!configuration.spot);
764
setPrices(getSpotAndStandardPrices(priceData, configuration));
765
}, [configuration]);
766
767
useEffect(() => {
768
if (configuration.spot && prices != null && !prices.spot) {
769
setNewSpot(false);
770
setConfig({ spot: false });
771
}
772
}, [prices, configuration.spot]);
773
774
return (
775
<div>
776
<div style={{ color: "#666", marginBottom: "5px" }}>
777
<b>
778
<Icon name="sliders" /> Provisioning
779
</b>
780
</div>
781
<Radio.Group
782
size="large"
783
buttonStyle="solid"
784
disabled={disabled}
785
value={newSpot ? "spot" : "standard"}
786
onChange={(e) => {
787
const spot = e.target.value == "standard" ? false : true;
788
setNewSpot(spot);
789
setConfig({ spot });
790
}}
791
>
792
<Radio.Button value="spot" disabled={!prices?.spot}>
793
Spot{" "}
794
{prices?.spot
795
? `${currency(prices.spot)}/hour (${prices.discount}% discount)`
796
: "(not available)"}{" "}
797
</Radio.Button>
798
<Radio.Button value="standard">
799
Standard{" "}
800
{prices != null ? `${currency(prices.standard)}/hour` : undefined}{" "}
801
</Radio.Button>
802
</Radio.Group>
803
<div style={{ color: "#666", marginTop: "5px" }}>
804
Standard VM's run until you stop them, whereas spot VM's are up to 91%
805
off, but will automatically stop when there is a surge in demand. Spot
806
instances might also not be available in a given region, so you may have
807
to try different regions.{" "}
808
{configuration.acceleratorType && (
809
<> GPU's are always in high demand.</>
810
)}
811
{newSpot && (
812
<Alert
813
style={{ margin: "5px 0" }}
814
type="warning"
815
showIcon
816
description={
817
<div style={{ maxWidth: "100%", lineHeight: 1 }}>
818
This is a heavily discounted spot instance. It will
819
automatically{" "}
820
{configuration.autoRestart ? " reboot if possible " : " stop "}{" "}
821
when there is a surge in demand.
822
{!disabled && (
823
<Popconfirm
824
title="Switch to Standard?"
825
description={
826
<div style={{ maxWidth: "450px" }}>
827
This will switch to a non-discounted standard instance,
828
which stays running even if there is high demand. You
829
can switch back to a spot instance using the blue toggle
830
above.
831
</div>
832
}
833
onConfirm={() => {
834
setNewSpot(false);
835
setConfig({ spot: false });
836
}}
837
okText="Switch to Standard"
838
cancelText="Cancel"
839
>
840
<Button type="link">Switch to Standard</Button>
841
</Popconfirm>
842
)}
843
{!configuration.autoRestart && (
844
<Popconfirm
845
title="Enable Automatic Restart?"
846
description={
847
<div style={{ maxWidth: "450px" }}>
848
CoCalc will automatically restart your compute server if
849
it is killed due to high demand. Note that there might
850
not be any compute resources available, in which case
851
you will have to wait for your server to start. You can
852
disable this in the "Automatically Restart" section
853
below.
854
</div>
855
}
856
onConfirm={() => {
857
setConfig({ autoRestart: true });
858
}}
859
okText="Enable Automatic Restart"
860
cancelText="Cancel"
861
>
862
<Button type="link">Enable Automatic Restart</Button>
863
</Popconfirm>
864
)}
865
</div>
866
}
867
/>
868
)}
869
</div>
870
</div>
871
);
872
}
873
874
function getSpotAndStandardPrices(priceData, configuration) {
875
try {
876
const standard = computeCost({
877
priceData,
878
configuration: { ...configuration, spot: false },
879
});
880
let spot: number | null = null;
881
try {
882
spot = computeCost({
883
priceData,
884
configuration: { ...configuration, spot: true },
885
});
886
} catch (_) {
887
// some machines have no spot instance support, eg h3's.
888
}
889
return {
890
standard,
891
spot,
892
discount: spot != null ? Math.round((1 - spot / standard) * 100) : 0,
893
};
894
} catch (_) {
895
return null;
896
}
897
}
898
899
function Zone({ priceData, setConfig, configuration, disabled }) {
900
const [newZone, setNewZone] = useState<string>(configuration.zone ?? "");
901
useEffect(() => {
902
setNewZone(configuration.zone);
903
}, [configuration.zone]);
904
905
const zones = getZones(priceData, configuration);
906
const options = zones.map(({ zone, lowCO2 }) => {
907
return {
908
value: zone,
909
search: `${zone} ${lowCO2 ? " co 2" : ""}`,
910
label: `${zone} ${lowCO2 ? " - 🍃 Low CO2" : ""}`,
911
};
912
});
913
914
return (
915
<div>
916
{configuration.machineType ? (
917
<div style={{ color: "#666", marginBottom: "5px" }}>
918
<b>
919
<Icon name="aim" /> Zone
920
</b>{" "}
921
in {configuration.region} with {configuration.machineType}{" "}
922
{configuration.spot ? "spot" : ""} VM's
923
</div>
924
) : undefined}
925
<Select
926
disabled={disabled}
927
style={{ width: SELECTOR_WIDTH }}
928
options={options}
929
value={newZone}
930
onChange={(zone) => {
931
setNewZone(zone);
932
setConfig({ zone });
933
}}
934
showSearch
935
optionFilterProp="children"
936
filterOption={filterOption}
937
/>
938
</div>
939
);
940
}
941
942
function MachineType({ priceData, setConfig, configuration, disabled, state }) {
943
const [archType, setArchType] = useState<Architecture>(
944
getArchitecture(configuration),
945
);
946
const [sortByPrice, setSortByPrice] = useState<boolean>(true);
947
const [newMachineType, setNewMachineType] = useState<string>(
948
configuration.machineType ?? "",
949
);
950
951
useEffect(() => {
952
setNewMachineType(configuration.machineType);
953
setArchType(getArchitecture(configuration));
954
}, [configuration.machineType]);
955
956
useEffect(() => {
957
if (archType == "arm64" && getArchitecture(configuration) != "arm64") {
958
setNewMachineType("t2a-standard-4");
959
setConfig({ machineType: "t2a-standard-4" });
960
return;
961
}
962
if (archType == "x86_64" && getArchitecture(configuration) == "arm64") {
963
setNewMachineType("t2d-standard-4");
964
setConfig({ machineType: "t2d-standard-4" });
965
return;
966
}
967
}, [archType, configuration.machineType]);
968
969
const machineTypes = Object.keys(priceData.machineTypes);
970
const allOptions = machineTypes
971
.filter((machineType) => {
972
const { acceleratorType } = configuration;
973
if (!acceleratorType) {
974
if (machineType.startsWith("g") || machineType.startsWith("a")) {
975
return false;
976
}
977
// Check if the machine type's architecture matches the selected architecture
978
const machineArch = getMachineTypeArchitecture(machineType);
979
if (archType == "arm64" && machineArch != "arm64") {
980
return false;
981
}
982
if (archType == "x86_64" && machineArch != "x86_64") {
983
return false;
984
}
985
} else {
986
if (acceleratorType == "nvidia-tesla-t4") {
987
return machineType.startsWith("n1-");
988
} else {
989
const machines =
990
priceData.accelerators[acceleratorType].machineType[
991
configuration.acceleratorCount ?? 1
992
] ?? [];
993
return machines.includes(machineType);
994
}
995
}
996
997
return true;
998
})
999
.map((machineType) => {
1000
let cost;
1001
try {
1002
cost = computeInstanceCost({
1003
priceData,
1004
configuration: { ...configuration, machineType },
1005
});
1006
} catch (_) {
1007
cost = null;
1008
}
1009
const data = priceData.machineTypes[machineType];
1010
const { memory, vcpu } = data;
1011
return {
1012
value: machineType,
1013
search: machineType + ` memory:${memory} ram:${memory} cpu:${vcpu} `,
1014
cost,
1015
label: (
1016
<div key={machineType} style={{ display: "flex" }}>
1017
<div style={{ flex: 1 }}>{machineType}</div>
1018
<div style={{ flex: 1 }}>
1019
{cost ? (
1020
<CostPerHour cost={cost} />
1021
) : (
1022
<span style={{ color: "#666" }}>(region/zone changes)</span>
1023
)}
1024
</div>
1025
<div style={{ flex: 2 }}>
1026
<RamAndCpu machineType={machineType} priceData={priceData} />
1027
</div>
1028
</div>
1029
),
1030
};
1031
});
1032
const options = [
1033
{
1034
label: "Machine Types",
1035
options: allOptions.filter((x) => x.cost),
1036
},
1037
{
1038
label: "Location Will Change",
1039
options: allOptions.filter((x) => !x.cost),
1040
},
1041
];
1042
1043
if (sortByPrice) {
1044
options[0].options.sort((a, b) => {
1045
return cmp(a.cost, b.cost);
1046
});
1047
}
1048
1049
return (
1050
<div>
1051
<div style={{ color: "#666", marginBottom: "5px" }}>
1052
<Tooltip
1053
title={
1054
(state ?? "deprovisioned") != "deprovisioned"
1055
? "Can only be changed when machine is deprovisioned"
1056
: archType == "x86_64"
1057
? "Intel or AMD X86_64 architecture machines"
1058
: "ARM64 architecture machines"
1059
}
1060
>
1061
<Radio.Group
1062
style={{ float: "right" }}
1063
disabled={
1064
disabled ||
1065
configuration.acceleratorType ||
1066
(state ?? "deprovisioned") != "deprovisioned"
1067
}
1068
options={[
1069
{ value: "x86_64", label: "X86_64" },
1070
{ value: "arm64", label: "ARM64" },
1071
]}
1072
value={archType}
1073
onChange={({ target: { value } }) => {
1074
setArchType(value);
1075
}}
1076
/>
1077
</Tooltip>
1078
<b>
1079
<Icon name="microchip" /> Machine Type
1080
</b>
1081
</div>
1082
<div>
1083
<Select
1084
disabled={disabled}
1085
style={{ width: "100%" }}
1086
options={options as any}
1087
value={newMachineType}
1088
onChange={(machineType) => {
1089
setNewMachineType(machineType);
1090
setConfig({ machineType });
1091
}}
1092
showSearch
1093
optionFilterProp="children"
1094
filterOption={filterOption}
1095
/>
1096
</div>
1097
<div>
1098
<Checkbox
1099
disabled={disabled}
1100
style={{ marginTop: "5px" }}
1101
checked={sortByPrice}
1102
onChange={() => setSortByPrice(!sortByPrice)}
1103
>
1104
Sort by price
1105
</Checkbox>
1106
</div>
1107
<div style={{ color: "#666", marginTop: "5px" }}>
1108
Prices and availability depend on the region and provisioning type, so
1109
adjust those below to find the best overall value. Price above is just
1110
for the machine, and not the disk or GPU. Search for <code>cpu:4⌴</code>{" "}
1111
and <code>ram:8⌴</code> to only show options with 4 vCPUs and 8GB RAM.
1112
</div>
1113
</div>
1114
);
1115
}
1116
1117
function BootDisk(props) {
1118
return (
1119
<Disk
1120
{...props}
1121
minSizeGb={getMinDiskSizeGb(props)}
1122
maxSizeGb={65536}
1123
computeDiskCost={computeDiskCost}
1124
/>
1125
);
1126
}
1127
1128
function Image(props) {
1129
const { state = "deprovisioned" } = props;
1130
return (
1131
<div>
1132
<div style={{ color: "#666", marginBottom: "5px" }}>
1133
<b>
1134
<Icon name="disk-round" /> Image
1135
</b>
1136
</div>
1137
{state == "deprovisioned" && (
1138
<div style={{ color: "#666", marginBottom: "5px" }}>
1139
Select compute server image. You will be able to use sudo as root with
1140
no password, and can install anything into the Ubuntu Linux image,
1141
including commercial software.
1142
</div>
1143
)}
1144
<SelectImage {...props} />
1145
{state != "deprovisioned" && (
1146
<div style={{ color: "#666", marginTop: "5px" }}>
1147
You can only edit the image when server is deprovisioned.
1148
</div>
1149
)}
1150
<div style={{ color: "#666", marginTop: "5px" }}>
1151
<ImageDescription configuration={props.configuration} />
1152
</div>
1153
</div>
1154
);
1155
}
1156
1157
// We do NOT include the P4, P100, V100 or K80, which are older
1158
// and for which our base image and drivers don't work.
1159
// If for some reason we need them, we will have to switch to
1160
// different base drivers or have even more images.
1161
1162
// NOTE: H200 disabled because it requires a reservation.
1163
1164
const ACCELERATOR_TYPES = [
1165
"nvidia-tesla-t4",
1166
"nvidia-l4",
1167
"nvidia-tesla-a100",
1168
"nvidia-a100-80gb",
1169
"nvidia-h100-80gb",
1170
// "nvidia-h200-141gb",
1171
// these are too hard to properly keep software image for:
1172
// "nvidia-tesla-v100",
1173
//"nvidia-tesla-p100",
1174
//"nvidia-tesla-p4",
1175
];
1176
1177
/*
1178
<A href="https://www.nvidia.com/en-us/data-center/tesla-p100/">P100</A>,{" "}
1179
<A href="https://www.nvidia.com/en-us/data-center/v100/">V100</A>,{" "}
1180
<A href="https://www.nvidia.com/content/dam/en-zz/Solutions/design-visualization/solutions/resources/documents1/nvidia-p4-datasheet.pdf">
1181
P4
1182
</A>
1183
*/
1184
1185
function GPU({
1186
priceData,
1187
setConfig,
1188
configuration,
1189
disabled,
1190
state,
1191
IMAGES,
1192
setCloud,
1193
}) {
1194
const { acceleratorType, acceleratorCount } = configuration;
1195
const head = (
1196
<div style={{ color: "#666", marginBottom: "5px" }}>
1197
<b>
1198
<Icon style={{ float: "right", fontSize: "50px" }} name="gpu" />
1199
<Icon name="cube" /> NVIDIA GPU{" "}
1200
<div style={{ float: "right" }}>
1201
<A href="https://www.nvidia.com/content/dam/en-zz/Solutions/design-visualization/solutions/resources/documents1/Datasheet_NVIDIA_T4_Virtualization.pdf">
1202
T4
1203
</A>
1204
, <A href="https://www.nvidia.com/en-us/data-center/l4/">L4</A>,{" "}
1205
<A href="https://www.nvidia.com/en-us/data-center/a100/">A100</A>,{" "}
1206
<A href="https://www.nvidia.com/en-us/data-center/h100/">H100</A>
1207
</div>
1208
</b>
1209
</div>
1210
);
1211
1212
const theSwitch = (
1213
<Switch
1214
disabled={disabled || (state ?? "deprovisioned") != "deprovisioned"}
1215
checkedChildren={"NVIDIA GPU"}
1216
unCheckedChildren={"NO GPU"}
1217
checked={!!acceleratorType}
1218
onChange={() => {
1219
if (!!acceleratorType) {
1220
setConfig({ acceleratorType: "", acceleratorCount: 0 });
1221
} else {
1222
setConfig({
1223
...DEFAULT_GPU_CONFIG,
1224
spot: configuration?.spot ?? false,
1225
});
1226
}
1227
}}
1228
/>
1229
);
1230
if (!acceleratorType) {
1231
return (
1232
<div>
1233
{head}
1234
{theSwitch}
1235
</div>
1236
);
1237
}
1238
1239
const options = ACCELERATOR_TYPES.filter(
1240
(acceleratorType) => priceData.accelerators[acceleratorType] != null,
1241
).map((acceleratorType: GoogleCloudAcceleratorType) => {
1242
let cost;
1243
const config1 = { ...configuration, acceleratorType, acceleratorCount };
1244
const changes = { acceleratorType, acceleratorCount };
1245
try {
1246
cost = computeAcceleratorCost({ priceData, configuration: config1 });
1247
} catch (_) {
1248
const newChanges = ensureConsistentConfiguration(
1249
priceData,
1250
config1,
1251
changes,
1252
IMAGES,
1253
);
1254
cost = computeAcceleratorCost({
1255
priceData,
1256
configuration: { ...config1, ...newChanges },
1257
});
1258
}
1259
const memory = priceData.accelerators[acceleratorType].memory;
1260
return {
1261
value: acceleratorType,
1262
search: acceleratorType,
1263
cost,
1264
memory,
1265
label: (
1266
<div key={acceleratorType} style={{ display: "flex" }}>
1267
<div style={{ flex: 1 }}>
1268
{displayAcceleratorType(acceleratorType, memory)}
1269
</div>
1270
<div style={{ flex: 1 }}>
1271
<CostPerHour cost={cost} />
1272
</div>
1273
</div>
1274
),
1275
};
1276
});
1277
1278
const countOptions: any[] = [];
1279
const min = priceData.accelerators[acceleratorType]?.count ?? 1;
1280
const max = priceData.accelerators[acceleratorType]?.max ?? 1;
1281
for (let i = min; i <= max; i *= 2) {
1282
countOptions.push({ label: `${i}`, value: i });
1283
}
1284
1285
return (
1286
<div>
1287
{head}
1288
{theSwitch}
1289
<div style={{ marginTop: "15px" }}>
1290
<Select
1291
disabled={disabled || (state ?? "deprovisioned") != "deprovisioned"}
1292
style={{ width: SELECTOR_WIDTH }}
1293
options={options as any}
1294
value={acceleratorType}
1295
onChange={(type) => {
1296
setConfig({ acceleratorType: type });
1297
// todo -- change count if necessary
1298
}}
1299
showSearch
1300
optionFilterProp="children"
1301
filterOption={filterOption}
1302
/>
1303
<Select
1304
style={{ marginLeft: "15px", width: "75px" }}
1305
disabled={disabled || (state ?? "deprovisioned") != "deprovisioned"}
1306
options={countOptions}
1307
value={acceleratorCount}
1308
onChange={(count) => {
1309
setConfig({ acceleratorCount: count });
1310
}}
1311
/>
1312
{acceleratorCount && acceleratorType && (
1313
<div style={{ color: "#666", marginTop: "10px" }}>
1314
You have selected {acceleratorCount} dedicated{" "}
1315
<b>{displayAcceleratorType(acceleratorType)}</b>{" "}
1316
{plural(acceleratorCount, "GPU")}, with a total of{" "}
1317
<b>
1318
{priceData.accelerators[acceleratorType].memory *
1319
acceleratorCount}
1320
GB GPU RAM
1321
</b>
1322
.{" "}
1323
{acceleratorCount > 1 && (
1324
<>
1325
The {acceleratorCount} GPUs will be available on the same
1326
server.
1327
</>
1328
)}
1329
{
1330
(state ?? "deprovisioned") != "deprovisioned" && (
1331
<div>
1332
You can only change the GPU configuration when the server is
1333
deprovisioned.
1334
</div>
1335
) /* this is mostly a google limitation, not cocalc, though we will eventually do somthing involving recreating the machine. BUT note that e.g., changing the count for L4's actually breaks booting up! */
1336
}
1337
{setCloud != null &&
1338
availableClouds().includes("hyperstack") &&
1339
(state ?? "deprovisioned") == "deprovisioned" && (
1340
<Alert
1341
showIcon
1342
style={{ margin: "10px 0 5px 0" }}
1343
type="warning"
1344
description={
1345
<div>
1346
We have partnered with Hyperstack cloud to provide NVIDIA
1347
H100, A100, L40, and RTX-A4/5/6000 GPUs at a{" "}
1348
<b>much cheaper price</b> than Google cloud.{" "}
1349
<Popconfirm
1350
title="Switch to Hyperstack"
1351
description={
1352
<div style={{ maxWidth: "450px" }}>
1353
This will change the cloud for this compute server
1354
to Hyperstack, and reset its configuration. Your
1355
compute server is not storing any data so this is
1356
safe.
1357
</div>
1358
}
1359
onConfirm={() => {
1360
setCloud("hyperstack");
1361
}}
1362
okText="Switch to Hyperstack"
1363
cancelText="Cancel"
1364
>
1365
<Button type="link">Switch...</Button>
1366
</Popconfirm>
1367
</div>
1368
}
1369
/>
1370
)}
1371
</div>
1372
)}
1373
</div>
1374
</div>
1375
);
1376
}
1377
/*
1378
{acceleratorType?.includes("a100") && configuration.spot ? (
1379
<div style={{ marginTop: "5px", color: "#666" }}>
1380
<b>WARNING:</b> A100 spot instances are rarely available. Consider
1381
standard provisioning instead.
1382
</div>
1383
) : undefined}
1384
*/
1385
1386
function ensureConsistentConfiguration(
1387
priceData,
1388
configuration: GoogleCloudConfigurationType,
1389
changes: Partial<GoogleCloudConfigurationType>,
1390
IMAGES: Images,
1391
) {
1392
const newConfiguration = { ...configuration, ...changes };
1393
const newChanges = { ...changes };
1394
ensureConsistentImage(newConfiguration, newChanges, IMAGES);
1395
ensureConsistentAccelerator(priceData, newConfiguration, newChanges);
1396
ensureConsistentNvidiaL4andA100(priceData, newConfiguration, newChanges);
1397
ensureConsistentZoneWithRegion(priceData, newConfiguration, newChanges);
1398
ensureConsistentRegionAndZoneWithMachineType(
1399
priceData,
1400
newConfiguration,
1401
newChanges,
1402
);
1403
ensureSufficientDiskSize(newConfiguration, newChanges, IMAGES);
1404
ensureConsistentDiskType(priceData, newConfiguration, newChanges);
1405
1406
return newChanges;
1407
}
1408
1409
// We make the image consistent with the gpu selection.
1410
function ensureConsistentImage(configuration, changes, IMAGES) {
1411
const { gpu } = IMAGES[configuration.image] ?? {};
1412
const gpuSelected =
1413
configuration.acceleratorType && configuration.acceleratorCount > 0;
1414
if (gpu == gpuSelected) {
1415
// they are consistent
1416
return;
1417
}
1418
if (gpu && !gpuSelected) {
1419
// GPU image but non-GPU machine -- change image to non-GPU
1420
configuration["image"] = changes["image"] = "python";
1421
configuration["tag"] = changes["tag"] = null;
1422
} else if (!gpu && gpuSelected) {
1423
// GPU machine but not image -- change image to pytorch
1424
configuration["image"] = changes["image"] = "pytorch";
1425
configuration["tag"] = changes["tag"] = null;
1426
}
1427
}
1428
1429
function ensureSufficientDiskSize(configuration, changes, IMAGES) {
1430
const min = getMinDiskSizeGb({ configuration, IMAGES });
1431
if ((configuration.diskSizeGb ?? 0) < min) {
1432
changes.diskSizeGb = min;
1433
}
1434
}
1435
1436
function ensureConsistentDiskType(priceData, configuration, changes) {
1437
const { machineType } = configuration;
1438
const m = machineType.split("-")[0];
1439
if (configuration.diskType == "hyperdisk-balanced") {
1440
// make sure machine is supported
1441
const { supportedMachineTypes } = priceData.extra["hyperdisk-balanced"];
1442
if (!supportedMachineTypes.includes(m)) {
1443
// can't use hyperdisk on this machine, so fix.
1444
configuration.diskType = changes.diskType = "pd-balanced";
1445
}
1446
} else {
1447
const { requiredMachineTypes } = priceData.extra["hyperdisk-balanced"];
1448
if (requiredMachineTypes.includes(m)) {
1449
// must use hyperdisk on this machine, so fix.
1450
configuration.diskType = changes.diskType = "hyperdisk-balanced";
1451
}
1452
}
1453
}
1454
1455
function ensureConsistentZoneWithRegion(priceData, configuration, changes) {
1456
if (configuration.zone.startsWith(configuration.region)) {
1457
return;
1458
}
1459
if (changes["region"]) {
1460
// currently changing region, so set a zone that matches the region
1461
for (const zone in priceData.zones) {
1462
if (zone.startsWith(configuration.region)) {
1463
configuration["zone"] = changes["zone"] = zone;
1464
break;
1465
}
1466
}
1467
} else {
1468
// probably changing the zone, so set the region from the zone
1469
configuration["region"] = changes["region"] = zoneToRegion(
1470
configuration.zone,
1471
);
1472
}
1473
}
1474
1475
function ensureConsistentAccelerator(priceData, configuration, changes) {
1476
let { acceleratorType } = configuration;
1477
if (!acceleratorType) {
1478
return;
1479
}
1480
if (
1481
acceleratorType == "nvidia-tesla-a100" ||
1482
acceleratorType == "nvidia-a100-80gb" ||
1483
acceleratorType == "nvidia-l4"
1484
) {
1485
// L4 and A100 are handled elsewhere.
1486
return;
1487
}
1488
1489
// have a GPU
1490
let data = priceData.accelerators[acceleratorType];
1491
if (!data) {
1492
// accelerator type no longer exists; replace it by one that does.
1493
for (const type in priceData.accelerators) {
1494
acceleratorType =
1495
configuration["acceleratorType"] =
1496
changes["acceleratorType"] =
1497
type;
1498
data = priceData.accelerators[acceleratorType];
1499
break;
1500
}
1501
}
1502
if (data == null) {
1503
throw Error("bug");
1504
}
1505
// Ensure the machine type is consistent
1506
if (!configuration.machineType.startsWith(data.machineType)) {
1507
if (changes["machineType"]) {
1508
// if you are explicitly changing the machine type, then we respect
1509
// that and disabled the gpu
1510
configuration["acceleratorType"] = changes["acceleratorType"] = "";
1511
configuration["acceleratorCount"] = changes["acceleratorCount"] = 0;
1512
return;
1513
} else {
1514
// changing something else, so we fix the machine type
1515
for (const type in priceData.machineTypes) {
1516
if (type.startsWith(data.machineType)) {
1517
configuration["machineType"] = changes["machineType"] =
1518
type.startsWith("n1-") ? DEFAULT_GPU_INSTANCE : type;
1519
break;
1520
}
1521
}
1522
}
1523
}
1524
ensureZoneIsConsistentWithGPU(priceData, configuration, changes);
1525
1526
// Ensure the count is consistent
1527
const count = configuration.acceleratorCount ?? 0;
1528
if (count < data.count) {
1529
changes["acceleratorCount"] = data.count;
1530
} else if (count > data.max) {
1531
changes["acceleratorCount"] = data.max;
1532
}
1533
}
1534
1535
function ensureZoneIsConsistentWithGPU(priceData, configuration, changes) {
1536
if (!configuration.acceleratorType) return;
1537
1538
const data = priceData.accelerators[configuration.acceleratorType];
1539
if (!data) {
1540
// invalid acceleratorType.
1541
return;
1542
}
1543
1544
// Ensure the region/zone is consistent with accelerator type
1545
const prices = data[configuration.spot ? "spot" : "prices"];
1546
if (prices[configuration.zone] == null) {
1547
// there are no GPUs in the selected zone of the selected type.
1548
// If you just explicitly changed the GPU type, then we fix this by changing the zone.
1549
if (changes["acceleratorType"] != null) {
1550
// fix the region and zone
1551
// find cheapest zone in the world.
1552
let price = 999999999;
1553
let zoneChoice = "";
1554
for (const zone in prices) {
1555
if (prices[zone] < price) {
1556
price = prices[zone];
1557
zoneChoice = zone;
1558
}
1559
}
1560
if (zoneChoice) {
1561
changes["zone"] = configuration["zone"] = zoneChoice;
1562
changes["region"] = configuration["region"] = zoneToRegion(zoneChoice);
1563
return;
1564
}
1565
} else {
1566
// You did not change the GPU type, so we disable the GPU
1567
configuration["acceleratorType"] = changes["acceleratorType"] = "";
1568
configuration["acceleratorCount"] = changes["acceleratorCount"] = 0;
1569
return;
1570
}
1571
}
1572
}
1573
1574
// The Nvidia L4 and A100 are a little different, etc.
1575
function ensureConsistentNvidiaL4andA100(priceData, configuration, changes) {
1576
const { machineType, acceleratorType } = configuration;
1577
1578
// L4 or A100 GPU machine type, but switching to no GPU, so we have
1579
// to change the machine type
1580
if (
1581
machineType.startsWith("g2-") ||
1582
machineType.startsWith("a2-") ||
1583
machineType.startsWith("a3-")
1584
) {
1585
if (!acceleratorType) {
1586
// Easy case -- the user is explicitly changing the GPU from being set
1587
// to NOT be set, and the GPU is L4 or A100. In this case,
1588
// we just set the machine type to some non-gpu type
1589
// and we're done.
1590
configuration.machineType = changes.machineType = FALLBACK_INSTANCE;
1591
return;
1592
}
1593
}
1594
if (
1595
acceleratorType != "nvidia-h200-141gb" &&
1596
acceleratorType != "nvidia-h100-80gb" &&
1597
acceleratorType != "nvidia-tesla-a100" &&
1598
acceleratorType != "nvidia-a100-80gb" &&
1599
acceleratorType != "nvidia-l4"
1600
) {
1601
// We're not switching to an A100 or L4, so not handled further here.
1602
return;
1603
}
1604
1605
if (!configuration.acceleratorCount) {
1606
configuration.acceleratorCount = changes.acceleratorCount = 1;
1607
}
1608
1609
// Ensure machine type is consistent with the GPU and count we're switching to.
1610
let machineTypes =
1611
priceData.accelerators[acceleratorType]?.machineType[
1612
configuration.acceleratorCount
1613
];
1614
if (machineTypes == null) {
1615
configuration.acceleratorCount = changes.acceleratorCount = 1;
1616
machineTypes =
1617
priceData.accelerators[acceleratorType]?.machineType[
1618
configuration.acceleratorCount
1619
];
1620
1621
if (machineTypes == null) {
1622
// maybe 1 gpu isn't allowed, e.g., with H200
1623
const machineType = priceData.accelerators[acceleratorType]?.machineType;
1624
if (machineType != null) {
1625
for (const count in machineType) {
1626
configuration.acceleratorCount = changes.acceleratorCount =
1627
parseInt(count);
1628
machineTypes =
1629
priceData.accelerators[acceleratorType]?.machineType[
1630
configuration.acceleratorCount
1631
];
1632
}
1633
}
1634
}
1635
}
1636
if (machineTypes == null) {
1637
throw Error("bug -- this can't happen");
1638
}
1639
1640
if (!machineTypes.includes(configuration.machineType)) {
1641
configuration.machineType = changes.machineType =
1642
machineTypes[0].startsWith("n1-")
1643
? DEFAULT_GPU_INSTANCE
1644
: machineTypes[0];
1645
}
1646
}
1647
1648
function ensureConsistentRegionAndZoneWithMachineType(
1649
priceData,
1650
configuration,
1651
changes,
1652
) {
1653
// Specifically selecting a machine type. We make this the
1654
// highest priority, so if you are changing this, we make everything
1655
// else fit it.
1656
const machineType = configuration["machineType"];
1657
if (priceData.machineTypes[machineType] == null) {
1658
console.warn(
1659
`BUG -- This should never happen: unknown machineType = '${machineType}'`,
1660
);
1661
// invalid machineType
1662
if (configuration.acceleratorType) {
1663
configuration["machineType"] = changes["machineType"] =
1664
DEFAULT_GPU_INSTANCE;
1665
} else {
1666
configuration["machineType"] = changes["machineType"] = FALLBACK_INSTANCE;
1667
}
1668
return;
1669
}
1670
1671
const i = machineType.indexOf("-");
1672
const prefix = machineType.slice(0, i);
1673
1674
let zoneHasMachineType = (
1675
priceData.zones[configuration.zone]?.machineTypes ?? []
1676
).includes(prefix);
1677
const regionToCost =
1678
priceData.machineTypes[machineType][
1679
configuration.spot ? "spot" : "prices"
1680
] ?? {};
1681
const regionHasMachineType = regionToCost[configuration.region] != null;
1682
1683
if (!regionHasMachineType) {
1684
// Our machine type is not in the currently selected region,
1685
// so find cheapest region with our requested machine type.
1686
let price = 1e8;
1687
for (const region in regionToCost) {
1688
if (regionToCost[region] < price) {
1689
price = regionToCost[region];
1690
configuration["region"] = changes["region"] = region;
1691
// since we changed the region:
1692
zoneHasMachineType = false;
1693
}
1694
}
1695
}
1696
if (!zoneHasMachineType) {
1697
// now the region has the machine type, but the zone doesn't (or
1698
// region changed so zone has to change).
1699
// So we find some zone with the machine in that region
1700
for (const zone in priceData.zones) {
1701
if (zone.startsWith(configuration["region"])) {
1702
if ((priceData.zones[zone]?.machineTypes ?? []).includes(prefix)) {
1703
configuration["zone"] = changes["zone"] = zone;
1704
break;
1705
}
1706
}
1707
}
1708
}
1709
1710
if (configuration.acceleratorType && configuration.acceleratorCount) {
1711
if (priceData.accelerators[configuration.acceleratorType] == null) {
1712
// The accelerator type no longer exists in the pricing data (e.g., maybe it was deprecated),
1713
// so replace it by one that exists.
1714
for (const type in priceData.accelerators) {
1715
configuration.acceleratorType = changes.acceleratorType = type;
1716
break;
1717
}
1718
}
1719
// have a GPU -- make sure zone works
1720
if (
1721
!priceData.accelerators[configuration.acceleratorType].prices[
1722
configuration.zone
1723
]
1724
) {
1725
// try to find a different zone in the region that works
1726
let fixed = false;
1727
const region = zoneToRegion(configuration["zone"]);
1728
for (const zone in priceData.accelerators[configuration.acceleratorType]
1729
?.prices) {
1730
if (zone.startsWith(region)) {
1731
fixed = true;
1732
configuration.zone = changes.zone = zone;
1733
break;
1734
}
1735
}
1736
if (!fixed) {
1737
// just choose cheapest zone in some region
1738
const zone = cheapestZone(
1739
priceData.accelerators[configuration.acceleratorType][
1740
configuration.spot ? "spot" : "prices"
1741
],
1742
);
1743
configuration.zone = changes.zone = zone;
1744
configuration.region = changes.region = zoneToRegion(zone);
1745
}
1746
}
1747
}
1748
}
1749
1750
function zoneToRegion(zone: string): string {
1751
const i = zone.lastIndexOf("-");
1752
return zone.slice(0, i);
1753
}
1754
1755
function Network({ setConfig, configuration, loading, priceData }) {
1756
const [externalIp, setExternalIp] = useState<boolean>(
1757
configuration.externalIp ?? true,
1758
);
1759
useEffect(() => {
1760
setExternalIp(configuration.externalIp ?? true);
1761
}, [configuration.externalIp]);
1762
1763
return (
1764
<div>
1765
<div style={{ color: "#666", marginBottom: "5px" }}>
1766
<b>
1767
<Icon name="network" /> Network
1768
</b>
1769
<br />
1770
All compute servers on Google cloud have full network access with
1771
unlimited data transfer in for free. Data transfer out{" "}
1772
<b>costs {currency(DATA_TRANSFER_OUT_COST_PER_GiB)}/GiB</b>.
1773
</div>
1774
<Checkbox
1775
checked={externalIp}
1776
disabled={
1777
true /* compute servers can't work without external ip or Cloud NAT (which costs a lot), so changing this always disabled. Before: disabled || (state ?? "deprovisioned") != "deprovisioned"*/
1778
}
1779
onChange={() => {
1780
setExternalIp(!externalIp);
1781
setConfig({ externalIp: !externalIp });
1782
}}
1783
>
1784
External IP Address
1785
</Checkbox>
1786
<div style={{ marginTop: "5px" }}>
1787
<Typography.Paragraph
1788
style={{ color: "#666" }}
1789
ellipsis={{
1790
expandable: true,
1791
rows: 2,
1792
symbol: "more",
1793
}}
1794
>
1795
{/* TODO: we can and will in theory support all this without external
1796
ip using a gateway. E.g., google cloud shell has ssh to host, etc. */}
1797
An external IP address is required and costs{" "}
1798
{configuration.spot
1799
? `${currency(
1800
markup({ cost: EXTERNAL_IP_COST.spot, priceData }),
1801
)}/hour`
1802
: `${currency(
1803
markup({
1804
cost: EXTERNAL_IP_COST.standard,
1805
priceData,
1806
}),
1807
)}/hour`}{" "}
1808
while the VM is running (there is no charge when not running).
1809
</Typography.Paragraph>
1810
</div>
1811
{externalIp && (
1812
<DNS
1813
setConfig={setConfig}
1814
configuration={configuration}
1815
loading={loading}
1816
/>
1817
)}
1818
</div>
1819
);
1820
}
1821
1822
function cheapestZone(costs: { [zone: string]: number }): string {
1823
let price = 99999999999999999;
1824
let choice = "";
1825
for (const zone in costs) {
1826
if (costs[zone] < price) {
1827
choice = zone;
1828
price = costs[zone];
1829
}
1830
}
1831
return choice;
1832
}
1833
1834
function CostPerHour({
1835
cost,
1836
extra,
1837
style,
1838
}: {
1839
cost?: number;
1840
extra?;
1841
style?;
1842
}) {
1843
if (cost == null) {
1844
return null;
1845
}
1846
return (
1847
<div style={{ fontFamily: "monospace", ...style }}>
1848
{currency(cost)}/hour
1849
{extra}
1850
</div>
1851
);
1852
}
1853
1854
function Admin({ id, configuration, loading, template }) {
1855
const isAdmin = useTypedRedux("account", "is_admin");
1856
const [error, setError] = useState<string>("");
1857
const [calling, setCalling] = useState<boolean>(false);
1858
if (!isAdmin) {
1859
return null;
1860
}
1861
return (
1862
<div>
1863
<div style={{ color: "#666", marginBottom: "5px" }}>
1864
<b>
1865
<Icon name="users" /> Admin
1866
</b>
1867
<br />
1868
Settings and functionality only available to admins.
1869
<br />
1870
<ShowError error={error} setError={setError} />
1871
<Tooltip title="Once you have tested the currently selected image, click this button to mark it as tested.">
1872
<Button
1873
disabled={loading || !id || calling}
1874
onClick={async () => {
1875
try {
1876
setCalling(true);
1877
await setImageTested({ id, tested: true });
1878
// force reload to database via GCP api call
1879
await reloadImages("compute_servers_images_google", true);
1880
} catch (err) {
1881
setError(`${err}`);
1882
} finally {
1883
setCalling(false);
1884
}
1885
}}
1886
>
1887
Mark Google Cloud Image Tested{" "}
1888
{calling && <Spin style={{ marginLeft: "15px" }} />}
1889
</Button>
1890
</Tooltip>
1891
<pre>
1892
id={id}, configuration={JSON.stringify(configuration, undefined, 2)}
1893
</pre>
1894
<Template id={id} template={template} />
1895
</div>
1896
</div>
1897
);
1898
}
1899
1900