Skip to content

Commit

Permalink
feat: improving tasks progress
Browse files Browse the repository at this point in the history
  • Loading branch information
axel7083 committed Jan 12, 2024
1 parent a93c4d4 commit 067625c
Show file tree
Hide file tree
Showing 8 changed files with 72 additions and 74 deletions.
23 changes: 4 additions & 19 deletions packages/backend/src/ai.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,13 @@
"readme": "# Locallm\n\nThis repo contains artifacts that can be used to build and run LLM (Large Language Model) services locally on your Mac using podman. These containerized LLM services can be used to help developers quickly prototype new LLM based applications, without the need for relying on any other externally hosted services. Since they are already containerized, it also helps developers move from their prototype to production quicker. \n\n## Current Locallm Services: \n\n* [Chatbot](#chatbot)\n* [Text Summarization](#text-summarization)\n* [Fine-tuning](#fine-tuning)\n\n### Chatbot\n\nA simple chatbot using the gradio UI. Learn how to build and run this model service here: [Chatbot](/chatbot/).\n\n### Text Summarization\n\nAn LLM app that can summarize arbitrarily long text inputs. Learn how to build and run this model service here: [Text Summarization](/summarizer/).\n\n### Fine Tuning \n\nThis application allows a user to select a model and a data set they'd like to fine-tune that model on. Once the application finishes, it outputs a new fine-tuned model for the user to apply to other LLM services. Learn how to build and run this model training job here: [Fine-tuning](/finetune/).\n\n## Architecture\n![](https://raw.githubusercontent.com/MichaelClifford/locallm/main/assets/arch.jpg)\n\nThe diagram above indicates the general architecture for each of the individual model services contained in this repo. The core code available here is the \"LLM Task Service\" and the \"API Server\", bundled together under `model_services`. With an appropriately chosen model downloaded onto your host,`model_services/builds` contains the Containerfiles required to build an ARM or an x86 (with CUDA) image depending on your need. These model services are intended to be light-weight and run with smaller hardware footprints (given the Locallm name), but they can be run on any hardware that supports containers and scaled up if needed.\n\nWe also provide demo \"AI Applications\" under `ai_applications` for each model service to provide an example of how a developers could interact with the model service for their own needs. ",
"models": [
{
"id": "stable-diffusion-xl-base-1.0",
"name": "stable diffusion xl base 1.0",
"id": "llama-2-7b-chat.Q5_K_S",
"name": "Llama-2-7B-Chat-GGUF",
"hw": "CPU",
"registry": "Hugging Face",
"popularity": 3,
"license": "openrail++"
},
{
"id": "albedobase-xl-1.3",
"name": "AlbedoBase XL 1.3",
"hw": "CPU",
"registry": "Civital",
"popularity": 3,
"license": "openrail++"
},
{
"id": "sdxl-turbo",
"name": "SDXL Turbo",
"hw": "CPU",
"registry": "Hugging Face",
"popularity": 3,
"license": "sai-c-community"
"license": "?",
"url": "https://huggingface.co/TheBloke/Llama-2-7B-Chat-GGUF/resolve/main/llama-2-7b-chat.Q5_K_S.gguf"
}
]
}
Expand Down
83 changes: 53 additions & 30 deletions packages/backend/src/managers/applicationManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Task } from '@shared/models/ITask';
import { TaskUtils } from '../utils/taskUtils';
import { getParentDirectory } from '../utils/pathUtils';
import { a } from 'vitest/dist/suite-dF4WyktM';
import type { LocalModelInfo } from '@shared/models/ILocalModelInfo';

// TODO: Need to be configured
export const AI_STUDIO_FOLDER = path.join('podman-desktop', 'ai-studio');
Expand Down Expand Up @@ -117,26 +118,29 @@ export class ApplicationManager {
const filteredContainers = aiConfig.application.containers
.filter((container) => container.arch === undefined || container.arch === arch())

filteredContainers.forEach((container) => {
taskUtil.setTask({
id: container.name,
state: 'loading',
name: `Building ${container.name}`,
})
})
// Download first model available (if exist)
if(recipe.models && recipe.models.length > 0) {
const model = recipe.models[0];
taskUtil.setTask({
id: model.id,
state: 'loading',
name: `Downloading model ${model.name}`,
});

// start downloading the model
const modelId = 'id';
taskUtil.setTask({
id: 'id',
state: 'loading',
name: `Downloading model ${modelId}`,
})
await this.downloadModelMain(model.id, model.url, taskUtil)
}

filteredContainers.forEach((container) => {
taskUtil.setTask({
id: container.name,
state: 'loading',
name: `Building ${container.name}`,
})
});

// Promise all the build images
return Promise.all(
[this.downloadModelMain('id', 'https://huggingface.co/TheBloke/Llama-2-7B-Chat-GGUF/resolve/main/llama-2-7b-chat.Q5_K_S.gguf', taskUtil),
...filteredContainers.map((container) =>
filteredContainers.map((container) =>
{
// We use the parent directory of our configFile as the rootdir, then we append the contextDir provided
const context = path.join(getParentDirectory(configFile), container.contextdir);
Expand Down Expand Up @@ -166,15 +170,10 @@ export class ApplicationManager {
});
}
)
]




)
}


downloadModelMain(modelId: string, url: string, taskUtil: TaskUtils, destFileName?: string): Promise<string> {
return new Promise((resolve, reject) => {
const downloadCallback = (result: DownloadModelResult) => {
Expand All @@ -184,14 +183,15 @@ export class ApplicationManager {
} else {
taskUtil.setTaskState(modelId, 'error');
reject(result.error)
}
}
}
this.downloadModel(modelId, url, downloadCallback, destFileName)

this.downloadModel(modelId, url, taskUtil, downloadCallback, destFileName)
})
}

downloadModel(modelId: string, url: string, callback: (message: DownloadModelResult) => void, destFileName?: string) {
const destDir = path.resolve(this.extensionContext.storagePath, 'models', modelId);
downloadModel(modelId: string, url: string, taskUtil: TaskUtils, callback: (message: DownloadModelResult) => void, destFileName?: string) {
const destDir = path.join(this.homeDirectory, AI_STUDIO_FOLDER, 'models', modelId);
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir, { recursive: true });
}
Expand All @@ -204,17 +204,20 @@ export class ApplicationManager {
let progress = 0;
https.get(url, (resp) => {
if (resp.headers.location) {
this.downloadModel(modelId, resp.headers.location, callback, destFileName);
this.downloadModel(modelId, resp.headers.location, taskUtil, callback, destFileName);
return;
} else {
if (totalFileSize === 0 && resp.headers['content-length']) {
totalFileSize = parseFloat(resp.headers['content-length']);
}
}

resp.on('data', (chunk) => {
progress += chunk.length;
progress += chunk.length;
const progressValue = progress * 100 / totalFileSize;

taskUtil.setTaskProgress(modelId, progressValue);

// send progress in percentage (ex. 1.2%, 2.6%, 80.1%) to frontend
//this.sendProgress(progressValue);
if (progressValue === 100) {
Expand All @@ -223,7 +226,7 @@ export class ApplicationManager {
});
}
});
file.on('finish', () => {
file.on('finish', () => {
file.close();
});
file.on('error', (e) => {
Expand All @@ -235,4 +238,24 @@ export class ApplicationManager {
resp.pipe(file);
});
}

// todo: move somewhere else (dedicated to models)
getLocalModels(): LocalModelInfo[] {
const result: LocalModelInfo[] = [];
const modelsDir = path.join(this.homeDirectory, AI_STUDIO_FOLDER, 'models');
const entries = fs.readdirSync(modelsDir, { withFileTypes: true });
const dirs = entries.filter(dir => dir.isDirectory());
for (const d of dirs) {
const modelEntries = fs.readdirSync(path.resolve(d.path, d.name));
if (modelEntries.length != 1) {
// we support models with one file only for now
continue;
}
result.push({
id: d.name,
file: modelEntries[0],
})
}
return result;
}
}
6 changes: 2 additions & 4 deletions packages/backend/src/studio-api-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,14 @@ import content from './ai.json';
import { ApplicationManager } from './managers/applicationManager';
import { RecipeStatusRegistry } from './registries/RecipeStatusRegistry';
import { RecipeStatus } from '@shared/models/IRecipeStatus';
import { Task } from '@shared/models/ITask';
import { ModelInfo } from '@shared/models/IModelInfo';
import { Studio } from './studio';

export const RECENT_CATEGORY_ID = 'recent-category';

export class StudioApiImpl implements StudioAPI {
constructor(
private applicationManager: ApplicationManager,
private recipeStatusRegistry: RecipeStatusRegistry,
private studio: Studio,
) {}

async openURL(url: string): Promise<void> {
Expand Down Expand Up @@ -69,8 +66,9 @@ export class StudioApiImpl implements StudioAPI {
}

async getLocalModels(): Promise<ModelInfo[]> {
const local = this.studio.getLocalModels();
const local = this.applicationManager.getLocalModels();
const localIds = local.map(l => l.id);
return content.recipes.flatMap(r => r.models.filter(m => localIds.includes(m.id)));
}

}
20 changes: 0 additions & 20 deletions packages/backend/src/studio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@ export class Studio {
this.studioApi = new StudioApiImpl(
applicationManager,
recipeStatusRegistry,
this
);
// Register the instance
this.rpcExtension.registerInstance<StudioApiImpl>(StudioApiImpl, this.studioApi);
Expand All @@ -117,23 +116,4 @@ export class Studio {
localResourceRoots: [Uri.joinPath(extensionUri, 'media')],
};
}

getLocalModels(): LocalModelInfo[] {
const result: LocalModelInfo[] = [];
const modelsDir = path.resolve(this.#extensionContext.storagePath, 'models');
const entries = fs.readdirSync(modelsDir, { withFileTypes: true });
const dirs = entries.filter(dir => dir.isDirectory());
for (const d of dirs) {
const modelEntries = fs.readdirSync(path.resolve(d.path, d.name));
if (modelEntries.length != 1) {
// we support models with one file only for now
continue;
}
result.push({
id: d.name,
file: modelEntries[0],
})
}
return result;
}
}
10 changes: 10 additions & 0 deletions packages/backend/src/utils/taskUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ export class TaskUtils {
})
}

setTaskProgress(taskId: string, value: number) {
if(!this.tasks.has(taskId))
throw new Error('task not found.');
const task = this.tasks.get(taskId);
this.setTask({
...task,
progress: value,
})
}

toRecipeStatus(): RecipeStatus {
return {
recipeId: this.recipeId,
Expand Down
2 changes: 1 addition & 1 deletion packages/frontend/src/lib/progress/TasksProgress.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export let tasks: Task[] = [];
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z"/>
</svg>
{/if}
{task.name}
{task.name} {#if task.progress}({task.progress}%){/if}
</li>
{/each}
</ul>
1 change: 1 addition & 0 deletions packages/shared/models/IModelInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export interface ModelInfo {
registry: string;
popularity: number;
license: string;
url: string;
}
1 change: 1 addition & 0 deletions packages/shared/models/ITask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ export type TaskState = 'loading' | 'error' | 'success'
export interface Task {
id: string,
state: TaskState;
progress?: number
name: string;
}

0 comments on commit 067625c

Please sign in to comment.