diff --git a/apps/console/web/src/components/CreateJob.tsx b/apps/console/web/src/components/CreateJob.tsx index f905064ed..330684cc4 100644 --- a/apps/console/web/src/components/CreateJob.tsx +++ b/apps/console/web/src/components/CreateJob.tsx @@ -76,6 +76,13 @@ const STEP_INDEX: Record = { model: 1, template: 2, dataset: 3, se const MIN_CLIENT_MAX_CONCURRENCY = 1; const MAX_CLIENT_MAX_CONCURRENCY = 1024; const MIN_ADAPTIVE_MAX_FACTOR = 1; +const MIN_ADAPTIVE_HEALTHY_WINDOW = 1; +const MIN_ADAPTIVE_ADDITIVE_INCREASE = 1; +const ADAPTIVE_CLIENT_PARAM_FIELDS = new Set([ + 'adaptiveMaxFactor', + 'adaptiveHealthyWindow', + 'adaptiveAdditiveIncrease', +]); const BYTES_PER_MEBIBYTE = 1024 * 1024; const FULL_PARSE_CACHE_MAX_BYTES = 20 * BYTES_PER_MEBIBYTE; @@ -100,6 +107,8 @@ export function CreateJob({ onBack }: CreateJobProps) { const [maxConcurrency, setMaxConcurrency] = useState(''); const [adaptiveConcurrency, setAdaptiveConcurrency] = useState(true); const [adaptiveMaxFactor, setAdaptiveMaxFactor] = useState(''); + const [adaptiveHealthyWindow, setAdaptiveHealthyWindow] = useState(''); + const [adaptiveAdditiveIncrease, setAdaptiveAdditiveIncrease] = useState(''); const [retryMaxRetries, setRetryMaxRetries] = useState(''); const [retryBaseDelay, setRetryBaseDelay] = useState(''); const [retryMaxDelay, setRetryMaxDelay] = useState(''); @@ -404,7 +413,9 @@ export function CreateJob({ onBack }: CreateJobProps) { setParamErrors(prev => ({ ...prev, replicas: err })); }, [replicaLimits, replicas]); - const hasParamErrors = Object.values(paramErrors).some(e => e !== ''); + const hasParamErrors = Object.entries(paramErrors).some(([field, error]) => ( + error !== '' && (adaptiveConcurrency || !ADAPTIVE_CLIENT_PARAM_FIELDS.has(field)) + )); // Assemble extra_body.aibrix.client from the Settings inputs, omitting blanks // so unset fields fall back to metadata-service defaults. Returns undefined @@ -421,7 +432,9 @@ export function CreateJob({ onBack }: CreateJobProps) { maxConcurrency: parseNumber(maxConcurrency), // Only forward the toggle when the user diverged from the default (on). adaptiveConcurrency: adaptiveConcurrency ? undefined : false, - adaptiveMaxFactor: parseNumber(adaptiveMaxFactor), + adaptiveMaxFactor: adaptiveConcurrency ? parseNumber(adaptiveMaxFactor) : undefined, + adaptiveHealthyWindow: adaptiveConcurrency ? parseNumber(adaptiveHealthyWindow) : undefined, + adaptiveAdditiveIncrease: adaptiveConcurrency ? parseNumber(adaptiveAdditiveIncrease) : undefined, retryPolicy: hasRetry ? retry : undefined, }; const hasAny = Object.values(client).some(v => v !== undefined); @@ -1077,7 +1090,24 @@ export function CreateJob({ onBack }: CreateJobProps) { Optional smart-client concurrency and retry controls. Leave blank to use service defaults.

-
+ + +

+ Initial probing grows by at least 25%; after the first decrease, growth uses the Additive Increase step. +

+ +

{MIN_CLIENT_MAX_CONCURRENCY} - {MAX_CLIENT_MAX_CONCURRENCY}

@@ -1101,33 +1131,71 @@ export function CreateJob({ onBack }: CreateJobProps) { handleParamChange('adaptiveMaxFactor', e.target.value, setAdaptiveMaxFactor, MIN_ADAPTIVE_MAX_FACTOR, undefined, false)} placeholder="e.g. 8" - className={`w-full px-4 py-2 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-teal-500/30 focus:border-teal-500 ${ - paramErrors.adaptiveMaxFactor ? 'border-red-300' : 'border-gray-200' + className={`w-full px-4 py-2 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-teal-500/30 focus:border-teal-500 disabled:bg-gray-50 disabled:text-gray-400 disabled:cursor-not-allowed ${ + adaptiveConcurrency && paramErrors.adaptiveMaxFactor ? 'border-red-300' : 'border-gray-200' }`} /> - {paramErrors.adaptiveMaxFactor && ( + {adaptiveConcurrency && paramErrors.adaptiveMaxFactor && (

{paramErrors.adaptiveMaxFactor}

)}
-
- +
+ +

Healthy completions before each increase, ≥ 1

+ handleParamChange( + 'adaptiveHealthyWindow', + e.target.value, + setAdaptiveHealthyWindow, + MIN_ADAPTIVE_HEALTHY_WINDOW, + undefined, + true, + )} + placeholder="e.g. 8" + className={`w-full px-4 py-2 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-teal-500/30 focus:border-teal-500 disabled:bg-gray-50 disabled:text-gray-400 disabled:cursor-not-allowed ${ + adaptiveConcurrency && paramErrors.adaptiveHealthyWindow ? 'border-red-300' : 'border-gray-200' + }`} + /> + {adaptiveConcurrency && paramErrors.adaptiveHealthyWindow && ( +

{paramErrors.adaptiveHealthyWindow}

+ )} +
+ +
+ +

Fixed growth step after the first decrease, ≥ 1

+ handleParamChange( + 'adaptiveAdditiveIncrease', + e.target.value, + setAdaptiveAdditiveIncrease, + MIN_ADAPTIVE_ADDITIVE_INCREASE, + undefined, + true, + )} + placeholder="e.g. 1" + className={`w-full px-4 py-2 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-teal-500/30 focus:border-teal-500 disabled:bg-gray-50 disabled:text-gray-400 disabled:cursor-not-allowed ${ + adaptiveConcurrency && paramErrors.adaptiveAdditiveIncrease ? 'border-red-300' : 'border-gray-200' + }`} + /> + {adaptiveConcurrency && paramErrors.adaptiveAdditiveIncrease && ( +

{paramErrors.adaptiveAdditiveIncrease}

+ )} +
+

Retry Policy

-
+

≥ 0

diff --git a/apps/console/web/src/utils/api.test.ts b/apps/console/web/src/utils/api.test.ts index 41457f2b6..d796fccf5 100644 --- a/apps/console/web/src/utils/api.test.ts +++ b/apps/console/web/src/utils/api.test.ts @@ -132,6 +132,24 @@ describe('api helpers', () => { }); }); + it('serializes adaptive AIMD settings to the job client contract', () => { + expect(camelToSnake({ + client: { + adaptiveConcurrency: true, + adaptiveMaxFactor: 16, + adaptiveHealthyWindow: 8, + adaptiveAdditiveIncrease: 2, + }, + })).toEqual({ + client: { + adaptive_concurrency: true, + adaptive_max_factor: 16, + adaptive_healthy_window: 8, + adaptive_additive_increase: 2, + }, + }); + }); + it('publishes the first jobs page while older pages are still loading', async () => { let resolveSecondPage!: (response: Response) => void; const secondPage = new Promise((resolve) => {