feat: worker_config

This commit is contained in:
jialin
2025-12-16 18:43:28 +08:00
parent 5225fd328d
commit 6968f9fa6b
12 changed files with 512 additions and 121 deletions
+3 -1
View File
@@ -90,5 +90,7 @@ Same applies to the <span class="bold-text">/opt/dtk</span> directory.`,
'Need to create a new token? Click <a href="{link}" target="_blank">here</a>.',
'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>.`,
'clusters.addworker.message.success':
'{count} workers have been added to the cluster.'
'{count} workers have been added to the cluster.',
'clusters.create.serverUrl': 'Server URL',
'clusters.create.workerConfig': 'Worker Configuration'
};
+6 -2
View File
@@ -90,7 +90,9 @@ Same applies to the <span class="bold-text">/opt/dtk</span> directory.`,
'Need to create a new token? Click <a href="{link}" target="_blank">here</a>.',
'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>.`,
'clusters.addworker.message.success':
'{count} workers have been added to the cluster.'
'{count} workers have been added to the cluster.',
'clusters.create.serverUrl': 'Server URL',
'clusters.create.workerConfig': 'Worker Configuration'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
@@ -167,5 +169,7 @@ Same applies to the <span class="bold-text">/opt/dtk</span> directory.`,
// 71. 'clusters.addworker.cacheVolume': 'Model Cache Volume Mount',
// 72. 'clusters.addworker.cacheVolume.tips': 'If you want to customize 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 /)',
// 74. 'clusters.addworker.message.success': '{count} workers have been added to the cluster.'
// 74. 'clusters.addworker.message.success': '{count} workers have been added to the cluster.',
// 75. 'clusters.create.serverUrl': 'Server URL',
// 76. 'clusters.create.workerConfig': 'Worker Configuration'
// ========== End of To-Do List ==========
+6 -2
View File
@@ -90,7 +90,9 @@ export default {
'Need to create a new token? Click <a href="{link}" target="_blank">here</a>.',
'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>.`,
'clusters.addworker.message.success':
'{count} workers have been added to the cluster.'
'{count} workers have been added to the cluster.',
'clusters.create.serverUrl': 'Server URL',
'clusters.create.workerConfig': 'Worker Configuration'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
@@ -98,5 +100,7 @@ export default {
// 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 customize the model cache directory, you can specify the path to mount it.',
// 5. 'clusters.addworker.message.success': '{count} workers have been added to the cluster.'
// 5. 'clusters.addworker.message.success': '{count} workers have been added to the cluster.',
// 6. 'clusters.create.serverUrl': 'Server URL',
// 7. 'clusters.create.workerConfig': 'Worker Configuration'
// ================================================================
+3 -1
View File
@@ -88,5 +88,7 @@ export default {
'需要创建令牌?点击<a href="{link}" target="_blank">这里</a>。',
'clusters.addworker.amdNotes-01':
'如果 <span class="bold-text">/opt/rocm</span> 目录不存在,请创建一个指向已安装 ROCm 路径的符号链接:<span class="bold-text">ln -s /path/to/rocm /opt/rocm</span>。',
'clusters.addworker.message.success': '已将 {count} 个工作节点添加到集群中。'
'clusters.addworker.message.success': '已将 {count} 个工作节点添加到集群中。',
'clusters.create.serverUrl': '服务器地址',
'clusters.create.workerConfig': '节点配置'
};
@@ -1,6 +1,4 @@
import EditorWrap from '@/components/editor-wrap';
import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import { LoadingOutlined } from '@ant-design/icons';
import Editor, { loader } from '@monaco-editor/react';
import * as monaco from 'monaco-editor';
@@ -11,15 +9,11 @@ import React, {
useImperativeHandle,
useRef
} from 'react';
import createSchema from '../config/schema/create.json';
import updateBuiltinSchema from '../config/schema/update-builtin.json';
import udpateCustomSchema from '../config/schema/update-custom.json';
loader.config({ monaco });
interface ViewerProps {
ref?: any;
lang: string;
defaultLang?: string;
config?: any;
value: string;
@@ -28,22 +22,19 @@ interface ViewerProps {
header?: React.ReactNode;
placeholder?: string;
variant?: 'bordered' | 'borderless';
actionStatus: {
action: PageActionType;
isBuiltIn: boolean;
};
schema?: any;
}
const path = 'inmemory://model/config.yaml';
const ViewerEditor: React.FC<ViewerProps> = forwardRef((props, ref) => {
const EditorInner: React.FC<ViewerProps> = forwardRef((props, ref) => {
const {
value,
height = 380,
theme = 'vs-dark',
header,
variant = 'borderless',
actionStatus,
schema,
placeholder
} = props;
@@ -59,12 +50,7 @@ const ViewerEditor: React.FC<ViewerProps> = forwardRef((props, ref) => {
{
uri: 'http://example.com/schema-name.json',
fileMatch: [yamlUri.toString()],
schema:
actionStatus.action === PageAction.CREATE
? createSchema
: actionStatus.isBuiltIn
? updateBuiltinSchema
: udpateCustomSchema
schema: schema
}
]
});
@@ -151,4 +137,4 @@ const ViewerEditor: React.FC<ViewerProps> = forwardRef((props, ref) => {
);
});
export default ViewerEditor;
export default EditorInner;
+172
View File
@@ -0,0 +1,172 @@
import useUserSettings from '@/hooks/use-user-settings';
import { ImportOutlined } from '@ant-design/icons';
import { loader } from '@monaco-editor/react';
import { useIntl } from '@umijs/max';
import { Button, message, Typography, Upload } from 'antd';
import { RcFile } from 'antd/lib/upload';
import * as monaco from 'monaco-editor';
import React, {
forwardRef,
useEffect,
useImperativeHandle,
useRef
} from 'react';
import styled from 'styled-components';
import EditorInner from './editor';
const { Text } = Typography;
loader.config({ monaco });
const Container = styled.div`
position: relative;
border: 1px solid var(--ant-color-border);
border-radius: var(--ant-border-radius);
.monaco-editor .scroll-decoration {
box-shadow: none;
}
`;
const ErrorText = styled(Text)`
position: absolute;
left: 0;
right: 0;
bottom: 0;
padding: 4px 6px;
background-color: var(--ant-color-bg-elevated);
border-radius: 0 0 var(--ant-border-radius) var(--ant-border-radius);
`;
const Header = styled.div`
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
height: 40px;
padding-inline: 10px;
font-size: 14px;
border-bottom: 1px solid var(--ant-color-border);
background-color: var(--ant-color-fill-tertiary);
`;
interface ViewerProps {
ref?: any;
title?: React.ReactNode;
defaultLang?: string;
config?: any;
value: string;
height?: string | number;
placeholder?: string;
variant?: 'bordered' | 'borderless';
validateMessage?: React.ReactNode;
schema?: any;
onUpload?: (content: string) => void;
}
const YamlEditor: React.FC<ViewerProps> = forwardRef((props, ref) => {
const {
value,
height = 380,
variant = 'borderless',
schema,
placeholder,
validateMessage,
title,
onUpload
} = props;
const intl = useIntl();
const { userSettings } = useUserSettings();
const editorRef = useRef<any>(null);
const setContent = (val: string) => {
editorRef.current?.setValue?.(val);
};
const beforeUpload = (file: RcFile) => {
const isYaml =
file.type === 'application/x-yaml' ||
file.type === 'text/yaml' ||
file.name.endsWith('.yaml') ||
file.name.endsWith('.yml');
if (!isYaml) {
message.error('You can only upload YAML file!');
}
const reader = new FileReader();
reader.onload = (e) => {
const content = e.target?.result;
if (typeof content === 'string') {
onUpload?.(content);
setContent(content);
} else {
message.error('Failed to read file content!');
}
};
reader.readAsText(file);
// Prevent upload
return false;
};
const renderHeader = () => {
return (
<Header>
<span className="title">{title || 'YAML'}</span>
<Upload
name="file"
multiple={false}
beforeUpload={beforeUpload}
showUploadList={false}
accept=".yaml,.yml,text/yaml,application/x-yaml"
>
<Button icon={<ImportOutlined />} type="text" size="small">
{intl.formatMessage({ id: 'common.button.import' })}
</Button>
</Upload>
</Header>
);
};
useImperativeHandle(ref, () => ({
format: () => {
editorRef.current?.format();
},
getValue: () => {
return editorRef.current?.getValue?.();
},
setValue: (val: string) => {
editorRef.current?.setValue?.(val);
},
dispose: () => {
editorRef.current?.dispose?.();
},
validate() {
return editorRef.current?.validate();
},
editor: editorRef.current
}));
useEffect(() => {
editorRef.current?.format();
}, [value]);
return (
<Container>
<EditorInner
ref={editorRef}
header={renderHeader()}
variant={variant}
height={height}
theme={userSettings?.isDarkTheme ? 'vs-dark' : 'vs-light'}
value={value}
placeholder={placeholder}
schema={schema}
/>
{validateMessage && (
<ErrorText type="danger">{validateMessage}</ErrorText>
)}
</Container>
);
});
export default YamlEditor;
+21 -94
View File
@@ -1,51 +1,17 @@
import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import useUserSettings from '@/hooks/use-user-settings';
import { ImportOutlined } from '@ant-design/icons';
import YamlEditor from '@/pages/_components/yaml-editor';
import { useIntl } from '@umijs/max';
import { Button, message, Typography, Upload } from 'antd';
import { RcFile } from 'antd/lib/upload';
import React, {
forwardRef,
useImperativeHandle,
useRef,
useState
} from 'react';
import styled from 'styled-components';
import { yaml2Json, yamlTemplate } from '../config';
import YamlEditor from './yaml-editor';
const { Text } = Typography;
const ErrorText = styled(Text)`
position: absolute;
left: 0;
right: 0;
bottom: 0;
padding: 4px 6px;
background-color: var(--ant-color-bg-elevated);
border-radius: 0 0 var(--ant-border-radius) var(--ant-border-radius);
`;
const Header = styled.div`
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
height: 40px;
padding-inline: 10px;
border-bottom: 1px solid var(--ant-color-border);
background-color: var(--ant-color-fill-tertiary);
`;
const Container = styled.div`
position: relative;
border: 1px solid var(--ant-color-border);
border-radius: var(--ant-border-radius);
.monaco-editor .scroll-decoration {
box-shadow: none;
}
`;
import createSchema from '../config/schema/create.json';
import updateBuiltinSchema from '../config/schema/update-builtin.json';
import udpateCustomSchema from '../config/schema/update-custom.json';
interface ImportYAMLProps {
ref?: any;
@@ -60,7 +26,6 @@ interface ImportYAMLProps {
const ImportYAML: React.FC<ImportYAMLProps> = forwardRef(
({ actionStatus, content = '' }, ref) => {
const intl = useIntl();
const { userSettings } = useUserSettings();
const editorRef = useRef<any>(null);
const [fileContent, setFileContent] = useState<string>(
actionStatus.action === PageAction.CREATE ? yamlTemplate : content
@@ -71,30 +36,6 @@ const ImportYAML: React.FC<ImportYAMLProps> = forwardRef(
editorRef.current?.setValue?.(val);
};
const beforeUpload = (file: RcFile) => {
const isYaml =
file.type === 'application/x-yaml' ||
file.type === 'text/yaml' ||
file.name.endsWith('.yaml') ||
file.name.endsWith('.yml');
if (!isYaml) {
message.error('You can only upload YAML file!');
}
const reader = new FileReader();
reader.onload = (e) => {
const content = e.target?.result;
if (typeof content === 'string') {
setFileContent(content);
setContent(content);
} else {
message.error('Failed to read file content!');
}
};
reader.readAsText(file);
// Prevent upload
return false;
};
const validate = () => {
const markers = editorRef.current?.validate();
if (markers && markers.length > 0) {
@@ -152,25 +93,6 @@ const ImportYAML: React.FC<ImportYAMLProps> = forwardRef(
}
};
const renderHeader = () => {
return (
<Header>
<span className="title">YAML</span>
<Upload
name="file"
multiple={false}
beforeUpload={beforeUpload}
showUploadList={false}
accept=".yaml,.yml,text/yaml,application/x-yaml"
>
<Button icon={<ImportOutlined />} type="text" size="small">
{intl.formatMessage({ id: 'common.button.import' })}
</Button>
</Upload>
</Header>
);
};
useImperativeHandle(ref, () => ({
getContent: () => {
return getContent();
@@ -182,18 +104,23 @@ const ImportYAML: React.FC<ImportYAMLProps> = forwardRef(
}));
return (
<Container>
<YamlEditor
ref={editorRef}
lang="yaml"
actionStatus={actionStatus}
theme={userSettings?.isDarkTheme ? 'vs-dark' : 'vs-light'}
value={fileContent}
height={'calc(100vh - 260px)'}
header={renderHeader()}
></YamlEditor>
{error && <ErrorText type="danger">{error}</ErrorText>}
</Container>
<YamlEditor
ref={editorRef}
value={fileContent}
height={'calc(100vh - 260px)'}
validateMessage={error}
onUpload={(content) => {
setError('');
setContent(content);
}}
schema={
actionStatus.action === PageAction.CREATE
? createSchema
: actionStatus.isBuiltIn
? updateBuiltinSchema
: udpateCustomSchema
}
></YamlEditor>
);
}
);
@@ -1,6 +1,8 @@
import SealInput from '@/components/seal-form/seal-input';
import SealTextArea from '@/components/seal-form/seal-textarea';
import { PageActionType } from '@/config/types';
import CollapsePanel from '@/pages/_components/collapse-panel';
import { json2Yaml, yaml2Json } from '@/pages/backends/config';
import { useIntl } from '@umijs/max';
import { Form } from 'antd';
import React, { forwardRef, useEffect, useImperativeHandle } from 'react';
@@ -9,6 +11,7 @@ import {
ClusterFormData as FormData,
ClusterListItem as ListItem
} from '../config/types';
import AdvanceConfig from '../step-forms/advance-config';
import CloudProvider from './cloud-provider-form';
type AddModalProps = {
@@ -23,12 +26,27 @@ const ClusterForm: React.FC<AddModalProps> = forwardRef(
({ action, provider, currentData, credentialList, onFinish }, ref) => {
const [form] = Form.useForm();
const intl = useIntl();
const [activeKey, setActiveKey] = React.useState<string[]>([]);
const advanceConfigRef = React.useRef<any>(null);
const handleOnCollapseChange = (keys: string | string[]) => {
setActiveKey(Array.isArray(keys) ? keys : [keys]);
if (currentData && advanceConfigRef.current) {
}
};
useEffect(() => {
if (currentData) {
form.setFieldsValue(currentData);
if (advanceConfigRef.current) {
const workerConfigYaml = json2Yaml({
worker_config: currentData.worker_config || {}
});
advanceConfigRef.current?.setYamlValue(workerConfigYaml);
}
}
}, [currentData]);
}, [currentData, advanceConfigRef.current]);
useImperativeHandle(ref, () => ({
resetFields: () => {
@@ -44,7 +62,15 @@ const ClusterForm: React.FC<AddModalProps> = forwardRef(
return form.getFieldsValue();
},
validateFields: async () => {
return await form.validateFields();
const values = await form.validateFields();
const workerConfig = yaml2Json(
advanceConfigRef.current?.getYamlValue()
);
return {
...values,
...workerConfig
};
}
}));
@@ -91,6 +117,24 @@ const ClusterForm: React.FC<AddModalProps> = forwardRef(
label={intl.formatMessage({ id: 'common.table.description' })}
></SealTextArea>
</Form.Item>
<CollapsePanel
accordion={false}
activeKey={activeKey}
onChange={handleOnCollapseChange}
items={[
{
key: 'advanceConfig',
label: intl.formatMessage({ id: 'resources.form.advanced' }),
forceRender: true,
children: (
<AdvanceConfig
action={action}
ref={advanceConfigRef}
></AdvanceConfig>
)
}
]}
></CollapsePanel>
</Form>
);
}
@@ -53,6 +53,7 @@ export interface ClusterListItem {
name: string;
display_name: string;
description: string;
worker_config: Record<string, any>;
provider: ProviderType;
credential_id: number;
zone: string;
@@ -74,5 +75,7 @@ export interface ClusterFormData {
credential_id: number;
zone: string;
region: string;
server_url?: string;
worker_config?: Record<string, any>;
worker_pools?: NodePoolFormData[];
}
@@ -0,0 +1,119 @@
{
"type": "object",
"properties": {
"worker_config": {
"type": "object",
"additionalProperties": false,
"properties": {
"debug": {
"type": "boolean",
"description": "Enable debug mode"
},
"cache_dir": {
"type": "string",
"description": "Cache directory path"
},
"log_dir": {
"type": "string",
"description": "Log directory path"
},
"bin_dir": {
"type": "string",
"description": "Binary directory path"
},
"system_default_container_registry": {
"type": "string",
"description": "Default container registry"
},
"image_name_override": {
"type": "string",
"description": "Override image name"
},
"image_repo": {
"type": "string",
"description": "Container image repository"
},
"gateway_mode": {
"type": "string",
"description": "Gateway mode",
"enum": ["worker", "gateway", "disabled"]
},
"gateway_kubeconfig": {
"type": "string",
"description": "Path to kubeconfig used by gateway"
},
"gateway_concurrency": {
"type": "integer",
"minimum": 0,
"description": "Gateway concurrency limit"
},
"service_discovery_name": {
"type": "string",
"description": "Service discovery name"
},
"namespace": {
"type": "string",
"description": "Kubernetes namespace"
},
"huggingface_token": {
"type": "string",
"description": "Hugging Face access token"
},
"disable_worker_metrics": {
"type": "boolean",
"description": "Disable worker metrics collection"
},
"worker_port": {
"type": "integer",
"minimum": 1,
"maximum": 65535,
"description": "Worker service port"
},
"worker_metrics_port": {
"type": "integer",
"minimum": 1,
"maximum": 65535,
"description": "Worker metrics port"
},
"service_port_range": {
"type": "string",
"pattern": "^\\d+-\\d+$",
"description": "Service port range, e.g. 30000-31000"
},
"ray_port_range": {
"type": "string",
"pattern": "^\\d+-\\d+$",
"description": "Ray port range, e.g. 20000-20100"
},
"resources": {
"type": "object",
"description": "Additional resource definitions",
"additionalProperties": {
"type": "object"
}
},
"pipx_path": {
"type": "string",
"description": "pipx installation path"
},
"tools_download_base_url": {
"type": "string",
"description": "Base URL for downloading tools"
},
"enable_hf_transfer": {
"type": "boolean",
"description": "Enable Hugging Face transfer acceleration"
},
"enable_hf_xet": {
"type": "boolean",
"description": "Enable Hugging Face Xet support"
},
"proxy_mode": {
"type": "string",
"enum": ["worker", "system", "disabled"],
"description": "Proxy mode"
}
}
}
}
}
@@ -0,0 +1,53 @@
export default `# This is a template for worker_config.
worker_config:
debug: true
# directories
cache_dir:
log_dir:
bin_dir:
# container & image
system_default_container_registry:
image_repo: gpustack/worker
image_name_override: ""
# gateway
gateway_mode:
gateway_concurrency: 0
gateway_kubeconfig: ""
# service & networking
service_discovery_name:
namespace: default
worker_port:
worker_metrics_port:
service_port_range:
ray_port_range:
# resources
resources:
gpu:
count:
type:
cpu:
limit:
memory:
limit:
# huggingface
huggingface_token: ""
enable_hf_transfer: true
enable_hf_xet: true
# metrics
disable_worker_metrics: false
# tools & runtime
pipx_path:
tools_download_base_url:
# proxy
proxy_mode: worker
`;
@@ -0,0 +1,75 @@
import SealInput from '@/components/seal-form/seal-input';
import { PageActionType } from '@/config/types';
import YamlEditor from '@/pages/_components/yaml-editor';
import { useIntl } from '@umijs/max';
import { Form } from 'antd';
import React, { forwardRef, useImperativeHandle } from 'react';
import { ClusterFormData as FormData } from '../config/types';
import schema from '../config/worker-config.json';
import yamlTemplate from '../config/yaml-template';
const ClusterAdvanceConfig: React.FC<{ action: PageActionType; ref?: any }> =
forwardRef(({ action }, ref) => {
const [form] = Form.useForm();
const intl = useIntl();
const editorRef = React.useRef<any>(null);
const [fileContent, setFileContent] = React.useState<string>('');
useImperativeHandle(ref, () => ({
getYamlValue: () => {
return editorRef.current?.getValue();
},
setYamlValue: (values: any) => {
console.log('setYamlValue:', values, editorRef.current);
editorRef.current?.setValue(values);
}
}));
return (
<>
<Form.Item<FormData>
name="server_url"
rules={[
{
required: false,
message: ''
}
]}
>
<SealInput.Input
label={intl.formatMessage({ id: 'clusters.create.serverUrl' })}
required={false}
trim={true}
></SealInput.Input>
</Form.Item>
<Form.Item<FormData>
hidden
name="worker_config"
rules={[
{
required: false,
message: ''
}
]}
>
<SealInput.TextArea
required={false}
trim={false}
></SealInput.TextArea>
</Form.Item>
<YamlEditor
ref={editorRef}
title={`${intl.formatMessage({ id: 'clusters.create.workerConfig' })} YAML`}
value={fileContent}
height={300}
onUpload={(content) => {
setFileContent(content);
}}
schema={schema}
placeholder={yamlTemplate}
></YamlEditor>
</>
);
});
export default ClusterAdvanceConfig;