feat: support deploying cpu workers for k8s cluster

This commit is contained in:
Yuxing Deng
2026-06-10 15:26:13 +08:00
committed by jialin
parent c8860207e8
commit ded270ff25
11 changed files with 124 additions and 38 deletions
+4
View File
@@ -70,7 +70,11 @@ export default {
'For <span class="bold-text">non-Docker</span> clusters, please register clusters or manage worker pools from the Clusters page.', 'For <span class="bold-text">non-Docker</span> clusters, please register clusters or manage worker pools from the Clusters page.',
'clusters.addworker.selectGPU': 'Select GPU Vendor', 'clusters.addworker.selectGPU': 'Select GPU Vendor',
'clusters.addworker.selectGPU.multiTag': 'Multi-select', 'clusters.addworker.selectGPU.multiTag': 'Multi-select',
'clusters.addworker.selectGPU.subtitle':
'You can select multiple GPU Vendors or none for CPU-only clusters',
'clusters.addworker.checkEnv': 'Check Environment', 'clusters.addworker.checkEnv': 'Check Environment',
'clusters.addworker.checkEnv.cpuOnlyTips':
'Use the following command to verify that the Kubernetes cluster has at least one ready node. You are registering a CPU-only cluster.',
'clusters.addworker.specifyArgs': 'Specify Arguments', 'clusters.addworker.specifyArgs': 'Specify Arguments',
'clusters.addworker.dtkVersion': 'DTK Version', 'clusters.addworker.dtkVersion': 'DTK Version',
'clusters.addworker.runCommand': 'Run Command', 'clusters.addworker.runCommand': 'Run Command',
+4
View File
@@ -70,7 +70,11 @@ export default {
'For <span class="bold-text">non-Docker</span> clusters, please register clusters or manage worker pools from the Clusters page.', 'For <span class="bold-text">non-Docker</span> clusters, please register clusters or manage worker pools from the Clusters page.',
'clusters.addworker.selectGPU': 'Select GPU Vendor', 'clusters.addworker.selectGPU': 'Select GPU Vendor',
'clusters.addworker.selectGPU.multiTag': 'Multi-select', 'clusters.addworker.selectGPU.multiTag': 'Multi-select',
'clusters.addworker.selectGPU.subtitle':
'複数の GPU ベンダーを選択するか、CPU クラスター専用の場合は選択不要です',
'clusters.addworker.checkEnv': 'Check Environment', 'clusters.addworker.checkEnv': 'Check Environment',
'clusters.addworker.checkEnv.cpuOnlyTips':
'以下のコマンドを使用して、Kubernetes クラスターに少なくとも 1 つのレディーノードがあることを確認してください。CPU クラスターを登録しています。',
'clusters.addworker.specifyArgs': 'Specify Arguments', 'clusters.addworker.specifyArgs': 'Specify Arguments',
'clusters.addworker.dtkVersion': 'DTK バージョン', 'clusters.addworker.dtkVersion': 'DTK バージョン',
'clusters.addworker.runCommand': 'Run Command', 'clusters.addworker.runCommand': 'Run Command',
+4
View File
@@ -70,7 +70,11 @@ export default {
'Для <span class="bold-text">не-Docker</span> кластеров, пожалуйста, регистрируйте кластеры или управляйте пулами воркеров на странице Кластеры.', 'Для <span class="bold-text">не-Docker</span> кластеров, пожалуйста, регистрируйте кластеры или управляйте пулами воркеров на странице Кластеры.',
'clusters.addworker.selectGPU': 'Выбрать производителя GPU', 'clusters.addworker.selectGPU': 'Выбрать производителя GPU',
'clusters.addworker.selectGPU.multiTag': 'Multi-select', 'clusters.addworker.selectGPU.multiTag': 'Multi-select',
'clusters.addworker.selectGPU.subtitle':
'Вы можете выбрать несколько производителей GPU или не выбирать для кластера только с CPU',
'clusters.addworker.checkEnv': 'Проверить окружение', 'clusters.addworker.checkEnv': 'Проверить окружение',
'clusters.addworker.checkEnv.cpuOnlyTips':
'Используйте следующую команду, чтобы убедиться, что в кластере Kubernetes есть хотя бы один готовый узел. Вы регистрируете кластер только с CPU.',
'clusters.addworker.specifyArgs': 'Указать аргументы', 'clusters.addworker.specifyArgs': 'Указать аргументы',
'clusters.addworker.dtkVersion': 'Версия DTK', 'clusters.addworker.dtkVersion': 'Версия DTK',
'clusters.addworker.runCommand': 'Выполнить команду', 'clusters.addworker.runCommand': 'Выполнить команду',
+4
View File
@@ -70,7 +70,11 @@ export default {
'<span class="bold-text">Docker dışı</span> kümeler için lütfen Kümeler sayfasından küme kaydı oluşturun veya işçi havuzlarını yönetin.', '<span class="bold-text">Docker dışı</span> kümeler için lütfen Kümeler sayfasından küme kaydı oluşturun veya işçi havuzlarını yönetin.',
'clusters.addworker.selectGPU': 'GPU Üreticisi Seç', 'clusters.addworker.selectGPU': 'GPU Üreticisi Seç',
'clusters.addworker.selectGPU.multiTag': 'Multi-select', 'clusters.addworker.selectGPU.multiTag': 'Multi-select',
'clusters.addworker.selectGPU.subtitle':
'Birden fazla GPU Üreticisi seçebilir veya yalnızca CPU kümeleri için hiçbirini seçmeyebilirsiniz',
'clusters.addworker.checkEnv': 'Ortamı Kontrol Et', 'clusters.addworker.checkEnv': 'Ortamı Kontrol Et',
'clusters.addworker.checkEnv.cpuOnlyTips':
'Kubernetes kümesinde en az bir hazır düğüm olduğunu doğrulamak için aşağıdaki komutu kullanın. Yalnızca CPU kümelerini kaydediyorsunuz.',
'clusters.addworker.specifyArgs': 'Argümanları Belirle', 'clusters.addworker.specifyArgs': 'Argümanları Belirle',
'clusters.addworker.dtkVersion': 'DTK Sürümü', 'clusters.addworker.dtkVersion': 'DTK Sürümü',
'clusters.addworker.runCommand': 'Komutu Çalıştır', 'clusters.addworker.runCommand': 'Komutu Çalıştır',
+4
View File
@@ -68,7 +68,11 @@ export default {
'<span class="bold-text">非 Docker</span> 集群请前往集群页面注册集群或管理节点池。', '<span class="bold-text">非 Docker</span> 集群请前往集群页面注册集群或管理节点池。',
'clusters.addworker.selectGPU': '选择 GPU 厂商', 'clusters.addworker.selectGPU': '选择 GPU 厂商',
'clusters.addworker.selectGPU.multiTag': '可多选', 'clusters.addworker.selectGPU.multiTag': '可多选',
'clusters.addworker.selectGPU.subtitle':
'可选择多个 GPU 厂商,或不选择以用于仅 CPU 的集群',
'clusters.addworker.checkEnv': '检查环境', 'clusters.addworker.checkEnv': '检查环境',
'clusters.addworker.checkEnv.cpuOnlyTips':
'使用以下命令验证 Kubernetes 集群中至少有一个就绪节点。你正在注册仅 CPU 的集群。',
'clusters.addworker.specifyArgs': '指定参数', 'clusters.addworker.specifyArgs': '指定参数',
'clusters.addworker.dtkVersion': 'DTK 版本', 'clusters.addworker.dtkVersion': 'DTK 版本',
'clusters.addworker.runCommand': '运行指令', 'clusters.addworker.runCommand': '运行指令',
@@ -99,24 +99,30 @@ const AddWorkerSteps: React.FC<AddWorkerProps> = (props) => {
// Downstream steps (check env, run command, ...) only make sense after a // Downstream steps (check env, run command, ...) only make sense after a
// GPU vendor has been chosen. If the user toggled off every vendor in // GPU vendor has been chosen. If the user toggled off every vendor in
// multi-select, gate them shut so the wrong panel can't be opened. // multi-select, gate them shut so the wrong panel can't be opened.
// Exception: for K8s clusters, Run Command is always accessible (CPU-only
// workers don't need a GPU vendor).
const selectedGPUs = const selectedGPUs =
(summary.get('selectedGPUs') as string[] | undefined) || []; (summary.get('selectedGPUs') as string[] | undefined) || [];
const currentGPU = (summary.get('currentGPU') as string | undefined) || ''; const currentGPU = (summary.get('currentGPU') as string | undefined) || '';
const noVendorSelected = !currentGPU && selectedGPUs.length === 0; const noVendorSelected = !currentGPU && selectedGPUs.length === 0;
const downstreamDisabled = disabled || noVendorSelected; const isK8s = provider === ProviderValueMap.Kubernetes;
// For non-K8s providers, all downstream steps are gated by vendor selection.
// For K8s, both CheckEnv and RunCommand are always accessible — CheckEnv
// shows a ready-nodes check when no vendor is selected (CPU-only workers).
const downstreamDisabled = disabled || (noVendorSelected && !isK8s);
React.useEffect(() => { React.useEffect(() => {
// If the user just deselected everything, collapse any downstream // If the user just deselected everything on a non-K8s provider, collapse
// panel back to the GPU step so they aren't left looking at a stale // any downstream panel back to the GPU step so they aren't left looking at
// disabled-but-open command. Functional updater so we don't have to // a stale disabled-but-open command. For K8s, CheckEnv stays open with a
// depend on `collapseKey` and re-run the effect on every toggle. // ready-nodes check, so no collapsing needed.
if (!noVendorSelected) return; if (!noVendorSelected || isK8s) return;
setCollapseKey((prev) => setCollapseKey((prev) =>
prev.has(StepNamesMap.SelectGPU) prev.has(StepNamesMap.SelectGPU)
? prev ? prev
: new Set([StepNamesMap.SelectGPU]) : new Set([StepNamesMap.SelectGPU])
); );
}, [noVendorSelected]); }, [noVendorSelected, isK8s]);
return ( return (
<AddWorkerContext.Provider <AddWorkerContext.Provider
@@ -145,7 +151,9 @@ const AddWorkerSteps: React.FC<AddWorkerProps> = (props) => {
!stepList.includes(StepNamesMap.SelectCluster)) && ( !stepList.includes(StepNamesMap.SelectCluster)) && (
<> <>
<SelectVendor disabled={disabled}></SelectVendor> <SelectVendor disabled={disabled}></SelectVendor>
<CheckEnvironment disabled={downstreamDisabled}></CheckEnvironment> {(isK8s || !noVendorSelected) && (
<CheckEnvironment disabled={disabled}></CheckEnvironment>
)}
{provider === ProviderValueMap.Kubernetes && ( {provider === ProviderValueMap.Kubernetes && (
<K8sRunCommand disabled={downstreamDisabled}></K8sRunCommand> <K8sRunCommand disabled={downstreamDisabled}></K8sRunCommand>
@@ -1,6 +1,7 @@
import { BulbOutlined } from '@ant-design/icons'; import { BulbOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Alert, Typography } from 'antd'; import { Alert, Typography } from 'antd';
import { ProviderValueMap } from '../../config';
import CheckEnvCommand from '../check-env-command'; import CheckEnvCommand from '../check-env-command';
import { useAddWorkerContext } from './add-worker-context'; import { useAddWorkerContext } from './add-worker-context';
import { AddWorkerStepProps, StepNamesMap } from './config'; import { AddWorkerStepProps, StepNamesMap } from './config';
@@ -10,8 +11,8 @@ import StepCollapse from './step-collapse';
const CheckEnvironment: React.FC<AddWorkerStepProps> = ({ disabled }) => { const CheckEnvironment: React.FC<AddWorkerStepProps> = ({ disabled }) => {
const { stepList, summary, provider } = useAddWorkerContext(); const { stepList, summary, provider } = useAddWorkerContext();
const intl = useIntl(); const intl = useIntl();
const currentGPU = summary.get('currentGPU'); const currentGPU = summary.get('currentGPU') as string;
const currentGPUs: string[] = summary.get('selectedGPUs') || []; const currentGPUs: string[] = (summary.get('selectedGPUs') as string[]) || [];
const workerCommand = summary.get('workerCommand') || { const workerCommand = summary.get('workerCommand') || {
label: '', label: '',
link: '', link: '',
@@ -19,6 +20,8 @@ const CheckEnvironment: React.FC<AddWorkerStepProps> = ({ disabled }) => {
}; };
const stepIndex = stepList.indexOf(StepNamesMap.CheckEnv) + 1; const stepIndex = stepList.indexOf(StepNamesMap.CheckEnv) + 1;
const isK8s = provider === ProviderValueMap.Kubernetes;
const isCPUOnly = isK8s && !currentGPU && currentGPUs.length === 0;
return ( return (
<StepCollapse <StepCollapse
@@ -31,27 +34,37 @@ const CheckEnvironment: React.FC<AddWorkerStepProps> = ({ disabled }) => {
</Title> </Title>
} }
> >
<Alert {isCPUOnly ? (
type="info" <Typography.Paragraph style={{ marginBottom: 8 }}>
showIcon {intl.formatMessage({
icon={<BulbOutlined />} id: 'clusters.addworker.checkEnv.cpuOnlyTips'
style={{ })}
marginBottom: 8 </Typography.Paragraph>
}} ) : (
title={ <>
<span <Alert
dangerouslySetInnerHTML={{ type="info"
__html: intl.formatMessage( showIcon
{ id: 'clusters.create.addworker.tips' }, icon={<BulbOutlined />}
{ label: workerCommand.label, link: workerCommand.link } style={{
) marginBottom: 8
}} }}
></span> title={
} <span
></Alert> dangerouslySetInnerHTML={{
<Typography.Paragraph style={{ marginBottom: 8 }}> __html: intl.formatMessage(
{intl.formatMessage({ id: 'cluster.create.checkEnv.tips' })} { id: 'clusters.create.addworker.tips' },
</Typography.Paragraph> { label: workerCommand.label, link: workerCommand.link }
)
}}
></span>
}
></Alert>
<Typography.Paragraph style={{ marginBottom: 8 }}>
{intl.formatMessage({ id: 'cluster.create.checkEnv.tips' })}
</Typography.Paragraph>
</>
)}
<CheckEnvCommand <CheckEnvCommand
provider={provider} provider={provider}
currentGPU={currentGPU} currentGPU={currentGPU}
@@ -4,7 +4,6 @@ import {
GPUsConfigs GPUsConfigs
} from '@/pages/resources/config/gpu-driver'; } from '@/pages/resources/config/gpu-driver';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Tag } from 'antd';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { ProviderValueMap } from '../../config'; import { ProviderValueMap } from '../../config';
import SupportedGPUs from '../support-gpus'; import SupportedGPUs from '../support-gpus';
@@ -94,7 +93,9 @@ const SelectVendor: React.FC<AddWorkerStepProps> = ({ disabled }) => {
}; };
useEffect(() => { useEffect(() => {
// init a default selection // K8s clusters allow selecting none (CPU-only workers), so skip default
// selection. Other providers still default to NVIDIA.
if (multiCapable) return;
handleSelect(GPUDriverMap.NVIDIA, { handleSelect(GPUDriverMap.NVIDIA, {
label: 'NVIDIA', label: 'NVIDIA',
hiddenTitle: true, hiddenTitle: true,
@@ -116,18 +117,18 @@ const SelectVendor: React.FC<AddWorkerStepProps> = ({ disabled }) => {
{stepIndex}.{' '} {stepIndex}.{' '}
{intl.formatMessage({ id: 'clusters.addworker.selectGPU' })} {intl.formatMessage({ id: 'clusters.addworker.selectGPU' })}
{multiCapable && ( {multiCapable && (
<Tag <span
color="blue"
style={{ style={{
marginLeft: 8, marginLeft: 8,
fontWeight: 400, fontWeight: 400,
borderRadius: 4 fontSize: 13,
color: 'var(--ant-color-text-secondary)'
}} }}
> >
{intl.formatMessage({ {intl.formatMessage({
id: 'clusters.addworker.selectGPU.multiTag' id: 'clusters.addworker.selectGPU.subtitle'
})} })}
</Tag> </span>
)} )}
</Title> </Title>
} }
@@ -1,7 +1,12 @@
import { addWorkerGuide } from '@/pages/resources/config'; import { addWorkerGuide } from '@/pages/resources/config';
import { HighlightCode } from '@gpustack/core-ui'; import { HighlightCode } from '@gpustack/core-ui';
import React from 'react'; import React from 'react';
import { ProviderType } from '../config'; import { ProviderType, ProviderValueMap } from '../config';
// CPU-only (no GPU vendor selected) check for Kubernetes: verify at least one
// ready node exists in the target cluster.
const K8S_CPU_ONLY_CHECK =
'kubectl get nodes -o jsonpath=\'{.items[*].status.conditions[?(@.type=="Ready")].status}\' 2>/dev/null | grep -q "True" && echo "Ready nodes found. You are registering a CPU-only cluster." || (echo "No ready nodes found"; exit 1)';
type ViewModalProps = { type ViewModalProps = {
provider: ProviderType; provider: ProviderType;
@@ -25,6 +30,12 @@ const AddWorkerCommand: React.FC<ViewModalProps> = ({
: currentGPU : currentGPU
? [currentGPU] ? [currentGPU]
: []; : [];
// CPU-only K8s flow: no GPU vendor selected, check for ready nodes instead.
if (!keys.length && provider === ProviderValueMap.Kubernetes) {
return K8S_CPU_ONLY_CHECK;
}
if (!keys.length) return ''; if (!keys.length) return '';
const lines = keys const lines = keys
.map((k) => configs.checkEnvCommand(k)?.[provider || '']) .map((k) => configs.checkEnvCommand(k)?.[provider || ''])
@@ -1,3 +1,4 @@
import { CheckOutlined } from '@ant-design/icons';
import { IconFont, TemplateCard } from '@gpustack/core-ui'; import { IconFont, TemplateCard } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Tooltip } from 'antd'; import { Tooltip } from 'antd';
@@ -62,6 +63,29 @@ const Header = styled.div`
const CardBox = styled.div` const CardBox = styled.div`
display: flex; display: flex;
overflow: hidden; overflow: hidden;
position: relative;
`;
const ActiveBadge = styled.div`
position: absolute;
right: 0;
bottom: 0;
width: 20px;
height: 20px;
background-color: var(--ant-color-primary);
clip-path: path('M 20 0 L 20 15 Q 20 20 15 20 L 0 20 Z');
z-index: 1;
display: flex;
align-items: flex-end;
justify-content: flex-end;
pointer-events: none;
.anticon {
color: #fff;
font-size: 10px;
margin-right: 2px;
margin-bottom: 2px;
}
`; `;
interface ProviderCatalogProps { interface ProviderCatalogProps {
@@ -160,6 +184,13 @@ const ProviderCatalog: React.FC<ProviderCatalogProps> = ({
header={renderTitle(action)} header={renderTitle(action)}
icon={action.icon} icon={action.icon}
></TemplateCard> ></TemplateCard>
{(Array.isArray(current)
? current.includes(action.key)
: current === action.key) && (
<ActiveBadge>
<CheckOutlined />
</ActiveBadge>
)}
</CardBox> </CardBox>
</Tooltip> </Tooltip>
))} ))}
@@ -24,6 +24,7 @@ const Box = styled.div`
background-color: rgba(255, 255, 255, 0.1); background-color: rgba(255, 255, 255, 0.1);
&.active { &.active {
background-color: var(--ant-color-fill-tertiary); background-color: var(--ant-color-fill-tertiary);
border-color: var(--ant-color-primary);
} }
.template-card-icon { .template-card-icon {
margin-right: 0; margin-right: 0;
@@ -57,6 +58,7 @@ const Box = styled.div`
} }
&.active { &.active {
background-color: var(--ant-color-bg-solid); background-color: var(--ant-color-bg-solid);
border-color: var(--ant-color-primary);
} }
} }
} }