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 -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) => {