feat: model cache volume mount, clear gpu selector

This commit is contained in:
jialin
2025-12-09 19:07:40 +08:00
parent daa10617c0
commit 087675889f
14 changed files with 280 additions and 143 deletions
+5
View File
@@ -73,6 +73,9 @@ Same applies to the <span class="bold-text">/opt/dtk</span> directory.`,
'clusters.addworker.enterWorkerIP': 'Enter worker IP',
'clusters.addworker.enterWorkerIP.error': 'Please enter the worker IP.',
'clusters.addworker.extraVolume': 'Additional Volume Mount',
'clusters.addworker.cacheVolume': 'Model Cache Volume Mount',
'clusters.addworker.cacheVolume.tips':
'If you want to custom the model cache directory, you can specify the path to mount it.',
'clusters.addworker.configSummary': 'Configuration Summary',
'clusters.addworker.gpuVendor': 'GPU Vendor',
'clusters.addworker.workerIP': 'Worker IP',
@@ -80,6 +83,8 @@ Same applies to the <span class="bold-text">/opt/dtk</span> directory.`,
'clusters.addworker.autoDetect': 'Auto-detect',
'clusters.addworker.extraVolume.holder':
'e.g. /data/models (path must start with /)',
'clusters.addworker.cacheVolume.holder':
'e.g. /data/cache (path must start with /)',
'clusters.addworker.vendorNotes.title': 'Notes for {vendor} Device',
'clusters.button.genToken':
'Need to create a new token? Click <a href="{link}" target="_blank">here</a>.',
+9 -1
View File
@@ -73,6 +73,9 @@ Same applies to the <span class="bold-text">/opt/dtk</span> directory.`,
'clusters.addworker.enterWorkerIP': 'Enter worker IP',
'clusters.addworker.enterWorkerIP.error': 'Please enter the worker IP.',
'clusters.addworker.extraVolume': 'Additional Volume Mount',
'clusters.addworker.cacheVolume': 'Model Cache Volume Mount',
'clusters.addworker.cacheVolume.tips':
'If you want to custom the model cache directory, you can specify the path to mount it.',
'clusters.addworker.configSummary': 'Configuration Summary',
'clusters.addworker.gpuVendor': 'GPU Vendor',
'clusters.addworker.workerIP': 'Worker IP',
@@ -80,6 +83,8 @@ Same applies to the <span class="bold-text">/opt/dtk</span> directory.`,
'clusters.addworker.autoDetect': 'Auto-detect',
'clusters.addworker.extraVolume.holder':
'e.g. /data/models (path must start with /)',
'clusters.addworker.cacheVolume.holder':
'e.g. /data/cache (path must start with /)',
'clusters.addworker.vendorNotes.title': 'Notes for {vendor} Device',
'clusters.button.genToken':
'Need to create a new token? Click <a href="{link}" target="_blank">here</a>.',
@@ -156,5 +161,8 @@ Same applies to the <span class="bold-text">/opt/dtk</span> directory.`,
// 67. 'clusters.addworker.extraVolume.holder': 'e.g. /data/models (path must start with /)'
// 68. 'clusters.addworker.vendorNotes.title': 'Notes for {vendor} Device',
// 69. 'clusters.button.genToken': 'Need to create a new token? Click <a href="{link}" target="_blank">here</a>.',
// 70. 'clusters.addworker.amdNotes-01': `If the <span class="bold-text">/opt/rocm</span> directory does not exist, please create a symbolic link pointing to the ROCm installed path: <span class="bold-text">ln -s /path/to/rocm /opt/rocm</span>.`
// 70. 'clusters.addworker.amdNotes-01': `If the <span class="bold-text">/opt/rocm</span> directory does not exist, please create a symbolic link pointing to the ROCm installed path: <span class="bold-text">ln -s /path/to/rocm /opt/rocm</span>.`,
// 71. 'clusters.addworker.cacheVolume': 'Model Cache Volume Mount',
// 72. 'clusters.addworker.cacheVolume.tips': 'If you want to custom the model cache directory, you can specify the path to mount it.',
// 73. 'clusters.addworker.cacheVolume.holder': 'e.g. /data/cache (path must start with /)',
// ========== End of To-Do List ==========
+7
View File
@@ -73,6 +73,9 @@ export default {
'clusters.addworker.enterWorkerIP': 'Введите IP воркера',
'clusters.addworker.enterWorkerIP.error': 'Пожалуйста, введите IP воркера.',
'clusters.addworker.extraVolume': 'Дополнительное монтирование тома',
'clusters.addworker.cacheVolume': 'Model Cache Volume Mount',
'clusters.addworker.cacheVolume.tips':
'If you want to custom the model cache directory, you can specify the path to mount it.',
'clusters.addworker.configSummary': 'Сводка конфигурации',
'clusters.addworker.gpuVendor': 'Производитель GPU',
'clusters.addworker.workerIP': 'IP воркера',
@@ -80,6 +83,8 @@ export default {
'clusters.addworker.autoDetect': 'Автоопределение',
'clusters.addworker.extraVolume.holder':
'напр. /data/models (путь должен начинаться с /)',
'clusters.addworker.cacheVolume.holder':
'напр. /data/cache (путь должен начинаться с /)',
'clusters.addworker.vendorNotes.title': 'Примечания для устройств {vendor}',
'clusters.button.genToken':
'Need to create a new token? Click <a href="{link}" target="_blank">here</a>.',
@@ -89,4 +94,6 @@ export default {
// ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'clusters.addworker.amdNotes-01': `If the <span class="bold-text">/opt/rocm</span> directory does not exist, please create a symbolic link pointing to the ROCm installed path: <span class="bold-text">ln -s /path/to/rocm /opt/rocm</span>.`,
// 2. 'clusters.button.genToken': 'Need to create a new token? Click <a href="{link}" target="_blank">here</a>.',
// 3. 'clusters.addworker.cacheVolume': 'Model Cache Volume Mount',
// 4. 'clusters.addworker.cacheVolume.tips': 'If you want to custom the model cache directory, you can specify the path to mount it.',
// ================================================================
+6 -1
View File
@@ -48,7 +48,7 @@ export default {
'clusters.addworker.nvidiaNotes-01':
'如果节点有多个出站 IP 地址,请填写 <span class="bold-text">WORKER_IP</span>,以确保使用指定的 IP。可通过命令 <span class="bold-text">hostname -I | xargs -n1</span> 进行确认。',
'clusters.addworker.nvidiaNotes-02':
'如果模型目录已存在于节点上,请添加 <span class="bold-text">--volume</span> 参数进行挂载。',
'如果 worker 上已经存在模型目录,你可以指定该路径进行挂载。',
'clusters.addworker.hygonNotes':
'如果 <span class="bold-text">/opt/hyhal</span> 目录不存在,请创建指向 Hygon 安装路径的符号链接:<span class="bold-text">/opt/hyhal</span>。与 <span class="bold-text">/opt/dtk</span> 目录相同。',
'clusters.addworker.corexNotes':
@@ -71,6 +71,9 @@ export default {
'clusters.addworker.enterWorkerIP': '输入节点 IP',
'clusters.addworker.enterWorkerIP.error': '请输入节点 IP',
'clusters.addworker.extraVolume': '额外卷挂载',
'clusters.addworker.cacheVolume': '缓存卷挂载',
'clusters.addworker.cacheVolume.tips':
'如果你想自定义模型缓存目录,可以指定路径进行挂载。',
'clusters.addworker.configSummary': '配置摘要',
'clusters.addworker.gpuVendor': 'GPU 厂商',
'clusters.addworker.workerIP': '节点 IP',
@@ -78,6 +81,8 @@ export default {
'clusters.addworker.autoDetect': '自动检测',
'clusters.addworker.extraVolume.holder':
'例如:/data/models(路径需以 / 开头)',
'clusters.addworker.cacheVolume.holder':
'例如:/data/cache(路径需以 / 开头)',
'clusters.addworker.vendorNotes.title': '{vendor}设备注意事项',
'clusters.button.genToken':
'需要创建令牌?点击<a href="{link}" target="_blank">这里</a>。',
+5 -1
View File
@@ -316,8 +316,12 @@ const VersionsForm: React.FC<AddModalProps> = ({
>
<SealInput.Input
trim
label={intl.formatMessage({ id: 'backend.imageName' })}
required
placeholder={intl.formatMessage(
{ id: 'common.help.eg' },
{ content: 'vllm/vllm-openai:v0.12.0' }
)}
label={intl.formatMessage({ id: 'backend.imageName' })}
></SealInput.Input>
</Form.Item>
</Box>
@@ -6,6 +6,7 @@ type ViewModalProps = {
currentGPU?: string;
workerIP?: string;
modelDir?: string;
cacheDir?: string;
registrationInfo: {
token: string;
image: string;
@@ -17,6 +18,7 @@ const AddWorkerCommand: React.FC<ViewModalProps> = ({
registrationInfo,
workerIP,
modelDir,
cacheDir,
currentGPU
}) => {
const code = React.useMemo(() => {
@@ -28,13 +30,14 @@ const AddWorkerCommand: React.FC<ViewModalProps> = ({
tag: '',
workerIP: workerIP,
modelDir: modelDir,
cacheDir: cacheDir,
image: registrationInfo.image,
token: registrationInfo.token || '${token}'
})
?.trim()
.replace(/\s+$/gm, '')
.replace(/\\+$/, '');
}, [registrationInfo, currentGPU, workerIP, modelDir]);
}, [registrationInfo, currentGPU, workerIP, modelDir, cacheDir]);
return (
<HighlightCode
@@ -42,6 +42,10 @@ export interface SummaryDataKeys {
enabled: boolean;
path: string;
};
cacheDirConfig: {
enabled: boolean;
path: string;
};
workerIPConfig: {
enabled: boolean;
ip: string;
@@ -21,6 +21,10 @@ const DockerRunCommand = () => {
path: '',
required: false
};
const cacheDirConfig = summary.get('cacheDirConfig') || {
enable: false,
path: ''
};
const currentGPU = summary.get('currentGPU') || '';
const stepIndex = stepList.indexOf(StepNamesMap.RunCommand) + 1;
@@ -51,6 +55,7 @@ const DockerRunCommand = () => {
registrationInfo={registrationInfo}
workerIP={workerIPConfig.enable ? workerIPConfig.ip : ''}
modelDir={modelDirConfig.enable ? modelDirConfig.path : ''}
cacheDir={cacheDirConfig.enable ? cacheDirConfig.path : ''}
currentGPU={currentGPU}
/>
</StepCollapse>
@@ -2,12 +2,79 @@ import AlertInfoBlock from '@/components/alert-info/block';
import { ExclamationCircleFilled } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Input, Switch } from 'antd';
import { useEffect } from 'react';
import React, { useEffect } from 'react';
import styled from 'styled-components';
import { useAddWorkerContext } from './add-worker-context';
import { StepNamesMap } from './config';
import { NotesWrapper, SwitchWrapper, Tips, Title } from './constainers';
import StepCollapse from './step-collapse';
const ButtonWrapper = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
`;
const SwitchSetting: React.FC<{
label: string;
checked: boolean;
value?: string;
placeholder?: string;
tips?: React.ReactNode;
errorMessage?: React.ReactNode;
extra?: React.ReactNode;
onInputChange?: (value: string) => void;
onChange: (checked: boolean) => void;
}> = ({
label,
checked,
onChange,
value,
placeholder,
tips,
extra,
errorMessage,
onInputChange
}) => {
return (
<SwitchWrapper>
<ButtonWrapper>
<span style={{ color: 'var(--ant-color-text)', fontWeight: 500 }}>
<span>{label}</span>
</span>
<Switch checked={checked} onChange={onChange}></Switch>
</ButtonWrapper>
{tips && (
<Tips
dangerouslySetInnerHTML={{
__html: tips
}}
></Tips>
)}
{checked && (
<>
<Input
style={{ width: '100%' }}
value={value}
placeholder={placeholder}
onChange={(e) => onInputChange?.(e.target.value)}
/>
{errorMessage && (
<Tips
style={{
color: 'var(--ant-color-error)'
}}
>
{errorMessage}
</Tips>
)}
</>
)}
{extra}
</SwitchWrapper>
);
};
const SpecifyArguments = () => {
const intl = useIntl();
const { stepList, summary, updateField, registerField } =
@@ -24,6 +91,11 @@ const SpecifyArguments = () => {
path: ''
};
const cacheDirConfig = summary.get('cacheDirConfig') || {
enable: false,
path: ''
};
const setWorkerIPConfig = (config: {
enable: boolean;
ip?: string;
@@ -42,6 +114,13 @@ const SpecifyArguments = () => {
});
};
const setCacheDirConfig = (config: { enable: boolean; path?: string }) => {
updateField('cacheDirConfig', {
...cacheDirConfig,
...config
});
};
const beforeNext = async () => {
if (workerIPConfig.enable && !workerIPConfig.ip) {
setWorkerIPConfig({
@@ -56,16 +135,13 @@ const SpecifyArguments = () => {
};
useEffect(() => {
const unregister = registerField('workerIPConfig');
const unregisterWorkerIP = registerField('workerIPConfig');
const unregisterModelDir = registerField('modelDirConfig');
const unregisterCacheDir = registerField('cacheDirConfig');
return () => {
unregister();
};
}, []);
useEffect(() => {
const unregister = registerField('modelDirConfig');
return () => {
unregister();
unregisterWorkerIP();
unregisterModelDir();
unregisterCacheDir();
};
}, []);
@@ -80,6 +156,11 @@ const SpecifyArguments = () => {
enable: false,
path: ''
});
updateField('cacheDirConfig', {
enable: false,
path: ''
});
}, []);
return (
@@ -102,123 +183,111 @@ const SpecifyArguments = () => {
}}
>
{/* worker IP config */}
<SwitchWrapper>
<div className="button">
<span style={{ color: 'var(--ant-color-text)', fontWeight: 500 }}>
{workerIPConfig.enable
? intl.formatMessage({
id: 'clusters.addworker.specifyWorkerIP'
})
: intl.formatMessage({
id: 'clusters.addworker.detectWorkerIP'
})}
</span>
<Switch
checked={workerIPConfig.enable}
onChange={(checked) =>
setWorkerIPConfig({
...workerIPConfig,
enable: checked,
required: false
<SwitchSetting
label={
workerIPConfig.enable
? intl.formatMessage({
id: 'clusters.addworker.specifyWorkerIP'
})
}
></Switch>
</div>
{workerIPConfig.enable && (
<>
<Input
style={{ width: '100%' }}
placeholder={intl.formatMessage({
id: 'clusters.addworker.enterWorkerIP'
})}
value={workerIPConfig.ip}
onChange={(e) =>
setWorkerIPConfig({
...workerIPConfig,
ip: e.target.value
})
: intl.formatMessage({
id: 'clusters.addworker.detectWorkerIP'
})
}
placeholder={intl.formatMessage({
id: 'clusters.addworker.enterWorkerIP'
})}
value={workerIPConfig.ip}
checked={workerIPConfig.enable}
errorMessage={
workerIPConfig.required &&
!workerIPConfig.ip &&
intl.formatMessage({
id: 'clusters.addworker.enterWorkerIP.error'
})
}
onChange={(checked) =>
setWorkerIPConfig({
...workerIPConfig,
enable: checked,
required: false
})
}
onInputChange={(value) =>
setWorkerIPConfig({
...workerIPConfig,
ip: value
})
}
extra={
!workerIPConfig.enable && (
<AlertInfoBlock
maxHeight={200}
contentStyle={{
paddingLeft: 0
}}
type="warning"
icon={<ExclamationCircleFilled />}
message={
<NotesWrapper>
<li
style={{
marginLeft: '0 !important',
listStyleType: 'none'
}}
dangerouslySetInnerHTML={{
__html: intl.formatMessage({
id: 'clusters.addworker.nvidiaNotes-01'
})
}}
></li>
</NotesWrapper>
}
/>
{workerIPConfig.required && !workerIPConfig.ip && (
<Tips
style={{
color: 'var(--ant-color-error)'
}}
>
{intl.formatMessage({
id: 'clusters.addworker.enterWorkerIP.error'
})}
</Tips>
)}
</>
)}
{!workerIPConfig.enable && (
<AlertInfoBlock
maxHeight={200}
contentStyle={{
paddingLeft: 0
}}
style={{ marginBottom: 8 }}
type="warning"
icon={<ExclamationCircleFilled />}
message={
<NotesWrapper>
<li
style={{
marginLeft: '0 !important',
listStyleType: 'none'
}}
dangerouslySetInnerHTML={{
__html: intl.formatMessage({
id: 'clusters.addworker.nvidiaNotes-01'
})
}}
></li>
</NotesWrapper>
}
></AlertInfoBlock>
)}
</SwitchWrapper>
></AlertInfoBlock>
)
}
></SwitchSetting>
{/* model directory config */}
<SwitchWrapper>
<div className="button">
<span style={{ color: 'var(--ant-color-text)', fontWeight: 500 }}>
{/* optional */}
<span>
{intl.formatMessage({ id: 'clusters.addworker.extraVolume' })}
</span>
</span>
<Switch
checked={modelDirConfig.enable}
onChange={(checked) =>
setModelDirConfig({ ...modelDirConfig, enable: checked })
}
></Switch>
</div>
<Tips
dangerouslySetInnerHTML={{
__html: intl.formatMessage({
id: 'clusters.addworker.nvidiaNotes-02'
})
}}
></Tips>
{modelDirConfig.enable && (
<Input
style={{ width: '100%' }}
value={modelDirConfig.path}
placeholder={intl.formatMessage({
id: 'clusters.addworker.extraVolume.holder'
})}
onChange={(e) =>
setModelDirConfig({
...modelDirConfig,
path: e.target.value
})
}
/>
)}
</SwitchWrapper>
<SwitchSetting
label={intl.formatMessage({ id: 'clusters.addworker.extraVolume' })}
tips={intl.formatMessage({
id: 'clusters.addworker.nvidiaNotes-02'
})}
placeholder={intl.formatMessage({
id: 'clusters.addworker.extraVolume.holder'
})}
checked={modelDirConfig.enable}
value={modelDirConfig.path}
onChange={(checked) =>
setModelDirConfig({ ...modelDirConfig, enable: checked })
}
onInputChange={(value) =>
setModelDirConfig({
...modelDirConfig,
path: value
})
}
></SwitchSetting>
{/* cache directory config */}
<SwitchSetting
label={intl.formatMessage({ id: 'clusters.addworker.cacheVolume' })}
tips={intl.formatMessage({
id: 'clusters.addworker.cacheVolume.tips'
})}
placeholder={intl.formatMessage({
id: 'clusters.addworker.cacheVolume.holder'
})}
checked={cacheDirConfig.enable}
value={cacheDirConfig.path}
onChange={(checked) =>
setCacheDirConfig({ ...cacheDirConfig, enable: checked })
}
onInputChange={(value) =>
setCacheDirConfig({
...cacheDirConfig,
path: value
})
}
></SwitchSetting>
</div>
</StepCollapse>
);
@@ -27,6 +27,11 @@ const SummaryData: React.FC = () => {
path: ''
};
const cacheDirConfig = summary.get('cacheDirConfig') || {
enable: false,
path: ''
};
return (
<ConfigWrapper>
<Title>
@@ -129,6 +134,32 @@ const SummaryData: React.FC = () => {
)}
</span>
</div>
<div className="item">
<span className="label">
{intl.formatMessage({ id: 'clusters.addworker.cacheVolume' })}:
</span>
<span className="value">
{cacheDirConfig.enable && cacheDirConfig.path
? cacheDirConfig.path
: ''}
{(!cacheDirConfig.path || !cacheDirConfig.enable) && (
<StopOutlined
style={{
color: 'var(--ant-color-text-tertiary)'
}}
/>
)}
{cacheDirConfig.enable && cacheDirConfig.path && (
<CheckCircleOutlined
style={{
color: 'var(--ant-color-success)',
marginLeft: 4
}}
/>
)}
</span>
</div>
</div>
</ConfigWrapper>
);
@@ -36,7 +36,7 @@ const AccessControlModal: React.FC<
message.success(intl.formatMessage({ id: 'common.message.success' }));
onCancel?.();
} catch (error) {
message.error(intl.formatMessage({ id: 'common.message.failed' }));
// do nothing, error message handled in request layer
}
};
+5 -12
View File
@@ -6,7 +6,7 @@ import CollapsePanel from '@/pages/_components/collapse-panel';
import { useWrapperContext } from '@/pages/_components/column-wrapper/use-wrapper-context';
import { useIntl } from '@umijs/max';
import useMemoizedFn from 'ahooks/lib/useMemoizedFn';
import { Form, Segmented } from 'antd';
import { Form } from 'antd';
import _ from 'lodash';
import React, { forwardRef, useImperativeHandle, useMemo } from 'react';
import styled from 'styled-components';
@@ -42,14 +42,6 @@ const scheduleRequiredFields = ['gpu_selector'];
const performanceRequiredFields = ['speculative_config'];
const SegmentedInner = styled(Segmented)`
width: 100%;
border-radius: 0;
.ant-segmented-item {
flex: 1;
}
`;
const SegmentedHeader = styled.div<{ $top?: number }>`
position: sticky;
top: ${(props) => props.$top || 0}px;
@@ -260,9 +252,10 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
await onClusterChange?.(value);
getGPUOptionList({ clusterId: value });
getBackendOptions({ cluster_id: value });
if (scheduleType === ScheduleValueMap.Manual) {
form.setFieldValue(['gpu_selector', 'gpu_ids'], []);
}
form.setFieldsValue({
scheduleType: ScheduleValueMap.Auto,
gpu_selector: null
});
await new Promise((resolve) => {
setTimeout(resolve, 150);
});
+2 -1
View File
@@ -174,6 +174,7 @@ export const useCheckCompatibility = () => {
const handleEvaluate = async (data: any) => {
try {
// when no cluster selected, show warning and prompt user to add cluster first
console.log('handleEvaluate', data);
if (!data.cluster_id) {
setWarningStatus({
show: true,
@@ -388,7 +389,7 @@ export const useCheckCompatibility = () => {
const handleDoEvalute = async (formData: FormData) => {
const currentRequestId = updateRequestId();
const evalutionData = await handleEvaluate(formData);
if (currentRequestId === requestIdRef.current) {
if (currentRequestId === requestIdRef.current && evalutionData) {
handleShowCompatibleAlert?.(evalutionData);
return evalutionData;
}
+3 -1
View File
@@ -127,13 +127,15 @@ export const dockerEnvCommandMap = {
)
};
const setNormalArgs = (params: any) => {
console.log('params++++++++++++++', params);
return `sudo docker run -d --name gpustack-worker \\
--restart=unless-stopped \\
--privileged \\
--network=host \\
--volume /var/run/docker.sock:/var/run/docker.sock \\
--volume gpustack-data:/var/lib/gpustack \\
${params.modelDir ? `--volume ${params.modelDir}:${params.modelDir} \\` : ''}`;
${params.modelDir ? `--volume ${params.modelDir}:${params.modelDir} \\` : ''}
${params.cacheDir ? `--volume ${params.cacheDir}:/var/lib/gpustack/cache \\` : ''}`;
};
const setImageArgs = (params: any) => {