尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

【Bug已解决】[Web] WebGPU EP graph-capture replay misbinds an internal uniform buffer for a static-shape…

【Bug已解决】[Web] WebGPU EP graph-capture replay misbinds an internal uniform buffer for a static-shape… 【Bug已解决】[Web] WebGPU EP graph-capture replay misbinds an internal uniform buffer for a static-shape encoder transformer (incorrect outputs) 解决方案一、现象长什么样在浏览器里用 ONNX Runtime 的 WebGPU Execution ProviderEP跑一个静态形状static-shape的 encoder transformer比如一个固定序列长度的文本/音频编码器时开启 graph capture把整图录制成一次 compute pass 重放以提升性能后输出结果和不开启 capture 时不一致而且每次重放都给出错误但确定的输出。现象# 现象 A开 graph capture 后输出数值错关掉就正确 # capture 关正确 logits # capture 开数值明显偏移但进程不报错 # 现象 B同一个 uniform buffer 被绑到了错误的 binding slot # 调试 WebGPU 发现encoder 里某个内部 uniform如 layernorm 的 # eps / 形状常量在 capture 录制时绑的是 binding 3重放时却绑到了 # binding 3 被别的 buffer 占用 —— 于是算子读了错误的常量 # 现象 C只在 static-shape 路径触发 # dynamic-shape 走的是另一套绑定逻辑每次重新 bind没问题 # static-shape 走了“录制一次、重放多次”的 capture 路径才暴露最坑的是现象 A不报错、输出“看起来像那么回事”但就是错只有和未 capture 的参考对拍才发现偏差。这是 WebGPU EP 审查里典型的“录制/重放资源绑定错位”。二、背景WebGPU 的 compute 管线靠bind group把资源buffer/texture绑到 shader 的 binding slot。ONNX Runtime WebGPU EP 对“静态形状”的图做优化把整张计算图录制成一个GPUCommandEncoder 一组bindGroup之后每次推理只copyBufferToBuffer更新输入、然后commandEncoder.finish()重放省去每帧重建 bind group 的开销——这就是 graph capture。问题出在录制时构建的 bind group 里某个内部 uniform buffer由 EP 在内部创建用于存放 layernorm eps、头数等常量被绑定到了一个在重放时会被输入 buffer 覆盖的 slot或者更常见——EP 在“静态形状”路径下复用了上一次 capture 的 bind group 缓存但这次图的 uniform 布局和上次不同缓存命中了错误的 bind group。于是重放时算子从错误的 binding 读了常量输出错。这是 WebGPU EP 审查里“录制态与重放态资源视图不一致”的典型问题。三、根因capture 缓存的 bind group 未随 uniform 布局失效静态形状路径复用了上次 capture 的 bind group但本次图的 uniform buffer 布局变了即便形状相同常量内容/顺序不同缓存没失效 → 绑错。内部 uniform 与输入 buffer 共用 slot 区间EP 把内部 uniform 和用户输入 buffer 编进同一个 bind group 的不同 slot但 slot 分配逻辑在录制和重放两处不一致导致重放时 uniform 被输入覆盖。缺少 capture vs 非 capture 的数值对拍没有断言“开/关 capture 输出一致”绑定错位长期存在。本质是WebGPU EP 的 graph capture 在静态形状路径下复用了失效的 bind group 缓存且内部 uniform 的 slot 分配在录制/重放间不一致缺少一致性对拍。四、最小可运行复现下面用 Python 模拟“capture 缓存 bind group 未随 uniform 布局失效导致重放绑错”class BindGroup: def __init__(self, layout_key, bindings): self.layout_key layout_key self.bindings bindings # dict: slot - buffer_id class WebGpuCaptureBuggy: def __init__(self): self._capture_cache {} # layout_key - BindGroup def run(self, graph, inputs, use_captureTrue): # layout_key 只看“形状”不看“uniform 内容/顺序” key graph[shape] if use_capture and key in self._capture_cache: bg self._capture_cache[key] # ← 复用旧 bind group else: # 录制把 uniform(槽3) 和 input(槽0) 编进 bind group bg BindGroup(key, {shape: key, 0: inputs[input_buf], 3: graph[uniform_buf]}) self._capture_cache[key] bg # 重放用 bg 里的绑定但这次 graph 的 uniform 内容变了 return bg.bindings[3] graph[uniform_buf] # 第一次uniformA g1 {shape: static, uniform_buf: A} ep WebGpuCaptureBuggy() print(ep.run(g1, {input_buf: in1}, use_captureTrue)) # True首次录制 # 第二次同样 static 形状但 uniform 内容变成 B布局/顺序变了 g2 {shape: static, uniform_buf: B} print(ep.run(g2, {input_buf: in2}, use_captureTrue)) # False → 绑错复用了 A第二次run返回False证明重放时绑的还是第一次的 uniform A而不是本次的 B。五、解决方案第一层最小直接修复最小修复capture 缓存的 key 不能只看形状必须包含 uniform 布局的签名且每次 capture 用本次图的 uniform 重建 bind groupclass WebGpuCaptureFixed: def __init__(self): self._capture_cache {} def _layout_signature(self, graph): # 签名 形状 uniform 内容/顺序的哈希确保布局变了就失效 return (graph[shape], graph[uniform_sig]) def run(self, graph, inputs, use_captureTrue): key self._layout_signature(graph) if use_capture and key in self._capture_cache: bg self._capture_cache[key] else: # 录制永远用本次 graph 的 uniform_buf bg BindGroup(key, {0: inputs[input_buf], 3: graph[uniform_buf]}) self._capture_cache[key] bg return bg.bindings[3] graph[uniform_buf]这一层改动最小缓存 key 加上 uniform 签名重放必绑本次 uniform错位消失。但依赖“每处 capture 都加签名”下看第二层。六、解决方案第二层结构性改进把“WebGPU graph capture 的 bind group 必须按完整布局签名缓存、且内部 uniform slot 与输入严格分区”固化成单一事实来源。下面这个 dataclass 集中管理from dataclasses import dataclass, field from typing import Dict, Tuple import hashlib dataclass class WebGpuUniformBindPolicy: 单一事实来源WebGPU capture 的 bind group 缓存与 slot 契约。 _cache: Dict[Tuple, dict] field(default_factorydict) UNIFORM_SLOT_START: int 16 # 内部 uniform 占 [16, ...)输入占 [0,16) def layout_signature(self, shape: str, uniform_order: tuple) - Tuple: h hashlib.sha1(str(uniform_order).encode()).hexdigest()[:12] return (shape, h) def build_bind_group(self, shape, uniform_order, input_buf, uniform_buf): sig self.layout_signature(shape, uniform_order) # 内部 uniform 严格放在 UNIFORM_SLOT_START 之后与输入 slot 不重叠 bindings {0: input_buf, self.UNIFORM_SLOT_START: uniform_buf} self._cache[sig] bindings return bindings def replay(self, shape, uniform_order, expected_uniform): sig self.layout_signature(shape, uniform_order) if sig not in self._cache: raise RuntimeError(capture not found; layout changed, re-record) # 断言重放绑定的 uniform 确实是本次预期的 bound self._cache[sig][self.UNIFORM_SLOT_START] if bound ! expected_uniform: raise AssertionError(funiform misbind: bound {bound} ! {expected_uniform}) return True这一层的关键收益完整签名缓存 key 含 uniform 顺序哈希布局变即失效杜绝复用旧 bind groupslot 分区内部 uniform 固定占UNIFORM_SLOT_START与输入 slot[0,16)严格不重叠杜绝覆盖重放校验replay断言绑定的 uniform 是本次预期的错位立刻报错单一事实来源所有 capture 绑定约定收口在WebGpuUniformBindPolicy。七、解决方案第三层断言 / CI 守护把第二层钉成 pytest挂进 CI确保 capture 重放一致、uniform 不绑错import pytest from your_package.webgpu_bind import WebGpuUniformBindPolicy def test_capture_replay_consistent(): # 断言 1同布局 capture/重放uniform 一致 p WebGpuUniformBindPolicy() bg p.build_bind_group(static, (eps, heads), in_buf, U_A) assert p.replay(static, (eps, heads), U_A) def test_layout_change_invalidates_cache(): # 断言 2uniform 顺序变了必须重录而非复用旧绑定 p WebGpuUniformBindPolicy() p.build_bind_group(static, (eps, heads), in_buf, U_A) # 顺序不同 新签名旧缓存不会被误用 try: p.replay(static, (heads, eps), U_B) assert False, should require re-record except RuntimeError: pass def test_uniform_slot_isolated_from_input(): # 断言 3uniform slot 与输入 slot 不重叠 p WebGpuUniformBindPolicy() bg p.build_bind_group(static, (eps,), in_buf, U_A) assert 0 in bg and p.UNIFORM_SLOT_START in bg assert 0 ! p.UNIFORM_SLOT_START def test_misbind_detected(): # 断言 4重放绑错 uniform 必须报错 p WebGpuUniformBindPolicy() p.build_bind_group(static, (eps,), in_buf, U_A) with pytest.raises(AssertionError): p.replay(static, (eps,), U_WRONG)四条断言从“重放一致”“布局变失效”“slot 隔离”“绑错报错”四面把回归钉死在 CI。八、排查清单WebGPU EP 开 graph capture 后输出错时对比开/关 capture 的输出。不一致就高度怀疑 bind group 错位现象 A。capture 缓存 key 是否只看形状必须包含 uniform 布局/顺序签名否则复用旧绑定现象 B。内部 uniform 的 binding slot 是否与输入 buffer 重叠重叠就会重放时被覆盖。用第二层WebGpuUniformBindPolicy完整签名缓存 uniform slot 严格分区 重放校验。加第三层 pytest断言“重放一致、布局变失效、slot 隔离、绑错报错”。static-shape 才走 capture 路径dynamic-shape 正常——这是定位到该路径的关键线索。九、小结WebGPU EP 的 graph-capture 错位 bug 本质是静态形状路径下复用了失效的 bind group 缓存缓存 key 只看形状、不含 uniform 布局签名且内部 uniform 的 binding slot 与输入 buffer 分配不一致导致重放时算子从错误 binding 读取常量、输出静默错误且因不报错只能靠对拍发现。修复分三层——第一层缓存 key 加上 uniform 布局签名、每次 capture 用本次 uniform 重建第二层用WebGpuUniformBindPolicy这个 dataclass 把“完整签名缓存 uniform slot 严格分区 重放校验”收口成单一事实来源第三层用四条 pytest 把“重放一致、布局变失效、slot 隔离、绑错报错”钉死在 CI。核心心法graph capture 的 bind group 缓存必须以完整布局含 uniform 顺序为签名且内部 uniform 的 slot 必须与输入严格分区否则重放必绑错。
返回列表