Skip to content

feat: mutiple backends for recipes #3114

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
69 changes: 37 additions & 32 deletions PACKAGING-GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,56 +33,61 @@ The catalog also lists the models that may be associated to recipes. A model is
citizen in AI Lab as they will be listed in the Models page and can be tested through the playground.

A model has the following attributes:
- ```id```: a unique identifier for the model
- ```name```: the model name
- ```description```: a detailed description about the model
- ```registry```: the model registry where the model is stored
- ```popularity```: an integer field giving the rating of the model. Can be thought as the number of stars
- ```license```: the license under which the model is available
- ```url```: the URL used to download the model
- ```memory```: the memory footprint of the model in bytes, as computed by the workflow `.github/workflows/compute-model-sizes.yaml`
- ```sha256```: the SHA-256 checksum to be used to verify the downloaded model is identical to the original. It is optional and it must be HEX encoded

- `id`: a unique identifier for the model
- `name`: the model name
- `description`: a detailed description about the model
- `registry`: the model registry where the model is stored
- `popularity`: an integer field giving the rating of the model. Can be thought as the number of stars
- `license`: the license under which the model is available
- `url`: the URL used to download the model
- `memory`: the memory footprint of the model in bytes, as computed by the workflow `.github/workflows/compute-model-sizes.yaml`
- `sha256`: the SHA-256 checksum to be used to verify the downloaded model is identical to the original. It is optional and it must be HEX encoded

#### Recipes

A recipe is a sample AI application that is packaged as one or several containers. It is built by AI Lab when the user chooses to download and run it on their workstation. It is provided as
source code and AI Lab will make sure the container images are built prior to launching the containers.

A recipe has the following attributes:
- ```id```: a unique identifier to the recipe
- ```name```: the recipe name
- ```description```: a detailed description about the recipe
- ```repository```: the URL where the recipe code can be retrieved
- ```ref```: an optional ref in the repository to checkout (a branch name, tag name, or commit full id - short commit id won't be recognized). If not defined, the default branch will be used
- ```categories```: an array of category id to be associated by this recipe
- ```basedir```: an optional path within the repository where the ai-lab.yaml file is located. If not provided, the ai-lab.yaml is assumed to be located at the root the repository
- ```readme```: a markdown description of the recipe
- ```models```: an array of model id to be associated with this recipe

- `id`: a unique identifier to the recipe
- `name`: the recipe name
- `description`: a detailed description about the recipe
- `repository`: the URL where the recipe code can be retrieved
- `ref`: an optional ref in the repository to checkout (a branch name, tag name, or commit full id - short commit id won't be recognized). If not defined, the default branch will be used
- `categories`: an array of category id to be associated by this recipe
- `basedir`: an optional path within the repository where the ai-lab.yaml file is located. If not provided, the ai-lab.yaml is assumed to be located at the root the repository
- `readme`: a markdown description of the recipe
- `models`: an array of model id to be associated with this recipe
- `backends`: an array of backends from which models may be associated with this recipe. The backends are used to filter models when associating them to a recipe.

#### Recipe configuration file

The configuration file is called ```ai-lab.yaml``` and follows the following syntax.
The configuration file is called `ai-lab.yaml` and follows the following syntax.

The root elements are called ```version``` and ```application```.
The root elements are called `version` and `application`.

```version``` represents the version of the specifications that ai-lab adheres to (so far, the only accepted value here is `v1.0`).
`version` represents the version of the specifications that ai-lab adheres to (so far, the only accepted value here is `v1.0`).

```application``` contains an attribute called ```containers``` whose syntax is an array of objects containing the following attributes:
- ```name```: the name of the container
- ```contextdir```: the context directory used to build the container.
- ```containerfile```: the containerfile used to build the image
- ```model-service```: a boolean flag used to indicate if the container is running the model or not
- ```arch```: an optional array of architecture for which this image is compatible with. The values follow the
[GOARCH specification](https://go.dev/src/go/build/syslist.go)
- ```gpu-env```: an optional array of GPU environment for which this image is compatible with. The only accepted value here is cuda.
- ```ports```: an optional array of ports for which the application listens to.
`application` contains an attribute called `containers` whose syntax is an array of objects containing the following attributes:

- `name`: the name of the container
- `contextdir`: the context directory used to build the container.
- `containerfile`: the containerfile used to build the image
- `model-service`: a boolean flag used to indicate if the container is running the model or not
- `arch`: an optional array of architecture for which this image is compatible with. The values follow the
[GOARCH specification](https://go.dev/src/go/build/syslist.go)
- `gpu-env`: an optional array of GPU environment for which this image is compatible with. The only accepted value here is cuda.
- `ports`: an optional array of ports for which the application listens to.
- `image`: an optional image name to be used when building the container image.

The container that is running the service (having the ```model-service``` flag equal to ```true```) can use at runtime
the model managed by AI Lab through an environment variable ```MODEL_PATH``` whose value is the full path name of the
The container that is running the service (having the `model-service` flag equal to `true`) can use at runtime
the model managed by AI Lab through an environment variable `MODEL_PATH` whose value is the full path name of the
model file.

Below is given an example of such a configuration file:

```yaml
application:
containers:
Expand Down
28 changes: 14 additions & 14 deletions packages/backend/src/assets/ai.json

Large diffs are not rendered by default.

13 changes: 9 additions & 4 deletions packages/backend/src/managers/catalogManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,9 +289,14 @@ export class CatalogManager extends Publisher<ApplicationCatalog> implements Dis
result = res;
break;
}
case 'tools':
result = result.filter(r => values.includes(r.backend ?? ''));
case 'tools': {
let res: Recipe[] = [];
for (const value of values) {
res = [...res, ...result.filter(r => r.backends?.includes(value))];
}
result = res;
Comment on lines +293 to +297
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: I don't understand what this code is doing? can we add a comment? filtering result? shallow copy?

break;
}
case 'frameworks': {
let res: Recipe[] = [];
for (const value of values) {
Expand Down Expand Up @@ -325,13 +330,13 @@ export class CatalogManager extends Publisher<ApplicationCatalog> implements Dis
choices.tools = this.filterRecipes(subfilters).choices.tools;
} else {
choices.tools = result
.map(r => r.backend)
.flatMap(r => r.backends)
.filter(b => b !== undefined)
.filter((value, index, array) => array.indexOf(value) === index)
.sort((a, b) => a.localeCompare(b))
Comment on lines +333 to 336
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remark (non-blocking): non related to this PR, but this filtering chain is unreadable, what is r ? what is b?, what is the type of value, what is the type of a and b? :/

.map(t => ({
name: t,
count: result.filter(r => r.backend === t).length,
count: result.filter(r => r.backends?.includes(t)).length,
}));
}

Expand Down
2 changes: 1 addition & 1 deletion packages/backend/src/managers/recipes/RecipeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export class RecipeManager implements Disposable {

let inferenceServer: InferenceServer | undefined;
// if the recipe has a defined backend, we gives priority to using an inference server
if (recipe.backend && recipe.backend === model.backend) {
if (model.backend && recipe.backends?.includes(model.backend)) {
let task: Task | undefined;
try {
inferenceServer = this.inferenceManager.findServerByModel(model);
Expand Down
6 changes: 3 additions & 3 deletions packages/backend/src/tests/ai-test.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"description": "",
"repository": "",
"readme": "",
"backend": "tool1",
"backends": ["tool1"],
"languages": ["lang1", "lang10"],
"frameworks": ["fw1", "fw10"]
},
Expand All @@ -38,7 +38,7 @@
"description": "",
"repository": "",
"readme": "",
"backend": "tool2",
"backends": ["tool2"],
"languages": ["lang2", "lang10"],
"frameworks": ["fw2", "fw10"]
},
Expand All @@ -49,7 +49,7 @@
"description": "",
"repository": "",
"readme": "",
"backend": "tool3",
"backends": ["tool3"],
"languages": ["lang3", "lang11"],
"frameworks": ["fw2", "fw10", "fw11"]
}
Expand Down
92 changes: 92 additions & 0 deletions packages/backend/src/utils/catalogUtils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,98 @@ describe('sanitize', () => {
expect(catalog.recipes[0].languages).toStrictEqual(['lang1']);
expect(catalog.recipes[0].frameworks).toStrictEqual(['fw1']);
});

test('should return backend recipe as string', () => {
const raw = {
version: '1.0',
recipes: [
{
id: 'chatbot',
description: 'This is a Streamlit chat demo application.',
name: 'ChatBot',
repository: 'https://github.com/containers/ai-lab-recipes',
ref: 'v1.1.3',
icon: 'natural-language-processing',
categories: ['natural-language-processing'],
basedir: 'recipes/natural_language_processing/chatbot',
readme: '',
recommended: ['hf.instructlab.granite-7b-lab-GGUF', 'hf.instructlab.merlinite-7b-lab-GGUF'],
backends: ['llama-cpp'],
languages: ['lang1'],
frameworks: ['fw1'],
},
],
models: [
{
id: 'Mistral-7B-Instruct-v0.3-Q4_K_M.gguf',
name: 'Mistral-7B-Instruct-v0.3-Q4_K_M',
description: 'Model imported from path\\Mistral-7B-Instruct-v0.3-Q4_K_M.gguf',
hw: 'CPU',
file: {
path: 'path',
file: 'Mistral-7B-Instruct-v0.3-Q4_K_M.gguf',
size: 4372812000,
creation: '2024-06-19T12:14:12.489Z',
},
memory: 4372812000,
},
],
};
expect(hasCatalogWrongFormat(raw)).toBeFalsy();
const catalog = sanitize(raw);
expect(catalog.version).equals(CatalogFormat.CURRENT);
expect(catalog.models[0].backend).toBeUndefined();
expect(catalog.models[0].name).equals('Mistral-7B-Instruct-v0.3-Q4_K_M');
expect(catalog.recipes[0].languages).toStrictEqual(['lang1']);
expect(catalog.recipes[0].frameworks).toStrictEqual(['fw1']);
expect(catalog.recipes[0].backends).toStrictEqual(['llama-cpp']);
});

test('should return multiple backend recipe as array', () => {
const raw = {
version: '1.0',
recipes: [
{
id: 'chatbot',
description: 'This is a Streamlit chat demo application.',
name: 'ChatBot',
repository: 'https://github.com/containers/ai-lab-recipes',
ref: 'v1.1.3',
icon: 'natural-language-processing',
categories: ['natural-language-processing'],
basedir: 'recipes/natural_language_processing/chatbot',
readme: '',
recommended: ['hf.instructlab.granite-7b-lab-GGUF', 'hf.instructlab.merlinite-7b-lab-GGUF'],
backends: ['llama-cpp', 'openvino'],
languages: ['lang1'],
frameworks: ['fw1'],
},
],
models: [
{
id: 'Mistral-7B-Instruct-v0.3-Q4_K_M.gguf',
name: 'Mistral-7B-Instruct-v0.3-Q4_K_M',
description: 'Model imported from path\\Mistral-7B-Instruct-v0.3-Q4_K_M.gguf',
hw: 'CPU',
file: {
path: 'path',
file: 'Mistral-7B-Instruct-v0.3-Q4_K_M.gguf',
size: 4372812000,
creation: '2024-06-19T12:14:12.489Z',
},
memory: 4372812000,
},
],
};
expect(hasCatalogWrongFormat(raw)).toBeFalsy();
const catalog = sanitize(raw);
expect(catalog.version).equals(CatalogFormat.CURRENT);
expect(catalog.models[0].backend).toBeUndefined();
expect(catalog.models[0].name).equals('Mistral-7B-Instruct-v0.3-Q4_K_M');
expect(catalog.recipes[0].languages).toStrictEqual(['lang1']);
expect(catalog.recipes[0].frameworks).toStrictEqual(['fw1']);
expect(catalog.recipes[0].backends).toStrictEqual(['llama-cpp', 'openvino']);
});
});

describe('merge', () => {
Expand Down
11 changes: 10 additions & 1 deletion packages/backend/src/utils/catalogUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,15 @@ export function isStringArray(obj: unknown): obj is Array<string> {
return Array.isArray(obj) && obj.every(item => typeof item === 'string');
}

function sanitizeBackends(recipe: object): string[] | undefined {
if ('backend' in recipe && typeof recipe.backend === 'string') {
return [recipe.backend];
} else if ('backends' in recipe && Array.isArray(recipe.backends)) {
return recipe.backends;
}
return undefined;
}

export function sanitizeRecipe(recipe: unknown): Recipe {
if (
isNonNullObject(recipe) &&
Expand Down Expand Up @@ -143,7 +152,7 @@ export function sanitizeRecipe(recipe: unknown): Recipe {
icon: 'icon' in recipe && typeof recipe.icon === 'string' ? recipe.icon : undefined,
basedir: 'basedir' in recipe && typeof recipe.basedir === 'string' ? recipe.basedir : undefined,
recommended: 'recommended' in recipe && isStringArray(recipe.recommended) ? recipe.recommended : undefined,
backend: 'backend' in recipe && typeof recipe.backend === 'string' ? recipe.backend : undefined,
backends: sanitizeBackends(recipe),
languages: 'languages' in recipe && isStringArray(recipe.languages) ? recipe.languages : undefined,
frameworks: 'frameworks' in recipe && isStringArray(recipe.frameworks) ? recipe.frameworks : undefined,
};
Expand Down
2 changes: 1 addition & 1 deletion packages/frontend/src/lib/RecipeCardTags.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const recipe = {
categories: ['natural-language-processing', 'audio'],
languages: ['java', 'python'],
frameworks: ['langchain', 'vectordb'],
backend: 'whisper-cpp',
backends: ['whisper-cpp'],
};

class ResizeObserver {
Expand Down
2 changes: 1 addition & 1 deletion packages/frontend/src/lib/RecipeCardTags.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ let { recipe }: Props = $props();

const TAGS: string[] = [
...recipe.categories,
...(recipe.backend !== undefined ? [recipe.backend] : []),
...(recipe.backends ?? []),
...(recipe.frameworks ?? []),
...(recipe.languages ?? []),
];
Expand Down
Loading
Loading