Vietiniai kūrėjų procesai: WebGPU ir WebContainers panaudojimas LLM koregavimui naršyklėje
Gilus pasinėrimas į LLM vykdymą ir pritaikymą tiesiogiai naršyklės smėlio dėžėje (sandbox). Sujungiame StackBlitz WebContainers su WebGPU ir LoRA, kad sukurtume nemokamą, privačią AI žaidimų aikštelę.

Didžiųjų kalbos modelių (LLM) vykdymui kūrimo etape paprastai reikalingos debesijos pagrindu veikiančios API arba sunkios vietinės tarnybos (daemons), tokios kaip Ollama. Nors debesijos API siūlo didelį greitį, jos reikalauja periodinių prenumeratos mokesčių, priklauso nuo interneto ryšio ir kelia potencialių duomenų privatumo problemų.
Dėl naujausių žiniatinklio platformų patobulinimų dabar galime visiškai išvengti šių kompromisų. Sujungę StackBlitz WebContainers — kurie paleidžia pilną Node.js vykdymo aplinką tiesiogiai naršyklės kortelėje — su WebGPU vietiniam aparatinės įrangos spartinimui, galime sukurti visiškai izoliuotą, privačią ir nemokamą dirbtinio intelekto (AI) kūrėjų žaidimų aikštelę.
Šiame vadove apžvelgsime naršyklėje veikiančios AI IDE architektūrą. Įdiegsime Web Worker programą, skirtą valdyti WebGPU išvedimą (inference), išnagrinėsime, kaip pritaikyti Low-Rank Adaptation (LoRA) adapterius tiesiogiai kliento pusės Javascript kode, ir prijungsime AI variklį prie WebContainer virtualios failų sistemos.
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] |
+-------------------------------------------------------------------------+- Pagrindinė Gija (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 Kešas: 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
Kad naršyklėje veikiantis modelis būtų naudingas konkrečioms kodų bazėms, galime įkelti Low-Rank Adaptation (LoRA) adapterį. LoRA pakeičia išvesties elgseną atnaujindama nedidelę dalį bendrųjų modelio svorių (dažnai mažiau nei 1%), todėl jį atsisiųsti ir pritaikyti vykdymo metu galima labai greitai.
Šiuolaikinėse WebLLM aplinkose adapteriai pateikiami kaip atskiri .bin failai ir sluoksniuojami ant bazinio modelio svorių. Štai kaip sukonfigūruojame WebLLM, kad įkeltų bazinį modelį (Qwen2.5-Coder-1.5B-Instruct-q4f16_1-MLC) ir pritaikytų pasirinktinį konkrečios kodų bazės adapterį:
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
}
]
};Kai WebLLM inicializuojamas, jis ištraukia bazinius svorius ir sukompiliuoja WebGPU šeiderius. Vykdymo metu GPU atlieka LoRA delta matricos skaičiavimus lygiagrečiai su pagrindiniais dėmesio (attention) blokais. Tai leidžia kūrėjams suderinti modelį savo kompiuteryje, įkelti 10 MB adapterį ir akimirksniu išplatinti pasirinktinius kodavimo šablonus komandos naršyklėse.
Bridging WebContainers and WebGPU
Dabar, kai turime fone veikiantį workerį, atliekantį modelio skaičiavimus, galime jį prijungti prie mūsų WebContainer darbo srities. Pagrindinė gija koordinuos failo skaitymą iš WebContainer virtualios failų sistemos, turinio siuntimą į WebGPU workerį ir gauto AI pakoreguoto kodo įrašymą atgal.
Štai šį ryšio srautą valdanti koordinavimo klasė:
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
Vietinių LLM vykdymas kliento pusėje yra itin efektyvus po atsisiuntimo, tačiau jis veikia griežtose naršyklės skirto smėlio dėžės apribojimų ribose.
1. VRAM skyrimo ribos (VRAM Allocation Limits)
WebGPU skiria atminties blokus tiesiogiai priimančio kompiuterio vaizdo plokštėje. Tačiau naršyklės nustato griežtus apribojimus atskiriems GPU skyrimams (dažnai ribojama iki 2 GB – 4 GB, priklausomai nuo operacinės sistemos ir techninės įrangos lygio).
- Švelninimas: Modeliai turi būti griežtai kvantuoti. 1,5 milijardo parametrų modeliui, tokiam kaip Qwen2.5-Coder-Instruct, reikia ~1,3 GB VRAM, kai naudojama
q4f16_1(4 bitų) kvantavimo schema, o distiliuoti samprotavimo modeliai, tokie kaip DeepSeek-R1-Distill-Qwen-1.5B, veikia ~1,4 GB VRAM ribose, o tai puikiai telpa naršyklės saugumo ribose.
2. Kortelės atminties perpildymas (OOM)
Nors WebGPU atmintis randasi pačiame GPU, WebAssembly (WASM) ir Javascript vykdymo aplinka vis tiek valdo koordinavimo sluoksnius. Jei modelis bando įkelti didelius svorius tiesiai į CPU atmintį prieš išsiųsdamas juos į GPU, Chrome gali sukelti puslapio atminties perpildymo (OOM) klaidą.
- Švelninimas: Naudokite srautinio perdavimo masyvo buferius (streaming array buffers). Siunčiant atsisiunčiamus svorius tiesiai į IndexedDB ir po to juos dalimis nuskaitant tiesiai į GPU tekstūras, išvengiama viso modelio failo saugojimo Javascript atmintyje.
3. Atsarginiai variantai (Fallbacks)
Jei naudotojo naršyklė nepalaiko WebGPU (pvz., pasenusios mobiliosios naršyklės arba konfigūracijos su išjungtomis vėliavėlėmis (flags)), koordinatorius turėtų sklandžiai pereiti prie WASM CPU vykdymo. Nors išvedimo greitis sumažės nuo ~30 žetonų/sek iki ~3 žetonų/sek, šis atsarginis variantas garantuoja, kad smėlio dėžė liks funkcionali.
Santrauka
Panaudojant WebGPU kartu su StackBlitz WebContainers, žiniatinklio programos iš paprastų pateikimo puslapių gali virsti pilnavertėmis, savarankiškomis kūrimo aplinkomis. Pasirinktinių, suderintų kūrėjų procesų platinimas dabar yra toks pat paprastas, kaip ir nedidelio LoRA adapterio failo pateikimas per CDN tinklą, leidžiantis komandoms bendradarbiauti smėlio dėžėse, kurios veikia kliento įrenginiuose be jokių infrastruktūros išlaidų.