Skip to content

[Vulkan 2.0.0] 常量 Reshape 别名生成非法 buffer offset,导致错误输出 / DeviceLost #35

Open

Description

@YDLuo-1

问题概述

在 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 源码:

  1. GpuGraph.cs:604:Reshape 输出被登记为输入的 alias。
  2. GpuGraph.cs:666:常量没有 arena slot,偏移保持 -1。
  3. GpuGraph.cs:906:Operand 使用原张量的 isConst[t],没有按 alias 根节点判断常量:
return isConst[t] ? (ConstF16(t), 0, mode) : (arena, off[Phys(t)], mode);
  1. GpuGraph.cs:868:-1 * 2 转为 ulong 后得到 18446744073709551614。
  2. 该偏移随后进入 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions