Skip to content

Commit 16cf675

Browse files
committed
frontend/onprem gpu: show upgrade
1 parent 3d19861 commit 16cf675

File tree

9 files changed

+140
-36
lines changed

9 files changed

+140
-36
lines changed

src/packages/frontend/project/settings/run-quota/hooks.tsx

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,16 @@
33
* License: AGPLv3 s.t. "Commons Clause" – see LICENSE.md for details
44
*/
55

6-
import { Map, List } from "immutable";
6+
import { List, Map } from "immutable";
77
import { fromPairs, isEqual } from "lodash";
88

9+
import { ProjectStatus } from "@cocalc/comm/project-status/types";
910
import {
1011
useEffect,
1112
useMemo,
1213
useState,
1314
useTypedRedux,
1415
} from "@cocalc/frontend/app-framework";
15-
import { ProjectStatus } from "@cocalc/comm/project-status/types";
1616
import {
1717
KUCALC_COCALC_COM,
1818
KUCALC_DISABLED,
@@ -21,19 +21,19 @@ import {
2121
import { round1, seconds2hms, server_time } from "@cocalc/util/misc";
2222
import { PROJECT_UPGRADES } from "@cocalc/util/schema";
2323
import {
24+
Upgrades,
2425
quota2upgrade_key,
2526
upgrade2quota_key,
26-
Upgrades,
2727
} from "@cocalc/util/upgrades/quota";
2828
import { IdleTimeoutPct, PercentBar, renderBoolean } from "./components";
2929
import {
30-
booleanValueStr,
3130
CurrentUsage,
3231
DisplayQuota,
3332
MAX_UPGRADES,
3433
PARAMS,
35-
renderValueUnit,
3634
Usage,
35+
booleanValueStr,
36+
renderValueUnit,
3737
} from "./misc";
3838

3939
export function useRunQuota(
@@ -53,7 +53,9 @@ export function useRunQuota(
5353
} else {
5454
return rq
5555
.map((val, key) => {
56-
if (typeof val !== "number") {
56+
if (key === "gpu") {
57+
return val?.get("num", 0);
58+
} else if (typeof val !== "number") {
5759
return val;
5860
} else if (key == "idle_timeout") {
5961
return seconds2hms(val, false, false);
@@ -120,9 +122,9 @@ export function useCurrentUsage({
120122
const last_edited: Date | undefined = project_map
121123
?.get(project_id)
122124
?.get("last_edited");
123-
const runQuota: Map<string, number | List<object>> | undefined = project_map
124-
?.get(project_id)
125-
?.get("run_quota");
125+
const runQuota:
126+
| Map<string, number | Map<string, number | string> | List<object>>
127+
| undefined = project_map?.get(project_id)?.get("run_quota");
126128

127129
const [currentUsage, setCurrentUsage] = useState<CurrentUsage>({});
128130

@@ -233,6 +235,14 @@ export function useCurrentUsage({
233235
const p = runQuota?.get(key);
234236
const x = List.isList(p) ? p?.size : "N/A";
235237
return [key, { display: `${x}`, element: <>{x}</> }];
238+
case "gpu":
239+
const gpu = runQuota?.get(key);
240+
if (!gpu) return [key, { display: "N/A", element: <>N/A</> }];
241+
const num = Map.isMap(gpu) ? gpu.get("num", 1) : 1;
242+
return [
243+
key,
244+
{ display: `${num}`, element: <>{JSON.stringify(num)}</> },
245+
];
236246
default:
237247
return [key, { display: name, element: <>{name}</> }];
238248
}
@@ -258,9 +268,9 @@ export function useDisplayedFields(): string[] {
258268
// we have to make a copy, because we might modify it below
259269
const fields: string[] = [...PROJECT_UPGRADES.field_order];
260270

261-
// on kucalc on-prem, we add ext_rw and patch
271+
// on kucalc on-prem, we add ext_rw, patch and gpu
262272
if (kucalc === KUCALC_ON_PREMISES) {
263-
fields.push(...["ext_rw", "patch"]);
273+
fields.push(...["ext_rw", "patch", "gpu"]);
264274
}
265275

266276
return fields.filter((key: keyof Upgrades) => {

src/packages/frontend/project/settings/run-quota/run-quota.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,10 @@ export const RunQuota: React.FC<Props> = React.memo(
166166
const { key, quota, quotaDedicated, usage } = record;
167167
if (QUOTAS_BOOLEAN.includes(key as any)) {
168168
return `This quota is ${booleanValueStr(quota)}.`;
169+
} else if (key === "gpu") {
170+
return usage != null
171+
? `There are ${usage.display} GPU(s) requested.`
172+
: ``;
169173
} else if (key === "patch") {
170174
return usage != null
171175
? `There are ${usage.display} patch(es) in total.`
@@ -221,6 +225,7 @@ export const RunQuota: React.FC<Props> = React.memo(
221225
// the usage of a boolean quota is always the same as its value
222226
if (QUOTAS_BOOLEAN.includes(record.key as any)) return;
223227
if (record.key === "patch") return;
228+
if (record.key === "gpu") return;
224229
const usage: Usage = record.usage;
225230
if (usage == null) return;
226231
const { element } = usage;
@@ -241,17 +246,17 @@ export const RunQuota: React.FC<Props> = React.memo(
241246
);
242247
}
243248

249+
console.log(record.key, val);
250+
244251
if (typeof val === "boolean") {
245252
return renderBoolean(val, projectIsRunning);
246-
} else if (typeof val === "number") {
247-
if (record.key === "idle_timeout") {
248-
return val;
249-
}
253+
} else if (record.key === "idle_timeout") {
254+
return val;
250255
} else if (Array.isArray(val)) {
251256
return val.length;
252257
} else {
253258
return (
254-
<Text strong={true} style={style}>
259+
<Text strong style={style}>
255260
<NoWrap>{val}</NoWrap>
256261
</Text>
257262
);

src/packages/frontend/project/settings/upgrade-usage.tsx

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,19 @@
33
* License: AGPLv3 s.t. "Commons Clause" – see LICENSE.md for details
44
*/
55

6-
import { Button, Card, Typography } from "antd";
7-
import { List } from "immutable";
8-
import { join } from "path";
96
import {
107
CSS,
118
React,
12-
redux,
139
Rendered,
10+
redux,
1411
useActions,
1512
useTypedRedux,
1613
} from "@cocalc/frontend/app-framework";
1714
import {
1815
Icon,
1916
Loading,
2017
Paragraph,
18+
Text,
2119
Title,
2220
UpgradeAdjustor,
2321
} from "@cocalc/frontend/components";
@@ -33,8 +31,13 @@ import {
3331
DedicatedDisk,
3432
DedicatedResources,
3533
} from "@cocalc/util/types/dedicated";
34+
import { process_gpu_quota } from "@cocalc/util/types/gpu";
35+
import { GPU } from "@cocalc/util/types/site-licenses";
3636
import { PRICES } from "@cocalc/util/upgrades/dedicated";
3737
import { dedicatedDiskDisplay } from "@cocalc/util/upgrades/utils";
38+
import { Button, Card, Typography } from "antd";
39+
import { List } from "immutable";
40+
import { join } from "path";
3841
import AdminQuotas from "./quota-editor/admin-quotas";
3942
import PayAsYouGoQuotaEditor from "./quota-editor/pay-as-you-go";
4043
import { RunQuota } from "./run-quota";
@@ -56,6 +59,7 @@ interface Props {
5659
all_projects_have_been_loaded?: boolean;
5760
site_license_ids: string[];
5861
dedicated_resources?: DedicatedResources;
62+
gpu?: GPU;
5963
mode: "project" | "flyout";
6064
}
6165

@@ -310,6 +314,47 @@ export const UpgradeUsage: React.FC<Props> = React.memo(
310314
);
311315
}
312316

317+
function render_gpu(): Rendered {
318+
if (dedicated_resources == null) return;
319+
const gpu = dedicated_resources.gpu;
320+
if (gpu == null || gpu === false) return;
321+
const info = process_gpu_quota({ gpu });
322+
const nodes = info.nodeSelector
323+
? ` on nodes labaled: ${Object.entries(info.nodeSelector)
324+
.map(([key, value]) => `${key}=${value}`)
325+
.join(", ")}`
326+
: "";
327+
const taint = info.tolerations
328+
? ` with taint: ${info.tolerations
329+
.map((t) => {
330+
if ("value" in t) {
331+
return `${t.key}=${t.value}`;
332+
} else {
333+
return `${t.key}`;
334+
}
335+
})
336+
.join(", ")}`
337+
: "";
338+
339+
return (
340+
<Card
341+
title={
342+
<>
343+
<Icon name="gpu" /> GPU
344+
</>
345+
}
346+
type="inner"
347+
style={{ marginTop: "15px" }}
348+
styles={{ body: { padding: "10px" } }}
349+
>
350+
<Text>
351+
Requesting {gpu.num} GPU(s){nodes}
352+
{taint}.
353+
</Text>
354+
</Card>
355+
);
356+
}
357+
313358
function render_support(): Rendered {
314359
if (!is_commercial) return; // don't render if not commercial
315360
return (
@@ -375,6 +420,7 @@ export const UpgradeUsage: React.FC<Props> = React.memo(
375420
{render_upgrades_button()}
376421
{renderQuotaEditor()}
377422
{render_dedicated_disks()}
423+
{render_gpu()}
378424
{render_site_license()}
379425
{render_support()}
380426
</div>

src/packages/frontend/projects/store.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ import { CUSTOM_IMG_PREFIX } from "@cocalc/frontend/custom-software/util";
1212
import { WebsocketState } from "@cocalc/frontend/project/websocket/websocket-state";
1313
import { webapp_client } from "@cocalc/frontend/webapp-client";
1414
import {
15-
LLMServicesAvailable,
16-
LLMServiceName,
1715
LANGUAGE_MODEL_SERVICES,
16+
LLMServiceName,
17+
LLMServicesAvailable,
1818
} from "@cocalc/util/db-schema/llm-utils";
1919
import {
2020
cmp,
@@ -29,7 +29,7 @@ import {
2929
} from "@cocalc/util/misc";
3030
import { DEFAULT_QUOTAS, PROJECT_UPGRADES } from "@cocalc/util/schema";
3131
import { DedicatedDisk, DedicatedVM } from "@cocalc/util/types/dedicated";
32-
import { SiteLicenseQuota } from "@cocalc/util/types/site-licenses";
32+
import { GPU, SiteLicenseQuota } from "@cocalc/util/types/site-licenses";
3333
import { site_license_quota } from "@cocalc/util/upgrades/quota";
3434
import { Upgrades } from "@cocalc/util/upgrades/types";
3535

@@ -451,6 +451,7 @@ export class ProjectsStore extends Store<ProjectsState> {
451451
public get_total_site_license_dedicated(project_id: string): {
452452
vm: false | DedicatedVM;
453453
disks: DedicatedDisk[];
454+
gpu: GPU | false;
454455
} {
455456
const site_license: any = this.getIn([
456457
"project_map",
@@ -459,6 +460,7 @@ export class ProjectsStore extends Store<ProjectsState> {
459460
])?.toJS();
460461
const disks: DedicatedDisk[] = [];
461462
let vm: false | DedicatedVM = false;
463+
let gpu: GPU | false = false;
462464
for (const license of Object.values(site_license ?? {})) {
463465
// could be null in the moment when a license is removed!
464466
if (license == null) continue;
@@ -470,8 +472,11 @@ export class ProjectsStore extends Store<ProjectsState> {
470472
if (!vm && typeof quota.dedicated_vm?.machine === "string") {
471473
vm = quota.dedicated_vm;
472474
}
475+
if (!gpu && typeof quota.gpu === "object") {
476+
gpu = quota.gpu;
477+
}
473478
}
474-
return { vm, disks };
479+
return { vm, disks, gpu };
475480
}
476481

477482
// Return string array of the site licenses that are applied to this project.

src/packages/frontend/site-licenses/purchase/quota-editor.tsx

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -522,7 +522,7 @@ export const QuotaEditor: React.FC<Props> = (props: Props) => {
522522

523523
function render_gpu_help(): JSX.Element {
524524
return (
525-
<HelpIcon title="GPU Support" style={{ float: "right" }} maxWidth="500px">
525+
<HelpIcon title="GPU Support" maxWidth="500px">
526526
<Paragraph>
527527
This configures a license, which will cause the project to run on a
528528
GPU. You need to configure your VMs in your cluster in such a way,
@@ -579,11 +579,13 @@ export const QuotaEditor: React.FC<Props> = (props: Props) => {
579579
disabled={disabled}
580580
style={{ fontWeight: "normal" }}
581581
onChange={(e) =>
582-
e.target.checked
583-
? onChange({
584-
gpu: { num: num > 0 ? num : 1, toleration, nodeLabel },
585-
})
586-
: onChange({ gpu: { num } })
582+
onChange({
583+
gpu: {
584+
num: e.target.checked ? Math.max(1, num) : 0,
585+
toleration,
586+
nodeLabel,
587+
},
588+
})
587589
}
588590
>
589591
<Text strong>Configure GPU</Text> {render_gpu_help()}
@@ -635,10 +637,14 @@ export const QuotaEditor: React.FC<Props> = (props: Props) => {
635637
(optional, [1])
636638
</Paragraph>
637639
<Paragraph type="secondary">
638-
[1] format: <code>key=value</code>. Keep empty if you do not use
639-
label selectors or taints. Specify mulitple ones via a "," comma.
640+
[1] format: <code>key=value</code> or for taints, also{" "}
641+
<code>key</code> to tolerate the key regardless of value. Keep the
642+
field empty if you do not use label selectors or taints. Specify
643+
mulitple ones via a "," comma. Below is a "debug" view.
640644
</Paragraph>
641-
<pre>{JSON.stringify(debug, null, 2)}</pre>
645+
<pre style={{ fontSize: "85%" }}>
646+
{JSON.stringify(debug, null, 2)}
647+
</pre>
642648
</Col>
643649
) : undefined}
644650
</Row>

src/packages/util/types/dedicated.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
* License: AGPLv3 s.t. "Commons Clause" – see LICENSE.md for details
44
*/
55

6+
import { GPU } from "./site-licenses";
7+
68
export interface VMsType {
79
[id: string]:
810
| {
@@ -36,7 +38,7 @@ export interface DiskType {
3638

3739
export const DedicatedDiskSpeedNames = ["standard", "balanced", "ssd"] as const;
3840

39-
export type DedicatedDiskSpeeds = typeof DedicatedDiskSpeedNames[number];
41+
export type DedicatedDiskSpeeds = (typeof DedicatedDiskSpeedNames)[number];
4042

4143
export interface DedicatedDiskConfig {
4244
size_gb: number;
@@ -69,4 +71,5 @@ export function isDedicatedDisk(d): d is DedicatedDisk {
6971
export type DedicatedResources = {
7072
vm: false | DedicatedVM;
7173
disks: DedicatedDisk[];
74+
gpu: GPU | false;
7275
};

src/packages/util/types/gpu.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,28 @@ export function extract_gpu(quota: SiteLicenseQuota = {}) {
99
return { num: 0 };
1010
}
1111

12-
export function process_gpu_quota(quota: SiteLicenseQuota = {}) {
12+
type GPUQuotaInfo = {
13+
resources?: { limits: { "nvidia.com/gpu": number } };
14+
nodeSelector?: { [key: string]: string };
15+
tolerations?: (
16+
| {
17+
key: string;
18+
operator: string;
19+
value: string;
20+
effect: string;
21+
}
22+
| {
23+
key: string;
24+
operator: string;
25+
effect: string;
26+
}
27+
)[];
28+
};
29+
30+
export function process_gpu_quota(quota: SiteLicenseQuota = {}): GPUQuotaInfo {
1331
const { num = 0, toleration = "", nodeLabel = "" } = extract_gpu(quota);
1432

15-
const debug: any = {};
33+
const debug: GPUQuotaInfo = {};
1634
if (num > 0) {
1735
debug.resources = { limits: { "nvidia.com/gpu": num } };
1836
if (nodeLabel) {

0 commit comments

Comments
 (0)