feat: add speculative decoding

This commit is contained in:
jialin
2025-11-03 10:14:30 +08:00
parent 608b2e31c7
commit 3613fb3f69
15 changed files with 356 additions and 165 deletions
+4
View File
@@ -169,6 +169,10 @@
gap: 8px;
}
.gap-4 {
gap: 4px;
}
.gap-20 {
gap: 20px;
}
+7 -1
View File
@@ -218,5 +218,11 @@ export default {
'models.form.ramRatio.tips':
'Ratio of system RAM to GPU VRAM used for KV cache. For example, 2.0 means the cache in RAM can be twice as large as the GPU VRAM.',
'models.form.ramSize.tips': `Maximum size of the KV cache stored in system memory (GiB). If set, this value overrides "{content}".`,
'models.form.chunkSize.tips': 'Number of tokens per KV cache chunk.'
'models.form.chunkSize.tips': 'Number of tokens per KV cache chunk.',
'models.form.flavor.throughput.tips':
'optimized for high throughput under high request concurrency.',
'models.form.flavor.latency.tips':
'optimized for low latency under low request concurrency.',
'models.form.flavor.reference.tips':
'the most compatible option with full precision.'
};
+7 -1
View File
@@ -218,7 +218,13 @@ export default {
'models.form.ramRatio.tips':
'Ratio of system RAM to GPU VRAM used for KV cache. For example, 2.0 means the cache in RAM can be twice as large as the GPU VRAM.',
'models.form.ramSize.tips': `Maximum size of the KV cache stored in system memory (GiB). If set, this value overrides "{content}".`,
'models.form.chunkSize.tips': 'Number of tokens per KV cache chunk.'
'models.form.chunkSize.tips': 'Number of tokens per KV cache chunk.',
'models.form.flavor.throughput.tips':
'optimized for high throughput under high request concurrency.',
'models.form.flavor.latency.tips':
'optimized for low latency under low request concurrency.',
'models.form.flavor.reference.tips':
'the most compatible option with full precision.'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
+10
View File
@@ -8,6 +8,7 @@ import {
BackendItem,
CatalogItem,
CatalogSpec,
DraftModelItem,
EvaluateResult,
EvaluateSpec,
FormData,
@@ -27,6 +28,8 @@ export const BACKEND_LIST_API = '/inference-backends/list';
export const MY_MODELS_API = '/my-models';
export const DRAFT_MODELS_API = '/draft-models';
const setProxyUrl = (url: string) => {
return `/proxy?url=${encodeURIComponent(url)}`;
};
@@ -443,3 +446,10 @@ export async function queryMyModelDetail(id: number) {
method: 'GET'
});
}
export async function queryDraftModelList(params?: Global.SearchParams) {
return request<{ items: DraftModelItem[] }>(DRAFT_MODELS_API, {
method: 'GET',
params
});
}
+1 -23
View File
@@ -10,7 +10,7 @@ import { SearchOutlined, SyncOutlined } from '@ant-design/icons';
import { PageContainer } from '@ant-design/pro-components';
import { useIntl, useNavigate } from '@umijs/max';
import { useMemoizedFn } from 'ahooks';
import { Button, Input, Pagination, Space, message } from 'antd';
import { Button, Input, Space, message } from 'antd';
import { useAtom } from 'jotai';
import _ from 'lodash';
import React, { useCallback, useEffect, useState } from 'react';
@@ -159,17 +159,6 @@ const Catalog: React.FC = () => {
[openDeployModal]
);
const handleOnPageChange = useCallback(
(page: number, pageSize?: number) => {
setQueryParams({
...queryParams,
page,
perPage: pageSize || 10
});
},
[queryParams]
);
const handleSearch = (e: any) => {
fetchData({
...queryParams,
@@ -312,17 +301,6 @@ const Catalog: React.FC = () => {
subTitle={intl.formatMessage({ id: 'noresult.catalog.subTitle' })}
></NoResult>
</ScrollerContext.Provider>
<PageWrapper>
<Pagination
hideOnSinglePage={queryParams.perPage === 100}
align="end"
defaultCurrent={1}
total={dataSource.total}
pageSize={queryParams.perPage}
showSizeChanger
onChange={handleOnPageChange}
/>
</PageWrapper>
<DelopyBuiltInModal
open={openDeployModal.show}
action={PageAction.CREATE}
+40 -42
View File
@@ -1,16 +1,16 @@
import fallbackImg from '@/assets/images/img.png';
import AutoTooltip from '@/components/auto-tooltip';
import IconFont from '@/components/icon-font';
import TagWrapper from '@/components/tags-wrapper';
import ThemeTag from '@/components/tags-wrapper/theme-tag';
import { useIntl } from '@umijs/max';
import { Typography } from 'antd';
import classNames from 'classnames';
import _ from 'lodash';
import React, { useCallback } from 'react';
import React, { useCallback, useMemo } from 'react';
import { modelCategories } from '../config';
import { CatalogItem as CatalogItemType } from '../config/types';
import '../style/catalog-item.less';
import { categoryConfig } from './model-tag';
const COLORS = ['blue', 'purple', 'orange'];
interface CatalogItemProps {
@@ -47,11 +47,38 @@ const CatalogItem: React.FC<CatalogItemProps> = (props) => {
height: 22
}}
>
{sItem}B
{sItem.label}
</ThemeTag>
);
}, []);
const description = useMemo(() => {
return (
<Typography.Paragraph
className="desc"
ellipsis={{
rows: 2,
tooltip: (
<div
className="custome-scrollbar"
style={{
display: 'flex',
justifyContent: 'flex-start',
maxHeight: 300,
maxWidth: 300,
overflow: 'auto'
}}
>
{data.description}
</div>
)
}}
>
{data.description}
</Typography.Paragraph>
);
}, [data.description]);
return (
<div
onClick={handleOnClick}
@@ -66,32 +93,8 @@ const CatalogItem: React.FC<CatalogItemProps> = (props) => {
onError={handleOnError}
/>
</div>
<AutoTooltip ghost style={{ flex: 1 }}>
{data.name}
</AutoTooltip>
<AutoTooltip ghost>{data.name}</AutoTooltip>
</div>
<Typography.Paragraph
className="desc"
ellipsis={{
rows: 2,
tooltip: (
<div
className="custome-scrollbar"
style={{
display: 'flex',
justifyContent: 'flex-start',
maxHeight: 300,
maxWidth: 300,
overflow: 'auto'
}}
>
{data.description}
</div>
)
}}
>
{data.description}
</Typography.Paragraph>
</div>
<div className="item-footer">
<div className="update-time">
@@ -121,9 +124,10 @@ const CatalogItem: React.FC<CatalogItemProps> = (props) => {
{data.categories.map((sItem, i) => {
return (
<ThemeTag
icon={categoryConfig[sItem]?.icon}
key={sItem}
className="tag-item"
color="blue"
color={categoryConfig[sItem]?.color || 'blue'}
opacity={0.7}
>
{_.find(modelCategories, { value: sItem })?.label || sItem}
@@ -147,22 +151,16 @@ const CatalogItem: React.FC<CatalogItemProps> = (props) => {
</ThemeTag>
);
})}
{data.sizes?.length > 0 && (
<>
<span className="dot"></span>
<div className="box">
<TagWrapper
gap={8}
dataList={data.sizes}
renderTag={renderTag}
></TagWrapper>
</div>
</>
)}
<span className="dot"></span>
<ThemeTag>
{data.activated_size
? `${data.size}B-A${data.activated_size}B`
: `${data.size}B`}
</ThemeTag>
</div>
</div>
</div>
);
};
export default React.memo(CatalogItem);
export default CatalogItem;
@@ -540,8 +540,8 @@ const AddModal: React.FC<AddModalProps> = (props) => {
>
<CatalogFormContext.Provider
value={{
sizeOptions: sizeOptions,
quantizationOptions: quantizationOptions,
sizeOptions: [],
quantizationOptions: [],
onSizeChange: handleOnSizeChange,
onQuantizationChange: handleOnQuantizationChange
}}
+12
View File
@@ -42,6 +42,9 @@ export const FormContext = React.createContext<FormContextProps>(
{} as FormContextProps
);
/**
* Catalog form context
*/
export const CatalogFormContext = React.createContext<CatalogFormContextProps>(
{} as CatalogFormContextProps
);
@@ -50,6 +53,10 @@ export const FormOuterContext = React.createContext<FormOuterContextProps>(
{} as FormOuterContextProps
);
/**
* Hooks to use the form context
*/
export const useFormContext = () => {
const context = React.useContext(FormContext);
if (!context) {
@@ -68,6 +75,11 @@ export const useCatalogFormContext = () => {
return context;
};
/**
*
* Hooks to use the outer form context
*/
export const useFormOuterContext = () => {
const context = React.useContext(FormOuterContext);
if (!context) {
+22 -1
View File
@@ -82,6 +82,14 @@ export interface FormData {
ram_ratio: number;
ram_size: number;
};
speculative_config: {
enabled: boolean;
algorithm: string;
draft_model_name: string;
num_draft_tokens: number;
ngram_min_match_length: number;
ngram_max_match_length: number;
};
}
interface ComputedResourceClaim {
@@ -168,7 +176,8 @@ export interface CatalogItem {
icon: string;
categories: string[];
capabilities: string[];
sizes: number[];
size: number;
activated_size: number;
licenses: string[];
release_date: string;
}
@@ -279,3 +288,15 @@ export interface BackendItem {
is_deprecated: boolean;
}[];
}
export interface DraftModelItem {
source: string;
huggingface_repo_id: string;
huggingface_filename: string;
ollama_library_model_name: string;
model_scope_model_id: string;
model_scope_file_path: string;
local_path: string;
name: string;
algorithm: string;
}
+28 -34
View File
@@ -81,41 +81,35 @@ const AdvanceConfig = () => {
[backendOptionsMap.vllm, backendOptionsMap.ascendMindie].includes(
backend
) && (
<div style={{ paddingBottom: 22 }}>
<Form.Item<FormData>
name="distributed_inference_across_workers"
valuePropName="checked"
style={{ padding: '0 10px', marginBottom: 0 }}
noStyle
>
<CheckboxField
description={intl.formatMessage({
id: 'models.form.distribution.tips'
})}
label={intl.formatMessage({
id: 'resources.form.enableDistributedInferenceAcrossWorkers'
})}
></CheckboxField>
</Form.Item>
</div>
<Form.Item<FormData>
name="distributed_inference_across_workers"
valuePropName="checked"
style={{ marginBottom: 8 }}
>
<CheckboxField
description={intl.formatMessage({
id: 'models.form.distribution.tips'
})}
label={intl.formatMessage({
id: 'resources.form.enableDistributedInferenceAcrossWorkers'
})}
></CheckboxField>
</Form.Item>
)}
<div style={{ paddingBottom: 22 }}>
<Form.Item<FormData>
name="restart_on_error"
valuePropName="checked"
style={{ padding: '0 10px', marginBottom: 0 }}
noStyle
>
<CheckboxField
description={intl.formatMessage({
id: 'models.form.restart.onerror.tips'
})}
label={intl.formatMessage({
id: 'models.form.restart.onerror'
})}
></CheckboxField>
</Form.Item>
</div>
<Form.Item<FormData>
name="restart_on_error"
valuePropName="checked"
style={{ marginBottom: 8 }}
>
<CheckboxField
description={intl.formatMessage({
id: 'models.form.restart.onerror.tips'
})}
label={intl.formatMessage({
id: 'models.form.restart.onerror'
})}
></CheckboxField>
</Form.Item>
</>
);
};
+9
View File
@@ -366,12 +366,21 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
categories: null,
restart_on_error: true,
distributed_inference_across_workers: true,
mode: 'throughput',
extended_kv_cache: {
enabled: false,
chunk_size: null,
ram_ratio: 1.2,
ram_size: null
},
speculative_config: {
enabled: false,
algorithm: '',
draft_model_name: null,
num_draft_tokens: 3,
ngram_min_match_length: 1,
ngram_max_match_length: 10
},
...initialValues
}}
>
+24 -26
View File
@@ -49,32 +49,30 @@ const KVCacheForm = () => {
return (
<>
<div style={{ paddingBottom: 22 }}>
<Form.Item<FormData>
data-field="extended_kv_cache.enabled"
name={['extended_kv_cache', 'enabled']}
valuePropName="checked"
style={{ padding: '0 10px', marginBottom: 0 }}
extra={
!builtInBackend && (
<span
dangerouslySetInnerHTML={{
__html: intl.formatMessage({ id: 'models.form.kvCache.tips' })
}}
></span>
)
}
>
<CheckboxField
description={intl.formatMessage({
id: 'models.form.kvCache.tips2'
})}
disabled={!builtInBackend}
onChange={handleOnChange}
label={intl.formatMessage({ id: 'models.form.extendedkvcache' })}
></CheckboxField>
</Form.Item>
</div>
<Form.Item<FormData>
data-field="extended_kv_cache.enabled"
name={['extended_kv_cache', 'enabled']}
valuePropName="checked"
style={{ marginBottom: 8 }}
extra={
!builtInBackend && (
<span
dangerouslySetInnerHTML={{
__html: intl.formatMessage({ id: 'models.form.kvCache.tips' })
}}
></span>
)
}
>
<CheckboxField
description={intl.formatMessage({
id: 'models.form.kvCache.tips2'
})}
disabled={!builtInBackend}
onChange={handleOnChange}
label={intl.formatMessage({ id: 'models.form.extendedkvcache' })}
></CheckboxField>
</Form.Item>
{kvCacheEnabled && (
<>
<Form.Item<FormData> name={['extended_kv_cache', 'ram_ratio']}>
+36 -26
View File
@@ -1,41 +1,51 @@
import SealSelect from '@/components/seal-form/seal-select';
import TooltipList from '@/components/tooltip-list';
import { useIntl } from '@umijs/max';
import { Form } from 'antd';
import React from 'react';
import { deployFormKeyMap } from '../config';
import { useFormContext } from '../config/form-context';
import KVCacheForm from './kv-cache';
import SpeculativeDecode from './speculative-decode';
const flavorTipsList = [
{
title: 'Throughput',
tips: 'models.form.flavor.throughput.tips'
},
{
title: 'Latency',
tips: 'models.form.flavor.latency.tips'
},
{
title: 'Reference',
tips: 'models.form.flavor.reference.tips'
}
];
const Performance: React.FC = () => {
const intl = useIntl();
const form = Form.useFormInstance();
const { formKey } = useFormContext();
return (
<>
<div data-field="extended_kv_cache.enabled"></div>
{formKey === deployFormKeyMap.catalog && (
<Form.Item name="mode">
<SealSelect
description={<TooltipList list={flavorTipsList}></TooltipList>}
label="Mode"
options={[
{ label: 'Throughput', value: 'throughput' },
{ label: 'Latency', value: 'latency' },
{ label: 'Reference', value: 'reference' }
]}
></SealSelect>
</Form.Item>
)}
<KVCacheForm></KVCacheForm>
{/* <div style={{ paddingBottom: 22, paddingLeft: 10 }}>
<Form.Item<FormData>
name="optimize_long_prompt"
valuePropName="checked"
style={{ padding: '0 10px', marginBottom: 0 }}
noStyle
>
<CheckboxField
label={intl.formatMessage({ id: 'models.form.optimizeLongPrompt' })}
></CheckboxField>
</Form.Item>
</div>
<div style={{ paddingBottom: 22, paddingLeft: 10 }}>
<Form.Item<FormData>
name="enable_speculative_decoding"
valuePropName="checked"
style={{ padding: '0 10px', marginBottom: 0 }}
noStyle
>
<CheckboxField
label={intl.formatMessage({
id: 'models.form.enableSpeculativeDecoding'
})}
></CheckboxField>
</Form.Item>
</div> */}
<SpeculativeDecode></SpeculativeDecode>
</>
);
};
@@ -0,0 +1,150 @@
import CheckboxField from '@/components/seal-form/checkbox-field';
import SealInputNumber from '@/components/seal-form/input-number';
import SealInput from '@/components/seal-form/seal-input';
import SealSelect from '@/components/seal-form/seal-select';
import useAppUtils from '@/hooks/use-app-utils';
import { useIntl } from '@umijs/max';
import { Form } from 'antd';
import { useEffect, useState } from 'react';
import { queryDraftModelList } from '../apis';
import { FormData } from '../config/types';
const AlgorithmMap = {
Eagle3: 'eagle3',
MTP: 'mtp',
Ngram: 'ngram'
};
const SpeculativeDecode = () => {
const intl = useIntl();
const { getRuleMessage } = useAppUtils();
const form = Form.useFormInstance();
const speculativeEnabled = Form.useWatch(
['speculative_config', 'enabled'],
form
);
const algorithm = Form.useWatch(['speculative_config', 'algorithm'], form);
const [draftModelList, setDraftModelList] = useState<
Global.BaseOption<string>[]
>([]);
const fetchDraftModels = async () => {
const response = await queryDraftModelList({
page: 1,
perPage: 100
});
const options = response.items.map((item) => ({
label: item.name,
value: item.name
}));
setDraftModelList(options);
};
const handleSpeculativeEnabledChange = (e: any) => {
if (e.target.checked) {
form.setFieldValue('speculative_config', {
enabled: true,
algorithm: AlgorithmMap.Eagle3,
draft_model_name: null,
num_draft_tokens: 3,
ngram_min_match_length: 1,
ngram_max_match_length: 10
});
}
};
useEffect(() => {
if (algorithm === AlgorithmMap.Eagle3) {
fetchDraftModels();
}
}, [algorithm]);
return (
<>
<Form.Item<FormData>
name={['speculative_config', 'enabled']}
valuePropName="checked"
style={{ marginBottom: 8 }}
>
<CheckboxField
label={'Enable Speculative Decoding'}
onChange={handleSpeculativeEnabledChange}
></CheckboxField>
</Form.Item>
{speculativeEnabled && (
<>
<Form.Item<FormData>
name={['speculative_config', 'algorithm']}
rules={[
{
required: true,
message: getRuleMessage('select', 'Algorithm', false)
}
]}
>
<SealSelect
required
label={'Algorithm'}
options={[
{ label: 'Eagle3', value: AlgorithmMap.Eagle3 },
{ label: 'MTP', value: AlgorithmMap.MTP },
{ label: 'N-gram', value: AlgorithmMap.Ngram }
]}
></SealSelect>
</Form.Item>
{algorithm === AlgorithmMap.Eagle3 && (
<Form.Item<FormData>
name={['speculative_config', 'draft_model_name']}
rules={[
{
required: true,
message: getRuleMessage('select', 'Draft Model', false)
}
]}
>
<SealSelect
required
label={'Draft Model'}
options={draftModelList}
></SealSelect>
</Form.Item>
)}
<Form.Item<FormData>
name={['speculative_config', 'num_draft_tokens']}
>
<SealInputNumber
label={'Number of Draft Tokens'}
min={1}
step={1}
precision={0}
/>
</Form.Item>
{algorithm === AlgorithmMap.Ngram && (
<>
<Form.Item<FormData>
name={['speculative_config', 'ngram_min_match_length']}
>
<SealInputNumber
label={'Minimum N-gram Match Length'}
min={1}
step={1}
/>
</Form.Item>
<Form.Item<FormData>
name={['speculative_config', 'ngram_max_match_length']}
>
<SealInput.Input
label={'Maximum N-gram Match Length'}
min={2}
step={1}
/>
</Form.Item>
</>
)}
</>
)}
</>
);
};
export default SpeculativeDecode;
+4 -9
View File
@@ -1,10 +1,10 @@
.catalog-item {
height: 180px;
height: 150px;
overflow: hidden;
display: flex;
padding: 16px 20px;
flex-direction: column;
justify-content: flex-start;
justify-content: space-between;
align-items: flex-start;
border: 1px solid var(--ant-color-border);
border-radius: var(--border-radius-base);
@@ -12,15 +12,12 @@
width: 100%;
.img {
width: 32px;
display: flex;
justify-content: center;
align-items: center;
height: 100%;
img {
height: 32px;
width: 32px;
object-fit: contain;
}
}
@@ -29,7 +26,6 @@
display: flex;
flex-direction: column;
justify-content: flex-start;
flex: 1;
width: 100%;
}
@@ -47,8 +43,7 @@
display: flex;
align-items: center;
justify-content: flex-start;
gap: 12px;
margin-bottom: 12px;
gap: 16px;
.name {
flex: 1;
@@ -67,7 +62,7 @@
width: 100%;
display: flex;
flex-direction: column;
gap: 12px;
gap: 16px;
justify-content: flex-start;
align-items: flex-start;
}