用WebAssembly + WebGP构建浏览器内AI推理引擎:让隐私数据“不离端“

📅 2026/7/27 1:22:47 👁️ 阅读次数 📝 编程学习
用WebAssembly + WebGP构建浏览器内AI推理引擎:让隐私数据“不离端“

用WebAssembly + WebGP构建浏览器内AI推理引擎:让隐私数据"不离端"

为什么需要"浏览器内AI推理"?

现有AI产品(如ChatGPT、Claude)都是"云端推理"——用户数据需要上传到服务器,推理完成后再返回结果。这带来三个问题:

  1. 隐私风险:敏感数据(如医疗记录、商业机密)不能上传
  2. 网络延迟:即使用户在本地,也要等待"上传+下载"
  3. 成本:云端GPU推理成本高($0.01-0.1/1000 tokens)

浏览器内推理的优势:

  • 零延迟:数据不离开用户设备
  • 零成本:不需要云端GPU
  • 隐私友好:符合GDPR"数据本地化"要求

技术栈:ONNX Runtime Web + WebAssembly + WebGPU

ONNX Runtime Web:微软开源的"跨平台推理引擎",可以把PyTorch/TensorFlow模型转换成"浏览器可运行"的格式。

WebAssembly (WASM):让C++/Rust代码在浏览器里运行(接近原生速度)。

WebGPU:浏览器里的GPU加速API(类似CUDA,但是标准Web API)。

实战:把GPT-2模型转换成"浏览器可运行"格式

第一步:训练或下载预训练模型

# 用PyTorch训练一个简单的文本生成模型(或下载GPT-2小版本) import torch from transformers import GPT2LMHeadModel, GPT2Tokenizer # 下载GPT-2(小版本,117M参数) model = GPT2LMHeadModel.from_pretrained('gpt2') tokenizer = GPT2Tokenizer.from_pretrained('gpt2') # 保存为PyTorch格式 torch.save(model.state_dict(), 'gpt2_pytorch.pth')

第二步:转换成ONNX格式

ONNX是"开放神经网络交换格式"——可以让模型在各种推理引擎上运行。

# convert_to_onnx.py import torch from transformers import GPT2LMHeadModel import onnx from onnxruntime.quantization import quantize_dynamic, QuantType # 加载PyTorch模型 model = GPT2LMHeadModel.from_pretrained('gpt2') model.eval() # 推理模式 # 创建dummy输入(用于导出) dummy_input = torch.randint(0, 50257, (1, 128)) # batch=1, seq_len=128 # 导出为ONNX torch.onnx.export( model, dummy_input, 'gpt2.onnx', input_names=['input_ids'], output_names=['logits'], dynamic_axes={ 'input_ids': {0: 'batch', 1: 'sequence'}, 'logits': {0: 'batch', 1: 'sequence'} }, opset_version=14 ) # 量化模型(减小体积,提升速度) quantize_dynamic( 'gpt2.onnx', 'gpt2_quantized.onnx', weight_type=QuantType.QUInt8 ) print('模型转换完成!') print(f'原始大小: {os.path.getsize("gpt2.onnx") / 1024 / 1024:.2f} MB') print(f'量化后大小: {os.path.getsize("gpt2_quantized.onnx") / 1024 / 1024:.2f} MB')

结果:GPT-2模型从500MB(PyTorch)降到120MB(ONNX量化后)——仍然很大,但浏览器可以懒加载。

第三步:用ONNX Runtime Web在浏览器里加载模型

// 安装ONNX Runtime Web npm install onnxruntime-web // src/inference.ts import * as ort from 'onnxruntime-web'; import { GPT2Tokenizer } from './tokenizer'; // 需要把tokenizer转换成JS版本 class BrowserLLM { private session: ort.InferenceSession | null = null; private tokenizer: GPT2Tokenizer; async init(modelUrl: string) { // 1. 创建ONNX Runtime会话(选择WebGPU或WASM后端) const options: ort.InferenceSession.SessionOptions = { executionProviders: ['webgpu'], // 优先WebGPU,回退到wasm graphOptimizationLevel: 'all', }; try { this.session = await ort.InferenceSession.create(modelUrl, options); } catch (error) { console.warn('WebGPU not available, falling back to WASM'); options.executionProviders = ['wasm']; this.session = await ort.InferenceSession.create(modelUrl, options); } // 2. 加载tokenizer(通常需要单独转换) this.tokenizer = await GPT2Tokenizer.fromPretrained('./tokenizer.json'); } async generate(prompt: string, maxLength: number = 50): Promise<string> { if (!this.session) throw new Error('Model not loaded'); // 3. Tokenize输入 let inputIds = this.tokenizer.encode(prompt); const attentionMask = inputIds.map(() => 1); // 4. 自回归生成(逐个token生成) for (let i = 0; i < maxLength; i++) { // 准备输入张量 const inputTensor = new ort.Tensor('int64', inputIds, [1, inputIds.length]); const maskTensor = new ort.Tensor('int64', attentionMask, [1, attentionMask.length]); // 5. 运行推理 const outputs = await this.session.run({ input_ids: inputTensor, attention_mask: maskTensor, }); // 6. 获取logits(下一个token的概率分布) const logits = outputs.logits.data as Float32Array; const nextTokenId = this.sampleToken(logits, inputIds.length); // 7. 如果生成了结束token,停止 if (nextTokenId === this.tokenizer.eosTokenId) break; // 8. 添加新token到输入 inputIds = [...inputIds, nextTokenId]; attentionMask.push(1); } // 9. 解码生成的token return this.tokenizer.decode(inputIds); } private sampleToken(logits: Float32Array, seqLen: number): number { // 简化的采样(实际应该用temperature、top-k等) const lastTokenLogits = logits.slice((seqLen - 1) * 50257, seqLen * 50257); const maxIndex = lastTokenLogits.indexOf(Math.max(...lastTokenLogits)); return maxIndex; } }

第四步:在React组件里使用

// components/BrowserLLM.tsx import { useState, useEffect } from 'react'; import { BrowserLLM } from '@/lib/browserLLM'; export function BrowserLLM() { const [llm, setLLM] = useState<BrowserLLM | null>(null); const [prompt, setPrompt] = useState(''); const [output, setOutput] = useState(''); const [loading, setLoading] = useState(false); useEffect(() => { const initLLM = async () => { const model = new BrowserLLM(); // 从服务器下载模型(可以缓存到IndexedDB) await model.init('/models/gpt2_quantized.onnx'); setLLM(model); }; initLLM(); }, []); async function handleGenerate() { if (!llm || !prompt.trim()) return; setLoading(true); try { const result = await llm.generate(prompt); setOutput(result); } catch (error) { console.error('Inference failed:', error); setOutput('生成失败,请重试'); } finally { setLoading(false); } } return ( <div className="max-w-2xl mx-auto p-6"> <h2 className="text-2xl font-bold mb-4">浏览器内AI推理</h2> <p className="text-gray-600 mb-4">数据不会上传到服务器</p> <textarea value={prompt} onChange={(e) => setPrompt(e.target.value)} placeholder="输入提示词..." className="w-full p-3 border rounded mb-4" rows={4} /> <button onClick={handleGenerate} disabled={loading || !llm} className="bg-blue-600 text-white px-6 py-2 rounded disabled:bg-gray-300" > {loading ? '生成中...' : '生成'} </button> {output && ( <div className="mt-6 p-4 bg-gray-50 rounded"> <h3 className="font-semibold mb-2">生成结果:</h3> <p>{output}</p> </div> )} </div> ); }

性能优化:让浏览器内推理"可用"

优化一:模型量化(INT8/INT4)

# 用ONNX Runtime的量化工具(更激进的量化) from onnxruntime.quantization import quantize_dynamic, QuantType # INT8量化(模型大小减小75%) quantize_dynamic( 'gpt2.onnx', 'gpt2_int8.onnx', weight_type=QuantType.QUInt8 ) # INT4量化(需要onnxruntime 1.16+,模型大小减小87.5%) # 注意:INT4会有精度损失

优化二:模型分片(懒加载)

// 把大模型分成多个小块(如每个层一个文件) // 只在需要时加载对应层 class ShardedModelLoader { private loadedShards = new Set<number>(); async loadShard(shardId: number) { if (this.loadedShards.has(shardId)) return; const shard = await fetch(`/models/gpt2_shard_${shardId}.onnx`); // ... 加载逻辑 this.loadedShards.add(shardId); } async generate(prompt: string) { // 逐层加载并推理 for (let layer = 0; layer < 12; layer++) { await this.loadShard(layer); // ... 推理该层 } } }

优化三:用WebGPU加速

// 检查WebGPU可用性 async function checkWebGPU() { if (!navigator.gpu) { console.warn('WebGPU not supported'); return false; } try { const adapter = await navigator.gpu.requestAdapter(); if (!adapter) return false; const device = await adapter.requestDevice(); console.log('WebGPU available:', device); return true; } catch (error) { console.warn('WebGPU initialization failed:', error); return false; } } // 在ONNX Runtime里启用WebGPU const session = await ort.InferenceSession.create(modelUrl, { executionProviders: ['webgpu'], webgpuDevice: await navigator.gpu.requestAdapter().then(a => a!.requestDevice()) });

实际测试:浏览器内推理 vs 云端推理

测试场景:生成100个token

指标云端GPT-4 API浏览器内GPT-2(WASM)浏览器内GPT-2(WebGPU)
延迟(首token)500-1000ms2000-5000ms500-1000ms
吞吐量(tokens/秒)20-405-1015-25
隐私数据上传数据本地数据本地
成本$0.03/1K tokens$0$0

结论:浏览器内推理目前还"不够快"(尤其是WASM模式),但WebGPU可以大幅改善。适合"隐私敏感"或"离线使用"场景。

部署:模型文件托管与缓存

方案一:从CDN下载模型

// 用Service Worker缓存模型文件(离线可用) self.addEventListener('fetch', (event) => { if (event.request.url.includes('/models/')) { event.respondWith( caches.match(event.request).then((response) => { return response || fetch(event.request).then((response) => { return caches.open('models-v1').then((cache) => { cache.put(event.request, response.clone()); return response; }); }); }) ); } });

方案二:用IndexedDB存储模型

// 下载模型后存到IndexedDB(避免重复下载) async function cacheModelInIndexedDB(modelUrl: string) { const cache = await indexedDB.open('llm-models', 1); // 检查是否已缓存 const cached = await cache.get('gpt2'); if (cached) return cached; // 下载并缓存 const response = await fetch(modelUrl); const buffer = await response.arrayBuffer(); await cache.put('gpt2', buffer); return buffer; }

结论:浏览器内AI推理是"隐私优先AI"的未来

虽然目前性能不如云端GPU,但随着WebGPU的普及和模型量化技术的进步,"浏览器内AI推理"会越来越实用。

适合场景:
✅ 隐私敏感应用(医疗、金融)
✅ 离线应用(PWA)
✅ 高并发场景(不需要云端GPU)

不适合场景:
❌ 需要最新知识(模型是离线训练的)
❌ 需要超大模型(如GPT-4级别,浏览器跑不动)

下一步:从"一个小模型"(如DistilBERT做文本分类)开始尝试浏览器内推理——你会发现"隐私+零成本"的巨大价值。


这是为账号19tanjinxi生成的第2607/0726/9篇技术博客,主题:浏览器内AI推理引擎。