Flussi di lavoro per sviluppatori local-first: Sfruttare WebGPU e WebContainers per il fine-tuning dei LLM nel browser
Un approfondimento sull'esecuzione e l'adattamento dei LLM direttamente nella sandbox del browser. Combiniamo StackBlitz WebContainers con WebGPU e LoRA per creare un parco giochi AI gratuito e privato.

L'esecuzione di modelli linguistici di grandi dimensioni (LLM) durante lo sviluppo richiede in genere API basate su cloud o pesanti daemon locali come Ollama. Sebbene le API cloud offrano un'elevata velocità, introducono costi di abbonamento ricorrenti, dipendenza da Internet e potenziali problemi di privacy dei dati.
Con i recenti progressi della piattaforma web, ora possiamo aggirare completamente questi compromessi. Combinando StackBlitz WebContainers — che eseguono un intero ambiente operativo Node.js direttamente all'interno di una scheda del browser — con WebGPU per l'accelerazione hardware locale, possiamo creare un ambiente di sviluppo IA completamente isolato, privato e gratuito.
In questa guida esamineremo l'architettura di un IDE IA nel browser. Implementeremo un Web Worker per gestire l'inferenza WebGPU, esploreremo come applicare adattatori Low-Rank Adaptation (LoRA) direttamente in Javascript lato client e collegheremo il motore IA al file system virtuale di 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] |
+-------------------------------------------------------------------------+- Thread Principale: 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).
- Cache 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
Per rendere il modello residente nel browser utile per basi di codice specifiche, possiamo caricare un adattatore Low-Rank Adaptation (LoRA). LoRA altera il comportamento dell'output aggiornando una frazione minima dei pesi totali del modello (spesso meno dell'1%), il che rende il download e l'applicazione estremamente rapidi in fase di esecuzione.
Nei moderni ambienti WebLLM, gli adattatori sono confezionati come file .bin separati e stratificati sopra i pesi del modello di base. Ecco come configuriamo WebLLM per caricare un modello di base (Qwen2.5-Coder-1.5B-Instruct-q4f16_1-MLC) e applicare un adattatore personalizzato specifico per la base di codice:
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
}
]
};Quando WebLLM si inizializza, recupera i pesi di base e compila gli shader WebGPU. Durante il forward pass, la GPU applica i calcoli della matrice delta LoRA in parallelo con i blocchi di attenzione principali. Ciò consente agli sviluppatori di ottimizzare un modello sulla propria macchina, caricare un adattatore da 10 MB e distribuire istantaneamente modelli di codifica personalizzati ai browser del team.
Bridging WebContainers and WebGPU
Ora che abbiamo un worker in background che esegue il nostro modello, possiamo collegarlo al nostro spazio di lavoro WebContainer. Il thread principale coordinerà la lettura di un file dal file system virtuale di WebContainer, l'invio del contenuto al worker WebGPU e la scrittura dell'output modificato dall'IA in risposta.
Ecco la classe coordinatrice che gestisce questo flusso di comunicazione:
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
L'esecuzione di LLM lato client è estremamente efficiente una volta scaricato il modello, ma opera all'interno delle rigide limitazioni della sandbox assegnata dal browser.
1. Limiti di allocazione della VRAM
WebGPU alloca blocchi di memoria direttamente sulla scheda grafica della macchina host. Tuttavia, i browser impongono limiti rigorosi alle singole allocazioni GPU (spesso limitate a 2 GB - 4 GB a seconda del sistema operativo e della fascia hardware).
- Mitigazione: I modelli devono essere rigorosamente quantizzati. Un modello da 1,5 miliardi di parametri come Qwen2.5-Coder-Instruct richiede circa 1,3 GB di VRAM con uno schema
q4f16_1, gemma-4-e2b-it-q4f16_1-MLC di Google richiede circa 1,6 GB (grazie alla sua architettura Effective 2B), e i modelli di ragionamento distillati come DeepSeek-R1-Distill-Qwen-1.5B funzionano entro ~1,4 GB di VRAM, rientrando ampiamente nei margini di sicurezza del browser.
2. Tab Memory Crashing (OOM)
Mentre la memoria WebGPU risiede sulla GPU, WebAssembly (WASM) e il runtime JavaScript gestiscono ancora i livelli di orchestrazione. Se un modello tenta di caricare pesi elevati direttamente nella memoria della CPU prima di inviarli alla GPU, Chrome potrebbe causare un crash per memoria esaurita ("Page Out of Memory" o OOM).
- Mitigazione: Utilizzare streaming array buffers. Lo streaming dei pesi scaricati direttamente in IndexedDB, e la successiva analisi blocco per blocco direttamente nelle texture della GPU, evita di memorizzare l'intero file del modello nella memoria heap di JavaScript.
3. Fallback
Se il browser dell'utente non supporta WebGPU (come i browser mobili obsoleti o le configurazioni con flag disabilitati), l'ororchestratore dovrebbe ripiegare in modo fluido sulla esecuzione su CPU tramite WASM. Sebbene la velocità di inferenza scenda da ~30 token/sec a ~3 token/sec, questo fallback garantisce che la sandbox rimanga funzionale.
Riepilogo
Sfruttando WebGPU insieme a StackBlitz WebContainers, le applicazioni web possono passare da semplici pagine statiche a interi ambienti di sviluppo autosufficienti. La distribuzione di flussi di lavoro di sviluppo personalizzati e ottimizzati è ora semplice quanto distribuire un piccolo file adattatore LoRA tramite una rete CDN, consentendo ai team di collaborare in sandbox eseguite interamente sulle macchine client con zero costi di infrastruttura.