返回日志
5 分钟阅读

本地优先开发工作流:利用 WebGPU 和 WebContainers 在浏览器内进行 LLM 微调

深入探讨直接在浏览器沙盒中运行和适配 LLM。我们结合 StackBlitz WebContainers 与 WebGPU 和 LoRA,构建一个零成本、私密的 AI 游乐场。

WebGPUWebContainersLocal-First AIIn-Browser LLMsWASMWeb Engineering
本地优先开发工作流:利用 WebGPU 和 WebContainers 在浏览器内进行 LLM 微调

在开发过程中运行大型语言模型 (LLM) 通常需要基于云端的 API 或像 Ollama 这样的本地重型守护进程。虽然云端 API 提供了极高的速度,但它们也带来了周期性的订阅费用、网络依赖以及潜在的数据隐私问题。

随着最近 Web 平台的进步,我们现在可以完全绕过这些权衡。通过将 StackBlitz WebContainers(直接在浏览器标签页中运行完整的 Node.js 运行环境)与用于本地硬件加速的 WebGPU 相结合,我们可以构建一个完全沙盒化、私密且免费的 AI 开发者游乐场。

在本指南中,我们将探讨浏览器内 AI IDE 的架构。我们将实现一个用于管理 WebGPU 推理的 Web Worker,探讨如何直接在客户端 Javascript 中应用低秩适应 (LoRA) 适配器,并将 AI 引擎桥接到 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. 主线程 (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.
  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. 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

为了让浏览器中运行的模型对特定的代码库有用,我们可以加载低秩适应 (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 shader。在正向传播 (forward pass) 期间,GPU 将与主注意力机制块 (attention blocks) 并行应用 LoRA 增量矩阵计算。这允许开发人员在自己的机器上微调模型,上传一个 10MB 的适配器,并立即使团队的浏览器获得自定义编码模式的生成能力。


Bridging WebContainers and WebGPU

现在我们有了一个后台 worker 来运行我们的模型,我们可以将它挂载到 WebContainer 工作区。主线程将协调从 WebContainer 虚拟文件系统中读取文件、将内容发送到 WebGPU worker,并将 AI 修改后的输出写回。

以下是管理此通信流的协调器类:

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 分配施加了硬性限制(通常限制为 2GB 至 4GB,具体取决于操作系统和硬件配置层级)。

  • 缓解措施:模型必须经过严格量化。使用 q4f16_1(4 位)量化方案时,像 Qwen2.5-Coder-Instruct 这样拥有 15 亿参数的模型需要约 1.3GB 的 VRAM,且像 DeepSeek-R1-Distill-Qwen-1.5B 这样的蒸馏推理模型可运行在约 1.4GB 的 VRAM 以内,这非常符合浏览器的安全边界。

2. 标签页内存崩溃 (OOM)

虽然 WebGPU 内存位于 GPU 上,但 WebAssembly (WASM) 和 Javascript 运行环境仍然管理着协调层。如果模型在将权重运送到 GPU 之前尝试直接加载大型权重到 CPU 内存中,Chrome 可能会触发“页面内存不足”(OOM) 崩溃。

  • 缓解措施:使用流式数组缓冲区 (streaming array buffers)。将下载的权重直接流式传输到 IndexedDB,然后将其逐块直接解析到 GPU 纹理中,避免了在 Javascript 堆内存中存储整个模型文件。

3. 降级方案 (Fallbacks)

如果用户的浏览器不支持 WebGPU(例如过时的移动浏览器或禁用相关 flag 的配置),协调器应该优雅地降级到 WASM CPU 执行。虽然推理速度将从约 30 token/秒降至约 3 token/秒,但这种降级策略保证了沙盒仍然处于可用状态。


总结

通过将 WebGPU 与 StackBlitz WebContainers 结合使用,Web 应用可以从简单的静态展示页面过渡为完整的、自包含的开发环境。分发自定义、微调后的开发工作流现在就像通过 CDN 提供一个极小的 LoRA 适配器文件一样简单,从而允许团队在完全在客户端机器上运行的沙盒中进行协作,且零基础设施成本。

Share this article