Skip to content

Commit 7ea5f0e

Browse files
committed
feat(FR-1448): allow user to select multi agents when creating multi-node session (#4309)
resolves #4256 (FR-1448) This PR adds support for selecting multiple agents when launching a session, available for [Backend.AI](http://Backend.AI) manager version 25.15.0 and above. Key changes: - Modified `AgentSelect` component to handle multiple agent selection - Updated agent selection logic in `ResourceAllocationFormItems` to support multi-agent mode - Added special handling for the "auto" option in multi-select mode - Implemented proper agent_list generation in session creation payload - Added 'multi-agents' feature detection for manager version 25.16.0+ - Changed agent default value from string to array format ['auto'] - Removed restrictions that previously disabled cluster settings when a specific agent was selected **for testing, using test server which supports multi node session(like** **dogbowl)** **Prior to version 25.16.0, it must behave identically to the existing version.** how to test: - single mode - Please verify that the agent_list is sent as a string[] when using multi-agents. - multi mode - Please verify that the agent_list is being passed as a string array when using multi-agents. ![CleanShot 2025-09-19 at 17.15.50@2x.png](https://app.graphite.dev/user-attachments/assets/e078b5c2-c9e7-4bb0-b74a-3654f9b22503.png) ![CleanShot 2025-09-19 at 17.15.30@2x.png](https://app.graphite.dev/user-attachments/assets/adcbb2fb-388c-4f98-9d45-161b20e69550.png) ![CleanShot 2025-11-13 at 10.39.50@2x.png](https://app.graphite.com/user-attachments/assets/6dcd76fa-fcc7-4f8c-bfd0-bca9ba34c1a0.png) **Checklist:** - [ ] Documentation - [x] Minium required manager version: 25.16.0 - [ ] Specific setting for review - [ ] Minimum requirements to check during review - [ ] Test case(s) to demonstrate the difference of before/after
1 parent 71d1e09 commit 7ea5f0e

28 files changed

+117
-42
lines changed

react/src/components/AgentSelect.tsx

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ import { AgentSelectQuery } from '../__generated__/AgentSelectQuery.graphql';
22
import { useBAIPaginationOptionState } from '../hooks/reactPaginationQueryOptions';
33
import ResourceNumber from './ResourceNumber';
44
import { useControllableValue } from 'ahooks';
5-
import { Select, SelectProps } from 'antd';
5+
import { Select, SelectProps, theme } from 'antd';
66
import { filterOutEmpty, BAIFlex, mergeFilterValues } from 'backend.ai-ui';
77
import _ from 'lodash';
88
import React, { useDeferredValue, useState } from 'react';
99
import { useTranslation } from 'react-i18next';
1010
import { graphql, useLazyLoadQuery } from 'react-relay';
1111

12-
interface Props extends SelectProps {
12+
interface Props extends Omit<SelectProps, 'options'> {
1313
autoSelectDefault?: boolean;
1414
fetchKey?: string;
1515
resourceGroup?: string | null;
@@ -24,6 +24,7 @@ const AgentSelect: React.FC<Props> = ({
2424
const [value, setValue] = useControllableValue(selectProps);
2525
const [searchStr, setSearchStr] = useState<string | undefined>(undefined);
2626
const deferredSearchStr = useDeferredValue(searchStr);
27+
const { token } = theme.useToken();
2728

2829
const { baiPaginationOption } = useBAIPaginationOptionState({
2930
current: 1,
@@ -94,7 +95,13 @@ const AgentSelect: React.FC<Props> = ({
9495
.value();
9596
return {
9697
label: (
97-
<BAIFlex direction="row" justify="between">
98+
<BAIFlex
99+
direction="row"
100+
justify="between"
101+
style={{
102+
marginRight: token.marginXS,
103+
}}
104+
>
98105
{agent?.id}
99106
<BAIFlex direction="row" gap={'xxs'}>
100107
{_.map(remainingSlotsInfo, (slot, key) => {
@@ -122,19 +129,33 @@ const AgentSelect: React.FC<Props> = ({
122129
: undefined;
123130
return (
124131
<Select
125-
onChange={(value, option) => {
126-
setValue(value, option);
127-
}}
128132
loading={searchStr !== deferredSearchStr}
129133
filterOption={false}
130134
showSearch
131135
searchValue={searchStr}
132136
onSearch={(v) => {
133137
setSearchStr(v);
134138
}}
139+
options={filterOutEmpty([autoSelectIfMatch, ...agentOptions])}
140+
//override props.onChange and props.value, it is handled by useControllableValue
135141
{...selectProps}
142+
onChange={(value: unknown, option) => {
143+
if (
144+
selectProps.mode === 'multiple' &&
145+
_.isArray(value) &&
146+
_.isArray(option)
147+
) {
148+
if (_.last(value) === 'auto' || value.length === 0) {
149+
value = ['auto'];
150+
option = _.last(option);
151+
} else if (value[0] === 'auto' && value.length > 1) {
152+
value = value.slice(1);
153+
option = option.slice(1);
154+
}
155+
}
156+
setValue(value, option);
157+
}}
136158
value={value}
137-
options={filterOutEmpty([autoSelectIfMatch, ...agentOptions])}
138159
/>
139160
);
140161
};

react/src/components/AutoRefreshSwitch.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { Switch, SwitchProps, Typography } from 'antd';
2-
import { BAIFlex } from 'backend.ai-ui';
2+
import { BAIFlex, useInterval } from 'backend.ai-ui';
33
import React from 'react';
4-
import { useInterval } from 'src/hooks/useIntervalValue';
54

65
const { Text } = Typography;
76

react/src/components/SessionFormItems/ResourceAllocationFormItems.tsx

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export const RESOURCE_ALLOCATION_INITIAL_FORM_VALUES: DeepPartial<ResourceAlloca
4646
cluster_mode: 'single-node',
4747
cluster_size: 1,
4848
enabledAutomaticShmem: true,
49-
agent: 'auto',
49+
agent: ['auto'],
5050
};
5151

5252
export const isMinOversMaxValue = (min: number, max: number) => {
@@ -67,7 +67,7 @@ export interface ResourceAllocationFormValue {
6767
cluster_size: number;
6868
enabledAutomaticShmem: boolean;
6969
allocationPreset?: string;
70-
agent?: string;
70+
agent?: string[] | string;
7171
}
7272

7373
export type MergedResourceAllocationFormValue = ResourceAllocationFormValue &
@@ -95,6 +95,7 @@ const ResourceAllocationFormItems: React.FC<
9595
const { token } = theme.useToken();
9696

9797
const baiClient = useSuspendedBackendaiClient();
98+
const supportMultiAgents = baiClient.supports('multi-agents');
9899

99100
const [{ keypairResourcePolicy, sessionLimitAndRemaining }] =
100101
useCurrentKeyPairResourcePolicyLazyLoadQuery();
@@ -1225,22 +1226,40 @@ const ResourceAllocationFormItems: React.FC<
12251226
<Form.Item
12261227
label={t('session.launcher.SelectAgent')}
12271228
required
1228-
tooltip={<Trans i18nKey={'session.launcher.DescSelectAgent'} />}
1229+
tooltip={
1230+
<Trans
1231+
i18nKey={
1232+
supportMultiAgents
1233+
? 'session.launcher.DescSelectAgent'
1234+
: 'session.launcher.DescSelectAgentBefore2516'
1235+
}
1236+
/>
1237+
}
12291238
>
12301239
<BAIFlex gap={'xs'}>
12311240
<Suspense>
12321241
<Form.Item required noStyle style={{ flex: 1 }} name="agent">
12331242
<AgentSelect
12341243
resourceGroup={currentResourceGroupInForm}
12351244
fetchKey={agentFetchKey}
1245+
mode={supportMultiAgents ? 'multiple' : undefined}
1246+
labelRender={
1247+
supportMultiAgents
1248+
? ({ label, value }) => {
1249+
return value === 'auto' ? label : value;
1250+
}
1251+
: undefined
1252+
}
12361253
onChange={(value) => {
1237-
if (value !== 'auto') {
1254+
if (
1255+
!supportMultiAgents &&
1256+
!_.isEqual(_.castArray(value), ['auto'])
1257+
) {
12381258
form.setFieldsValue({
12391259
cluster_mode: 'single-node',
12401260
cluster_size: 1,
12411261
});
12421262
}
1243-
// TODO: set cluster mode to single node and cluster size to 1 when agent value is not "auto"
12441263
}}
12451264
></AgentSelect>
12461265
</Form.Item>
@@ -1286,7 +1305,12 @@ const ResourceAllocationFormItems: React.FC<
12861305
onChange={() => {
12871306
form.validateFields().catch(() => {});
12881307
}}
1289-
disabled={getFieldValue('agent') !== 'auto'}
1308+
disabled={
1309+
!supportMultiAgents &&
1310+
!_.isEqual(_.castArray(getFieldValue('agent')), [
1311+
'auto',
1312+
])
1313+
}
12901314
>
12911315
<Radio.Button value="single-node">
12921316
{t('session.launcher.SingleNode')}
@@ -1351,7 +1375,11 @@ const ResourceAllocationFormItems: React.FC<
13511375
}
13521376
disabled={
13531377
derivedClusterSizeMaxLimit === 1 ||
1354-
getFieldValue('agent') !== 'auto'
1378+
(!supportMultiAgents &&
1379+
!_.isEqual(
1380+
_.castArray(getFieldValue('agent')),
1381+
['auto'],
1382+
))
13551383
}
13561384
sliderProps={{
13571385
marks: {

react/src/components/SessionLauncherPreview.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -523,7 +523,7 @@ const SessionLauncherPreview: React.FC<{
523523
{baiClient.supports('agent-select') &&
524524
!baiClient?._config?.hideAgents && (
525525
<Descriptions.Item label={t('session.launcher.AgentNode')}>
526-
{form.getFieldValue('agent') ||
526+
{_.castArray(form.getFieldValue('agent')).join(', ') ||
527527
t('session.launcher.AutoSelect')}
528528
</Descriptions.Item>
529529
)}

react/src/hooks/useStartSession.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -255,12 +255,14 @@ export const useStartSession = () => {
255255
// Agent selection (optional)
256256
...(baiClient.supports('agent-select') &&
257257
!baiClient?._config?.hideAgents &&
258-
values.agent !== 'auto'
258+
values.agent !== undefined &&
259+
!_.isEqual(values.agent, ['auto'])
259260
? {
260261
// Filter out undefined values
261-
agent_list: [values.agent].filter(
262-
(agent): agent is string => !!agent,
263-
),
262+
agent_list: _.chain(values.agent)
263+
.castArray()
264+
.filter((agent): agent is string => !!agent)
265+
.value(),
264266
}
265267
: undefined),
266268
},

react/src/pages/SessionLauncherPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1276,7 +1276,7 @@ const SessionLauncherPage = () => {
12761276
command: undefined,
12771277
scheduleDate: undefined,
12781278
},
1279-
agent: 'auto', // Add the missing 'agent' property
1279+
agent: ['auto'], // Add the missing 'agent' property
12801280
} as SessionLauncherFormData,
12811281
formValue,
12821282
);

resources/i18n/de.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1470,7 +1470,8 @@
14701470
"DescMemory": " <p> Der Computerspeicher ist ein temporärer Speicherbereich. </p> <p> Es enthält die Daten und Anweisungen, die die Central Processing Unit (CPU) benötigt. </p> <p> Wenn Sie eine GPU in einem Machine Learning-Workload verwenden, müssen Sie dem Arbeitsspeicher mindestens den doppelten Arbeitsspeicher der GPU zuweisen. Andernfalls erhöht sich die Leerlaufzeit der GPU, was zu einer Leistungseinbuße führt. </p>",
14711471
"DescMultiNode": " <p> Beim Ausführen einer Sitzung werden ein verwalteter Knoten und ein oder mehrere Worker-Knoten auf mehrere physische Knoten oder virtuelle Maschinen aufgeteilt. </p>",
14721472
"DescOpenMPOptimization": "<p>Dieser Wert legt die Anzahl der Threads fest, die für parallele Regionen verwendet werden sollen, indem er den Anfangswert der internen Steuervariablen nthreads-var festlegt.</p><p>Backend.AI setzt diesen Wert gleich der Anzahl der CPU-Kerne, was zur Folge hat, dass typische High-Performance-Computing-Workloads beschleunigt werden. Bei einigen Multi-Thread-Workloads werden jedoch mehrere Prozesse, die OpenMP verwenden, gleichzeitig verwendet, was zu einer abnormal großen Anzahl von Threads und einer erheblichen Leistungsverschlechterung führt. In diesem Fall stellen Sie diesen Wert bitte auf 1 oder 2 ein.</p>>",
1473-
"DescSelectAgent": "Die auf der rechten Seite des Agenten angezeigten Ressourcen stellen die tatsächliche Menge der verfügbaren Ressourcen dar. \nDerzeit ist die Agentenauswahl nur in einer Single-Node-Single-Container-Umgebung verfügbar. \nDie Standardeinstellung ist die Agentenzuteilung durch den Scheduler.",
1473+
"DescSelectAgent": "Die rechts neben einem Agenten angezeigten Ressourcen geben die tatsächlich verfügbaren Mengen an. Ausgewählte Agenten stellen die Kandidaten für eine Zuweisung dar; auch wenn mehrere ausgewählt sind, kann nur einem Agenten zugewiesen werden. Die Standardeinstellung ist die automatische Zuweisung durch den Scheduler.",
1474+
"DescSelectAgentBefore2516": "Die rechts beim Agenten angezeigten Ressourcen entsprechen der tatsächlich verfügbaren Menge. Derzeit ist die Agentenauswahl nur in einer Einzelknoten-/Einzelcontainer-Umgebung verfügbar. Standardmäßig erfolgt die Agentenzuweisung durch den Scheduler.",
14741475
"DescSession": " <p> Eine Sitzung ist eine Einheit einer Rechenumgebung, die gemäß einer bestimmten Umgebung und Ressourcen erstellt wird. </p> <p> Wenn dieser Wert auf einen Wert größer als 1 gesetzt ist, werden mehrere Sitzungen erstellt, die dem oben genannten Ressourcensatz entsprechen. </p> <p> Wenn nicht genügend Ressourcen verfügbar sind, werden Anforderungen zum Erstellen von Sitzungen, die nicht erstellt werden können, in die Warteschlange gestellt. </p>",
14751476
"DescSetEnv": " <p> Wenn Sie eine Standardumgebungsvariable im Container setzen und auch eine Umgebungsvariable mit demselben Namen auf der Backend.AI setzen, hat der auf der Backend.AI festgelegte Wert Vorrang. </p> <p style='color:var(--paper-red-400);'> Jeder Eingabewert wird nach dem Start des Sitzungsstarters initialisiert. <br /> Außerdem werden diese Umgebungen nur übergeben, wenn sowohl die Variable als auch der Wert nicht leer sind. </p>",
14761477
"DescSetPreOpenPort": "Preopen Ports\" bezieht sich auf einen bestimmten <span style='color:var(--paper-red-400);'>internen Container-Port</span>, der eingehende Verbindungen von externen Geräten oder Computern auf einem Netzwerkgerät oder Computer erlaubt.",

resources/i18n/el.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1469,7 +1469,8 @@
14691469
"DescMemory": " <p> Η μνήμη του υπολογιστή είναι μια προσωρινή περιοχή αποθήκευσης. </p> <p> Διατηρεί τα δεδομένα και τις οδηγίες που χρειάζεται η κεντρική μονάδα επεξεργασίας (CPU). </p> <p> Όταν χρησιμοποιείτε GPU σε φόρτο εργασίας μηχανικής εκμάθησης, πρέπει να εκχωρήσετε τουλάχιστον δύο φορές τη μνήμη της GPU στη μνήμη. Διαφορετικά, ο χρόνος αδράνειας της GPU θα αυξηθεί, οδηγώντας σε ποινή απόδοσης. </p>",
14701470
"DescMultiNode": " <p> Κατά την εκτέλεση μιας περιόδου σύνδεσης, ένας διαχειριζόμενος κόμβος και ένας ή περισσότεροι κόμβοι εργαζομένων χωρίζονται σε πολλούς φυσικούς κόμβους ή εικονικές μηχανές. </p>",
14711471
"DescOpenMPOptimization": "<p>Αυτή η τιμή καθορίζει τον αριθμό των νημάτων που θα χρησιμοποιηθούν για τις παράλληλες περιοχές, θέτοντας την αρχική τιμή της εσωτερικής μεταβλητής ελέγχου nthreads-var.</p><p>Το Backend.AI θέτει αυτή την τιμή ίση με τον αριθμό των πυρήνων της CPU, γεγονός που έχει ως αποτέλεσμα την επιτάχυνση τυπικών υπολογιστικών φόρτων εργασίας υψηλών επιδόσεων. Ωστόσο, για ορισμένα φορτία εργασίας με πολλά νήματα, χρησιμοποιούνται ταυτόχρονα πολλές διεργασίες που χρησιμοποιούν OpenMP, με αποτέλεσμα να δημιουργείται ασυνήθιστα μεγάλος αριθμός νημάτων και σημαντική υποβάθμιση των επιδόσεων. Σε αυτή την περίπτωση, παρακαλούμε προσαρμόστε αυτή την τιμή σε 1 ή 2.</p>",
1472-
"DescSelectAgent": "Οι πόροι που εμφανίζονται στη δεξιά πλευρά του πράκτορα αντιπροσωπεύουν την πραγματική ποσότητα των διαθέσιμων πόρων. \nΠρος το παρόν, η επιλογή πράκτορα είναι διαθέσιμη μόνο σε περιβάλλον ενός κοντέινερ ενός κόμβου. \nΗ προεπιλεγμένη ρύθμιση είναι η κατανομή πράκτορα από τον προγραμματιστή.",
1472+
"DescSelectAgent": "Οι πόροι στα δεξιά του agent δείχνουν την πραγματικά διαθέσιμη ποσότητα. Οι επιλεγμένοι agent αντιπροσωπεύουν υποψήφιους προς ανάθεση — ακόμα κι αν επιλεγούν πολλοί, η ανάθεση μπορεί να γίνει μόνο σε έναν agent. Η προεπιλεγμένη ρύθμιση είναι η αυτόματη ανάθεση από τον scheduler.",
1473+
"DescSelectAgentBefore2516": "Οι πόροι που εμφανίζονται στη δεξιά πλευρά του agent αντιπροσωπεύουν την πραγματική ποσότητα διαθέσιμων πόρων. Προς το παρόν, η επιλογή agent είναι διαθέσιμη μόνο σε περιβάλλον με έναν κόμβο και ένα κοντέινερ. Η προεπιλεγμένη ρύθμιση είναι η ανάθεση των agent από τον scheduler.",
14731474
"DescSession": " <p> Η περίοδος σύνδεσης είναι μια μονάδα υπολογιστικού περιβάλλοντος που δημιουργείται σύμφωνα με ένα καθορισμένο περιβάλλον και πόρους. </p> <p> Εάν αυτή η τιμή έχει οριστεί σε μια τιμή μεγαλύτερη από 1, δημιουργούνται πολλές συνεδρίες που αντιστοιχούν στον παραπάνω πόρο. </p> <p> Εάν δεν υπάρχουν αρκετοί διαθέσιμοι πόροι, τα αιτήματα για δημιουργία περιόδων σύνδεσης που δεν μπορούν να δημιουργηθούν τίθενται στην ουρά αναμονής. </p>",
14741475
"DescSetEnv": " <p> Εάν ορίσετε μια προεπιλεγμένη μεταβλητή περιβάλλοντος στο κοντέινερ και επίσης ορίσετε μια μεταβλητή περιβάλλοντος με το ίδιο όνομα στο Backend.AI, υπερισχύει η τιμή που έχει οριστεί στο Backend.AI. </p> <p style='color:var(--paper-red-400);'> Κάθε τιμή εισόδου θα αρχικοποιηθεί μετά την έναρξη της εκκίνησης περιόδου λειτουργίας. <br /> Επίσης, αυτά τα περιβάλλοντα θα περάσουν μόνο αν και η μεταβλητή και η τιμή δεν είναι κενά. </p>",
14751476
"DescSetPreOpenPort": "Οι \"Προανοιχτές θύρες\" αναφέρονται σε μια συγκεκριμένη <span style='color:var(--paper-red-400);'>εσωτερική θύρα εμπορευματοκιβωτίου</span> που επιτρέπει εισερχόμενες συνδέσεις από εξωτερικές συσκευές ή υπολογιστές σε μια συσκευή ή έναν υπολογιστή δικτύου.",

0 commit comments

Comments
 (0)