Skip to content

Commit 25d47b1

Browse files
committed
feat(FR-2867): add unified memory accelerator UX in Resource Allocation
1 parent 0177ac9 commit 25d47b1

3 files changed

Lines changed: 233 additions & 2 deletions

File tree

react/src/components/SessionFormItems/ResourceAllocationFormItems.test.ts

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,83 @@ import {
88
ResourcePreset,
99
} from '../../hooks/useResourceLimitAndRemaining';
1010
import { Image } from '../ImageEnvironmentSelectFormItems';
11-
import { getAllocatablePresetNames } from './ResourceAllocationFormItems';
11+
import {
12+
displayUnitToInputSizeUnit,
13+
getAllocatablePresetNames,
14+
getUnifiedAcceleratorValueFromMem,
15+
isUnifiedAcceleratorSlot,
16+
} from './ResourceAllocationFormItems';
17+
18+
describe('displayUnitToInputSizeUnit', () => {
19+
it('maps known binary display units to InputSizeUnit', () => {
20+
expect(displayUnitToInputSizeUnit('GiB')).toBe('g');
21+
expect(displayUnitToInputSizeUnit('MiB')).toBe('m');
22+
expect(displayUnitToInputSizeUnit('TiB')).toBe('t');
23+
expect(displayUnitToInputSizeUnit('KiB')).toBe('k');
24+
expect(displayUnitToInputSizeUnit('PiB')).toBe('p');
25+
expect(displayUnitToInputSizeUnit('EiB')).toBe('e');
26+
});
27+
28+
it('falls back to "g" for unknown or empty values', () => {
29+
expect(displayUnitToInputSizeUnit('foo')).toBe('g');
30+
expect(displayUnitToInputSizeUnit('')).toBe('g');
31+
expect(displayUnitToInputSizeUnit(null)).toBe('g');
32+
expect(displayUnitToInputSizeUnit(undefined)).toBe('g');
33+
});
34+
});
35+
36+
describe('getUnifiedAcceleratorValueFromMem', () => {
37+
const slotsGiB = { 'cuda.unified': { display_unit: 'GiB' } };
38+
const slotsMiB = { 'cuda.unified': { display_unit: 'MiB' } };
39+
const slotsNoMeta = { 'cuda.unified': {} };
40+
41+
it('returns mem converted to the slot display unit (GiB)', () => {
42+
expect(
43+
getUnifiedAcceleratorValueFromMem('8g', 'cuda.unified', slotsGiB),
44+
).toBe(8);
45+
});
46+
47+
it('returns mem converted to the slot display unit (MiB)', () => {
48+
expect(
49+
getUnifiedAcceleratorValueFromMem('8g', 'cuda.unified', slotsMiB),
50+
).toBe(8192);
51+
});
52+
53+
it('falls back to GiB when slot metadata has no display_unit', () => {
54+
expect(
55+
getUnifiedAcceleratorValueFromMem('8g', 'cuda.unified', slotsNoMeta),
56+
).toBe(8);
57+
});
58+
59+
it('returns 0 for undefined or zero mem', () => {
60+
expect(
61+
getUnifiedAcceleratorValueFromMem(undefined, 'cuda.unified', slotsGiB),
62+
).toBe(0);
63+
expect(getUnifiedAcceleratorValueFromMem(0, 'cuda.unified', slotsGiB)).toBe(
64+
0,
65+
);
66+
});
67+
});
68+
69+
describe('isUnifiedAcceleratorSlot', () => {
70+
it('returns true for slot names ending with .unified', () => {
71+
expect(isUnifiedAcceleratorSlot('cuda.unified')).toBe(true);
72+
expect(isUnifiedAcceleratorSlot('rocm.unified')).toBe(true);
73+
});
74+
75+
it('returns false for discrete accelerator slot names', () => {
76+
expect(isUnifiedAcceleratorSlot('cuda.shares')).toBe(false);
77+
expect(isUnifiedAcceleratorSlot('cuda.device')).toBe(false);
78+
expect(isUnifiedAcceleratorSlot('cuda.mem')).toBe(false);
79+
expect(isUnifiedAcceleratorSlot('rocm.device')).toBe(false);
80+
});
81+
82+
it('returns false for nullish or empty input', () => {
83+
expect(isUnifiedAcceleratorSlot(undefined)).toBe(false);
84+
expect(isUnifiedAcceleratorSlot(null)).toBe(false);
85+
expect(isUnifiedAcceleratorSlot('')).toBe(false);
86+
});
87+
});
1288

1389
describe('getAllocatablePresetNames', () => {
1490
const presets: Array<ResourcePreset> = [

react/src/components/SessionFormItems/ResourceAllocationFormItems.tsx

Lines changed: 155 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
addNumberWithUnits,
88
compareNumberWithUnits,
99
convertToBinaryUnit,
10+
InputSizeUnit,
1011
} from '../../helper';
1112
import { useSuspendedBackendaiClient } from '../../hooks';
1213
import { useResourceSlots } from '../../hooks/backendai';
@@ -62,6 +63,58 @@ export const isMinOversMaxValue = (min: number, max: number) => {
6263
return min >= max;
6364
};
6465

66+
/**
67+
* Returns true when the given accelerator slot name represents a unified
68+
* memory architecture, where the accelerator memory and the host memory
69+
* share a single physical pool. Identified by a `.unified` suffix on the
70+
* slot name (e.g. `cuda.unified`).
71+
*/
72+
export const isUnifiedAcceleratorSlot = (slotName?: string | null): boolean => {
73+
return !!slotName && _.endsWith(slotName, '.unified');
74+
};
75+
76+
/**
77+
* Maps a slot's `display_unit` (e.g. `GiB`, `MiB`, `TiB`) to the
78+
* corresponding `InputSizeUnit` accepted by `convertToBinaryUnit`. Falls
79+
* back to `'g'` (GiB) when the display unit is missing or not a recognized
80+
* binary size unit.
81+
*/
82+
export const displayUnitToInputSizeUnit = (
83+
displayUnit?: string | null,
84+
): InputSizeUnit => {
85+
const first = (_.first(displayUnit ?? '') ?? '').toLowerCase();
86+
const validUnits: ReadonlyArray<InputSizeUnit> = [
87+
'k',
88+
'm',
89+
'g',
90+
't',
91+
'p',
92+
'e',
93+
];
94+
const match = validUnits.find((u) => u === first);
95+
return match ?? 'g';
96+
};
97+
98+
/**
99+
* Returns the accelerator-field number derived from the host memory value
100+
* for a unified-memory slot. Reads the slot's `display_unit` from
101+
* `mergedResourceSlots` and converts `mem` to that unit. Falls back to
102+
* `'g'` (GiB) when the display unit is unavailable.
103+
*/
104+
export const getUnifiedAcceleratorValueFromMem = (
105+
mem: string | number | undefined,
106+
slotName: string | undefined,
107+
mergedResourceSlots:
108+
| Record<string, { display_unit?: string } | undefined>
109+
| undefined,
110+
): number => {
111+
const displayUnit = slotName
112+
? mergedResourceSlots?.[slotName]?.display_unit
113+
: undefined;
114+
const targetUnit = displayUnitToInputSizeUnit(displayUnit);
115+
return convertToBinaryUnit(mem || '0g', targetUnit)?.number ?? 0;
116+
};
117+
65118
export interface ResourceAllocationFormValue {
66119
resource: {
67120
cpu: number;
@@ -267,6 +320,28 @@ const ResourceAllocationFormItems: React.FC<
267320
}
268321
};
269322

323+
// Centralized helper that keeps the `resource.accelerator` field in sync
324+
// with `resource.mem` whenever the active accelerator slot is a unified
325+
// one. Call this from every code path that mutates `resource.mem` or
326+
// `resource.acceleratorType` so submit-time form state is consistent.
327+
const syncUnifiedAcceleratorIfNeeded = useEventNotStable(() => {
328+
const activeAcceleratorType = form.getFieldValue([
329+
'resource',
330+
'acceleratorType',
331+
]);
332+
if (!isUnifiedAcceleratorSlot(activeAcceleratorType)) {
333+
return;
334+
}
335+
form.setFieldValue(
336+
['resource', 'accelerator'],
337+
getUnifiedAcceleratorValueFromMem(
338+
form.getFieldValue(['resource', 'mem']),
339+
activeAcceleratorType,
340+
mergedResourceSlots,
341+
),
342+
);
343+
});
344+
270345
const ensureValidAcceleratorType = useEventNotStable(() => {
271346
const currentAcceleratorType = form.getFieldValue([
272347
'resource',
@@ -283,6 +358,9 @@ const ResourceAllocationFormItems: React.FC<
283358
acceleratorType: nextAcceleratorType || currentAcceleratorType,
284359
},
285360
});
361+
// The accelerator type may have changed; mirror `mem` into the
362+
// accelerator field if the resolved type is a unified slot.
363+
syncUnifiedAcceleratorIfNeeded();
286364
});
287365

288366
const updateResourceFieldsBasedOnImage = useEventNotStable(
@@ -388,6 +466,9 @@ const ResourceAllocationFormItems: React.FC<
388466
if (form.getFieldValue('enabledAutomaticShmem')) {
389467
runShmemAutomationRule(form.getFieldValue(['resource', 'mem']) || '0g');
390468
}
469+
// mem and/or acceleratorType may have changed above; keep the
470+
// accelerator field consistent for unified slots.
471+
syncUnifiedAcceleratorIfNeeded();
391472
form
392473
.validateFields(['resource'], {
393474
recursive: true,
@@ -445,6 +526,9 @@ const ResourceAllocationFormItems: React.FC<
445526
if (!hasPresetShmem) {
446527
runShmemAutomationRule(mem || '0g');
447528
}
529+
// mem and/or acceleratorType may have changed above; keep the
530+
// accelerator field consistent for unified slots.
531+
syncUnifiedAcceleratorIfNeeded();
448532

449533
form
450534
.validateFields(['resource'], {
@@ -864,6 +948,13 @@ const ResourceAllocationFormItems: React.FC<
864948
) {
865949
runShmemAutomationRule(M_plus_S || '0g');
866950
}
951+
// When the active accelerator slot is a
952+
// unified-memory slot, the accelerator field
953+
// shares a single pool with host memory.
954+
// Derive the accelerator value from `mem` at
955+
// the source of change so submit-time form
956+
// state is always consistent.
957+
syncUnifiedAcceleratorIfNeeded();
867958
}}
868959
/>
869960
</Form.Item>
@@ -921,6 +1012,10 @@ const ResourceAllocationFormItems: React.FC<
9211012
currentAcceleratorType as keyof typeof resourceSlots
9221013
] === 'unique';
9231014

1015+
const isUnifiedType = isUnifiedAcceleratorSlot(
1016+
currentAcceleratorType,
1017+
);
1018+
9241019
const isSingleCluster =
9251020
form.getFieldValue('cluster_size') < 2;
9261021
const hasQuantumSize = _.isNumber(
@@ -965,9 +1060,15 @@ const ResourceAllocationFormItems: React.FC<
9651060
/>
9661061
),
9671062
}}
1063+
extra={
1064+
isUnifiedType
1065+
? t('session.launcher.UnifiedAcceleratorMemoryNote')
1066+
: undefined
1067+
}
9681068
dependencies={[
9691069
['resource', 'acceleratorType'],
9701070
'cluster_size',
1071+
['resource', 'mem'],
9711072
]}
9721073
rules={[
9731074
{
@@ -1139,7 +1240,8 @@ const ResourceAllocationFormItems: React.FC<
11391240
},
11401241
}}
11411242
disabled={
1142-
supportedAcceleratorTypesInRGByImage?.length === 0
1243+
supportedAcceleratorTypesInRGByImage?.length ===
1244+
0 || isUnifiedType
11431245
}
11441246
min={0}
11451247
max={
@@ -1193,6 +1295,58 @@ const ResourceAllocationFormItems: React.FC<
11931295
};
11941296
},
11951297
)}
1298+
onChange={(nextType: string) => {
1299+
// Changing the slot type mutates the
1300+
// active allocation; the previously
1301+
// selected preset no longer matches.
1302+
form.setFieldValue(
1303+
'allocationPreset',
1304+
'custom',
1305+
);
1306+
// Keep the accelerator field consistent
1307+
// at the moment the slot type changes,
1308+
// rather than relying on a watcher
1309+
// effect that runs after render.
1310+
if (isUnifiedAcceleratorSlot(nextType)) {
1311+
// Switching INTO a unified slot:
1312+
// mirror the current `mem` value
1313+
// converted to the slot's display
1314+
// unit. Write acceleratorType first
1315+
// so the shared helper sees the new
1316+
// active slot when it reads from the
1317+
// form.
1318+
form.setFieldValue(
1319+
['resource', 'acceleratorType'],
1320+
nextType,
1321+
);
1322+
syncUnifiedAcceleratorIfNeeded();
1323+
} else if (
1324+
isUnifiedAcceleratorSlot(
1325+
currentAcceleratorType,
1326+
)
1327+
) {
1328+
// Switching OUT of a unified slot
1329+
// into a discrete one: reset to the
1330+
// discrete slot's min so the stale
1331+
// mirrored GiB value from unified
1332+
// mode does not bleed through as a
1333+
// device count.
1334+
//
1335+
// Discrete-to-discrete switches are
1336+
// intentionally NOT reset here:
1337+
// ensureValidAcceleratorType clamps
1338+
// the existing value if it falls
1339+
// outside the new slot's range, so
1340+
// the user's current allocation is
1341+
// preserved across discrete slot
1342+
// changes.
1343+
form.setFieldValue(
1344+
['resource', 'accelerator'],
1345+
resourceLimits.accelerators[nextType]
1346+
?.min ?? 0,
1347+
);
1348+
}
1349+
}}
11961350
/>
11971351
</Form.Item>
11981352
) : undefined,

resources/i18n/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2882,6 +2882,7 @@
28822882
"TensorBoardPrepared": "TensorBoard app prepared.",
28832883
"TitleSession": "Session (Backend.AI)",
28842884
"TotalAllocation": "Total Allocation",
2885+
"UnifiedAcceleratorMemoryNote": "Accelerator memory is shared with host memory on this device and cannot be set separately.",
28852886
"UserResourceLimit": "User Resource Limit",
28862887
"UsingBootstrapScriptInfo": "Bootstrap script is included.",
28872888
"Version": "Version",

0 commit comments

Comments
 (0)