From 74eae10d203bf6bf13183835b8e1d38be5e5fda0 Mon Sep 17 00:00:00 2001 From: Gage Krumbach Date: Fri, 16 Aug 2024 13:02:19 -0500 Subject: [PATCH] Refactor accelerator profile state hooks to no longer manage its own selected split init and selected accelerator --- frontend/src/api/k8s/inferenceServices.ts | 22 +- frontend/src/api/k8s/notebooks.ts | 9 +- frontend/src/api/k8s/servingRuntimes.ts | 30 ++- frontend/src/api/k8s/utils.ts | 29 ++- .../ServingRuntimeDetails.tsx | 6 +- .../ManageServingRuntimeModal.tsx | 30 ++- .../ServingRuntimeSizeSection.tsx | 13 +- .../ServingRuntimeTemplateSection.tsx | 8 +- .../kServeModal/ManageKServeModal.tsx | 29 ++- .../projects/useServingAcceleratorProfile.ts | 3 +- .../modelServing/screens/projects/utils.ts | 35 ++- frontend/src/pages/modelServing/utils.ts | 31 ++- .../server/AcceleratorProfileSelectField.tsx | 90 ++++--- .../screens/server/NotebookServerDetails.tsx | 6 +- .../screens/server/SpawnerPage.tsx | 24 +- .../notebook/NotebookStatusToggle.tsx | 8 +- .../useNotebookAcceleratorProfile.ts | 3 +- .../screens/spawner/SpawnerFooter.tsx | 13 +- .../projects/screens/spawner/SpawnerPage.tsx | 21 +- frontend/src/pages/projects/types.ts | 4 +- frontend/src/utilities/tolerations.ts | 13 +- .../utilities/useAcceleratorProfileState.ts | 220 +++++++++--------- 22 files changed, 382 insertions(+), 265 deletions(-) diff --git a/frontend/src/api/k8s/inferenceServices.ts b/frontend/src/api/k8s/inferenceServices.ts index 0e64b7656a..3ce0662846 100644 --- a/frontend/src/api/k8s/inferenceServices.ts +++ b/frontend/src/api/k8s/inferenceServices.ts @@ -14,6 +14,7 @@ import { translateDisplayNameForK8s } from '~/concepts/k8s/utils'; import { applyK8sAPIOptions } from '~/api/apiMergeUtils'; import { AcceleratorProfileState } from '~/utilities/useAcceleratorProfileState'; import { ContainerResources } from '~/types'; +import { AcceleratorProfileSelectFieldState } from '~/pages/notebookController/screens/server/AcceleratorProfileSelectField'; import { getModelServingProjects } from './projects'; import { assemblePodSpecOptions } from './utils'; @@ -23,7 +24,8 @@ export const assembleInferenceService = ( editName?: string, isModelMesh?: boolean, inferenceService?: InferenceServiceKind, - acceleratorState?: AcceleratorProfileState, + initialAcceleratorProfile?: AcceleratorProfileState, + selectedAcceleratorProfile?: AcceleratorProfileSelectFieldState, ): InferenceServiceKind => { const { storage, @@ -143,7 +145,11 @@ export const assembleInferenceService = ( }, }; - const { tolerations, resources } = assemblePodSpecOptions(resourceSettings, acceleratorState); + const { tolerations, resources } = assemblePodSpecOptions( + resourceSettings, + initialAcceleratorProfile, + selectedAcceleratorProfile, + ); if (tolerations.length !== 0) { updateInferenceService.spec.predictor.tolerations = tolerations; @@ -224,7 +230,8 @@ export const createInferenceService = ( data: CreatingInferenceServiceObject, secretKey?: string, isModelMesh?: boolean, - acceleratorState?: AcceleratorProfileState, + initialAcceleratorProfile?: AcceleratorProfileState, + selectedAcceleratorProfile?: AcceleratorProfileSelectFieldState, dryRun = false, ): Promise => { const inferenceService = assembleInferenceService( @@ -233,7 +240,8 @@ export const createInferenceService = ( undefined, isModelMesh, undefined, - acceleratorState, + initialAcceleratorProfile, + selectedAcceleratorProfile, ); return k8sCreateResource( applyK8sAPIOptions( @@ -251,7 +259,8 @@ export const updateInferenceService = ( existingData: InferenceServiceKind, secretKey?: string, isModelMesh?: boolean, - acceleratorState?: AcceleratorProfileState, + initialAcceleratorProfile?: AcceleratorProfileState, + selectedAcceleratorProfile?: AcceleratorProfileSelectFieldState, dryRun = false, ): Promise => { const inferenceService = assembleInferenceService( @@ -260,7 +269,8 @@ export const updateInferenceService = ( existingData.metadata.name, isModelMesh, existingData, - acceleratorState, + initialAcceleratorProfile, + selectedAcceleratorProfile, ); return k8sUpdateResource( diff --git a/frontend/src/api/k8s/notebooks.ts b/frontend/src/api/k8s/notebooks.ts index 56e1d2cd73..f2b897538f 100644 --- a/frontend/src/api/k8s/notebooks.ts +++ b/frontend/src/api/k8s/notebooks.ts @@ -41,7 +41,8 @@ export const assembleNotebook = ( description, notebookSize, envFrom, - acceleratorProfile, + initialAcceleratorProfile, + selectedAcceleratorProfile, image, volumes: formVolumes, volumeMounts: formVolumeMounts, @@ -55,7 +56,8 @@ export const assembleNotebook = ( const { affinity, tolerations, resources } = assemblePodSpecOptions( notebookSize.resources, - acceleratorProfile, + initialAcceleratorProfile, + selectedAcceleratorProfile, tolerationSettings, existingTolerations, undefined, @@ -107,8 +109,7 @@ export const assembleNotebook = ( 'notebooks.opendatahub.io/last-image-selection': imageSelection, 'notebooks.opendatahub.io/inject-oauth': 'true', 'opendatahub.io/username': username, - 'opendatahub.io/accelerator-name': - acceleratorProfile.acceleratorProfile?.metadata.name || '', + 'opendatahub.io/accelerator-name': selectedAcceleratorProfile.profile?.metadata.name || '', }, name: notebookId, namespace: projectName, diff --git a/frontend/src/api/k8s/servingRuntimes.ts b/frontend/src/api/k8s/servingRuntimes.ts index e3b933dcb9..d00e5f2757 100644 --- a/frontend/src/api/k8s/servingRuntimes.ts +++ b/frontend/src/api/k8s/servingRuntimes.ts @@ -19,6 +19,7 @@ import { getModelServingRuntimeName } from '~/pages/modelServing/utils'; import { getDisplayNameFromK8sResource, translateDisplayNameForK8s } from '~/concepts/k8s/utils'; import { applyK8sAPIOptions } from '~/api/apiMergeUtils'; import { AcceleratorProfileState } from '~/utilities/useAcceleratorProfileState'; +import { AcceleratorProfileSelectFieldState } from '~/pages/notebookController/screens/server/AcceleratorProfileSelectField'; import { getModelServingProjects } from './projects'; import { assemblePodSpecOptions, getshmVolume, getshmVolumeMount } from './utils'; @@ -28,7 +29,8 @@ export const assembleServingRuntime = ( servingRuntime: ServingRuntimeKind, isCustomServingRuntimesEnabled: boolean, isEditing?: boolean, - acceleratorProfileState?: AcceleratorProfileState, + initialAcceleratorProfile?: AcceleratorProfileState, + selectedAcceleratorProfile?: AcceleratorProfileSelectFieldState, isModelMesh?: boolean, ): ServingRuntimeKind => { const { name: displayName, numReplicas, modelSize, externalRoute, tokenAuth } = data; @@ -71,7 +73,7 @@ export const assembleServingRuntime = ( ...(isCustomServingRuntimesEnabled && { 'opendatahub.io/template-display-name': getDisplayNameFromK8sResource(servingRuntime), 'opendatahub.io/accelerator-name': - acceleratorProfileState?.acceleratorProfile?.metadata.name || '', + selectedAcceleratorProfile?.profile?.metadata.name || '', }), }, }; @@ -80,8 +82,7 @@ export const assembleServingRuntime = ( ...updatedServingRuntime.metadata, annotations: { ...annotations, - 'opendatahub.io/accelerator-name': - acceleratorProfileState?.acceleratorProfile?.metadata.name || '', + 'opendatahub.io/accelerator-name': selectedAcceleratorProfile?.profile?.metadata.name || '', ...(isCustomServingRuntimesEnabled && { 'openshift.io/display-name': displayName.trim() }), }, }; @@ -107,7 +108,8 @@ export const assembleServingRuntime = ( const { affinity, tolerations, resources } = assemblePodSpecOptions( resourceSettings, - acceleratorProfileState, + initialAcceleratorProfile, + selectedAcceleratorProfile, undefined, servingRuntime.spec.tolerations, undefined, @@ -210,7 +212,8 @@ export const updateServingRuntime = (options: { existingData: ServingRuntimeKind; isCustomServingRuntimesEnabled: boolean; opts?: K8sAPIOptions; - acceleratorProfileState?: AcceleratorProfileState; + initialAcceleratorProfile?: AcceleratorProfileState; + selectedAcceleratorProfile?: AcceleratorProfileSelectFieldState; isModelMesh?: boolean; }): Promise => { const { @@ -218,7 +221,8 @@ export const updateServingRuntime = (options: { existingData, isCustomServingRuntimesEnabled, opts, - acceleratorProfileState, + initialAcceleratorProfile, + selectedAcceleratorProfile, isModelMesh, } = options; @@ -228,7 +232,8 @@ export const updateServingRuntime = (options: { existingData, isCustomServingRuntimesEnabled, true, - acceleratorProfileState, + initialAcceleratorProfile, + selectedAcceleratorProfile, isModelMesh, ); @@ -249,7 +254,8 @@ export const createServingRuntime = (options: { servingRuntime: ServingRuntimeKind; isCustomServingRuntimesEnabled: boolean; opts?: K8sAPIOptions; - acceleratorProfileState?: AcceleratorProfileState; + initialAcceleratorProfile?: AcceleratorProfileState; + selectedAcceleratorProfile?: AcceleratorProfileSelectFieldState; isModelMesh?: boolean; }): Promise => { const { @@ -258,7 +264,8 @@ export const createServingRuntime = (options: { servingRuntime, isCustomServingRuntimesEnabled, opts, - acceleratorProfileState, + initialAcceleratorProfile, + selectedAcceleratorProfile, isModelMesh, } = options; const assembledServingRuntime = assembleServingRuntime( @@ -267,7 +274,8 @@ export const createServingRuntime = (options: { servingRuntime, isCustomServingRuntimesEnabled, false, - acceleratorProfileState, + initialAcceleratorProfile, + selectedAcceleratorProfile, isModelMesh, ); diff --git a/frontend/src/api/k8s/utils.ts b/frontend/src/api/k8s/utils.ts index ddf008bba8..4cf2ea1ae0 100644 --- a/frontend/src/api/k8s/utils.ts +++ b/frontend/src/api/k8s/utils.ts @@ -1,3 +1,4 @@ +import { AcceleratorProfileSelectFieldState } from '~/pages/notebookController/screens/server/AcceleratorProfileSelectField'; import { PodAffinity, ContainerResources, @@ -11,7 +12,8 @@ import { AcceleratorProfileState } from '~/utilities/useAcceleratorProfileState' export const assemblePodSpecOptions = ( resourceSettings: ContainerResources, - acceleratorProfileState?: AcceleratorProfileState, + initialAcceleratorProfile?: AcceleratorProfileState, + selectedAcceleratorProfile?: AcceleratorProfileSelectFieldState, tolerationSettings?: TolerationSettings, existingTolerations?: Toleration[], affinitySettings?: PodAffinity, @@ -27,39 +29,36 @@ export const assemblePodSpecOptions = ( requests: { ...existingResources?.requests, ...resourceSettings.requests }, }; - if ( - acceleratorProfileState?.additionalOptions?.useExisting && - !acceleratorProfileState.useExisting - ) { + if (selectedAcceleratorProfile?.useExistingSettings) { resources = structuredClone(resourceSettings); } // Clear the last accelerator from the resources - if (acceleratorProfileState?.initialAcceleratorProfile) { + if (initialAcceleratorProfile?.acceleratorProfile) { if (resources.limits) { - delete resources.limits[acceleratorProfileState.initialAcceleratorProfile.spec.identifier]; + delete resources.limits[initialAcceleratorProfile.acceleratorProfile.spec.identifier]; } if (resources.requests) { - delete resources.requests[acceleratorProfileState.initialAcceleratorProfile.spec.identifier]; + delete resources.requests[initialAcceleratorProfile.acceleratorProfile.spec.identifier]; } } // Add back the new accelerator to the resources if count > 0 - if (acceleratorProfileState?.acceleratorProfile && acceleratorProfileState.count > 0) { + if (selectedAcceleratorProfile?.profile && selectedAcceleratorProfile.count > 0) { if (resources.limits) { - resources.limits[acceleratorProfileState.acceleratorProfile.spec.identifier] = - acceleratorProfileState.count; + resources.limits[selectedAcceleratorProfile.profile.spec.identifier] = + selectedAcceleratorProfile.count; } if (resources.requests) { - resources.requests[acceleratorProfileState.acceleratorProfile.spec.identifier] = - acceleratorProfileState.count; + resources.requests[selectedAcceleratorProfile.profile.spec.identifier] = + selectedAcceleratorProfile.count; } } const tolerations = determineTolerations( tolerationSettings, - acceleratorProfileState, - existingTolerations, + initialAcceleratorProfile, + selectedAcceleratorProfile, ); return { affinity, tolerations, resources }; }; diff --git a/frontend/src/pages/modelServing/screens/projects/ModelMeshSection/ServingRuntimeDetails.tsx b/frontend/src/pages/modelServing/screens/projects/ModelMeshSection/ServingRuntimeDetails.tsx index fef1d92170..b56c1cff90 100644 --- a/frontend/src/pages/modelServing/screens/projects/ModelMeshSection/ServingRuntimeDetails.tsx +++ b/frontend/src/pages/modelServing/screens/projects/ModelMeshSection/ServingRuntimeDetails.tsx @@ -20,7 +20,7 @@ type ServingRuntimeDetailsProps = { const ServingRuntimeDetails: React.FC = ({ obj, isvc }) => { const { dashboardConfig } = React.useContext(AppContext); - const [acceleratorProfile] = useServingAcceleratorProfile(obj, isvc); + const acceleratorProfile = useServingAcceleratorProfile(obj, isvc); const selectedAcceleratorProfile = acceleratorProfile.acceleratorProfile; const enabledAcceleratorProfiles = acceleratorProfile.acceleratorProfiles.filter( (ac) => ac.spec.enabled, @@ -64,12 +64,12 @@ const ServingRuntimeDetails: React.FC = ({ obj, isvc }` : enabledAcceleratorProfiles.length === 0 ? 'No accelerator enabled' - : acceleratorProfile.useExisting + : acceleratorProfile.unknownProfileDetected ? 'Unknown' : 'No accelerator selected'} - {!acceleratorProfile.useExisting && acceleratorProfile.acceleratorProfile && ( + {!acceleratorProfile.unknownProfileDetected && acceleratorProfile.acceleratorProfile && ( Number of accelerators {acceleratorProfile.count} diff --git a/frontend/src/pages/modelServing/screens/projects/ServingRuntimeModal/ManageServingRuntimeModal.tsx b/frontend/src/pages/modelServing/screens/projects/ServingRuntimeModal/ManageServingRuntimeModal.tsx index ee0a74c171..b031249ef0 100644 --- a/frontend/src/pages/modelServing/screens/projects/ServingRuntimeModal/ManageServingRuntimeModal.tsx +++ b/frontend/src/pages/modelServing/screens/projects/ServingRuntimeModal/ManageServingRuntimeModal.tsx @@ -18,6 +18,8 @@ import DashboardModalFooter from '~/concepts/dashboard/DashboardModalFooter'; import { NamespaceApplicationCase } from '~/pages/projects/types'; import { ServingRuntimeEditInfo } from '~/pages/modelServing/screens/types'; import { useAccessReview } from '~/api'; +import { AcceleratorProfileSelectFieldState } from '~/pages/notebookController/screens/server/AcceleratorProfileSelectField'; +import useGenericObjectState from '~/utilities/useGenericObjectState'; import ServingRuntimeReplicaSection from './ServingRuntimeReplicaSection'; import ServingRuntimeSizeSection from './ServingRuntimeSizeSection'; import ServingRuntimeTemplateSection from './ServingRuntimeTemplateSection'; @@ -49,8 +51,13 @@ const ManageServingRuntimeModal: React.FC = ({ editInfo, }) => { const [createData, setCreateData, resetData, sizes] = useCreateServingRuntimeObject(editInfo); - const [acceleratorProfileState, setAcceleratorProfileState, resetAcceleratorProfileData] = - useServingAcceleratorProfile(editInfo?.servingRuntime); + const initialAcceleratorProfile = useServingAcceleratorProfile(editInfo?.servingRuntime); + const [selectedAcceleratorProfile, setSelectedAcceleratorProfile] = + useGenericObjectState({ + profile: undefined, + count: 0, + useExistingSettings: false, + }); const [actionInProgress, setActionInProgress] = React.useState(false); const [error, setError] = React.useState(); @@ -78,7 +85,13 @@ const ManageServingRuntimeModal: React.FC = ({ actionInProgress || tokenErrors || !inputValueValid || - !isModelServerEditInfoChanged(createData, sizes, acceleratorProfileState, editInfo); + !isModelServerEditInfoChanged( + createData, + sizes, + initialAcceleratorProfile, + selectedAcceleratorProfile, + editInfo, + ); const servingRuntimeSelected = React.useMemo( () => @@ -92,7 +105,6 @@ const ManageServingRuntimeModal: React.FC = ({ setError(undefined); setActionInProgress(false); resetData(); - resetAcceleratorProfileData(); }; const setErrorModal = (e: Error) => { @@ -115,7 +127,8 @@ const ManageServingRuntimeModal: React.FC = ({ namespace, editInfo, allowCreate, - acceleratorProfileState, + initialAcceleratorProfile, + selectedAcceleratorProfile, NamespaceApplicationCase.MODEL_MESH_PROMOTION, currentProject, undefined, @@ -162,7 +175,7 @@ const ManageServingRuntimeModal: React.FC = ({ setData={setCreateData} templates={servingRuntimeTemplates || []} isEditing={!!editInfo} - acceleratorProfileState={acceleratorProfileState} + selectedAcceleratorProfile={selectedAcceleratorProfile} /> @@ -179,8 +192,9 @@ const ManageServingRuntimeModal: React.FC = ({ setData={setCreateData} sizes={sizes} servingRuntimeSelected={servingRuntimeSelected} - acceleratorProfileState={acceleratorProfileState} - setAcceleratorProfileState={setAcceleratorProfileState} + acceleratorProfileState={initialAcceleratorProfile} + selectedAcceleratorProfile={selectedAcceleratorProfile} + setSelectedAcceleratorProfile={setSelectedAcceleratorProfile} infoContent="Select a server size that will accommodate your largest model. See the product documentation for more information." /> diff --git a/frontend/src/pages/modelServing/screens/projects/ServingRuntimeModal/ServingRuntimeSizeSection.tsx b/frontend/src/pages/modelServing/screens/projects/ServingRuntimeModal/ServingRuntimeSizeSection.tsx index d851053525..e6dc9fe2ef 100644 --- a/frontend/src/pages/modelServing/screens/projects/ServingRuntimeModal/ServingRuntimeSizeSection.tsx +++ b/frontend/src/pages/modelServing/screens/projects/ServingRuntimeModal/ServingRuntimeSizeSection.tsx @@ -9,7 +9,9 @@ import { } from '~/pages/modelServing/screens/types'; import { ServingRuntimeKind } from '~/k8sTypes'; import { isGpuDisabled } from '~/pages/modelServing/screens/projects/utils'; -import AcceleratorProfileSelectField from '~/pages/notebookController/screens/server/AcceleratorProfileSelectField'; +import AcceleratorProfileSelectField, { + AcceleratorProfileSelectFieldState, +} from '~/pages/notebookController/screens/server/AcceleratorProfileSelectField'; import { getCompatibleAcceleratorIdentifiers } from '~/pages/projects/screens/spawner/spawnerUtils'; import { AcceleratorProfileState } from '~/utilities/useAcceleratorProfileState'; import SimpleSelect from '~/components/SimpleSelect'; @@ -23,7 +25,8 @@ type ServingRuntimeSizeSectionProps = { sizes: ModelServingSize[]; servingRuntimeSelected?: ServingRuntimeKind; acceleratorProfileState: AcceleratorProfileState; - setAcceleratorProfileState: UpdateObjectAtPropAndValue; + selectedAcceleratorProfile: AcceleratorProfileSelectFieldState; + setSelectedAcceleratorProfile: UpdateObjectAtPropAndValue; infoContent?: string; }; @@ -33,7 +36,8 @@ const ServingRuntimeSizeSection: React.FC = ({ sizes, servingRuntimeSelected, acceleratorProfileState, - setAcceleratorProfileState, + selectedAcceleratorProfile, + setSelectedAcceleratorProfile, infoContent, }) => { const [supportedAcceleratorProfiles, setSupportedAcceleratorProfiles] = React.useState< @@ -111,10 +115,11 @@ const ServingRuntimeSizeSection: React.FC = ({ )} diff --git a/frontend/src/pages/modelServing/screens/projects/ServingRuntimeModal/ServingRuntimeTemplateSection.tsx b/frontend/src/pages/modelServing/screens/projects/ServingRuntimeModal/ServingRuntimeTemplateSection.tsx index 4b88b1b99a..a7ed7bed1e 100644 --- a/frontend/src/pages/modelServing/screens/projects/ServingRuntimeModal/ServingRuntimeTemplateSection.tsx +++ b/frontend/src/pages/modelServing/screens/projects/ServingRuntimeModal/ServingRuntimeTemplateSection.tsx @@ -10,14 +10,14 @@ import { } from '~/pages/modelServing/customServingRuntimes/utils'; import { isCompatibleWithAccelerator as isCompatibleWithAcceleratorProfile } from '~/pages/projects/screens/spawner/spawnerUtils'; import SimpleSelect from '~/components/SimpleSelect'; -import { AcceleratorProfileState } from '~/utilities/useAcceleratorProfileState'; +import { AcceleratorProfileSelectFieldState } from '~/pages/notebookController/screens/server/AcceleratorProfileSelectField'; type ServingRuntimeTemplateSectionProps = { data: CreatingServingRuntimeObject; setData: UpdateObjectAtPropAndValue; templates: TemplateKind[]; isEditing?: boolean; - acceleratorProfileState: AcceleratorProfileState; + selectedAcceleratorProfile: AcceleratorProfileSelectFieldState; }; const ServingRuntimeTemplateSection: React.FC = ({ @@ -25,7 +25,7 @@ const ServingRuntimeTemplateSection: React.FC { const filteredTemplates = React.useMemo( () => @@ -50,7 +50,7 @@ const ServingRuntimeTemplateSection: React.FC {isCompatibleWithAcceleratorProfile( - acceleratorProfileState.acceleratorProfile?.spec.identifier, + selectedAcceleratorProfile.profile?.spec.identifier, template.objects[0], ) && } diff --git a/frontend/src/pages/modelServing/screens/projects/kServeModal/ManageKServeModal.tsx b/frontend/src/pages/modelServing/screens/projects/kServeModal/ManageKServeModal.tsx index 60da77f14d..680a9e2963 100644 --- a/frontend/src/pages/modelServing/screens/projects/kServeModal/ManageKServeModal.tsx +++ b/frontend/src/pages/modelServing/screens/projects/kServeModal/ManageKServeModal.tsx @@ -47,6 +47,8 @@ import { useAccessReview } from '~/api'; import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; import { RegisteredModelDeployInfo } from '~/pages/modelRegistry/screens/RegisteredModels/useRegisteredModelDeployInfo'; import usePrefillDeployModalFromModelRegistry from '~/pages/modelRegistry/screens/RegisteredModels/usePrefillDeployModalFromModelRegistry'; +import { AcceleratorProfileSelectFieldState } from '~/pages/notebookController/screens/server/AcceleratorProfileSelectField'; +import useGenericObjectState from '~/utilities/useGenericObjectState'; import KServeAutoscalerReplicaSection from './KServeAutoscalerReplicaSection'; const accessReviewResource: AccessReviewResourceAttributes = { @@ -110,11 +112,19 @@ const ManageKServeModal: React.FC = ({ const isInferenceServiceNameWithinLimit = translateDisplayNameForK8s(createDataInferenceService.name).length <= 253; - const [acceleratorProfileState, setAcceleratorProfileState, resetAcceleratorProfileData] = - useServingAcceleratorProfile( - editInfo?.servingRuntimeEditInfo?.servingRuntime, - editInfo?.inferenceServiceEditInfo, - ); + const acceleratorProfileState = useServingAcceleratorProfile( + editInfo?.servingRuntimeEditInfo?.servingRuntime, + editInfo?.inferenceServiceEditInfo, + ); + const [ + selectedAcceleratorProfile, + setSelectedAcceleratorProfile, + resetSelectedAcceleratorProfile, + ] = useGenericObjectState({ + profile: undefined, + count: 0, + useExistingSettings: false, + }); const customServingRuntimesEnabled = useCustomServingRuntimesEnabled(); const [allowCreate] = useAccessReview({ ...accessReviewResource, @@ -187,7 +197,7 @@ const ManageKServeModal: React.FC = ({ setActionInProgress(false); resetDataServingRuntime(); resetDataInferenceService(); - resetAcceleratorProfileData(); + resetSelectedAcceleratorProfile(); setAlertVisible(true); }; @@ -217,6 +227,7 @@ const ManageKServeModal: React.FC = ({ editInfo?.servingRuntimeEditInfo, false, acceleratorProfileState, + selectedAcceleratorProfile, NamespaceApplicationCase.KSERVE_PROMOTION, projectContext?.currentProject, servingRuntimeName, @@ -229,6 +240,7 @@ const ManageKServeModal: React.FC = ({ servingRuntimeName, false, acceleratorProfileState, + selectedAcceleratorProfile, allowCreate, editInfo?.secrets, ); @@ -325,7 +337,7 @@ const ManageKServeModal: React.FC = ({ setData={setCreateDataServingRuntime} templates={servingRuntimeTemplates || []} isEditing={!!editInfo} - acceleratorProfileState={acceleratorProfileState} + selectedAcceleratorProfile={selectedAcceleratorProfile} /> @@ -351,7 +363,8 @@ const ManageKServeModal: React.FC = ({ sizes={sizes} servingRuntimeSelected={servingRuntimeSelected} acceleratorProfileState={acceleratorProfileState} - setAcceleratorProfileState={setAcceleratorProfileState} + selectedAcceleratorProfile={selectedAcceleratorProfile} + setSelectedAcceleratorProfile={setSelectedAcceleratorProfile} infoContent="Select a server size that will accommodate your largest model. See the product documentation for more information." /> diff --git a/frontend/src/pages/modelServing/screens/projects/useServingAcceleratorProfile.ts b/frontend/src/pages/modelServing/screens/projects/useServingAcceleratorProfile.ts index 53b52c97c4..c7adcd09e5 100644 --- a/frontend/src/pages/modelServing/screens/projects/useServingAcceleratorProfile.ts +++ b/frontend/src/pages/modelServing/screens/projects/useServingAcceleratorProfile.ts @@ -2,12 +2,11 @@ import { InferenceServiceKind, ServingRuntimeKind } from '~/k8sTypes'; import useAcceleratorProfileState, { AcceleratorProfileState, } from '~/utilities/useAcceleratorProfileState'; -import { GenericObjectState } from '~/utilities/useGenericObjectState'; const useServingAcceleratorProfile = ( servingRuntime?: ServingRuntimeKind | null, inferenceService?: InferenceServiceKind | null, -): GenericObjectState => { +): AcceleratorProfileState => { const acceleratorProfileName = servingRuntime?.metadata.annotations?.['opendatahub.io/accelerator-name']; const resources = diff --git a/frontend/src/pages/modelServing/screens/projects/utils.ts b/frontend/src/pages/modelServing/screens/projects/utils.ts index fcdcaa16d0..c7ffc18ecd 100644 --- a/frontend/src/pages/modelServing/screens/projects/utils.ts +++ b/frontend/src/pages/modelServing/screens/projects/utils.ts @@ -43,6 +43,7 @@ import { } from '~/api'; import { isDataConnectionAWS } from '~/pages/projects/screens/detail/data-connections/utils'; import { removeLeadingSlash } from '~/utilities/string'; +import { AcceleratorProfileSelectFieldState } from '~/pages/notebookController/screens/server/AcceleratorProfileSelectField'; export const getServingRuntimeSizes = (config: DashboardConfigKind): ModelServingSize[] => { let sizes = config.spec.modelServerSizes || []; @@ -313,7 +314,8 @@ const createInferenceServiceAndDataConnection = ( existingStorage: boolean, editInfo?: InferenceServiceKind, isModelMesh?: boolean, - acceleratorProfileState?: AcceleratorProfileState, + initialAcceleratorProfile?: AcceleratorProfileState, + selectedAcceleratorProfile?: AcceleratorProfileSelectFieldState, dryRun = false, ) => { if (!existingStorage) { @@ -324,14 +326,16 @@ const createInferenceServiceAndDataConnection = ( editInfo, secret.metadata.name, isModelMesh, - acceleratorProfileState, + initialAcceleratorProfile, + selectedAcceleratorProfile, dryRun, ) : createInferenceService( inferenceServiceData, secret.metadata.name, isModelMesh, - acceleratorProfileState, + initialAcceleratorProfile, + selectedAcceleratorProfile, dryRun, ), ); @@ -342,14 +346,16 @@ const createInferenceServiceAndDataConnection = ( editInfo, undefined, isModelMesh, - acceleratorProfileState, + initialAcceleratorProfile, + selectedAcceleratorProfile, dryRun, ) : createInferenceService( inferenceServiceData, undefined, isModelMesh, - acceleratorProfileState, + initialAcceleratorProfile, + selectedAcceleratorProfile, dryRun, ); }; @@ -359,7 +365,8 @@ export const getSubmitInferenceServiceResourceFn = ( editInfo?: InferenceServiceKind, servingRuntimeName?: string, isModelMesh?: boolean, - acceleratorProfileState?: AcceleratorProfileState, + initialAcceleratorProfile?: AcceleratorProfileState, + selectedAcceleratorProfile?: AcceleratorProfileSelectFieldState, allowCreate?: boolean, secrets?: SecretKind[], ): ((opts: { dryRun?: boolean }) => Promise) => { @@ -388,7 +395,8 @@ export const getSubmitInferenceServiceResourceFn = ( existingStorage, editInfo, isModelMesh, - acceleratorProfileState, + initialAcceleratorProfile, + selectedAcceleratorProfile, dryRun, ).then((inferenceService) => setUpTokenAuth( @@ -420,7 +428,8 @@ export const getSubmitServingRuntimeResourcesFn = ( namespace: string, editInfo: ServingRuntimeEditInfo | undefined, allowCreate: boolean, - acceleratorProfileState: AcceleratorProfileState, + initialAcceleratorProfile: AcceleratorProfileState, + selectedAcceleratorProfile: AcceleratorProfileSelectFieldState, servingPlatformEnablement: NamespaceApplicationCase, currentProject?: ProjectKind, name?: string, @@ -443,8 +452,8 @@ export const getSubmitServingRuntimeResourcesFn = ( const createTokenAuth = servingRuntimeData.tokenAuth && allowCreate; const controlledState = isGpuDisabled(servingRuntimeSelected) - ? { count: 0, acceleratorProfiles: [], useExisting: false } - : acceleratorProfileState; + ? { count: 0, useExistingSettings: false } + : selectedAcceleratorProfile; if (!editInfo && !currentProject) { // This should be impossible to hit on resource creation, current project is undefined only on edit @@ -471,7 +480,8 @@ export const getSubmitServingRuntimeResourcesFn = ( opts: { dryRun, }, - acceleratorProfileState: controlledState, + selectedAcceleratorProfile: controlledState, + initialAcceleratorProfile, isModelMesh, }), setUpTokenAuth( @@ -495,7 +505,8 @@ export const getSubmitServingRuntimeResourcesFn = ( opts: { dryRun, }, - acceleratorProfileState: controlledState, + selectedAcceleratorProfile: controlledState, + initialAcceleratorProfile, isModelMesh, }).then((servingRuntime) => setUpTokenAuth( diff --git a/frontend/src/pages/modelServing/utils.ts b/frontend/src/pages/modelServing/utils.ts index 08414c484e..d17b91bb88 100644 --- a/frontend/src/pages/modelServing/utils.ts +++ b/frontend/src/pages/modelServing/utils.ts @@ -37,7 +37,7 @@ import { ServingRuntimeToken, } from '~/pages/modelServing/screens/types'; import { AcceleratorProfileState } from '~/utilities/useAcceleratorProfileState'; -import { getAcceleratorProfileCount } from '~/utilities/utils'; +import { AcceleratorProfileSelectFieldState } from '~/pages/notebookController/screens/server/AcceleratorProfileSelectField'; type TokenNames = { serviceAccountName: string; @@ -243,40 +243,35 @@ export const getServingRuntimeTokens = (tokens?: SecretKind[]): ServingRuntimeTo })); const isAcceleratorProfileChanged = ( - acceleratorProfileState: AcceleratorProfileState, - servingRuntime: ServingRuntimeKind, + initialAcceleratorProfile: AcceleratorProfileState, + selectedAcceleratorProfile: AcceleratorProfileSelectFieldState, ) => { - const { acceleratorProfile } = acceleratorProfileState; - const { initialAcceleratorProfile } = acceleratorProfileState; - // both are none, check if it's using existing - if (!acceleratorProfile && !initialAcceleratorProfile) { - if (acceleratorProfileState.additionalOptions?.useExisting) { - return !acceleratorProfileState.useExisting; + if (!selectedAcceleratorProfile.profile && !initialAcceleratorProfile.acceleratorProfile) { + if (selectedAcceleratorProfile.useExistingSettings) { + return !selectedAcceleratorProfile.useExistingSettings; } return false; } // one is none, another is set, changed - if (!acceleratorProfile || !initialAcceleratorProfile) { + if (!selectedAcceleratorProfile.profile || !initialAcceleratorProfile.acceleratorProfile) { return true; } // compare the name, gpu count return ( - acceleratorProfile.metadata.name !== initialAcceleratorProfile.metadata.name || - acceleratorProfileState.count !== - getAcceleratorProfileCount( - initialAcceleratorProfile, - servingRuntime.spec.containers[0].resources || {}, - ) + selectedAcceleratorProfile.profile.metadata.name !== + initialAcceleratorProfile.acceleratorProfile.metadata.name || + selectedAcceleratorProfile.count !== initialAcceleratorProfile.count ); }; export const isModelServerEditInfoChanged = ( createData: CreatingServingRuntimeObject, sizes: ModelServingSize[], - acceleratorProfileState: AcceleratorProfileState, + initialAcceleratorProfile: AcceleratorProfileState, + selectedAcceleratorProfile: AcceleratorProfileSelectFieldState, editInfo?: ServingRuntimeEditInfo, ): boolean => editInfo?.servingRuntime @@ -287,7 +282,7 @@ export const isModelServerEditInfoChanged = ( String(createData.externalRoute) || editInfo.servingRuntime.metadata.annotations['enable-auth'] !== String(createData.tokenAuth) || - isAcceleratorProfileChanged(acceleratorProfileState, editInfo.servingRuntime) || + isAcceleratorProfileChanged(initialAcceleratorProfile, selectedAcceleratorProfile) || (createData.tokenAuth && !_.isEqual( getServingRuntimeTokens(editInfo.secrets) diff --git a/frontend/src/pages/notebookController/screens/server/AcceleratorProfileSelectField.tsx b/frontend/src/pages/notebookController/screens/server/AcceleratorProfileSelectField.tsx index 3659a9c517..d7906c19ea 100644 --- a/frontend/src/pages/notebookController/screens/server/AcceleratorProfileSelectField.tsx +++ b/frontend/src/pages/notebookController/screens/server/AcceleratorProfileSelectField.tsx @@ -17,41 +17,50 @@ import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons'; import { isHTMLInputElement } from '~/utilities/utils'; import { AcceleratorProfileKind } from '~/k8sTypes'; import SimpleSelect, { SimpleSelectOption } from '~/components/SimpleSelect'; -import { UpdateObjectAtPropAndValue } from '~/pages/projects/types'; import { AcceleratorProfileState } from '~/utilities/useAcceleratorProfileState'; +import { UpdateObjectAtPropAndValue } from '~/pages/projects/types'; import useDetectedAccelerators from './useDetectedAccelerators'; type AcceleratorProfileSelectFieldProps = { acceleratorProfileState: AcceleratorProfileState; - setAcceleratorProfileState: UpdateObjectAtPropAndValue; supportedAcceleratorProfiles?: string[]; resourceDisplayName?: string; infoContent?: string; + selectedAcceleratorProfile: AcceleratorProfileSelectFieldState; + setSelectedAcceleratorProfile: UpdateObjectAtPropAndValue; +}; + +export type AcceleratorProfileSelectFieldState = { + profile?: AcceleratorProfileKind; + count: number; + useExistingSettings: boolean; }; const AcceleratorProfileSelectField: React.FC = ({ acceleratorProfileState, - setAcceleratorProfileState, supportedAcceleratorProfiles, resourceDisplayName = 'image', infoContent, + selectedAcceleratorProfile, + setSelectedAcceleratorProfile, }) => { const [detectedAccelerators] = useDetectedAccelerators(); - const { - acceleratorProfile, - count: acceleratorCount, - acceleratorProfiles, - useExisting, - additionalOptions, - } = acceleratorProfileState; + React.useEffect(() => { + setSelectedAcceleratorProfile('profile', acceleratorProfileState.acceleratorProfile); + setSelectedAcceleratorProfile('count', acceleratorProfileState.count); + setSelectedAcceleratorProfile( + 'useExistingSettings', + acceleratorProfileState.unknownProfileDetected, + ); + }, [acceleratorProfileState, setSelectedAcceleratorProfile]); const generateAcceleratorCountWarning = (newSize: number) => { - if (!acceleratorProfile) { + if (!selectedAcceleratorProfile.profile) { return ''; } - const { identifier } = acceleratorProfile.spec; + const { identifier } = selectedAcceleratorProfile.profile.spec; const detectedAcceleratorCount = Object.entries(detectedAccelerators.available).find( ([id]) => identifier === id, @@ -69,12 +78,14 @@ const AcceleratorProfileSelectField: React.FC supportedAcceleratorProfiles?.includes(cr.spec.identifier); - const enabledAcceleratorProfiles = acceleratorProfiles.filter((ac) => ac.spec.enabled); + const enabledAcceleratorProfiles = acceleratorProfileState.acceleratorProfiles.filter( + (ac) => ac.spec.enabled, + ); const formatOption = (cr: AcceleratorProfileKind): SimpleSelectOption => { const displayName = `${cr.spec.displayName}${!cr.spec.enabled ? ' (disabled)' : ''}`; @@ -112,13 +123,13 @@ const AcceleratorProfileSelectField: React.FC formatOption(ac)); let acceleratorAlertMessage: { title: string; variant: AlertVariant } | null = null; - if (acceleratorProfile && supportedAcceleratorProfiles !== undefined) { + if (selectedAcceleratorProfile.profile && supportedAcceleratorProfiles !== undefined) { if (supportedAcceleratorProfiles.length === 0) { acceleratorAlertMessage = { title: `The ${resourceDisplayName} you have selected doesn't support the selected accelerator. It is recommended to use a compatible ${resourceDisplayName} for optimal performance.`, variant: AlertVariant.info, }; - } else if (!isAcceleratorProfileSupported(acceleratorProfile)) { + } else if (!isAcceleratorProfileSupported(selectedAcceleratorProfile.profile)) { acceleratorAlertMessage = { title: `The ${resourceDisplayName} you have selected is not compatible with the selected accelerator`, variant: AlertVariant.warning, @@ -133,18 +144,21 @@ const AcceleratorProfileSelectField: React.FC { - setAcceleratorProfileState('count', Math.max(acceleratorCount + step, 1)); + setSelectedAcceleratorProfile('count', Math.max(selectedAcceleratorProfile.count + step, 1)); }; // if there is more than a none option, show the dropdown @@ -171,25 +185,31 @@ const AcceleratorProfileSelectField: React.FC { if (isPlaceholder) { // none - setAcceleratorProfileState('useExisting', false); - setAcceleratorProfileState('acceleratorProfile', undefined); - setAcceleratorProfileState('count', 0); + setSelectedAcceleratorProfile('useExistingSettings', false); + setSelectedAcceleratorProfile('profile', undefined); + setSelectedAcceleratorProfile('count', 0); } else if (key === 'use-existing') { // use existing settings - setAcceleratorProfileState('useExisting', true); - setAcceleratorProfileState('acceleratorProfile', undefined); - setAcceleratorProfileState('count', 0); + setSelectedAcceleratorProfile('useExistingSettings', true); + setSelectedAcceleratorProfile('profile', undefined); + setSelectedAcceleratorProfile('count', 0); } else { // normal flow - setAcceleratorProfileState('count', 1); - setAcceleratorProfileState('useExisting', false); - setAcceleratorProfileState( - 'acceleratorProfile', - acceleratorProfiles.find((ac) => ac.metadata.name === key), + setSelectedAcceleratorProfile('count', 1); + setSelectedAcceleratorProfile('useExistingSettings', false); + setSelectedAcceleratorProfile( + 'profile', + acceleratorProfileState.acceleratorProfiles.find( + (ac) => ac.metadata.name === key, + ), ); } }} @@ -206,7 +226,7 @@ const AcceleratorProfileSelectField: React.FC )} - {acceleratorProfile && ( + {selectedAcceleratorProfile.profile && ( @@ -214,7 +234,7 @@ const AcceleratorProfileSelectField: React.FC onStep(1)} @@ -222,7 +242,7 @@ const AcceleratorProfileSelectField: React.FC { if (isHTMLInputElement(event.target)) { const newSize = Number(event.target.value); - setAcceleratorProfileState('count', Math.max(newSize, 1)); + setSelectedAcceleratorProfile('count', Math.max(newSize, 1)); } }} /> diff --git a/frontend/src/pages/notebookController/screens/server/NotebookServerDetails.tsx b/frontend/src/pages/notebookController/screens/server/NotebookServerDetails.tsx index 814a6215cb..4181a67cf9 100644 --- a/frontend/src/pages/notebookController/screens/server/NotebookServerDetails.tsx +++ b/frontend/src/pages/notebookController/screens/server/NotebookServerDetails.tsx @@ -28,7 +28,7 @@ const NotebookServerDetails: React.FC = () => { const { images, loaded } = useWatchImages(); const [isExpanded, setExpanded] = React.useState(false); const { dashboardConfig } = useAppContext(); - const [acceleratorProfile] = useNotebookAcceleratorProfile(notebook); + const acceleratorProfile = useNotebookAcceleratorProfile(notebook); const container: PodContainer | undefined = notebook?.spec.template.spec.containers.find( (currentContainer) => currentContainer.name === notebook.metadata.name, @@ -112,12 +112,12 @@ const NotebookServerDetails: React.FC = () => { {acceleratorProfile.acceleratorProfile ? acceleratorProfile.acceleratorProfile.spec.displayName - : acceleratorProfile.useExisting + : acceleratorProfile.unknownProfileDetected ? 'Unknown' : 'None'} - {!acceleratorProfile.useExisting && ( + {!acceleratorProfile.unknownProfileDetected && ( Number of accelerators {acceleratorProfile.count} diff --git a/frontend/src/pages/notebookController/screens/server/SpawnerPage.tsx b/frontend/src/pages/notebookController/screens/server/SpawnerPage.tsx index 975518d13e..15151e3b20 100644 --- a/frontend/src/pages/notebookController/screens/server/SpawnerPage.tsx +++ b/frontend/src/pages/notebookController/screens/server/SpawnerPage.tsx @@ -42,6 +42,7 @@ import useNotebookAcceleratorProfile from '~/pages/projects/screens/detail/noteb import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; import { fireFormTrackingEvent } from '~/concepts/analyticsTracking/segmentIOUtils'; import { TrackingOutcome } from '~/concepts/analyticsTracking/trackingProperties'; +import useGenericObjectState from '~/utilities/useGenericObjectState'; import SizeSelectField from './SizeSelectField'; import useSpawnerNotebookModalState from './useSpawnerNotebookModalState'; import BrowserTabPreferenceCheckbox from './BrowserTabPreferenceCheckbox'; @@ -49,7 +50,9 @@ import EnvironmentVariablesRow from './EnvironmentVariablesRow'; import ImageSelector from './ImageSelector'; import { usePreferredNotebookSize } from './usePreferredNotebookSize'; import StartServerModal from './StartServerModal'; -import AcceleratorProfileSelectField from './AcceleratorProfileSelectField'; +import AcceleratorProfileSelectField, { + AcceleratorProfileSelectFieldState, +} from './AcceleratorProfileSelectField'; import '~/pages/notebookController/NotebookController.scss'; @@ -72,11 +75,17 @@ const SpawnerPage: React.FC = () => { tag: undefined, }); const { selectedSize, setSelectedSize, sizes } = usePreferredNotebookSize(); - const [acceleratorProfile, setAcceleratorProfile] = - useNotebookAcceleratorProfile(currentUserNotebook); + const acceleratorProfile = useNotebookAcceleratorProfile(currentUserNotebook); const [variableRows, setVariableRows] = React.useState([]); const [submitError, setSubmitError] = React.useState(null); + const [selectedAcceleratorProfile, setSelectedAcceleratorProfile] = + useGenericObjectState({ + profile: undefined, + count: 0, + useExistingSettings: false, + }); + const disableSubmit = createInProgress || variableRows.some(({ errors }) => Object.keys(errors).length > 0); @@ -236,10 +245,12 @@ const SpawnerPage: React.FC = () => { outcome: TrackingOutcome.submit, accelerator: acceleratorProfile.acceleratorProfile ? `${acceleratorProfile.acceleratorProfile.spec.displayName} (${acceleratorProfile.acceleratorProfile.metadata.name}): ${acceleratorProfile.acceleratorProfile.spec.identifier}` - : acceleratorProfile.useExisting + : acceleratorProfile.unknownProfileDetected ? 'Unknown' : 'None', - acceleratorCount: acceleratorProfile.useExisting ? undefined : acceleratorProfile.count, + acceleratorCount: acceleratorProfile.unknownProfileDetected + ? undefined + : acceleratorProfile.count, lastSelectedSize: selectedSize.name, lastSelectedImage: `${selectedImageTag.image?.name}:${selectedImageTag.tag?.name}`, }); @@ -322,7 +333,8 @@ const SpawnerPage: React.FC = () => { /> diff --git a/frontend/src/pages/projects/notebook/NotebookStatusToggle.tsx b/frontend/src/pages/projects/notebook/NotebookStatusToggle.tsx index ed218796b6..654dede41f 100644 --- a/frontend/src/pages/projects/notebook/NotebookStatusToggle.tsx +++ b/frontend/src/pages/projects/notebook/NotebookStatusToggle.tsx @@ -28,7 +28,7 @@ const NotebookStatusToggle: React.FC = ({ isDisabled, }) => { const { notebook, isStarting, isRunning, isStopping, refresh } = notebookState; - const [acceleratorProfile] = useNotebookAcceleratorProfile(notebook); + const acceleratorProfile = useNotebookAcceleratorProfile(notebook); const { size } = useNotebookDeploymentSize(notebook); const [isOpenConfirm, setOpenConfirm] = React.useState(false); const [inProgress, setInProgress] = React.useState(false); @@ -55,10 +55,12 @@ const NotebookStatusToggle: React.FC = ({ (action: 'started' | 'stopped') => { fireFormTrackingEvent(`Workbench ${action === 'started' ? 'Started' : 'Stopped'}`, { outcome: TrackingOutcome.submit, - acceleratorCount: acceleratorProfile.useExisting ? undefined : acceleratorProfile.count, + acceleratorCount: acceleratorProfile.unknownProfileDetected + ? undefined + : acceleratorProfile.count, accelerator: acceleratorProfile.acceleratorProfile ? `${acceleratorProfile.acceleratorProfile.spec.displayName} (${acceleratorProfile.acceleratorProfile.metadata.name}): ${acceleratorProfile.acceleratorProfile.spec.identifier}` - : acceleratorProfile.useExisting + : acceleratorProfile.unknownProfileDetected ? 'Unknown' : 'None', lastSelectedSize: diff --git a/frontend/src/pages/projects/screens/detail/notebooks/useNotebookAcceleratorProfile.ts b/frontend/src/pages/projects/screens/detail/notebooks/useNotebookAcceleratorProfile.ts index 79e282d9ff..6343b628c7 100644 --- a/frontend/src/pages/projects/screens/detail/notebooks/useNotebookAcceleratorProfile.ts +++ b/frontend/src/pages/projects/screens/detail/notebooks/useNotebookAcceleratorProfile.ts @@ -3,11 +3,10 @@ import { Notebook } from '~/types'; import useAcceleratorProfileState, { AcceleratorProfileState, } from '~/utilities/useAcceleratorProfileState'; -import { GenericObjectState } from '~/utilities/useGenericObjectState'; const useNotebookAcceleratorProfile = ( notebook?: NotebookKind | Notebook | null, -): GenericObjectState => { +): AcceleratorProfileState => { const name = notebook?.metadata.annotations?.['opendatahub.io/accelerator-name']; const resources = notebook?.spec.template.spec.containers.find( (container) => container.name === notebook.metadata.name, diff --git a/frontend/src/pages/projects/screens/spawner/SpawnerFooter.tsx b/frontend/src/pages/projects/screens/spawner/SpawnerFooter.tsx index de095ceafc..22978b9fd3 100644 --- a/frontend/src/pages/projects/screens/spawner/SpawnerFooter.tsx +++ b/frontend/src/pages/projects/screens/spawner/SpawnerFooter.tsx @@ -84,14 +84,15 @@ const SpawnerFooter: React.FC = ({ editNotebook, existingDataConnections, ); - const afterStart = (name: string, type: 'created' | 'updated') => { - const { acceleratorProfile, notebookSize, image } = startNotebookData; + const { selectedAcceleratorProfile, notebookSize, image } = startNotebookData; const tep: FormTrackingEventProperties = { - acceleratorCount: acceleratorProfile.useExisting ? undefined : acceleratorProfile.count, - accelerator: acceleratorProfile.acceleratorProfile - ? `${acceleratorProfile.acceleratorProfile.spec.displayName} (${acceleratorProfile.acceleratorProfile.metadata.name}): ${acceleratorProfile.acceleratorProfile.spec.identifier}` - : acceleratorProfile.useExisting + acceleratorCount: selectedAcceleratorProfile.useExistingSettings + ? undefined + : selectedAcceleratorProfile.count, + accelerator: selectedAcceleratorProfile.profile + ? `${selectedAcceleratorProfile.profile.spec.displayName} (${selectedAcceleratorProfile.profile.metadata.name}): ${selectedAcceleratorProfile.profile.spec.identifier}` + : selectedAcceleratorProfile.useExistingSettings ? 'Unknown' : 'None', lastSelectedSize: notebookSize.name, diff --git a/frontend/src/pages/projects/screens/spawner/SpawnerPage.tsx b/frontend/src/pages/projects/screens/spawner/SpawnerPage.tsx index d64e5afcd4..790da0f874 100644 --- a/frontend/src/pages/projects/screens/spawner/SpawnerPage.tsx +++ b/frontend/src/pages/projects/screens/spawner/SpawnerPage.tsx @@ -21,10 +21,13 @@ import useNotebookImageData from '~/pages/projects/screens/detail/notebooks/useN import NotebookRestartAlert from '~/pages/projects/components/NotebookRestartAlert'; import useWillNotebooksRestart from '~/pages/projects/notebook/useWillNotebooksRestart'; import CanEnableElyraPipelinesCheck from '~/concepts/pipelines/elyra/CanEnableElyraPipelinesCheck'; -import AcceleratorProfileSelectField from '~/pages/notebookController/screens/server/AcceleratorProfileSelectField'; +import AcceleratorProfileSelectField, { + AcceleratorProfileSelectFieldState, +} from '~/pages/notebookController/screens/server/AcceleratorProfileSelectField'; import useNotebookAcceleratorProfile from '~/pages/projects/screens/detail/notebooks/useNotebookAcceleratorProfile'; import { NotebookImageAvailability } from '~/pages/projects/screens/detail/notebooks/const'; import { getDescriptionFromK8sResource, getDisplayNameFromK8sResource } from '~/concepts/k8s/utils'; +import useGenericObjectState from '~/utilities/useGenericObjectState'; import { SpawnerPageSectionID } from './types'; import { ScrollableSelectorID, SpawnerPageSectionTitles } from './const'; import SpawnerFooter from './SpawnerFooter'; @@ -72,6 +75,13 @@ const SpawnerPage: React.FC = ({ existingNotebook }) => { existingNotebook, ); + const [selectedAcceleratorProfile, setSelectedAcceleratorProfile] = + useGenericObjectState({ + profile: undefined, + count: 0, + useExistingSettings: false, + }); + const restartNotebooks = useWillNotebooksRestart([existingNotebook?.metadata.name || '']); React.useEffect(() => { @@ -94,8 +104,7 @@ const SpawnerPage: React.FC = ({ existingNotebook }) => { } }, [data, loaded, loadError]); - const [notebookAcceleratorProfileState, setNotebookAcceleratorProfileState] = - useNotebookAcceleratorProfile(existingNotebook); + const notebookAcceleratorProfileState = useNotebookAcceleratorProfile(existingNotebook); React.useEffect(() => { if (selectedImage.imageStream) { @@ -181,8 +190,9 @@ const SpawnerPage: React.FC = ({ existingNotebook }) => { /> = ({ existingNotebook }) => { projectName: currentProject.metadata.name, image: selectedImage, notebookSize: selectedSize, - acceleratorProfile: notebookAcceleratorProfileState, + initialAcceleratorProfile: notebookAcceleratorProfileState, + selectedAcceleratorProfile, volumes: [], volumeMounts: [], existingTolerations: existingNotebook?.spec.template.spec.tolerations || [], diff --git a/frontend/src/pages/projects/types.ts b/frontend/src/pages/projects/types.ts index 4ffdceb253..e8e6be0de3 100644 --- a/frontend/src/pages/projects/types.ts +++ b/frontend/src/pages/projects/types.ts @@ -12,6 +12,7 @@ import { import { ValueOf } from '~/typeHelpers'; import { AWSSecretKind } from '~/k8sTypes'; import { AcceleratorProfileState } from '~/utilities/useAcceleratorProfileState'; +import { AcceleratorProfileSelectFieldState } from '~/pages/notebookController/screens/server/AcceleratorProfileSelectField'; import { AwsKeys } from './dataConnections/const'; export type UpdateObjectAtPropAndValue = (propKey: keyof T, propValue: ValueOf) => void; @@ -65,7 +66,8 @@ export type StartNotebookData = { projectName: string; notebookName: string; notebookSize: NotebookSize; - acceleratorProfile: AcceleratorProfileState; + initialAcceleratorProfile: AcceleratorProfileState; + selectedAcceleratorProfile: AcceleratorProfileSelectFieldState; image: ImageStreamAndVersion; volumes?: Volume[]; volumeMounts?: VolumeMount[]; diff --git a/frontend/src/utilities/tolerations.ts b/frontend/src/utilities/tolerations.ts index 44ab2962d5..c4ba281faf 100644 --- a/frontend/src/utilities/tolerations.ts +++ b/frontend/src/utilities/tolerations.ts @@ -2,6 +2,7 @@ import { Patch } from '@openshift/dynamic-plugin-sdk-utils'; import _ from 'lodash-es'; import { Toleration, TolerationEffect, TolerationOperator, TolerationSettings } from '~/types'; import { DashboardConfigKind, NotebookKind } from '~/k8sTypes'; +import { AcceleratorProfileSelectFieldState } from '~/pages/notebookController/screens/server/AcceleratorProfileSelectField'; import { AcceleratorProfileState } from './useAcceleratorProfileState'; export type TolerationChanges = { @@ -11,24 +12,25 @@ export type TolerationChanges = { export const determineTolerations = ( tolerationSettings?: TolerationSettings, - acceleratorProfileState?: AcceleratorProfileState, + initialAcceleratorProfile?: AcceleratorProfileState, + selectedAcceleratorProfile?: AcceleratorProfileSelectFieldState, existingTolerations?: Toleration[], ): Toleration[] => { let tolerations = existingTolerations || []; // remove old accelerator tolerations if they exist - if (acceleratorProfileState?.initialAcceleratorProfile) { + if (initialAcceleratorProfile?.acceleratorProfile) { tolerations = tolerations.filter( (t) => - !acceleratorProfileState.initialAcceleratorProfile?.spec.tolerations?.some((t2) => + !initialAcceleratorProfile.acceleratorProfile?.spec.tolerations?.some((t2) => _.isEqual(t2, t), ), ); } // add new accelerator tolerations if they exist - if (acceleratorProfileState?.acceleratorProfile?.spec.tolerations) { - tolerations.push(...acceleratorProfileState.acceleratorProfile.spec.tolerations); + if (selectedAcceleratorProfile?.profile?.spec.tolerations) { + tolerations.push(...selectedAcceleratorProfile.profile.spec.tolerations); } // remove duplicated tolerations @@ -61,6 +63,7 @@ export const computeNotebooksTolerations = ( const settings = determineTolerations( dashboardConfig.spec.notebookController?.notebookTolerationSettings, undefined, + undefined, tolerations, ); diff --git a/frontend/src/utilities/useAcceleratorProfileState.ts b/frontend/src/utilities/useAcceleratorProfileState.ts index 239de80f51..831db9e7e2 100644 --- a/frontend/src/utilities/useAcceleratorProfileState.ts +++ b/frontend/src/utilities/useAcceleratorProfileState.ts @@ -9,127 +9,147 @@ import { TolerationEffect, TolerationOperator, } from '~/types'; -import useGenericObjectState, { GenericObjectState } from '~/utilities/useGenericObjectState'; import { getAcceleratorProfileCount, isEnumMember } from '~/utilities/utils'; export type AcceleratorProfileState = { - acceleratorProfile?: AcceleratorProfileKind; acceleratorProfiles: AcceleratorProfileKind[]; - initialAcceleratorProfile?: AcceleratorProfileKind; - useExisting: boolean; count: number; - additionalOptions?: { - useExisting?: boolean; - useDisabled?: AcceleratorProfileKind; - }; -}; +} & ( + | { acceleratorProfile?: AcceleratorProfileKind; unknownProfileDetected: false } + | { + acceleratorProfile: undefined; + unknownProfileDetected: true; + } +); const useAcceleratorProfileState = ( resources?: ContainerResources, tolerations?: Toleration[], existingAcceleratorProfileName?: string, -): GenericObjectState => { - const [acceleratorProfileState, setData, resetData] = - useGenericObjectState({ - acceleratorProfile: undefined, - acceleratorProfiles: [], - initialAcceleratorProfile: undefined, - count: 0, - useExisting: false, - }); +): AcceleratorProfileState => { + // const [acceleratorProfileState, setData] = useGenericObjectState({ + // acceleratorProfiles: [], + // acceleratorProfile: undefined, + // count: 0, + // unknownProfileDetected: false, + // }); const { dashboardNamespace } = useDashboardNamespace(); - const [acceleratorProfiles, loaded, loadError, refresh] = - useAcceleratorProfiles(dashboardNamespace); - - React.useEffect(() => { - if (loaded && !loadError) { - setData('acceleratorProfiles', acceleratorProfiles); - - // Exit early if no resources = not in edit mode - if (!resources) { - return; - } + const [acceleratorProfiles, loaded, loadError] = useAcceleratorProfiles(dashboardNamespace); + return React.useMemo(() => { + if (!loaded || loadError) { + return { + acceleratorProfiles: [], + acceleratorProfile: undefined, + count: 0, + unknownProfileDetected: false, + }; + } + // Exit early if no resources = not in edit mode + if (resources) { const acceleratorProfile = acceleratorProfiles.find( (cr) => cr.metadata.name === existingAcceleratorProfileName, ); if (acceleratorProfile) { - setData('acceleratorProfile', acceleratorProfile); - setData('initialAcceleratorProfile', acceleratorProfile); - setData('count', getAcceleratorProfileCount(acceleratorProfile, resources)); - if (!acceleratorProfile.spec.enabled) { - setData('additionalOptions', { useDisabled: acceleratorProfile }); - } - } else { - // check if there is accelerator usage in the container - // this is to handle the case where the accelerator is disabled, deleted, or empty - const possibleAcceleratorRequests = Object.entries(resources.requests ?? {}) - .filter(([key]) => !isEnumMember(key, ContainerResourceAttributes)) - .map(([key, value]) => ({ identifier: key, count: value })); - if (possibleAcceleratorRequests.length > 0) { - // check if they are just using the nvidia.com/gpu - // if so, lets migrate them over to using the migrated-gpu accelerator profile if it exists - const nvidiaAcceleratorRequests = possibleAcceleratorRequests.find( - (request) => request.identifier === 'nvidia.com/gpu', - ); + // setData('acceleratorProfile', acceleratorProfile); + // setData('count', getAcceleratorProfileCount(acceleratorProfile, resources)); + // setData('unknownProfileDetected', false); + return { + acceleratorProfiles, + acceleratorProfile, + count: getAcceleratorProfileCount(acceleratorProfile, resources), + unknownProfileDetected: false, + }; + } + // check if there is accelerator usage in the container + // this is to handle the case where the accelerator is disabled, deleted, or empty + const possibleAcceleratorRequests = Object.entries(resources.requests ?? {}) + .filter(([key]) => !isEnumMember(key, ContainerResourceAttributes)) + .map(([key, value]) => ({ identifier: key, count: value })); + if (possibleAcceleratorRequests.length > 0) { + // check if they are just using the nvidia.com/gpu + // if so, lets migrate them over to using the migrated-gpu accelerator profile if it exists + const nvidiaAcceleratorRequests = possibleAcceleratorRequests.find( + (request) => request.identifier === 'nvidia.com/gpu', + ); - if ( - nvidiaAcceleratorRequests && - tolerations?.some( - (toleration) => - toleration.key === 'nvidia.com/gpu' && - toleration.operator === 'Exists' && - toleration.effect === 'NoSchedule', - ) - ) { - const migratedAcceleratorProfile = acceleratorProfiles.find( - (cr) => cr.metadata.name === 'migrated-gpu', - ); + if ( + nvidiaAcceleratorRequests && + tolerations?.some( + (toleration) => + toleration.key === 'nvidia.com/gpu' && + toleration.operator === 'Exists' && + toleration.effect === 'NoSchedule', + ) + ) { + const migratedAcceleratorProfile = acceleratorProfiles.find( + (cr) => cr.metadata.name === 'migrated-gpu', + ); - if (migratedAcceleratorProfile) { - setData('acceleratorProfile', migratedAcceleratorProfile); - setData('initialAcceleratorProfile', migratedAcceleratorProfile); - setData('count', Number(nvidiaAcceleratorRequests.count ?? 0)); - if (!migratedAcceleratorProfile.spec.enabled) { - setData('additionalOptions', { useDisabled: acceleratorProfile }); - } - } else { - // create a fake accelerator to use - const fakeAcceleratorProfile: AcceleratorProfileKind = { - apiVersion: 'dashboard.opendatahub.io/v1', - kind: 'AcceleratorProfile', - metadata: { - name: 'migrated-gpu', - }, - spec: { - identifier: 'nvidia.com/gpu', - displayName: 'NVIDIA GPU', - enabled: true, - tolerations: [ - { - key: 'nvidia.com/gpu', - operator: TolerationOperator.EXISTS, - effect: TolerationEffect.NO_SCHEDULE, - }, - ], + if (migratedAcceleratorProfile) { + // setData('acceleratorProfile', migratedAcceleratorProfile); + // setData('count', Number(nvidiaAcceleratorRequests.count ?? 0)); + // setData('unknownProfileDetected', false); + return { + acceleratorProfiles, + acceleratorProfile: migratedAcceleratorProfile, + count: getAcceleratorProfileCount(migratedAcceleratorProfile, resources), + unknownProfileDetected: false, + }; + } + // create a fake accelerator to use + const fakeAcceleratorProfile: AcceleratorProfileKind = { + apiVersion: 'dashboard.opendatahub.io/v1', + kind: 'AcceleratorProfile', + metadata: { + name: 'migrated-gpu', + }, + spec: { + identifier: 'nvidia.com/gpu', + displayName: 'NVIDIA GPU', + enabled: true, + tolerations: [ + { + key: 'nvidia.com/gpu', + operator: TolerationOperator.EXISTS, + effect: TolerationEffect.NO_SCHEDULE, }, - }; + ], + }, + }; - setData('acceleratorProfile', fakeAcceleratorProfile); - setData('acceleratorProfiles', [fakeAcceleratorProfile, ...acceleratorProfiles]); - setData('initialAcceleratorProfile', fakeAcceleratorProfile); - setData('count', Number(nvidiaAcceleratorRequests.count ?? 0)); - } - } else { - // fallback to using the existing accelerator - setData('useExisting', true); - setData('additionalOptions', { useExisting: true }); - } + // setData('acceleratorProfile', fakeAcceleratorProfile); + // setData('acceleratorProfiles', [fakeAcceleratorProfile, ...acceleratorProfiles]); + // setData('count', Number(nvidiaAcceleratorRequests.count ?? 0)); + // setData('unknownProfileDetected', false); + return { + acceleratorProfiles: [fakeAcceleratorProfile, ...acceleratorProfiles], + acceleratorProfile: fakeAcceleratorProfile, + count: Number(nvidiaAcceleratorRequests.count ?? 0), + unknownProfileDetected: false, + }; } + // fallback to using the existing accelerator + // setData('unknownProfileDetected', true); + // setData('acceleratorProfile', undefined); + // setData('count', 0); + return { + acceleratorProfiles, + acceleratorProfile: undefined, + count: 0, + unknownProfileDetected: true, + }; } } + + return { + acceleratorProfiles, + acceleratorProfile: undefined, + count: 0, + unknownProfileDetected: false, + }; }, [ acceleratorProfiles, loaded, @@ -137,15 +157,7 @@ const useAcceleratorProfileState = ( resources, tolerations, existingAcceleratorProfileName, - setData, ]); - - const resetDataAndRefresh = React.useCallback(() => { - resetData(); - refresh(); - }, [refresh, resetData]); - - return [acceleratorProfileState, setData, resetDataAndRefresh]; }; export default useAcceleratorProfileState;