Repository navigation
[Vulkan 2.0.0] 常量 Reshape 别名生成非法 buffer offset,导致错误输出 / DeviceLost #35
Description
问题概述
在 NuGet Sdcb.SimdPaddleOCR 2.0.0 的 Vulkan 后端,常量经 Reshape 后作为 Add 输入时,GPU planner 没有正确沿用别名根节点的常量缓冲区,而是生成了 18446744073709551614(ulong(-2))的非法字节偏移。
在两份 PP-OCRv6 检测模型中,此问题触发 vkWaitForFences: ErrorDeviceLost 并回退 CPU。进一步缩减为 4 个节点、2230 字节、不含业务权重/图片的合成 ONNX 后,同一非法绑定仍可复现;合成模型在本机这次没有 DeviceLost,但 GPU 返回了错误数值。因此本 issue 的核心是非法 descriptor / 错误常量别名处理,不声称任意模型、驱动都会以 DeviceLost 表现。
环境与验证边界
- Windows 11 x64,.NET 10.0.12。
- RTX 4070 Laptop 8 GB,NVIDIA 572.83,Vulkan 1.4.303。
- 实测 DLL:NuGet 2.0.0,repository commit
697208399240f21ec8ff28218fc8cd2c4dbbe0ee。 - 核心 DLL SHA256:
a19f836064037f509dfe3665a20993b7461b081f9ce7d5f4d64a8ccc117bb127。 - 未修改第三方源码、DLL 或驱动。以下内部反射仅用于固定版本诊断。
- 当前 main 的
GpuGraph.cs中同一Operand判断仍在(源码核查);未把这一点称为已运行最新版/PR Switch to the shared Sdcb.SimdOnnx engine #34。 - 这与 CPU NHWC 形状信息兼容问题是两项独立问题。最小复现显式设置
PPOCR_NHWC=0,且 ONNX 提供完整中间形状。
最小复现
1. 生成合成 ONNX
保存为 make_model.py,使用 Python + numpy + onnx 执行。本机使用 onnx 1.17.0 / numpy 2.2.6,onnx.checker.check_model(..., full_check=True) 通过。
from pathlib import Path
import numpy as np
import onnx
from onnx import helper as h, numpy_helper as nh, TensorProto as T
target = Path(__file__).with_name('constant-reshape-bias.onnx')
if target.exists():
raise FileExistsError(target)
shape = [1, 16, 320, 320]
graph = h.make_graph([
h.make_node('Conv', ['x', 'w'], ['conv'], kernel_shape=[3, 3], strides=[2, 2], pads=[1, 1, 1, 1]),
h.make_node('Reshape', ['bias', 'bias_shape'], ['bias4']),
h.make_node('Add', ['conv', 'bias4'], ['sum']),
h.make_node('Relu', ['sum'], ['y']),
], 'constant-reshape-bias', [h.make_tensor_value_info('x', T.FLOAT, [1, 3, 640, 640])],
[h.make_tensor_value_info('y', T.FLOAT, shape)], initializer=[
nh.from_array(np.full((16, 3, 3, 3), 0.01, dtype=np.float32), 'w'),
nh.from_array(np.arange(16, dtype=np.float32), 'bias'),
nh.from_array(np.array([1, 16, 1, 1], dtype=np.int64), 'bias_shape'),
], value_info=[h.make_tensor_value_info('conv', T.FLOAT, shape),
h.make_tensor_value_info('bias4', T.FLOAT, [1, 16, 1, 1]),
h.make_tensor_value_info('sum', T.FLOAT, shape)])
model = h.make_model(graph, opset_imports=[h.make_opsetid('', 14)], ir_version=8)
onnx.checker.check_model(model, full_check=True)
onnx.save(model, target)
print(f'ONNX checker passed: {target.name}, {target.stat().st_size} bytes')生成的 constant-reshape-bias.onnx SHA256:
3d489776dad2f3a48d7942627396cc8ee87b33b157e6601b7161ebe238225a19。
结构为 Conv → Add → Relu,Add 的另一输入来自常量偏置 [16] → Reshape → [1,16,1,1]。输入全零时,输出各通道应分别为常数 0 到 15。
2. .NET 诊断程序
dotnet new console -n Repro -f net10.0
dotnet add Repro package Sdcb.SimdPaddleOCR --version 2.0.0将 Repro/Program.cs 替换为以下代码。使用内部 GPU runner 是为了保留原始错误,避免高层 OCR 自动回退 CPU 掩盖结果;它不是建议的业务调用方式。
using System.Reflection;
using Sdcb.SimdPaddleOCR.OnnxSharp;
// Fixed 2.0.0 internal diagnostic, not a production API or a workaround.
Environment.SetEnvironmentVariable("PPOCR_NHWC", "0");
const BindingFlags F = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
using var model = Model.Load(File.ReadAllBytes(args[0]));
int[] shape = [1, 3, 640, 640];
float[] input = new float[3 * 640 * 640];
using var cpu = new InferenceSession(model, shape, intraOpThreads: 4);
float[] expected = cpu.Run(input);
if (expected.Length != 16 * 320 * 320 || expected.Where((v, i) => v != i / (320 * 320)).Any())
throw new Exception("CPU result differs from the analytically expected channel biases");
Console.WriteLine("CPU reference: all 1638400 values match channel biases 0..15");
var asm = typeof(Model).Assembly;
var backend = asm.GetType("Sdcb.SimdPaddleOCR.Backends.Vulkan.GpuBackend", true)!;
object device = backend.GetMethod("TryGetDevice", F)!.Invoke(null, null) ?? throw new Exception("No Vulkan device");
var type = asm.GetType("Sdcb.SimdPaddleOCR.Backends.Vulkan.GpuDetGraph", true)!;
using var runner = (IDisposable)Activator.CreateInstance(type, F, null, [device, cpu.CompiledModel], null)!;
object schedule = type.GetMethod("Prepare", F)!.Invoke(runner, [shape, int.MaxValue, -1])!;
Array recs = (Array)schedule.GetType().GetField("Recs", F)!.GetValue(schedule)!;
bool found = false;
foreach (object rec in recs)
{
string tag = (string)rec.GetType().GetField("Tag", F)!.GetValue(rec)!;
Array binds = (Array)rec.GetType().GetField("Binds", F)!.GetValue(rec)!;
foreach (object bind in binds)
{
ulong offset = (ulong)bind.GetType().GetProperty("ByteOffset")!.GetValue(bind)!;
if (offset == ulong.MaxValue - 1)
{
found = true;
Console.WriteLine($"INVALID_BINDING {tag}: offset={offset}");
}
}
}
if (!found) throw new Exception("The expected 2.0.0 invalid binding was not reproduced");
// Omit --execute to inspect the bug without submitting its invalid descriptor.
if (args.Contains("--execute"))
{
try
{
var run = type.GetMethod("Run", F)!.CreateDelegate<RunGpu>(runner);
float[] actual = run(shape, input, int.MaxValue, -1).ToArray();
if (actual.Length != expected.Length) throw new Exception("GPU output length mismatch");
int bad = actual.Where((v, i) => !float.IsFinite(v) || Math.Abs(v - expected[i]) > 0.001f).Count();
Console.WriteLine($"GPU returned; mismatched/nonfinite values vs analytical CPU reference: {bad}/{actual.Length}");
}
catch (Exception ex) { Console.Error.WriteLine(ex); Environment.ExitCode = 1; }
}
delegate ReadOnlySpan<float> RunGpu(int[] shape, ReadOnlySpan<float> input, int nodeLimit, int outTensor);python make_model.py
# 仅构建调度并检查绑定,不提交非法 descriptor:
dotnet run --project Repro -c Release -- constant-reshape-bias.onnx
# 可选:实际执行 GPU;原始真实模型在这个错误处会 DeviceLost:
dotnet run --project Repro -c Release -- constant-reshape-bias.onnx --execute本机合成模型结果
CPU reference: all 1638400 values match channel biases 0..15
INVALID_BINDING binAdd n2: offset=18446744073709551614
GPU returned; mismatched/nonfinite values vs analytical CPU reference: 1536000/1638400
诊断程序构建为 0 警告 / 0 错误。合成模型只需 Prepare 即可观察非法偏移,不必真的提交它来复现这个错误。
源码与实际调度证据
固定 2.0.0 源码:
- GpuGraph.cs:604:Reshape 输出被登记为输入的 alias。
- GpuGraph.cs:666:常量没有 arena slot,偏移保持
-1。 - GpuGraph.cs:906:
Operand使用原张量的isConst[t],没有按 alias 根节点判断常量:
return isConst[t] ? (ConstF16(t), 0, mode) : (arena, off[Phys(t)], mode);- GpuGraph.cs:868:
-1 * 2转为ulong后得到18446744073709551614。 - 该偏移随后进入
GpuRunner.RecordOne的VkDescriptorBufferInfo,违反 VUID-VkDescriptorBufferInfo-offset-00340:offset 必须小于 buffer size。
合成模型实际记录:
NODE 1 Reshape in=2,3 out=5
t2: constant, shape=[16], offset=-1
t5: non-constant flag, shape=[1,16,1,1], alias=2, offset=-1
DISPATCH 1 binAdd n2
second binding offset=18446744073709551614
真实模型上的受控定位
另对“训练后导出的 v6 tiny DET”和“官方预训练权重导出的 v6 medium DET”分别做了三项独立进程检查。均使用全零 float32 1×3×640×640,直接调用 GPU runner,无图片前后处理、REC 或 CPU 回退:
| 执行范围 | tiny DET | medium DET |
|---|---|---|
| 仅第一个卷积(dispatch prefix 1) | 完成 | 完成 |
| 再执行第一处偏置 Add(prefix 2) | ErrorDeviceLost | ErrorDeviceLost |
| 只在诊断进程把该 Add 的第二个输入绑定纠正为 alias 根常量、offset=0,再执行 prefix 2 | 完成 | 完成 |
tiny 为 t280 → t1(16 个 fp16 数,32 字节),medium 为 t415 → t107(64 个 fp16 数,128 字节)。这六项预期结果均通过断言。修改的是隔离进程的一条调度记录,不是正式修复;前缀没有执行到完整模型输出,因此不把它当作完整 GPU 精度验收。
期望行为 / 修复方向
- 正确解析常量别名,并使用真实常量缓冲区;广播形状仍须保留 Reshape 后的逻辑形状。
- 在提交前校验 descriptor 的 offset/range,避免把未分配的 arena slot 转成无符号地址。
- 添加“常量 Reshape → Add”回归用例,验证 GPU 数值,而不只是能否正常返回。
- 若这种图表达暂不支持,应在建图时明确拒绝,而不是提交非法 descriptor。
注意到 #34 正在迁移到共享 SimdOnnx 引擎,先提交 issue 请维护者确认适合在 2.x 修复,还是将同一回归用例放到新引擎。这里没有假定 #34 已修复或仍存在该问题,也没有提交 PR。
Activity