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