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' }),