Skip to content

Commit

Permalink
feat(ml): support multiple urls (#14347)
Browse files Browse the repository at this point in the history
* support multiple url

* update api

* styling

unnecessary `?.`

* update docs, make new url field go first

add load balancing section

* update tests

doc formatting

wording

wording

linting

* small styling

* `url` -> `urls`

* fix tests

* update docs

* make docusaurus happy

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
  • Loading branch information
mertalev and alextran1502 authored Dec 4, 2024
1 parent 411878c commit 4bf1b84
Show file tree
Hide file tree
Showing 22 changed files with 199 additions and 70 deletions.
42 changes: 31 additions & 11 deletions docs/docs/guides/remote-machine-learning.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
# Remote Machine Learning

To alleviate [performance issues on low-memory systems](/docs/FAQ.mdx#why-is-immich-slow-on-low-memory-systems-like-the-raspberry-pi) like the Raspberry Pi, you may also host Immich's machine-learning container on a more powerful system (e.g. your laptop or desktop computer):

- Set the URL in Machine Learning Settings on the Admin Settings page to point to the designated ML system, e.g. `http://workstation:3003`.
- Copy the following `docker-compose.yml` to your ML system.
- If using [hardware acceleration](/docs/features/ml-hardware-acceleration), the [hwaccel.ml.yml](https://github.com/immich-app/immich/releases/latest/download/hwaccel.ml.yml) file also needs to be added
- Start the container by running `docker compose up -d`.
To alleviate [performance issues on low-memory systems](/docs/FAQ.mdx#why-is-immich-slow-on-low-memory-systems-like-the-raspberry-pi) like the Raspberry Pi, you may also host Immich's machine learning container on a more powerful system, such as your laptop or desktop computer. The server container will send requests containing the image preview to the remote machine learning container for processing. The machine learning container does not persist this data or associate it with a particular user.

:::info
Smart Search and Face Detection will use this feature, but Facial Recognition is handled in the server.
Smart Search and Face Detection will use this feature, but Facial Recognition will not. This is because Facial Recognition uses the _outputs_ of these models that have already been saved to the database. As such, its processing is between the server container and the database.
:::

:::danger
When using remote machine learning, the thumbnails are sent to the remote machine learning container. Use this option carefully when running this on a public computer or a paid processing cloud.
Image previews are sent to the remote machine learning container. Use this option carefully when running this on a public computer or a paid processing cloud. Additionally, as an internal service, the machine learning container has no security measures whatsoever. Please be mindful of where it's deployed and who can access it.
:::

1. Ensure the remote server has Docker installed
2. Copy the following `docker-compose.yml` to the remote server

:::info
If using hardware acceleration, the [hwaccel.ml.yml](https://github.com/immich-app/immich/releases/latest/download/hwaccel.ml.yml) file also needs to be added and the `docker-compose.yml` needs to be configured as described in the [hardware acceleration documentation](/docs/features/ml-hardware-acceleration)
:::

```yaml
Expand All @@ -37,8 +39,26 @@ volumes:
model-cache:
```
Please note that version mismatches between both hosts may cause instabilities and bugs, so make sure to always perform updates together.
3. Start the remote machine learning container by running `docker compose up -d`

:::info
Version mismatches between both hosts may cause bugs and instability, so remember to update this container as well when updating the local Immich instance.
:::

4. Navigate to the [Machine Learning Settings](https://my.immich.app/admin/system-settings?isOpen=machine-learning)
5. Click _Add URL_
6. Fill the new field with the URL to the remote machine learning container, e.g. `http://ip:port`

## Forcing remote processing

Adding a new URL to the settings is recommended over replacing the existing URL. This is because it will allow machine learning tasks to be processed successfully when the remote server is down by falling back to the local machine learning container. If you do not want machine learning tasks to be processed locally when the remote server is not available, you can instead replace the existing URL and only provide the remote container's URL. If doing this, you can remove the `immich-machine-learning` section of the local `docker-compose.yml` file to save resources, as this service will never be used.

Do note that this will mean that Smart Search and Face Detection jobs will fail to be processed when the remote instance is not available. This in turn means that tasks dependent on these features—Duplicate Detection and Facial Recognition—will not run for affected assets. If this occurs, you must manually click the _Missing_ button next to Smart Search and Face Detection in the [Job Status](http://my.immich.app/admin/jobs-status) page for the jobs to be retried.

## Load balancing

While several URLs can be provided in the settings, they are tried sequentially; there is no attempt to distribute load across multiple containers. It is recommended to use a dedicated load balancer for such use-cases and specify it as the only URL. Among other things, it may enable the use of different APIs on the same server by running multiple containers with different configurations. For example, one might run an OpenVINO container in addition to a CUDA container, or run a standard release container to maximize both CPU and GPU utilization.

:::caution
As an internal service, the machine learning container has no security measures whatsoever. Please be mindful of where it's deployed and who can access it.
:::tip
The machine learning container can be shared among several Immich instances regardless of the models a particular instance uses. However, using different models will lead to higher peak memory usage.
:::
2 changes: 1 addition & 1 deletion docs/docs/install/config-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ The default configuration looks like this:
},
"machineLearning": {
"enabled": true,
"url": "http://immich-machine-learning:3003",
"url": ["http://immich-machine-learning:3003"],
"clip": {
"enabled": true,
"modelName": "ViT-B-32__openai"
Expand Down
4 changes: 3 additions & 1 deletion i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"add_to": "Add to...",
"add_to_album": "Add to album",
"add_to_shared_album": "Add to shared album",
"add_url": "Add URL",
"added_to_archive": "Added to archive",
"added_to_favorites": "Added to favorites",
"added_to_favorites_count": "Added {count, number} to favorites",
Expand Down Expand Up @@ -132,7 +133,7 @@
"machine_learning_smart_search_description": "Search for images semantically using CLIP embeddings",
"machine_learning_smart_search_enabled": "Enable smart search",
"machine_learning_smart_search_enabled_description": "If disabled, images will not be encoded for smart search.",
"machine_learning_url_description": "URL of the machine learning server",
"machine_learning_url_description": "The URL of the machine learning server. If more than one URL is provided, each server will be attempted one-at-a-time until one responds successfully, in order from first to last.",
"manage_concurrency": "Manage Concurrency",
"manage_log_settings": "Manage log settings",
"map_dark_style": "Dark style",
Expand Down Expand Up @@ -1045,6 +1046,7 @@
"remove_from_album": "Remove from album",
"remove_from_favorites": "Remove from favorites",
"remove_from_shared_link": "Remove from shared link",
"remove_url": "Remove URL",
"remove_user": "Remove user",
"removed_api_key": "Removed API Key: {name}",
"removed_from_archive": "Removed from archive",
Expand Down
34 changes: 27 additions & 7 deletions mobile/openapi/lib/model/system_config_machine_learning_dto.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 11 additions & 1 deletion open-api/immich-openapi-specs.json
Original file line number Diff line number Diff line change
Expand Up @@ -11857,15 +11857,25 @@
"$ref": "#/components/schemas/FacialRecognitionConfig"
},
"url": {
"deprecated": true,
"description": "This property was deprecated in v1.122.0",
"type": "string"
},
"urls": {
"items": {
"format": "uri",
"type": "string"
},
"minItems": 1,
"type": "array"
}
},
"required": [
"clip",
"duplicateDetection",
"enabled",
"facialRecognition",
"url"
"urls"
],
"type": "object"
},
Expand Down
4 changes: 3 additions & 1 deletion open-api/typescript-sdk/src/fetch-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1178,7 +1178,9 @@ export type SystemConfigMachineLearningDto = {
duplicateDetection: DuplicateDetectionConfig;
enabled: boolean;
facialRecognition: FacialRecognitionConfig;
url: string;
/** This property was deprecated in v1.122.0 */
url?: string;
urls: string[];
};
export type SystemConfigMapDto = {
darkStyle: string;
Expand Down
4 changes: 2 additions & 2 deletions server/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export interface SystemConfig {
};
machineLearning: {
enabled: boolean;
url: string;
urls: string[];
clip: {
enabled: boolean;
modelName: string;
Expand Down Expand Up @@ -206,7 +206,7 @@ export const defaults = Object.freeze<SystemConfig>({
},
machineLearning: {
enabled: process.env.IMMICH_MACHINE_LEARNING_ENABLED !== 'false',
url: process.env.IMMICH_MACHINE_LEARNING_URL || 'http://immich-machine-learning:3003',
urls: [process.env.IMMICH_MACHINE_LEARNING_URL || 'http://immich-machine-learning:3003'],
clip: {
enabled: true,
modelName: 'ViT-B-32__openai',
Expand Down
2 changes: 2 additions & 0 deletions server/src/cores/storage.core.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { vitest } from 'vitest';

vitest.mock('src/constants', () => ({
APP_MEDIA_LOCATION: '/photos',
ADDED_IN_PREFIX: 'This property was added in ',
DEPRECATED_IN_PREFIX: 'This property was deprecated in ',
}));

describe('StorageCore', () => {
Expand Down
15 changes: 12 additions & 3 deletions server/src/dtos/system-config.dto.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { Exclude, Transform, Type } from 'class-transformer';
import {
ArrayMinSize,
IsBoolean,
IsEnum,
IsInt,
Expand All @@ -16,6 +17,7 @@ import {
ValidateNested,
} from 'class-validator';
import { SystemConfig } from 'src/config';
import { PropertyLifecycle } from 'src/decorators';
import { CLIPConfig, DuplicateDetectionConfig, FacialRecognitionConfig } from 'src/dtos/model-config.dto';
import {
AudioCodec,
Expand Down Expand Up @@ -269,9 +271,16 @@ class SystemConfigMachineLearningDto {
@ValidateBoolean()
enabled!: boolean;

@IsUrl({ require_tld: false, allow_underscores: true })
@PropertyLifecycle({ deprecatedAt: 'v1.122.0' })
@Exclude()
url?: string;

@IsUrl({ require_tld: false, allow_underscores: true }, { each: true })
@ArrayMinSize(1)
@Transform(({ obj, value }) => (obj.url ? [obj.url] : value))
@ValidateIf((dto) => dto.enabled)
url!: string;
@ApiProperty({ type: 'array', items: { type: 'string', format: 'uri' }, minItems: 1 })
urls!: string[];

@Type(() => CLIPConfig)
@ValidateNested()
Expand Down
6 changes: 3 additions & 3 deletions server/src/interfaces/machine-learning.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export type DetectedFaces = { faces: Face[] } & VisualResponse;
export type MachineLearningRequest = ClipVisualRequest | ClipTextualRequest | FacialRecognitionRequest;

export interface IMachineLearningRepository {
encodeImage(url: string, imagePath: string, config: ModelOptions): Promise<number[]>;
encodeText(url: string, text: string, config: ModelOptions): Promise<number[]>;
detectFaces(url: string, imagePath: string, config: FaceDetectionOptions): Promise<DetectedFaces>;
encodeImage(urls: string[], imagePath: string, config: ModelOptions): Promise<number[]>;
encodeText(urls: string[], text: string, config: ModelOptions): Promise<number[]>;
detectFaces(urls: string[], imagePath: string, config: FaceDetectionOptions): Promise<DetectedFaces>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class RenameMachineLearningUrlToUrls1733339482860 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
UPDATE system_metadata
SET value = jsonb_insert(value #- '{machineLearning,url}', '{machineLearning,urls}'::text[], jsonb_build_array(value->'machineLearning'->'url'))
WHERE key = 'system-config' AND value->'machineLearning'->'url' IS NOT NULL
`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
UPDATE system_metadata
SET value = jsonb_insert(value #- '{machineLearning,urls}', '{machineLearning,url}'::text[], to_jsonb(value->'machineLearning'->'urls'->>0))
WHERE key = 'system-config' AND value->'machineLearning'->'urls' IS NOT NULL AND jsonb_array_length(value->'machineLearning'->'urls') >= 1
`);
}
}
2 changes: 1 addition & 1 deletion server/src/repositories/event.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect
this.emitHandlers[event].push(item);
}

async emit<T extends EmitEvent>(event: T, ...args: ArgsOf<T>): Promise<void> {
emit<T extends EmitEvent>(event: T, ...args: ArgsOf<T>): Promise<void> {
return this.onEvent({ name: event, args, server: false });
}

Expand Down
Loading

0 comments on commit 4bf1b84

Please sign in to comment.