WebGPU 浏览器推理:把 LLM 跑进 Chrome 不是梦
实测 WebLLM + WebGPU 在浏览器端跑 LLM,性能基准、踩坑记录和完整示例代码。
# WebGPU 浏览器推理:把 LLM 跑进 Chrome 不是梦
去年 WebGPU 在 Chrome 上正式启用,我当时就盯着这个特性看了很久。想着:要是能把 LLM 跑在浏览器里,就不用后端的 GPU 服务器了,成本能省一大半。
一年后的今天,我实测了一遍。结论是:能跑,但有条件限制。写这篇就是为了让大家少走弯路。
技术选型
浏览器端跑 LLM,目前主流方案有两个:
2. **Transformers.js**:HuggingFace 出品,支持 WebGPU 和 WebAssembly 两种后端
我选的是 WebLLM,原因是它对流式输出的支持更好——浏览器里做 ChatUI,不流式输出体验极差。
环境准备
浏览器要求
Node.js 项目初始化
npm create vite@latest webgpu-llm -- --template react-ts
cd webgpu-llm
npm install @mlc-ai/web-llm
第一个 Demo:让模型说话
最简单的用法:
import { CreateMLCEngine } from '@mlc-ai/web-llm';
const engine = await CreateMLCEngine({
initProgressCallback: (report) => {
console.log(report.text); // 打印加载进度
},
});
const chunks = engine.chat.completions.create({
messages: [{ role: 'user', content: '你好,介绍一下你自己' }],
stream: true,
});
for await (const chunk of chunks) {
process.stdout.write(chunk.choices[0]?.delta?.content || '');
}
就这么几行,模型就跑起来了。但别高兴太早——这是理想情况。
实际踩坑
坑一:模型加载慢到怀疑人生
我第一次跑的时候,等了将近 3 分钟才看到第一个 token 输出。查了原因,发现是模型文件从 CDN 拉取的。
解决方案:把模型文件下载到本地,或者用 pwa-kit 缓存到 IndexedDB。
// 指定本地模型路径
const engine = await CreateMLCEngine('dist/models/Llama-3.2-1B-Instruct-q4f16_1/', {
initProgressCallback: (report) => {
setProgress(report.text);
},
});
我选的模型是 Llama-3.2-1B,这是专门为移动端/浏览器优化的轻量模型。7B 的模型在浏览器里跑,显存直接爆掉——我的测试机是 MacBook Pro 16GB,7B Q4 模型加载到一半就 OOM 了。
**经验:浏览器端推理,模型不要超过 3B。超过 3B 的请用后端方案。**
坑二:流式输出不稳定
WebGPU 的 compute shader 调用有延迟,导致流式输出的 token 间隔不均匀。有时候 200ms 出一个 token,有时候 2s 出一个。
这个问题在低端设备上更明显。我在一台 2020 年的 MacBook Air 上测试,卡顿感很强。但在 M2 Max 上几乎感觉不到延迟。
**经验:低端设备上不要用流式输出,等完整响应再渲染,用户体验反而更好。**
坑三:内存泄漏
我做了个简单的 chat 界面,连续对话 20 轮之后,浏览器标签页直接崩了。查了 Chrome DevTools 的 Memory 面板,发现是 WebGPU buffer 没有正确释放。
目前的 workaround 是每 10 轮对话重建一次 engine:
let messageHistory: Array<{role: string; content: string}> = [];
async function sendMessage(content: string) {
messageHistory.push({ role: 'user', content });
// 每10轮重建engine,防止内存泄漏
if (messageHistory.length % 10 === 0) {
await engine.reload();
}
const chunks = engine.chat.completions.create({
messages: messageHistory,
stream: true,
});
// ...处理输出
}
这个问题 upstream 已经在修了,但还没正式 release。关注 [WebLLM GitHub Issues](https://github.com/mlc-ai/web-llm/issues)。
坑四:并发请求会死
浏览器限制了 WebGPU 的资源,同时发起多个推理请求会导致互抢显存,直接卡死。
我的做法是加一个请求队列:
const requestQueue: Array<() => void> = [];
let processing = false;
async function enqueueRequest(fn: () => void) {
return new Promise(resolve => {
requestQueue.push(() => {
fn().finally(resolve);
processing = false;
processNext();
});
processing = true;
processNext();
});
}
function processNext() {
if (!processing || requestQueue.length === 0) return;
const fn = requestQueue.shift()!;
fn();
}
性能基准
我在不同设备上测了 Llama-3.2-1B 的推理速度:
| 设备 | 上下文长度 | tokens/s | 内存占用 |
|------|-----------|---------|---------|
| MacBook Pro M2 Max 16GB | 2048 | ~18 | ~800MB |
| MacBook Pro M2 Max 16GB | 8192 | ~12 | ~1.2GB |
| MacBook Air M1 8GB | 2048 | ~10 | ~700MB |
| ThinkPad X1 Carbon (Intel Iris) | 2048 | ~3 | ~600MB |
| iPhone 15 Pro | 1024 | ~8 | ~500MB |
**结论:Apple Silicon 是浏览器端推理的最佳选择。Intel 核显方案不推荐,速度慢且发热严重。**
适用场景
浏览器端推理不是万能的,适合这些场景:
2. **离线场景**:无网络环境下的 AI 功能
3. **成本敏感**:不想付 API 调用费,愿意牺牲一点性能
4. **原型验证**:快速 demo,不想搭后端
不适合的场景:
2. **长上下文**:超过 4K tokens 的场景,浏览器内存扛不住
3. **低配设备**:没有 GPU 的老旧电脑,体验极差
完整示例代码
import { CreateMLCEngine } from '@mlc-ai/web-llm';
import { useState, useRef } from 'react';
export default function App() {
const [engine, setEngine] = useState<any>(null);
const [messages, setMessages] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const init = async () => {
const e = await CreateMLCEngine('dist/models/Llama-3.2-1B-Instruct-q4f16_1/', {
initProgressCallback: (report: any) => {
console.log(report.text);
},
});
setEngine(e);
setLoading(false);
};
const send = async (content: string) => {
if (!engine) return;
const newMessages = [...messages, { role: 'user', content }];
setMessages(newMessages);
let assistantContent = '';
const chunks = engine.chat.completions.create({
messages: newMessages,
stream: true,
});
for await (const chunk of chunks) {
const delta = chunk.choices[0]?.delta?.content || '';
assistantContent += delta;
setMessages([...newMessages, { role: 'assistant', content: assistantContent }]);
}
};
return (
<div>
{loading ? <p>加载模型中...</p> : (
<>
{messages.map((m, i) => (
<div key={i} className={m.role === 'user' ? 'user' : 'assistant'}>
{m.content}
</div>
))}
<input onKeyPress={e => e.key === 'Enter' && send(e.currentTarget.value)} />
</>
)}
</div>
);
}
总结
浏览器端推理这条路,现在走的人还不多,坑也多。但趋势是不可逆的——随着 WebGPU 普及和模型越来越小,未来会有越来越多的场景迁移到浏览器端。
如果你正在考虑做这个方向,我的建议是:
2. **重点优化加载体验**,模型加载慢是最大的 UX 痛点
3. **做好降级方案**,WebGPU 不可用时 fallback 到后端 API
4. **关注上游动态**,WebLLM 更新很快,很多 bug 已经修了
技术还在成熟中,现在入局不算早也不算晚。