[ML] Add a new memory usage by job and by model view (#149419)

Adds a new Memory usage page to the ML app.
This contains the page which was originally called Nodes, under the
Model Management section, and introduces a new tree map chart to display
memory usage of jobs and trained models.


![image](https://user-images.githubusercontent.com/22172091/217040806-bf1b2d51-32ce-4801-8d8f-e27c3d7a9f62.png)


If kibana is running in a serverless environment, the Memory usage page
will only show the overall memory usage chart. There will be no
reference made to "nodes".

**Refactoring**
Organises related server side code under a `model_management` section.
Moves routes to a new `model_management` file.
Adds a new `isServerless` function to the client side to spoof
information we should son get from kibana to tell whether we are running
in a serverless environment.

---------

Co-authored-by: István Zoltán Szabó <istvan.szabo@elastic.co>
This commit is contained in:
James Gowdy 2023-02-07 10:53:36 +00:00 committed by GitHub
parent c25da5920d
commit ec192acac2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
94 changed files with 1215 additions and 586 deletions

View file

@ -15,7 +15,7 @@ export const ML_PAGES = {
DATA_FRAME_ANALYTICS_SOURCE_SELECTION: 'data_frame_analytics/source_selection',
DATA_FRAME_ANALYTICS_CREATE_JOB: 'data_frame_analytics/new_job',
TRAINED_MODELS_MANAGE: 'trained_models',
TRAINED_MODELS_NODES: 'trained_models/nodes',
MEMORY_USAGE: 'memory_usage',
DATA_FRAME_ANALYTICS_EXPLORATION: 'data_frame_analytics/exploration',
DATA_FRAME_ANALYTICS_MAP: 'data_frame_analytics/map',
/**

View file

@ -203,7 +203,7 @@ export interface TrainedModelsQueryState {
modelId?: string;
}
export interface TrainedModelsNodesQueryState {
export interface MemoryUsageNodesQueryState {
nodeId?: string;
}
@ -276,7 +276,7 @@ export type MlLocatorState =
| MlGenericUrlState
| NotificationsUrlState
| TrainedModelsUrlState
| TrainedModelsNodesUrlState;
| MemoryUsageUrlState;
export type MlLocatorParams = MlLocatorState & SerializableRecord;
@ -287,9 +287,9 @@ export type TrainedModelsUrlState = MLPageState<
TrainedModelsQueryState | undefined
>;
export type TrainedModelsNodesUrlState = MLPageState<
typeof ML_PAGES.TRAINED_MODELS_NODES,
TrainedModelsNodesQueryState | undefined
export type MemoryUsageUrlState = MLPageState<
typeof ML_PAGES.MEMORY_USAGE,
MemoryUsageNodesQueryState | undefined
>;
export interface NotificationsQueryState {

View file

@ -9,6 +9,7 @@ import type { DataFrameAnalyticsConfig } from './data_frame_analytics';
import type { FeatureImportanceBaseline, TotalFeatureImportance } from './feature_importance';
import type { XOR } from './common';
import type { DeploymentState, TrainedModelType } from '../constants/trained_models';
import type { MlSavedObjectType } from './saved_objects';
export interface IngestStats {
count: number;
@ -236,3 +237,47 @@ export interface NodesOverviewResponse {
_nodes: { total: number; failed: number; successful: number };
nodes: NodeDeploymentStatsResponse[];
}
export interface MemoryUsageInfo {
id: string;
type: MlSavedObjectType;
size: number;
nodeNames: string[];
}
export interface MemoryStatsResponse {
_nodes: { total: number; failed: number; successful: number };
cluster_name: string;
nodes: Record<
string,
{
jvm: {
heap_max_in_bytes: number;
java_inference_in_bytes: number;
java_inference_max_in_bytes: number;
};
mem: {
adjusted_total_in_bytes: number;
total_in_bytes: number;
ml: {
data_frame_analytics_in_bytes: number;
native_code_overhead_in_bytes: number;
max_in_bytes: number;
anomaly_detectors_in_bytes: number;
native_inference_in_bytes: number;
};
};
transport_address: string;
roles: string[];
name: string;
attributes: Record<`${'ml.'}${string}`, string>;
ephemeral_id: string;
}
>;
}
// @ts-expect-error TrainedModelDeploymentStatsResponse missing properties from MlTrainedModelDeploymentStats
export interface TrainedModelStatsResponse extends estypes.MlTrainedModelStats {
deployment_stats?: Omit<TrainedModelDeploymentStatsResponse, 'model_id'>;
model_size_stats?: TrainedModelModelSizeStats;
}

View file

@ -45,6 +45,12 @@ interface AppProps {
const localStorage = new Storage(window.localStorage);
// temporary function to hardcode the serverless state
// this will be replaced by the true serverless information from kibana
export function isServerless() {
return false;
}
/**
* Provides global services available across the entire ML app.
*/
@ -54,6 +60,7 @@ export function getMlGlobalServices(httpStart: HttpStart, usageCollection?: Usag
httpService,
mlApiServices: mlApiServicesProvider(httpService),
mlUsageCollection: mlUsageCollectionProvider(usageCollection),
isServerless,
};
}

View file

@ -169,7 +169,7 @@ const CommonPageWrapper: FC<CommonPageWrapperProps> = React.memo(({ pageDeps, ro
{routeList.map((route) => {
return (
<Route
key={route.id}
key={route.path}
path={route.path}
exact
render={(props) => {

View file

@ -96,6 +96,15 @@ export function useSideNavItems(activeRoute: MlRoute | undefined) {
disabled: disableLinks,
testSubj: 'mlMainTab notifications',
},
{
id: 'memory_usage',
pathId: ML_PAGES.MEMORY_USAGE,
name: i18n.translate('xpack.ml.navMenu.memoryUsageText', {
defaultMessage: 'Memory Usage',
}),
disabled: disableLinks || !canViewMlNodes,
testSubj: 'mlMainTab nodesOverview',
},
],
},
{
@ -196,15 +205,6 @@ export function useSideNavItems(activeRoute: MlRoute | undefined) {
disabled: disableLinks,
testSubj: 'mlMainTab trainedModels',
},
{
id: 'nodes_overview',
pathId: ML_PAGES.TRAINED_MODELS_NODES,
name: i18n.translate('xpack.ml.navMenu.nodesOverviewText', {
defaultMessage: 'Nodes',
}),
disabled: disableLinks || !canViewMlNodes,
testSubj: 'mlMainTab nodesOverview',
},
],
},
{

View file

@ -0,0 +1,14 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useMemo } from 'react';
import { useMlKibana } from './kibana_context';
export const useIsServerless = () => {
const isServerless = useMlKibana().services.mlServices.isServerless;
return useMemo(() => isServerless(), [isServerless]);
};

View file

@ -27,7 +27,7 @@ import { BUILT_IN_MODEL_TAG } from '../../../../../../common/constants/data_fram
import { useTrainedModelsApiService } from '../../../../services/ml_api_service/trained_models';
import { GetDataFrameAnalyticsResponse } from '../../../../services/ml_api_service/data_frame_analytics';
import { useToastNotificationService } from '../../../../services/toast_notification_service';
import { ModelsTableToConfigMapping } from '../../../../trained_models/models_management';
import { ModelsTableToConfigMapping } from '../../../../model_management';
import { DataFrameAnalyticsConfig } from '../../../common';
import { useMlApiContext } from '../../../../contexts/kibana';
import { TrainedModelConfigResponse } from '../../../../../../common/types/trained_models';

View file

@ -7,7 +7,7 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiBasicTableColumn } from '@elastic/eui';
import { TrainedModelLink } from '../../../../../trained_models/models_management';
import { TrainedModelLink } from '../../../../../model_management';
import type { MlSavedObjectType } from '../../../../../../../common/types/saved_objects';
import type {
AnalyticsManagementItems,

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { MemoryUsagePage } from './memory_usage_page';

View file

@ -0,0 +1,34 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
euiPaletteComplimentary,
euiPaletteForTemperature,
euiPaletteGray,
euiPalettePositive,
euiPaletteWarm,
} from '@elastic/eui';
import { MlSavedObjectType } from '../../../common/types/saved_objects';
type MemoryItem = MlSavedObjectType | 'jvm-heap-size' | 'estimated-available-memory';
export function getMemoryItemColor(typeIn: MemoryItem) {
switch (typeIn) {
case 'anomaly-detector':
return euiPaletteWarm(5)[1];
case 'data-frame-analytics':
return euiPalettePositive(5)[2];
case 'trained-model':
return euiPaletteForTemperature(5)[1];
case 'estimated-available-memory':
return euiPaletteGray(5)[0];
case 'jvm-heap-size':
return euiPaletteComplimentary(5)[4];
default:
return euiPaletteGray(5)[4];
}
}

View file

@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { JobMemoryTreeMap } from './tree_map';
export { MemoryPage } from './memory_page';

View file

@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiCallOut, EuiSpacer } from '@elastic/eui';
import React, { FC } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { JobMemoryTreeMap } from './tree_map';
export const MemoryPage: FC = () => {
return (
<>
<EuiSpacer size="s" />
<EuiCallOut color="primary">
<FormattedMessage
id="xpack.ml.memoryUsage.treeMap.infoCallout"
defaultMessage="Memory usage for active machine learning jobs and trained models."
/>
</EuiCallOut>
<JobMemoryTreeMap />
</>
);
};

View file

@ -0,0 +1,201 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { FC, useEffect, useState, useCallback, useMemo } from 'react';
import {
Chart,
Settings,
Partition,
PartitionLayout,
ShapeTreeNode,
LIGHT_THEME,
DARK_THEME,
} from '@elastic/charts';
import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme';
import { FIELD_FORMAT_IDS } from '@kbn/field-formats-plugin/common';
import { EuiComboBox, EuiComboBoxOptionOption, EuiEmptyPrompt, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { MemoryUsageInfo } from '../../../../common/types/trained_models';
import { JobType, MlSavedObjectType } from '../../../../common/types/saved_objects';
import { useTrainedModelsApiService } from '../../services/ml_api_service/trained_models';
import { LoadingWrapper } from '../../jobs/new_job/pages/components/charts/loading_wrapper';
import { useFieldFormatter, useUiSettings } from '../../contexts/kibana';
import { useRefresh } from '../../routing/use_refresh';
import { getMemoryItemColor } from '../memory_item_colors';
import { useToastNotificationService } from '../../services/toast_notification_service';
interface Props {
node?: string;
type?: MlSavedObjectType;
height?: string;
}
const DEFAULT_CHART_HEIGHT = '400px';
const TYPE_LABELS: Record<string, MlSavedObjectType> = {
[i18n.translate('xpack.ml.memoryUsage.treeMap.adLabel', {
defaultMessage: 'Anomaly detection jobs',
})]: 'anomaly-detector',
[i18n.translate('xpack.ml.memoryUsage.treeMap.dfaLabel', {
defaultMessage: 'Data frame analytics jobs',
})]: 'data-frame-analytics',
[i18n.translate('xpack.ml.memoryUsage.treeMap.modelsLabel', {
defaultMessage: 'Trained models',
})]: 'trained-model',
} as const;
const TYPE_LABELS_INVERTED = Object.entries(TYPE_LABELS).reduce<Record<MlSavedObjectType, string>>(
(acc, [label, type]) => {
acc[type] = label;
return acc;
},
{} as Record<MlSavedObjectType, string>
);
const TYPE_OPTIONS: EuiComboBoxOptionOption[] = Object.entries(TYPE_LABELS).map(
([label, type]) => ({
label,
color: getMemoryItemColor(type),
})
);
export const JobMemoryTreeMap: FC<Props> = ({ node, type, height }) => {
const isDarkTheme = useUiSettings().get('theme:darkMode');
const { theme, baseTheme } = useMemo(
() =>
isDarkTheme
? { theme: EUI_CHARTS_THEME_DARK, baseTheme: DARK_THEME }
: { theme: EUI_CHARTS_THEME_LIGHT, baseTheme: LIGHT_THEME },
[isDarkTheme]
);
const bytesFormatter = useFieldFormatter(FIELD_FORMAT_IDS.BYTES);
const { displayErrorToast } = useToastNotificationService();
const refresh = useRefresh();
const chartHeight = height ?? DEFAULT_CHART_HEIGHT;
const trainedModelsApiService = useTrainedModelsApiService();
const [allData, setAllData] = useState<MemoryUsageInfo[]>([]);
const [data, setData] = useState<MemoryUsageInfo[]>([]);
const [loading, setLoading] = useState(false);
const [selectedOptions, setSelectedOptions] = useState(TYPE_OPTIONS);
const filterData = useCallback(
(dataIn: MemoryUsageInfo[]) => {
const types = selectedOptions.map((o) => TYPE_LABELS[o.label]);
return dataIn.filter((d) => types.includes(d.type));
},
[selectedOptions]
);
const loadJobMemorySize = useCallback(async () => {
setLoading(true);
try {
const resp = await trainedModelsApiService.memoryUsage(type, node);
setAllData(resp);
} catch (error) {
displayErrorToast(
error,
i18n.translate('xpack.ml.memoryUsage.treeMap.fetchFailedErrorMessage', {
defaultMessage: 'Models memory usage fetch failed',
})
);
}
setLoading(false);
}, [trainedModelsApiService, type, node, displayErrorToast]);
useEffect(
function redrawOnFilterChange() {
setData(filterData(allData));
},
[selectedOptions, allData, filterData]
);
useEffect(
function updateOnTimerRefresh() {
loadJobMemorySize();
},
[loadJobMemorySize, refresh]
);
return (
<div
style={{ height: chartHeight }}
data-test-subj={`mlJobTreeMap ${data.length ? 'withData' : 'empty'}`}
>
<EuiSpacer size="s" />
<LoadingWrapper height={chartHeight} hasData={data.length > 0} loading={loading}>
<EuiComboBox
fullWidth
options={TYPE_OPTIONS}
selectedOptions={selectedOptions}
onChange={setSelectedOptions}
isClearable={false}
/>
<EuiSpacer size="s" />
{data.length ? (
<Chart>
<Settings baseTheme={baseTheme} theme={theme.theme} />
<Partition<MemoryUsageInfo>
id="memoryUsageTreeMap"
data={data}
layout={PartitionLayout.treemap}
valueAccessor={(d) => d.size}
valueFormatter={(size: number) => bytesFormatter(size)}
layers={[
{
groupByRollup: (d: MemoryUsageInfo) => d.type,
nodeLabel: (d) => TYPE_LABELS_INVERTED[d as MlSavedObjectType],
fillLabel: {
valueFormatter: (size: number) => bytesFormatter(size),
},
shape: {
fillColor: (d: ShapeTreeNode) => getMemoryItemColor(d.dataName as JobType),
},
},
{
groupByRollup: (d: MemoryUsageInfo) => d.id,
nodeLabel: (d) => `${d}`,
fillLabel: {
valueFont: {
fontWeight: 100,
},
},
shape: {
fillColor: (d: ShapeTreeNode) => {
// color the shape the same as its parent.
const parentId = d.parent.path[d.parent.path.length - 1].value as JobType;
return getMemoryItemColor(parentId);
},
},
},
]}
/>
</Chart>
) : (
<EuiEmptyPrompt
titleSize="xs"
iconType="alert"
data-test-subj="mlEmptyMemoryUsageTreeMap"
title={
<h2>
<FormattedMessage
id="xpack.ml.memoryUsage.treeMap.emptyPrompt"
defaultMessage="No open jobs or trained models match the current selection. "
/>
</h2>
}
/>
)}
</LoadingWrapper>
</div>
);
};

View file

@ -0,0 +1,72 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { FC, useCallback, useState } from 'react';
import { mlTimefilterRefresh$, useTimefilter } from '@kbn/ml-date-picker';
import { EuiFlexGroup, EuiFlexItem, EuiTabs, EuiTab } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { NodesList } from './nodes_overview';
import { MlPageHeader } from '../components/page_header';
import { MemoryPage, JobMemoryTreeMap } from './memory_tree_map';
import { useIsServerless } from '../contexts/kibana/use_is_serverless';
import { SavedObjectsWarning } from '../components/saved_objects_warning';
enum TAB {
NODES,
MEMORY_USAGE,
}
export const MemoryUsagePage: FC = () => {
const serverless = useIsServerless();
const [selectedTab, setSelectedTab] = useState<TAB>(TAB.NODES);
useTimefilter({ timeRangeSelector: false, autoRefreshSelector: true });
const refresh = useCallback(() => {
mlTimefilterRefresh$.next({
lastRefresh: Date.now(),
});
}, []);
return (
<>
<MlPageHeader>
<EuiFlexGroup responsive={false} wrap={false} alignItems={'center'} gutterSize={'m'}>
<EuiFlexItem grow={false}>
<FormattedMessage
id="xpack.ml.memoryUsage.memoryUsageHeader"
defaultMessage="Memory Usage"
/>
</EuiFlexItem>
</EuiFlexGroup>
</MlPageHeader>
<SavedObjectsWarning onCloseFlyout={refresh} />
{serverless ? (
<JobMemoryTreeMap />
) : (
<>
<EuiTabs>
<EuiTab
isSelected={selectedTab === TAB.NODES}
onClick={() => setSelectedTab(TAB.NODES)}
>
<FormattedMessage id="xpack.ml.memoryUsage.nodesTab" defaultMessage="Nodes" />
</EuiTab>
<EuiTab
isSelected={selectedTab === TAB.MEMORY_USAGE}
onClick={() => setSelectedTab(TAB.MEMORY_USAGE)}
>
<FormattedMessage id="xpack.ml.memoryUsage.memoryTab" defaultMessage="Memory usage" />
</EuiTab>
</EuiTabs>
{selectedTab === TAB.NODES ? <NodesList /> : <MemoryPage />}
</>
)}
</>
);
};

View file

@ -0,0 +1,151 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { FC, useState } from 'react';
import {
EuiDescriptionList,
EuiFlexGrid,
EuiFlexItem,
EuiPanel,
EuiSpacer,
EuiTab,
EuiTabs,
EuiTitle,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { cloneDeep } from 'lodash';
import { FIELD_FORMAT_IDS } from '@kbn/field-formats-plugin/common';
import { css } from '@emotion/react';
import { NodeItem } from './nodes_list';
import { useListItemsFormatter } from '../../model_management/expanded_row';
import { AllocatedModels } from './allocated_models';
import { useFieldFormatter } from '../../contexts/kibana/use_field_formatter';
import { JobMemoryTreeMap } from '../memory_tree_map';
interface ExpandedRowProps {
item: NodeItem;
}
enum TAB {
DETAILS,
MEMORY_USAGE,
}
export const ExpandedRow: FC<ExpandedRowProps> = ({ item }) => {
const bytesFormatter = useFieldFormatter(FIELD_FORMAT_IDS.BYTES);
const [selectedTab, setSelectedTab] = useState<TAB>(TAB.DETAILS);
const formatToListItems = useListItemsFormatter();
const {
allocated_models: allocatedModels,
attributes,
memory_overview: memoryOverview,
id,
...details
} = cloneDeep(item);
// Process node attributes
attributes['ml.machine_memory'] = bytesFormatter(attributes['ml.machine_memory']);
attributes['ml.max_jvm_size'] = bytesFormatter(attributes['ml.max_jvm_size']);
return (
<div
css={css`
width: 100%;
`}
>
<EuiTabs>
<EuiTab
isSelected={selectedTab === TAB.DETAILS}
onClick={() => setSelectedTab(TAB.DETAILS)}
>
<FormattedMessage
id="xpack.ml.trainedModels.nodesList.expandedRow.detailsTabTitle"
defaultMessage="Details"
/>
</EuiTab>
<EuiTab
isSelected={selectedTab === TAB.MEMORY_USAGE}
onClick={() => setSelectedTab(TAB.MEMORY_USAGE)}
>
<FormattedMessage
id="xpack.ml.trainedModels.nodesList.expandedRow.memoryTabTitle"
defaultMessage="Memory usage"
/>
</EuiTab>
</EuiTabs>
{selectedTab === TAB.DETAILS ? (
<>
<EuiSpacer size="s" />
<EuiFlexGrid columns={2} gutterSize={'s'}>
<EuiFlexItem>
<EuiPanel hasShadow={false}>
<EuiTitle size={'xs'}>
<h5>
<FormattedMessage
id="xpack.ml.trainedModels.nodesList.expandedRow.detailsTitle"
defaultMessage="Details"
/>
</h5>
</EuiTitle>
<EuiSpacer size={'m'} />
<EuiDescriptionList
compressed={true}
type="column"
listItems={formatToListItems(details)}
/>
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem>
<EuiPanel hasShadow={false}>
<EuiTitle size={'xs'}>
<h5>
<FormattedMessage
id="xpack.ml.trainedModels.nodesList.expandedRow.attributesTitle"
defaultMessage="Attributes"
/>
</h5>
</EuiTitle>
<EuiSpacer size={'m'} />
<EuiDescriptionList
compressed={true}
type="column"
listItems={formatToListItems(attributes)}
/>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGrid>
{allocatedModels.length > 0 ? (
<>
<EuiSpacer size={'s'} />
<EuiPanel hasShadow={false}>
<EuiTitle size={'xs'}>
<h5>
<FormattedMessage
id="xpack.ml.trainedModels.nodesList.expandedRow.allocatedModelsTitle"
defaultMessage="Allocated trained models"
/>
</h5>
</EuiTitle>
<EuiSpacer size={'m'} />
<AllocatedModels models={allocatedModels} />
</EuiPanel>
</>
) : null}
</>
) : (
<>
<JobMemoryTreeMap node={item.name} />
</>
)}
</div>
);
};

View file

@ -18,11 +18,11 @@ import {
LineAnnotation,
AnnotationDomainType,
} from '@elastic/charts';
import { EuiIcon, euiPaletteGray } from '@elastic/eui';
import { EuiIcon } from '@elastic/eui';
import { FIELD_FORMAT_IDS } from '@kbn/field-formats-plugin/common';
import { NodeDeploymentStatsResponse } from '../../../../common/types/trained_models';
import { useFieldFormatter } from '../../contexts/kibana/use_field_formatter';
import { useCurrentEuiTheme } from '../../components/color_range_legend';
import { getMemoryItemColor } from '../memory_item_colors';
interface MemoryPreviewChartProps {
memoryOverview: NodeDeploymentStatsResponse['memory_overview'];
@ -31,42 +31,39 @@ interface MemoryPreviewChartProps {
export const MemoryPreviewChart: FC<MemoryPreviewChartProps> = ({ memoryOverview }) => {
const bytesFormatter = useFieldFormatter(FIELD_FORMAT_IDS.BYTES);
const { euiTheme } = useCurrentEuiTheme();
const groups = useMemo(
() => ({
jvm: {
name: i18n.translate('xpack.ml.trainedModels.nodesList.jvmHeapSIze', {
defaultMessage: 'JVM heap size',
}),
colour: euiTheme.euiColorVis1,
color: getMemoryItemColor('jvm-heap-size'),
},
trained_models: {
name: i18n.translate('xpack.ml.trainedModels.nodesList.modelsMemoryUsage', {
defaultMessage: 'Trained models',
}),
colour: euiTheme.euiColorVis2,
color: getMemoryItemColor('trained-model'),
},
anomaly_detection: {
name: i18n.translate('xpack.ml.trainedModels.nodesList.adMemoryUsage', {
defaultMessage: 'Anomaly detection jobs',
}),
colour: euiTheme.euiColorVis6,
color: getMemoryItemColor('anomaly-detector'),
},
dfa_training: {
name: i18n.translate('xpack.ml.trainedModels.nodesList.dfaMemoryUsage', {
defaultMessage: 'Data frame analytics jobs',
}),
colour: euiTheme.euiColorVis4,
color: getMemoryItemColor('data-frame-analytics'),
},
available: {
name: i18n.translate('xpack.ml.trainedModels.nodesList.availableMemory', {
defaultMessage: 'Estimated available memory',
}),
colour: euiPaletteGray(5)[0],
color: getMemoryItemColor('estimated-available-memory'),
},
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
@ -106,7 +103,7 @@ export const MemoryPreviewChart: FC<MemoryPreviewChartProps> = ({ memoryOverview
const barSeriesColorAccessor: SeriesColorAccessor = ({ specId, yAccessor, splitAccessors }) => {
const group = splitAccessors.get('g');
return Object.values(groups).find((v) => v.name === group)!.colour;
return Object.values(groups).find((v) => v.name === group)!.color;
};
return (

View file

@ -33,7 +33,7 @@ import { useRefresh } from '../../routing/use_refresh';
export type NodeItem = NodeDeploymentStatsResponse;
interface PageUrlState {
pageKey: typeof ML_PAGES.TRAINED_MODELS_NODES;
pageKey: typeof ML_PAGES.MEMORY_USAGE;
pageUrlState: ListingPageUrlState;
}
@ -61,7 +61,7 @@ export const NodesList: FC<NodesListProps> = ({ compactView = false }) => {
{}
);
const [pageState, updatePageState] = usePageUrlState<PageUrlState>(
ML_PAGES.TRAINED_MODELS_NODES,
ML_PAGES.MEMORY_USAGE,
getDefaultNodesListState()
);

View file

@ -16,9 +16,9 @@ import {
EuiButton,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { useTrainedModelsApiService } from '../../services/ml_api_service/trained_models';
import { useToastNotificationService } from '../../services/toast_notification_service';
import { DeleteSpaceAwareItemCheckModal } from '../../components/delete_space_aware_item_check_modal';
import { useTrainedModelsApiService } from '../services/ml_api_service/trained_models';
import { useToastNotificationService } from '../services/toast_notification_service';
import { DeleteSpaceAwareItemCheckModal } from '../components/delete_space_aware_item_check_modal';
interface DeleteModelsModalProps {
modelIds: string[];

View file

@ -30,8 +30,8 @@ import type { Observable } from 'rxjs';
import type { CoreTheme, OverlayStart } from '@kbn/core/public';
import { css } from '@emotion/react';
import { numberValidator } from '@kbn/ml-agg-utils';
import { isCloudTrial } from '../../services/ml_server_info';
import { composeValidators, requiredValidator } from '../../../../common/util/validators';
import { isCloudTrial } from '../services/ml_server_info';
import { composeValidators, requiredValidator } from '../../../common/util/validators';
interface DeploymentSetupProps {
config: ThreadingParams;

View file

@ -20,6 +20,7 @@ import {
EuiTabbedContent,
EuiTabbedContentTab,
EuiTitle,
useEuiPaddingSize,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { FIELD_FORMAT_IDS } from '@kbn/field-formats-plugin/common';
@ -27,30 +28,38 @@ import { isPopulatedObject } from '@kbn/ml-is-populated-object';
import { isDefined } from '@kbn/ml-is-defined';
import type { ModelItemFull } from './models_list';
import { ModelPipelines } from './pipelines';
import { AllocatedModels } from '../nodes_overview/allocated_models';
import type { AllocatedModel } from '../../../../common/types/trained_models';
import { useFieldFormatter } from '../../contexts/kibana/use_field_formatter';
import { AllocatedModels } from '../memory_usage/nodes_overview/allocated_models';
import type { AllocatedModel } from '../../../common/types/trained_models';
import { useFieldFormatter } from '../contexts/kibana/use_field_formatter';
interface ExpandedRowProps {
item: ModelItemFull;
}
const badgeFormatter = (items: string[]) => {
if (items.length === 0) return;
return (
<div>
{items.map((item) => (
<EuiBadge key={item} color="hollow">
{item}
</EuiBadge>
))}
</div>
);
const useBadgeFormatter = () => {
const xs = useEuiPaddingSize('xs');
function badgeFormatter(items: string[]) {
if (items.length === 0) return;
return (
<div>
{items.map((item) => (
<span css={{ marginRight: xs! }} key={item}>
<EuiBadge color="hollow" css={{ marginRight: xs! }}>
{item}
</EuiBadge>
</span>
))}
</div>
);
}
return { badgeFormatter };
};
export function useListItemsFormatter() {
const bytesFormatter = useFieldFormatter(FIELD_FORMAT_IDS.BYTES);
const dateFormatter = useFieldFormatter(FIELD_FORMAT_IDS.DATE);
const { badgeFormatter } = useBadgeFormatter();
const formatterDictionary: Record<string, (value: any) => JSX.Element | string | undefined> =
useMemo(

View file

@ -10,16 +10,16 @@ import { i18n } from '@kbn/i18n';
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
import { EuiToolTip } from '@elastic/eui';
import React, { useCallback, useMemo } from 'react';
import { BUILT_IN_MODEL_TAG } from '../../../../common/constants/data_frame_analytics';
import { useTrainedModelsApiService } from '../../services/ml_api_service/trained_models';
import { BUILT_IN_MODEL_TAG } from '../../../common/constants/data_frame_analytics';
import { useTrainedModelsApiService } from '../services/ml_api_service/trained_models';
import { getUserConfirmationProvider } from './force_stop_dialog';
import { useToastNotificationService } from '../../services/toast_notification_service';
import { useToastNotificationService } from '../services/toast_notification_service';
import { getUserInputThreadingParamsProvider } from './deployment_setup';
import { useMlKibana, useMlLocator, useNavigateToPath } from '../../contexts/kibana';
import { getAnalysisType } from '../../../../common/util/analytics_utils';
import { DataFrameAnalysisConfigType } from '../../../../common/types/data_frame_analytics';
import { ML_PAGES } from '../../../../common/constants/locator';
import { DEPLOYMENT_STATE, TRAINED_MODEL_TYPE } from '../../../../common/constants/trained_models';
import { useMlKibana, useMlLocator, useNavigateToPath } from '../contexts/kibana';
import { getAnalysisType } from '../../../common/util/analytics_utils';
import { DataFrameAnalysisConfigType } from '../../../common/types/data_frame_analytics';
import { ML_PAGES } from '../../../common/constants/locator';
import { DEPLOYMENT_STATE, TRAINED_MODEL_TYPE } from '../../../common/constants/trained_models';
import { isTestable } from './test_models';
import { ModelItem } from './models_list';

View file

@ -7,8 +7,8 @@
import { EuiLink } from '@elastic/eui';
import React, { FC } from 'react';
import { useMlLink } from '../../contexts/kibana';
import { ML_PAGES } from '../../../../common/constants/locator';
import { useMlLink } from '../contexts/kibana';
import { ML_PAGES } from '../../../common/constants/locator';
export interface TrainedModelLinkProps {
id: string;

View file

@ -29,25 +29,25 @@ import { usePageUrlState } from '@kbn/ml-url-state';
import { useTimefilter } from '@kbn/ml-date-picker';
import { useModelActions } from './model_actions';
import { ModelsTableToConfigMapping } from '.';
import { ModelsBarStats, StatsBar } from '../../components/stats_bar';
import { useMlKibana } from '../../contexts/kibana';
import { useTrainedModelsApiService } from '../../services/ml_api_service/trained_models';
import { ModelsBarStats, StatsBar } from '../components/stats_bar';
import { useMlKibana } from '../contexts/kibana';
import { useTrainedModelsApiService } from '../services/ml_api_service/trained_models';
import {
ModelPipelines,
TrainedModelConfigResponse,
TrainedModelStat,
} from '../../../../common/types/trained_models';
import { BUILT_IN_MODEL_TAG } from '../../../../common/constants/data_frame_analytics';
} from '../../../common/types/trained_models';
import { BUILT_IN_MODEL_TAG } from '../../../common/constants/data_frame_analytics';
import { DeleteModelsModal } from './delete_models_modal';
import { ML_PAGES } from '../../../../common/constants/locator';
import { ListingPageUrlState } from '../../../../common/types/common';
import { ML_PAGES } from '../../../common/constants/locator';
import { ListingPageUrlState } from '../../../common/types/common';
import { ExpandedRow } from './expanded_row';
import { useTableSettings } from '../../data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings';
import { useToastNotificationService } from '../../services/toast_notification_service';
import { useFieldFormatter } from '../../contexts/kibana/use_field_formatter';
import { useRefresh } from '../../routing/use_refresh';
import { BUILT_IN_MODEL_TYPE } from '../../../../common/constants/trained_models';
import { SavedObjectsWarning } from '../../components/saved_objects_warning';
import { useTableSettings } from '../data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings';
import { useToastNotificationService } from '../services/toast_notification_service';
import { useFieldFormatter } from '../contexts/kibana/use_field_formatter';
import { useRefresh } from '../routing/use_refresh';
import { BUILT_IN_MODEL_TYPE } from '../../../common/constants/trained_models';
import { SavedObjectsWarning } from '../components/saved_objects_warning';
import { TestTrainedModelFlyout } from './test_models';
type Stats = Omit<TrainedModelStat, 'model_id'>;

View file

@ -12,9 +12,9 @@ import { FormattedMessage } from '@kbn/i18n-react';
import { EuiBasicTableColumn } from '@elastic/eui/src/components/basic_table/basic_table';
import { i18n } from '@kbn/i18n';
import { FIELD_FORMAT_IDS } from '@kbn/field-formats-plugin/common';
import { useFieldFormatter } from '../../../contexts/kibana/use_field_formatter';
import { useFieldFormatter } from '../../contexts/kibana/use_field_formatter';
import { IngestStatsResponse } from './pipelines';
import { HelpIcon } from '../../../components/help_icon';
import { HelpIcon } from '../../components/help_icon';
interface ProcessorsStatsProps {
stats: Exclude<IngestStatsResponse, undefined>['pipelines'][string]['processors'];

View file

@ -16,7 +16,7 @@ import {
EuiAccordion,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { useMlKibana } from '../../../contexts/kibana';
import { useMlKibana } from '../../contexts/kibana';
import { ModelItem } from '../models_list';
import { ProcessorsStats } from './expanded_row';

View file

@ -14,7 +14,7 @@ import { EuiSpacer, EuiSelect, EuiFormRow, EuiAccordion, EuiCodeBlock } from '@e
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
import { i18n } from '@kbn/i18n';
import { useMlKibana } from '../../../../contexts/kibana';
import { useMlKibana } from '../../../contexts/kibana';
import { RUNNING_STATE } from './inference_base';
import type { InferrerType } from '.';

View file

@ -10,9 +10,9 @@ import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { i18n } from '@kbn/i18n';
import { map } from 'rxjs/operators';
import { MLHttpFetchError } from '../../../../../../common/util/errors';
import { SupportedPytorchTasksType } from '../../../../../../common/constants/trained_models';
import { trainedModelsApiProvider } from '../../../../services/ml_api_service/trained_models';
import { MLHttpFetchError } from '../../../../../common/util/errors';
import { SupportedPytorchTasksType } from '../../../../../common/constants/trained_models';
import { trainedModelsApiProvider } from '../../../services/ml_api_service/trained_models';
import { getInferenceInfoComponent } from './inference_info';
export type InferenceType =

View file

@ -22,7 +22,7 @@ import {
} from '@elastic/eui';
import { ErrorMessage } from '../../inference_error';
import { extractErrorMessage } from '../../../../../../../common';
import { extractErrorMessage } from '../../../../../../common';
import type { InferrerType } from '..';
import { useIndexInput, InferenceInputFormIndexControls } from '../index_input';
import { RUNNING_STATE } from '../inference_base';

View file

@ -12,7 +12,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
import { EuiSpacer, EuiButton, EuiTabs, EuiTab, EuiForm } from '@elastic/eui';
import { ErrorMessage } from '../../inference_error';
import { extractErrorMessage } from '../../../../../../../common';
import { extractErrorMessage } from '../../../../../../common';
import type { InferrerType } from '..';
import { OutputLoadingContent } from '../../output_loading';
import { RUNNING_STATE } from '../inference_base';

View file

@ -7,12 +7,12 @@
import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { i18n } from '@kbn/i18n';
import { trainedModelsApiProvider } from '../../../../../services/ml_api_service/trained_models';
import { trainedModelsApiProvider } from '../../../../services/ml_api_service/trained_models';
import { InferenceBase, INPUT_TYPE } from '../inference_base';
import type { InferResponse } from '../inference_base';
import { getGeneralInputComponent } from '../text_input';
import { getNerOutputComponent } from './ner_output';
import { SUPPORTED_PYTORCH_TASKS } from '../../../../../../../common/constants/trained_models';
import { SUPPORTED_PYTORCH_TASKS } from '../../../../../../common/constants/trained_models';
export type FormattedNerResponse = Array<{
value: string;

View file

@ -21,7 +21,7 @@ import {
import {
useCurrentEuiTheme,
EuiThemeType,
} from '../../../../../components/color_range_legend/use_color_range';
} from '../../../../components/color_range_legend/use_color_range';
import type { NerInference, NerResponse } from './ner_inference';
import { INPUT_TYPE } from '../inference_base';

View file

@ -13,8 +13,8 @@ import { InferenceBase, INPUT_TYPE } from '../inference_base';
import type { InferResponse } from '../inference_base';
import { getQuestionAnsweringInput } from './question_answering_input';
import { getQuestionAnsweringOutputComponent } from './question_answering_output';
import { SUPPORTED_PYTORCH_TASKS } from '../../../../../../../common/constants/trained_models';
import { trainedModelsApiProvider } from '../../../../../services/ml_api_service/trained_models';
import { SUPPORTED_PYTORCH_TASKS } from '../../../../../../common/constants/trained_models';
import { trainedModelsApiProvider } from '../../../../services/ml_api_service/trained_models';
export interface RawQuestionAnsweringResponse {
inference_results: Array<{

View file

@ -10,7 +10,7 @@ import useObservable from 'react-use/lib/useObservable';
import { EuiBadge, EuiHorizontalRule } from '@elastic/eui';
import { useCurrentEuiTheme } from '../../../../../components/color_range_legend/use_color_range';
import { useCurrentEuiTheme } from '../../../../components/color_range_legend/use_color_range';
import type {
QuestionAnsweringInference,

View file

@ -9,7 +9,7 @@ import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import React, { FC } from 'react';
import { Observable } from 'rxjs';
import useObservable from 'react-use/lib/useObservable';
import { MLJobEditor } from '../../../../jobs/jobs_list/components/ml_job_editor';
import { MLJobEditor } from '../../../jobs/jobs_list/components/ml_job_editor';
import type { InferrerType } from '.';
import { NerResponse } from './ner';

View file

@ -13,8 +13,8 @@ import type { TextClassificationResponse, RawTextClassificationResponse } from '
import { processResponse, processInferenceResult } from './common';
import { getGeneralInputComponent } from '../text_input';
import { getFillMaskOutputComponent } from './fill_mask_output';
import { SUPPORTED_PYTORCH_TASKS } from '../../../../../../../common/constants/trained_models';
import { trainedModelsApiProvider } from '../../../../../services/ml_api_service/trained_models';
import { SUPPORTED_PYTORCH_TASKS } from '../../../../../../common/constants/trained_models';
import { trainedModelsApiProvider } from '../../../../services/ml_api_service/trained_models';
const MASK = '[MASK]';

View file

@ -13,7 +13,7 @@ import { processInferenceResult, processResponse } from './common';
import { getGeneralInputComponent } from '../text_input';
import { getLangIdentOutputComponent } from './lang_ident_output';
import type { TextClassificationResponse, RawTextClassificationResponse } from './common';
import { trainedModelsApiProvider } from '../../../../../services/ml_api_service/trained_models';
import { trainedModelsApiProvider } from '../../../../services/ml_api_service/trained_models';
export class LangIdentInference extends InferenceBase<TextClassificationResponse> {
protected inferenceType: InferenceType = 'classification';

View file

@ -12,8 +12,8 @@ import { processInferenceResult, processResponse } from './common';
import type { TextClassificationResponse, RawTextClassificationResponse } from './common';
import { getGeneralInputComponent } from '../text_input';
import { getTextClassificationOutputComponent } from './text_classification_output';
import { SUPPORTED_PYTORCH_TASKS } from '../../../../../../../common/constants/trained_models';
import { trainedModelsApiProvider } from '../../../../../services/ml_api_service/trained_models';
import { SUPPORTED_PYTORCH_TASKS } from '../../../../../../common/constants/trained_models';
import { trainedModelsApiProvider } from '../../../../services/ml_api_service/trained_models';
export class TextClassificationInference extends InferenceBase<TextClassificationResponse> {
protected inferenceType = SUPPORTED_PYTORCH_TASKS.TEXT_CLASSIFICATION;

View file

@ -9,14 +9,14 @@ import { i18n } from '@kbn/i18n';
import { BehaviorSubject } from 'rxjs';
import { map } from 'rxjs/operators';
import { estypes } from '@elastic/elasticsearch';
import { trainedModelsApiProvider } from '../../../../../services/ml_api_service/trained_models';
import { trainedModelsApiProvider } from '../../../../services/ml_api_service/trained_models';
import { InferenceBase, INPUT_TYPE } from '../inference_base';
import { processInferenceResult, processResponse } from './common';
import type { TextClassificationResponse, RawTextClassificationResponse } from './common';
import { getZeroShotClassificationInput } from './zero_shot_classification_input';
import { getTextClassificationOutputComponent } from './text_classification_output';
import { SUPPORTED_PYTORCH_TASKS } from '../../../../../../../common/constants/trained_models';
import { SUPPORTED_PYTORCH_TASKS } from '../../../../../../common/constants/trained_models';
export class ZeroShotClassificationInference extends InferenceBase<TextClassificationResponse> {
protected inferenceType = SUPPORTED_PYTORCH_TASKS.ZERO_SHOT_CLASSIFICATION;

View file

@ -11,8 +11,8 @@ import { InferenceBase, INPUT_TYPE } from '../inference_base';
import type { InferResponse } from '../inference_base';
import { getGeneralInputComponent } from '../text_input';
import { getTextEmbeddingOutputComponent } from './text_embedding_output';
import { SUPPORTED_PYTORCH_TASKS } from '../../../../../../../common/constants/trained_models';
import { trainedModelsApiProvider } from '../../../../../services/ml_api_service/trained_models';
import { SUPPORTED_PYTORCH_TASKS } from '../../../../../../common/constants/trained_models';
import { trainedModelsApiProvider } from '../../../../services/ml_api_service/trained_models';
export interface RawTextEmbeddingResponse {
inference_results: Array<{ predicted_value: number[] }>;

View file

@ -23,8 +23,8 @@ import { TextEmbeddingInference } from './models/text_embedding';
import {
TRAINED_MODEL_TYPE,
SUPPORTED_PYTORCH_TASKS,
} from '../../../../../common/constants/trained_models';
import { useMlApiContext } from '../../../contexts/kibana';
} from '../../../../common/constants/trained_models';
import { useMlApiContext } from '../../contexts/kibana';
import { InferenceInputForm } from './models/inference_input_form';
import { InferrerType } from './models';
import { INPUT_TYPE } from './models/inference_base';

View file

@ -22,7 +22,7 @@ import {
import { SelectedModel } from './selected_model';
import { INPUT_TYPE } from './models/inference_base';
import { useTrainedModelsApiService } from '../../../services/ml_api_service/trained_models';
import { useTrainedModelsApiService } from '../../services/ml_api_service/trained_models';
interface Props {
modelId: string;

View file

@ -9,8 +9,8 @@ import {
TRAINED_MODEL_TYPE,
DEPLOYMENT_STATE,
SUPPORTED_PYTORCH_TASKS,
} from '../../../../../common/constants/trained_models';
import type { SupportedPytorchTasksType } from '../../../../../common/constants/trained_models';
} from '../../../../common/constants/trained_models';
import type { SupportedPytorchTasksType } from '../../../../common/constants/trained_models';
import type { ModelItem } from '../models_list';
const PYTORCH_TYPES = Object.values(SUPPORTED_PYTORCH_TASKS);

View file

@ -19,11 +19,13 @@ import { SavedObjectsWarning } from '../components/saved_objects_warning';
import { UpgradeWarning } from '../components/upgrade';
import { HelpMenu } from '../components/help_menu';
import { useMlKibana } from '../contexts/kibana';
import { NodesList } from '../trained_models/nodes_overview';
import { NodesList } from '../memory_usage/nodes_overview';
import { MlPageHeader } from '../components/page_header';
import { PageTitle } from '../components/page_title';
import { useIsServerless } from '../contexts/kibana/use_is_serverless';
export const OverviewPage: FC = () => {
const serverless = useIsServerless();
const canViewMlNodes = checkPermission('canViewMlNodes');
const disableCreateAnomalyDetectionJob = !checkPermission('canCreateJob') || !mlNodesAvailable();
@ -62,7 +64,7 @@ export const OverviewPage: FC = () => {
<GettingStartedCallout />
{canViewMlNodes ? (
{canViewMlNodes && serverless === false ? (
<>
<EuiPanel hasShadow={false} hasBorder>
<NodesList compactView />

View file

@ -17,3 +17,4 @@ export * from './explorer';
export * from './access_denied';
export * from './trained_models';
export * from './notifications';
export * from './memory_usage';

View file

@ -0,0 +1,53 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { FC } from 'react';
import { i18n } from '@kbn/i18n';
import { ML_PAGES } from '../../../locator';
import { NavigateToPath } from '../../contexts/kibana';
import { MlRoute, PageLoader, PageProps, createPath } from '../router';
import { useResolver } from '../use_resolver';
import { basicResolvers } from '../resolvers';
import { getBreadcrumbWithUrlForApp } from '../breadcrumbs';
import { MemoryUsagePage } from '../../memory_usage';
export const nodesListRouteFactory = (
navigateToPath: NavigateToPath,
basePath: string
): MlRoute => ({
path: createPath(ML_PAGES.MEMORY_USAGE),
render: (props, deps) => <PageWrapper {...props} deps={deps} />,
title: i18n.translate('xpack.ml.modelManagement.memoryUsage.docTitle', {
defaultMessage: 'Memory Usage',
}),
breadcrumbs: [
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
{
text: i18n.translate('xpack.ml.trainedModelsBreadcrumbs.nodeOverviewLabel', {
defaultMessage: 'Memory Usage',
}),
},
],
enableDatePicker: true,
});
const PageWrapper: FC<PageProps> = ({ location, deps }) => {
const { context } = useResolver(
undefined,
undefined,
deps.config,
deps.dataViewsContract,
deps.getSavedSearchDeps,
basicResolvers(deps)
);
return (
<PageLoader context={context}>
<MemoryUsagePage />
</PageLoader>
);
};

View file

@ -6,4 +6,3 @@
*/
export * from './models_list';
export * from './nodes_list';

View file

@ -15,7 +15,7 @@ import { createPath, MlRoute, PageLoader, PageProps } from '../../router';
import { useResolver } from '../../use_resolver';
import { basicResolvers } from '../../resolvers';
import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs';
import { ModelsList } from '../../../trained_models/models_management';
import { ModelsList } from '../../../model_management';
import { MlPageHeader } from '../../../components/page_header';
export const modelsListRouteFactory = (
@ -52,7 +52,6 @@ const PageWrapper: FC<PageProps> = ({ location, deps }) => {
);
return (
<PageLoader context={context}>
<ModelsList />
<MlPageHeader>
<EuiFlexGroup responsive={false} wrap={false} alignItems={'center'} gutterSize={'m'}>
<EuiFlexItem grow={false}>
@ -63,6 +62,7 @@ const PageWrapper: FC<PageProps> = ({ location, deps }) => {
</EuiFlexItem>
</EuiFlexGroup>
</MlPageHeader>
<ModelsList />
</PageLoader>
);
};

View file

@ -1,68 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { FC } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { useTimefilter } from '@kbn/ml-date-picker';
import { ML_PAGES } from '../../../../locator';
import { NavigateToPath } from '../../../contexts/kibana';
import { createPath, MlRoute, PageLoader, PageProps } from '../../router';
import { useResolver } from '../../use_resolver';
import { basicResolvers } from '../../resolvers';
import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs';
import { NodesList } from '../../../trained_models/nodes_overview';
import { MlPageHeader } from '../../../components/page_header';
export const nodesListRouteFactory = (
navigateToPath: NavigateToPath,
basePath: string
): MlRoute => ({
path: createPath(ML_PAGES.TRAINED_MODELS_NODES),
render: (props, deps) => <PageWrapper {...props} deps={deps} />,
title: i18n.translate('xpack.ml.modelManagement.nodesOverview.docTitle', {
defaultMessage: 'Nodes',
}),
breadcrumbs: [
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
getBreadcrumbWithUrlForApp('TRAINED_MODELS', navigateToPath, basePath),
{
text: i18n.translate('xpack.ml.trainedModelsBreadcrumbs.nodeOverviewLabel', {
defaultMessage: 'Nodes',
}),
},
],
enableDatePicker: true,
});
const PageWrapper: FC<PageProps> = ({ location, deps }) => {
const { context } = useResolver(
undefined,
undefined,
deps.config,
deps.dataViewsContract,
deps.getSavedSearchDeps,
basicResolvers(deps)
);
useTimefilter({ timeRangeSelector: false, autoRefreshSelector: true });
return (
<PageLoader context={context}>
<MlPageHeader>
<EuiFlexGroup responsive={false} wrap={false} alignItems={'center'} gutterSize={'m'}>
<EuiFlexItem grow={false}>
<FormattedMessage
id="xpack.ml.modelManagement.nodesOverviewHeader"
defaultMessage="Nodes"
/>
</EuiFlexItem>
</EuiFlexGroup>
</MlPageHeader>
<NodesList />
</PageLoader>
);
};

View file

@ -9,6 +9,7 @@ import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { useMemo } from 'react';
import { HttpFetchQuery } from '@kbn/core/public';
import { MlSavedObjectType } from '../../../../common/types/saved_objects';
import { HttpService } from '../http_service';
import { basePath } from '.';
import { useMlKibana } from '../../contexts/kibana';
@ -17,6 +18,7 @@ import type {
ModelPipelines,
TrainedModelStat,
NodesOverviewResponse,
MemoryUsageInfo,
} from '../../../../common/types/trained_models';
export interface InferenceQueryParams {
@ -119,7 +121,7 @@ export function trainedModelsApiProvider(httpService: HttpService) {
getTrainedModelsNodesOverview() {
return httpService.http<NodesOverviewResponse>({
path: `${apiBasePath}/trained_models/nodes_overview`,
path: `${apiBasePath}/model_management/nodes_overview`,
method: 'GET',
});
},
@ -185,6 +187,14 @@ export function trainedModelsApiProvider(httpService: HttpService) {
body,
});
},
memoryUsage(type?: MlSavedObjectType, node?: string, showClosedJobs = false) {
return httpService.http<MemoryUsageInfo[]>({
path: `${apiBasePath}/model_management/memory_usage`,
method: 'GET',
query: { type, node, showClosedJobs },
});
},
};
}

View file

@ -1,113 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { FC } from 'react';
import {
EuiDescriptionList,
EuiFlexGrid,
EuiFlexItem,
EuiPanel,
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { cloneDeep } from 'lodash';
import { FIELD_FORMAT_IDS } from '@kbn/field-formats-plugin/common';
import { css } from '@emotion/react';
import { NodeItem } from './nodes_list';
import { useListItemsFormatter } from '../models_management/expanded_row';
import { AllocatedModels } from './allocated_models';
import { useFieldFormatter } from '../../contexts/kibana/use_field_formatter';
interface ExpandedRowProps {
item: NodeItem;
}
export const ExpandedRow: FC<ExpandedRowProps> = ({ item }) => {
const bytesFormatter = useFieldFormatter(FIELD_FORMAT_IDS.BYTES);
const formatToListItems = useListItemsFormatter();
const {
allocated_models: allocatedModels,
attributes,
memory_overview: memoryOverview,
id,
...details
} = cloneDeep(item);
// Process node attributes
attributes['ml.machine_memory'] = bytesFormatter(attributes['ml.machine_memory']);
attributes['ml.max_jvm_size'] = bytesFormatter(attributes['ml.max_jvm_size']);
return (
<div
css={css`
width: 100%;
`}
>
<EuiFlexGrid columns={2} gutterSize={'s'}>
<EuiFlexItem>
<EuiPanel>
<EuiTitle size={'xs'}>
<h5>
<FormattedMessage
id="xpack.ml.trainedModels.nodesList.expandedRow.detailsTitle"
defaultMessage="Details"
/>
</h5>
</EuiTitle>
<EuiSpacer size={'m'} />
<EuiDescriptionList
compressed={true}
type="column"
listItems={formatToListItems(details)}
/>
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem>
<EuiPanel>
<EuiTitle size={'xs'}>
<h5>
<FormattedMessage
id="xpack.ml.trainedModels.nodesList.expandedRow.attributesTitle"
defaultMessage="Attributes"
/>
</h5>
</EuiTitle>
<EuiSpacer size={'m'} />
<EuiDescriptionList
compressed={true}
type="column"
listItems={formatToListItems(attributes)}
/>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGrid>
{allocatedModels.length > 0 ? (
<>
<EuiSpacer size={'s'} />
<EuiPanel>
<EuiTitle size={'xs'}>
<h5>
<FormattedMessage
id="xpack.ml.trainedModels.nodesList.expandedRow.allocatedModelsTitle"
defaultMessage="Allocated models"
/>
</h5>
</EuiTitle>
<EuiSpacer size={'m'} />
<AllocatedModels models={allocatedModels} />
</EuiPanel>
</>
) : null}
</div>
);
};

View file

@ -6,10 +6,7 @@
*/
import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public';
import type {
TrainedModelsNodesUrlState,
TrainedModelsUrlState,
} from '../../../common/types/locator';
import type { MemoryUsageUrlState, TrainedModelsUrlState } from '../../../common/types/locator';
import { ML_PAGES } from '../../../common/constants/locator';
import type { AppPageState, ListingPageUrlState } from '../../../common/types/common';
@ -41,11 +38,11 @@ export function formatTrainedModelsManagementUrl(
return url;
}
export function formatTrainedModelsNodesManagementUrl(
export function formatMemoryUsageUrl(
appBasePath: string,
mlUrlGeneratorState: TrainedModelsNodesUrlState['pageState']
mlUrlGeneratorState: MemoryUsageUrlState['pageState']
): string {
let url = `${appBasePath}/${ML_PAGES.TRAINED_MODELS_NODES}`;
let url = `${appBasePath}/${ML_PAGES.MEMORY_USAGE}`;
if (mlUrlGeneratorState) {
const { nodeId } = mlUrlGeneratorState;
if (nodeId) {
@ -54,7 +51,7 @@ export function formatTrainedModelsNodesManagementUrl(
};
const queryState: AppPageState<ListingPageUrlState> = {
[ML_PAGES.TRAINED_MODELS_NODES]: nodesListState,
[ML_PAGES.MEMORY_USAGE]: nodesListState,
};
url = setStateToKbnUrl<AppPageState<ListingPageUrlState>>(

View file

@ -29,7 +29,7 @@ import {
} from './formatters';
import {
formatTrainedModelsManagementUrl,
formatTrainedModelsNodesManagementUrl,
formatMemoryUsageUrl,
} from './formatters/trained_models';
export type { MlLocatorParams, MlLocator };
@ -74,8 +74,8 @@ export class MlLocatorDefinition implements LocatorDefinition<MlLocatorParams> {
case ML_PAGES.TRAINED_MODELS_MANAGE:
path = formatTrainedModelsManagementUrl('', params.pageState);
break;
case ML_PAGES.TRAINED_MODELS_NODES:
path = formatTrainedModelsNodesManagementUrl('', params.pageState);
case ML_PAGES.MEMORY_USAGE:
path = formatMemoryUsageUrl('', params.pageState);
break;
case ML_PAGES.ANOMALY_DETECTION_CREATE_JOB:
case ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_RECOGNIZER:

View file

@ -49,11 +49,11 @@ const MODEL_MANAGEMENT_DEEP_LINK: AppDeepLink = {
path: `/${ML_PAGES.TRAINED_MODELS_MANAGE}`,
},
{
id: 'mlNodesOverviewDeepLink',
title: i18n.translate('xpack.ml.deepLink.nodesOverview', {
defaultMessage: 'Nodes',
id: 'mlMemoryUsageDeepLink',
title: i18n.translate('xpack.ml.deepLink.memoryUsage', {
defaultMessage: 'Memory usage',
}),
path: `/${ML_PAGES.TRAINED_MODELS_NODES}`,
path: `/${ML_PAGES.MEMORY_USAGE}`,
},
],
};

View file

@ -6,5 +6,4 @@
*/
export { analyticsAuditMessagesProvider } from './analytics_audit_messages';
export { modelsProvider } from './models_provider';
export { AnalyticsManager } from './analytics_manager';

View file

@ -1,220 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { IScopedClusterClient } from '@kbn/core/server';
import { pick } from 'lodash';
import {
MlTrainedModelStats,
NodesInfoNodeInfo,
} from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { isDefined } from '@kbn/ml-is-defined';
import type {
NodeDeploymentStatsResponse,
PipelineDefinition,
NodesOverviewResponse,
} from '../../../common/types/trained_models';
import type { MlClient } from '../../lib/ml_client';
import {
TrainedModelDeploymentStatsResponse,
TrainedModelModelSizeStats,
} from '../../../common/types/trained_models';
export type ModelService = ReturnType<typeof modelsProvider>;
const NODE_FIELDS = ['attributes', 'name', 'roles'] as const;
export type RequiredNodeFields = Pick<NodesInfoNodeInfo, typeof NODE_FIELDS[number]>;
// @ts-expect-error TrainedModelDeploymentStatsResponse missing properties from MlTrainedModelDeploymentStats
interface TrainedModelStatsResponse extends MlTrainedModelStats {
deployment_stats?: Omit<TrainedModelDeploymentStatsResponse, 'model_id'>;
model_size_stats?: TrainedModelModelSizeStats;
}
export interface MemoryStatsResponse {
_nodes: { total: number; failed: number; successful: number };
cluster_name: string;
nodes: Record<
string,
{
jvm: {
heap_max_in_bytes: number;
java_inference_in_bytes: number;
java_inference_max_in_bytes: number;
};
mem: {
adjusted_total_in_bytes: number;
total_in_bytes: number;
ml: {
data_frame_analytics_in_bytes: number;
native_code_overhead_in_bytes: number;
max_in_bytes: number;
anomaly_detectors_in_bytes: number;
native_inference_in_bytes: number;
};
};
transport_address: string;
roles: string[];
name: string;
attributes: Record<`${'ml.'}${string}`, string>;
ephemeral_id: string;
}
>;
}
export function modelsProvider(client: IScopedClusterClient, mlClient: MlClient) {
return {
/**
* Retrieves the map of model ids and aliases with associated pipelines.
* @param modelIds - Array of models ids and model aliases.
*/
async getModelsPipelines(modelIds: string[]) {
const modelIdsMap = new Map<string, Record<string, PipelineDefinition> | null>(
modelIds.map((id: string) => [id, null])
);
try {
const body = await client.asCurrentUser.ingest.getPipeline();
for (const [pipelineName, pipelineDefinition] of Object.entries(body)) {
const { processors } = pipelineDefinition as { processors: Array<Record<string, any>> };
for (const processor of processors) {
const id = processor.inference?.model_id;
if (modelIdsMap.has(id)) {
const obj = modelIdsMap.get(id);
if (obj === null) {
modelIdsMap.set(id, { [pipelineName]: pipelineDefinition });
} else {
obj![pipelineName] = pipelineDefinition;
}
}
}
}
} catch (error) {
if (error.statusCode === 404) {
// ES returns 404 when there are no pipelines
// Instead, we should return the modelIdsMap and a 200
return modelIdsMap;
}
throw error;
}
return modelIdsMap;
},
/**
* Provides the ML nodes overview with allocated models.
*/
async getNodesOverview(): Promise<NodesOverviewResponse> {
// TODO set node_id to ml:true when elasticsearch client is updated.
const response = (await mlClient.getMemoryStats()) as MemoryStatsResponse;
const { trained_model_stats: trainedModelStats } = await mlClient.getTrainedModelsStats({
size: 10000,
});
const mlNodes = Object.entries(response.nodes).filter(([, node]) =>
node.roles.includes('ml')
);
const nodeDeploymentStatsResponses: NodeDeploymentStatsResponse[] = mlNodes.map(
([nodeId, node]) => {
const nodeFields = pick(node, NODE_FIELDS) as RequiredNodeFields;
nodeFields.attributes = nodeFields.attributes;
const allocatedModels = (trainedModelStats as TrainedModelStatsResponse[])
.filter(
(d) =>
isDefined(d.deployment_stats) &&
isDefined(d.deployment_stats.nodes) &&
d.deployment_stats.nodes.some((n) => Object.keys(n.node)[0] === nodeId)
)
.map((d) => {
const modelSizeState = d.model_size_stats;
const deploymentStats = d.deployment_stats;
if (!deploymentStats || !modelSizeState) {
throw new Error('deploymentStats or modelSizeState not defined');
}
const { nodes, ...rest } = deploymentStats;
const { node: tempNode, ...nodeRest } = nodes.find(
(v) => Object.keys(v.node)[0] === nodeId
)!;
return {
model_id: d.model_id,
...rest,
...modelSizeState,
node: nodeRest,
};
});
const modelsMemoryUsage = allocatedModels.map((v) => {
return {
model_id: v.model_id,
model_size: v.required_native_memory_bytes,
};
});
const memoryRes = {
adTotalMemory: node.mem.ml.anomaly_detectors_in_bytes,
dfaTotalMemory: node.mem.ml.data_frame_analytics_in_bytes,
trainedModelsTotalMemory: node.mem.ml.native_inference_in_bytes,
};
for (const key of Object.keys(memoryRes)) {
if (memoryRes[key as keyof typeof memoryRes] > 0) {
/**
* The amount of memory needed to load the ML native code shared libraries. The assumption is that the first
* ML job to run on a given node will do this, and then subsequent ML jobs on the same node will reuse the
* same already-loaded code.
*/
memoryRes[key as keyof typeof memoryRes] += node.mem.ml.native_code_overhead_in_bytes;
break;
}
}
return {
id: nodeId,
...nodeFields,
allocated_models: allocatedModels,
memory_overview: {
machine_memory: {
total: node.mem.adjusted_total_in_bytes,
jvm: node.jvm.heap_max_in_bytes,
},
anomaly_detection: {
total: memoryRes.adTotalMemory,
},
dfa_training: {
total: memoryRes.dfaTotalMemory,
},
trained_models: {
total: memoryRes.trainedModelsTotalMemory,
by_model: modelsMemoryUsage,
},
ml_max_in_bytes: node.mem.ml.max_in_bytes,
},
};
}
);
return {
// TODO preserve _nodes from the response when getMemoryStats method is updated to support ml:true filter
_nodes: {
...response._nodes,
total: mlNodes.length,
successful: mlNodes.length,
},
nodes: nodeDeploymentStatsResponses,
};
},
};
}

View file

@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { modelsProvider } from './models_provider';
export { MemoryUsageService } from './memory_usage';

View file

@ -5,16 +5,12 @@
* 2.0.
*/
import { MemoryStatsResponse, ModelService, modelsProvider } from './models_provider';
import { IScopedClusterClient } from '@kbn/core/server';
import { MlClient } from '../../lib/ml_client';
import { MemoryUsageService } from './memory_usage';
import type { MlClient } from '../../lib/ml_client';
import mockResponse from './__mocks__/mock_deployment_response.json';
import type { MemoryStatsResponse } from '../../../common/types/trained_models';
describe('Model service', () => {
const client = {
asInternalUser: {},
} as unknown as jest.Mocked<IScopedClusterClient>;
const mlClient = {
getTrainedModelsStats: jest.fn(() => {
return Promise.resolve({
@ -137,10 +133,10 @@ describe('Model service', () => {
}),
} as unknown as jest.Mocked<MlClient>;
let service: ModelService;
let service: MemoryUsageService;
beforeEach(() => {
service = modelsProvider(client, mlClient);
service = new MemoryUsageService(mlClient);
});
afterEach(() => {});

View file

@ -0,0 +1,277 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import numeral from '@elastic/numeral';
import { pick } from 'lodash';
import { isDefined } from '@kbn/ml-is-defined';
import type {
MemoryUsageInfo,
TrainedModelStatsResponse,
MemoryStatsResponse,
} from '../../../common/types/trained_models';
import type { JobStats } from '../../../common/types/anomaly_detection_jobs';
import type { MlSavedObjectType } from '../../../common/types/saved_objects';
import type { MlClient } from '../../lib/ml_client';
import type {
NodeDeploymentStatsResponse,
NodesOverviewResponse,
} from '../../../common/types/trained_models';
// @ts-expect-error numeral missing value
const AD_EXTRA_MEMORY = numeral('10MB').value();
// @ts-expect-error numeral missing value
const DFA_EXTRA_MEMORY = numeral('5MB').value();
const NODE_FIELDS = ['attributes', 'name', 'roles'] as const;
export type RequiredNodeFields = Pick<estypes.NodesInfoNodeInfo, typeof NODE_FIELDS[number]>;
export class MemoryUsageService {
constructor(private readonly mlClient: MlClient) {}
public async getMemorySizes(itemType?: MlSavedObjectType, node?: string, showClosedJobs = false) {
let memories: MemoryUsageInfo[] = [];
switch (itemType) {
case 'anomaly-detector':
memories = await this.getADJobsSizes();
break;
case 'data-frame-analytics':
memories = await this.getDFAJobsSizes();
break;
case 'trained-model':
memories = await this.getTrainedModelsSizes();
break;
default:
memories = [
...(await this.getADJobsSizes()),
...(await this.getDFAJobsSizes()),
...(await this.getTrainedModelsSizes()),
];
break;
}
return memories.filter((m) => nodeFilter(m, node, showClosedJobs));
}
private async getADJobsSizes() {
const jobs = await this.mlClient.getJobStats();
return jobs.jobs.map(this.getADJobMemorySize);
}
private async getTrainedModelsSizes() {
const [models, stats] = await Promise.all([
this.mlClient.getTrainedModels(),
this.mlClient.getTrainedModelsStats(),
]);
const statsMap = stats.trained_model_stats.reduce<Record<string, estypes.MlTrainedModelStats>>(
(acc, cur) => {
acc[cur.model_id] = cur;
return acc;
},
{}
);
return models.trained_model_configs.map((m) =>
this.getTrainedModelMemorySize(m, statsMap[m.model_id])
);
}
private async getDFAJobsSizes() {
const [jobs, jobsStats] = await Promise.all([
this.mlClient.getDataFrameAnalytics(),
this.mlClient.getDataFrameAnalyticsStats(),
]);
const statsMap = jobsStats.data_frame_analytics.reduce<
Record<string, estypes.MlDataframeAnalytics>
>((acc, cur) => {
acc[cur.id] = cur;
return acc;
}, {});
return jobs.data_frame_analytics.map((j) => this.getDFAJobMemorySize(j, statsMap[j.id]));
}
private getADJobMemorySize(jobStats: JobStats): MemoryUsageInfo {
let memory = 0;
switch (jobStats.model_size_stats.assignment_memory_basis) {
case 'model_memory_limit':
memory = (jobStats.model_size_stats.model_bytes_memory_limit as number) ?? 0;
break;
case 'current_model_bytes':
memory = jobStats.model_size_stats.model_bytes as number;
break;
case 'peak_model_bytes':
memory = (jobStats.model_size_stats.peak_model_bytes as number) ?? 0;
break;
}
const size = memory + AD_EXTRA_MEMORY;
const nodeName = jobStats.node?.name;
return {
id: jobStats.job_id,
type: 'anomaly-detector',
size,
nodeNames: nodeName ? [nodeName] : [],
};
}
private getDFAJobMemorySize(
job: estypes.MlDataframeAnalyticsSummary,
jobStats: estypes.MlDataframeAnalytics
): MemoryUsageInfo {
const mml = job.model_memory_limit ?? '0mb';
// @ts-expect-error numeral missing value
const memory = numeral(mml.toUpperCase()).value();
const size = memory + DFA_EXTRA_MEMORY;
const nodeName = jobStats.node?.name;
return {
id: jobStats.id,
type: 'data-frame-analytics',
size,
nodeNames: nodeName ? [nodeName] : [],
};
}
private getTrainedModelMemorySize(
trainedModel: estypes.MlTrainedModelConfig,
trainedModelStats: estypes.MlTrainedModelStats
): MemoryUsageInfo {
const memory = trainedModelStats.model_size_stats.required_native_memory_bytes;
const size = memory + AD_EXTRA_MEMORY;
const nodes = (trainedModelStats.deployment_stats?.nodes ??
[]) as estypes.MlTrainedModelDeploymentNodesStats[];
return {
id: trainedModelStats.model_id,
type: 'trained-model',
size,
nodeNames: nodes.map((n) => Object.values(n.node)[0].name),
};
}
/**
* Provides the ML nodes overview with allocated models.
*/
async getNodesOverview(): Promise<NodesOverviewResponse> {
// TODO set node_id to ml:true when elasticsearch client is updated.
const response = (await this.mlClient.getMemoryStats()) as MemoryStatsResponse;
const { trained_model_stats: trainedModelStats } = await this.mlClient.getTrainedModelsStats({
size: 10000,
});
const mlNodes = Object.entries(response.nodes).filter(([, node]) => node.roles.includes('ml'));
const nodeDeploymentStatsResponses: NodeDeploymentStatsResponse[] = mlNodes.map(
([nodeId, node]) => {
const nodeFields = pick(node, NODE_FIELDS) as RequiredNodeFields;
nodeFields.attributes = nodeFields.attributes;
const allocatedModels = (trainedModelStats as TrainedModelStatsResponse[])
.filter(
(d) =>
isDefined(d.deployment_stats) &&
isDefined(d.deployment_stats.nodes) &&
d.deployment_stats.nodes.some((n) => Object.keys(n.node)[0] === nodeId)
)
.map((d) => {
const modelSizeState = d.model_size_stats;
const deploymentStats = d.deployment_stats;
if (!deploymentStats || !modelSizeState) {
throw new Error('deploymentStats or modelSizeState not defined');
}
const { nodes, ...rest } = deploymentStats;
const { node: tempNode, ...nodeRest } = nodes.find(
(v) => Object.keys(v.node)[0] === nodeId
)!;
return {
model_id: d.model_id,
...rest,
...modelSizeState,
node: nodeRest,
};
});
const modelsMemoryUsage = allocatedModels.map((v) => {
return {
model_id: v.model_id,
model_size: v.required_native_memory_bytes,
};
});
const memoryRes = {
adTotalMemory: node.mem.ml.anomaly_detectors_in_bytes,
dfaTotalMemory: node.mem.ml.data_frame_analytics_in_bytes,
trainedModelsTotalMemory: node.mem.ml.native_inference_in_bytes,
};
for (const key of Object.keys(memoryRes)) {
if (memoryRes[key as keyof typeof memoryRes] > 0) {
/**
* The amount of memory needed to load the ML native code shared libraries. The assumption is that the first
* ML job to run on a given node will do this, and then subsequent ML jobs on the same node will reuse the
* same already-loaded code.
*/
memoryRes[key as keyof typeof memoryRes] += node.mem.ml.native_code_overhead_in_bytes;
break;
}
}
return {
id: nodeId,
...nodeFields,
allocated_models: allocatedModels,
memory_overview: {
machine_memory: {
total: node.mem.adjusted_total_in_bytes,
jvm: node.jvm.heap_max_in_bytes,
},
anomaly_detection: {
total: memoryRes.adTotalMemory,
},
dfa_training: {
total: memoryRes.dfaTotalMemory,
},
trained_models: {
total: memoryRes.trainedModelsTotalMemory,
by_model: modelsMemoryUsage,
},
ml_max_in_bytes: node.mem.ml.max_in_bytes,
},
};
}
);
return {
// TODO preserve _nodes from the response when getMemoryStats method is updated to support ml:true filter
_nodes: {
...response._nodes,
total: mlNodes.length,
successful: mlNodes.length,
},
nodes: nodeDeploymentStatsResponses,
};
}
}
function nodeFilter(m: MemoryUsageInfo, node?: string, showClosedJobs = false) {
if (m.nodeNames.length === 0) {
return showClosedJobs;
}
if (node === undefined) {
return true;
}
return m.nodeNames.includes(node);
}

View file

@ -0,0 +1,55 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { IScopedClusterClient } from '@kbn/core/server';
import type { PipelineDefinition } from '../../../common/types/trained_models';
export type ModelService = ReturnType<typeof modelsProvider>;
export function modelsProvider(client: IScopedClusterClient) {
return {
/**
* Retrieves the map of model ids and aliases with associated pipelines.
* @param modelIds - Array of models ids and model aliases.
*/
async getModelsPipelines(modelIds: string[]) {
const modelIdsMap = new Map<string, Record<string, PipelineDefinition> | null>(
modelIds.map((id: string) => [id, null])
);
try {
const body = await client.asCurrentUser.ingest.getPipeline();
for (const [pipelineName, pipelineDefinition] of Object.entries(body)) {
const { processors } = pipelineDefinition as { processors: Array<Record<string, any>> };
for (const processor of processors) {
const id = processor.inference?.model_id;
if (modelIdsMap.has(id)) {
const obj = modelIdsMap.get(id);
if (obj === null) {
modelIdsMap.set(id, { [pipelineName]: pipelineDefinition });
} else {
obj![pipelineName] = pipelineDefinition;
}
}
}
}
} catch (error) {
if (error.statusCode === 404) {
// ES returns 404 when there are no pipelines
// Instead, we should return the modelIdsMap and a 200
return modelIdsMap;
}
throw error;
}
return modelIdsMap;
},
};
}

View file

@ -47,6 +47,7 @@ import { jobServiceRoutes } from './routes/job_service';
import { savedObjectsRoutes } from './routes/saved_objects';
import { jobValidationRoutes } from './routes/job_validation';
import { resultsServiceRoutes } from './routes/results_service';
import { modelManagementRoutes } from './routes/model_management';
import { systemRoutes } from './routes/system';
import { MlLicense } from '../common/license';
import { createSharedServices, SharedServices } from './shared_services';
@ -217,6 +218,7 @@ export class MlServerPlugin
jobRoutes(routeInit);
jobServiceRoutes(routeInit);
managementRoutes(routeInit);
modelManagementRoutes(routeInit);
resultsServiceRoutes(routeInit);
jobValidationRoutes(routeInit);
savedObjectsRoutes(routeInit, {

View file

@ -170,7 +170,6 @@
"GetTrainedModel",
"GetTrainedModelStats",
"GetTrainedModelStatsById",
"GetTrainedModelsNodesOverview",
"GetTrainedModelPipelines",
"StartTrainedModelDeployment",
"UpdateTrainedModelDeployment",
@ -184,6 +183,10 @@
"PreviewAlert",
"Management",
"ManagementList"
"ManagementList",
"ModelManagement",
"GetModelManagementNodesOverview",
"GetModelManagementMemoryUsage"
]
}

View file

@ -0,0 +1,98 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { schema } from '@kbn/config-schema';
import { RouteInitialization } from '../types';
import { wrapError } from '../client/error_wrapper';
import { MemoryUsageService } from '../models/model_management';
import { itemTypeLiterals } from './schemas/saved_objects';
export function modelManagementRoutes({ router, routeGuard }: RouteInitialization) {
/**
* @apiGroup ModelManagement
*
* @api {get} /api/ml/model_management/nodes_overview Get node overview about the models allocation
* @apiName GetModelManagementNodesOverview
* @apiDescription Retrieves the list of ML nodes with memory breakdown and allocated models info
*/
router.get(
{
path: '/api/ml/model_management/nodes_overview',
validate: {},
options: {
tags: [
'access:ml:canViewMlNodes',
'access:ml:canGetDataFrameAnalytics',
'access:ml:canGetJobs',
'access:ml:canGetTrainedModels',
],
},
},
routeGuard.fullLicenseAPIGuard(async ({ client, mlClient, response }) => {
try {
const memoryUsageService = new MemoryUsageService(mlClient);
const result = await memoryUsageService.getNodesOverview();
return response.ok({
body: result,
});
} catch (e) {
return response.customError(wrapError(e));
}
})
);
/**
* @apiGroup ModelManagement
*
* @api {get} /api/ml/model_management/memory_usage Memory usage for jobs and trained models
* @apiName GetModelManagementMemoryUsage
* @apiDescription Returns the memory usage for jobs and trained models
*/
router.get(
{
path: '/api/ml/model_management/memory_usage',
validate: {
query: schema.object({
type: schema.maybe(itemTypeLiterals),
node: schema.maybe(schema.string()),
showClosedJobs: schema.maybe(schema.boolean()),
}),
},
options: {
tags: [
'access:ml:canViewMlNodes',
'access:ml:canGetDataFrameAnalytics',
'access:ml:canGetJobs',
'access:ml:canGetTrainedModels',
],
},
},
routeGuard.fullLicenseAPIGuard(async ({ mlClient, response, request }) => {
try {
const memoryUsageService = new MemoryUsageService(mlClient);
return response.ok({
body: await memoryUsageService.getMemorySizes(
request.query.type,
request.query.node,
request.query.showClosedJobs
),
});
} catch (e) {
return response.customError(wrapError(e));
}
})
);
}

View file

@ -19,6 +19,7 @@ export const itemTypeLiterals = schema.oneOf([
]);
export const itemTypeSchema = schema.object({ jobType: itemTypeLiterals });
export const jobTypeSchema = schema.object({ jobType: jobTypeLiterals });
export const updateJobsSpaces = schema.object({
jobType: jobTypeLiterals,

View file

@ -19,10 +19,11 @@ import {
pipelineSimulateBody,
updateDeploymentParamsSchema,
} from './schemas/inference_schema';
import { modelsProvider } from '../models/data_frame_analytics';
import { TrainedModelConfigResponse } from '../../common/types/trained_models';
import { mlLog } from '../lib/log';
import { forceQuerySchema } from './schemas/anomaly_detectors_schema';
import { modelsProvider } from '../models/model_management';
export function trainedModelsRoutes({ router, routeGuard }: RouteInitialization) {
/**
@ -68,7 +69,7 @@ export function trainedModelsRoutes({ router, routeGuard }: RouteInitialization)
)
);
const pipelinesResponse = await modelsProvider(client, mlClient).getModelsPipelines(
const pipelinesResponse = await modelsProvider(client).getModelsPipelines(
modelIdsAndAliases
);
for (const model of result) {
@ -178,9 +179,7 @@ export function trainedModelsRoutes({ router, routeGuard }: RouteInitialization)
routeGuard.fullLicenseAPIGuard(async ({ client, request, mlClient, response }) => {
try {
const { modelId } = request.params;
const result = await modelsProvider(client, mlClient).getModelsPipelines(
modelId.split(',')
);
const result = await modelsProvider(client).getModelsPipelines(modelId.split(','));
return response.ok({
body: [...result].map(([id, pipelines]) => ({ model_id: id, pipelines })),
});
@ -260,38 +259,6 @@ export function trainedModelsRoutes({ router, routeGuard }: RouteInitialization)
})
);
/**
* @apiGroup TrainedModels
*
* @api {get} /api/ml/trained_models/nodes_overview Get node overview about the models allocation
* @apiName GetTrainedModelsNodesOverview
* @apiDescription Retrieves the list of ML nodes with memory breakdown and allocated models info
*/
router.get(
{
path: '/api/ml/trained_models/nodes_overview',
validate: {},
options: {
tags: [
'access:ml:canViewMlNodes',
'access:ml:canGetDataFrameAnalytics',
'access:ml:canGetJobs',
'access:ml:canGetTrainedModels',
],
},
},
routeGuard.fullLicenseAPIGuard(async ({ client, mlClient, request, response }) => {
try {
const result = await modelsProvider(client, mlClient).getNodesOverview();
return response.ok({
body: result,
});
} catch (e) {
return response.customError(wrapError(e));
}
})
);
/**
* @apiGroup TrainedModels
*

View file

@ -21232,7 +21232,6 @@
"xpack.ml.deepLink.filterListsSettings": "Listes de filtres",
"xpack.ml.deepLink.indexDataVisualizer": "Index Data Visualizer (Visualiseur de données pour les index)",
"xpack.ml.deepLink.modelManagement": "Gestion des modèles",
"xpack.ml.deepLink.nodesOverview": "Nœuds",
"xpack.ml.deepLink.overview": "Aperçu",
"xpack.ml.deepLink.settings": "Paramètres",
"xpack.ml.deepLink.trainedModels": "Modèles entraînés",
@ -21772,8 +21771,6 @@
"xpack.ml.mlEntitySelector.dfaOptionsLabel": "Analyse du cadre de données",
"xpack.ml.mlEntitySelector.fetchError": "Impossible de récupérer les entités de ML",
"xpack.ml.mlEntitySelector.trainedModelsLabel": "Modèles entraînés",
"xpack.ml.modelManagement.nodesOverview.docTitle": "Nœuds",
"xpack.ml.modelManagement.nodesOverviewHeader": "Nœuds",
"xpack.ml.modelManagement.trainedModels.docTitle": "Modèles entraînés",
"xpack.ml.modelManagement.trainedModelsHeader": "Modèles entraînés",
"xpack.ml.modelManagementLabel": "Gestion des modèles",
@ -21884,7 +21881,6 @@
"xpack.ml.navMenu.logCategorizationLinkText": "Analyse du modèle de log",
"xpack.ml.navMenu.mlAppNameText": "Machine Learning",
"xpack.ml.navMenu.modelManagementText": "Gestion des modèles",
"xpack.ml.navMenu.nodesOverviewText": "Nœuds",
"xpack.ml.navMenu.notificationsTabLinkText": "Notifications",
"xpack.ml.navMenu.overviewTabLinkText": "Aperçu",
"xpack.ml.navMenu.settingsTabLinkText": "Paramètres",

View file

@ -21212,7 +21212,6 @@
"xpack.ml.deepLink.filterListsSettings": "フィルターリスト",
"xpack.ml.deepLink.indexDataVisualizer": "インデックスデータビジュアライザー",
"xpack.ml.deepLink.modelManagement": "モデル管理",
"xpack.ml.deepLink.nodesOverview": "ノード",
"xpack.ml.deepLink.overview": "概要",
"xpack.ml.deepLink.settings": "設定",
"xpack.ml.deepLink.trainedModels": "学習済みモデル",
@ -21752,8 +21751,6 @@
"xpack.ml.mlEntitySelector.dfaOptionsLabel": "データフレーム分析",
"xpack.ml.mlEntitySelector.fetchError": "MLエンティティを取得できませんでした",
"xpack.ml.mlEntitySelector.trainedModelsLabel": "学習済みモデル",
"xpack.ml.modelManagement.nodesOverview.docTitle": "ノード",
"xpack.ml.modelManagement.nodesOverviewHeader": "ノード",
"xpack.ml.modelManagement.trainedModels.docTitle": "学習済みモデル",
"xpack.ml.modelManagement.trainedModelsHeader": "学習済みモデル",
"xpack.ml.modelManagementLabel": "モデル管理",
@ -21864,7 +21861,6 @@
"xpack.ml.navMenu.logCategorizationLinkText": "ログパターン分析",
"xpack.ml.navMenu.mlAppNameText": "機械学習",
"xpack.ml.navMenu.modelManagementText": "モデル管理",
"xpack.ml.navMenu.nodesOverviewText": "ノード",
"xpack.ml.navMenu.notificationsTabLinkText": "通知",
"xpack.ml.navMenu.overviewTabLinkText": "概要",
"xpack.ml.navMenu.settingsTabLinkText": "設定",

View file

@ -21242,7 +21242,6 @@
"xpack.ml.deepLink.filterListsSettings": "筛选列表",
"xpack.ml.deepLink.indexDataVisualizer": "索引数据可视化工具",
"xpack.ml.deepLink.modelManagement": "模型管理",
"xpack.ml.deepLink.nodesOverview": "节点",
"xpack.ml.deepLink.overview": "概览",
"xpack.ml.deepLink.settings": "设置",
"xpack.ml.deepLink.trainedModels": "已训练模型",
@ -21782,8 +21781,6 @@
"xpack.ml.mlEntitySelector.dfaOptionsLabel": "数据帧分析",
"xpack.ml.mlEntitySelector.fetchError": "无法提取 ML 实体",
"xpack.ml.mlEntitySelector.trainedModelsLabel": "已训练模型",
"xpack.ml.modelManagement.nodesOverview.docTitle": "节点",
"xpack.ml.modelManagement.nodesOverviewHeader": "节点",
"xpack.ml.modelManagement.trainedModels.docTitle": "已训练模型",
"xpack.ml.modelManagement.trainedModelsHeader": "已训练模型",
"xpack.ml.modelManagementLabel": "模型管理",
@ -21894,7 +21891,6 @@
"xpack.ml.navMenu.logCategorizationLinkText": "日志模式分析",
"xpack.ml.navMenu.mlAppNameText": "Machine Learning",
"xpack.ml.navMenu.modelManagementText": "模型管理",
"xpack.ml.navMenu.nodesOverviewText": "节点",
"xpack.ml.navMenu.notificationsTabLinkText": "通知",
"xpack.ml.navMenu.overviewTabLinkText": "概览",
"xpack.ml.navMenu.settingsTabLinkText": "设置",