From 6299dcd084abaf02d721ad964e29daefabc9ef25 Mon Sep 17 00:00:00 2001 From: Thiago Dallacqua <104855841+thiagodallacqua-hpe@users.noreply.github.com> Date: Fri, 12 Jul 2024 15:28:38 -0300 Subject: [PATCH] feat: add basic lineage MDLM link (#9482) --- .../react/src/components/CheckpointModal.tsx | 11 + webui/react/src/components/OverviewStats.tsx | 4 +- .../responses/experiment-details/set-a.json | 285 ++++++++++++++++++ webui/react/src/ioTypes.ts | 2 + webui/react/src/pages/ModelVersionDetails.tsx | 15 +- .../pages/TrialDetails/TrialInfoBox.test.tsx | 21 ++ .../src/pages/TrialDetails/TrialInfoBox.tsx | 27 +- webui/react/src/services/decoder.ts | 1 + webui/react/src/types.ts | 23 ++ webui/react/src/utils/integrations.test.ts | 32 ++ webui/react/src/utils/integrations.ts | 8 + 11 files changed, 426 insertions(+), 3 deletions(-) create mode 100644 webui/react/src/utils/integrations.test.ts create mode 100644 webui/react/src/utils/integrations.ts diff --git a/webui/react/src/components/CheckpointModal.tsx b/webui/react/src/components/CheckpointModal.tsx index ff31f1df364..624b420b3ba 100644 --- a/webui/react/src/components/CheckpointModal.tsx +++ b/webui/react/src/components/CheckpointModal.tsx @@ -16,6 +16,7 @@ import { } from 'types'; import { formatDatetime } from 'utils/datetime'; import handleError, { DetError, ErrorType } from 'utils/error'; +import { createPachydermLineageLink } from 'utils/integrations'; import { humanReadableBytes } from 'utils/string'; import { checkpointSize } from 'utils/workload'; @@ -146,6 +147,16 @@ ${checkpoint?.totalBatches}? This action may complete or fail without further no { label: 'State', value: }, ]; + if (config.integrations?.pachyderm !== undefined) { + const pachydermData = config.integrations.pachyderm; + const url = createPachydermLineageLink(pachydermData); + + glossaryContent.splice(1, 0, { + label: 'Data Input', + value: {pachydermData.dataset.repo}, + }); + } + if (checkpoint.uuid) glossaryContent.push({ label: 'UUID', value: checkpoint.uuid }); glossaryContent.push({ label: 'Location', value: getStorageLocation(config, checkpoint) }); if (searcherMetric) diff --git a/webui/react/src/components/OverviewStats.tsx b/webui/react/src/components/OverviewStats.tsx index a516baddfe1..0ea3642b17e 100644 --- a/webui/react/src/components/OverviewStats.tsx +++ b/webui/react/src/components/OverviewStats.tsx @@ -5,10 +5,12 @@ import Row from 'hew/Row'; import { Label, TypographySize } from 'hew/Typography'; import React from 'react'; +import { AnyMouseEvent } from 'utils/routes'; + interface Props { children: React.ReactNode; focused?: boolean; - onClick?: () => void; + onClick?: (e: AnyMouseEvent) => void; title: string; } diff --git a/webui/react/src/fixtures/responses/experiment-details/set-a.json b/webui/react/src/fixtures/responses/experiment-details/set-a.json index 19aed4419fe..86eda3d0c10 100644 --- a/webui/react/src/fixtures/responses/experiment-details/set-a.json +++ b/webui/react/src/fixtures/responses/experiment-details/set-a.json @@ -2523,5 +2523,290 @@ "source_trial_id": null } } + }, + { + "experiment": { + "id": 7230, + "description": "", + "labels": [], + "startTime": "2024-06-26T19:00:17.969118Z", + "endTime": "2024-06-26T19:06:30.861340Z", + "state": "STATE_COMPLETED", + "archived": false, + "numTrials": 1, + "trialIds": [49301], + "displayName": "", + "userId": 1262, + "username": "thiago.menezes-dallacqua-admin", + "resourcePool": "compute-pool", + "searcherType": "\"single\"", + "searcherMetric": "", + "hyperparameters": null, + "name": "core-api-stage-2", + "notes": "", + "jobId": "4f75ae18-b425-4b3c-a9f6-8b6bc69d5403", + "forkedFrom": 7129, + "progress": 1, + "projectId": 2014, + "projectName": "test integration 1", + "workspaceId": 1816, + "workspaceName": "test integration", + "parentArchived": false, + "config": { + "bind_mounts": [], + "checkpoint_policy": "best", + "checkpoint_storage": { + "access_key": null, + "bucket": "det-determined-main-us-west-2-573932760021", + "endpoint_url": null, + "prefix": null, + "save_experiment_best": 0, + "save_trial_best": 1, + "save_trial_latest": 1, + "secret_key": null, + "type": "s3" + }, + "data": {}, + "debug": false, + "description": null, + "entrypoint": "python3 2_checkpoints.py", + "environment": { + "add_capabilities": [], + "drop_capabilities": [], + "environment_variables": { + "cpu": [], + "cuda": [], + "rocm": [] + }, + "force_pull_image": false, + "image": { + "cpu": "determinedai/pytorch-ngc-dev:e960eae", + "cuda": "determinedai/pytorch-ngc-dev:e960eae", + "rocm": "determinedai/environments:rocm-5.0-pytorch-1.10-tf-2.7-rocm-622d512" + }, + "pod_spec": null, + "ports": {}, + "proxy_ports": [], + "registry_auth": null + }, + "hyperparameters": {}, + "integrations": { + "pachyderm": { + "dataset": { + "branch": "master", + "commit": "1d2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b", + "project": "test-project", + "repo": "test-data", + "token": "1234567890abcdef1234567890abcdef" + }, + "pachd": { + "host": "localhost", + "port": 30650 + }, + "proxy": { + "host": "localhost", + "port": 80, + "scheme": "http" + } + } + }, + "labels": [], + "log_policies": [], + "max_restarts": 0, + "min_checkpoint_period": { + "batches": 0 + }, + "min_validation_period": { + "batches": 0 + }, + "name": "core-api-stage-2", + "optimizations": { + "aggregation_frequency": 1, + "auto_tune_tensor_fusion": false, + "average_aggregated_gradients": true, + "average_training_metrics": true, + "grad_updates_size_file": null, + "gradient_compression": false, + "mixed_precision": "O0", + "tensor_fusion_cycle_time": 1, + "tensor_fusion_threshold": 64 + }, + "pbs": {}, + "perform_initial_validation": false, + "profiling": { + "begin_on_batch": 0, + "enabled": false, + "end_after_batch": null, + "sync_timings": true + }, + "project": "test integration 1", + "records_per_epoch": 0, + "reproducibility": { + "experiment_seed": 1718898986 + }, + "resources": { + "devices": [], + "is_single_node": null, + "max_slots": null, + "native_parallel": false, + "priority": null, + "resource_pool": "compute-pool", + "shm_size": null, + "slots_per_trial": 1, + "weight": 1 + }, + "scheduling_unit": 100, + "searcher": { + "max_length": 1, + "metric": "x", + "name": "single", + "smaller_is_better": true, + "source_checkpoint_uuid": null, + "source_trial_id": null + }, + "slurm": {}, + "workspace": "test integration" + }, + "originalConfig": "environment:\n add_capabilities: []\n drop_capabilities: []\n environment_variables:\n cpu: []\n cuda: []\n rocm: []\n force_pull_image: false\n image:\n cpu: determinedai/pytorch-ngc-dev:e960eae\n cuda: determinedai/pytorch-ngc-dev:e960eae\n rocm: determinedai/environments:rocm-5.0-pytorch-1.10-tf-2.7-rocm-622d512\n pod_spec: null\n ports: {}\n proxy_ports: []\nproject: test integration 1\nworkspace: test integration\nbind_mounts: []\ncheckpoint_policy: best\ncheckpoint_storage:\n access_key: null\n bucket: det-determined-main-us-west-2-573932760021\n endpoint_url: null\n prefix: null\n save_experiment_best: 0\n save_trial_best: 1\n save_trial_latest: 1\n secret_key: null\n type: s3\ndata: {}\ndebug: false\ndescription: null\nentrypoint: python3 2_checkpoints.py\nhyperparameters: {}\nintegrations:\n pachyderm:\n dataset:\n branch: master\n commit: 1d2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b\n project: test-project\n repo: test-data\n token: 1234567890abcdef1234567890abcdef\n pachd:\n host: localhost\n port: 30650\n proxy:\n host: localhost\n port: 80\n scheme: http\nlabels: []\nlog_policies: []\nmax_restarts: 0\nmin_checkpoint_period:\n batches: 0\nmin_validation_period:\n batches: 0\nname: core-api-stage-2\noptimizations:\n aggregation_frequency: 1\n auto_tune_tensor_fusion: false\n average_aggregated_gradients: true\n average_training_metrics: true\n grad_updates_size_file: null\n gradient_compression: false\n mixed_precision: O0\n tensor_fusion_cycle_time: 1\n tensor_fusion_threshold: 64\npbs: {}\nperform_initial_validation: false\nprofiling:\n begin_on_batch: 0\n enabled: false\n end_after_batch: null\n sync_timings: true\nrecords_per_epoch: 0\nreproducibility:\n experiment_seed: 1718898986\nresources:\n devices: []\n is_single_node: null\n max_slots: null\n native_parallel: false\n priority: null\n resource_pool: compute-pool\n shm_size: null\n slots_per_trial: 1\n weight: 1\nscheduling_unit: 100\nsearcher:\n max_length: 1\n metric: x\n name: single\n smaller_is_better: true\n source_checkpoint_uuid: null\n source_trial_id: null\nslurm: {}\n", + "projectOwnerId": 1262, + "checkpointSize": "79", + "checkpointCount": 2, + "unmanaged": false, + "modelDefinitionSize": 5717, + "pachydermIntegration": { + "dataset": { + "branch": "master", + "commit": "1d2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b", + "project": "test-project", + "repo": "test-data", + "token": "1234567890abcdef1234567890abcdef" + }, + "pachd": { + "host": "localhost", + "port": 30650 + }, + "proxy": { + "host": "localhost", + "port": 80, + "scheme": "http" + } + } + }, + "jobSummary": null, + "config": { + "bind_mounts": [], + "checkpoint_policy": "best", + "checkpoint_storage": { + "access_key": null, + "bucket": "det-determined-main-us-west-2-573932760021", + "endpoint_url": null, + "prefix": null, + "save_experiment_best": 0, + "save_trial_best": 1, + "save_trial_latest": 1, + "secret_key": null, + "type": "s3" + }, + "data": {}, + "debug": false, + "description": null, + "entrypoint": "python3 2_checkpoints.py", + "environment": { + "add_capabilities": [], + "drop_capabilities": [], + "environment_variables": { + "cpu": [], + "cuda": [], + "rocm": [] + }, + "force_pull_image": false, + "image": { + "cpu": "determinedai/pytorch-ngc-dev:e960eae", + "cuda": "determinedai/pytorch-ngc-dev:e960eae", + "rocm": "determinedai/environments:rocm-5.0-pytorch-1.10-tf-2.7-rocm-622d512" + }, + "pod_spec": null, + "ports": {}, + "proxy_ports": [], + "registry_auth": null + }, + "hyperparameters": {}, + "integrations": { + "pachyderm": { + "dataset": { + "branch": "master", + "commit": "1d2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b", + "project": "test-project", + "repo": "test-data", + "token": "1234567890abcdef1234567890abcdef" + }, + "pachd": { + "host": "localhost", + "port": 30650 + }, + "proxy": { + "host": "localhost", + "port": 80, + "scheme": "http" + } + } + }, + "labels": [], + "log_policies": [], + "max_restarts": 0, + "min_checkpoint_period": { + "batches": 0 + }, + "min_validation_period": { + "batches": 0 + }, + "name": "core-api-stage-2", + "optimizations": { + "aggregation_frequency": 1, + "auto_tune_tensor_fusion": false, + "average_aggregated_gradients": true, + "average_training_metrics": true, + "grad_updates_size_file": null, + "gradient_compression": false, + "mixed_precision": "O0", + "tensor_fusion_cycle_time": 1, + "tensor_fusion_threshold": 64 + }, + "pbs": {}, + "perform_initial_validation": false, + "profiling": { + "begin_on_batch": 0, + "enabled": false, + "end_after_batch": null, + "sync_timings": true + }, + "project": "test integration 1", + "records_per_epoch": 0, + "reproducibility": { + "experiment_seed": 1718898986 + }, + "resources": { + "devices": [], + "is_single_node": null, + "max_slots": null, + "native_parallel": false, + "priority": null, + "resource_pool": "compute-pool", + "shm_size": null, + "slots_per_trial": 1, + "weight": 1 + }, + "scheduling_unit": 100, + "searcher": { + "max_length": 1, + "metric": "x", + "name": "single", + "smaller_is_better": true, + "source_checkpoint_uuid": null, + "source_trial_id": null + }, + "slurm": {}, + "workspace": "test integration" + } } ] diff --git a/webui/react/src/ioTypes.ts b/webui/react/src/ioTypes.ts index 2da81f0f9f7..e67f6b2f371 100644 --- a/webui/react/src/ioTypes.ts +++ b/webui/react/src/ioTypes.ts @@ -7,6 +7,7 @@ import { CheckpointStorageType, ExperimentSearcherName, HyperparameterType, + Integration, LogLevel, Primitive, RunState, @@ -215,6 +216,7 @@ export const ioExperimentConfig = io.type({ checkpoint_storage: optional(ioCheckpointStorage), description: optional(io.string), hyperparameters: ioHyperparameters, + integrations: optional(Integration), labels: optional(io.array(io.string)), max_restarts: io.number, name: io.string, diff --git a/webui/react/src/pages/ModelVersionDetails.tsx b/webui/react/src/pages/ModelVersionDetails.tsx index 373529d06a0..9e77c6d4d24 100644 --- a/webui/react/src/pages/ModelVersionDetails.tsx +++ b/webui/react/src/pages/ModelVersionDetails.tsx @@ -23,6 +23,7 @@ import { getModelVersion, patchModelVersion } from 'services/api'; import workspaceStore from 'stores/workspaces'; import { Metadata, ModelVersion, Note, ValueOf } from 'types'; import handleError, { ErrorType } from 'utils/error'; +import { createPachydermLineageLink } from 'utils/integrations'; import { isAborted, isNotFound } from 'utils/service'; import { humanReadableBytes } from 'utils/string'; import { checkpointSize } from 'utils/workload'; @@ -186,7 +187,8 @@ const ModelVersionDetails: React.FC = () => { .sort((a, b) => checkpointResources[a] - checkpointResources[b]) .map((key) => ({ name: key, size: humanReadableBytes(checkpointResources[key]) })); const hasExperiment = !!modelVersion.checkpoint.experimentId; - return [ + const pachydermData = modelVersion.checkpoint.experimentConfig?.integrations?.pachyderm; + const infoElements = [ { label: 'Source', value: hasExperiment ? ( @@ -227,6 +229,17 @@ const ModelVersionDetails: React.FC = () => { value: resources.map((resource) => renderResource(resource.name, resource.size)), }, ]; + + if (pachydermData !== undefined) { + const url = createPachydermLineageLink(pachydermData); + + infoElements.splice(1, 0, { + label: 'Data Input', + value: {pachydermData?.dataset.repo}, + }); + } + + return infoElements; }, [modelVersion?.checkpoint]); const validationMetrics = useMemo(() => { diff --git a/webui/react/src/pages/TrialDetails/TrialInfoBox.test.tsx b/webui/react/src/pages/TrialDetails/TrialInfoBox.test.tsx index 9b6684b0046..c95e522995b 100644 --- a/webui/react/src/pages/TrialDetails/TrialInfoBox.test.tsx +++ b/webui/react/src/pages/TrialDetails/TrialInfoBox.test.tsx @@ -6,6 +6,7 @@ import {} from 'stores/cluster'; import { ThemeProvider } from 'components/ThemeProvider'; import { ExperimentBase, TrialDetails } from 'types'; +import { mockIntegrationData } from 'utils/integrations.test'; import TrialInfoBox from './TrialInfoBox'; @@ -230,4 +231,24 @@ describe('Trial Info Box', () => { expect(await screen.findByText('Forever')).toBeVisible(); }); }); + + describe('Lineage card', () => { + it('should show Data input card with tge lineage link when pachyderm integration data is present', async () => { + const mockExperimentWith = Object.assign( + { ...mockExperiment }, + { + config: { ...mockExperiment.config, integrations: { pachyderm: mockIntegrationData } }, + }, + ); + + setup(mockTrial1, mockExperimentWith); + expect(await screen.findByText('Data input')).toBeVisible(); + expect(await screen.findByText(mockIntegrationData.dataset.repo)).toBeVisible(); + }); + + it('should not show Data input card when pachyderm integration is missing', () => { + setup(mockTrial1, mockExperiment); + expect(screen.queryByText('Data input')).not.toBeInTheDocument(); + }); + }); }); diff --git a/webui/react/src/pages/TrialDetails/TrialInfoBox.tsx b/webui/react/src/pages/TrialDetails/TrialInfoBox.tsx index bb7d72a1c00..cb10cae97f9 100644 --- a/webui/react/src/pages/TrialDetails/TrialInfoBox.tsx +++ b/webui/react/src/pages/TrialDetails/TrialInfoBox.tsx @@ -11,11 +11,13 @@ import Section from 'components/Section'; import TimeAgo from 'components/TimeAgo'; import { useCheckpointFlow } from 'hooks/useCheckpointFlow'; import { NodeElement } from 'pages/ResourcePool/Topology'; -import { paths } from 'routes/utils'; +import { handlePath, paths } from 'routes/utils'; import { getTaskAcceleratorData } from 'services/api'; import { V1AcceleratorData } from 'services/api-ts-sdk/api'; import { CheckpointWorkloadExtended, ExperimentBase, TrialDetails } from 'types'; import handleError from 'utils/error'; +import { createPachydermLineageLink } from 'utils/integrations'; +import { AnyMouseEvent } from 'utils/routes'; import { humanReadableBytes, pluralizer } from 'utils/string'; import css from './TrialInfoBox.module.scss'; @@ -151,6 +153,28 @@ const TrialInfoBox: React.FC = ({ trial, experiment }: Props) => { }, [acceleratorData]); const allocationModal = useModal(allocationModalComponent); + const lineageComponent = useMemo(() => { + const { + config: { integrations }, + } = experiment; + + if (integrations?.pachyderm !== undefined) { + const url = createPachydermLineageLink(integrations.pachyderm); + const handleClickDataInput = (e: AnyMouseEvent) => { + handlePath(e, { + path: url, + }); + }; + + return ( + + {integrations.pachyderm.dataset.repo} + + ); + } + + return null; + }, [experiment]); return (
@@ -186,6 +210,7 @@ const TrialInfoBox: React.FC = ({ trial, experiment }: Props) => { ) : null} {{logRetentionDays}} + {lineageComponent}
diff --git a/webui/react/src/services/decoder.ts b/webui/react/src/services/decoder.ts index 748fcd537cc..9b16ec27578 100644 --- a/webui/react/src/services/decoder.ts +++ b/webui/react/src/services/decoder.ts @@ -373,6 +373,7 @@ export const ioToExperimentConfig = ( : undefined, description: io.description || undefined, hyperparameters: ioToHyperparametereters(io.hyperparameters), + integrations: io.integrations || undefined, labels: io.labels || undefined, maxRestarts: io.max_restarts, name: io.name, diff --git a/webui/react/src/types.ts b/webui/react/src/types.ts index 3d2467667c1..edf64d4efde 100644 --- a/webui/react/src/types.ts +++ b/webui/react/src/types.ts @@ -367,6 +367,28 @@ export type HyperparameterType = t.TypeOf; export type Hyperparameters = { [keys: string]: Hyperparameters | HyperparameterBase; }; + +const PachydermIntegrationData = t.type({ + dataset: t.type({ + branch: t.string, + commit: t.string, + project: t.string, + repo: t.string, + token: t.string, + }), + pachd: t.type({ + host: t.string, + port: t.number, + }), + proxy: t.type({ + host: t.string, + port: t.number, + scheme: t.string, + }), +}); +export const Integration = t.partial({ pachyderm: PachydermIntegrationData }); +export type IntegrationType = t.TypeOf; +export type PachydermIntegrationDataType = t.TypeOf; const Hyperparameters: t.RecursiveType> = t.recursion( 'Hyperparameters', () => t.record(t.string, t.union([Hyperparameters, HyperparameterBase])), @@ -451,6 +473,7 @@ export const ExperimentConfig = t.intersection([ t.partial({ checkpointStorage: CheckpointStorage, description: t.string, + integrations: Integration, labels: t.array(t.string), profiling: t.type({ enabled: t.boolean, diff --git a/webui/react/src/utils/integrations.test.ts b/webui/react/src/utils/integrations.test.ts new file mode 100644 index 00000000000..38f3a0d29d5 --- /dev/null +++ b/webui/react/src/utils/integrations.test.ts @@ -0,0 +1,32 @@ +import { PachydermIntegrationDataType } from 'types'; + +import { createPachydermLineageLink } from './integrations'; + +export const mockIntegrationData: PachydermIntegrationDataType = { + dataset: { + branch: 'test_branch', + commit: 'commit_example_123', + project: 'test-project', + repo: 'test-data', + token: 'token_example_123', + }, + pachd: { + host: 'test_host', + port: 123456, + }, + proxy: { + host: 'test_host', + port: 12, + scheme: 'http', + }, +}; +const expectedResult = `${mockIntegrationData.proxy.scheme}://${mockIntegrationData.proxy.host}:${mockIntegrationData.proxy.port}/lineage/${mockIntegrationData.dataset.project}/repos/${mockIntegrationData.dataset.repo}/commit/${mockIntegrationData.dataset.commit}/?branchId=${mockIntegrationData.dataset.branch}`; + +describe('Integrations', () => { + describe('createPachydermLineageLink', () => { + it('should return the link when passed pachyderm integration data', () => { + const result = createPachydermLineageLink(mockIntegrationData); + expect(result).toBe(expectedResult); + }); + }); +}); diff --git a/webui/react/src/utils/integrations.ts b/webui/react/src/utils/integrations.ts new file mode 100644 index 00000000000..d3c4881f659 --- /dev/null +++ b/webui/react/src/utils/integrations.ts @@ -0,0 +1,8 @@ +import { PachydermIntegrationDataType } from 'types'; + +export const createPachydermLineageLink = ( + pachydermIntegrationData: PachydermIntegrationDataType, +): string => { + const { dataset, proxy } = pachydermIntegrationData; + return `${proxy.scheme}://${proxy.host}:${proxy.port}/lineage/${dataset.project}/repos/${dataset.repo}/commit/${dataset.commit}/?branchId=${dataset.branch}`; +};