Skip to content

Commit bec8ee7

Browse files
authored
Merge pull request #9641 from zexi/automated-cherry-pick-of-#9640-upstream-master
Automated cherry pick of #9640: feat(ai): LLM 套餐 GPU 支持按型号与数量配置
2 parents d50f90b + 6f67a99 commit bec8ee7

12 files changed

Lines changed: 312 additions & 68 deletions

File tree

containers/Ai/locales/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,9 @@
423423
"aice.save_as_instant_model_disabled_tip": "Select one or more models to save them to the model library.",
424424
"aice.install_instant_model": "Install Model Data",
425425
"aice.devices": "GPU Model",
426+
"aice.devices.count": "Quantity",
427+
"aice.devices.add": "Add GPU",
428+
"aice.devices.count_unit": "cards",
426429
"aice.model_name": "Model Name",
427430
"aice.llm_type": "Type",
428431
"aice.llm_type.llm": "LLM Type",

containers/Ai/locales/ja-JP.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,9 @@
422422
"aice.save_as_instant_model_disabled_tip": "1 件以上のモデルを選択すると、モデルライブラリに保存できます。",
423423
"aice.install_instant_model": "モデルデータをインストール",
424424
"aice.devices": "GPUモデル",
425+
"aice.devices.count": "数量",
426+
"aice.devices.add": "GPUを追加",
427+
"aice.devices.count_unit": "",
425428
"aice.model_name": "モデル名",
426429
"aice.llm_type": "タイプ",
427430
"aice.llm_type.llm": "LLMタイプ",

containers/Ai/locales/zh-CN.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,9 @@
429429
"aice.save_as_instant_model_disabled_tip": "选择一个或多个模型,可保存为一个模型到模型库中。",
430430
"aice.install_instant_model": "安装模型数据",
431431
"aice.devices": "GPU型号",
432+
"aice.devices.count": "数量",
433+
"aice.devices.add": "添加 GPU",
434+
"aice.devices.count_unit": "",
432435
"aice.model_name": "模型名称",
433436
"aice.model_tag": "模型标签",
434437
"aice.import": "导入",

containers/Ai/sections/LlmGpuDeviceSelect.vue

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
<!--
2-
GPU 型号多选,与推理模板 / 模型目录导入 SKU 表单中的 device 字段一致
2+
GPU 型号多选(兼容旧用法);推理模板请使用 LlmGpuDevicesEditor
33
-->
44
<template>
5-
<base-select
6-
:value="value"
7-
:options="specList"
8-
:select-props="mergedSelectProps"
5+
<llm-gpu-devices-editor
6+
:value="editorValue"
97
v-bind="$attrs"
10-
v-on="$listeners" />
8+
@change="onEditorChange" />
119
</template>
1210

1311
<script>
12+
import LlmGpuDevicesEditor from '@Ai/sections/LlmGpuDevicesEditor.vue'
13+
import { aggregateDevicesToRows, expandRowsToDevices } from '@Ai/utils/deviceFormUtils'
14+
1415
export default {
1516
name: 'LlmGpuDeviceSelect',
17+
components: {
18+
LlmGpuDevicesEditor,
19+
},
1620
inheritAttrs: false,
1721
props: {
1822
value: {
@@ -25,19 +29,18 @@ export default {
2529
},
2630
},
2731
computed: {
28-
specList () {
29-
const list = Object.values(this.$store.getters.capability?.pci_model_types || {})
30-
.filter(item => item.hypervisor === 'pod')
31-
return list.map(item => ({ key: item.model, label: item.model }))
32-
},
33-
mergedSelectProps () {
34-
return {
35-
mode: 'multiple',
36-
placeholder: this.$t('common.tips.select', [this.$t('aice.devices')]),
37-
allowClear: true,
38-
...this.selectProps,
32+
editorValue () {
33+
if (!Array.isArray(this.value) || this.value.length === 0) return []
34+
if (typeof this.value[0] === 'object' && this.value[0] != null && 'model' in this.value[0]) {
35+
return this.value
3936
}
37+
return aggregateDevicesToRows(this.value.map(model => ({ model })))
38+
},
39+
},
40+
methods: {
41+
onEditorChange (rows) {
42+
this.$emit('input', rows)
43+
this.$emit('change', rows)
4044
},
4145
},
4246
}
43-
</script>
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
<!--
2+
GPU 型号 + 数量编辑,用于推理模板创建/编辑(每行可选型号与张数,支持多行)。
3+
-->
4+
<template>
5+
<div class="llm-gpu-devices-editor">
6+
<div
7+
v-for="(row, index) in innerRows"
8+
:key="rowKeys[index]"
9+
class="llm-gpu-devices-editor__row">
10+
<base-select
11+
:value="row.model"
12+
:options="specList"
13+
:select-props="modelSelectProps"
14+
class="llm-gpu-devices-editor__model"
15+
@change="val => onModelChange(index, val)" />
16+
<a-input-number
17+
:value="row.count"
18+
:min="1"
19+
:max="maxCount"
20+
:precision="0"
21+
class="llm-gpu-devices-editor__count"
22+
@change="val => onCountChange(index, val)" />
23+
<span class="llm-gpu-devices-editor__unit">{{ $t('aice.devices.count_unit') }}</span>
24+
<a-button
25+
v-if="innerRows.length > 1"
26+
shape="circle"
27+
icon="minus"
28+
size="small"
29+
class="llm-gpu-devices-editor__remove"
30+
@click="removeRow(index)" />
31+
</div>
32+
<a-button type="link" icon="plus" class="pl-0" @click="addRow">
33+
{{ $t('aice.devices.add') }}
34+
</a-button>
35+
</div>
36+
</template>
37+
38+
<script>
39+
import { uuid } from '@/utils/utils'
40+
import { createEmptyDeviceRow, normalizeDeviceRows } from '@Ai/utils/deviceFormUtils'
41+
42+
export default {
43+
name: 'LlmGpuDevicesEditor',
44+
props: {
45+
value: {
46+
type: Array,
47+
default: () => [],
48+
},
49+
maxCount: {
50+
type: Number,
51+
default: 16,
52+
},
53+
},
54+
data () {
55+
return {
56+
rowKeys: [uuid()],
57+
}
58+
},
59+
computed: {
60+
specList () {
61+
const list = Object.values(this.$store.getters.capability?.pci_model_types || {})
62+
.filter(item => item.hypervisor === 'pod')
63+
return list.map(item => ({ key: item.model, label: item.model }))
64+
},
65+
modelSelectProps () {
66+
return {
67+
placeholder: this.$t('common.tips.select', [this.$t('aice.devices')]),
68+
allowClear: true,
69+
}
70+
},
71+
innerRows () {
72+
return normalizeDeviceRows(this.value)
73+
},
74+
},
75+
watch: {
76+
value: {
77+
immediate: true,
78+
handler (val) {
79+
const rows = normalizeDeviceRows(val)
80+
while (this.rowKeys.length < rows.length) {
81+
this.rowKeys.push(uuid())
82+
}
83+
while (this.rowKeys.length > rows.length) {
84+
this.rowKeys.pop()
85+
}
86+
},
87+
},
88+
},
89+
methods: {
90+
emitRows (rows) {
91+
const normalized = normalizeDeviceRows(rows)
92+
this.$emit('input', normalized)
93+
this.$emit('change', normalized)
94+
},
95+
onModelChange (index, model) {
96+
const rows = this.innerRows.map((row, i) => (
97+
i === index ? { ...row, model } : { ...row }
98+
))
99+
this.emitRows(rows)
100+
},
101+
onCountChange (index, count) {
102+
const rows = this.innerRows.map((row, i) => (
103+
i === index ? { ...row, count: count ?? 1 } : { ...row }
104+
))
105+
this.emitRows(rows)
106+
},
107+
addRow () {
108+
this.rowKeys.push(uuid())
109+
this.emitRows([...this.innerRows, createEmptyDeviceRow()])
110+
},
111+
removeRow (index) {
112+
if (this.innerRows.length <= 1) return
113+
this.rowKeys.splice(index, 1)
114+
const rows = this.innerRows.filter((_, i) => i !== index)
115+
this.emitRows(rows.length ? rows : [createEmptyDeviceRow()])
116+
},
117+
},
118+
}
119+
</script>
120+
121+
<style lang="less" scoped>
122+
.llm-gpu-devices-editor__row {
123+
display: flex;
124+
align-items: center;
125+
gap: 8px;
126+
margin-bottom: 8px;
127+
}
128+
.llm-gpu-devices-editor__model {
129+
flex: 1;
130+
min-width: 0;
131+
}
132+
.llm-gpu-devices-editor__count {
133+
width: 88px;
134+
}
135+
.llm-gpu-devices-editor__unit {
136+
color: rgba(0, 0, 0, 0.45);
137+
white-space: nowrap;
138+
}
139+
.llm-gpu-devices-editor__remove {
140+
flex-shrink: 0;
141+
}
142+
</style>

containers/Ai/utils/communityImages.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { getDefaultPortMappingsForType, getDefaultSkuSpecForType } from '@Ai/views/llm-sku/constants/llmTypeConfig'
2+
import { expandRowsToDevices } from '@Ai/utils/deviceFormUtils'
23

34
export const BUNDLE_IMPORT_KIND = 'bundle'
45

@@ -201,6 +202,11 @@ function resolveSkuGenerateName (record) {
201202

202203
function applySkuDevices (data, devices) {
203204
if (!Array.isArray(devices) || !devices.length) return
205+
if (devices[0] && typeof devices[0] === 'object' && devices[0] != null && 'model' in devices[0]) {
206+
const expanded = expandRowsToDevices(devices)
207+
if (expanded.length) data.devices = expanded
208+
return
209+
}
204210
data.devices = devices.map(model => ({ model }))
205211
}
206212

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/**
2+
* GPU device rows ↔ API devices[] conversion for llm_sku forms.
3+
* API: devices: [{ model }] — one entry per physical GPU card.
4+
* Form: [{ model, count }] — model with quantity.
5+
*/
6+
7+
export function aggregateDevicesToRows (devices) {
8+
if (!Array.isArray(devices) || devices.length === 0) return []
9+
const counts = new Map()
10+
const order = []
11+
devices.forEach((d) => {
12+
const model = d?.model
13+
if (!model) return
14+
if (!counts.has(model)) {
15+
order.push(model)
16+
counts.set(model, 0)
17+
}
18+
counts.set(model, counts.get(model) + 1)
19+
})
20+
return order.map(model => ({ model, count: counts.get(model) }))
21+
}
22+
23+
export function expandRowsToDevices (rows) {
24+
if (!Array.isArray(rows)) return []
25+
const out = []
26+
rows.forEach((row) => {
27+
const model = String(row?.model || '').trim()
28+
const count = Math.max(1, parseInt(row?.count, 10) || 1)
29+
if (!model) return
30+
for (let i = 0; i < count; i++) {
31+
out.push({ model })
32+
}
33+
})
34+
return out
35+
}
36+
37+
export function formatDevicesDisplay (devices) {
38+
const rows = aggregateDevicesToRows(devices)
39+
if (!rows.length) return '-'
40+
return rows.map(r => `${r.model} ×${r.count}`).join(', ')
41+
}
42+
43+
export function isValidDeviceRows (rows, { allowEmpty = false } = {}) {
44+
if (!Array.isArray(rows) || rows.length === 0) return allowEmpty
45+
const withModel = rows.filter(row => String(row?.model || '').trim())
46+
if (withModel.length === 0) return allowEmpty
47+
return withModel.every(row => Number(row.count) >= 1)
48+
}
49+
50+
export function deviceRowsHaveContent (rows) {
51+
return Array.isArray(rows) && rows.some(row => String(row?.model || '').trim())
52+
}
53+
54+
export function createEmptyDeviceRow () {
55+
return { model: undefined, count: 1 }
56+
}
57+
58+
export function normalizeDeviceRows (rows) {
59+
if (!Array.isArray(rows) || rows.length === 0) {
60+
return [createEmptyDeviceRow()]
61+
}
62+
return rows.map(row => ({
63+
model: row?.model,
64+
count: Math.max(1, parseInt(row?.count, 10) || 1),
65+
}))
66+
}

containers/Ai/views/llm-deployment/create/index.vue

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,6 @@
4040

4141
<a-divider orientation="left">{{ $t('aice.llm_deployment.create.deployment') }}</a-divider>
4242

43-
<a-form-item :label="$t('aice.llm_deployment.placement_strategy')">
44-
<a-radio-group v-decorator="decorators.placement_strategy">
45-
<a-radio value="spread">{{ $t('aice.llm_deployment.placement_strategy.spread') }}</a-radio>
46-
<a-radio value="binpack">{{ $t('aice.llm_deployment.placement_strategy.binpack') }}</a-radio>
47-
</a-radio-group>
48-
</a-form-item>
4943
<a-form-item :label="$t('compute.text_104')" class="mb-0">
5044
<server-network
5145
:form="form"
@@ -135,7 +129,6 @@ export default {
135129
],
136130
},
137131
],
138-
placement_strategy: ['placement_strategy', { initialValue: 'spread' }],
139132
network: {
140133
networkType: ['networkType', { initialValue: NETWORK_OPTIONS_MAP.default.key }],
141134
networkConfig: {
@@ -187,7 +180,15 @@ export default {
187180
}
188181
},
189182
skuParams () {
190-
return { scope: this.$store.getters.scope, details: true, limit: 20, filter: ['llm_type.in(vllm,ollama,sglang)'] }
183+
return {
184+
scope: this.$store.getters.scope,
185+
details: true,
186+
limit: 20,
187+
filter: [
188+
'status.equals(ready)',
189+
'llm_type.in(vllm,ollama,sglang)',
190+
],
191+
}
191192
},
192193
networkParams () {
193194
return {
@@ -280,7 +281,7 @@ export default {
280281
name: values.name,
281282
llm_sku_id: values.llm_sku_id,
282283
replicas: 1,
283-
placement_strategy: values.placement_strategy,
284+
placement_strategy: 'spread',
284285
access_policy: 'authed',
285286
auto_start: true,
286287
nets: this.genNetworks(values),

0 commit comments

Comments
 (0)