Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions frontend/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ export const API = {
// Fleets
VOLUMES_DELETE: (projectName: IProject['project_name']) => `${API.BASE()}/project/${projectName}/volumes/delete`,

// PRESETS
PRESETS_GET: (projectName: IProject['project_name']) => `${API.BASE()}/project/${projectName}/presets/get`,
PRESETS_DELETE: (projectName: IProject['project_name']) => `${API.BASE()}/project/${projectName}/presets/delete`,

// METRICS
JOB_METRICS: (projectName: IProject['project_name'], runName: IRun['run_spec']['run_name']) =>
`${API.BASE()}/project/${projectName}/metrics/job/${runName}`,
Expand Down Expand Up @@ -183,6 +187,11 @@ export const API = {
LIST: () => `${API.VOLUME.BASE()}/list`,
},

PRESET: {
BASE: () => `${API.BASE()}/presets`,
LIST: () => `${API.PRESET.BASE()}/list`,
},

USER_PUBLIC_KEYS: {
BASE: () => `${API.BASE()}/users/public_keys`,
LIST: () => `${API.USER_PUBLIC_KEYS.BASE()}/list`,
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export type { ModalProps } from '@cloudscape-design/components/modal';
export { default as AnchorNavigation } from '@cloudscape-design/components/anchor-navigation';
export { default as ExpandableSection } from '@cloudscape-design/components/expandable-section';
export { default as KeyValuePairs } from '@cloudscape-design/components/key-value-pairs';
export { default as TreeView } from '@cloudscape-design/components/tree-view';
export { I18nProvider } from '@cloudscape-design/components/i18n';
export { default as Wizard } from '@cloudscape-design/components/wizard';
export { default as SegmentedControl } from '@cloudscape-design/components/segmented-control';
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ export const DISCORD_URL = 'https://discord.gg/u8SmfwPpMd';
export const QUICK_START_URL = 'https://dstack.ai/docs/quickstart/';
export const TALLY_FORM_ID = '3xYlYG';
export const DOCS_URL = 'https://dstack.ai/docs/';
export const PRESETS_DOCS_URL = 'https://dstack.ai/docs/concepts/presets/';
export const FLEETS_DOCS_URL = 'https://dstack.ai/docs/concepts/fleets/';
export const DEFAULT_TABLE_PAGE_SIZE = 20;
3 changes: 2 additions & 1 deletion frontend/src/layouts/AppLayout/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ export const useSideNavigation = () => {
{ type: 'link', text: t('navigation.fleets'), href: ROUTES.FLEETS.LIST },
{ type: 'link', text: t('navigation.instances'), href: ROUTES.INSTANCES.LIST },
{ type: 'link', text: t('navigation.volumes'), href: ROUTES.VOLUMES.LIST },
{ type: 'link', text: t('navigation.events'), href: ROUTES.EVENTS.LIST },
{ type: 'link', text: t('navigation.models'), href: ROUTES.MODELS.LIST },
process.env.UI_VERSION === 'sky' && { type: 'link', text: t('navigation.presets'), href: ROUTES.PRESETS.LIST },
{ type: 'link', text: t('navigation.events'), href: ROUTES.EVENTS.LIST },
{ type: 'link', text: t('navigation.project_other'), href: ROUTES.PROJECT.LIST },

isGlobalAdmin && {
Expand Down
94 changes: 94 additions & 0 deletions frontend/src/libs/presets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/**
* Formats a token count the way the CLI does: exact binary multiples keep
* binary names (32768 is "32K"), anything else rounds as decimal (1500 is
* "1.5K").
*/
export const formatTokenCount = (value: number): string => {
for (const [divisor, suffix] of [
[1024 * 1024, 'M'],
[1024, 'K'],
] as const) {
if (value >= divisor && value % divisor === 0) {
return `${value / divisor}${suffix}`;
}
}

if (value >= 999950) {
return `${trimZero((value / 1_000_000).toFixed(1))}M`;
}

if (value >= 1000) {
return `${trimZero((value / 1000).toFixed(1))}K`;
}

return String(value);
};

const trimZero = (value: string): string => (value.endsWith('.0') ? value.slice(0, -2) : value);

/** As the CLI prints durations: 999.6 reads as 1s rather than 1000ms. */
const formatDurationMs = (value: number): string => (value < 999.5 ? `${round(value)}ms` : `${round(value / 1000)}s`);

const round = (value: number): string => String(Math.round(value * 100) / 100);

/**
* Request counts, wall time, and token totals say nothing about how the preset
* performs: the totals are the workload multiplied by the request count, which
* the constraints already state.
*/
const HIDDEN_METRICS = new Set([
'successful_requests',
'failed_requests',
'duration_seconds',
'total_input_tokens',
'total_output_tokens',
]);

const METRIC_LABELS: Record<string, string> = {
output_tok_per_s: 'TPS',
per_user_tok_per_s: 'TPS/user',
total_input_tokens: 'Input tokens',
total_output_tokens: 'Output tokens',
ttft_ms: 'TTFT',
tpot_ms: 'TPOT',
};

const TOKEN_COUNT_METRICS = new Set(['total_input_tokens', 'total_output_tokens']);

export type BenchmarkMetric = { label: string; value: string };

const formatMetricValue = (key: string, value: number): string => {
if (TOKEN_COUNT_METRICS.has(key)) return formatTokenCount(value);
if (key.endsWith('_ms')) return formatDurationMs(value);
return round(value);
};

/**
* The metrics worth showing, flattened: a metric measured as a distribution
* becomes one entry per statistic.
*/
export const getBenchmarkMetrics = (metrics: HashMap): BenchmarkMetric[] => {
const entries: BenchmarkMetric[] = [];

Object.entries(metrics ?? {}).forEach(([key, value]) => {
if (HIDDEN_METRICS.has(key)) return;
const label = METRIC_LABELS[key] ?? key;

if (typeof value === 'number') {
entries.push({ label, value: formatMetricValue(key, value) });
return;
}

if (value && typeof value === 'object') {
Object.entries(value as HashMap).forEach(([statistic, statisticValue]) => {
if (typeof statisticValue !== 'number') return;
entries.push({
label: `${label} ${statistic}`,
value: formatMetricValue(key, statisticValue),
});
});
}
});

return entries;
};
47 changes: 46 additions & 1 deletion frontend/src/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@
"volumes": "Volumes",
"instances": "Instances",
"offers": "Offers",
"events": "Events"
"events": "Events",
"presets": "Presets"
},

"backend": {
Expand Down Expand Up @@ -834,5 +835,49 @@
"confirm_dialog": {
"title": "Confirm delete",
"message": "Are you sure you want to delete?"
},
"presets": {
"list_page_title": "Presets",
"empty_message_title": "No presets",
"empty_message_text": "Presets are created and pushed with the CLI.",
"documentation": "Documentation",
"nomatch_message_title": "No matches",
"nomatch_message_text": "We can't find a match. Try to change project or clear filter",
"filter_property_placeholder": "Filter by properties",
"name": "Name",
"id": "ID",
"project": "Project",
"base": "Base",
"repo": "Repo",
"user": "User",
"created_at": "Created",
"context_length": "Context length",
"benchmark": "Benchmark",
"details": "Details",
"inspect": "Inspect",
"delete_confirm_title": "Delete presets",
"delete_confirm_message": "Are you sure you want to delete these presets?",
"verified_on": "Verified on",
"replica_group": "Replica group {{name}}",
"step_pull": "Pull",
"step_pull_description": "Pull the preset to your machine.",
"step_export": "Export",
"step_export_description": "Export it as a service configuration, along with the files it references.",
"step_apply": "Apply",
"step_apply_description": "Deploy the service to any cloud, Kubernetes cluster, or on-prem fleet.",
"replica": "Replica {{index}}",
"deploy": "Deploy",
"no_cli": "No CLI installed?",
"no_cli_description": "To use dstack, install the CLI on your local machine.",
"constraints": "Constraints",
"concurrency": "Concurrency",
"dataset": "Dataset",
"input_tokens": "Input tokens",
"output_tokens": "Output tokens",
"shared_prefix": "Shared prefix",
"no_fleet_description": "Deploying the service requires a fleet with matching resources.",
"fleets": "Fleets",
"superseded_alert": "A later push took this preset's name. It stays available by ID.",
"fleets_link": "How to create a fleet"
}
}
13 changes: 13 additions & 0 deletions frontend/src/pages/Events/List/hooks/useColumnDefinitions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,19 @@ export const useColumnsDefinitions = () => {
</div>
);

case 'preset':
return (
<div>
Preset{' '}
{target.project_name && (
<NavigateLink href={ROUTES.PROJECT.DETAILS.FORMAT(target.project_name)}>
{target.project_name}
</NavigateLink>
)}
/{target.name}
</div>
);

default:
return '---';
}
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/pages/Events/List/hooks/useFilters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const filterKeys: Record<string, RequestParamsKeys> = {
TARGET_VOLUMES: 'target_volumes',
TARGET_GATEWAYS: 'target_gateways',
TARGET_SECRETS: 'target_secrets',
TARGET_PRESETS: 'target_presets',
WITHIN_PROJECTS: 'within_projects',
WITHIN_FLEETS: 'within_fleets',
WITHIN_RUNS: 'within_runs',
Expand All @@ -47,6 +48,7 @@ const multipleChoiseKeys: RequestParamsKeys[] = [
'target_volumes',
'target_gateways',
'target_secrets',
'target_presets',
'within_projects',
'within_fleets',
'within_runs',
Expand All @@ -64,6 +66,7 @@ const targetTypes = [
{ label: 'Volume', value: 'volume' },
{ label: 'Gateway', value: 'gateway' },
{ label: 'Secret', value: 'secret' },
{ label: 'Preset', value: 'preset' },
];

const baseFilteringProperties = [
Expand Down Expand Up @@ -121,6 +124,12 @@ const baseFilteringProperties = [
propertyLabel: 'Target secret IDs',
groupValuesLabel: 'Secret ids',
},
{
key: filterKeys.TARGET_PRESETS,
operators: ['='],
propertyLabel: 'Target preset IDs',
groupValuesLabel: 'Preset ids',
},

{
key: filterKeys.WITHIN_PROJECTS,
Expand Down
50 changes: 50 additions & 0 deletions frontend/src/pages/Presets/Details/Benchmark/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React, { FC } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';

import { Box, ColumnLayout, Container, Header, Loader } from 'components';

import { formatTokenCount, getBenchmarkMetrics } from 'libs/presets';
import { useGetPresetQuery } from 'services/preset';

export const PresetBenchmark: FC = () => {
const { t } = useTranslation();
const params = useParams();
const paramProjectName = params.projectName ?? '';
const paramPresetId = params.presetId ?? '';

const { data, isLoading } = useGetPresetQuery({
project_name: paramProjectName,
id: paramPresetId,
});

if (isLoading || !data)
return (
<Container>
<Loader />
</Container>
);

const metrics = getBenchmarkMetrics(data.spec.preset.benchmark.metrics as HashMap);

return (
<Container header={<Header variant="h2">{t('presets.benchmark')}</Header>}>
<ColumnLayout columns={4} variant="text-grid">
<div>
<Box variant="awsui-key-label">{t('presets.context_length')}</Box>
<div>{formatTokenCount(data.spec.preset.context_length)}</div>
</div>
<div>
<Box variant="awsui-key-label">{t('presets.concurrency')}</Box>
<div>{String((data.spec.preset.benchmark.workload as HashMap)?.concurrency)}</div>
</div>
{metrics.map(({ label, value }) => (
<div key={label}>
<Box variant="awsui-key-label">{label}</Box>
<div>{value}</div>
</div>
))}
</ColumnLayout>
</Container>
);
};
63 changes: 63 additions & 0 deletions frontend/src/pages/Presets/Details/Constraints/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React, { FC } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';

import { Box, ColumnLayout, Container, Header, Loader } from 'components';

import { formatTokenCount } from 'libs/presets';
import { useGetPresetQuery } from 'services/preset';

const DEFAULT_DATASET = 'random';

export const PresetConstraints: FC = () => {
const { t } = useTranslation();
const params = useParams();
const paramProjectName = params.projectName ?? '';
const paramPresetId = params.presetId ?? '';

const { data, isLoading } = useGetPresetQuery({
project_name: paramProjectName,
id: paramPresetId,
});

if (isLoading || !data)
return (
<Container>
<Loader />
</Container>
);

// The conditions the benchmark holds for: the workload it measured and the
// context the service was verified to serve.
const workload = (data.spec.preset.benchmark.workload ?? {}) as HashMap;
const dataset = workload.dataset as string | undefined;
const inputTokens = workload.input_tokens as number;
const sharedPrefix = (workload.shared_prefix_tokens as number) ?? 0;

return (
<Container header={<Header variant="h2">{t('presets.constraints')}</Header>}>
<ColumnLayout columns={4} variant="text-grid">
<div>
<Box variant="awsui-key-label">{t('presets.dataset')}</Box>
<div>{dataset ?? DEFAULT_DATASET}</div>
</div>
<div>
<Box variant="awsui-key-label">{t('presets.input_tokens')}</Box>
<div>{formatTokenCount(inputTokens)}</div>
</div>
<div>
<Box variant="awsui-key-label">{t('presets.output_tokens')}</Box>
<div>{formatTokenCount(workload.output_tokens as number)}</div>
</div>
{sharedPrefix > 0 && (
<div>
<Box variant="awsui-key-label">{t('presets.shared_prefix')}</Box>
<div>
{formatTokenCount(sharedPrefix)} ({Math.round((100 * sharedPrefix) / inputTokens)}%)
</div>
</div>
)}
</ColumnLayout>
</Container>
);
};
Loading
Loading