Skip to content

Commit dc6684b

Browse files
committed
onprem/license/gpu: configure gpu field
1 parent 5de174b commit dc6684b

File tree

4 files changed

+153
-62
lines changed

4 files changed

+153
-62
lines changed

src/packages/frontend/components/text-input.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ interface Props {
2020
onBlur?: () => void;
2121
disabled?: boolean;
2222
style?: CSS;
23+
size?: "small";
2324
}
2425

2526
export const TextInput: React.FC<Props> = React.memo(
@@ -34,6 +35,7 @@ export const TextInput: React.FC<Props> = React.memo(
3435
onBlur,
3536
disabled = false,
3637
style,
38+
size,
3739
} = props;
3840

3941
const inputRef = React.useRef<any>(null);
@@ -65,6 +67,7 @@ export const TextInput: React.FC<Props> = React.memo(
6567
style={{ marginBottom: "15px" }}
6668
bsStyle="success"
6769
onClick={saveChange}
70+
bsSize={size === "small" ? "xsmall" : undefined}
6871
>
6972
<Icon name="save" /> Save
7073
</Button>
@@ -87,6 +90,7 @@ export const TextInput: React.FC<Props> = React.memo(
8790
onFocus={onFocus}
8891
onBlur={onBlur}
8992
disabled={disabled}
93+
size={size === "small" ? "small" : undefined}
9094
/>
9195
</Form.Item>
9296
);
@@ -98,5 +102,5 @@ export const TextInput: React.FC<Props> = React.memo(
98102
{render_save_button()}
99103
</Form>
100104
);
101-
}
105+
},
102106
);

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

Lines changed: 96 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ import {
3434
A,
3535
Gap,
3636
HelpIcon,
37-
NumberInput,
3837
Paragraph,
3938
TextInput,
4039
} from "@cocalc/frontend/components";
@@ -53,6 +52,7 @@ import {
5352
import { User } from "@cocalc/util/licenses/purchase/types";
5453
import { money } from "@cocalc/util/licenses/purchase/utils";
5554
import { plural, round1, test_valid_jsonpatch } from "@cocalc/util/misc";
55+
import { extract_gpu, process_gpu_quota } from "@cocalc/util/types/gpu";
5656
import { SiteLicenseQuota } from "@cocalc/util/types/site-licenses";
5757
import { DEDICATED_VM_ONPREM_MACHINE } from "@cocalc/util/upgrades/consts";
5858
import { Upgrades } from "@cocalc/util/upgrades/quota";
@@ -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">
525+
<HelpIcon title="GPU Support" style={{ float: "right" }} 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,
@@ -533,73 +533,114 @@ export const QuotaEditor: React.FC<Props> = (props: Props) => {
533533
<code>nvidia.com/gpu: 1</code>.
534534
</Paragraph>
535535
<Paragraph>
536-
On top of that, you can optionally specify a taint, which will be
537-
tolerated by that pod. This helps with keeping all other project pods
538-
away from your GPU enabled nodes:{" "}
539-
<code>
540-
tolerations:{" "}
541-
{JSON.stringify([
542-
{
543-
key: "gpu",
544-
operator: "Equal",
545-
value: "cocalc",
546-
effect: "NoSchedule",
547-
},
548-
])}
549-
</code>
550-
.
536+
On top of that, you can optionally specify a{" "}
537+
<Text strong>taint toleration</Text>. This helps with keeping all
538+
other project pods away from your GPU enabled nodes. e.g.{" "}
539+
<Text code>gpu=cocalc</Text> ends up as:{" "}
540+
<pre>
541+
{JSON.stringify({
542+
tolerations: [
543+
{
544+
key: "gpu",
545+
operator: "Equal",
546+
value: "cocalc",
547+
effect: "NoSchedule",
548+
},
549+
],
550+
})}
551+
</pre>
552+
</Paragraph>
553+
<Paragraph>
554+
You can also specify a <Text strong>node selector</Text>, useful if
555+
you have several different types of GPU nodes in your cluster, and you
556+
want to restrict where the project can run. E.g.{" "}
557+
<Text code>gpu-type=nvidia-tesla-v100</Text> ends up as{" "}
558+
<pre>
559+
{JSON.stringify({
560+
nodeSelector: { "gpu-type": "nvidia-tesla-v100" },
561+
})}
562+
</pre>
551563
</Paragraph>
552564
</HelpIcon>
553565
);
554566
}
555567

556568
function render_gpu(): JSX.Element {
557-
const { gpu } = quota;
558-
const { num = 0, toleration = "" } =
559-
typeof gpu === "object"
560-
? gpu
561-
: typeof gpu === "number"
562-
? { num: gpu }
563-
: {};
569+
const { num = 0, toleration = "", nodeLabel = "" } = extract_gpu(quota);
570+
571+
const debug = process_gpu_quota(quota);
572+
564573
return (
565574
<Row style={ROW_STYLE}>
566575
<Col md={col.control}>
567576
<Paragraph>
568-
<Text strong>Request a GPU</Text>
569-
</Paragraph>{" "}
570-
<Paragraph>
571-
<NumberInput
572-
number={num}
573-
min={0}
574-
max={8}
575-
on_change={(num: number) =>
576-
onChange({ gpu: { num, toleration } })
577-
}
578-
/>{" "}
579-
GPUs (<code>nvidia.com/gpu: {num}</code>)
580-
</Paragraph>
581-
<Paragraph>
582-
<Text>(optional) Tolerate a taint:</Text>{" "}
583-
<TextInput
577+
<Checkbox
578+
checked={num > 0}
584579
disabled={disabled}
585-
type={"text"}
586-
on_change={(tol) =>
587-
onChange({ gpu: { num, toleration: tol.trim() } })
580+
style={{ fontWeight: "normal" }}
581+
onChange={(e) =>
582+
e.target.checked
583+
? onChange({
584+
gpu: { num: num > 0 ? num : 1, toleration, nodeLabel },
585+
})
586+
: onChange({ gpu: { num } })
588587
}
589-
style={{
590-
fontWeight: "normal",
591-
display: "inline-block",
592-
margin: "0 10px",
593-
}}
594-
text={toleration}
595-
/>{" "}
596-
(Enter <code>key=value</code> of a node taint. e.g.{" "}
597-
<code>gpu=cocalc</code> means to ignore nodes tatined with key{" "}
598-
<code>gpu</code> and value <code>cocalc</code>. Keep empty if you do
599-
not use taints!)
588+
>
589+
<Text strong>Configure GPU</Text> {render_gpu_help()}
590+
</Checkbox>
600591
</Paragraph>
601-
{render_gpu_help()}
602592
</Col>
593+
{num > 0 ? (
594+
<Col md={col.desc}>
595+
<Paragraph>
596+
<Text strong>Number of GPUs</Text>{" "}
597+
<InputNumber
598+
min={1}
599+
size={"small"}
600+
value={num}
601+
onChange={(num: number) =>
602+
onChange({ gpu: { num, toleration, nodeLabel } })
603+
}
604+
/>{" "}
605+
(usually "1")
606+
</Paragraph>
607+
<Paragraph>
608+
<Text strong>Node selector:</Text>{" "}
609+
<TextInput
610+
size={"small"}
611+
disabled={disabled}
612+
type={"text"}
613+
on_change={(label) =>
614+
onChange({
615+
gpu: { nodeLabel: label.trim(), toleration, num },
616+
})
617+
}
618+
style={{ display: "inline-block" }}
619+
text={nodeLabel}
620+
/>{" "}
621+
(optional, [1])
622+
</Paragraph>
623+
<Paragraph>
624+
<Text strong>Tolerate a taint:</Text>{" "}
625+
<TextInput
626+
size={"small"}
627+
disabled={disabled}
628+
type={"text"}
629+
on_change={(tol) =>
630+
onChange({ gpu: { toleration: tol.trim(), nodeLabel, num } })
631+
}
632+
style={{ display: "inline-block" }}
633+
text={toleration}
634+
/>{" "}
635+
(optional, [1])
636+
</Paragraph>
637+
<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+
</Paragraph>
641+
<pre>{JSON.stringify(debug, null, 2)}</pre>
642+
</Col>
643+
) : undefined}
603644
</Row>
604645
);
605646
}

src/packages/util/types/gpu.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// On-prem: parsing and converting the quota.gpu information
2+
3+
import { SiteLicenseQuota } from "./site-licenses";
4+
5+
export function extract_gpu(quota: SiteLicenseQuota = {}) {
6+
const { gpu } = quota;
7+
if (gpu == null) return { num: 0 };
8+
if (typeof gpu === "object") return gpu;
9+
return { num: 0 };
10+
}
11+
12+
export function process_gpu_quota(quota: SiteLicenseQuota = {}) {
13+
const { num = 0, toleration = "", nodeLabel = "" } = extract_gpu(quota);
14+
15+
const debug: any = {};
16+
if (num > 0) {
17+
debug.resources = { limits: { "nvidia.com/gpu": num } };
18+
if (nodeLabel) {
19+
debug.nodeSelector = {};
20+
for (const label of nodeLabel.split(",")) {
21+
const [key, val] = label.trim().split("=");
22+
debug.nodeSelector[key] = val;
23+
}
24+
}
25+
if (toleration) {
26+
debug.tolerations = [];
27+
for (const tol of toleration.split(",")) {
28+
const [key, val] = tol.trim().split("=");
29+
if (val) {
30+
debug.tolerations.push({
31+
key,
32+
operator: "Equal",
33+
value: val,
34+
effect: "NoSchedule",
35+
});
36+
} else {
37+
debug.tolerations.push({
38+
key,
39+
operator: "Exists",
40+
effect: "NoSchedule",
41+
});
42+
}
43+
}
44+
}
45+
}
46+
return debug;
47+
}

src/packages/util/types/site-licenses.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,11 @@ export interface SiteLicenseQuota {
2727
// JSON array of Array of JSON Patch Operations, e.g. "[{op: \"add\", path: \"/foo\", value: \"bar\"}]"
2828
// It's not an array of objects, because somewhere the array is converted to weird map of "0, 1, 2,..." indexed objects.
2929
patch?: string;
30-
gpu?:
31-
| number
32-
| {
33-
num?: number; // usualy 1, to set nvidia.com/gpu=1
34-
toleration?: string; // e.g. gpu=cocalc for key=value
35-
};
30+
gpu?: {
31+
num?: number; // usualy 1, to set nvidia.com/gpu=1, 0 means "disabled"
32+
toleration?: string; // e.g. gpu=cocalc for key=value
33+
nodeLabel?: string; // e.g. gpu=cocalc for key=value
34+
};
3635
}
3736

3837
// For typescript use of these from user side, we make this available:

0 commit comments

Comments
 (0)