ONNX · WebGPU · Graph Surgery

5D GridSample 进不了浏览器之后:LivePortrait 的 WebGPU 移植实战

当一个神经网络算子没有浏览器实现时,真正的问题不是“换哪个库”,而是能否把计算改写成 WebGPU 支持的图,并证明它在数值上仍然是同一个模型。

Timeline Studio 工程手记2026-07-17约 15 分钟
5D 张量改写为 4D 采样平面并进入 WebGPU 的概念图

Timeline Studio 的数字人链路使用 JoyVASA 从音频生成动作,再由 LivePortrait 把动作施加到源肖像。模型本身可以导出为 ONNX,真正阻塞浏览器的是生成器中的 rank-5 运算和两个 5D GridSample

01. 先把“数字人”拆成可验证的流水线

这条链路不是单模型调用。它至少包含外观特征提取、源图动作提取、音频驱动、嘴型重定向、关键点拼接、3D 特征形变与 SPADE 图像生成。JoyVASA 的 HuBERT 与 diffusion denoiser 负责把语音转成动作序列,LivePortrait 则负责神经渲染。

Audio
JoyVASA
Motion
Keypoints
3D Warp
LivePortrait
SPADE
RGB Frame

先拆管线的价值是可定位:音频驱动的误差、关键点组合的误差、warping 的误差和最终生成器的误差可以分别测量。否则最终人脸“不对”,你几乎无法知道问题来自哪一段。

02. 真正的阻塞点:浏览器没有 5D GridSample

基线 combined generator 使用 opset 20,共 277 个节点,其中有 13 个 rank-5 Conv 与两个 rank-5 GridSample。原仓库附带的 Linux .so 插件无法在浏览器加载,而 ONNX Runtime Web 的 WebGPU 执行提供程序也不能直接执行这两个 5D 采样节点。

输入特征可写成 [N, C, D, H, W],采样网格则在三个空间轴上给出目标坐标。Web 端现成的 GridSample 只处理二维平面。直接删除深度维、把它当批次维或者只取最近切片,都会改变模型含义。

工程原则:当目标运行时缺少算子时,图改写必须保持数学语义。视觉上“差不多”不能替代误差统计。

03. 等价改写:4D 采样 + 深度线性插值

三线性采样可以拆成两步:先在深度轴上找到目标坐标两侧的切片 d0d1;分别对这两个 2D 平面执行双线性 GridSample;最后按深度小数部分 α 做线性插值。

d0 = floor(z)
d1 = min(d0 + 1, D - 1)
alpha = z - d0
v0 = GridSample4D(feature[d0], gridXY)
v1 = GridSample4D(feature[d1], gridXY)
output = (1 - alpha) * v0 + alpha * v1

实现时需要把深度切片组织成批次,使 WebGPU 能并行执行 4D GridSample,再还原为原来的空间布局。稀疏动作网格还存在经过验证的共享 XY 结构,可以去掉重复的深度采样。改写后的图从 277 个节点增加到 316 个节点,但全部落在 Web 端已支持的算子集合里。

04. 数值验证:证明改写前后是同一个函数

验证使用固定输入,将原始 512×512 generator 与 WebGPU 改写图的完整输出逐元素比较。除了最大误差,还同时查看均值和高分位误差,以免少量异常点被平均值掩盖。

7.8976e-6最大绝对误差
1.3853e-7平均绝对误差
1.7881e-699.9% 分位误差

JoyVASA 的 HuBERT 与 diffusion denoiser ONNX 导出也分别对齐 PyTorch,最大绝对误差为 7.75e-66.44e-6。只有每一段都建立基线,端到端结果才具有可解释性。

05. 图能跑以后,运行时问题才刚开始

ONNX Runtime Web 的 WebGPU Worker 必须加载匹配的 Asyncify glue 与 WASM 文件。使用普通或 JSEP glue 会触发 webgpuInit is not a function。多个大型执行提供程序会话还需要串行创建,否则 ORT 可能拒绝并发初始化。

模型包约 906 MB,不适合进入 Git 仓库。项目把它拆成 50 MB 分片,发布到 revision-pinned 的 Hugging Face 模型仓库,并由 Service Worker 缓存。版本固定同时保护了两个假设:图结构不变,运行时代码期待的输入输出也不变。

await createJoyVasaSession();
releaseTransientGpuResources();
await createLivePortraitSession();
appearanceFeature = await extractOnce(sourcePortrait);
for (const motion of sparseMotions)
  frames.push(await render(appearanceFeature, motion));

06. “跑通”不等于“实时”

项目提供 mixed-FP16 256px 快速预览与 mixed-FP16 512px 质量档。外观特征在帧间保持 GPU 驻留,神经关键帧以 1–2 fps 稀疏渲染,再插值到编码帧率,降低总推理次数。

但必须诚实:在验证设备上,512×512 神经帧仍需要约 25–60 秒。界面展示真实阶段、帧数与耗时,不把稀疏关键帧插值包装成“实时神经渲染”。生成帧会检查非有限值与时间异常,问题帧重试一次,仍失败则沿用上一有效帧。

07. 端到端验收必须走完媒体闭环

单个 ONNX session 返回 tensor 只代表算子层通过。真正的验收还包括:模型分片下载、WebGPU 初始化、JoyVASA 50 步动作生成、LivePortrait 渲染、WebM 编码、浏览器播放、媒体库插入和视觉轨替换。

最终验收产物是一段可播放的 3.56 秒 WebM。抽帧能看到张嘴、闭嘴以及头眼运动,完成后浏览器控制台错误为 0。这个闭环很重要,因为编码、Blob 生命周期或轨道替换中的任意错误,都能让“模型成功”变成“用户什么也得不到”。

结论:算子移植的核心是可证明

将桌面端模型带进浏览器,最有价值的能力不是写一段兼容代码,而是建立完整证据链:审计图、定位不支持算子、构造等价变换、逐层对齐、统计误差、验证真实运行时,最后走完整个媒体产品闭环。

WebGPU 正在快速扩大浏览器的计算边界。但在这个边界上,最可靠的优化不是更响亮的性能口号,而是每一步都有数字、失败路径和可复现产物。