feat: add context length

This commit is contained in:
jialin
2026-01-06 17:21:32 +08:00
parent d8b3331648
commit 0f7a475ec9
23 changed files with 164 additions and 40 deletions
+3 -3
View File
@@ -48,7 +48,7 @@ const SealSlider: React.FC<SealSliderProps> = (props) => {
props.onChange?.(newValue);
};
const renderLabel = React.useMemo(() => {
const renderLabel = () => {
return (
<span
className="slider-label"
@@ -70,13 +70,13 @@ const SealSlider: React.FC<SealSliderProps> = (props) => {
)}
</span>
);
}, [label, labelWidth, description, value, max, min, step, defaultValue]);
};
return (
<SliderWrapper className="slider-wrapper">
<Wrapper
required={required}
status={checkStatus || status}
label={renderLabel}
label={renderLabel()}
isFocus={true}
variant="borderless"
>
+1 -1
View File
@@ -175,7 +175,7 @@ const SimpleSelect: React.FC<SelectProps & { ref?: any; showTags?: boolean }> =
return (
<TagWrapper
bordered={false}
variant="filled"
closable={props.closable}
onClose={props.onClose}
style={{
+6 -1
View File
@@ -231,7 +231,12 @@ const Wrapper: FC<WrapperProps> = ({
onClick={onClick}
>
{label && (
<Label $isFocus={isFocus} $hasPrefix={hasPrefix} onClick={onClick}>
<Label
$isFocus={isFocus}
$hasPrefix={hasPrefix}
onClick={onClick}
className="label-wrapper"
>
<LabelInfo
label={label}
required={required}
+4 -1
View File
@@ -6,6 +6,9 @@ const SliderWrapper = styled.div`
height: 100%;
justify-content: center;
}
.label-wrapper {
width: 100%;
}
.borderless {
background-color: transparent;
@@ -38,7 +41,7 @@ const SliderWrapper = styled.div`
.label-val {
position: absolute !important;
top: -14px;
right: -10px;
right: 0px;
width: 80px;
border-radius: var(--border-radius-base);
text-align: center;
+3 -4
View File
@@ -2,7 +2,6 @@ import { COLOR_PRIMARY } from './index';
export default {
'root-entry-name': 'variable',
// cssVar: true,
hashed: false,
components: {
Input: {
@@ -72,9 +71,9 @@ export default {
optionSelectedFontWeight: 400
},
Slider: {
handleSize: 10,
handleSizeHover: 10,
railSize: 5,
handleSize: 8,
handleSizeHover: 8,
railSize: 4,
handleActiveOutlineColor: '#646464', // disable default outline
handleActiveColor: '#656565', // same as the rail color
handleColor: '#474747', // same as the rail color
+3 -4
View File
@@ -2,7 +2,6 @@ import { COLOR_PRIMARY } from './index';
export default {
'root-entry-name': 'variable',
// cssVar: true,
hashed: false,
components: {
Input: {
@@ -70,9 +69,9 @@ export default {
optionSelectedFontWeight: 400
},
Slider: {
handleSize: 10,
handleSizeHover: 10,
railSize: 5,
handleSize: 8,
handleSizeHover: 8,
railSize: 4,
handleActiveOutlineColor: '#B4B4B4', // disable default outline
handleActiveColor: '#D0D0D0', // same as the rail color
handleColor: '#D0D0D0', // same as the rail color
+2 -1
View File
@@ -272,5 +272,6 @@ export default {
'models.catalog.nogpus.tips':
'No compatible GPUs are available in the selected cluster for this model.',
'models.form.modelfile.notfound': `The model file path you specified does not exist on the GPUStack server. It's recommended to place the model file at the same path on both the GPUStack server and GPUStack workers. This helps GPUStack make better decisions.`,
'models.form.readyWorkers': 'workers ready'
'models.form.readyWorkers': 'workers ready',
'models.form.maxContextLength': 'Maximum Context Length'
};
+4 -2
View File
@@ -272,7 +272,8 @@ export default {
'models.catalog.nogpus.tips':
'No compatible GPUs are available in the selected cluster for this model.',
'models.form.modelfile.notfound': `The model file path you specified does not exist on the GPUStack server. It's recommended to place the model file at the same path on both the GPUStack server and GPUStack workers. This helps GPUStack make better decisions.`,
'models.form.readyWorkers': 'workers ready'
'models.form.readyWorkers': 'workers ready',
'models.form.maxContextLength': 'Maximum Context Length'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
@@ -367,5 +368,6 @@ export default {
// 72. 'models.form.backendVersions.tips': `To use more versions, go to the {link} page and edit the backend to add versions.`,
// 73. 'models.catalog.nogpus.tips': 'No compatible GPUs are available in the selected cluster for this model.',
// 74. 'models.form.modelfile.notfound': `The model file path you specified does not exist on the GPUStack server. It's recommended to place the model file at the same path on both the GPUStack server and GPUStack workers. This helps GPUStack make better decisions.`,
// 75. 'models.form.readyWorkers': 'workers ready'
// 75. 'models.form.readyWorkers': 'workers ready',
// 76. 'models.form.maxContextLength': 'Maximum Context Length'
// ========== End of To-Do List ==========
+4 -4
View File
@@ -267,8 +267,7 @@ export default {
'models.table.genericProxy':
'Используйте следующий префикс пути и укажите имя модели либо в заголовке запроса <span class="bold-text">X-GPUStack-Model</span>, либо в поле model в теле запроса. Все запросы с этим префиксом пути будут перенаправлены в бэкенд вывода.',
'models.form.backendVersion.deprecated': 'Устаревший',
'models.accessSettings.public.desc':
'Доступно всем без аутентификации.',
'models.accessSettings.public.desc': 'Доступно всем без аутентификации.',
'models.accessSettings.authed.tips':
'Доступно всем аутентифицированным пользователям платформы.',
'models.accessSettings.allowedUsers.tips':
@@ -277,9 +276,10 @@ export default {
'models.catalog.nogpus.tips':
'В выбранном кластере нет доступных GPU, совместимых с этой моделью.',
'models.form.modelfile.notfound': `Указанный путь к файлу модели не существует на сервере GPUStack. Рекомендуется размещать файл модели по одному и тому же пути как на сервере GPUStack, так и на воркерах GPUStack. Это поможет системе принимать лучшие решения по распределению ресурсов.`,
'models.form.readyWorkers': 'воркеров готово'
'models.form.readyWorkers': 'воркеров готово',
'models.form.maxContextLength': 'Maximum Context Length'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'models.form.maxContextLength': 'Maximum Context Length'
// ========== End of To-Do List ==========
+2 -1
View File
@@ -256,5 +256,6 @@ export default {
'models.catalog.nogpus.tips': '所选集群中没有兼容该模型的 GPU。',
'models.form.modelfile.notfound':
'你指定的模型文件路径在 GPUStack 服务器上不存在。建议在 GPUStack 服务器和 GPUStack 节点上使用相同的模型文件路径,这有助于 GPUStack 做出更优的调度与决策。',
'models.form.readyWorkers': '节点就绪'
'models.form.readyWorkers': '节点就绪',
'models.form.maxContextLength': '最大上下文长度'
};
+19 -2
View File
@@ -8,6 +8,8 @@ import { useIntl } from '@umijs/max';
import { Button, Tag } from 'antd';
import _ from 'lodash';
import { useMemo } from 'react';
import semverCoerce from 'semver/functions/coerce';
import semverGt from 'semver/functions/gt';
import styled from 'styled-components';
import {
backendActions,
@@ -140,7 +142,7 @@ const BackendCard: React.FC<BackendCardProps> = ({ data, onSelect }) => {
const icon = customIcons[data.id % customIcons.length];
return (
<TagInner color={color} bordered={false}>
<TagInner color={color} variant="filled">
{icon}
</TagInner>
);
@@ -157,13 +159,28 @@ const BackendCard: React.FC<BackendCardProps> = ({ data, onSelect }) => {
e.stopPropagation();
};
const sortVersions = (v2: string, v1: string) => {
const sv1 = semverCoerce(v1);
const sv2 = semverCoerce(v2);
if (!sv1 && !sv2) return 0;
if (!sv1) return 1;
if (!sv2) return -1;
if (semverGt(sv1, sv2)) return -1;
return 1;
};
const renderTag = (item: any) => {
return (
<AutoTooltip
ghost
minWidth={20}
showTitle
title={_.join(data.framework_index_map?.[item], ', ') || false}
title={
_.join(data.framework_index_map?.[item].sort(sortVersions), ', ') ||
false
}
>
<ThemeTag
key={item}
+17
View File
@@ -454,3 +454,20 @@ export async function queryDraftModelList(params?: Global.SearchParams) {
params
});
}
export async function queryModelContextLength(params: {
model: {
source: string;
model_scope_model_id?: string;
huggingface_repo_id?: string;
local_path?: string;
};
}) {
return request<{ native: number; scaled: number }>(
`${MODELS_API}/context-length`,
{
method: 'POST',
data: params
}
);
}
@@ -575,7 +575,6 @@ const AddModal: FC<AddModalProps> = (props) => {
modelSource={props.source}
setIsGGUF={handleSetIsGGUF}
></ModelCard>
{isGGUF && (
<HFModelFile
ref={modelFileRef}
@@ -590,7 +589,6 @@ const AddModal: FC<AddModalProps> = (props) => {
</ColWrapper>
</>
)}
<FormWrapper>
<ColumnWrapper
styles={{
@@ -82,7 +82,7 @@ const IncompatiableInfo: React.FC<IncompatiableInfoProps> = (props) => {
if (isEvaluating) {
return (
<CompatibleTag color="blue" bordered={false}>
<CompatibleTag color="blue" variant="filled">
<Tooltip title={intl.formatMessage({ id: 'models.form.evaluating' })}>
<LoadingOutlined />
</Tooltip>
@@ -695,7 +695,7 @@ const InstanceItem: React.FC<InstanceItemProps> = ({
</Col>
<Col span={4}>
<span
style={{ paddingLeft: '50px', gap: 4 }}
style={{ paddingLeft: '40px', gap: 4 }}
className="flex-center"
>
<InstanceStatusTag
@@ -22,6 +22,7 @@ interface FormContextProps {
workerLabelOptions: CascaderOption[];
backendOptions: BackendOption[];
initialValues?: FormData; // for editing model
modelContextData?: Record<string, any>;
clearCacheFormValues?: () => void;
onValuesChange?: (changedValues: any, allValues: any) => void;
onBackendChange: (backend: string, option: any) => void;
+2 -1
View File
@@ -358,7 +358,8 @@ export const DO_NOT_TRIGGER_CHECK_COMPATIBILITY = [
'extended_kv_cache.enabled',
'extended_kv_cache.ram_size',
'speculative_config.enabled',
'speculative_config.draft_model'
'speculative_config.draft_model',
'max_context_len'
];
// ignore to compare old and new data when these fields change in updating model
+22 -5
View File
@@ -17,7 +17,8 @@ const AdvanceConfig = () => {
const form = Form.useFormInstance();
const EnviromentVars = Form.useWatch('env', form);
const backend = Form.useWatch('backend', form);
const { onValuesChange, backendOptions, formKey } = useFormContext();
const { onValuesChange, backendOptions, isGGUF, modelContextData } =
useFormContext();
const currentBackendOptions = useMemo(() => {
return backendOptions?.find((item) => item.value === backend);
@@ -46,6 +47,10 @@ const AdvanceConfig = () => {
onValuesChange?.({}, form.getFieldsValue());
};
const handleContextLengthChange = _.debounce((value: number) => {
onValuesChange?.({}, form.getFieldsValue());
}, 300);
return (
<>
<Form.Item<FormData>
@@ -63,10 +68,22 @@ const AdvanceConfig = () => {
options={modelCategories}
></SealSelect>
</Form.Item>
<Form.Item<FormData> name="max_context_len">
<SealSlider label="Max Context Length" inputnumber></SealSlider>
</Form.Item>
{!isGGUF && modelContextData?.native && (
<Form.Item<FormData> name="max_context_len">
<SealSlider
label={intl.formatMessage({ id: 'models.form.maxContextLength' })}
inputnumber={true}
onChange={handleContextLengthChange}
included={true}
max={modelContextData?.scaled || 163840}
tooltip={{}}
marks={{
[modelContextData?.native]: 'native',
[modelContextData?.scaled]: 'scaled'
}}
></SealSlider>
</Form.Item>
)}
<BackendParametersList></BackendParametersList>
<Form.Item<FormData> name="env">
<LabelSelector
+29 -1
View File
@@ -8,7 +8,12 @@ import { useIntl } from '@umijs/max';
import useMemoizedFn from 'ahooks/lib/useMemoizedFn';
import { Form } from 'antd';
import _ from 'lodash';
import React, { forwardRef, useImperativeHandle, useMemo } from 'react';
import React, {
forwardRef,
useEffect,
useImperativeHandle,
useMemo
} from 'react';
import styled from 'styled-components';
import {
DeployFormKeyMap,
@@ -29,6 +34,7 @@ import { generateGPUIds } from '../config/utils';
import useFieldScroll from '../hooks/use-field-scroll';
import { useGenerateGPUOptions } from '../hooks/use-form-initial-values';
import useQueryBackends from '../hooks/use-query-backends';
import { useQueryContextLength } from '../services/use-query-context-length';
import AdvanceConfig from './advance-config';
import BasicForm from './basic';
import Performance from './performance';
@@ -103,6 +109,10 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
const intl = useIntl();
const [activeKey, setActiveKey] = React.useState<string[]>([]);
const [target, setTarget] = React.useState<string>(TABKeysMap.BASIC);
const { modelContextData, fetchContextLength } = useQueryContextLength();
const localPath = Form.useWatch('local_path', form);
const modelScopeModelId = Form.useWatch('model_scope_model_id', form);
const huggingfaceRepoId = Form.useWatch('huggingface_repo_id', form);
const segmentOptions = [
{
@@ -390,6 +400,23 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
};
});
useEffect(() => {
if (isGGUF || (!localPath && !modelScopeModelId && !huggingfaceRepoId)) {
return;
}
let params = {};
if (source === modelSourceMap.local_path_value) {
params = { local_path: localPath };
} else if (source === modelSourceMap.modelscope_value) {
params = { model_scope_model_id: modelScopeModelId };
} else if (source === modelSourceMap.huggingface_value) {
params = { huggingface_repo_id: huggingfaceRepoId };
}
// TODO
// fetchContextLength({ ...params, source });
}, [isGGUF, source, localPath, modelScopeModelId, huggingfaceRepoId]);
return (
<FormContext.Provider
value={{
@@ -401,6 +428,7 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
backendOptions: backendOptions,
workerLabelOptions: workerLabelOptions,
initialValues: initialValues,
modelContextData: modelContextData,
clearCacheFormValues: clearCacheFormValues,
onValuesChange: onValuesChange,
onBackendChange: handleBackendChange
@@ -115,7 +115,7 @@ const useModelsColumns = ({
title: intl.formatMessage({ id: 'models.table.replicas.edit' })
},
render: (text: number, record: ListItem) => (
<span style={{ paddingLeft: 10, minWidth: '33px' }}>
<span style={{ minWidth: '23px' }}>
{record.ready_replicas} / {record.replicas}
</span>
)
@@ -0,0 +1,29 @@
import { useState } from 'react';
import { queryModelContextLength } from '../apis';
export const useQueryContextLength = () => {
const [modelContextData, setModelContextData] = useState<{
native: number;
scaled: number;
}>({} as any);
const fetchContextLength = async (params: {
source: string;
model_scope_model_id?: string;
huggingface_repo_id?: string;
local_path?: string;
}) => {
try {
const res = await queryModelContextLength({
model: params
});
setModelContextData(res);
} catch (error) {
setModelContextData({} as any);
}
};
return {
modelContextData,
fetchContextLength
};
};