diff --git a/frontend/src/api/prometheus/NimPerformanceMetrics.ts b/frontend/src/api/prometheus/NimPerformanceMetrics.ts new file mode 100644 index 0000000000..34baa32962 --- /dev/null +++ b/frontend/src/api/prometheus/NimPerformanceMetrics.ts @@ -0,0 +1,337 @@ +import React from 'react'; +import { NimMetricGraphDefinition } from '~/concepts/metrics/kserve/types'; +import { defaultResponsePredicate } from '~/api/prometheus/usePrometheusQueryRange'; +import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; +import { TimeframeTitle } from '~/concepts/metrics/types'; +import useQueryRangeResourceData from '~/api/prometheus/useQueryRangeResourceData'; +import { PendingContextResourceData, PrometheusQueryRangeResultValue } from '~/types'; +import { DEFAULT_PENDING_CONTEXT_RESOURCE } from '~/api/prometheus/const'; + +type RequestCountData = { + data: { + successCount: PendingContextResourceData; + failedCount: PendingContextResourceData; + }; + refreshAll: () => void; +}; + + +// Graph #1 - KV Cache usage over time +type KVCacheUsageData = { + data: { + kvCacheUsage: PendingContextResourceData; + }; + refreshAll: () => void; +}; + + +export const useFetchNimKVCacheUsageData = ( + metricsDef: NimMetricGraphDefinition, + timeframe: TimeframeTitle, + endInMs: number, + namespace: string, +): KVCacheUsageData => { + const active = useIsAreaAvailable(SupportedArea.K_SERVE_METRICS).status; + + const kvCacheUsage = useQueryRangeResourceData( + active, + metricsDef.queries[0]?.query, + endInMs, + timeframe, + defaultResponsePredicate, + namespace, + ); + + const data = React.useMemo( + () => ({ + kvCacheUsage, + }), + [kvCacheUsage], + ); + + return useAllSettledContextResourceData(data, { + kvCacheUsage: DEFAULT_PENDING_CONTEXT_RESOURCE, + }); +}; + + +// Graph #3 - Total Prompt Token Count and Total Generation Token Count +type TokensCountData = { + data: { + totalPromptTokenCount: PendingContextResourceData; + totalGenerationTokenCount: PendingContextResourceData; + }; + refreshAll: () => void; +}; + + +export const useFetchNimTokensCountData = ( + metricsDef: NimMetricGraphDefinition, + timeframe: TimeframeTitle, + endInMs: number, + namespace: string, +): TokensCountData => { + const active = useIsAreaAvailable(SupportedArea.K_SERVE_METRICS).status; + + // Extract the queries for "Total Prompt Token Count" and "Total Generation Token Count + const totalPromptTokenCount = useQueryRangeResourceData( + active, + metricsDef.queries[0]?.query, + endInMs, + timeframe, + defaultResponsePredicate, + namespace, + ); + + const totalGenerationTokenCount = useQueryRangeResourceData( + active, + metricsDef.queries[1]?.query, + endInMs, + timeframe, + defaultResponsePredicate, + namespace, + ); + + const data = React.useMemo( + () => ({ + totalPromptTokenCount, totalGenerationTokenCount + }), + [totalPromptTokenCount, totalGenerationTokenCount], + ); + + return useAllSettledContextResourceData(data, { + totalPromptTokenCount: DEFAULT_PENDING_CONTEXT_RESOURCE, + totalGenerationTokenCount: DEFAULT_PENDING_CONTEXT_RESOURCE, + }); +}; + + +// Graph #4 - Time to First Token +type TimeToFirstTokenData = { + data: { + timeToFirstToken: PendingContextResourceData; + }; + refreshAll: () => void; +}; + + +export const useFetchNimTimeToFirstTokenData = ( + metricsDef: NimMetricGraphDefinition, + timeframe: TimeframeTitle, + endInMs: number, + namespace: string, +): TimeToFirstTokenData => { + const active = useIsAreaAvailable(SupportedArea.K_SERVE_METRICS).status; + + const timeToFirstToken = useQueryRangeResourceData( + active, + metricsDef.queries[0]?.query, + endInMs, + timeframe, + defaultResponsePredicate, + namespace, + ); + + const data = React.useMemo( + () => ({ + timeToFirstToken, + }), + [timeToFirstToken], + ); + + return useAllSettledContextResourceData(data, { + timeToFirstToken: DEFAULT_PENDING_CONTEXT_RESOURCE, + }); +}; + +// Graph #5 +type TimePerOutputTokenData = { + data: { + timePerOutputToken: PendingContextResourceData; + }; + refreshAll: () => void; +}; +export const useFetchNimTimePerOutputTokenData = ( + metricsDef: NimMetricGraphDefinition, + timeframe: TimeframeTitle, + endInMs: number, + namespace: string, +): TimePerOutputTokenData => { + // Check if Nim metrics are active + const active = useIsAreaAvailable(SupportedArea.K_SERVE_METRICS).status; + // Extract the query for TIME_PER_OUTPUT_TOKEN + const timePerOutputTokenQuery = metricsDef.queries[0].query; // Assumes it's the first query in the metric definition + // Fetch data using useQueryRangeResourceData + const timePerOutputToken = useQueryRangeResourceData( + active, + timePerOutputTokenQuery, + endInMs, + timeframe, + defaultResponsePredicate, + namespace, + ); + // Memoize the fetched data + const data = React.useMemo( + () => ({ + timePerOutputToken, + }), + [timePerOutputToken], + ); + // Return all-settled context resource data + return useAllSettledContextResourceData(data, { + timePerOutputToken: DEFAULT_PENDING_CONTEXT_RESOURCE, + }); +}; + +// Graph #6 +type RequestsOutcomesData = { + data: { + successCount: PendingContextResourceData; + failedCount: PendingContextResourceData; + }; + refreshAll: () => void; +}; + +export const useFetchNimRequestsOutcomesData = ( + metricsDef: NimMetricGraphDefinition, + timeframe: TimeframeTitle, + endInMs: number, + namespace: string, +): RequestsOutcomesData => { + const active = useIsAreaAvailable(SupportedArea.K_SERVE_METRICS).status; + + const successQuery = metricsDef.queries[0]?.query; + const failedQuery = metricsDef.queries[1]?.query; + + const successCount = useQueryRangeResourceData( + active, + successQuery, + endInMs, + timeframe, + defaultResponsePredicate, + namespace, + ); + + const failedCount = useQueryRangeResourceData( + active, + failedQuery, + endInMs, + timeframe, + defaultResponsePredicate, + namespace, + ); + + const data = React.useMemo( + () => ({ + successCount, + failedCount, + }), + [failedCount, successCount], + ); + + return useAllSettledContextResourceData(data, { + successCount: DEFAULT_PENDING_CONTEXT_RESOURCE, + failedCount: DEFAULT_PENDING_CONTEXT_RESOURCE, + }); +}; + +// Graph #2 +type CurrentRequestsData = { + data: { + requestsWaiting: PendingContextResourceData; + requestsRunning: PendingContextResourceData; + maxRequests: PendingContextResourceData; + }; + refreshAll: () => void; +}; + +export const useFetchNimCurrentRequestsData = ( + metricsDef: NimMetricGraphDefinition, + timeframe: TimeframeTitle, + endInMs: number, + namespace: string, +): CurrentRequestsData => { + // Check if Nim metrics are active + const active = useIsAreaAvailable(SupportedArea.K_SERVE_METRICS).status; + + // Extract the queries for "Requests waiting", "Requests running", and "Max requests" + const requestsWaitingQuery = metricsDef.queries[0].query; + const requestsRunningQuery = metricsDef.queries[1].query; + const maxRequestsQuery = metricsDef.queries[2].query; + + // Fetch data using useQueryRangeResourceData + const requestsWaiting = useQueryRangeResourceData( + active, + requestsWaitingQuery, + endInMs, + timeframe, + defaultResponsePredicate, + namespace, + ); + + const requestsRunning = useQueryRangeResourceData( + active, + requestsRunningQuery, + endInMs, + timeframe, + defaultResponsePredicate, + namespace, + ); + + const maxRequests = useQueryRangeResourceData( + active, + maxRequestsQuery, + endInMs, + timeframe, + defaultResponsePredicate, + namespace, + ); + + // Combine the fetched data + const data = React.useMemo( + () => ({ + requestsWaiting, + requestsRunning, + maxRequests, + }), + [requestsWaiting, requestsRunning, maxRequests], + ); + + // Use helper to handle pending state and refresh functionality + return useAllSettledContextResourceData(data, { + requestsWaiting: DEFAULT_PENDING_CONTEXT_RESOURCE, + requestsRunning: DEFAULT_PENDING_CONTEXT_RESOURCE, + maxRequests: DEFAULT_PENDING_CONTEXT_RESOURCE, + }); +}; + + +const useAllSettledContextResourceData = < + T, + U extends Record>, +>( + data: U, + defaultValue: U, +) => { + const refreshAll = React.useCallback(() => { + Object.values(data).forEach((x) => x.refresh()); + }, [data]); + + const result = React.useMemo( + () => ({ + data, + refreshAll, + }), + [data, refreshAll], + ); + + // store the result in a reference and only update the reference so long as there are no pending queries + const resultRef = React.useRef({ data: defaultValue, refreshAll }); + + // only update the ref when all values are settled, i.e. not pending. + if (!Object.values(result.data).some((x) => x.pending)) { + resultRef.current = result; + } + + return resultRef.current; +}; diff --git a/frontend/src/api/prometheus/kservePerformanceMetrics.ts b/frontend/src/api/prometheus/kservePerformanceMetrics.ts index 129b3bd349..7ef4518ff6 100644 --- a/frontend/src/api/prometheus/kservePerformanceMetrics.ts +++ b/frontend/src/api/prometheus/kservePerformanceMetrics.ts @@ -178,6 +178,296 @@ export const useFetchKserveMemoryUsageData = ( }); }; +// Graph #1 - KV Cache usage over time +type KVCacheUsageData = { + data: { + kvCacheUsage: PendingContextResourceData; + }; + refreshAll: () => void; +}; + + +export const useFetchKserveKVCacheUsageData = ( + metricsDef: KserveMetricGraphDefinition, + timeframe: TimeframeTitle, + endInMs: number, + namespace: string, +): KVCacheUsageData => { + const active = useIsAreaAvailable(SupportedArea.K_SERVE_METRICS).status; + + const kvCacheUsage = useQueryRangeResourceData( + active, + metricsDef.queries[0]?.query, + endInMs, + timeframe, + defaultResponsePredicate, + namespace, + ); + + const data = React.useMemo( + () => ({ + kvCacheUsage, + }), + [kvCacheUsage], + ); + + return useAllSettledContextResourceData(data, { + kvCacheUsage: DEFAULT_PENDING_CONTEXT_RESOURCE, + }); +}; + + +// Graph #3 - Total Prompt Token Count and Total Generation Token Count +type TokensCountData = { + data: { + totalPromptTokenCount: PendingContextResourceData; + totalGenerationTokenCount: PendingContextResourceData; + }; + refreshAll: () => void; +}; + + +export const useFetchKserveTokensCountData = ( + metricsDef: KserveMetricGraphDefinition, + timeframe: TimeframeTitle, + endInMs: number, + namespace: string, +): TokensCountData => { + const active = useIsAreaAvailable(SupportedArea.K_SERVE_METRICS).status; + + // Extract the queries for "Total Prompt Token Count" and "Total Generation Token Count + const totalPromptTokenCount = useQueryRangeResourceData( + active, + metricsDef.queries[0]?.query, + endInMs, + timeframe, + defaultResponsePredicate, + namespace, + ); + + const totalGenerationTokenCount = useQueryRangeResourceData( + active, + metricsDef.queries[1]?.query, + endInMs, + timeframe, + defaultResponsePredicate, + namespace, + ); + + const data = React.useMemo( + () => ({ + totalPromptTokenCount,totalGenerationTokenCount + }), + [totalPromptTokenCount, totalGenerationTokenCount], + ); + + return useAllSettledContextResourceData(data, { + totalPromptTokenCount: DEFAULT_PENDING_CONTEXT_RESOURCE, + totalGenerationTokenCount: DEFAULT_PENDING_CONTEXT_RESOURCE, + }); +}; + + +// Graph #4 - Time to First Token +type TimeToFirstTokenData = { + data: { + timeToFirstToken: PendingContextResourceData; + }; + refreshAll: () => void; +}; + + +export const useFetchKserveTimeToFirstTokenData = ( + metricsDef: KserveMetricGraphDefinition, + timeframe: TimeframeTitle, + endInMs: number, + namespace: string, +): TimeToFirstTokenData => { + const active = useIsAreaAvailable(SupportedArea.K_SERVE_METRICS).status; + + const timeToFirstToken = useQueryRangeResourceData( + active, + metricsDef.queries[0]?.query, + endInMs, + timeframe, + defaultResponsePredicate, + namespace, + ); + + const data = React.useMemo( + () => ({ + timeToFirstToken, + }), + [timeToFirstToken], + ); + + return useAllSettledContextResourceData(data, { + timeToFirstToken: DEFAULT_PENDING_CONTEXT_RESOURCE, + }); +}; + +// Graph #5 +type TimePerOutputTokenData = { + data: { + timePerOutputToken: PendingContextResourceData; + }; + refreshAll: () => void; +}; +export const useFetchKserveTimePerOutputTokenData = ( + metricsDef: KserveMetricGraphDefinition, + timeframe: TimeframeTitle, + endInMs: number, + namespace: string, +): TimePerOutputTokenData => { + // Check if KServe metrics are active + const active = useIsAreaAvailable(SupportedArea.K_SERVE_METRICS).status; + // Extract the query for TIME_PER_OUTPUT_TOKEN + const timePerOutputTokenQuery = metricsDef.queries[0].query; // Assumes it's the first query in the metric definition + // Fetch data using useQueryRangeResourceData + const timePerOutputToken = useQueryRangeResourceData( + active, + timePerOutputTokenQuery, + endInMs, + timeframe, + defaultResponsePredicate, + namespace, + ); + // Memoize the fetched data + const data = React.useMemo( + () => ({ + timePerOutputToken, + }), + [timePerOutputToken], + ); + // Return all-settled context resource data + return useAllSettledContextResourceData(data, { + timePerOutputToken: DEFAULT_PENDING_CONTEXT_RESOURCE, + }); +}; + +// Graph #6 +type RequestsOutcomesData = { + data: { + successCount: PendingContextResourceData; + failedCount: PendingContextResourceData; + }; + refreshAll: () => void; +}; + +export const useFetchKserveRequestsOutcomesData = ( + metricsDef: KserveMetricGraphDefinition, + timeframe: TimeframeTitle, + endInMs: number, + namespace: string, +): RequestsOutcomesData => { + const active = useIsAreaAvailable(SupportedArea.K_SERVE_METRICS).status; + + const successQuery = metricsDef.queries[0]?.query; + const failedQuery = metricsDef.queries[1]?.query; + + const successCount = useQueryRangeResourceData( + active, + successQuery, + endInMs, + timeframe, + defaultResponsePredicate, + namespace, + ); + + const failedCount = useQueryRangeResourceData( + active, + failedQuery, + endInMs, + timeframe, + defaultResponsePredicate, + namespace, + ); + + const data = React.useMemo( + () => ({ + successCount, + failedCount, + }), + [failedCount, successCount], + ); + + return useAllSettledContextResourceData(data, { + successCount: DEFAULT_PENDING_CONTEXT_RESOURCE, + failedCount: DEFAULT_PENDING_CONTEXT_RESOURCE, + }); +}; + +// Graph #2 +type CurrentRequestsData = { + data: { + requestsWaiting: PendingContextResourceData; + requestsRunning: PendingContextResourceData; + maxRequests: PendingContextResourceData; + }; + refreshAll: () => void; +}; + +export const useFetchKserveCurrentRequestsData = ( + metricsDef: KserveMetricGraphDefinition, + timeframe: TimeframeTitle, + endInMs: number, + namespace: string, +): CurrentRequestsData => { + // Check if KServe metrics are active + const active = useIsAreaAvailable(SupportedArea.K_SERVE_METRICS).status; + + // Extract the queries for "Requests waiting", "Requests running", and "Max requests" + const requestsWaitingQuery = metricsDef.queries[0].query; + const requestsRunningQuery = metricsDef.queries[1].query; + const maxRequestsQuery = metricsDef.queries[2].query; + + // Fetch data using useQueryRangeResourceData + const requestsWaiting = useQueryRangeResourceData( + active, + requestsWaitingQuery, + endInMs, + timeframe, + defaultResponsePredicate, + namespace, + ); + + const requestsRunning = useQueryRangeResourceData( + active, + requestsRunningQuery, + endInMs, + timeframe, + defaultResponsePredicate, + namespace, + ); + + const maxRequests = useQueryRangeResourceData( + active, + maxRequestsQuery, + endInMs, + timeframe, + defaultResponsePredicate, + namespace, + ); + + // Combine the fetched data + const data = React.useMemo( + () => ({ + requestsWaiting, + requestsRunning, + maxRequests, + }), + [requestsWaiting, requestsRunning, maxRequests], + ); + + // Use helper to handle pending state and refresh functionality + return useAllSettledContextResourceData(data, { + requestsWaiting: DEFAULT_PENDING_CONTEXT_RESOURCE, + requestsRunning: DEFAULT_PENDING_CONTEXT_RESOURCE, + maxRequests: DEFAULT_PENDING_CONTEXT_RESOURCE, + }); +}; + + const useAllSettledContextResourceData = < T, U extends Record>, diff --git a/frontend/src/concepts/metrics/kserve/NimMetricsContext.tsx b/frontend/src/concepts/metrics/kserve/NimMetricsContext.tsx new file mode 100644 index 0000000000..2e9a11be12 --- /dev/null +++ b/frontend/src/concepts/metrics/kserve/NimMetricsContext.tsx @@ -0,0 +1,121 @@ +import * as React from 'react'; +import { + Bullseye, + EmptyState, + EmptyStateBody, + EmptyStateVariant, + Spinner, +} from '@patternfly/react-core'; +import { CubesIcon, ErrorCircleOIcon } from '@patternfly/react-icons'; +import { MetricsCommonContext } from '~/concepts/metrics/MetricsCommonContext'; +import useKserveMetricsConfigMap from '~/concepts/metrics/kserve/useKserveMetricsConfigMap'; +import useNimMetricsGraphDefinitions from '~/concepts/metrics/kserve/useNimMetricsGraphDefinition'; +import useRefreshInterval from '~/utilities/useRefreshInterval'; +import { RefreshIntervalValue } from '~/concepts/metrics/const'; +import { RefreshIntervalTitle, TimeframeTitle } from '~/concepts/metrics/types'; +import { NimMetricGraphDefinition } from '~/concepts/metrics/kserve/types'; +import { conditionalArea, SupportedArea } from '~/concepts/areas'; + +type NimMetricsContextProps = { + namespace: string; + timeframe: TimeframeTitle; + refreshInterval: RefreshIntervalTitle; + lastUpdateTime: number; + graphDefinitions: NimMetricGraphDefinition[]; +}; + +export const NimMetricsContext = React.createContext({ + namespace: '', + timeframe: TimeframeTitle.ONE_DAY, + refreshInterval: RefreshIntervalTitle.FIVE_MINUTES, + lastUpdateTime: 0, + graphDefinitions: [], +}); + +type NimMetricsContextProviderProps = { + children: React.ReactNode; + namespace: string; + modelName: string; +}; + +export const NimMetricsContextProvider = conditionalArea( + SupportedArea.K_SERVE_METRICS, + true, +)(({ children, namespace, modelName }) => { + const { currentTimeframe, currentRefreshInterval, lastUpdateTime, setLastUpdateTime } = + React.useContext(MetricsCommonContext); + const [configMap, configMapLoaded, configMapError] = useKserveMetricsConfigMap( + namespace, + modelName, + ); + const { + graphDefinitions, + error: graphDefinitionsError, + loaded: graphDefinitionsLoaded, + supported, + } = useNimMetricsGraphDefinitions(configMap); + + const loaded = configMapLoaded && graphDefinitionsLoaded; + + const error = graphDefinitionsError || configMapError; + + const refreshAllMetrics = React.useCallback(() => { + setLastUpdateTime(Date.now()); + }, [setLastUpdateTime]); + + useRefreshInterval(RefreshIntervalValue[currentRefreshInterval], refreshAllMetrics); + + const contextValue = React.useMemo( + () => ({ + namespace, + lastUpdateTime, + refreshInterval: currentRefreshInterval, + timeframe: currentTimeframe, + graphDefinitions, + }), + [currentRefreshInterval, currentTimeframe, graphDefinitions, lastUpdateTime, namespace], + ); + + if (error) { + return ( + + Error loading metrics configuration + + ); + } + + if (!loaded) { + return ( + + + + ); + } + + if (!supported) { + return ( + + + {modelName} is using a custom serving runtime. Metrics are only supported for models + served via a pre-installed runtime when the single-model serving platform is enabled for a + project. + + + ); + } + + return ( + {children} + ); +}); diff --git a/frontend/src/concepts/metrics/kserve/const.ts b/frontend/src/concepts/metrics/kserve/const.ts index 674f29a2d8..5f24ef9b26 100644 --- a/frontend/src/concepts/metrics/kserve/const.ts +++ b/frontend/src/concepts/metrics/kserve/const.ts @@ -6,3 +6,14 @@ export enum KserveMetricsGraphTypes { REQUEST_COUNT = 'REQUEST_COUNT', MEAN_LATENCY = 'MEAN_LATENCY', } + +export enum NimMetricsGraphTypes { + TIME_TO_FIRST_TOKEN = 'TIME_TO_FIRST_TOKEN', + TIME_PER_OUTPUT_TOKEN = 'TIME_PER_OUTPUT_TOKEN', + KV_CACHE = 'KV_CACHE', + CURRENT_REQUESTS = 'CURRENT_REQUESTS', + TOKENS_COUNT = 'TOKENS_COUNT', + REQUEST_OUTCOMES = 'REQUEST_OUTCOMES' +} + + diff --git a/frontend/src/concepts/metrics/kserve/content/KservePerformanceGraphs.tsx b/frontend/src/concepts/metrics/kserve/content/KservePerformanceGraphs.tsx index 6c785721e2..9740a69ea0 100644 --- a/frontend/src/concepts/metrics/kserve/content/KservePerformanceGraphs.tsx +++ b/frontend/src/concepts/metrics/kserve/content/KservePerformanceGraphs.tsx @@ -55,6 +55,7 @@ const KservePerformanceGraphs: React.FC = ({ ); } + // Condition IS necessary as graph types are provided by the backend. // We need to guard against receiving an unknown value at runtime and fail gracefully. // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition diff --git a/frontend/src/concepts/metrics/kserve/content/KserveRequestCountGraph.tsx b/frontend/src/concepts/metrics/kserve/content/KserveRequestCountGraph.tsx index a3eac20af5..c28f92114a 100644 --- a/frontend/src/concepts/metrics/kserve/content/KserveRequestCountGraph.tsx +++ b/frontend/src/concepts/metrics/kserve/content/KserveRequestCountGraph.tsx @@ -40,4 +40,4 @@ const KserveRequestCountGraph: React.FC = ({ ); }; -export default KserveRequestCountGraph; +export default KserveRequestCountGraph; \ No newline at end of file diff --git a/frontend/src/concepts/metrics/kserve/content/NIMCurrentRequestsGraph.tsx b/frontend/src/concepts/metrics/kserve/content/NIMCurrentRequestsGraph.tsx new file mode 100644 index 0000000000..55372bfa6a --- /dev/null +++ b/frontend/src/concepts/metrics/kserve/content/NIMCurrentRequestsGraph.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { NimMetricGraphDefinition } from '~/concepts/metrics/kserve/types'; +import { TimeframeTitle } from '~/concepts/metrics/types'; +import MetricsChart from '~/pages/modelServing/screens/metrics/MetricsChart'; +import { useFetchNimCurrentRequestsData } from '~/api/prometheus/NimPerformanceMetrics'; +import { convertPrometheusNaNToZero } from '~/pages/modelServing/screens/metrics/utils'; +import { MetricsChartTypes } from '~/pages/modelServing/screens/metrics/types'; +type NimCurrentRequestsGraphProps = { + graphDefinition: NimMetricGraphDefinition; // Contains queries and title + timeframe: TimeframeTitle; // Time range + end: number; // End timestamp + namespace: string; // Namespace +}; +const NimCurrentRequestsGraph: React.FC = ({ + graphDefinition, + timeframe, + end, + namespace, +}) => { + // Fetch the data for "Running", "Waiting", and "Max Requests" + const { + data: { requestsWaiting, requestsRunning, maxRequests }, + } = useFetchNimCurrentRequestsData(graphDefinition, timeframe, end, namespace); + return ( + + ); +}; +export default NimCurrentRequestsGraph; \ No newline at end of file diff --git a/frontend/src/concepts/metrics/kserve/content/NIMKVCacheUsageGraph.tsx b/frontend/src/concepts/metrics/kserve/content/NIMKVCacheUsageGraph.tsx new file mode 100644 index 0000000000..015ab70813 --- /dev/null +++ b/frontend/src/concepts/metrics/kserve/content/NIMKVCacheUsageGraph.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { NimMetricGraphDefinition } from '~/concepts/metrics/kserve/types'; +import { TimeframeTitle } from '~/concepts/metrics/types'; +import { useFetchNimKVCacheUsageData } from '~/api/prometheus/NimPerformanceMetrics'; +import MetricsChart from '~/pages/modelServing/screens/metrics/MetricsChart'; +import { MetricsChartTypes } from '~/pages/modelServing/screens/metrics/types'; +import { toPercentage } from '~/pages/modelServing/screens/metrics/utils'; + +// Graph #1 - KV Cache usage over time +type NimKVCacheUsageGraphProps = { + graphDefinition: NimMetricGraphDefinition; + timeframe: TimeframeTitle; + end: number; + namespace: string; +}; + +const NimKVCacheUsageGraph: React.FC = ({ + graphDefinition, + timeframe, + end, + namespace, +}) => { + const { + data: { kvCacheUsage }, + } = useFetchNimKVCacheUsageData(graphDefinition, timeframe, end, namespace); + + return ( + ({ + y: [0, 100], + })} + /> + ); +}; + + +export default NimKVCacheUsageGraph; \ No newline at end of file diff --git a/frontend/src/concepts/metrics/kserve/content/NIMRequestsOutcomesGraph.tsx b/frontend/src/concepts/metrics/kserve/content/NIMRequestsOutcomesGraph.tsx new file mode 100644 index 0000000000..464961c29e --- /dev/null +++ b/frontend/src/concepts/metrics/kserve/content/NIMRequestsOutcomesGraph.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { NimMetricGraphDefinition } from '~/concepts/metrics/kserve/types'; +import { useFetchNimRequestsOutcomesData } from '~/api/prometheus/NimPerformanceMetrics'; +import MetricsChart from '~/pages/modelServing/screens/metrics/MetricsChart'; +import { TimeframeTitle } from '~/concepts/metrics/types'; +import { MetricsChartTypes } from '~/pages/modelServing/screens/metrics/types'; + +type NimRequestsOutcomesGraphProps = { + graphDefinition: NimMetricGraphDefinition; + timeframe: TimeframeTitle; + end: number; + namespace: string; +}; + +const NimRequestsOutcomesGraph: React.FC = ({ + graphDefinition, + timeframe, + end, + namespace, +}) => { + const { + data: { successCount, failedCount }, + } = useFetchNimRequestsOutcomesData(graphDefinition, timeframe, end, namespace); + + return ( + + ); +}; + +export default NimRequestsOutcomesGraph; diff --git a/frontend/src/concepts/metrics/kserve/content/NIMTimeForFirstTokenGraphs.tsx b/frontend/src/concepts/metrics/kserve/content/NIMTimeForFirstTokenGraphs.tsx new file mode 100644 index 0000000000..c4de023edd --- /dev/null +++ b/frontend/src/concepts/metrics/kserve/content/NIMTimeForFirstTokenGraphs.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { NimMetricGraphDefinition } from '~/concepts/metrics/kserve/types'; +import { TimeframeTitle } from '~/concepts/metrics/types'; +import { useFetchNimTimeToFirstTokenData } from '~/api/prometheus/NimPerformanceMetrics'; +import MetricsChart from '~/pages/modelServing/screens/metrics/MetricsChart'; +import { MetricsChartTypes } from '~/pages/modelServing/screens/metrics/types'; +import { convertPrometheusNaNToZero } from '~/pages/modelServing/screens/metrics/utils'; + +// Graph #4 - Time to First Token +type NimTimeToFirstTokenGraphProps = { + graphDefinition: NimMetricGraphDefinition; + timeframe: TimeframeTitle; + end: number; + namespace: string; +}; + +const NimTimeToFirstTokenGraph: React.FC = ({ + graphDefinition, + timeframe, + end, + namespace, +}) => { + const { + data: { timeToFirstToken }, + } = useFetchNimTimeToFirstTokenData(graphDefinition, timeframe, end, namespace); + + return ( + + ); +}; + +export default NimTimeToFirstTokenGraph; diff --git a/frontend/src/concepts/metrics/kserve/content/NIMTimePerOutputTokenGraph.tsx b/frontend/src/concepts/metrics/kserve/content/NIMTimePerOutputTokenGraph.tsx new file mode 100644 index 0000000000..3e4b59cff1 --- /dev/null +++ b/frontend/src/concepts/metrics/kserve/content/NIMTimePerOutputTokenGraph.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { NimMetricGraphDefinition } from '~/concepts/metrics/kserve/types'; +import { TimeframeTitle } from '~/concepts/metrics/types'; +import MetricsChart from '~/pages/modelServing/screens/metrics/MetricsChart'; +import { useFetchNimTimePerOutputTokenData } from '~/api/prometheus/NimPerformanceMetrics'; +import { convertPrometheusNaNToZero } from '~/pages/modelServing/screens/metrics/utils'; +import { MetricsChartTypes } from '~/pages/modelServing/screens/metrics/types'; + + +type NimTimePerOutputTokenGraphProps = { + graphDefinition: NimMetricGraphDefinition; // Contains query and title + timeframe: TimeframeTitle; // Time range + end: number; // End timestamp + namespace: string; // Namespace +}; +const NimTimePerOutputTokenGraph: React.FC = ({ + graphDefinition, + timeframe, + end, + namespace, +}) => { + // Fetch the data for "Time per Output Token" + const { + data: { timePerOutputToken }, + } = useFetchNimTimePerOutputTokenData(graphDefinition, timeframe, end, namespace); + return ( + + ); + +}; +export default NimTimePerOutputTokenGraph; \ No newline at end of file diff --git a/frontend/src/concepts/metrics/kserve/content/NIMTokensCountGraph.tsx b/frontend/src/concepts/metrics/kserve/content/NIMTokensCountGraph.tsx new file mode 100644 index 0000000000..ddc68b527f --- /dev/null +++ b/frontend/src/concepts/metrics/kserve/content/NIMTokensCountGraph.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { NimMetricGraphDefinition } from '~/concepts/metrics/kserve/types'; +import { TimeframeTitle } from '~/concepts/metrics/types'; +import { useFetchNimTokensCountData } from '~/api/prometheus/NimPerformanceMetrics'; +import MetricsChart from '~/pages/modelServing/screens/metrics/MetricsChart'; +import { MetricsChartTypes } from '~/pages/modelServing/screens/metrics/types'; +import { convertPrometheusNaNToZero } from '~/pages/modelServing/screens/metrics/utils'; + +// Graph #3 - Total Prompt Token Count and Total Generation Token Count +type NimTokensCountGraphProps = { + graphDefinition: NimMetricGraphDefinition; + timeframe: TimeframeTitle; + end: number; + namespace: string; +}; + +const NimTokensCountGraph: React.FC = ({ + graphDefinition, + timeframe, + end, + namespace, +}) => { + const { + data: { totalPromptTokenCount, totalGenerationTokenCount }, + } = useFetchNimTokensCountData(graphDefinition, timeframe, end, namespace); + + return ( + + ); +}; + +export default NimTokensCountGraph; diff --git a/frontend/src/concepts/metrics/kserve/content/NimMetricsContent.tsx b/frontend/src/concepts/metrics/kserve/content/NimMetricsContent.tsx new file mode 100644 index 0000000000..7cd9ac5d4d --- /dev/null +++ b/frontend/src/concepts/metrics/kserve/content/NimMetricsContent.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import NimPerformanceGraphs from '~/concepts/metrics/kserve/content/NimPerformanceGraphs'; +import { NimMetricsContext } from '~/concepts/metrics/kserve/NimMetricsContext'; + +const NimMetricsContent: React.FC = () => { + const { namespace, graphDefinitions, timeframe, lastUpdateTime } = + React.useContext(NimMetricsContext); + + return ( + + ); +}; + +export default NimMetricsContent; diff --git a/frontend/src/concepts/metrics/kserve/content/NimPerformanceGraphs.tsx b/frontend/src/concepts/metrics/kserve/content/NimPerformanceGraphs.tsx new file mode 100644 index 0000000000..d25ab1a9ff --- /dev/null +++ b/frontend/src/concepts/metrics/kserve/content/NimPerformanceGraphs.tsx @@ -0,0 +1,115 @@ +import { Stack, StackItem } from '@patternfly/react-core/dist/esm'; +import React from 'react'; +import { NimMetricGraphDefinition } from '~/concepts/metrics/kserve/types'; +import { NimMetricsGraphTypes } from '~/concepts/metrics/kserve/const'; +import { TimeframeTitle } from '~/concepts/metrics/types'; +import NIMTimeToFirstTokenGraph from './NIMTimeForFirstTokenGraphs'; +import NIMKVCacheUsageGraph from './NIMKVCacheUsageGraph'; +import NIMTokensCountGraph from './NIMTokensCountGraph'; +import NIMRequestsOutcomesGraph from './NIMRequestsOutcomesGraph'; +import NIMTimePerOutputTokenGraph from './NIMTimePerOutputTokenGraph'; +import NIMCurrentRequestsGraph from './NIMCurrentRequestsGraph'; + +type NimPerformanceGraphsProps = { + namespace: string; + graphDefinitions: NimMetricGraphDefinition[]; + timeframe: TimeframeTitle; + end: number; +}; + +const NimPerformanceGraphs: React.FC = ({ + namespace, + graphDefinitions, + timeframe, + end, +}) => { + const renderGraph = (graphDefinition: NimMetricGraphDefinition) => { + // Graph #1 - KV Cache usage over time + if (graphDefinition.type === NimMetricsGraphTypes.KV_CACHE) { + return ( + + ); + } + + + // Graph #3 - Total Prompt Token Count and Total Generation Token Count + if (graphDefinition.type === NimMetricsGraphTypes.TOKENS_COUNT) { + return ( + + ); + } + + + // Graph #4 - Time to First Token + if (graphDefinition.type === NimMetricsGraphTypes.TIME_TO_FIRST_TOKEN) { + return ( + + ); + } + + // Graph #5 - Time per Output Token + if (graphDefinition.type === NimMetricsGraphTypes.TIME_PER_OUTPUT_TOKEN) { + return ( + + ); + } + + // Graph #6- Requests Outcomes + if (graphDefinition.type === NimMetricsGraphTypes.REQUEST_OUTCOMES) { + return ( + + ); + } + + // Graph #2 Current Requests + if (graphDefinition.type === NimMetricsGraphTypes.CURRENT_REQUESTS) { + return ( + + ); + } + + + // TODO: add an unsupported graph type error state. + return null; + }; + + return ( + + {graphDefinitions.map((x) => ( + {renderGraph(x)} + ))} + + ); +}; + +export default NimPerformanceGraphs; diff --git a/frontend/src/concepts/metrics/kserve/types.ts b/frontend/src/concepts/metrics/kserve/types.ts index cd127327fd..aab81b011b 100644 --- a/frontend/src/concepts/metrics/kserve/types.ts +++ b/frontend/src/concepts/metrics/kserve/types.ts @@ -1,5 +1,5 @@ import { ConfigMapKind } from '~/k8sTypes'; -import { KserveMetricsGraphTypes } from '~/concepts/metrics/kserve/const'; +import { KserveMetricsGraphTypes, NimMetricsGraphTypes } from '~/concepts/metrics/kserve/const'; export type KserveMetricsConfigMapKind = ConfigMapKind & { data: { @@ -8,24 +8,49 @@ export type KserveMetricsConfigMapKind = ConfigMapKind & { }; }; -export type KserveMetricGraphDefinition = { - title: string; - type: KserveMetricsGraphTypes; - queries: KserveMetricQueryDefinition[]; -}; -export type KserveMetricQueryDefinition = { +export type MetricQueryDefinition = { title: string; query: string; }; + +//Kserve Data Type Defenitions + export type KserveMetricsDataObject = { config: KserveMetricGraphDefinition[]; }; +export type KserveMetricGraphDefinition = { + title: string; + type: KserveMetricsGraphTypes; + queries: MetricQueryDefinition[]; +}; + export type KserveMetricsDefinition = { supported: boolean; loaded: boolean; error?: Error; graphDefinitions: KserveMetricGraphDefinition[]; }; + +//Nim Data Type Defenitions +export type NimMetricsDataObject = { + config: NimMetricGraphDefinition[]; +}; + +export type NimMetricGraphDefinition = { + title: string; + type: NimMetricsGraphTypes; + queries: MetricQueryDefinition[]; +}; + +export type NimMetricsDefinition = { + supported: boolean; + loaded: boolean; + error?: Error; + graphDefinitions: NimMetricGraphDefinition[]; +}; + + + diff --git a/frontend/src/concepts/metrics/kserve/useNimMetricsGraphDefinition.ts b/frontend/src/concepts/metrics/kserve/useNimMetricsGraphDefinition.ts new file mode 100644 index 0000000000..76357dccd7 --- /dev/null +++ b/frontend/src/concepts/metrics/kserve/useNimMetricsGraphDefinition.ts @@ -0,0 +1,43 @@ +import React from 'react'; +import { + KserveMetricsConfigMapKind, + NimMetricsDefinition, +} from '~/concepts/metrics/kserve/types'; +import { isValidNimMetricsDataObject } from '~/concepts/metrics/kserve/utils'; + +const useNimMetricsGraphDefinitions = ( + kserveMetricsConfigMap: KserveMetricsConfigMapKind | null, +): NimMetricsDefinition => + React.useMemo(() => { + const result: NimMetricsDefinition = { + supported: false, + loaded: !!kserveMetricsConfigMap, + graphDefinitions: [], + }; + + if (kserveMetricsConfigMap) { + result.supported = kserveMetricsConfigMap.data.supported === 'true'; + + let parsed: unknown; + if (result.supported) { + try { + parsed = JSON.parse(kserveMetricsConfigMap.data.metrics); + } catch (e) { + result.error = new Error('Error reading metrics configuration: malformed JSON'); + result.loaded = true; + } + + if (!result.error) { + if (isValidNimMetricsDataObject(parsed)) { + result.graphDefinitions = parsed.config; + } else { + result.error = new Error('Error reading metrics configuration: schema mismatch'); + result.loaded = true; + } + } + } + } + return result; + }, [kserveMetricsConfigMap]); + +export default useNimMetricsGraphDefinitions; diff --git a/frontend/src/concepts/metrics/kserve/utils.ts b/frontend/src/concepts/metrics/kserve/utils.ts index ad5e7ecdea..a73d5e202c 100644 --- a/frontend/src/concepts/metrics/kserve/utils.ts +++ b/frontend/src/concepts/metrics/kserve/utils.ts @@ -2,6 +2,7 @@ import { ConfigMapKind } from '~/k8sTypes'; import { KserveMetricsConfigMapKind, KserveMetricsDataObject, + NimMetricsDataObject } from '~/concepts/metrics/kserve/types'; export const isKserveMetricsConfigMapKind = ( @@ -21,3 +22,11 @@ export const isValidKserveMetricsDataObject = (obj: unknown): obj is KserveMetri return 'config' in obj && Array.isArray(obj.config) && obj.config.length > 0; }; + +export const isValidNimMetricsDataObject = (obj: unknown): obj is NimMetricsDataObject => { + if (typeof obj !== 'object' || obj === null) { + return false; + } + + return 'config' in obj && Array.isArray(obj.config) && obj.config.length > 0; +}; diff --git a/frontend/src/pages/modelServing/screens/global/InferenceServiceTableRow.tsx b/frontend/src/pages/modelServing/screens/global/InferenceServiceTableRow.tsx index ff34ef8080..5b2fc88290 100644 --- a/frontend/src/pages/modelServing/screens/global/InferenceServiceTableRow.tsx +++ b/frontend/src/pages/modelServing/screens/global/InferenceServiceTableRow.tsx @@ -45,7 +45,7 @@ const InferenceServiceTableRow: React.FC = ({ const modelMesh = isModelMesh(inferenceService); const modelMeshMetricsSupported = modelMetricsEnabled && modelMesh; const kserveMetricsSupported = - modelMetricsEnabled && kserveMetricsEnabled && !modelMesh && !isKServeNIMEnabled; + modelMetricsEnabled && kserveMetricsEnabled && !modelMesh ; const displayName = getDisplayNameFromK8sResource(inferenceService); return ( diff --git a/frontend/src/pages/modelServing/screens/metrics/MetricsChart.tsx b/frontend/src/pages/modelServing/screens/metrics/MetricsChart.tsx index eaede96586..2e855360f3 100644 --- a/frontend/src/pages/modelServing/screens/metrics/MetricsChart.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/MetricsChart.tsx @@ -15,6 +15,7 @@ import { Chart, ChartArea, ChartAxis, + ChartDonut, ChartGroup, ChartLegendTooltip, ChartLine, @@ -194,20 +195,39 @@ const MetricsChart: React.FC = ({ height={400} width={chartWidth} padding={{ left: 70, right: 50, bottom: 70, top: 50 }} - themeColor={color ?? ChartThemeColor.multi} + themeColor={color ?? ChartThemeColor.multiUnordered} theme={theme} hasPatterns={hasPatterns} data-testid="metrics-chart-has-data" - {...legendProps} + {...(type !== MetricsChartTypes.DONUT && legendProps)} // Conditional spreading + showAxis={type === MetricsChartTypes.DONUT ? false : true} > - convertTimestamp(x, formatToShow(currentTimeframe))} - domain={{ - x: [lastUpdateTime - TimeframeTimeRange[currentTimeframe] * 1000, lastUpdateTime], - }} - fixLabelOverlap - /> - + {/* Conditionally render X Axis for non-DONUT chart types */} + {type !== MetricsChartTypes.DONUT && ( + //X-Axis + new Date(x).toLocaleDateString('en-US', { day: 'numeric', month: 'short' })} + + //X-Axis with hours timestamp + tickFormat={(x) => convertTimestamp(x, formatToShow(currentTimeframe))} + //Add title for the X-axis + label="Hours" // Add this for the axis title + style={{ + axisLabel: { padding: 40, fontSize: 14, fontWeight: 'bold', fill: '#555' }, + }} + domain={{ + x: [lastUpdateTime - TimeframeTimeRange[currentTimeframe] * 1000, lastUpdateTime], + }} + fixLabelOverlap + /> + )} + + {/* Conditionally render Y Axis for non-DONUT chart types */} + {type !== MetricsChartTypes.DONUT && ( + //Y-Axis + < ChartAxis dependentAxis tickCount={10} fixLabelOverlap /> + )} {graphLines.map((line, i) => { switch (type) { @@ -217,10 +237,30 @@ const MetricsChart: React.FC = ({ key={i} data={line.points.length === 0 ? [null] : line.points} name={line.name} + interpolation="monotoneX" // Smooths out the area line /> ); case MetricsChartTypes.LINE: - return ; + return ( + + {line.points.map((line1, index) => ( + + ))} + + ); + case MetricsChartTypes.DONUT: + return ; default: return null; } diff --git a/frontend/src/pages/modelServing/screens/metrics/MetricsPageTabs.tsx b/frontend/src/pages/modelServing/screens/metrics/MetricsPageTabs.tsx index 6486761d17..5a9fab36d2 100644 --- a/frontend/src/pages/modelServing/screens/metrics/MetricsPageTabs.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/MetricsPageTabs.tsx @@ -12,7 +12,7 @@ import PerformanceTab from './performance/PerformanceTab'; import BiasTab from './bias/BiasTab'; import BiasConfigurationAlertPopover from './bias/BiasConfigurationPage/BiasConfigurationAlertPopover'; import useMetricsPageEnabledTabs from './useMetricsPageEnabledTabs'; - +import NIMTab from './nim/NimTab'; import './MetricsPageTabs.scss'; type MetricsPageTabsProps = { @@ -26,7 +26,10 @@ const MetricsPageTabs: React.FC = ({ model }) => { const performanceMetricsAreaAvailable = useIsAreaAvailable( SupportedArea.PERFORMANCE_METRICS, ).status; - const { tab } = useParams<{ tab: MetricsTabKeys }>(); + //check availability of NIM metrics + const nimMetricsAreaAvailable = useIsAreaAvailable( + SupportedArea.NIM_MODEL, + ).status; const { tab } = useParams<{ tab: MetricsTabKeys }>(); const navigate = useNavigate(); if (!tab) { @@ -41,10 +44,20 @@ const MetricsPageTabs: React.FC = ({ model }) => { return ; } + //Display only one tab that is available if (enabledTabs.length === 1) { - return performanceMetricsAreaAvailable ? : ; + if (performanceMetricsAreaAvailable) { + return + } + else if (nimMetricsAreaAvailable) { + return + } + else { + return ; + } } + //Display multiple available tabs return ( = ({ model }) => { )} + + + {/* Add NIN metrics tab */} + {nimMetricsAreaAvailable && (// TODO - check the flag show + NIM Metrics} + aria-label="Nim tab" + className="odh-metrics-page-tabs__content" + data-testid="nim-tab" + > + + + )} + + {biasMetricsInstalled && ( = ({ title }) => ( + + + {title} + + + + Metrics coming soon + + } + icon={CubesIcon} + /> + + +); + +export default MetricsPlaceHolder; diff --git a/frontend/src/pages/modelServing/screens/metrics/nim/ModelGraphs.tsx b/frontend/src/pages/modelServing/screens/metrics/nim/ModelGraphs.tsx new file mode 100644 index 0000000000..de3f12a082 --- /dev/null +++ b/frontend/src/pages/modelServing/screens/metrics/nim/ModelGraphs.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; +import { InferenceServiceKind } from '~/k8sTypes'; +import { isModelMesh } from '~/pages/modelServing/utils'; +import ModelMeshMetrics from '~/pages/modelServing/screens/metrics/performance/ModelMeshMetrics'; +import NimMetrics from '~/pages/modelServing/screens/metrics/nim/NimMetrics'; + +type ModelGraphProps = { + model: InferenceServiceKind; +}; + +const ModelGraphs: React.FC = ({ model }) => + isModelMesh(model) ? : ; + +export default ModelGraphs; diff --git a/frontend/src/pages/modelServing/screens/metrics/nim/ModelMeshMetrics.tsx b/frontend/src/pages/modelServing/screens/metrics/nim/ModelMeshMetrics.tsx new file mode 100644 index 0000000000..ed008ecf21 --- /dev/null +++ b/frontend/src/pages/modelServing/screens/metrics/nim/ModelMeshMetrics.tsx @@ -0,0 +1,41 @@ +import * as React from 'react'; +import { Stack, StackItem } from '@patternfly/react-core'; +import MetricsChart from '~/pages/modelServing/screens/metrics/MetricsChart'; +import { + ModelMetricType, + ModelServingMetricsContext, +} from '~/pages/modelServing/screens/metrics/ModelServingMetricsContext'; +import EnsureMetricsAvailable from '~/pages/modelServing/screens/metrics/EnsureMetricsAvailable'; + +const ModelMeshMetrics: React.FC = () => { + const { data } = React.useContext(ModelServingMetricsContext); + + return ( + + + + + + + + ); +}; + +export default ModelMeshMetrics; diff --git a/frontend/src/pages/modelServing/screens/metrics/nim/NimMetrics.tsx b/frontend/src/pages/modelServing/screens/metrics/nim/NimMetrics.tsx new file mode 100644 index 0000000000..7238310ac6 --- /dev/null +++ b/frontend/src/pages/modelServing/screens/metrics/nim/NimMetrics.tsx @@ -0,0 +1,20 @@ +import * as React from 'react'; +import { ModelServingMetricsContext } from '~/pages/modelServing/screens/metrics/ModelServingMetricsContext'; +import { NimMetricsContextProvider } from '~/concepts/metrics/kserve/NimMetricsContext'; +import NimMetricsContent from '~/concepts/metrics/kserve/content/NimMetricsContent'; + +type NimMetricsProps = { + modelName: string; +}; + +const NimMetrics: React.FC = ({ modelName }) => { + const { namespace } = React.useContext(ModelServingMetricsContext); + + return ( + + + + ); +}; + +export default NimMetrics; diff --git a/frontend/src/pages/modelServing/screens/metrics/nim/NimTab.tsx b/frontend/src/pages/modelServing/screens/metrics/nim/NimTab.tsx new file mode 100644 index 0000000000..e8d04570ec --- /dev/null +++ b/frontend/src/pages/modelServing/screens/metrics/nim/NimTab.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { EmptyState, PageSection, Stack, StackItem } from '@patternfly/react-core'; +import { WarningTriangleIcon } from '@patternfly/react-icons'; +import { InferenceServiceKind } from '~/k8sTypes'; +import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; +import { isModelMesh } from '~/pages/modelServing/utils'; +import MetricsPageToolbar from '~/concepts/metrics/MetricsPageToolbar'; +import ModelGraphs from '~/pages/modelServing/screens/metrics/nim/ModelGraphs'; + +type NIMTabProps = { + model: InferenceServiceKind; +}; + +const NIMTab: React.FC = ({ model }) => { + const modelMesh = isModelMesh(model); + const NIMMetricsEnabled = useIsAreaAvailable(SupportedArea.NIM_MODEL).status; + + if (!modelMesh && !NIMMetricsEnabled) { + return ( + + + + + + ); + } + + return ( + + + + + + + + + ); +}; + +export default NIMTab; diff --git a/frontend/src/pages/modelServing/screens/metrics/nim/PerformanceTab.tsx b/frontend/src/pages/modelServing/screens/metrics/nim/PerformanceTab.tsx new file mode 100644 index 0000000000..81bfab4f28 --- /dev/null +++ b/frontend/src/pages/modelServing/screens/metrics/nim/PerformanceTab.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { EmptyState, PageSection, Stack, StackItem } from '@patternfly/react-core'; +import { WarningTriangleIcon } from '@patternfly/react-icons'; +import { InferenceServiceKind } from '~/k8sTypes'; +import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; +import { isModelMesh } from '~/pages/modelServing/utils'; +import MetricsPageToolbar from '~/concepts/metrics/MetricsPageToolbar'; +import ModelGraphs from '~/pages/modelServing/screens/metrics/performance/ModelGraphs'; + +type PerformanceTabsProps = { + model: InferenceServiceKind; +}; + +const PerformanceTab: React.FC = ({ model }) => { + const modelMesh = isModelMesh(model); + const kserveMetricsEnabled = useIsAreaAvailable(SupportedArea.K_SERVE_METRICS).status; + + if (!modelMesh && !kserveMetricsEnabled) { + return ( + + + + + + ); + } + + return ( + + + + + + + + + ); +}; + +export default PerformanceTab; diff --git a/frontend/src/pages/modelServing/screens/metrics/nim/ServerGraphs.tsx b/frontend/src/pages/modelServing/screens/metrics/nim/ServerGraphs.tsx new file mode 100644 index 0000000000..d97fc00a2a --- /dev/null +++ b/frontend/src/pages/modelServing/screens/metrics/nim/ServerGraphs.tsx @@ -0,0 +1,75 @@ +import * as React from 'react'; +import { Stack, StackItem } from '@patternfly/react-core'; +import MetricsChart from '~/pages/modelServing/screens/metrics/MetricsChart'; +import { + ModelServingMetricsContext, + ServerMetricType, +} from '~/pages/modelServing/screens/metrics/ModelServingMetricsContext'; +import { + convertPrometheusNaNToZero, + toPercentage, +} from '~/pages/modelServing/screens/metrics/utils'; +import { NamedMetricChartLine } from '~/pages/modelServing/screens/metrics/types'; + +const ServerGraphs: React.FC = () => { + const { data } = React.useContext(ModelServingMetricsContext); + + return ( + + + + + + ({ + name: line.metric.pod || '', + metric: { + ...data[ServerMetricType.AVG_RESPONSE_TIME], + data: convertPrometheusNaNToZero(line.values), + }, + }), + )} + color="green" + title="Average response time (ms)" + isStack + /> + + + ({ + y: [0, 100], + })} + /> + + + ({ + y: [0, 100], + })} + /> + + + ); +}; + +export default ServerGraphs; diff --git a/frontend/src/pages/modelServing/screens/metrics/nim/ServerMetricsPage.tsx b/frontend/src/pages/modelServing/screens/metrics/nim/ServerMetricsPage.tsx new file mode 100644 index 0000000000..2e6ee6768a --- /dev/null +++ b/frontend/src/pages/modelServing/screens/metrics/nim/ServerMetricsPage.tsx @@ -0,0 +1,28 @@ +import { PageSection, Stack, StackItem } from '@patternfly/react-core'; +import React, { ReactElement } from 'react'; +import MetricsPageToolbar from '~/concepts/metrics/MetricsPageToolbar'; +import ServerGraphs from '~/pages/modelServing/screens/metrics/performance/ServerGraphs'; +import { ServerMetricType } from '~/pages/modelServing/screens/metrics/ModelServingMetricsContext'; +import EnsureMetricsAvailable from '~/pages/modelServing/screens/metrics/EnsureMetricsAvailable'; + +const ServerMetricsPage = (): ReactElement => ( + + + + + + + + + + +); +export default ServerMetricsPage; diff --git a/frontend/src/pages/modelServing/screens/metrics/types.ts b/frontend/src/pages/modelServing/screens/metrics/types.ts index 60e9a2c1d9..6ab6893158 100644 --- a/frontend/src/pages/modelServing/screens/metrics/types.ts +++ b/frontend/src/pages/modelServing/screens/metrics/types.ts @@ -14,6 +14,7 @@ type MetricChartLineBase = { }; export type NamedMetricChartLine = MetricChartLineBase & { name: string; + color?: string; // Add customColor as an optional property }; export type UnnamedMetricChartLine = MetricChartLineBase & { /** Assumes chart title */ @@ -50,11 +51,13 @@ export type DomainCalculator = ( export enum MetricsChartTypes { AREA, LINE, + DONUT } export enum MetricsTabKeys { PERFORMANCE = 'performance', BIAS = 'bias', + NIM = 'nim' } export type BiasChartConfig = { diff --git a/frontend/src/pages/modelServing/screens/metrics/useMetricsPageEnabledTabs.ts b/frontend/src/pages/modelServing/screens/metrics/useMetricsPageEnabledTabs.ts index 6951ded38e..42ae590f3b 100644 --- a/frontend/src/pages/modelServing/screens/metrics/useMetricsPageEnabledTabs.ts +++ b/frontend/src/pages/modelServing/screens/metrics/useMetricsPageEnabledTabs.ts @@ -3,16 +3,25 @@ import { MetricsTabKeys } from './types'; const useMetricsPageEnabledTabs = (): MetricsTabKeys[] => { const enabledTabs: MetricsTabKeys[] = []; + //check availability of Bias metrics const biasMetricsAreaAvailable = useIsAreaAvailable(SupportedArea.BIAS_METRICS).status; + //check availability of Performance metrics const performanceMetricsAreaAvailable = useIsAreaAvailable( SupportedArea.PERFORMANCE_METRICS, ).status; + //check availability of NIM metrics + const nimMetricsAreaAvailable = useIsAreaAvailable( + SupportedArea.NIM_MODEL, + ).status; if (performanceMetricsAreaAvailable) { enabledTabs.push(MetricsTabKeys.PERFORMANCE); } if (biasMetricsAreaAvailable) { enabledTabs.push(MetricsTabKeys.BIAS); } + if (nimMetricsAreaAvailable) { + enabledTabs.push(MetricsTabKeys.NIM); + } return enabledTabs; }; diff --git a/frontend/src/pages/modelServing/useModelMetricsEnabled.ts b/frontend/src/pages/modelServing/useModelMetricsEnabled.ts index 7322e4b41d..e2d3257ee4 100644 --- a/frontend/src/pages/modelServing/useModelMetricsEnabled.ts +++ b/frontend/src/pages/modelServing/useModelMetricsEnabled.ts @@ -6,8 +6,10 @@ const useModelMetricsEnabled = (): [modelMetricsEnabled: boolean] => { ).status; const biasMetricsAreaAvailable = useIsAreaAvailable(SupportedArea.BIAS_METRICS).status; + const nimMetricsAreaAvailable = useIsAreaAvailable(SupportedArea.BIAS_METRICS).status; + const checkModelMetricsEnabled = () => - performanceMetricsAreaAvailable || biasMetricsAreaAvailable; + performanceMetricsAreaAvailable || biasMetricsAreaAvailable || nimMetricsAreaAvailable; return [checkModelMetricsEnabled()]; }; diff --git a/frontend/src/pages/projects/screens/detail/overview/serverModels/deployedModels/DeployedModelCard.tsx b/frontend/src/pages/projects/screens/detail/overview/serverModels/deployedModels/DeployedModelCard.tsx index 697bfef1c0..dba2113ef1 100644 --- a/frontend/src/pages/projects/screens/detail/overview/serverModels/deployedModels/DeployedModelCard.tsx +++ b/frontend/src/pages/projects/screens/detail/overview/serverModels/deployedModels/DeployedModelCard.tsx @@ -42,7 +42,7 @@ const DeployedModelCard: React.FC = ({ const isKServeNIMEnabled = isProjectNIMSupported(currentProject); const modelMetricsSupported = - modelMetricsEnabled && (modelMesh || kserveMetricsEnabled) && !isKServeNIMEnabled; + modelMetricsEnabled && (modelMesh || kserveMetricsEnabled); const inferenceServiceDisplayName = getDisplayNameFromK8sResource(inferenceService);