Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/frontend/compute/google-cloud-config.tsx
Views: 687
import type {1Images,2State,3GoogleCloudConfiguration as GoogleCloudConfigurationType,4ComputeServerTemplate,5GoogleCloudAcceleratorType,6} from "@cocalc/util/db-schema/compute-servers";7import { reloadImages, useImages, useGoogleImages } from "./images-hook";8import { GOOGLE_CLOUD_DEFAULTS } from "@cocalc/util/db-schema/compute-servers";9import { getMinDiskSizeGb } from "@cocalc/util/db-schema/compute-servers";10import {11Alert,12Button,13Checkbox,14Divider,15Popconfirm,16Radio,17Select,18Spin,19Switch,20Table,21Tooltip,22Typography,23} from "antd";24import { currency, cmp, plural } from "@cocalc/util/misc";25import computeCost, {26GoogleCloudData,27EXTERNAL_IP_COST,28DATA_TRANSFER_OUT_COST_PER_GiB,29markup,30computeAcceleratorCost,31computeInstanceCost,32computeDiskCost,33} from "@cocalc/util/compute/cloud/google-cloud/compute-cost";34import {35getGoogleCloudPriceData,36setImageTested,37setServerConfiguration,38} from "./api";39import { useEffect, useState } from "react";40import { A } from "@cocalc/frontend/components/A";41import { Icon } from "@cocalc/frontend/components/icon";42import { isEqual } from "lodash";43import { useTypedRedux } from "@cocalc/frontend/app-framework";44import SelectImage, { ImageLinks, ImageDescription } from "./select-image";45import Ephemeral from "./ephemeral";46import AutoRestart from "./auto-restart";47import AllowCollaboratorControl from "./allow-collaborator-control";48import NestedVirtualization from "./nested-virtualization";49import ShowError from "@cocalc/frontend/components/error";50import Proxy from "./proxy";51import CostOverview from "./cost-overview";52import Disk from "@cocalc/frontend/compute/cloud/common/disk";53import DNS from "@cocalc/frontend/compute/cloud/common/dns";54import ExcludeFromSync from "@cocalc/frontend/compute/exclude-from-sync";55import { availableClouds } from "./config";56import Template from "@cocalc/frontend/compute/cloud/common/template";57import Specs, {58RamAndCpu,59} from "@cocalc/frontend/compute/cloud/google-cloud/specs";60import { displayAcceleratorType } from "@cocalc/frontend/compute/cloud/google-cloud/accelerator";61import { filterOption } from "@cocalc/frontend/compute/util";6263export const SELECTOR_WIDTH = "350px";6465export const DEFAULT_GPU_CONFIG = GOOGLE_CLOUD_DEFAULTS.gpu2;6667// {68// acceleratorType: "nvidia-l4",69// acceleratorCount: 1,70// machineType: "g2-standard-4",71// region: "us-central1",72// zone: "us-central1-b",73// image: "pytorch",74// };7576const FALLBACK_INSTANCE = "n2-standard-4";77// an n1-standard-1 is SO dinky it causes huge trouble78// with downloading/processing models.79const DEFAULT_GPU_INSTANCE = "n1-highmem-2";8081interface ConfigurationType extends GoogleCloudConfigurationType {82valid?: boolean;83}8485interface Props {86configuration: ConfigurationType;87editable?: boolean;88// if id not set, then doesn't try to save anything to the backend89id?: number;90project_id?: string;91// called whenever changes are made.92onChange?: (configuration: ConfigurationType) => void;93disabled?: boolean;94state?: State;95data?;96setCloud?;97template?: ComputeServerTemplate;98}99100export default function GoogleCloudConfiguration({101configuration: configuration0,102editable,103id,104project_id,105onChange,106disabled,107state,108data,109setCloud,110template,111}: Props) {112const [IMAGES, ImagesError] = useImages();113const [googleImages, ImagesErrorGoogle] = useGoogleImages();114const [loading, setLoading] = useState<boolean>(false);115const [cost, setCost] = useState<number | null>(null);116const [priceData, setPriceData] = useState<GoogleCloudData | null>(null);117const [error, setError0] = useState<string>("");118const [configuration, setLocalConfiguration] =119useState<ConfigurationType>(configuration0);120const setError = (error) => {121setError0(error);122const valid = !error;123if (onChange != null && configuration.valid != valid) {124onChange({ ...configuration, valid });125}126};127128useEffect(() => {129if (!editable) {130setLocalConfiguration(configuration0);131}132}, [configuration0]);133134useEffect(() => {135(async () => {136try {137setLoading(true);138const data = await getGoogleCloudPriceData();139setPriceData(data);140} catch (err) {141setError(`${err}`);142} finally {143setLoading(false);144}145})();146}, []);147148useEffect(() => {149if (!editable || configuration == null || priceData == null) {150return;151}152try {153const cost = computeCost({ configuration, priceData });154setCost(cost);155} catch (err) {156setError(`${err}`);157setCost(null);158}159}, [configuration, priceData]);160161if (ImagesError != null) {162return ImagesError;163}164if (ImagesErrorGoogle != null) {165return ImagesErrorGoogle;166}167168if (IMAGES == null || googleImages == null) {169return <Spin />;170}171172const summary = (173<Specs174configuration={configuration}175priceData={priceData}176IMAGES={IMAGES}177/>178);179180if (!editable || !project_id) {181// short summary only182return summary;183}184185if (priceData == null) {186return <Spin />;187}188189const setConfig = async (changes) => {190let changed = false;191for (const key in changes) {192if (!isEqual(changes[key], configuration[key])) {193changed = true;194break;195}196}197if (!changed) {198// nothing at all changed199return;200}201202changes = ensureConsistentConfiguration(203priceData,204configuration,205changes,206IMAGES,207);208const newConfiguration = { ...configuration, ...changes };209210if (211(state ?? "deprovisioned") != "deprovisioned" &&212(configuration.region != newConfiguration.region ||213configuration.zone != newConfiguration.zone)214) {215setError(216"Can't change the region or zone without first deprovisioning the VM",217);218// make copy so config gets reset -- i.e., whatever change you just tried to make is reverted.219setLocalConfiguration({ ...configuration });220return;221}222223if (Object.keys(changes).length == 0) {224// nothing going to change225return;226}227228try {229setLoading(true);230if (onChange != null) {231onChange(newConfiguration);232}233setLocalConfiguration(newConfiguration);234if (id != null) {235await setServerConfiguration({ id, configuration: changes });236}237} catch (err) {238setError(`${err}`);239} finally {240setLoading(false);241}242};243244const columns = [245{246dataIndex: "value",247key: "value",248},249{ dataIndex: "label", key: "label", width: 130 },250];251252const dataSource = [253{254key: "provisioning",255label: (256<A href="https://cloud.google.com/compute/docs/instances/spot">257<Icon name="external-link" /> Provisioning258</A>259),260value: (261<Provisioning262disabled={loading || disabled}263priceData={priceData}264setConfig={setConfig}265configuration={configuration}266/>267),268},269{270key: "gpu",271label: (272<A href="https://cloud.google.com/compute/docs/gpus">273<Icon name="external-link" /> GPUs274</A>275),276value: (277<GPU278state={state}279disabled={loading || disabled}280priceData={priceData}281setConfig={setConfig}282configuration={configuration}283IMAGES={IMAGES}284setCloud={setCloud}285/>286),287},288{289key: "image",290label: (291<ImageLinks image={configuration.image} style={{ height: "90px" }} />292),293value: (294<Image295state={state}296disabled={loading || disabled}297setConfig={setConfig}298configuration={configuration}299gpu={300!!(configuration.acceleratorType && configuration.acceleratorCount)301}302googleImages={googleImages}303arch={304configuration.machineType?.startsWith("t2a-") ? "arm64" : "x86_64"305}306/>307),308},309310{311key: "machineType",312label: (313<A href="https://cloud.google.com/compute/docs/machine-resource#recommendations_for_machine_types">314<Icon name="external-link" /> Machine Types315</A>316),317value: (318<MachineType319state={state}320disabled={loading || disabled}321priceData={priceData}322setConfig={setConfig}323configuration={configuration}324/>325),326},327328{329key: "region",330label: (331<A href="https://cloud.google.com/about/locations">332<Icon name="external-link" /> Regions333</A>334),335value: (336<Region337disabled={338loading || disabled || (state ?? "deprovisioned") != "deprovisioned"339}340priceData={priceData}341setConfig={setConfig}342configuration={configuration}343/>344),345},346{347key: "zone",348label: (349<A href="https://cloud.google.com/about/locations">350<Icon name="external-link" /> Zones351</A>352),353value: (354<Zone355disabled={356loading || disabled || (state ?? "deprovisioned") != "deprovisioned"357}358priceData={priceData}359setConfig={setConfig}360configuration={configuration}361/>362),363},364365{366key: "disk",367label: (368<A href="https://cloud.google.com/compute/docs/disks/performance">369<Icon name="external-link" /> Disks370</A>371),372value: (373<BootDisk374id={id}375disabled={loading}376setConfig={setConfig}377configuration={configuration}378priceData={priceData}379state={state}380IMAGES={IMAGES}381/>382),383},384{385key: "exclude",386value: (387<ExcludeFromSync388id={id}389disabled={loading}390setConfig={setConfig}391configuration={configuration}392state={state}393style={{ marginTop: "10px", color: "#666" }}394/>395),396},397{398key: "network",399label: (400<A href="https://cloud.google.com/compute/docs/network-bandwidth">401<Icon name="external-link" /> Network402</A>403),404value: (405<Network406setConfig={setConfig}407configuration={configuration}408loading={loading}409priceData={priceData}410/>411),412},413{414key: "proxy",415label: <></>,416value: (417<Proxy418setConfig={setConfig}419configuration={configuration}420data={data}421state={state}422IMAGES={IMAGES}423project_id={project_id}424id={id}425/>426),427},428429{430key: "ephemeral",431label: <></>,432value: (433<Ephemeral434setConfig={setConfig}435configuration={configuration}436loading={loading}437/>438),439},440{441key: "auto-restart",442label: <></>,443value: (444<AutoRestart445setConfig={setConfig}446configuration={configuration}447loading={loading}448/>449),450},451{452key: "allow-collaborator-control",453label: <></>,454value: (455<AllowCollaboratorControl456setConfig={setConfig}457configuration={configuration}458loading={loading}459/>460),461},462{463key: "nested-virtualization",464label: <></>,465value: (466<NestedVirtualization467setConfig={setConfig}468configuration={configuration}469loading={loading}470/>471),472},473{474key: "admin",475label: <></>,476value: (477<Admin478id={id}479configuration={configuration}480loading={loading}481template={template}482/>483),484},485];486487const errDisplay = error ? (488<div489style={{490/*minHeight: "35px", */491padding: "5px 10px",492background: error ? "red" : undefined,493color: "white",494borderRadius: "5px",495}}496>497{error}498<Button499size="small"500onClick={() => {501setError("");502setLocalConfiguration(configuration0);503}}504style={{ float: "right" }}505>506Close507</Button>508</div>509) : undefined;510511return (512<div>513{loading && (514<div style={{ float: "right" }}>515<Spin delay={1000} />516</div>517)}518{errDisplay}519{cost != null && (520<CostOverview521cost={cost}522description={523<>524You pay <b>{currency(cost)}/hour</b> while the computer server is525running. The rate is{" "}526<b>527{currency(528computeCost({ configuration, priceData, state: "off" }),529)}530/hour531</b>{" "}532when the server is off, and there is no cost when it is533deprovisioned. Network data transfer out charges are not included534in the above cost, and depend on how much data leaves the server535(see the Network section below). Incoming networking is free.536</>537}538/>539)}540<Divider />541<div style={{ textAlign: "center", margin: "10px 80px" }}>{summary}</div>542<Divider />543<Table544showHeader={false}545style={{ marginTop: "5px" }}546columns={columns}547dataSource={dataSource}548pagination={false}549/>550{errDisplay}551</div>552);553}554555function Region({ priceData, setConfig, configuration, disabled }) {556const [sortByPrice, setSortByPrice] = useState<boolean>(true);557const [newRegion, setNewRegion] = useState<string>(configuration.region);558useEffect(() => {559setNewRegion(configuration.region);560}, [configuration.region]);561562const regions = getRegions(priceData, configuration);563if (sortByPrice) {564regions.sort((a, b) => cmp(a.cost, b.cost));565}566const options = regions.map(({ region, location, lowCO2, cost }) => {567const price = <CostPerHour cost={cost} extra={" (total)"} />;568return {569value: region,570search: `${region} ${location} ${lowCO2 ? " co2 " : ""}`,571label: (572<div key={region} style={{ display: "flex" }}>573<div style={{ flex: 1 }}> {region}</div>574<div style={{ flex: 1 }}>{price}</div>575<div style={{ flex: 0.7 }}> {lowCO2 ? "🍃 Low CO2" : ""}</div>576<div style={{ flex: 0.8 }}> {location?.split(",")[1].trim()}</div>577</div>578),579};580});581582return (583<div>584{configuration.machineType ? (585<div style={{ color: "#666", marginBottom: "5px" }}>586<b>587<Icon name="global" /> Region588</b>589</div>590) : undefined}591<div>592<Select593disabled={disabled}594style={{ width: "100%" }}595options={options as any}596value={newRegion}597onChange={(region) => {598setNewRegion(region);599setConfig({ region });600}}601showSearch602optionFilterProp="children"603filterOption={filterOption}604/>605</div>606<div>607<Checkbox608disabled={disabled}609style={{ marginTop: "5px" }}610checked={sortByPrice}611onChange={() => setSortByPrice(!sortByPrice)}612>613Sort by price614</Checkbox>615<div style={{ color: "#666", marginTop: "5px" }}>616Price above is total price in this region for the machine, disk and617GPU.618</div>619</div>620</div>621);622}623624// Gets the regions where the given VM type is available.625// Ignores the currently selected zone.626function getRegions(priceData, configuration) {627const lowCO2 = new Set<string>();628const regions = new Set<string>();629const location: { [region: string]: string } = {};630const cost: { [region: string]: number } = {};631const { machineType, spot } = configuration ?? {};632for (const zone in priceData.zones) {633const i = zone.lastIndexOf("-");634const region = zone.slice(0, i);635const zoneData = priceData.zones[zone];636if (machineType) {637if (!zoneData.machineTypes.includes(machineType.split("-")[0])) {638continue;639}640if (spot) {641if (priceData.machineTypes[machineType]?.spot?.[region] == null) {642continue;643}644}645}646if (cost[region] == null) {647try {648cost[region] = computeCost({649priceData,650configuration: { ...configuration, region, zone },651});652} catch (_) {653continue;654// console.warn({ ...configuration, region, zone }, err);655}656}657if (zoneData.lowCO2 || zoneData.lowC02) {658// C02 above because of typo in data.659lowCO2.add(region);660}661regions.add(region);662location[region] = zoneData.location;663}664const v = Array.from(regions);665v.sort((a, b) => {666for (const g of [667"us",668"northamerica",669"europe",670"asia",671"southamerica",672"australia",673]) {674if (a.startsWith(g) && !b.startsWith(g)) {675return -1;676}677if (!a.startsWith(g) && b.startsWith(g)) {678return 1;679}680}681return cmp(a, b);682});683const data: {684region: string;685location: string;686lowCO2: boolean;687cost?: number;688}[] = [];689for (const region of v) {690data.push({691region,692location: location[region],693lowCO2: lowCO2.has(region),694cost: cost[region],695});696}697return data;698}699700// Gets the zones compatible with the other configuration701function getZones(priceData, configuration) {702const lowCO2 = new Set<string>();703const zones = new Set<string>();704const { region, machineType, acceleratorType, spot } = configuration;705const prefix = machineType.split("-")[0];706for (const zone in priceData.zones) {707if (region != zoneToRegion(zone)) {708// this zone isn't in the chosen region.709continue;710}711const zoneData = priceData.zones[zone];712if (machineType) {713if (!zoneData.machineTypes.includes(prefix)) {714continue;715}716if (spot != null) {717if (priceData.machineTypes[machineType]?.spot?.[region] == null) {718continue;719}720}721}722if (acceleratorType) {723if (priceData.accelerators[acceleratorType]?.prices?.[zone] == null) {724// not in this zone.725continue;726}727}728if (zoneData.lowCO2 || zoneData.lowC02) {729// C02 above because of typo in data.730lowCO2.add(zone);731}732zones.add(zone);733}734const v = Array.from(zones);735v.sort();736const data: {737zone: string;738lowCO2: boolean;739}[] = [];740for (const zone of v) {741data.push({742zone,743lowCO2: lowCO2.has(zone),744});745}746return data;747}748749function Provisioning({ priceData, setConfig, configuration, disabled }) {750const [newSpot, setNewSpot] = useState<boolean>(!!configuration.spot);751const [prices, setPrices] = useState<{752spot: number | null;753standard: number;754discount: number;755} | null>(getSpotAndStandardPrices(priceData, configuration));756757useEffect(() => {758setNewSpot(!!configuration.spot);759setPrices(getSpotAndStandardPrices(priceData, configuration));760}, [configuration]);761762useEffect(() => {763if (configuration.spot && prices != null && !prices.spot) {764setNewSpot(false);765setConfig({ spot: false });766}767}, [prices, configuration.spot]);768769return (770<div>771<div style={{ color: "#666", marginBottom: "5px" }}>772<b>773<Icon name="sliders" /> Provisioning774</b>775</div>776<Radio.Group777size="large"778buttonStyle="solid"779disabled={disabled}780value={newSpot ? "spot" : "standard"}781onChange={(e) => {782const spot = e.target.value == "standard" ? false : true;783setNewSpot(spot);784setConfig({ spot });785}}786>787<Radio.Button value="spot" disabled={!prices?.spot}>788Spot{" "}789{prices?.spot790? `${currency(prices.spot)}/hour (${prices.discount}% discount)`791: "(not available)"}{" "}792</Radio.Button>793<Radio.Button value="standard">794Standard{" "}795{prices != null ? `${currency(prices.standard)}/hour` : undefined}{" "}796</Radio.Button>797</Radio.Group>798<div style={{ color: "#666", marginTop: "5px" }}>799Standard VM's run until you stop them, whereas spot VM's are up to 91%800off, but will automatically stop when there is a surge in demand. Spot801instances might also not be available in a given region, so you may have802to try different regions.{" "}803{configuration.acceleratorType && (804<> GPU's are always in high demand.</>805)}806{newSpot && (807<Alert808style={{ margin: "5px 0" }}809type="warning"810showIcon811description={812<div style={{ maxWidth: "100%", lineHeight: 1 }}>813This is a heavily discounted spot instance. It will814automatically{" "}815{configuration.autoRestart ? " reboot if possible " : " stop "}{" "}816when there is a surge in demand.817{!disabled && (818<Popconfirm819title="Switch to Standard?"820description={821<div style={{ maxWidth: "450px" }}>822This will switch to a non-discounted standard instance,823which stays running even if there is high demand. You824can switch back to a spot instance using the blue toggle825above.826</div>827}828onConfirm={() => {829setNewSpot(false);830setConfig({ spot: false });831}}832okText="Switch to Standard"833cancelText="Cancel"834>835<Button type="link">Switch to Standard</Button>836</Popconfirm>837)}838{!configuration.autoRestart && (839<Popconfirm840title="Enable Automatic Restart?"841description={842<div style={{ maxWidth: "450px" }}>843CoCalc will automatically restart your compute server if844it is killed due to high demand. Note that there might845not be any compute resources available, in which case846you will have to wait for your server to start. You can847disable this in the "Automatically Restart" section848below.849</div>850}851onConfirm={() => {852setConfig({ autoRestart: true });853}}854okText="Enable Automatic Restart"855cancelText="Cancel"856>857<Button type="link">Enable Automatic Restart</Button>858</Popconfirm>859)}860</div>861}862/>863)}864</div>865</div>866);867}868869function getSpotAndStandardPrices(priceData, configuration) {870try {871const standard = computeCost({872priceData,873configuration: { ...configuration, spot: false },874});875let spot: number | null = null;876try {877spot = computeCost({878priceData,879configuration: { ...configuration, spot: true },880});881} catch (_) {882// some machines have no spot instance support, eg h3's.883}884return {885standard,886spot,887discount: spot != null ? Math.round((1 - spot / standard) * 100) : 0,888};889} catch (_) {890return null;891}892}893894function Zone({ priceData, setConfig, configuration, disabled }) {895const [newZone, setNewZone] = useState<string>(configuration.zone ?? "");896useEffect(() => {897setNewZone(configuration.zone);898}, [configuration.zone]);899900const zones = getZones(priceData, configuration);901const options = zones.map(({ zone, lowCO2 }) => {902return {903value: zone,904search: `${zone} ${lowCO2 ? " co 2" : ""}`,905label: `${zone} ${lowCO2 ? " - 🍃 Low CO2" : ""}`,906};907});908909return (910<div>911{configuration.machineType ? (912<div style={{ color: "#666", marginBottom: "5px" }}>913<b>914<Icon name="aim" /> Zone915</b>{" "}916in {configuration.region} with {configuration.machineType}{" "}917{configuration.spot ? "spot" : ""} VM's918</div>919) : undefined}920<Select921disabled={disabled}922style={{ width: SELECTOR_WIDTH }}923options={options}924value={newZone}925onChange={(zone) => {926setNewZone(zone);927setConfig({ zone });928}}929showSearch930optionFilterProp="children"931filterOption={filterOption}932/>933</div>934);935}936937function MachineType({ priceData, setConfig, configuration, disabled, state }) {938const [archType, setArchType] = useState<"x86_64" | "arm64">(939configuration.machineType?.startsWith("t2a-") ? "arm64" : "x86_64",940);941const [sortByPrice, setSortByPrice] = useState<boolean>(true);942const [newMachineType, setNewMachineType] = useState<string>(943configuration.machineType ?? "",944);945useEffect(() => {946setNewMachineType(configuration.machineType);947setArchType(948configuration.machineType?.startsWith("t2a-") ? "arm64" : "x86_64",949);950}, [configuration.machineType]);951useEffect(() => {952if (archType == "arm64" && !configuration.machineType.startsWith("t2a-")) {953setNewMachineType("t2a-standard-4");954setConfig({ machineType: "t2a-standard-4" });955return;956}957if (archType == "x86_64" && configuration.machineType.startsWith("t2a-")) {958setNewMachineType("t2d-standard-4");959setConfig({ machineType: "t2d-standard-4" });960return;961}962}, [archType, configuration.machineType]);963964const machineTypes = Object.keys(priceData.machineTypes);965let allOptions = machineTypes966.filter((machineType) => {967const { acceleratorType } = configuration;968if (!acceleratorType) {969if (machineType.startsWith("g2-") || machineType.startsWith("a2-")) {970return false;971}972if (archType == "arm64" && !machineType.startsWith("t2a-")) {973return false;974}975if (archType == "x86_64" && machineType.startsWith("t2a-")) {976return false;977}978} else {979if (980acceleratorType == "nvidia-tesla-a100" ||981acceleratorType == "nvidia-a100-80gb" ||982acceleratorType == "nvidia-l4"983) {984const machines =985priceData.accelerators[acceleratorType].machineType[986configuration.acceleratorCount ?? 1987] ?? [];988return machines.includes(machineType);989} else {990return machineType.startsWith("n1-");991}992}993994return true;995})996.map((machineType) => {997let cost;998try {999cost = computeInstanceCost({1000priceData,1001configuration: { ...configuration, machineType },1002});1003} catch (_) {1004cost = null;1005}1006const data = priceData.machineTypes[machineType];1007const { memory, vcpu } = data;1008return {1009value: machineType,1010search: machineType + ` memory:${memory} ram:${memory} cpu:${vcpu} `,1011cost,1012label: (1013<div key={machineType} style={{ display: "flex" }}>1014<div style={{ flex: 1 }}>{machineType}</div>1015<div style={{ flex: 1 }}>1016{cost ? (1017<CostPerHour cost={cost} />1018) : (1019<span style={{ color: "#666" }}>(region/zone changes)</span>1020)}1021</div>1022<div style={{ flex: 2 }}>1023<RamAndCpu machineType={machineType} priceData={priceData} />1024</div>1025</div>1026),1027};1028});1029const options = [1030{1031label: "Machine Types",1032options: allOptions.filter((x) => x.cost),1033},1034{1035label: "Location Will Change",1036options: allOptions.filter((x) => !x.cost),1037},1038];10391040if (sortByPrice) {1041options[0].options.sort((a, b) => {1042return cmp(a.cost, b.cost);1043});1044}10451046return (1047<div>1048<div style={{ color: "#666", marginBottom: "5px" }}>1049<Tooltip1050title={1051(state ?? "deprovisioned") != "deprovisioned"1052? "Can only be changed when machine is deprovisioned"1053: archType == "x86_64"1054? "Intel or AMD X86_64 architecture machines"1055: "ARM64 architecture machines"1056}1057>1058<Radio.Group1059style={{ float: "right" }}1060disabled={1061disabled ||1062configuration.acceleratorType ||1063(state ?? "deprovisioned") != "deprovisioned"1064}1065options={[1066{ value: "x86_64", label: "X86_64" },1067{ value: "arm64", label: "ARM64" },1068]}1069value={archType}1070onChange={({ target: { value } }) => {1071setArchType(value);1072}}1073/>1074</Tooltip>1075<b>1076<Icon name="microchip" /> Machine Type1077</b>1078</div>1079<div>1080<Select1081disabled={disabled}1082style={{ width: "100%" }}1083options={options as any}1084value={newMachineType}1085onChange={(machineType) => {1086setNewMachineType(machineType);1087setConfig({ machineType });1088}}1089showSearch1090optionFilterProp="children"1091filterOption={filterOption}1092/>1093</div>1094<div>1095<Checkbox1096disabled={disabled}1097style={{ marginTop: "5px" }}1098checked={sortByPrice}1099onChange={() => setSortByPrice(!sortByPrice)}1100>1101Sort by price1102</Checkbox>1103</div>1104<div style={{ color: "#666", marginTop: "5px" }}>1105Prices and availability depend on the region and provisioning type, so1106adjust those below to find the best overall value. Price above is just1107for the machine, and not the disk or GPU. Search for <code>cpu:4⌴</code>{" "}1108and <code>ram:8⌴</code> to only show options with 4 vCPUs and 8GB RAM.1109</div>1110</div>1111);1112}11131114function BootDisk(props) {1115return (1116<Disk1117{...props}1118minSizeGb={getMinDiskSizeGb(props)}1119maxSizeGb={65536}1120computeDiskCost={computeDiskCost}1121/>1122);1123}11241125function Image(props) {1126const { state = "deprovisioned" } = props;1127return (1128<div>1129<div style={{ color: "#666", marginBottom: "5px" }}>1130<b>1131<Icon name="disk-round" /> Image1132</b>1133</div>1134{state == "deprovisioned" && (1135<div style={{ color: "#666", marginBottom: "5px" }}>1136Select compute server image. You will be able to use sudo as root with1137no password, and can install anything into the Ubuntu Linux image,1138including commercial software.1139</div>1140)}1141<SelectImage {...props} />1142{state != "deprovisioned" && (1143<div style={{ color: "#666", marginTop: "5px" }}>1144You can only edit the image when server is deprovisioned.1145</div>1146)}1147<div style={{ color: "#666", marginTop: "5px" }}>1148<ImageDescription configuration={props.configuration} />1149</div>1150</div>1151);1152}11531154// We do NOT include the P4, P100, V100 or K80, which are older1155// and for which our base image and drivers don't work.1156// If for some reason we need them, we will have to switch to1157// different base drivers or have even more images1158const ACCELERATOR_TYPES = [1159"nvidia-tesla-t4",1160"nvidia-l4",1161"nvidia-tesla-a100",1162"nvidia-a100-80gb",1163// "nvidia-tesla-v100",1164//"nvidia-tesla-p100",1165//"nvidia-tesla-p4",1166];11671168/*1169<A href="https://www.nvidia.com/en-us/data-center/tesla-p100/">P100</A>,{" "}1170<A href="https://www.nvidia.com/en-us/data-center/v100/">V100</A>,{" "}1171<A href="https://www.nvidia.com/content/dam/en-zz/Solutions/design-visualization/solutions/resources/documents1/nvidia-p4-datasheet.pdf">1172P41173</A>1174*/11751176function GPU({1177priceData,1178setConfig,1179configuration,1180disabled,1181state,1182IMAGES,1183setCloud,1184}) {1185const { acceleratorType, acceleratorCount } = configuration;1186const head = (1187<div style={{ color: "#666", marginBottom: "5px" }}>1188<b>1189<Icon style={{ float: "right", fontSize: "50px" }} name="gpu" />1190<Icon name="cube" /> NVIDIA GPU:{" "}1191<A href="https://www.nvidia.com/en-us/data-center/a100/">A100</A>,{" "}1192<A href="https://www.nvidia.com/en-us/data-center/l4/">L4</A>,{" "}1193<A href="https://www.nvidia.com/content/dam/en-zz/Solutions/design-visualization/solutions/resources/documents1/Datasheet_NVIDIA_T4_Virtualization.pdf">1194T41195</A>1196</b>1197</div>1198);11991200const theSwitch = (1201<Switch1202disabled={disabled || (state ?? "deprovisioned") != "deprovisioned"}1203checkedChildren={"NVIDIA GPU"}1204unCheckedChildren={"NO GPU"}1205checked={!!acceleratorType}1206onChange={() => {1207if (!!acceleratorType) {1208setConfig({ acceleratorType: "", acceleratorCount: 0 });1209} else {1210setConfig(DEFAULT_GPU_CONFIG);1211}1212}}1213/>1214);1215if (!acceleratorType) {1216return (1217<div>1218{head}1219{theSwitch}1220</div>1221);1222}12231224const options = ACCELERATOR_TYPES.filter(1225(acceleratorType) => priceData.accelerators[acceleratorType] != null,1226).map((acceleratorType: GoogleCloudAcceleratorType) => {1227let cost;1228const config1 = { ...configuration, acceleratorType, acceleratorCount };1229const changes = { acceleratorType, acceleratorCount };1230try {1231cost = computeAcceleratorCost({ priceData, configuration: config1 });1232} catch (_) {1233const newChanges = ensureConsistentConfiguration(1234priceData,1235config1,1236changes,1237IMAGES,1238);1239cost = computeAcceleratorCost({1240priceData,1241configuration: { ...config1, ...newChanges },1242});1243}1244const memory = priceData.accelerators[acceleratorType].memory;1245return {1246value: acceleratorType,1247search: acceleratorType,1248cost,1249memory,1250label: (1251<div key={acceleratorType} style={{ display: "flex" }}>1252<div style={{ flex: 1 }}>1253{displayAcceleratorType(acceleratorType, memory)}1254</div>1255<div style={{ flex: 1 }}>1256<CostPerHour cost={cost} />1257</div>1258</div>1259),1260};1261});12621263const countOptions: any[] = [];1264const min = priceData.accelerators[acceleratorType]?.count ?? 1;1265const max = priceData.accelerators[acceleratorType]?.max ?? 1;1266for (let i = min; i <= max; i *= 2) {1267countOptions.push({ label: `${i}`, value: i });1268}12691270return (1271<div>1272{head}1273{theSwitch}1274<div style={{ marginTop: "15px" }}>1275<Select1276disabled={disabled || (state ?? "deprovisioned") != "deprovisioned"}1277style={{ width: SELECTOR_WIDTH }}1278options={options as any}1279value={acceleratorType}1280onChange={(type) => {1281setConfig({ acceleratorType: type });1282// todo -- change count if necessary1283}}1284showSearch1285optionFilterProp="children"1286filterOption={filterOption}1287/>1288<Select1289style={{ marginLeft: "15px", width: "75px" }}1290disabled={disabled || (state ?? "deprovisioned") != "deprovisioned"}1291options={countOptions}1292value={acceleratorCount}1293onChange={(count) => {1294setConfig({ acceleratorCount: count });1295}}1296/>1297{acceleratorCount && acceleratorType && (1298<div style={{ color: "#666", marginTop: "10px" }}>1299You have selected {acceleratorCount} dedicated{" "}1300<b>{displayAcceleratorType(acceleratorType)}</b>{" "}1301{plural(acceleratorCount, "GPU")}, with a total of{" "}1302<b>1303{priceData.accelerators[acceleratorType].memory *1304acceleratorCount}1305GB GPU RAM1306</b>1307.{" "}1308{acceleratorCount > 1 && (1309<>1310The {acceleratorCount} GPUs will be available on the same1311server.1312</>1313)}1314{1315(state ?? "deprovisioned") != "deprovisioned" && (1316<div>1317You can only change the GPU configuration when the server is1318deprovisioned.1319</div>1320) /* 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! */1321}1322{setCloud != null &&1323availableClouds().includes("hyperstack") &&1324(state ?? "deprovisioned") == "deprovisioned" && (1325<Alert1326showIcon1327style={{ margin: "10px 0 5px 0" }}1328type="warning"1329description={1330<div>1331We have partnered with Hyperstack cloud to provide NVIDIA1332H100, A100, L40, and RTX-A4/5/6000 GPUs at a{" "}1333<b>much cheaper price</b> than Google cloud.{" "}1334<Popconfirm1335title="Switch to Hyperstack"1336description={1337<div style={{ maxWidth: "450px" }}>1338This will change the cloud for this compute server1339to Hyperstack, and reset its configuration. Your1340compute server is not storing any data so this is1341safe.1342</div>1343}1344onConfirm={() => {1345setCloud("hyperstack");1346}}1347okText="Switch to Hyperstack"1348cancelText="Cancel"1349>1350<Button type="link">Switch...</Button>1351</Popconfirm>1352</div>1353}1354/>1355)}1356</div>1357)}1358</div>1359</div>1360);1361}1362/*1363{acceleratorType?.includes("a100") && configuration.spot ? (1364<div style={{ marginTop: "5px", color: "#666" }}>1365<b>WARNING:</b> A100 spot instances are rarely available. Consider1366standard provisioning instead.1367</div>1368) : undefined}1369*/13701371function ensureConsistentConfiguration(1372priceData,1373configuration: GoogleCloudConfigurationType,1374changes: Partial<GoogleCloudConfigurationType>,1375IMAGES: Images,1376) {1377const newConfiguration = { ...configuration, ...changes };1378const newChanges = { ...changes };13791380ensureConsistentImage(newConfiguration, newChanges, IMAGES);13811382ensureConsistentAccelerator(priceData, newConfiguration, newChanges);13831384ensureConsistentNvidiaL4andA100(priceData, newConfiguration, newChanges);13851386ensureConsistentRegionAndZoneWithMachineType(1387priceData,1388newConfiguration,1389newChanges,1390);13911392ensureConsistentZoneWithRegion(priceData, newConfiguration, newChanges);13931394ensureSufficientDiskSize(newConfiguration, newChanges, IMAGES);13951396ensureConsistentDiskType(priceData, newConfiguration, newChanges);13971398return newChanges;1399}14001401// We make the image consistent with the gpu selection.1402function ensureConsistentImage(configuration, changes, IMAGES) {1403const { gpu } = IMAGES[configuration.image] ?? {};1404const gpuSelected =1405configuration.acceleratorType && configuration.acceleratorCount > 0;1406if (gpu == gpuSelected) {1407// they are consistent1408return;1409}1410if (gpu && !gpuSelected) {1411// GPU image but non-GPU machine -- change image to non-GPU1412configuration["image"] = changes["image"] = "python";1413configuration["tag"] = changes["tag"] = null;1414} else if (!gpu && gpuSelected) {1415// GPU machine but not image -- change image to pytorch1416configuration["image"] = changes["image"] = "pytorch";1417configuration["tag"] = changes["tag"] = null;1418}1419}14201421function ensureSufficientDiskSize(configuration, changes, IMAGES) {1422const min = getMinDiskSizeGb({ configuration, IMAGES });1423if ((configuration.diskSizeGb ?? 0) < min) {1424changes.diskSizeGb = min;1425}1426}14271428function ensureConsistentDiskType(priceData, configuration, changes) {1429const { machineType } = configuration;1430const m = machineType.split("-")[0];1431if (configuration.diskType == "hyperdisk-balanced") {1432// make sure machine is supported1433const { supportedMachineTypes } = priceData.extra["hyperdisk-balanced"];1434if (!supportedMachineTypes.includes(m)) {1435// can't use hyperdisk on this machine, so fix.1436configuration.diskType = changes.diskType = "pd-balanced";1437}1438} else {1439const { requiredMachineTypes } = priceData.extra["hyperdisk-balanced"];1440if (requiredMachineTypes.includes(m)) {1441// must use hyperdisk on this machine, so fix.1442configuration.diskType = changes.diskType = "hyperdisk-balanced";1443}1444}1445}14461447function ensureConsistentZoneWithRegion(priceData, configuration, changes) {1448if (configuration.zone.startsWith(configuration.region)) {1449return;1450}1451if (changes["region"]) {1452// currently changing region, so set a zone that matches the region1453for (const zone in priceData.zones) {1454if (zone.startsWith(configuration.region)) {1455changes["zone"] = zone;1456break;1457}1458}1459} else {1460// probably changing the zone, so set the region from the zone1461changes["region"] = zoneToRegion(configuration.zone);1462}1463}14641465function ensureConsistentAccelerator(priceData, configuration, changes) {1466let { acceleratorType } = configuration;1467if (!acceleratorType) {1468return;1469}1470if (1471acceleratorType == "nvidia-tesla-a100" ||1472acceleratorType == "nvidia-a100-80gb" ||1473acceleratorType == "nvidia-l4"1474) {1475// L4 and A100 are handled elsewhere.1476return;1477}14781479// have a GPU1480let data = priceData.accelerators[acceleratorType];1481if (!data) {1482// accelerator type no longer exists; replace it by one that does.1483for (const type in priceData.accelerators) {1484acceleratorType =1485configuration["acceleratorType"] =1486changes["acceleratorType"] =1487type;1488data = priceData.accelerators[acceleratorType];1489break;1490}1491}1492if (data == null) {1493throw Error("bug");1494}1495// Ensure the machine type is consistent1496if (!configuration.machineType.startsWith(data.machineType)) {1497if (changes["machineType"]) {1498// if you are explicitly changing the machine type, then we respect1499// that and disabled the gpu1500configuration["acceleratorType"] = changes["acceleratorType"] = "";1501configuration["acceleratorCount"] = changes["acceleratorCount"] = 0;1502return;1503} else {1504// changing something else, so we fix the machine type1505for (const type in priceData.machineTypes) {1506if (type.startsWith(data.machineType)) {1507configuration["machineType"] = changes["machineType"] =1508type.startsWith("n1-") ? DEFAULT_GPU_INSTANCE : type;1509break;1510}1511}1512}1513}1514ensureZoneIsConsistentWithGPU(priceData, configuration, changes);15151516// Ensure the count is consistent1517const count = configuration.acceleratorCount ?? 0;1518if (count < data.count) {1519changes["acceleratorCount"] = data.count;1520} else if (count > data.max) {1521changes["acceleratorCount"] = data.max;1522}1523}15241525function ensureZoneIsConsistentWithGPU(priceData, configuration, changes) {1526if (!configuration.acceleratorType) return;15271528const data = priceData.accelerators[configuration.acceleratorType];1529if (!data) {1530// invalid acceleratorType.1531return;1532}15331534// Ensure the region/zone is consistent with accelerator type1535const prices = data[configuration.spot ? "spot" : "prices"];1536if (prices[configuration.zone] == null) {1537// there are no GPUs in the selected zone of the selected type.1538// If you just explicitly changed the GPU type, then we fix this by changing the zone.1539if (changes["acceleratorType"] != null) {1540// fix the region and zone1541// find cheapest zone in the world.1542let price = 999999999;1543let zoneChoice = "";1544for (const zone in prices) {1545if (prices[zone] < price) {1546price = prices[zone];1547zoneChoice = zone;1548}1549}1550if (zoneChoice) {1551changes["zone"] = configuration["zone"] = zoneChoice;1552changes["region"] = configuration["region"] = zoneToRegion(zoneChoice);1553return;1554}1555} else {1556// You did not change the GPU type, so we disable the GPU1557configuration["acceleratorType"] = changes["acceleratorType"] = "";1558configuration["acceleratorCount"] = changes["acceleratorCount"] = 0;1559return;1560}1561}1562}15631564// The Nvidia L4 and A100 are a little different1565function ensureConsistentNvidiaL4andA100(priceData, configuration, changes) {1566const { machineType, acceleratorType } = configuration;15671568// L4 or A100 GPU machine type, but switching to no GPU, so we have1569// to change the machine type1570if (machineType.startsWith("g2-") || machineType.startsWith("a2-")) {1571if (!acceleratorType) {1572// Easy case -- the user is explicitly changing the GPU from being set1573// to NOT be set, and the GPU is L4 or A100. In this case,1574// we just set the machine type to some non-gpu type1575// and we're done.1576configuration.machineType = changes.machineType = FALLBACK_INSTANCE;1577return;1578}1579}1580if (1581acceleratorType != "nvidia-tesla-a100" &&1582acceleratorType != "nvidia-a100-80gb" &&1583acceleratorType != "nvidia-l4"1584) {1585// We're not switching to an A100 or L4, so not handled further here.1586return;1587}15881589if (!configuration.acceleratorCount) {1590configuration.acceleratorCount = changes.acceleratorCount = 1;1591}15921593// Ensure machine type is consistent with the GPU and count we're switching to.1594let machineTypes =1595priceData.accelerators[acceleratorType]?.machineType[1596configuration.acceleratorCount1597];1598if (machineTypes == null) {1599configuration.acceleratorCount = changes.acceleratorCount = 1;1600machineTypes =1601priceData.accelerators[acceleratorType]?.machineType[1602configuration.acceleratorCount1603];1604}1605if (machineTypes == null) {1606throw Error("bug -- this can't happen");1607}16081609if (!machineTypes.includes(configuration.machineType)) {1610configuration.machineType = changes.machineType =1611machineTypes[0].startsWith("n1-")1612? DEFAULT_GPU_INSTANCE1613: machineTypes[0];1614}1615}16161617function ensureConsistentRegionAndZoneWithMachineType(1618priceData,1619configuration,1620changes,1621) {1622// Specifically selecting a machine type. We make this the1623// highest priority, so if you are changing this, we make everything1624// else fit it.1625const machineType = configuration["machineType"];1626if (priceData.machineTypes[machineType] == null) {1627console.warn(1628`BUG -- This should never happen: unknonwn machineType = '${machineType}'`,1629);1630// invalid machineType1631if (configuration.acceleratorType) {1632configuration["machineType"] = changes["machineType"] =1633DEFAULT_GPU_INSTANCE;1634} else {1635configuration["machineType"] = changes["machineType"] = FALLBACK_INSTANCE;1636}1637return;1638}16391640const i = machineType.indexOf("-");1641const prefix = machineType.slice(0, i);16421643let zoneHasMachineType = (1644priceData.zones[configuration.zone]?.machineTypes ?? []1645).includes(prefix);1646const regionToCost =1647priceData.machineTypes[machineType][1648configuration.spot ? "spot" : "prices"1649] ?? {};1650const regionHasMachineType = regionToCost[configuration.region] != null;16511652if (!regionHasMachineType) {1653// Our machine type is not in the currently selected region,1654// so find cheapest region with our requested machine type.1655let price = 1e8;1656for (const region in regionToCost) {1657if (regionToCost[region] < price) {1658price = regionToCost[region];1659configuration["region"] = changes["region"] = region;1660// since we changed the region:1661zoneHasMachineType = false;1662}1663}1664}1665if (!zoneHasMachineType) {1666// now the region has the machine type, but the zone doesn't (or1667// region changed so zone has to change).1668// So we find some zone with the machine in that region1669for (const zone in priceData.zones) {1670if (zone.startsWith(configuration["region"])) {1671if ((priceData.zones[zone]?.machineTypes ?? []).includes(prefix)) {1672configuration["zone"] = changes["zone"] = zone;1673break;1674}1675}1676}1677}16781679if (configuration.acceleratorType && configuration.acceleratorCount) {1680if (priceData.accelerators[configuration.acceleratorType] == null) {1681// The accelerator type no longer exists in the pricing data (e.g., maybe it was deprecated),1682// so replace it by one that exists.1683for (const type in priceData.accelerators) {1684configuration.acceleratorType = changes.acceleratorType = type;1685break;1686}1687}1688// have a GPU -- make sure zone works1689if (1690!priceData.accelerators[configuration.acceleratorType].prices[1691configuration.zone1692]1693) {1694// try to find a different zone in the region that works1695let fixed = false;1696const region = zoneToRegion(configuration["zone"]);1697for (const zone in priceData.accelerators[configuration.acceleratorType]1698?.prices) {1699if (zone.startsWith(region)) {1700fixed = true;1701configuration.zone = changes.zone = zone;1702break;1703}1704}1705if (!fixed) {1706// just choose cheapest zone in some region1707const zone = cheapestZone(1708priceData.accelerators[configuration.acceleratorType][1709configuration.spot ? "spot" : "prices"1710],1711);1712configuration.zone = changes.zone = zone;1713configuration.region = changes.region = zoneToRegion(zone);1714}1715}1716}1717}17181719function zoneToRegion(zone: string): string {1720const i = zone.lastIndexOf("-");1721return zone.slice(0, i);1722}17231724function Network({ setConfig, configuration, loading, priceData }) {1725const [externalIp, setExternalIp] = useState<boolean>(1726configuration.externalIp ?? true,1727);1728useEffect(() => {1729setExternalIp(configuration.externalIp ?? true);1730}, [configuration.externalIp]);17311732return (1733<div>1734<div style={{ color: "#666", marginBottom: "5px" }}>1735<b>1736<Icon name="network" /> Network1737</b>1738<br />1739All compute servers on Google cloud have full network access with1740unlimited data transfer in for free. Data transfer out{" "}1741<b>costs {currency(DATA_TRANSFER_OUT_COST_PER_GiB)}/GiB</b>.1742</div>1743<Checkbox1744checked={externalIp}1745disabled={1746true /* 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"*/1747}1748onChange={() => {1749setExternalIp(!externalIp);1750setConfig({ externalIp: !externalIp });1751}}1752>1753External IP Address1754</Checkbox>1755<div style={{ marginTop: "5px" }}>1756<Typography.Paragraph1757style={{ color: "#666" }}1758ellipsis={{1759expandable: true,1760rows: 2,1761symbol: "more",1762}}1763>1764{/* TODO: we can and will in theory support all this without external1765ip using a gateway. E.g., google cloud shell has ssh to host, etc. */}1766An external IP address is required and costs{" "}1767{configuration.spot1768? `${currency(1769markup({ cost: EXTERNAL_IP_COST.spot, priceData }),1770)}/hour`1771: `${currency(1772markup({1773cost: EXTERNAL_IP_COST.standard,1774priceData,1775}),1776)}/hour`}{" "}1777while the VM is running (there is no charge when not running).1778</Typography.Paragraph>1779</div>1780{externalIp && (1781<DNS1782setConfig={setConfig}1783configuration={configuration}1784loading={loading}1785/>1786)}1787</div>1788);1789}17901791function cheapestZone(costs: { [zone: string]: number }): string {1792let price = 99999999999999999;1793let choice = "";1794for (const zone in costs) {1795if (costs[zone] < price) {1796choice = zone;1797price = costs[zone];1798}1799}1800return choice;1801}18021803function CostPerHour({1804cost,1805extra,1806style,1807}: {1808cost?: number;1809extra?;1810style?;1811}) {1812if (cost == null) {1813return null;1814}1815return (1816<div style={{ fontFamily: "monospace", ...style }}>1817{currency(cost)}/hour1818{extra}1819</div>1820);1821}18221823function Admin({ id, configuration, loading, template }) {1824const isAdmin = useTypedRedux("account", "is_admin");1825const [error, setError] = useState<string>("");1826const [calling, setCalling] = useState<boolean>(false);1827if (!isAdmin) {1828return null;1829}1830return (1831<div>1832<div style={{ color: "#666", marginBottom: "5px" }}>1833<b>1834<Icon name="users" /> Admin1835</b>1836<br />1837Settings and functionality only available to admins.1838<br />1839<ShowError error={error} setError={setError} />1840<Tooltip title="Once you have tested the currently selected image, click this button to mark it as tested.">1841<Button1842disabled={loading || !id || calling}1843onClick={async () => {1844try {1845setCalling(true);1846await setImageTested({ id, tested: true });1847// force reload to database via GCP api call1848await reloadImages("compute_servers_images_google", true);1849} catch (err) {1850setError(`${err}`);1851} finally {1852setCalling(false);1853}1854}}1855>1856Mark Google Cloud Image Tested{" "}1857{calling && <Spin style={{ marginLeft: "15px" }} />}1858</Button>1859</Tooltip>1860<pre>1861id={id}, configuration={JSON.stringify(configuration, undefined, 2)}1862</pre>1863<Template id={id} template={template} />1864</div>1865</div>1866);1867}186818691870