diff --git a/src/locales/en-US/gpuservice.ts b/src/locales/en-US/gpuservice.ts
index 93227b2b..e7fd2d46 100644
--- a/src/locales/en-US/gpuservice.ts
+++ b/src/locales/en-US/gpuservice.ts
@@ -122,6 +122,7 @@ export default {
'gpuservice.instance.mode.whole': 'Full GPU',
'gpuservice.instance.mode.sliced': 'By Ratio',
'gpuservice.instance.slice.memoryPercentage': 'VRAM Percentage (%)',
+ 'gpuservice.instance.slice.percentage': 'Percentage (%)',
'gpuservice.instance.slice.coresPercentage': 'Compute Percentage (%)',
'gpuservice.instance.slice.cores.min':
'The compute ratio must be no less than the VRAM ratio ({count}%)',
diff --git a/src/locales/ja-JP/gpuservice.ts b/src/locales/ja-JP/gpuservice.ts
index 22928c42..eb330dbc 100644
--- a/src/locales/ja-JP/gpuservice.ts
+++ b/src/locales/ja-JP/gpuservice.ts
@@ -121,6 +121,7 @@ export default {
'gpuservice.instance.mode.whole': 'GPU 全体',
'gpuservice.instance.mode.sliced': '比率で',
'gpuservice.instance.slice.memoryPercentage': 'VRAM の割合(%)',
+ 'gpuservice.instance.slice.percentage': '割合(%)',
'gpuservice.instance.slice.coresPercentage': '演算能力の割合(%)',
'gpuservice.instance.slice.cores.min':
'演算能力の割合は VRAM の割合({count}%)以上である必要があります',
diff --git a/src/locales/ru-RU/gpuservice.ts b/src/locales/ru-RU/gpuservice.ts
index 97f39577..2087b850 100644
--- a/src/locales/ru-RU/gpuservice.ts
+++ b/src/locales/ru-RU/gpuservice.ts
@@ -120,6 +120,7 @@ export default {
'gpuservice.instance.mode.whole': 'Весь GPU',
'gpuservice.instance.mode.sliced': 'По доле',
'gpuservice.instance.slice.memoryPercentage': 'Доля VRAM (%)',
+ 'gpuservice.instance.slice.percentage': 'Доля (%)',
'gpuservice.instance.slice.coresPercentage': 'Доля вычислений (%)',
'gpuservice.instance.slice.cores.min':
'Доля вычислений должна быть не меньше доли VRAM ({count}%)',
diff --git a/src/locales/tr-TR/gpuservice.ts b/src/locales/tr-TR/gpuservice.ts
index 31115802..62a0199e 100644
--- a/src/locales/tr-TR/gpuservice.ts
+++ b/src/locales/tr-TR/gpuservice.ts
@@ -116,6 +116,7 @@ export default {
'gpuservice.instance.mode.whole': 'Tam GPU',
'gpuservice.instance.mode.sliced': 'Orana Göre',
'gpuservice.instance.slice.memoryPercentage': 'VRAM Yüzdesi (%)',
+ 'gpuservice.instance.slice.percentage': 'Yüzde (%)',
'gpuservice.instance.slice.coresPercentage': 'İşlem Gücü Yüzdesi (%)',
'gpuservice.instance.slice.cores.min':
'İşlem gücü oranı VRAM oranından ({count}%) küçük olamaz',
diff --git a/src/locales/zh-CN/gpuservice.ts b/src/locales/zh-CN/gpuservice.ts
index 631a7ca9..1de8ff2c 100644
--- a/src/locales/zh-CN/gpuservice.ts
+++ b/src/locales/zh-CN/gpuservice.ts
@@ -111,6 +111,7 @@ export default {
'gpuservice.instance.mode.whole': '整卡',
'gpuservice.instance.mode.sliced': '按比例',
'gpuservice.instance.slice.memoryPercentage': '显存占比(%)',
+ 'gpuservice.instance.slice.percentage': '占比(%)',
'gpuservice.instance.slice.coresPercentage': '算力占比(%)',
'gpuservice.instance.slice.cores.min': '算力占比需不小于显存占比 {count}%',
'gpuservice.instance.slice.fullCores': '100% 算力',
diff --git a/src/pages/gpu-service/instance-types/apis/index.ts b/src/pages/gpu-service/instance-types/apis/index.ts
index 10b413ef..06261724 100644
--- a/src/pages/gpu-service/instance-types/apis/index.ts
+++ b/src/pages/gpu-service/instance-types/apis/index.ts
@@ -53,7 +53,7 @@ export async function deleteGPUInstanceType(params: {
});
}
-// PUT /gpu-instance-types/{name}/enactive?cluster_id — activate an instance type.
+// PUT /gpu-instance-types/{name}/activate?cluster_id — activate an instance type.
export async function activateGPUInstanceType(params: {
name: string;
cluster_id: number;
@@ -64,7 +64,7 @@ export async function activateGPUInstanceType(params: {
});
}
-// PUT /gpu-instance-types/{name}/deactive?cluster_id — deactivate an instance type.
+// PUT /gpu-instance-types/{name}/deactivate?cluster_id — deactivate an instance type.
export async function deactivateGPUInstanceType(params: {
name: string;
cluster_id: number;
diff --git a/src/pages/gpu-service/instance-types/components/flavor-display.tsx b/src/pages/gpu-service/instance-types/components/flavor-display.tsx
index 0b27c814..3b15096c 100644
--- a/src/pages/gpu-service/instance-types/components/flavor-display.tsx
+++ b/src/pages/gpu-service/instance-types/components/flavor-display.tsx
@@ -5,9 +5,10 @@ import { formatMemoryDisplay } from '../../instances/config';
import { manufactureColorMap } from '../../templates/config';
import { formatManufacturer } from '../../utils';
-// The subset of a flavor / instance-type spec the flavor display reads. Both
-// FlavorItem.spec and InstanceTypeSpec structurally satisfy it, so the create
-// drawer's dropdown and the management list share the same renderers.
+// The subset of a flavor / instance-type display shape the flavor renderers
+// read. Flavor specs satisfy it directly (minus sliceable, which the API
+// removed from flavors); the management list builds it from spec.acceleratable
+// + status.detail, deriving sliceable from slicedDetail.
interface FlavorSpecLike {
manufacturer?: string | null;
product?: string | null;
@@ -58,17 +59,6 @@ export const FlavorMeta: React.FC<{ spec?: FlavorSpecLike }> = ({
if (memory) {
pieces.push({memory});
}
- if (spec.acceleratable && spec.sliceable) {
- pieces.push(
-
- {intl.formatMessage({ id: 'gpuservice.instance.sliceable' })}
-
- );
- }
if (!pieces.length) return null;
return (
diff --git a/src/pages/gpu-service/instance-types/components/flavor-list.tsx b/src/pages/gpu-service/instance-types/components/flavor-list.tsx
index 80be522e..6e90bdc1 100644
--- a/src/pages/gpu-service/instance-types/components/flavor-list.tsx
+++ b/src/pages/gpu-service/instance-types/components/flavor-list.tsx
@@ -94,8 +94,10 @@ const FlavorList: React.FC = ({
)}
- {/* Memory / sliceable only apply to accelerator (GPU) flavors;
- a non-acceleratable (generic) flavor has neither. */}
+ {/* Memory only applies to accelerator (GPU) flavors; a
+ non-acceleratable (generic) flavor has none. (Sliceable is no
+ longer a flavor field — it is observed per instance type on
+ status.detail.slicedDetail.) */}
{spec.acceleratable && (
= ({
})}
value={formatMemoryDisplay(spec.memory ?? undefined) ?? '-'}
/>
-
)}
diff --git a/src/pages/gpu-service/instance-types/components/instance-type-card.tsx b/src/pages/gpu-service/instance-types/components/instance-type-card.tsx
index df83ef19..f9b37e4b 100644
--- a/src/pages/gpu-service/instance-types/components/instance-type-card.tsx
+++ b/src/pages/gpu-service/instance-types/components/instance-type-card.tsx
@@ -9,7 +9,7 @@ import {
import { useIntl } from '@umijs/max';
import { Button } from 'antd';
import _ from 'lodash';
-import { formatMemoryDisplay } from '../../instances/config';
+import { formatMemoryDisplay, isSliceableDetail } from '../../instances/config';
import { manufactureColorMap } from '../../templates/config';
import { ceilMilliToCore, parseQuantityToGi } from '../../utils';
import {
@@ -31,12 +31,16 @@ const InstanceTypeCard: React.FC = ({
}) => {
const intl = useIntl();
const spec = data.spec || {};
+ // Observed hardware (manufacturer / memory / sliced capability, …) comes
+ // from status.detail and may be absent until the operator backfills status.
+ const detail = data.status?.detail || {};
const unit = spec.unitResources || {};
const phase = data.status?.phase || '';
- const manufacturer = spec.manufacturer || '';
+ const manufacturer = detail.manufacturer || '';
const manufacturerColor = manufactureColorMap[manufacturer] ?? 'purple';
+ const sliceable = isSliceableDetail(detail.slicedDetail);
- const memoryText = formatMemoryDisplay(spec.memory ?? undefined);
+ const memoryText = formatMemoryDisplay(detail.memory ?? undefined);
// Base resources, formatted into a single "·"-separated line. Falsy parts
// (e.g. a CPU-only type without VRAM) drop out rather than showing "-".
@@ -71,7 +75,7 @@ const InstanceTypeCard: React.FC = ({
- {spec.product || data.name || '-'}
+ {detail.product || data.name || '-'}
@@ -115,13 +119,13 @@ const InstanceTypeCard: React.FC = ({
}}
/>
) : null}
- {spec.clockSpeed ? {spec.clockSpeed} : null}
+ {detail.clockSpeed ? {detail.clockSpeed} : null}
- {spec.sliceable
+ {sliceable
? intl.formatMessage({ id: 'gpuservice.instance.sliceable' })
: intl.formatMessage({
id: 'gpuservice.instanceType.notSliceable'
diff --git a/src/pages/gpu-service/instance-types/config/types.ts b/src/pages/gpu-service/instance-types/config/types.ts
index 0db754b8..1d3f7578 100644
--- a/src/pages/gpu-service/instance-types/config/types.ts
+++ b/src/pages/gpu-service/instance-types/config/types.ts
@@ -1,17 +1,17 @@
+import {
+ InstanceTypeDetail,
+ InstanceTypeResource
+} from '../../instances/config/types';
+
export interface UnitResources {
cpu?: string | null;
ram?: string | null;
}
+// spec carries user-defined fields only; observed hardware (manufacturer,
+// memory, sliced capability, …) lives on status.detail.
export interface InstanceTypeSpec {
displayName?: string | null;
- manufacturer?: string | null;
- product?: string | null;
- family?: string | null;
- memory?: string | null;
- cores?: string | null;
- clockSpeed?: string | null;
- sliceable?: boolean;
os?: string | null;
arch?: string | null;
acceleratable?: boolean;
@@ -22,8 +22,15 @@ export interface InstanceTypeSpec {
}
export interface InstanceTypeStatus {
+ // Observed hardware descriptor; absent until the operator backfills status.
+ detail?: InstanceTypeDetail | null;
phase?: string | null;
phaseMessage?: string | null;
+ // Per-mode resource accounting ({onceMaxRequest, remaining, capacity}).
+ accelerator?: InstanceTypeResource | null;
+ acceleratorShared?: InstanceTypeResource | null;
+ acceleratorSliced?: InstanceTypeResource | null;
+ cpu?: InstanceTypeResource | null;
}
// Row shape for the management list (GET /gpu-instance-types).
@@ -44,7 +51,6 @@ export interface FlavorItem {
family?: string | null;
memory?: string | null;
cores?: string | null;
- sliceable?: boolean;
acceleratable?: boolean;
acceleratorGroup?: string | null;
generalGroup?: string | null;
diff --git a/src/pages/gpu-service/instance-types/hooks/use-instance-type-columns.tsx b/src/pages/gpu-service/instance-types/hooks/use-instance-type-columns.tsx
index e3fe7c0f..3b13098a 100644
--- a/src/pages/gpu-service/instance-types/hooks/use-instance-type-columns.tsx
+++ b/src/pages/gpu-service/instance-types/hooks/use-instance-type-columns.tsx
@@ -10,6 +10,7 @@ import { Space, Tooltip } from 'antd';
import type { ColumnsType } from 'antd/lib/table';
import _ from 'lodash';
import { useMemo } from 'react';
+import { isSliceableDetail } from '../../instances/config';
import { ceilMilliToCore, parseQuantityToGi } from '../../utils';
import { FlavorOption } from '../components/flavor-display';
import {
@@ -95,17 +96,28 @@ const useInstanceTypeColumns = ({
{
// Flavor cell mirrors the create drawer's dropdown: product name on
// top, manufacturer · memory · sliceable on the meta line below.
+ // Observed hardware comes from status.detail (absent until the
+ // operator backfills status); sliceable is derived from slicedDetail.
title: intl.formatMessage({ id: 'gpuservice.instanceType.flavor' }),
- dataIndex: ['spec', 'product'],
+ dataIndex: ['status', 'detail', 'product'],
key: 'product',
ellipsis: { showTitle: false },
- render: (_text: string, record: ListItem) => (
-
- )
+ render: (_text: string, record: ListItem) => {
+ const detail = record.status?.detail;
+ return (
+
+ );
+ }
},
{
title: (
diff --git a/src/pages/gpu-service/instances/components/add-modal.tsx b/src/pages/gpu-service/instances/components/add-modal.tsx
index 86942e95..ac91e9ef 100644
--- a/src/pages/gpu-service/instances/components/add-modal.tsx
+++ b/src/pages/gpu-service/instances/components/add-modal.tsx
@@ -193,10 +193,13 @@ const AddModal: React.FC = ({
: undefined;
};
- // GPU types carry their accelerator vendor; non-acceleratable (CPU) types
- // all map to the single 'cpu' bucket used to match templates.
+ // GPU types carry their accelerator vendor on status.detail (observed — may
+ // be absent until the operator backfills status); non-acceleratable (CPU)
+ // types all map to the single 'cpu' bucket used to match templates.
const manufacturerOf = (instanceType: InstanceTypeItem) =>
- instanceType.spec.acceleratable ? instanceType.spec?.manufacturer : 'cpu';
+ instanceType.spec.acceleratable
+ ? (instanceType.status?.detail?.manufacturer ?? undefined)
+ : 'cpu';
// apply the selection of instance type and template
const applySelection = (
diff --git a/src/pages/gpu-service/instances/components/instance-type-item.tsx b/src/pages/gpu-service/instances/components/instance-type-item.tsx
index 70c4ded4..779670eb 100644
--- a/src/pages/gpu-service/instances/components/instance-type-item.tsx
+++ b/src/pages/gpu-service/instances/components/instance-type-item.tsx
@@ -7,7 +7,11 @@ import styled from 'styled-components';
import { manufactureColorMap } from '../../templates/config';
import { formatManufacturer } from '../../utils';
import { formatMemoryDisplay } from '../config';
-import { InstanceTypeItem as InstanceTypeItemModel } from '../config/types';
+import {
+ InstanceTypeItem as InstanceTypeItemModel,
+ InstanceTypeSnapshotSpec
+} from '../config/types';
+import { buildInstanceTypeSnapshotSpec } from '../utils/instance-description';
const Title = styled.div`
display: flex;
@@ -58,7 +62,10 @@ interface InstanceTypeItemProps {
}
interface MetadataSectionProps {
- spec: InstanceTypeItemModel['spec'];
+ // The flat snapshot / display model — built from a live item with
+ // buildInstanceTypeSnapshotSpec, or parsed back from a persisted
+ // `description` snapshot (readonly edit card).
+ spec: InstanceTypeSnapshotSpec;
// status.onceMaxRequest.acceleratorSliced (max sliceable percentage). Shown
// next to Max for sliceable types.
slicedMaxPercentage?: number;
@@ -104,8 +111,14 @@ const CPUManufacturerTag: React.FC<{ manufacturer?: string }> = ({
);
};
-function getInstanceDerived(item: InstanceTypeItemModel) {
- const spec = item.spec || {};
+// Derives the display fields from the flat snapshot spec (the UI document
+// format — built from a live item with buildInstanceTypeSnapshotSpec, or
+// parsed back from a persisted `description` snapshot). Observed hardware
+// (manufacturer / product / memory / cpu) originates from status.detail.
+function getInstanceDerived(
+ spec: InstanceTypeSnapshotSpec = {},
+ fallbackName?: string
+) {
const acceleratable = spec.acceleratable;
const cpuManufacturer = acceleratable
@@ -116,7 +129,9 @@ function getInstanceDerived(item: InstanceTypeItemModel) {
acceleratable,
isGPU: acceleratable,
manufacturer: acceleratable ? spec.manufacturer || '' : 'cpu', // GPU manufacturer or 'cpu' for non-acceleratable types
- displayName: acceleratable ? spec.product || item.name : 'CPU-only',
+ displayName: acceleratable
+ ? spec.displayName || spec.product || fallbackName
+ : spec.displayName || 'CPU-only',
ramUnit: spec.unitResourcesParsed?.ram?.value,
os: _.capitalize(spec.os) || '',
arch: spec.arch,
@@ -159,9 +174,7 @@ export const InstanceMetadataSection: React.FC = ({
}) => {
const intl = useIntl();
- const { ramUnit, cpuUnitCores, isGPU, arch } = getInstanceDerived({
- spec
- } as InstanceTypeItemModel);
+ const { ramUnit, cpuUnitCores, isGPU, arch } = getInstanceDerived(spec);
// Sliceable types append a "Sliceable {n}%" cell to the second row.
const showSliceable = !!spec.sliceable && (slicedMaxPercentage ?? 0) > 0;
@@ -217,10 +230,12 @@ const InstanceTypeItem: React.FC = ({
item,
action
}) => {
- const specData = item.spec || {};
+ // Fold the live (API-shaped) item into the flat display model: definition
+ // fields from spec, observed hardware from status.detail.
+ const specData = buildInstanceTypeSnapshotSpec(item);
const { acceleratable, manufacturer, displayName, cpuManufacturer } =
- getInstanceDerived(item);
+ getInstanceDerived(specData, item.name);
const manufacturerColor = manufactureColorMap[manufacturer] ?? 'purple';
const showManufacturerTag = acceleratable && !!manufacturer;
diff --git a/src/pages/gpu-service/instances/config/index.ts b/src/pages/gpu-service/instances/config/index.ts
index e4bfdbcb..5a2f3bca 100644
--- a/src/pages/gpu-service/instances/config/index.ts
+++ b/src/pages/gpu-service/instances/config/index.ts
@@ -3,7 +3,15 @@ import { StatusType } from '@/config/types';
import { IconFont, icons } from '@gpustack/core-ui';
import _ from 'lodash';
import React from 'react';
-import { ListItem } from '../config/types';
+import { AcceleratorSlicedDetail, ListItem } from '../config/types';
+
+// Whether a type can be sliced, per the API contract (replaces the removed
+// `spec.sliceable` boolean): logical (soft) slicing reports per-card capacity
+// or physical (e.g. MIG) profiles exist. Every level of slicedDetail may be
+// absent (exclude_none responses).
+export const isSliceableDetail = (detail?: AcceleratorSlicedDetail | null) =>
+ (detail?.logical?.count ?? 0) > 0 ||
+ (detail?.physical?.profiles?.length ?? 0) > 0;
export const InstanceStatusValueMap = {
Scheduling: 'Scheduling',
@@ -251,7 +259,7 @@ const parseQuantity = (value?: string | null): number => {
// Returns the slider max for the accelerator count: the largest
// tier.onceMaxRequest.accelerator across all tiers (not from candidates).
export const getAcceleratorMax = (
- tiers?: { onceMaxRequest: { accelerator?: string } }[] | null
+ tiers?: { onceMaxRequest: { accelerator?: string | null } }[] | null
) => {
if (!tiers?.length) return 0;
return tiers.reduce((acc, tier) => {
@@ -277,7 +285,10 @@ export const pickCandidateForAccelerator = <
>(
tiers:
| {
- onceMaxRequest: { accelerator?: string; acceleratorSliced?: string };
+ onceMaxRequest: {
+ accelerator?: string | null;
+ acceleratorSliced?: string | null;
+ };
candidates?: C[] | null;
}[]
| undefined
diff --git a/src/pages/gpu-service/instances/config/mock-data.ts b/src/pages/gpu-service/instances/config/mock-data.ts
deleted file mode 100644
index 32d9c9e0..00000000
--- a/src/pages/gpu-service/instances/config/mock-data.ts
+++ /dev/null
@@ -1,247 +0,0 @@
-export default {
- items: [
- {
- name: 'gpustack--nvidia-a10g-linux-amd64',
- spec: {
- memory: '24Gi',
- cores: '10240',
- sliceable: true,
- cpu: {
- cache: {}
- },
- cache: {},
- displayName: 'NVIDIA-A10G',
- acceleratorGroup: 'nvidia-a10g',
- generalGroup: 'generic',
- acceleratable: true,
- manufacturer: 'nvidia',
- product: 'NVIDIA-A10G',
- family: 'Ampere',
- os: 'linux',
- arch: 'amd64',
- unitResources: {
- cpu: '4',
- ram: '16Gi'
- },
- localStorage: '100Gi'
- },
- status: {
- onceMaxRequest: {
- accelerator: '1',
- acceleratorShared: '10',
- acceleratorSliced: '100',
- cpu: '0'
- },
- remaining: {
- accelerator: '1',
- acceleratorShared: '10',
- acceleratorSliced: '100',
- cpu: '0'
- },
- tiers: [
- {
- onceMaxRequest: {
- accelerator: '1',
- acceleratorShared: '10',
- acceleratorSliced: '100',
- cpu: '0'
- },
- remaining: {
- accelerator: '1',
- acceleratorShared: '10',
- acceleratorSliced: '100',
- cpu: '0'
- },
- candidates: [
- {
- cluster: '1',
- name: 'gpustack--nvidia-a10g-linux-amd64',
- phase: 'Active',
- accelerator: {
- onceMaxRequest: '1',
- remaining: '1',
- capacity: '1'
- },
- acceleratorShared: {
- onceMaxRequest: '10',
- remaining: '10',
- capacity: '10'
- },
- acceleratorSliced: {
- onceMaxRequest: '100',
- remaining: '100',
- capacity: '100'
- },
- cpu: {
- onceMaxRequest: '0',
- remaining: '0',
- capacity: '0'
- }
- }
- ]
- }
- ]
- }
- },
- {
- name: 'gpustack--nvidia-tesla-t4-linux-amd64',
- spec: {
- memory: '16Gi',
- cores: '2560',
- sliceable: true,
- cpu: {
- cache: {}
- },
- cache: {},
- displayName: 'Tesla-T4',
- acceleratorGroup: 'nvidia-tesla-t4',
- generalGroup: 'generic',
- acceleratable: true,
- manufacturer: 'nvidia',
- product: 'Tesla-T4',
- family: 'Turing',
- os: 'linux',
- arch: 'amd64',
- unitResources: {
- cpu: '4',
- ram: '16Gi'
- },
- localStorage: '100Gi'
- },
- status: {
- onceMaxRequest: {
- accelerator: '0',
- acceleratorShared: '0',
- acceleratorSliced: '0',
- cpu: '0'
- },
- remaining: {
- accelerator: '0',
- acceleratorShared: '0',
- acceleratorSliced: '0',
- cpu: '0'
- },
- tiers: [
- {
- onceMaxRequest: {
- accelerator: '0',
- acceleratorShared: '0',
- acceleratorSliced: '0',
- cpu: '0'
- },
- remaining: {
- accelerator: '0',
- acceleratorShared: '0',
- acceleratorSliced: '0',
- cpu: '0'
- },
- candidates: [
- {
- cluster: '1',
- name: 'gpustack--nvidia-tesla-t4-linux-amd64',
- phase: 'Active',
- accelerator: {
- onceMaxRequest: '0',
- remaining: '0',
- capacity: '1'
- },
- acceleratorShared: {
- onceMaxRequest: '0',
- remaining: '0',
- capacity: '10'
- },
- acceleratorSliced: {
- onceMaxRequest: '0',
- remaining: '0',
- capacity: '100'
- },
- cpu: {
- onceMaxRequest: '0',
- remaining: '0',
- capacity: '0'
- }
- }
- ]
- }
- ]
- }
- },
- {
- name: 'gpustack--generic-linux-amd64',
- spec: {
- sliceable: false,
- cpu: {
- cache: {}
- },
- cache: {},
- displayName: 'CPU-only',
- generalGroup: 'generic',
- acceleratable: false,
- os: 'linux',
- arch: 'amd64',
- unitResources: {
- cpu: '1',
- ram: '2Gi'
- },
- localStorage: '100Gi'
- },
- status: {
- onceMaxRequest: {
- accelerator: '0',
- acceleratorShared: '0',
- acceleratorSliced: '0',
- cpu: '16'
- },
- remaining: {
- accelerator: '0',
- acceleratorShared: '0',
- acceleratorSliced: '0',
- cpu: '23'
- },
- tiers: [
- {
- onceMaxRequest: {
- accelerator: '0',
- acceleratorShared: '0',
- acceleratorSliced: '0',
- cpu: '16'
- },
- remaining: {
- accelerator: '0',
- acceleratorShared: '0',
- acceleratorSliced: '0',
- cpu: '23'
- },
- candidates: [
- {
- cluster: '1',
- name: 'gpustack--generic-linux-amd64',
- phase: 'Active',
- accelerator: {
- onceMaxRequest: '0',
- remaining: '0',
- capacity: '0'
- },
- acceleratorShared: {
- onceMaxRequest: '0',
- remaining: '0',
- capacity: '0'
- },
- acceleratorSliced: {
- onceMaxRequest: '0',
- remaining: '0',
- capacity: '0'
- },
- cpu: {
- onceMaxRequest: '16',
- remaining: '23',
- capacity: '24'
- }
- }
- ]
- }
- ]
- }
- }
- ]
-};
diff --git a/src/pages/gpu-service/instances/config/types.ts b/src/pages/gpu-service/instances/config/types.ts
index 7240f7e6..091090c2 100644
--- a/src/pages/gpu-service/instances/config/types.ts
+++ b/src/pages/gpu-service/instances/config/types.ts
@@ -131,77 +131,129 @@ export interface InstanceTypeResource {
export interface InstanceTypeCandidate {
cluster: string;
name: string;
- accelerator: InstanceTypeResource;
- cpu: InstanceTypeResource;
+ accelerator?: InstanceTypeResource | null;
+ cpu?: InstanceTypeResource | null;
// Shared-mode available resource (not shown in the GPU Instance form).
- acceleratorShared: InstanceTypeResource;
+ acceleratorShared?: InstanceTypeResource | null;
// Sliced-mode available resource.
- acceleratorSliced: InstanceTypeResource;
- phase: 'Active' | 'Inactive' | 'Draining';
+ acceleratorSliced?: InstanceTypeResource | null;
+ // This candidate's sliced (partitioning) capability.
+ acceleratorSlicedDetail?: AcceleratorSlicedDetail | null;
+ phase?: 'Active' | 'Inactive' | 'Draining' | null;
}
-export interface InstanceTypeTierOnceMaxRequestResource {
- accelerator?: string;
- cpu: QuanityCPU;
- ram: QuanityMemory;
- localStorage: QuanityLocalStorage;
+// Per-mode maxima as plain number strings — the shape of the aggregated
+// status.onceMaxRequest / status.remaining AND of tier onceMaxRequest /
+// remaining (they are identical in the API). accelerator counts whole cards,
+// acceleratorShared / acceleratorSliced are percentages, cpu is cores. The
+// API carries no ram / localStorage here — RAM caps derive from
+// spec.unitResources, disk from spec.localStorage.
+export interface InstanceTypeOverviewResource {
+ accelerator?: `${number}` | null;
+ acceleratorShared?: `${number}` | null;
+ acceleratorSliced?: `${number}` | null;
+ cpu?: QuanityCPU | null;
}
export interface InstanceTypeTier {
- onceMaxRequest: InstanceTypeTierOnceMaxRequestResource;
+ onceMaxRequest: InstanceTypeOverviewResource;
+ remaining?: InstanceTypeOverviewResource | null;
+ // The tier's aggregated sliced (partitioning) capability.
+ acceleratorSlicedDetail?: AcceleratorSlicedDetail | null;
candidates?: InstanceTypeCandidate[] | null;
}
-export interface InstanceTypeOnceMaxRequestResource {
- accelerator?: `${number}` | null;
- cpu: QuanityCPU;
- ram: QuanityMemory;
- localStorage: QuanityLocalStorage;
- acceleratorShared: `${number}` | null;
- acceleratorSliced: `${number}` | null;
-}
-
export interface CPUCache {
- l1i: string;
- l1d: string;
- l2: string;
- l3: string;
+ l1i?: string | null;
+ l1d?: string | null;
+ l2?: string | null;
+ l3?: string | null;
}
export interface CPUInfo {
- physicalCores: string;
- threadsPerPhysicalCore: string;
- logicalCores: string;
- stepping: string | null;
- clockSpeed: string | null;
- maxClockSpeed: string | null;
- cacheLine: string;
- cache: CPUCache;
- manufacturer: string;
- product: string;
- family: string;
+ physicalCores?: string | null;
+ threadsPerPhysicalCore?: string | null;
+ logicalCores?: string | null;
+ stepping?: string | null;
+ clockSpeed?: string | null;
+ maxClockSpeed?: string | null;
+ cacheLine?: string | null;
+ cache?: CPUCache | null;
+ manufacturer?: string | null;
+ product?: string | null;
+ family?: string | null;
}
-export interface InstanceTypeSpec {
- group: string;
- acceleratable: boolean;
- manufacturer: string;
+// Sliced (partitioning) capability descriptor. Replaces the removed
+// `spec.sliceable` boolean: a type is sliceable when logical (soft) slicing
+// reports capacity or physical (e.g. MIG) profiles exist — see
+// isSliceableDetail in ./index. Appears as status.detail.slicedDetail and as
+// tier / candidate `acceleratorSlicedDetail` in the aggregated view.
+export interface AcceleratorSlicedLogicalDetail {
+ coresPercentageOvercommit?: boolean;
+ // Max soft slices per card; 0 → soft slicing unsupported.
+ count?: number | null;
+}
+
+export interface AcceleratorSlicedPhysicalDetailProfile {
+ name?: string | null;
+ count?: number | null;
+}
+
+export interface AcceleratorSlicedPhysicalDetail {
+ profiles?: AcceleratorSlicedPhysicalDetailProfile[] | null;
+ count?: number | null;
+}
+
+export interface AcceleratorSlicedDetail {
+ logical?: AcceleratorSlicedLogicalDetail | null;
+ physical?: AcceleratorSlicedPhysicalDetail | null;
+}
+
+// status.detail — the observed hardware descriptor. The API moved these off
+// spec (spec keeps user-defined fields only). The whole object is absent until
+// the operator backfills status, and every response is exclude_none — treat
+// every key as possibly missing.
+export interface InstanceTypeDetail {
+ // Device identity.
+ manufacturer?: string | null;
product?: string | null;
- memory?: string | null;
family?: string | null;
+ // Host node CPU (flat fields, as opposed to the nested `cpu` below).
+ physicalCores?: string | null;
+ threadsPerPhysicalCore?: string | null;
+ logicalCores?: string | null;
+ stepping?: string | null;
+ clockSpeed?: string | null;
+ maxClockSpeed?: string | null;
+ cacheLine?: string | null;
+ cache?: CPUCache | null;
+ // Accelerator hardware.
+ memory?: string | null;
+ cores?: string | null;
computeCapability?: string | null;
- sliced?: string | null;
- sliceable?: boolean;
+ slicedDetail?: AcceleratorSlicedDetail | null;
+ // The accelerator's own CPU (distinct from the flat host CPU fields above).
+ cpu?: CPUInfo | null;
+}
+
+// Mirrors the API spec object exactly (user-defined fields only — observed
+// hardware lives on status.detail), plus two UI-computed enrichments filled by
+// use-query-instance-types whose names exist nowhere in the API.
+export interface InstanceTypeSpec {
+ displayName?: string | null;
+ acceleratorGroup?: string | null;
+ generalGroup?: string | null;
+ acceleratable?: boolean;
+ os?: string;
+ arch?: string;
localStorage?: QuanityLocalStorage;
- maxComputeUnitCount?: number;
unitResources?: {
cpu: QuanityCPU;
ram: QuanityMemory;
};
- os?: string;
- arch?: string;
- cpu?: CPUInfo;
- cache?: Record;
+ // ---- UI-computed (not part of the API contract) ----
+ // spec.unitResources parsed to numbers.
unitResourcesParsed?: {
cpu: {
cores?: number;
@@ -214,10 +266,31 @@ export interface InstanceTypeSpec {
num: number;
} | null;
};
+ // Max requestable unit (card / core) count, derived from status.
+ maxComputeUnitCount?: number;
+}
+
+// Flat spec snapshot persisted in a GPU instance's `description` field at
+// create time (see utils/instance-description.ts) and reused as the display
+// model of the type card / metadata section. It merges the definition spec
+// with the observed hardware from status.detail and the derived `sliceable`.
+// The flat shape is a UI document format — do NOT confuse it with the API
+// InstanceTypeSpec; it stays flat for compatibility with snapshots persisted
+// by older instances.
+export interface InstanceTypeSnapshotSpec extends InstanceTypeSpec {
+ manufacturer?: string | null;
+ product?: string | null;
+ family?: string | null;
+ memory?: string | null;
+ sliceable?: boolean;
+ // Accelerator CPU identity only (from status.detail.cpu).
+ cpu?: Pick | null;
}
export interface InstanceTypeStatus {
- onceMaxRequest: InstanceTypeOnceMaxRequestResource;
+ detail?: InstanceTypeDetail | null;
+ onceMaxRequest: InstanceTypeOverviewResource;
+ remaining?: InstanceTypeOverviewResource | null;
tiers?: InstanceTypeTier[] | null;
}
diff --git a/src/pages/gpu-service/instances/forms/index.tsx b/src/pages/gpu-service/instances/forms/index.tsx
index af99f5d0..6d2db7a9 100644
--- a/src/pages/gpu-service/instances/forms/index.tsx
+++ b/src/pages/gpu-service/instances/forms/index.tsx
@@ -35,7 +35,11 @@ import { DefaultImagePullPolicy } from '../../templates/config';
import TemplateBasicForm, {
BasicResourceMax
} from '../../templates/forms/basic';
-import { pickCandidateForAccelerator, StorageModeValueMap } from '../config';
+import {
+ isSliceableDetail,
+ pickCandidateForAccelerator,
+ StorageModeValueMap
+} from '../config';
import { FormContext } from '../config/form-context';
import { FormData, InstanceTypeItem, ListItem } from '../config/types';
import instanceStyles from '../styles/instances.module.less';
@@ -289,15 +293,15 @@ const GPUServiceInstanceForm: React.FC = forwardRef(
const wholeFactor = isGPUType ? accelerator : cpuCount;
// Sliced mode: scale a single card's unit resources by the chosen
- // percentage. Scale CPU in millicores and RAM in MiB so fractional
- // slices stay precise and k8s-valid (integers) — e.g. 10% of a 4-core /
- // 16Gi card → 400m / 1638Mi, not a rounded-up 1 core / 1Gi.
+ // percentage, submitted as whole cores / whole Gi (floored, min 1) so
+ // the payload matches what the disabled CPU / RAM inputs display —
+ // e.g. 10% of a 4-core / 16Gi card → "1" / "1Gi".
if (sliced && unitResourcesParsed) {
const cpuCores = unitResourcesParsed.cpu?.cores ?? 0;
const ramValue = unitResourcesParsed.ram?.value ?? 0;
return {
- cpu: `${Math.max(1, _.floor((cpuCores * 1000 * percentage) / 100))}m`,
- ram: `${Math.max(1, _.floor((ramValue * 1024 * percentage) / 100))}Mi`
+ cpu: `${Math.max(1, _.floor((cpuCores * percentage) / 100))}`,
+ ram: `${Math.max(1, _.floor((ramValue * percentage) / 100))}Gi`
};
}
@@ -335,24 +339,34 @@ const GPUServiceInstanceForm: React.FC = forwardRef(
form.setFieldsValue({
spec: {
resources: {
- // Floor the scaled unit resources to whole units; CPU never drops
- // below 1 core so a small slice still gets a usable vCPU.
+ // Floor the scaled unit resources to whole units, never below 1 —
+ // a small slice (e.g. 8 GB × 10%) still shows a usable 1 vCPU /
+ // 1 GB instead of 0. Display-only: the submit path recomputes
+ // both precisely in millicores / Mi.
cpu:
cpuCores != null && percentage > 0
? Math.max(1, _.floor((cpuCores * percentage) / 100))
: null,
ram:
ramValue != null && percentage > 0
- ? _.floor((ramValue * percentage) / 100)
+ ? Math.max(1, _.floor((ramValue * percentage) / 100))
: null
}
}
} as any);
};
+ // Whether the selected type allows the compute (cores) ratio to exceed
+ // the memory ratio. Without overcommit there is no cores selector and the
+ // cores ratio is locked to (mirrors) the memory ratio.
+ const coresOvercommit =
+ !!selectedInstanceType?.status?.detail?.slicedDetail?.logical
+ ?.coresPercentageOvercommit;
+
// Single entry point for the sliced memory ratio: write the ratio and
- // rescale CPU / RAM off it. The compute (cores) ratio must stay >= memory,
- // so bump it up when memory overtakes it. Reused by the slider onChange.
+ // rescale CPU / RAM off it. With cores overcommit the compute ratio must
+ // stay >= memory (bump it up when memory overtakes it); without it the
+ // compute ratio always mirrors memory. Reused by the slider onChange.
const applySliceMemoryPercentage = (value: number) => {
const currentCores = _.toNumber(
form.getFieldValue([
@@ -361,7 +375,9 @@ const GPUServiceInstanceForm: React.FC = forwardRef(
'acceleratorSlicedCoresPercentage'
])
);
- const coresPercentage = currentCores >= value ? currentCores : value;
+ const coresPercentage = coresOvercommit
+ ? Math.max(currentCores, value)
+ : value;
form.setFieldsValue({
spec: {
resources: {
@@ -422,12 +438,14 @@ const GPUServiceInstanceForm: React.FC = forwardRef(
console.log('picked candidate', candidate, instanceType, count);
+ // The API carries no RAM max on onceMaxRequest — derive it from the
+ // per-unit RAM × the max requestable unit count (RAM always scales with
+ // the unit count). Disk max comes from spec.localStorage (UI-only cap).
+ const unitRamGi = instanceType.spec?.unitResourcesParsed?.ram?.value;
+ const maxUnits = instanceType.spec?.maxComputeUnitCount || 0;
setOnceMaxRequest({
cpu: ceilMilliToCore(candidate?.cpu?.onceMaxRequest)?.cores,
- // candidate no longer carries ram/localStorage: memory max comes from
- // the type-level onceMaxRequest.ram (already parsed to a Gi number by
- // the query hook), disk max from spec.localStorage (UI-only cap).
- memory: _.toNumber(instanceType.status?.onceMaxRequest?.ram) || null,
+ memory: unitRamGi && maxUnits ? unitRamGi * maxUnits : null,
localStorage:
parseQuantityToGi(instanceType.spec?.localStorage)?.value ?? null
});
@@ -513,7 +531,9 @@ const GPUServiceInstanceForm: React.FC = forwardRef(
const slicedMax =
_.toNumber(instanceType.status?.onceMaxRequest?.acceleratorSliced) || 0;
const defaultSliced =
- !!instanceType.spec?.sliceable && wholeMax < 1 && slicedMax > 0;
+ isSliceableDetail(instanceType.status?.detail?.slicedDetail) &&
+ wholeMax < 1 &&
+ slicedMax > 0;
if (defaultSliced) {
setSliceMode('sliced');
diff --git a/src/pages/gpu-service/instances/forms/instance-type.tsx b/src/pages/gpu-service/instances/forms/instance-type.tsx
index 1522b5dd..b06ee7f2 100644
--- a/src/pages/gpu-service/instances/forms/instance-type.tsx
+++ b/src/pages/gpu-service/instances/forms/instance-type.tsx
@@ -1,9 +1,9 @@
import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import NumberSelection from '@/pages/_components/number-selection';
-import { InputNumber, LabelInfo } from '@gpustack/core-ui';
+import { InputNumber } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
-import { Divider, Flex, Form, Segmented } from 'antd';
+import { Flex, Form, Segmented } from 'antd';
import _ from 'lodash';
import { useContext, useMemo } from 'react';
import styled from 'styled-components';
@@ -12,6 +12,7 @@ import { parseJsonSafe } from '../../utils';
import InstanceTypeItem, {
InstanceMetadataSection
} from '../components/instance-type-item';
+import { isSliceableDetail } from '../config';
import { FormContext } from '../config/form-context';
import {
FormData,
@@ -60,6 +61,21 @@ const InstanceTypePicker: React.FC = ({
// Fixed 10-tick percentage scale (10..100) for the sliced (percentage) mode.
const SLICE_PERCENT_TICKS = [10, 20, 30, 50];
+// The paired VRAM + Compute selectors (cores overcommit) are grouped in a
+// bordered card; a lone "Percentage" selector (no overcommit) renders bare so
+// it matches the whole-card GPU Count block's styling.
+const SliceFieldWrapper: React.FC<{
+ withCard: boolean;
+ children: React.ReactNode;
+}> = ({ withCard, children }) =>
+ withCard ? (
+
+ {children}
+
+ ) : (
+ <>{children}>
+ );
+
interface InstanceTypeFormItemProps {
action: PageActionType;
disabled?: boolean;
@@ -115,13 +131,6 @@ const InstanceTypeFormItem: React.FC = ({
return selectedInstanceType?.spec?.maxComputeUnitCount || 0;
}, [readonlyType, currentData, selectedInstanceType]);
- const isGPU = useMemo(() => {
- if (readonlyType) {
- return _.toNumber(currentData?.spec?.resources?.accelerator) > 0;
- }
- return selectedInstanceType?.spec?.acceleratable;
- }, [selectedInstanceType, readonlyType, currentData]);
-
const handleOnGPUCountChange = (value: number) => {
onGPUCountChange?.(value);
};
@@ -130,7 +139,9 @@ const InstanceTypeFormItem: React.FC = ({
// the section is editable (create / recreate, or edit after re-picking a
// type; a not-yet-re-typed edit renders a readonly card).
const showModeSwitch =
- !readonlyType && isGPUType && !!selectedInstanceType?.spec?.sliceable;
+ !readonlyType &&
+ isGPUType &&
+ isSliceableDetail(selectedInstanceType?.status?.detail?.slicedDetail);
const handleModeChange = (value: string) => {
onSliceModeChange?.(value as 'whole' | 'sliced');
@@ -164,10 +175,17 @@ const InstanceTypeFormItem: React.FC = ({
selectedInstanceType?.status?.onceMaxRequest?.acceleratorSliced
) || 0;
+ // Whether the compute (cores) ratio may exceed the memory ratio. When the
+ // type doesn't support overcommit, cores are locked to the memory ratio —
+ // no cores selector, and the memory selector reads as a plain "Percentage".
+ const coresOvercommit =
+ !!selectedInstanceType?.status?.detail?.slicedDetail?.logical
+ ?.coresPercentageOvercommit;
+
const modeSegmented = showModeSwitch ? (
= ({
// When the max ratio is below 10%, switch the ticks to a finer 1..10 scale
// so small slices are still selectable; otherwise use the 10..100 scale.
- const sliceTicks =
+ const sliceTicks: number[] =
slicedMaxPercentage < 10
? [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
: SLICE_PERCENT_TICKS;
@@ -246,8 +264,8 @@ const InstanceTypeFormItem: React.FC = ({
}}
>
{description.acceleratable
- ? `${description.product} x ${currentData?.spec?.resources?.accelerator}`
- : 'CPU'}
+ ? `${description.displayName || description.product} x ${currentData?.spec?.resources?.accelerator}`
+ : description.displayName || 'CPU'}
@@ -292,9 +310,7 @@ const InstanceTypeFormItem: React.FC = ({
{showModeSwitch && (
-
-
{modeSegmented}
-
+
{modeSegmented}
)}
{!noAvailableTypes && (
@@ -358,10 +374,13 @@ const InstanceTypeFormItem: React.FC = ({
)}
{!noAvailableTypes && isSliced && (
-
-
+
+ <>
name={['spec', 'resources', 'acceleratorSlicedMemoryPercentage']}
+ // Grouped with the compute selector inside one card — tighten
+ // the default 24px gap between the pair.
+ style={coresOvercommit ? { marginBottom: 0 } : undefined}
getValueProps={(value) => ({
value: value != null ? _.toNumber(value) : undefined
})}
@@ -405,68 +424,87 @@ const InstanceTypeFormItem: React.FC = ({
alwaysShowInput
required
disabled={disabled}
- style={{ border: 'none' }}
+ // Inside the card the selector drops its own border; the bare
+ // (no-overcommit) variant keeps it, like the GPU Count block.
+ style={coresOvercommit ? { border: 'none' } : undefined}
onChange={handleMemoryPercentageChange}
label={intl.formatMessage({
- id: 'gpuservice.instance.slice.memoryPercentage'
+ // Without cores overcommit this single ratio drives both
+ // VRAM and compute, so drop the "VRAM" qualifier.
+ id: coresOvercommit
+ ? 'gpuservice.instance.slice.memoryPercentage'
+ : 'gpuservice.instance.slice.percentage'
})}
/>
{/* Compute (cores) percentage. Fixed 10..100 ticks; ticks below the
- chosen memory ratio are disabled (cores must be >= memory). */}
-
- name={['spec', 'resources', 'acceleratorSlicedCoresPercentage']}
- style={{ marginBottom: 0 }}
- getValueProps={(value) => ({
- value: value != null ? _.toNumber(value) : undefined
- })}
- rules={[
- {
- required: true,
- validator: (_, value) => {
- const num = Number(value);
- if (value == null || value === '' || Number.isNaN(num)) {
- return Promise.reject(
- new Error(
- intl.formatMessage({
- id: 'gpuservice.instance.slice.percentage.required'
- })
- )
- );
- }
- if (num < slicedMemoryPercentage || num > 100) {
- return Promise.reject(
- new Error(
- intl.formatMessage(
- { id: 'gpuservice.instance.slice.cores.min' },
- { count: slicedMemoryPercentage }
- )
- )
- );
- }
- return Promise.resolve();
- }
- }
- ]}
- >
- = memory). Only
+ types with cores overcommit get the selector — without it the
+ ratio is locked to the memory percentage (the parent mirrors it),
+ carried by a hidden field so it still rides the submit. */}
+ {coresOvercommit ? (
+
+ name={['spec', 'resources', 'acceleratorSlicedCoresPercentage']}
+ style={{ marginBottom: 0 }}
+ getValueProps={(value) => ({
+ value: value != null ? _.toNumber(value) : undefined
})}
- />
-
-
-
+ rules={[
+ {
+ required: true,
+ validator: (_, value) => {
+ const num = Number(value);
+ if (value == null || value === '' || Number.isNaN(num)) {
+ return Promise.reject(
+ new Error(
+ intl.formatMessage({
+ id: 'gpuservice.instance.slice.percentage.required'
+ })
+ )
+ );
+ }
+ if (num < slicedMemoryPercentage || num > 100) {
+ return Promise.reject(
+ new Error(
+ intl.formatMessage(
+ { id: 'gpuservice.instance.slice.cores.min' },
+ { count: slicedMemoryPercentage }
+ )
+ )
+ );
+ }
+ return Promise.resolve();
+ }
+ }
+ ]}
+ >
+
+
+ ) : (
+
+ name={['spec', 'resources', 'acceleratorSlicedCoresPercentage']}
+ style={{ marginBottom: 0 }}
+ hidden
+ >
+
+
+ )}
+ >
+
)}
{/* A not-yet-re-typed edit renders a readonly card (no sliced UI), so
register the slice percentages as hidden fields — otherwise their
diff --git a/src/pages/gpu-service/instances/services/use-query-instance-types.ts b/src/pages/gpu-service/instances/services/use-query-instance-types.ts
index 2aa6e4ca..093c8420 100644
--- a/src/pages/gpu-service/instances/services/use-query-instance-types.ts
+++ b/src/pages/gpu-service/instances/services/use-query-instance-types.ts
@@ -1,8 +1,8 @@
import { useQueryData } from '@gpustack/core-ui';
import React from 'react';
import { ceilMilliToCore, parseQuantityToGi } from '../../utils';
-import { getAcceleratorMax } from '../config';
-import mockInstanceTypes from '../config/mock-data';
+import { queryGPUServiceInstanceTypes } from '../apis';
+import { getAcceleratorMax, isSliceableDetail } from '../config';
import { InstanceTypeItem } from '../config/types';
type InstanceType = InstanceTypeItem & {
@@ -13,7 +13,7 @@ export default function useQueryInstanceTypes() {
const fetchDetail = (
params: Global.SearchParams = { page: 1, perPage: 100 },
options?: any
- ) => Promise.resolve(mockInstanceTypes); // queryGPUServiceInstanceTypes(params, options);
+ ) => queryGPUServiceInstanceTypes(params, options);
const { detailData, loading, cancelRequest, fetchData } = useQueryData<
Global.PageResponse,
@@ -40,7 +40,7 @@ export default function useQueryInstanceTypes() {
// Sliceable types stay selectable as long as either whole-card or sliced
// capacity remains; unavailable only when both status.onceMaxRequest
// .accelerator and .acceleratorSliced are 0.
- if (item.spec?.sliceable) {
+ if (isSliceableDetail(item.status?.detail?.slicedDetail)) {
const wholeMax = Number(item.status?.onceMaxRequest?.accelerator) || 0;
const slicedMax =
Number(item.status?.onceMaxRequest?.acceleratorSliced) || 0;
@@ -77,17 +77,11 @@ export default function useQueryInstanceTypes() {
},
status: {
...item.status,
+ // Normalize cpu (possibly millicores) to a whole-core count string;
+ // the other onceMaxRequest fields are plain number strings already.
onceMaxRequest: {
...rawMax,
- cpu: rawMax?.cpu
- ? `${ceilMilliToCore(rawMax.cpu)?.cores || 0}`
- : '',
- ram: rawMax?.ram
- ? `${parseQuantityToGi(rawMax.ram)?.value || 0}`
- : '',
- localStorage: rawMax?.localStorage
- ? `${parseQuantityToGi(rawMax.localStorage)?.value || 0}`
- : ''
+ cpu: rawMax?.cpu ? `${ceilMilliToCore(rawMax.cpu)?.cores || 0}` : ''
}
},
diff --git a/src/pages/gpu-service/instances/utils/instance-description.ts b/src/pages/gpu-service/instances/utils/instance-description.ts
index a66f4421..43c9bf24 100644
--- a/src/pages/gpu-service/instances/utils/instance-description.ts
+++ b/src/pages/gpu-service/instances/utils/instance-description.ts
@@ -1,5 +1,25 @@
import _ from 'lodash';
-import { InstanceTypeItem } from '../config/types';
+import { isSliceableDetail } from '../config';
+import { InstanceTypeItem, InstanceTypeSnapshotSpec } from '../config/types';
+
+// Build the flat snapshot spec from a live (API-shaped) instance type:
+// definition fields from spec, observed hardware from status.detail, plus the
+// derived `sliceable`. This flat shape is the UI document format persisted in
+// the instance's `description` (older instances already carry it flat) and
+// doubles as the display model of the type card / metadata section.
+export const buildInstanceTypeSnapshotSpec = (
+ instanceType: InstanceTypeItem
+): InstanceTypeSnapshotSpec => {
+ const detail = instanceType.status?.detail;
+ return {
+ ...instanceType.spec,
+ ..._.pick(detail, ['manufacturer', 'product', 'family', 'memory']),
+ sliceable: isSliceableDetail(detail?.slicedDetail),
+ // Accelerator CPU identity only — the full CPU descriptor is too bulky to
+ // persist and the UI only shows who made it.
+ cpu: _.pick(detail?.cpu, ['manufacturer', 'product', 'family'])
+ };
+};
// Serialize the chosen instance type into the instance's `description` field —
// a persisted spec snapshot the form reads back to render the type card and
@@ -10,9 +30,6 @@ export const saveInstanceDataInDescription = (
): string => {
return JSON.stringify({
name: instanceType.name,
- spec: {
- ..._.omit(instanceType.spec, ['cache', 'cpu']),
- cpu: _.pick(instanceType.spec?.cpu, ['manufacturer', 'product', 'family'])
- }
+ spec: buildInstanceTypeSnapshotSpec(instanceType)
});
};
diff --git a/src/pages/gpu-service/instances/utils/render-instance-type.tsx b/src/pages/gpu-service/instances/utils/render-instance-type.tsx
index eb170ce8..fa31735f 100644
--- a/src/pages/gpu-service/instances/utils/render-instance-type.tsx
+++ b/src/pages/gpu-service/instances/utils/render-instance-type.tsx
@@ -11,10 +11,10 @@
* with ``buildInstanceTypeRecordFromMiB`` and feed it here.
*/
import _ from 'lodash';
-import { parseJsonSafe, parseQuantityToGi } from '../../utils';
+import { ceilMilliToCore, parseJsonSafe, parseQuantityToGi } from '../../utils';
import InstanceTypeCell from '../components/instance-type-cell';
import { formatMemoryDisplay } from '../config';
-import { InstanceTypeSpec, ListItem } from '../config/types';
+import { InstanceTypeSnapshotSpec, ListItem } from '../config/types';
// Minimal shape of the ``useIntl()`` result we depend on — keeps this module
// free of an intl package import.
@@ -25,7 +25,7 @@ const toGB = (v?: string | number) =>
const buildResourcesData = (
instanceType: {
- spec: InstanceTypeSpec;
+ spec: InstanceTypeSnapshotSpec;
},
options: {
count: number;
@@ -54,7 +54,7 @@ const getSliceMemoryPercentage = (record: ListItem) =>
_.toNumber(record.spec?.resources?.acceleratorSlicedMemoryPercentage) || 0;
const formatResources = (
- instanceTypeSpec: { spec: InstanceTypeSpec },
+ instanceTypeSpec: { spec: InstanceTypeSnapshotSpec },
record: ListItem
) => {
const resources = buildResourcesData(instanceTypeSpec, {
@@ -77,25 +77,28 @@ const formatResources = (
const sliceMemoryPercentage = getSliceMemoryPercentage(record);
- // Sliced: CPU / RAM carry the already-scaled values on spec.resources, and
- // VRAM is the per-card memory scaled by the memory percentage (floored,
- // min 1) — not the whole card's size.
+ // Sliced: CPU / RAM carry the already-scaled values on spec.resources —
+ // whole cores / whole Gi for instances created by the current form; parse
+ // (instead of echoing the raw quantity) so legacy instances persisted as
+ // millicores / Mi (e.g. "400m" / "1638Mi") render as whole units too. VRAM
+ // is the per-card memory scaled by the memory percentage (floored, min 1) —
+ // not the whole card's size.
if (sliceMemoryPercentage > 0) {
- const vramGi = parseQuantityToGi(
- (instanceTypeSpec.spec as any)?.memory
- )?.value;
+ const vramGi = parseQuantityToGi(instanceTypeSpec.spec?.memory)?.value;
const vram =
vramGi != null
? `${Math.max(1, _.floor((vramGi * sliceMemoryPercentage) / 100))} GB`
: undefined;
+ const cpuCores = ceilMilliToCore(
+ _.toString(record.spec?.resources?.cpu) || null
+ )?.cores;
+ const ramGi = parseQuantityToGi(
+ _.toString(record.spec?.resources?.ram) || null
+ )?.value;
return {
- cpu: record.spec?.resources?.cpu
- ? `${record.spec?.resources?.cpu} vCPU`
- : '-',
- ram: record.spec?.resources?.ram
- ? toGB(record.spec?.resources?.ram)
- : '-',
+ cpu: cpuCores != null ? `${Math.max(1, cpuCores)} vCPU` : '-',
+ ram: ramGi != null ? `${Math.max(1, ramGi)} GB` : '-',
vram,
localStorage: record.spec?.resources?.localStorage
? toGB(record.spec?.resources?.localStorage)
@@ -105,7 +108,7 @@ const formatResources = (
// VRAM = per-card GPU memory (a single card's size; not aggregated across
// cards — the model's marquee spec).
- const vram = formatMemoryDisplay((instanceTypeSpec.spec as any)?.memory);
+ const vram = formatMemoryDisplay(instanceTypeSpec.spec?.memory ?? undefined);
return {
cpu: resources.cpu ? `${resources.cpu} vCPU` : '-',
@@ -142,13 +145,17 @@ export const renderInstanceType = (
const accelerator = record.spec?.resources?.accelerator;
const sliceMemoryPercentage = getSliceMemoryPercentage(record);
const isSliced = description.acceleratable && sliceMemoryPercentage > 0;
+ // Type label (primary cell label and the popover's "Type" row) prefers the
+ // user-defined displayName persisted in the description snapshot, falling
+ // back to the hardware product.
+ const typeLabel = description.displayName || description.product;
const title =
options.title ??
(description.acceleratable
? isSliced
- ? `${description.product} (${sliceMemoryPercentage}%)`
- : `${description.product} x ${accelerator}`
- : 'CPU-only');
+ ? `${typeLabel} (${sliceMemoryPercentage}%)`
+ : `${typeLabel} x ${accelerator}`
+ : description.displayName || 'CPU-only');
const volume = (record.spec as any)?.volume;
// Spec popover grouped by category (GPU / CPU / Memory / Disk), mirroring
@@ -179,7 +186,7 @@ export const renderInstanceType = (
],
[
intl.formatMessage({ id: 'gpuservice.instance.section.type' }),
- description.product
+ typeLabel
],
[
intl.formatMessage({ id: 'gpuservice.instance.memory' }),