Локальні робочі процеси розробника: Використання WebGPU та WebContainers для донавчання LLM у браузері
Глибоке занурення в запуск та адаптацію LLM безпосередньо в пісочниці браузера. Мы об'єднуємо StackBlitz WebContainers із WebGPU та LoRA для створення безкоштовної приватної AI-пісочниці.

Запуск великих мовних моделей (LLM) під час розробки зазвичай вимагає використання хмарних API або важких локальних служб (демонів), таких як Ollama. Хоча хмарні API забезпечують високу швидкість, вони тягнуть за собою щомісячну абонентську плату, залежність від Інтернету та потенційні проблеми з конфіденційністю даних.
Завдяки нещодавнім удосконаленням веб-платформ тепер ми можемо повністю уникнути цих компромісів. Об'єднавши StackBlitz WebContainers — які запускають повноцінне робоче середовище Node.js прямо у вкладці браузера — з WebGPU для локального апаратного прискорення, ми можемо створити повністю ізольоване, приватне та безкоштовне середовище розробки ШІ (AI).
У цьому посібнику ми розглянемо архітектуру браузерної AI IDE. Ми реалізуємо Web Worker для керування інференсом (виведенням) WebGPU, розглянемо, як застосовувати адаптери Low-Rank Adaptation (LoRA) безпосередньо в клієнтському Javascript, і зв'яжемо ШІ-движок із віртуальною файловою системою WebContainer.
The Architecture of a Browser-Based Sandbox
To run both a Node.js development server and an LLM client-side, our architecture must isolate computation to prevent the browser's main thread from freezing.
+-------------------------------------------------------------------------+
| Browser Tab |
| |
| +---------------------------+ +-------------------------+ |
| | Main Thread | | Web Worker (WebGPU) | |
| | | | | |
| | +-----------------------+ | PostMessage| +--------------------+ | |
| | | WebContainer Instance | | <---------> | | WebLLM Engine | | |
| | | (Vite, Monaco Editor) | | | +----------+---------+ | |
| | +-----------+-----------+ | | | | |
| +-------------|-------------+ +-------------|-----------+ |
| v v |
| [Virtual Filesystem] [IndexedDB Cache] |
| | |
| v |
| [GPU VRAM] |
+-------------------------------------------------------------------------+- Основний Потік (Main Thread): Renders the Monaco Editor UI and boots the WebContainer instance. The WebContainer compiles code, runs dev servers, and serves hot-module reloaded (HMR) assets via a virtual network.
- WebGPU Worker: Houses the AI inference engine. Running the LLM inside a Web Worker prevents heavy WebGPU tensor calculations from blocking user interactions (like typing or scrolling).
- Кеш IndexedDB: Stores the quantized model weights (e.g., gemma-4-e2b-it-q4f16_1-MLC, Qwen2.5-Coder-1.5B-Instruct, or a distilled reasoning model like DeepSeek-R1-Distill-Qwen-1.5B). On first visit, the weights download; on subsequent visits, they load instantly from disk.
Implementing the WebGPU Worker
We will use the @mlc-ai/web-llm library to run our model inside the Web Worker. This library leverages WebGPU compiler backends to execute compiled weights directly on local silicon.
First, let's write the Web Worker code (llm.worker.ts) to handle model loading, inference, and token streaming:
import { CreateWebGPUEngine, WebGPUEngine } from "@mlc-ai/web-llm";
let engine: WebGPUEngine | null = null;
// Listen for commands from the main thread
self.onmessage = async (event: MessageEvent) => {
const { type, payload } = event.data;
switch (type) {
case "INIT_ENGINE":
try {
// payload.model can be a prebuilt model ID string or a custom model configuration object
engine = await CreateWebGPUEngine(payload.model, {
initProgressCallback: (report) => {
self.postMessage({ type: "INIT_PROGRESS", payload: report.text });
}
});
self.postMessage({ type: "ENGINE_READY" });
} catch (err) {
self.postMessage({ type: "ERROR", payload: (err as Error).message });
}
break;
case "GENERATE":
if (!engine) {
self.postMessage({ type: "ERROR", payload: "Engine not initialized" });
return;
}
try {
const { prompt, systemPrompt } = payload;
const messages = [
{ role: "system", content: systemPrompt || "You are a helpful coding assistant." },
{ role: "user", content: prompt }
];
const completion = await engine.chat.completions.create({
messages: messages as any,
stream: true,
temperature: 0.2,
max_tokens: 1024
});
for await (const chunk of completion) {
const content = chunk.choices[0]?.delta?.content || "";
if (content) {
self.postMessage({ type: "TOKEN", payload: content });
}
}
self.postMessage({ type: "GENERATE_COMPLETE" });
} catch (err) {
self.postMessage({ type: "ERROR", payload: (err as Error).message });
}
break;
default:
console.warn(`Unknown message type: ${type}`);
}
};Applying In-Browser LoRA Adapters
Щоб зробити модель у браузері корисною для конкретних кодових баз, ми можемо завантажити адаптер Low-Rank Adaptation (LoRA). LoRA змінює поведінку виведення, оновлюючи крихітну частку загальної ваги моделі (часто менше 1%), що забезпечує швидке завантаження та застосування під час виконання.
У сучасних середовищах WebLLM адаптери упаковуються як окремі файли .bin і накладаються поверх ваги базової моделі. Ось як ми налаштовуємо WebLLM для завантаження базової моделі (Qwen2.5-Coder-1.5B-Instruct-q4f16_1-MLC) та застосування користувацького адаптера кодової бази:
const modelConfig = {
model_id: "Qwen2.5-Coder-1.5B-Instruct-q4f16_1-MLC",
model_lib: "https://raw.githubusercontent.com/mlc-ai/binary-mlc-llm-libs/main/web-packages/Qwen2.5-Coder-1.5B-Instruct-q4f16_1-MLC-webgpu.wasm",
// Load custom adapter weights from an HTTP endpoint
lora_config: [
{
lora_name: "project-api-adapter",
lora_url: "https://cdn.myportfolio.com/adapters/project-api-lora.bin",
scale: 1.2
}
]
};При ініціалізації WebLLM завантажує базову вагу та компілює шейдери WebGPU. Під час прямого проходу (forward pass) GPU застосовує обчислення дельта-матриці LoRA паралельно з основними блоками уваги (attention). Це дозволяє розробникам донавчати модель на своїй машині, завантажувати 10-мегабайтний адаптер та миттєво поширювати користувацькі патерни кодування на браузери команди.
Bridging WebContainers and WebGPU
Тепер, коли у нас є фоновий воркер, який виконує нашу модель, ми можемо підключити його до нашого робочого простору WebContainer. Основний потік координуватиме читання файлу з віртуальної файлової системи WebContainer, надсилання вмісту воркеру WebGPU та запис зміненого ШІ виведення назад.
Ось клас-координатор, який керує цим потоком зв'язку:
import { WebContainer } from "@webcontainer/api";
export class SandboxAIManager {
private worker: Worker;
private webcontainer: WebContainer;
private onTokenCallback?: (token: string) => void;
constructor(webcontainer: WebContainer) {
this.webcontainer = webcontainer;
// Initialize Web Worker
this.worker = new Worker(
new URL("./llm.worker.ts", import.meta.url),
{ type: "module" }
);
this.worker.onmessage = this.handleWorkerMessage.bind(this);
}
public initModel(model: string | object) {
this.worker.postMessage({
type: "INIT_ENGINE",
payload: { model }
});
}
public async refactorFile(filePath: string, instruction: string, onToken: (t: string) => void) {
this.onTokenCallback = onToken;
// 1. Read file contents from WebContainer virtual filesystem
const fileBytes = await this.webcontainer.fs.readFile(filePath, "utf-8");
// 2. Format context prompt
const prompt = `
Refactor the following file according to this instruction: "${instruction}"
File Path: ${filePath}
Source Code:
\`\`\`typescript
${fileBytes}
\`\`\`
Return only the refactored code inside a single code block. Do not include markdown introductions.
`;
// 3. Trigger GPU inference in worker
this.worker.postMessage({
type: "GENERATE",
payload: { prompt }
});
}
private async handleWorkerMessage(event: MessageEvent) {
const { type, payload } = event.data;
if (type === "TOKEN" && this.onTokenCallback) {
this.onTokenCallback(payload);
} else if (type === "GENERATE_COMPLETE") {
console.log("Code generation finished.");
} else if (type === "INIT_PROGRESS") {
console.log(`Loading model progress: ${payload}`);
}
}
}Performance & VRAM Constraints in the Browser
Запуск LLM на стороні клієнта високоефективний після завантаження, але він працює в межах строгих обмежень пісочниці, що виділяються браузером.
1. Ліміти виділення VRAM (VRAM Allocation Limits)
WebGPU виділяє блоки пам'яті безпосередньо на відеокарті хост-комп'ютера. Однак браузери накладають жорсткі обмеження на окремі виділення пам'яті GPU (часто обмежуючись від 2 ГБ до 4 ГБ залежно від операційної системи та класу обладнання).
- Усунення: Моделі мають бути строго квантовані. Модель з 1,5 мільярдами параметрів, така як Qwen2.5-Coder-Instruct, вимагає близько 1,3 ГБ VRAM при використанні схеми квантування
q4f16_1(4-бітної), а дистильовані моделі міркувань, такі як DeepSeek-R1-Distill-Qwen-1.5B, працюють у межах ~1,4 ГБ VRAM, що чудово укладається в межі безпеки браузера.
2. Збої пам'яті вкладки (OOM)
У той час як пам'ять WebGPU живе на відеокарті, WebAssembly (WASM) та середовище виконання Javascript продовжують керувати рівнями оркестрації. Якщо модель намагається завантажити велику вагу безпосередньо в пам'ять процесора перед відправкою її на відеокарту, Chrome може викликати збій сторінки через брак пам'яті (OOM).
- Усунення: Використовуйте потокові буфери масивів (streaming array buffers). Потокова передача завантаженої ваги безпосередньо в IndexedDB з подальшим поблоковим аналізом прямо в текстури GPU дозволяє уникнути збереження всього файлу моделі в купі (heap) Javascript.
3. Резервні варіанти (Fallbacks)
Якщо браузер користувача не підтримує WebGPU (наприклад, застарілі мобільні браузери або конфігурації з вимкненими прапорцями), оркестратор повинен плавно переключитися на виконання на процесорі через WASM. Хоча швидкість виведення знизиться з ~30 токенів/сек до ~3 токенів/сек, цей резервний варіант гарантує працездатність пісочниці.
Резюме
Використовуючи WebGPU спільно зі StackBlitz WebContainers, веб-додатки можуть переходити від простих презентаційних сторінок до повноцінних автономних середовищ розробки. Поширення користувацьких донавчених робочих процесів тепер таке ж просте, як надання невеликого файлу адаптера LoRA через CDN, що дозволяє командам спільно працювати в пісочницях, що запускаються повністю на клієнтських машинах із нульовими витратами на інфраструктуру.