From a1a5bf3b3ffb4787efb8885452075820fcd013f9 Mon Sep 17 00:00:00 2001 From: Zhongxin Guo Date: Wed, 29 Oct 2025 03:29:50 -0700 Subject: [PATCH 1/9] add job type support; add inference job support --- .../components/job-information.jsx | 12 +++- .../job-submission/components/job-type.jsx | 64 +++++++++++++++++++ .../job-submission/job-submission-page.jsx | 31 ++++++++- .../job-submission/models/job-basic-info.js | 7 +- .../app/job-submission/models/job-protocol.js | 2 + .../job-submission/models/protocol-schema.js | 1 + 6 files changed, 112 insertions(+), 5 deletions(-) create mode 100644 src/webportal/src/app/job-submission/components/job-type.jsx diff --git a/src/webportal/src/app/job-submission/components/job-information.jsx b/src/webportal/src/app/job-submission/components/job-information.jsx index be5fd64c..54ae7984 100644 --- a/src/webportal/src/app/job-submission/components/job-information.jsx +++ b/src/webportal/src/app/job-submission/components/job-information.jsx @@ -29,6 +29,7 @@ import { FormTextField } from './form-text-field'; import { FormPage } from './form-page'; import { FormSpinButton } from './form-spin-button'; import { VirtualCluster } from './virtual-cluster'; +import { JobType } from './job-type'; import Card from '../../components/card'; import { JobBasicInfo } from '../models/job-basic-info'; import { PROTOCOL_TOOLTIPS } from '../utils/constants'; @@ -37,7 +38,7 @@ const JOB_NAME_REGX = /^[A-Za-z0-9\-._~]+$/; export const JobInformation = React.memo( ({ jobInformation, onChange, advanceFlag }) => { - const { name, virtualCluster, jobRetryCount } = jobInformation; + const { name, virtualCluster, jobRetryCount, jobType } = jobInformation; const onChangeProp = useCallback( (type, value) => { @@ -59,6 +60,11 @@ export const JobInformation = React.memo( [onChangeProp], ); + const onJobTypeChange = useCallback( + jobType => onChangeProp('jobType', jobType), + [onChangeProp], + ); + const onRetryCountChange = useCallback( val => onChangeProp('jobRetryCount', val), [onChangeProp], @@ -89,6 +95,10 @@ export const JobInformation = React.memo( onChange={onRetryCountChange} /> )} + ); diff --git a/src/webportal/src/app/job-submission/components/job-type.jsx b/src/webportal/src/app/job-submission/components/job-type.jsx new file mode 100644 index 00000000..f9dd234f --- /dev/null +++ b/src/webportal/src/app/job-submission/components/job-type.jsx @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import React, { useMemo, useCallback } from 'react'; +import { BasicSection } from './basic-section'; +import { Dropdown } from 'office-ui-fabric-react'; +import { FormShortSection } from './form-page'; +import PropTypes from 'prop-types'; +import context from './context'; + +export const JobType = React.memo(props => { + const { onChange, jobType } = props; + const jobTypes = ['others', 'training', 'inference']; + + const options = useMemo( + () => + jobTypes.map((jobType, index) => { + return { + key: `jobType_${index}`, + text: jobType, + }; + }), + [jobTypes], + ); + + const _onChange = useCallback( + (_, item) => { + if (onChange !== undefined) { + onChange(item.text); + if (item.text === 'inference') { + alert( + `Inference jobs have three forced parameters: + 1. INTERNAL_SERVER_IP=$PAI_HOST_IP_taskrole_0 : Fix value, used to inference service IP + 2. INTERNAL_SERVER_PORT=$PAI_PORT_LIST_taskrole_0_http : Fixed Value used to inference service port + 3. API_KEY="...": A random generated string, can be set arbitrarily) + +The three parameters will be automatically added to your job parameters upon switching to inference job type. + `, + ); + } + } + }, + [onChange], + ); + + const jobTypeIndex = options.findIndex(value => value.text === jobType); + return ( + + + + + + ); +}); + +JobType.propTypes = { + onChange: PropTypes.func, + jobType: PropTypes.string, +}; diff --git a/src/webportal/src/app/job-submission/job-submission-page.jsx b/src/webportal/src/app/job-submission/job-submission-page.jsx index 34a0c685..7f867b46 100644 --- a/src/webportal/src/app/job-submission/job-submission-page.jsx +++ b/src/webportal/src/app/job-submission/job-submission-page.jsx @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import React, { useState, useCallback, useEffect, useMemo } from 'react'; +import React, { useState, useCallback, useEffect, useMemo, use } from 'react'; import { Fabric, Stack, StackItem, Dropdown } from 'office-ui-fabric-react'; import { isNil, isEmpty, get, cloneDeep } from 'lodash'; import PropTypes from 'prop-types'; @@ -250,7 +250,7 @@ export const JobSubmissionPage = ({ break; } } - } catch {} // ignore all exceptions here + } catch { } // ignore all exceptions here if (!isEmpty(defaultStorageConfig)) { const storagePlugin = { plugin: STORAGE_PLUGIN, @@ -386,6 +386,33 @@ export const JobSubmissionPage = ({ .catch(alert); }, [jobInformation.virtualCluster]); + useEffect(() => { + if (jobInformation.jobType === 'inference') { + const hasApiKey = parameters.find(param => param.key === 'API_KEY'); + const hasInternalServerIp = parameters.find(param => param.key === 'INTERNAL_SERVER_IP'); + const hasInternalServerPort = parameters.find(param => param.key === 'INTERNAL_SERVER_PORT'); + if (!hasApiKey || !hasInternalServerIp || !hasInternalServerPort) { + const newParameters = [...parameters]; + if (!hasInternalServerIp) { + // set fixed INTERNAL_SERVER_IP + newParameters.push({ key: 'INTERNAL_SERVER_IP', value: '$PAI_HOST_IP_taskrole_0' }); + } + if (!hasInternalServerPort) { + // set fixed INTERNAL_SERVER_PORT + newParameters.push({ key: 'INTERNAL_SERVER_PORT', value: '$PAI_PORT_LIST_taskrole_0_http' }); + } + if (!hasApiKey) { + { + // set a random generated api key + const randomKey = crypto.randomUUID(); + newParameters.push({ key: 'API_KEY', value: randomKey }); + } + } + setParameters(newParameters); + } + } + }, [jobInformation.jobType, parameters]); + const onTemplateChange = useCallback((_, item) => { if (item.key === 'No') { return; diff --git a/src/webportal/src/app/job-submission/models/job-basic-info.js b/src/webportal/src/app/job-submission/models/job-basic-info.js index 4b81d9c1..4679dc2f 100644 --- a/src/webportal/src/app/job-submission/models/job-basic-info.js +++ b/src/webportal/src/app/job-submission/models/job-basic-info.js @@ -27,19 +27,21 @@ import { isEmpty, get } from 'lodash'; export class JobBasicInfo { constructor(props) { - const { name, jobRetryCount, virtualCluster } = props; + const { name, jobRetryCount, virtualCluster, jobType} = props; this.name = name || ''; this.jobRetryCount = jobRetryCount || 0; this.virtualCluster = virtualCluster || ''; + this.jobType = jobType || ''; } static fromProtocol(protocol) { - const { name, jobRetryCount } = protocol; + const { name, jobRetryCount, jobType } = protocol; const virtualCluster = get(protocol, 'defaults.virtualCluster', 'default'); return new JobBasicInfo({ name: name, jobRetryCount: jobRetryCount, virtualCluster: virtualCluster, + jobType: jobType, }); } @@ -59,6 +61,7 @@ export class JobBasicInfo { name: this.name, type: 'job', jobRetryCount: this.jobRetryCount, + jobType: this.jobType, }; } } diff --git a/src/webportal/src/app/job-submission/models/job-protocol.js b/src/webportal/src/app/job-submission/models/job-protocol.js index c41c73b1..35e9b796 100644 --- a/src/webportal/src/app/job-submission/models/job-protocol.js +++ b/src/webportal/src/app/job-submission/models/job-protocol.js @@ -37,6 +37,7 @@ export class JobProtocol { const { name, jobRetryCount, + jobType, prerequisites, parameters, taskRoles, @@ -53,6 +54,7 @@ export class JobProtocol { this.contributor = contributor || ''; this.type = 'job'; this.jobRetryCount = jobRetryCount || 0; + this.jobType = jobType || 'others'; this.prerequisites = prerequisites || []; this.parameters = parameters || {}; this.taskRoles = taskRoles || {}; diff --git a/src/webportal/src/app/job-submission/models/protocol-schema.js b/src/webportal/src/app/job-submission/models/protocol-schema.js index b48c4d0f..127a9693 100644 --- a/src/webportal/src/app/job-submission/models/protocol-schema.js +++ b/src/webportal/src/app/job-submission/models/protocol-schema.js @@ -173,6 +173,7 @@ export const jobProtocolSchema = Joi.object().keys({ version: [Joi.string(), Joi.number()], contributor: Joi.string(), description: Joi.string(), + jobType: Joi.string().valid(['others', 'training', 'inference']).default('others'), prerequisites: Joi.array() .items(prerequisitesSchema) From 3941484401085928150216326509ada5a800e340 Mon Sep 17 00:00:00 2001 From: Zhongxin Guo Date: Fri, 31 Oct 2025 00:13:06 -0700 Subject: [PATCH 2/9] update --- .../src/app/job-submission/components/job-type.jsx | 8 +++----- .../src/app/job-submission/job-submission-page.jsx | 4 ++-- .../src/app/job-submission/models/job-basic-info.js | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/webportal/src/app/job-submission/components/job-type.jsx b/src/webportal/src/app/job-submission/components/job-type.jsx index f9dd234f..3e43ae22 100644 --- a/src/webportal/src/app/job-submission/components/job-type.jsx +++ b/src/webportal/src/app/job-submission/components/job-type.jsx @@ -6,7 +6,6 @@ import { BasicSection } from './basic-section'; import { Dropdown } from 'office-ui-fabric-react'; import { FormShortSection } from './form-page'; import PropTypes from 'prop-types'; -import context from './context'; export const JobType = React.memo(props => { const { onChange, jobType } = props; @@ -20,7 +19,6 @@ export const JobType = React.memo(props => { text: jobType, }; }), - [jobTypes], ); const _onChange = useCallback( @@ -30,8 +28,8 @@ export const JobType = React.memo(props => { if (item.text === 'inference') { alert( `Inference jobs have three forced parameters: - 1. INTERNAL_SERVER_IP=$PAI_HOST_IP_taskrole_0 : Fix value, used to inference service IP - 2. INTERNAL_SERVER_PORT=$PAI_PORT_LIST_taskrole_0_http : Fixed Value used to inference service port + 1. INTERNAL_SERVER_IP=$PAI_HOST_IP_taskrole_0 : Fixed value, used to inference service IP + 2. INTERNAL_SERVER_PORT=$PAI_PORT_LIST_taskrole_0_http : Fixed value used to inference service port 3. API_KEY="...": A random generated string, can be set arbitrarily) The three parameters will be automatically added to your job parameters upon switching to inference job type. @@ -51,7 +49,7 @@ The three parameters will be automatically added to your job parameters upon swi placeholder='Select an option' options={options} onChange={_onChange} - selectedKey={jobTypeIndex === -1 ? null : `jobType_${jobTypeIndex}`} + selectedKey={jobTypeIndex === -1 ? "jobType_0" : `jobType_${jobTypeIndex}`} /> diff --git a/src/webportal/src/app/job-submission/job-submission-page.jsx b/src/webportal/src/app/job-submission/job-submission-page.jsx index 7f867b46..01caeb84 100644 --- a/src/webportal/src/app/job-submission/job-submission-page.jsx +++ b/src/webportal/src/app/job-submission/job-submission-page.jsx @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import React, { useState, useCallback, useEffect, useMemo, use } from 'react'; +import { useState, useCallback, useEffect, useMemo } from 'react'; import { Fabric, Stack, StackItem, Dropdown } from 'office-ui-fabric-react'; import { isNil, isEmpty, get, cloneDeep } from 'lodash'; import PropTypes from 'prop-types'; @@ -411,7 +411,7 @@ export const JobSubmissionPage = ({ setParameters(newParameters); } } - }, [jobInformation.jobType, parameters]); + }, [jobInformation.jobType]); const onTemplateChange = useCallback((_, item) => { if (item.key === 'No') { diff --git a/src/webportal/src/app/job-submission/models/job-basic-info.js b/src/webportal/src/app/job-submission/models/job-basic-info.js index 4679dc2f..ff5d0bee 100644 --- a/src/webportal/src/app/job-submission/models/job-basic-info.js +++ b/src/webportal/src/app/job-submission/models/job-basic-info.js @@ -27,7 +27,7 @@ import { isEmpty, get } from 'lodash'; export class JobBasicInfo { constructor(props) { - const { name, jobRetryCount, virtualCluster, jobType} = props; + const { name, jobRetryCount, virtualCluster, jobType } = props; this.name = name || ''; this.jobRetryCount = jobRetryCount || 0; this.virtualCluster = virtualCluster || ''; From c54e75d96c83c9430dfa2824b83334c14c7c1d33 Mon Sep 17 00:00:00 2001 From: Zhongxin Guo Date: Tue, 4 Nov 2025 03:38:18 -0800 Subject: [PATCH 3/9] deploy webportal-dind when webportal changed --- .github/workflows/build-deploy-changes.yaml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-deploy-changes.yaml b/.github/workflows/build-deploy-changes.yaml index ef0ac13a..08f30819 100644 --- a/.github/workflows/build-deploy-changes.yaml +++ b/.github/workflows/build-deploy-changes.yaml @@ -148,7 +148,14 @@ jobs: echo "Pushing config to cluster \"${{ secrets.PAI_CLUSTER_NAME }}\" ..." $GITHUB_WORKSPACE/paictl.py config push -m service -p $GITHUB_WORKSPACE/config/cluster-configuration < cluster_id echo "Starting to update \"${{ steps.changes.outputs.folders }}\" on ${{ secrets.PAI_CLUSTER_NAME }} ..." - $GITHUB_WORKSPACE/paictl.py service start -n ${{ steps.changes.outputs.folders }} < cluster_id + # + # Replace "webportal" with "webportal-dind" if "webportal" is changed + services_to_deploy="${{ steps.changes.outputs.folders }}" + if [[ " $services_to_deploy " == *" webportal "* ]]; then + services_to_deploy="${services_to_deploy//webportal/webportal-dind}" + fi + echo "Final services to deploy: $services_to_deploy" + $GITHUB_WORKSPACE/paictl.py service start -n $services_to_deploy < cluster_id kubectl get pod kubectl get service From 6ecfb2395ae1f3aa5e8d262be26cda94c3174b5a Mon Sep 17 00:00:00 2001 From: Zhongxin Guo Date: Wed, 5 Nov 2025 00:03:22 -0800 Subject: [PATCH 4/9] update --- .github/workflows/build-deploy-changes.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-deploy-changes.yaml b/.github/workflows/build-deploy-changes.yaml index 08f30819..103434e6 100644 --- a/.github/workflows/build-deploy-changes.yaml +++ b/.github/workflows/build-deploy-changes.yaml @@ -151,7 +151,7 @@ jobs: # # Replace "webportal" with "webportal-dind" if "webportal" is changed services_to_deploy="${{ steps.changes.outputs.folders }}" - if [[ " $services_to_deploy " == *" webportal "* ]]; then + if [[ " $services_to_deploy " == *"webportal "* ]]; then services_to_deploy="${services_to_deploy//webportal/webportal-dind}" fi echo "Final services to deploy: $services_to_deploy" From 5ab7f32d10a63950195a18ded71bc47727ec00cc Mon Sep 17 00:00:00 2001 From: Zhongxin Guo Date: Wed, 5 Nov 2025 00:32:44 -0800 Subject: [PATCH 5/9] update --- .github/workflows/build-deploy-changes.yaml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-deploy-changes.yaml b/.github/workflows/build-deploy-changes.yaml index 103434e6..8803dd25 100644 --- a/.github/workflows/build-deploy-changes.yaml +++ b/.github/workflows/build-deploy-changes.yaml @@ -148,12 +148,13 @@ jobs: echo "Pushing config to cluster \"${{ secrets.PAI_CLUSTER_NAME }}\" ..." $GITHUB_WORKSPACE/paictl.py config push -m service -p $GITHUB_WORKSPACE/config/cluster-configuration < cluster_id echo "Starting to update \"${{ steps.changes.outputs.folders }}\" on ${{ secrets.PAI_CLUSTER_NAME }} ..." - # - # Replace "webportal" with "webportal-dind" if "webportal" is changed + # Replace "webportal" with "webportal-dind" if "webportal" is changed services_to_deploy="${{ steps.changes.outputs.folders }}" - if [[ " $services_to_deploy " == *"webportal "* ]]; then - services_to_deploy="${services_to_deploy//webportal/webportal-dind}" - fi + if [[ " $services_to_deploy " == *"webportal"* ]]; then + services_to_deploy="${services_to_deploy//webportal /}" + services_to_deploy="${services_to_deploy//webportal-dind /}" + services_to_deploy="$services_to_deploy webportal-dind" + fiW echo "Final services to deploy: $services_to_deploy" $GITHUB_WORKSPACE/paictl.py service start -n $services_to_deploy < cluster_id kubectl get pod From a7f495e9aa90345a56d82772d81e4b5027627485 Mon Sep 17 00:00:00 2001 From: Zhongxin Guo Date: Wed, 5 Nov 2025 03:26:13 -0800 Subject: [PATCH 6/9] update --- .github/workflows/build-deploy-changes.yaml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-deploy-changes.yaml b/.github/workflows/build-deploy-changes.yaml index 8803dd25..bc5f5708 100644 --- a/.github/workflows/build-deploy-changes.yaml +++ b/.github/workflows/build-deploy-changes.yaml @@ -142,20 +142,21 @@ jobs: --overwrite-existing kubelogin convert-kubeconfig -l azurecli kubectl config use-context ${{ secrets.KUBERNETES_CLUSTER }} - echo "${{ secrets.PAI_CLUSTER_NAME }}" > cluster_id - echo "Stopping changed pai services \"${{ steps.changes.outputs.folders }}\" on ${{ secrets.PAI_CLUSTER_NAME }} ..." - $GITHUB_WORKSPACE/paictl.py service stop -n ${{ steps.changes.outputs.folders }} < cluster_id - echo "Pushing config to cluster \"${{ secrets.PAI_CLUSTER_NAME }}\" ..." - $GITHUB_WORKSPACE/paictl.py config push -m service -p $GITHUB_WORKSPACE/config/cluster-configuration < cluster_id - echo "Starting to update \"${{ steps.changes.outputs.folders }}\" on ${{ secrets.PAI_CLUSTER_NAME }} ..." # Replace "webportal" with "webportal-dind" if "webportal" is changed services_to_deploy="${{ steps.changes.outputs.folders }}" if [[ " $services_to_deploy " == *"webportal"* ]]; then services_to_deploy="${services_to_deploy//webportal /}" services_to_deploy="${services_to_deploy//webportal-dind /}" services_to_deploy="$services_to_deploy webportal-dind" - fiW + fi echo "Final services to deploy: $services_to_deploy" + + echo "${{ secrets.PAI_CLUSTER_NAME }}" > cluster_id + echo "Stopping changed pai services $services_to_deploy on ${{ secrets.PAI_CLUSTER_NAME }} ..." + $GITHUB_WORKSPACE/paictl.py service stop -n $services_to_deploy < cluster_id + echo "Pushing config to cluster \"${{ secrets.PAI_CLUSTER_NAME }}\" ..." + $GITHUB_WORKSPACE/paictl.py config push -m service -p $GITHUB_WORKSPACE/config/cluster-configuration < cluster_id + echo "Starting to update $services_to_deploy on ${{ secrets.PAI_CLUSTER_NAME }} ..." $GITHUB_WORKSPACE/paictl.py service start -n $services_to_deploy < cluster_id kubectl get pod kubectl get service From b994dc0733f37df92228ee8f121d2e7a4526f6fa Mon Sep 17 00:00:00 2001 From: Zhongxin Guo Date: Wed, 5 Nov 2025 03:56:30 -0800 Subject: [PATCH 7/9] update --- .github/workflows/build-deploy-changes.yaml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-deploy-changes.yaml b/.github/workflows/build-deploy-changes.yaml index bc5f5708..68fadcd5 100644 --- a/.github/workflows/build-deploy-changes.yaml +++ b/.github/workflows/build-deploy-changes.yaml @@ -144,10 +144,15 @@ jobs: kubectl config use-context ${{ secrets.KUBERNETES_CLUSTER }} # Replace "webportal" with "webportal-dind" if "webportal" is changed services_to_deploy="${{ steps.changes.outputs.folders }}" - if [[ " $services_to_deploy " == *"webportal"* ]]; then - services_to_deploy="${services_to_deploy//webportal /}" - services_to_deploy="${services_to_deploy//webportal-dind /}" - services_to_deploy="$services_to_deploy webportal-dind" + if echo " $services_to_deploy " | grep -q " webportal "; then + tmp="" + for s in $services_to_deploy; do + [ "$s" = "webportal" ] && continue + [ "$s" = "webportal-dind" ] && continue + tmp="$tmp $s" + done + services_to_deploy="$tmp webportal-dind" + services_to_deploy=$(echo "$services_to_deploy" | xargs) fi echo "Final services to deploy: $services_to_deploy" From 92f5646ad554a845111956ed94738bc62a1037d4 Mon Sep 17 00:00:00 2001 From: Zhongxin Guo Date: Thu, 6 Nov 2025 19:32:09 -0800 Subject: [PATCH 8/9] update --- .../src/app/job-submission/components/job-type.jsx | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/webportal/src/app/job-submission/components/job-type.jsx b/src/webportal/src/app/job-submission/components/job-type.jsx index 3e43ae22..6aea07ef 100644 --- a/src/webportal/src/app/job-submission/components/job-type.jsx +++ b/src/webportal/src/app/job-submission/components/job-type.jsx @@ -25,17 +25,6 @@ export const JobType = React.memo(props => { (_, item) => { if (onChange !== undefined) { onChange(item.text); - if (item.text === 'inference') { - alert( - `Inference jobs have three forced parameters: - 1. INTERNAL_SERVER_IP=$PAI_HOST_IP_taskrole_0 : Fixed value, used to inference service IP - 2. INTERNAL_SERVER_PORT=$PAI_PORT_LIST_taskrole_0_http : Fixed value used to inference service port - 3. API_KEY="...": A random generated string, can be set arbitrarily) - -The three parameters will be automatically added to your job parameters upon switching to inference job type. - `, - ); - } } }, [onChange], From 9d292ecc2fc1d725fda30a2c8bdfb6d8411c209a Mon Sep 17 00:00:00 2001 From: Zhongxin Guo Date: Thu, 6 Nov 2025 22:37:26 -0800 Subject: [PATCH 9/9] update --- src/webportal/src/app/job-submission/job-submission-page.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webportal/src/app/job-submission/job-submission-page.jsx b/src/webportal/src/app/job-submission/job-submission-page.jsx index 01caeb84..ea902799 100644 --- a/src/webportal/src/app/job-submission/job-submission-page.jsx +++ b/src/webportal/src/app/job-submission/job-submission-page.jsx @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { useState, useCallback, useEffect, useMemo } from 'react'; +import React, { useState, useCallback, useEffect, useMemo } from 'react'; import { Fabric, Stack, StackItem, Dropdown } from 'office-ui-fabric-react'; import { isNil, isEmpty, get, cloneDeep } from 'lodash'; import PropTypes from 'prop-types';