🚀 浏览器里跑 1.5B 参数大模型?我用 WebGPU + DeepSeek 做到了
想象一下:用户打开一个网页,不用登录、不用等服务器响应,直接在浏览器里和 AI 对话。数据不出本地,隐私完全可控。这就是端侧推理的魅力。
本文带你从零拆解一个基于 WebGPU 的浏览器端大模型推理项目,读完你将掌握:WebGPU 检测与适配、Transformers.js 模型加载、Web Worker 架构设计、单例模式的 JS 实现、4-bit 量化原理。
适合对前端 AI 推理感兴趣的同学,不需要机器学习基础,有 JS 基础就能看懂。
前言:为什么要在浏览器里跑大模型?
先说结论------浏览器端 AI 推理有三个核心优势:
- 零服务器成本:模型跑在用户本地,不需要 GPU 云服务器
- 隐私安全:数据不离开浏览器,天然符合数据合规要求
- 离线可用:模型缓存后,断网也能用
但挑战也很明显:浏览器内存有限、没有 CUDA、GPU 访问受限。
怎么解决?技术栈核心就三样东西:
| 技术 | 解决什么问题 |
|---|---|
| WebGPU | 浏览器里的 GPU 计算接口,让 JS 能调用 GPU 跑矩阵运算 |
| Transformers.js | Hugging Face 官方 JS 库,负责下载模型、分词、推理调度 |
| ONNX Runtime Web | ONNX 模型的浏览器运行时,WebGPU 计算的实际执行者 |
我用这套技术栈搭了一个 DeepSeek-R1-Distill-Qwen-1.5B 的端侧推理 Demo。这篇文章不讲虚的,直接从遇到的问题出发,逐个拆解里面的知识点。
💡 快速体验 :
git clone项目后cd deepseek-r1-gpu && npm install && npm run dev,Chrome 113+ 打开即可。
问题一:浏览器支持 GPU 计算吗?
WebGPU 是一个实验性 API,不是所有浏览器都支持。所以第一步必须检测兼容性。
粗筛:主线程快速判断
在 App.tsx 中:
typescript
const IS_WEBGPU_AVAILABLE = !!(navigator as any).gpu;
你可能会问:为什么要用 !! ?直接判断 navigator.gpu 行不行?
不行。因为 navigator.gpu 如果存在,返回的是一个 GPU 对象,不是 boolean。在 React 的条件渲染里,我们需要一个干净的 true/false:
typescript
// 如果不加 !!
if (navigator.gpu) { ... } // 可以工作,但类型不干净
// 加了 !!
!!undefined // → false
!!{} // → true
至于 as any------WebGPU 比较新,TypeScript 默认的 Navigator 类型里没有 gpu 属性。临时方案是 as any 跳过检查,正规方案是装类型声明:
bash
npm install -D @webgpu/types
json
// tsconfig.app.json
{
"compilerOptions": {
"types": ["vite/client", "@webgpu/types"]
}
}
细筛:Worker 里真正验证 GPU
主线程的 !!navigator.gpu 只是粗筛------浏览器可能声明支持 WebGPU,但实际没有可用的 GPU。真正的检测在 Worker 里:
javascript
// worker.js
async function check() {
try {
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) {
throw new Error("WebGPU is not supported (no adapter found)");
}
} catch (e) {
self.postMessage({ status: "error", data: e.toString() });
}
}
requestAdapter() 尝试获取 GPU 适配器(硬件的抽象接口)。返回 null 说明没有可用 GPU。这比单纯的 !!navigator.gpu 可靠得多。
🤔 思考一下 :为什么检测逻辑放在 Worker 里而不是主线程?------因为
requestAdapter()是异步的,放 Worker 里不会阻塞 UI 渲染。
问题二:模型怎么从 Hugging Face 下载到浏览器?
模型选择的是 DeepSeek-R1-Distill-Qwen-1.5B------DeepSeek R1 推理模型的蒸馏版本,基于 Qwen 架构,1.5B(15亿)参数。
但问题是:原版模型是 PyTorch 格式,浏览器跑不了。
解决方案是用 ONNX 社区提供的量化版本:
javascript
static model_id = "onnx-community/DeepSeek-R1-Distill-Qwen-1.5B-ONNX";
ONNX(Open Neural Network Exchange)是机器学习模型的通用交换格式,类似于图片领域的 PNG。模型转成 ONNX 格式后,ONNX Runtime Web 就能在浏览器里直接执行。
那下载和管理谁来做?Transformers.js------Hugging Face 官方的 JS 版 Transformers 库:
json
// package.json
"@huggingface/transformers": "^3.7.1"
它能自动从 Hugging Face Hub 下载模型和分词器,并且会缓存到浏览器(通过 Cache API / IndexedDB)。第一次加载可能要下载几百 MB,但之后再打开页面,直接从本地读取,秒开。
问题三:模型加载到一半用户又点了按钮怎么办?
这是个很现实的问题。模型下载可能要几十秒,如果用户手快点了两次"加载",就会触发两次下载------浪费带宽、内存溢出。
解决方案:单例模式------确保模型和分词器只加载一次。
从最简单的例子理解单例
项目里有一个教学 Demo(singleton/index.html):
javascript
class Popup {
static ins;
static getInstance() {
if (!Popup.ins) {
Popup.ins = new Popup();
}
return Popup.ins;
}
}
const a = Popup.getInstance();
const b = Popup.getInstance();
console.log(a === b); // true ------ 同一个实例
不管你调用多少次 getInstance(),永远返回同一个对象。这就是单例模式的核心思想。
生产级单例:??= 运算符
worker.js 里用了一种更优雅的写法:
javascript
class TextGenerationPipeline {
static model_id = "onnx-community/DeepSeek-R1-Distill-Qwen-1.5B-ONNX";
static async getInstance(progress_callback = null) {
this.tokenizer ??= AutoTokenizer.from_pretrained(this.model_id, {
progress_callback,
});
return Promise.all([this.tokenizer]);
}
}
关键在这行:
javascript
this.tokenizer ??= AutoTokenizer.from_pretrained(...)
??= 是 空值合并赋值运算符(ES2021),等价于:
javascript
if (this.tokenizer === null || this.tokenizer === undefined) {
this.tokenizer = AutoTokenizer.from_pretrained(...);
}
执行流程:
- 第一次调用 :
this.tokenizer是undefined→ 赋值 → 开始下载分词器 - 第二次调用 :
this.tokenizer已经是一个 Promise → 跳过赋值 → 直接返回 - 因为
??=只在值为null/undefined时才赋值,下载只会触发一次
而且注意:赋值的是一个 Promise。即使下载还没完成,多次调用也会共享同一个 Promise,不会重复下载。这个设计非常精妙------用一行代码同时解决了"懒加载"和"防重复"两个问题。
💡 为什么用
static而不是实例属性? 因为TextGenerationPipeline从来不会被new。所有方法和属性都是static的,本质上就是一个带命名空间的单例容器。
问题四:模型下载的时候页面卡死了怎么办?
模型下载和推理是重计算任务------下载几百 MB 文件、解析 tokenizer、初始化 ONNX 运行时。如果放在主线程,用户会看到页面直接冻住。
解决方案:Web Worker------在后台线程运行,主线程只管 UI。
创建 Worker(Vite 写法)
typescript
// App.tsx
worker.current = new Worker(new URL("./worker.js", import.meta.url), {
type: "module",
});
new URL("./worker.js", import.meta.url) 是 Vite 的 Worker 打包语法,Vite 会自动把它打包成独立 chunk。type: "module" 让 Worker 内部可以用 ES Module 语法。
主线程 ↔ Worker 通信
通信方式是 postMessage,双工的:
typescript
// 主线程 → Worker
worker.current.postMessage({ type: "check" });
worker.current.postMessage({ type: "load" });
// Worker → 主线程
self.postMessage({ status: "loading", data: "Loading model..." });
Worker 里的消息处理:
javascript
// worker.js
self.addEventListener("message", async (e) => {
const { type, data } = e.data;
switch (type) {
case "check": check(); break;
case "load": load(); break;
case "generate": break; // TODO: 文本生成
case "interrupt": break; // TODO: 中断生成
case "reset": break; // TODO: 重置状态
}
});
⚠️ 踩坑提醒 :Worker 运行在独立线程,没有
document和window对象 。所有 UI 更新必须通过postMessage传回主线程。如果你在 Worker 里写了document.getElementById(),直接报错。
问题五:1.5B 参数怎么塞进浏览器内存?
这是最核心的问题。1.5B 参数的模型,如果用 float32 存储:
1,500,000,000 参数 × 4 字节/参数 ≈ 6 GB
浏览器显然没有 6 GB GPU 内存给一个网页用。
解决方案:量化------降低权重精度,用更少的 bit 存储每个参数。
javascript
// worker.js(模型加载配置)
this.model ??= AutoModelForCausalLM.from_pretrained(this.model_id, {
dtype: "q4f16", // 4-bit 量化,float16 激活值
device: "webgpu", // 使用 WebGPU 后端
progress_callback,
});
q4f16 的含义:
q4--- 模型权重用 4-bit 整数量化f16--- 激活值用 float16(半精度浮点)
量化后的内存对比:
| 精度 | 每参数位数 | 1.5B 模型大小 | 能否在浏览器跑 |
|---|---|---|---|
| float32 | 32 bit | ~6 GB | ❌ 内存爆了 |
| float16 | 16 bit | ~3 GB | ❌ 勉强能装,推理会卡 |
| q4f16 | 4 bit | ~0.75 GB | ✅ 完全可行 |
4-bit 量化把内存需求压到了不到 1 GB,这就是浏览器能跑动 1.5B 模型的关键。
你可能会问:量化不会降低模型精度吗? 会,但 4-bit 量化后的模型在大多数任务上仍然能保持不错的效果。这就是工程上的 trade-off------用一点精度换取可行性。
完整架构:把这些知识串起来
把上面五个问题的解决方案组合在一起,整体架构是这样的:
yaml
┌──────────────────────────────────────────────────┐
│ 浏览器 │
│ │
│ ┌─── 主线程 (React UI) ─────────────────────┐ │
│ │ ① !!navigator.gpu 粗筛 │ │
│ │ ② useState 管理加载状态 │ │
│ │ ③ postMessage ←→ Worker 通信 │ │
│ └────────────────────┬───────────────────────┘ │
│ ↕ postMessage │
│ ┌─── Web Worker ─────┴──────────────────────┐ │
│ │ │ │
│ │ TextGenerationPipeline(单例,??= 懒加载) │ │
│ │ ├── AutoTokenizer │ │
│ │ │ └── HF Hub 下载 → 浏览器缓存 │ │
│ │ └── AutoModelForCausalLM │ │
│ │ ├── dtype: q4f16(4-bit 量化,~0.75GB)│ │
│ │ ├── device: webgpu │ │
│ │ └── ONNX Runtime Web │ │
│ │ └── WebGPU 计算着色器 │ │
│ └────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─── GPU 硬件 ───┐ │
│ │ 矩阵运算 │ │
│ │ 注意力计算 │ │
│ └────────────────┘ │
└──────────────────────────────────────────────────┘
模型加载流程:
scss
用户点击 "Load model"
│
▼
主线程 postMessage({ type: "load" })
│
▼
Worker 调用 TextGenerationPipeline.getInstance()
│
▼
??= 检查:是否已加载?──是──→ 返回已有 Promise
│ 否
▼
AutoTokenizer.from_pretrained() → 下载分词器
│
▼
AutoModelForCausalLM.from_pretrained() → 下载 ONNX 模型
│
▼
浏览器缓存(Cache API / IndexedDB)
│
▼
progress_callback → postMessage → UI 显示进度
技术名词速查
遇到不懂的术语?看这里:
| 名词 | 一句话解释 |
|---|---|
| WebGPU | 浏览器的下一代 GPU API,支持通用 GPU 计算(不仅是图形渲染) |
| ONNX | 机器学习模型的通用交换格式,类似图片领域的 PNG |
| Transformers.js | Hugging Face 官方 JS 库,负责模型下载、分词、推理调度 |
| ONNX Runtime Web | ONNX 模型的浏览器运行时,WebGPU 计算的实际执行者 |
| 量化 (Quantization) | 降低模型权重精度以减少内存,q4 = 4-bit 量化 |
| Web Worker | 浏览器的多线程方案,后台线程跑重计算,不卡 UI |
| 单例模式 | 确保一个类只有一个实例,防止重复加载资源 |
??= |
空值合并赋值运算符,仅在值为 null/undefined 时赋值 |
踩坑记录
坑 1:TypeScript 不认识 navigator.gpu
typescript
// ❌ 报错:Property 'gpu' does not exist on type 'Navigator'
navigator.gpu
// ✅ 临时方案:类型断言
(navigator as any).gpu
// ✅ 正规方案:安装类型声明
npm install -D @webgpu/types
坑 2:Worker 里操作 DOM 直接报错
Web Worker 没有 document 和 window。所有 UI 更新必须通过 postMessage 传回主线程。这是新手最容易踩的坑。
坑 3:首次加载慢是正常的
模型文件几百 MB,第一次加载确实要等。但浏览器会自动缓存,后续加载秒开。如果你在做 Demo 演示,记得提前加载好。
总结
这个项目代码量不大,但每一个技术选择背后都有明确的问题驱动:
| 遇到的问题 | 解决方案 | 核心技术 |
|---|---|---|
| 浏览器能不能用 GPU? | WebGPU 检测 | navigator.gpu + requestAdapter() |
| 模型怎么下载到浏览器? | Transformers.js | ONNX 格式 + HF Hub + 浏览器缓存 |
| 怎么防止重复加载? | 单例模式 | ??= 懒加载 + Promise 复用 |
| 下载时页面卡死? | Web Worker | 后台线程 + postMessage 通信 |
| 内存不够装 1.5B 参数? | 4-bit 量化 | q4f16,6GB → 0.75GB |
端侧 AI 推理是一个很有前景的方向------无需服务器、保护隐私、离线可用。WebGPU 目前还在快速迭代中(Chrome 113+ 已支持,Firefox/Safari 跟进中),相信未来在浏览器里跑更大、更强的模型将不再是梦想。
🔗 相关链接:
- DeepSeek-R1-Distill-Qwen-1.5B-ONNX --- 本文使用的量化模型
- Transformers.js 文档 --- Hugging Face 官方 JS 库
- WebGPU 规范 --- W3C 标准文档
- ONNX Runtime Web --- 浏览器端 ONNX 推理
💬 你在浏览器里跑过大模型吗?遇到过什么坑?欢迎评论区交流!