From 84a6c649c0ea49cce3feebb3cbd78f2972edc204 Mon Sep 17 00:00:00 2001 From: jialin Date: Thu, 24 Sep 2026 16:41:49 +0800 Subject: [PATCH] fix(llmodels): keep the scaling baseline in sync with list replica actions The inline replica edit and start/stop sent only `replicas`, leaving `baseline_replicas` stale, so the scheduler reconciled the count straight back and the action silently did nothing. Start also fell back to a hardcoded 1 instead of the declared baseline. Refs #6257 --- src/pages/llmodels/components/table-list.tsx | 44 ++++++++++++++++++-- src/pages/llmodels/config/types.ts | 4 ++ 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/src/pages/llmodels/components/table-list.tsx b/src/pages/llmodels/components/table-list.tsx index 826d04f8c..39894629c 100644 --- a/src/pages/llmodels/components/table-list.tsx +++ b/src/pages/llmodels/components/table-list.tsx @@ -111,6 +111,40 @@ const getFormattedData = (record: any, extraData = {}) => ({ } }); +// While scheduled scaling is on, `replicas` is the live count the scheduler +// writes and `baseline_replicas` is the idle count the user actually declared; +// the deploy form keeps the two equal (see `forms/index.tsx`). A list action +// that moved only `replicas` left the baseline behind, so the next reconcile +// drove the count straight back to it and the action looked like it had done +// nothing at all. +const getReplicasUpdate = (record: ListItem, replicas: number) => { + const schedule = record.scaling_schedule; + if (!schedule?.enabled) { + return { replicas }; + } + return { + replicas, + scaling_schedule: { ...schedule, baseline_replicas: replicas } + }; +}; + +// Start is only offered on a row already at `replicas: 0`, so the fallback is +// what actually decides the count — the row's own value can never be anything +// but the hardcoded 1. Under a schedule the baseline the user declared is the +// better answer. A baseline of 0 is deliberately not one of those: stopping +// from this list writes exactly that, and honouring it would make the next +// start a no-op. Whether a zero baseline should mean something else is the +// open question in gpustack/gpustack#6257. +const getStartReplicas = (record: ListItem) => { + if (record.replicas) { + return record.replicas; + } + const baseline = record.scaling_schedule?.enabled + ? record.scaling_schedule.baseline_replicas + : null; + return baseline || 1; +}; + const Models: React.FC = ({ handleNameChange, handleSearch, @@ -208,7 +242,9 @@ const Models: React.FC = ({ const handleOnCell = useMemoizedFn(async (record: any, extra: any) => { try { - await updateModel(getFormattedData(record, { replicas: extra.newValue })); + await updateModel( + getFormattedData(record, getReplicasUpdate(record, extra.newValue)) + ); message.success(intl.formatMessage({ id: 'common.message.success' })); if (extra.newValue > extra.oldValue) { updateExpandedRowKeys([record.id, ...expandedRowKeys]); @@ -229,11 +265,13 @@ const Models: React.FC = ({ ); const handleStartModel = async (row: ListItem) => { - await updateModel(getFormattedData(row, { replicas: row.replicas || 1 })); + await updateModel( + getFormattedData(row, getReplicasUpdate(row, getStartReplicas(row))) + ); }; const handleStopModel = async (row: ListItem) => { - await updateModel(getFormattedData(row, { replicas: 0 })); + await updateModel(getFormattedData(row, getReplicasUpdate(row, 0))); removeExpandedRowKey([row.id]); }; diff --git a/src/pages/llmodels/config/types.ts b/src/pages/llmodels/config/types.ts index c02d8232e..d79bd0d7d 100644 --- a/src/pages/llmodels/config/types.ts +++ b/src/pages/llmodels/config/types.ts @@ -51,6 +51,10 @@ export interface ListItem { }; gpu_type_selector?: GPUTypeSelector | null; worker_selector?: object; + // Present on the row so a list action can carry the schedule back untouched + // apart from its baseline. While it is enabled, `replicas` above is the + // scheduler-driven live count, not the user's declared one. + scaling_schedule?: ScalingSchedule | null; // --- PD: user intent --- roles?: RoleSpec[] | null;