Ternlight:7MB 嵌入模型在浏览器中运行的实现逻辑与工程取舍

Ternlight:7MB 嵌入模型在浏览器中运行的实现逻辑与工程取舍

嵌入模型WASM浏览器ML隐私

数据源:GitHub + HN discussion · HN

一、一个 7MB 的文件在浏览器里做了什么

打开 Ternlight 的 Demo 页面,浏览器会下载一个约 7MB 的 .wasm 文件,然后开始对 2000 条 React 文档逐条生成嵌入向量。整个过程没有 API 调用,没有后端服务,纯 CPU 计算。进度条跑完后,你在搜索框里输入「how do I run something when state changes」,几十毫秒内就能拿到语义匹配的结果。

这个项目来自开发者 soycaporal 的个人业余项目,在 Hacker News 上获得了 248 分和 56 条评论,成为当日热议话题。项目的核心主张很直接:把一个足够好用的嵌入模型压缩到可以在浏览器里舒服地运行,然后在 npm 上发布。

Ternlight 不是一个通用推理框架。它是模型权重、分词器和推理引擎三者打包进一个 WASM 文件的垂直整合方案。这种做法的好处是零运行时依赖 —— npm install 之后就能用,没有模型下载步骤,没有 ONNX Runtime 的跨平台适配负担。代价是灵活性受限:你只能用它内置的模型架构,无法替换或微调。

Ternlight 项目 Banner

二、三元量化:把矩阵乘法变成加减法

Ternlight 的技术路线建立在一个前提上:微软 2024 年提出的 BitNet b1.58 证明了,Transformer 的权重如果限制在 {-1, 0, +1} 三个值上,可以在保持质量的同时,将推理计算从浮点乘法简化为条件加减。这个结论直接改变了模型的成本结构 —— 矩阵乘法(matmul)是 Transformer 前向传播中最昂贵的操作,而当每个权重都是 -10+1 时,matmul 退化成了「根据符号对激活值做加法或减法」,不再涉及乘法运算。

Ternlight 的模型从 sentence-transformers/all-MiniLM-L6-v2 蒸馏而来,并在训练过程中引入了量化感知训练(QAT)。关键做法是:学生模型在训练的每一步都模拟部署时会使用的三元量化,让模型在学习语义知识的同时,也适应量化的约束。这不同于先训练一个全精度模型再做事后量化的常见路线 —— 事后量化的质量衰减通常更严重,因为模型从未在低精度下学习过。

作者在 HN 讨论中回应了一个关键问题:0.84 的 Spearman 保真度有多少来自 QAT?答案很明确 —— 全部。作者对嵌入层尝试了 int4 的后训练量化(PTQ)并做了消融实验来找大小和质量的平衡点,但 Transformer 层的三元权重完全是在训练中学出来的,不是事后拟合的。如果对同一个架构的 fp32 模型直接做后训练三元量化,质量损失会大得多。

量化差距的数据值得注意:在 SciFact 检索任务上,三元 QAT 模型的 NDCG@10 为 0.448,而相同架构的 fp32 基线模型为 0.443。三元版本在检索指标上略微反超了全精度基线 —— 虽然差值在噪声范围内,但至少说明三元约束在这个规模上没有造成实际损失。教师保真度的差距约为 3.5 个 Spearman 点(fp32 基线 0.86 vs QAT 0.83),这在工程上是可接受的权衡。

三、打包策略:从 36MB 到 5MB

训练完成后的 PyTorch 检查点约 36MB,主要是 fp32 格式的嵌入表。Ternlight 的打包器(packer)通过三个层次压缩到这个体积:

权重编码。 训练时的「影子权重」是 fp32 存储的,但前向传播始终用的是它们的三元投影。打包器丢弃这些 fp32 影子,只保留每个权重收敛到的三元编码。每四个权重打包进一个字节:00 表示 0,01 表示 +1,10 表示 -1,11 保留。压缩比约为 16 倍,且不引入额外精度损失 —— 因为推理本来就用的是这些三元值。

嵌入表压缩。 嵌入表(30,522 × 256)是模型体积的主要贡献者。base 版本对它做了 int4 逐行量化,每行附带一个 fp32 缩放因子,压缩到约 1.86MB。mini 版本进一步将嵌入表也做了三元打包。

整体捆绑。 打包后的 .bin 文件约为 2.75MB(ternary 嵌入)到 4.6MB(int4 嵌入),加上 BERT 分词器(~0.68MB)和 Rust 引擎代码(~1.7MB),最终 .wasm 约 5-7MB。所有内容通过 Rust 的 include_bytes! 宏在编译时嵌入,运行时不需要任何文件系统访问或网络请求。

Ternlight 体积与质量 Pareto 图

这个打包策略的一个隐形成本就是模型无法被替换或升级 —— 更换模型意味着重新编译整个 WASM 模块。对于需要灵活切换模型的场景,Transformers.js 的动态加载模式可能更合适。

四、推理引擎:Rust → WASM SIMD 的手工优化

引擎是 Rust 编写的硬编码计算图,编译到 wasm32-unknown-unknown 目标。embed(text) 的调用路径为:分词 → 嵌入查表 → 两层 Transformer → 均值池化 → L2 归一化 → 384 维单位向量。整个调用不涉及任何动态内存分配(引擎在初始化时预分配一块连续内存),也没有垃圾回收停顿。

三个推理阶段的优化措施:

跳过填充位。 BERT 分词器会将输入右填充到固定长度(128 token),但实际输入很少用满。引擎只遍历真实 token,跳过注意力掩码会置零的填充位置。输入越短,节省越大。

预解包权重。 引擎初始化时,将 2-bit 打包的三元权重展开为 i8-10+1 各占一个字节)。这牺牲少量内存来消除 matmul 内循环的条件解包步骤,使内循环变为连续的 i8 × i8 乘加操作。

SIMD 内联。 matmul 的内循环使用 WASM simd128 指令手工编写(16 路并行乘加)。SIMD 操作码直接烘焙在 WASM 字节码中,不依赖运行时的动态检测 —— 只要 WASM 运行时支持 simd128(V8、JavaScriptCore、Wasmtime 均支持),就能获得加速。

单次 embed() 调用约执行 2.18 亿次操作,其中约 92%(2.01 亿次)是三元权重矩阵的加减法,仅约 8%(1700 万次)是激活值的浮点乘法(注意力分数计算、softmax、LayerNorm、GELU)。这种「几乎全是加减法」的计算特征,使得 CPU 上的向量指令可以高效利用,从而在 M4 Max 上达到 1.82ms 的单次嵌入延迟和约每秒 550 次的吞吐量。

不过,HN 上有用户报告在 Firefox + i5-4570 上只得到约 35 emb/s,远低于标称的 195-400 emb/s。作者回应暗示可能与 SIMD 路径是否生效有关,建议尝试原生 Rust 二进制来隔离问题。这说明 WASM SIMD 在实际浏览器环境中的表现存在变数,尤其是在较旧的 CPU 上。

五、两个版本的选择逻辑

Ternlight 提供了两个 npm 包:@ternlight/base@ternlight/mini。两者共享相同的 API 表面(embedcosineSimsimilar),区别在于模型容量和体积。

指标@ternlight/mini@ternlight/base
传输体积(gzip WASM)5.0 MB7.2 MB
单次嵌入延迟(p50)2.5 ms5.1 ms
吞吐量(单线程)~400 emb/s~195 emb/s
Spearman vs MiniLM 教师0.8200.844
SciFact NDCG@100.4390.465
架构2 层 · d_model=256 · 4 头2 层 · d_model=384 · 6 头
参数量~9.5M~15.4M

base 版本在质量上有约 2.4 个 Spearman 点的优势,但体积增加 44%、延迟翻倍。mini 版本更快更小,适合对延迟敏感的搜索即输入场景;base 版本适合对检索质量要求更高的场景。选择本质上是在「模型容量 vs 加载时间」之间做权衡 —— 7MB 的 base 版本在 4G 网络下约需 1-2 秒下载,而 5MB 的 mini 版本可以控制在 1 秒以内。

值得注意的是,两个版本的最大输入长度都是 128 token(约 95 个英文单词),输出都是 384 维 L2 归一化向量。这个限制意味着 Ternlight 不适合处理长文档 —— 你需要提前做好分块(chunking),对每个块分别嵌入。

六、在浏览器端嵌入模型的生态位

在 Ternlight 出现之前,浏览器端运行嵌入模型的主要方案是 Transformers.js 和 ONNX Runtime Web。Transformers.js 可以加载 ONNX 格式的模型,支持 WebGPU 加速,但模型体积通常在 20-80MB 范围。举例来说,Xenova/all-MiniLM-L6-v2 的 ONNX 量化版本约 23MB,运行时还需要额外的 WASM 后端文件。

Ternlight 的差异化在于「激进的小」。7MB 的体积使得它可以作为页面资源的一部分随首屏加载,而不是需要用户明确等待的「模型下载」。对于静态网站(Jekyll、Hugo、Astro),这意味着可以把语义搜索打包进构建产物,实现完全无后端的向量搜索。

HN 讨论中有人提到将这个模型与 portable-hnswsqlite-vec 结合,在静态托管上实现基于 HTTP Range 请求的向量搜索 —— 模型嵌入在页面中,索引存储在 Parquet 文件里,全部通过 CDN 分发。这种架构对于文档站点、个人知识库和技术博客的站内搜索,提供了一个不需要 Elasticsearch 或向量数据库的替代方案。

在隐私方面,全部计算在用户设备上完成,意味着搜索查询和文档内容不会离开浏览器。对于处理敏感数据的企业内网应用、医疗信息检索或个人知识管理工具,这是一个有实际价值的特性。但这不意味着隐私问题被「解决」了 —— 模型本身的推理结果仍然可以通过其他渠道(例如页面中的分析脚本)被泄露,只是减少了网络传输这一攻击面。

七、已知限制和待解决的问题

Ternlight 目前的状态是一个功能完整的 MVP,而非生产级基础设施。以下几个方面存在明显的局限性:

语言支持。 模型蒸馏自 all-MiniLM-L6-v2,训练数据以英文为主。在 HN 讨论中,有用户询问法语等多语言支持,目前没有明确的多语言基准测试数据。考虑到底层使用了 BERT 的多语言词汇表,理论上可以处理部分非英文输入,但嵌入质量没有保证。

上下文长度。 128 token 的限制在检索场景中通常不是问题(大多数搜索查询远短于此),但对于需要嵌入段落或长句子的用例,必须提前分块。分块策略的选择会直接影响检索质量,而 Ternlight 本身不提供分块工具。

基准测试覆盖不足。 目前的公开评估只有 SciFact 检索和教师保真度两个指标,缺少 MTEB 排行榜上的完整对比。作者在 HN 回复中表示 MTEB 和 STS-B 基准测试在路线图上,同时也在计划用 gte-small 作为新的教师模型进行蒸馏。在更全面的基准结果出来之前,将 Ternlight 与 bge-small-en-v1.5 等成熟模型做质量对比是困难的。

不可替换模型。 前面提到过,WASM 的模型是编译时嵌入的。如果未来需要切换到不同的教师模型或调整架构,必须以新的 npm 包版本发布,用户需要重新安装。对于需要灵活切换模型的场景,这种刚性是一个明显的限制。

SIMD 兼容性。 实际性能与浏览器的 WASM SIMD 实现质量强相关。在较旧硬件或不支持 simd128 的环境中,性能退化可能很严重。目前的基准测试数据来自 M 系列 Mac + Node/V8,不具备跨硬件和跨浏览器的代表性。

八、一个值得关注的信号

Ternlight 展示了一个方向:当模型压缩到 5-7MB、推理延迟降到毫秒级,「把模型放进前端 bundle」从一个实验性想法变成了一个可操作的工程决策。语义搜索、意图匹配和文本聚类这些能力,可以被纳入前端工程师的工具箱,不再需要后端 ML 基础设施的配合。

三元量化和 BitNet 路线在 LLM 领域的应用仍处于探索阶段,但在嵌入模型这个更小的战场上,Ternlight 已经给出了一个有说服力的存在证明。一个不足 10MB 的 WASM 文件,在用户浏览器里完成上述全部工作 —— 这件事在 2024 年 BitNet 论文发表之前,是很难严肃讨论的。

本文的素材来自公开信息和社区讨论。如果你对这个话题有更深入的一手经验,欢迎指出文中的不足。

参考链接