Volver al Diario
8 min de lectura

Flujos de trabajo de desarrollo local-first: Aprovechando WebGPU y WebContainers para el ajuste fino de LLMs en el navegador

Una inmersión profunda en la ejecución y adaptación de LLMs directamente en el sandbox del navegador. Combinamos StackBlitz WebContainers con WebGPU y LoRA para construir un entorno de pruebas de IA gratuito y privado.

WebGPUWebContainersLocal-First AIIn-Browser LLMsWASMWeb Engineering
Flujos de trabajo de desarrollo local-first: Aprovechando WebGPU y WebContainers para el ajuste fino de LLMs en el navegador

Ejecutar modelos de lenguaje grandes (LLMs) durante el desarrollo normalmente requiere APIs basadas en la nube o pesados demonios locales como Ollama. Aunque las APIs de la nube ofrecen una gran velocidad, introducen tarifas de suscripción recurrentes, dependencia de internet y posibles problemas de privacidad de los datos.

Con los recientes avances de la plataforma web, ahora podemos evitar por completo estas compensaciones. Al combinar StackBlitz WebContainers —que ejecutan un entorno operativo completo de Node.js directamente dentro de una pestaña del navegador— con WebGPU para la aceleración de hardware local, podemos construir un entorno de desarrollo de IA completamente aislado, privado y gratuito.

En esta guía, analizaremos la arquitectura de un IDE de IA en el navegador. Implementaremos un Web Worker para gestionar la inferencia de WebGPU, exploraremos cómo aplicar adaptadores de Low-Rank Adaptation (LoRA) directamente en Javascript del lado del cliente y conectaremos el motor de IA al sistema de archivos virtual de 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]           |
+-------------------------------------------------------------------------+
  1. Hilo Principal: 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.
  2. 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).
  3. Caché de 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

Para hacer que el modelo residente en el navegador sea útil para bases de código específicas, podemos cargar un adaptador de Low-Rank Adaptation (LoRA). LoRA altera el comportamiento de salida al actualizar una fracción diminuta de los pesos totales del modelo (a menudo menos del 1%), lo que hace que sea rápido de descargar y aplicar en tiempo de ejecución.

En los entornos modernos de WebLLM, los adaptadores se empaquetan como archivos .bin separados y se superponen sobre los pesos del modelo base. Así es como configuramos WebLLM para cargar un modelo base (Qwen2.5-Coder-1.5B-Instruct-q4f16_1-MLC) y aplicar un adaptador personalizado específico de la base de código:

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
    }
  ]
};

Cuando WebLLM se inicializa, extrae los pesos base y compila los shaders de WebGPU. Durante el pase hacia adelante, la GPU realiza los cálculos de la matriz delta de LoRA en paralelo con los bloques de atención principales. Esto permite a los desarrolladores ajustar un modelo en su máquina, cargar un adaptador de 10 MB y distribuir instantáneamente patrones de codificación personalizados a los navegadores del equipo.


Bridging WebContainers and WebGPU

Ahora que tenemos un worker en segundo plano ejecutando nuestro modelo, podemos conectarlo a nuestro espacio de trabajo de WebContainer. El hilo principal coordinará la lectura de un archivo del sistema de archivos virtual de WebContainer, el envío del contenido al worker de WebGPU y la escritura de la salida modificada de la IA de vuelta.

Aquí está la clase coordinadora que gestiona este flujo de comunicación:

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

Ejecutar LLMs del lado del cliente es altamente eficiente una vez descargado, pero opera dentro de las estrictas limitaciones de sandbox asignadas por el navegador.

1. Límites de asignación de VRAM

WebGPU asigna bloques de memoria directamente en la tarjeta gráfica de la máquina host. Sin embargo, los navegadores imponen límites estrictos a las asignaciones individuales de la GPU (a menudo restringidas a entre 2 GB y 4 GB, dependiendo del sistema operativo y del nivel de hardware).

  • Mitigación: Los modelos deben estar estrictamente cuantizados. Un modelo de 1.500 millones de parámetros como Qwen2.5-Coder-Instruct requiere aproximadamente 1,3 GB de VRAM con un esquema q4f16_1, gemma-4-e2b-it-q4f16_1-MLC de Google requiere aproximadamente 1,6 GB (gracias a su arquitectura Effective 2B), y los modelos de razonamiento destilados como DeepSeek-R1-Distill-Qwen-1.5B se ejecutan dentro de ~1,4 GB de VRAM, lo que encaja perfectamente dentro de los márgenes de seguridad del navegador.

2. Caída de memoria de pestaña (OOM)

Mientras que la memoria WebGPU vive en la GPU, Web Assembly (WASM) y el entorno de ejecución de Javascript continúan gestionando las capas de orquestación. Si un modelo intenta cargar pesos grandes directamente en la memoria de la CPU antes de enviarlos a la GPU, Chrome podría provocar una caída por falta de memoria ("Page Out of Memory" u OOM).

  • Mitigación: Utilizar buffers de matriz de transmisión. La transmisión de los pesos descargados directamente a IndexedDB y su posterior análisis bloque por bloque directamente en las texturas de la GPU evita almacenar el archivo de modelo completo en la memoria heap de Javascript.

3. Fallbacks

Si el navegador del usuario no es compatible con WebGPU (como navegadores móviles obsoletos o configuraciones con flags deshabilitados), el orquestador debería recurrir de manera fluida a la ejecución en CPU mediante WASM. Aunque las velocidades de inferencia disminuirán de ~30 tokens/seg a ~3 tokens/seg, este fallback garantiza que el sandbox siga siendo funcional.


Resumen

Aprovechando WebGPU junto con StackBlitz WebContainers, las aplicaciones web pueden pasar de simples páginas de presentación a entornos de desarrollo completos y autónomos. Distribuir flujos de trabajo de desarrollo personalizados y ajustados ahora es tan simple como servir un pequeño archivo adaptador de LoRA a través de una red CDN, lo que permite a los equipos colaborar en entornos aislados que se ejecutan completamente en máquinas cliente con cero costos de infraestructura.

Share this article