MDCA-DETR复现实验
torchvision 0.16.2 内存越界问题

CPU bilinear_interpolate 越界访问分析与解决方案

苗夕远 · MDCA-DETR复现调试 · 2026-05-27

一、实验环境与问题概述

1.1 实验环境

由于 RTX 4070 Ti SUPER (Ada Lovelace) 比论文使用的 RTX 3090 (Ampere) 更新一代,不支持低版本 CUDA/PyTorch 库,因此采用更高版本组合。在 Windows 11 和 ArchLinux 双系统上分别测试,崩溃在两平台上均复现(Windows: 0xC0000005,ArchLinux: SIGSEGV)。最终使用 GDB 在 ArchLinux 上定位根因并修复。

项目实验环境论文原始环境
CPUIntel Core i5-14600KFIntel Xeon Platinum 8358P @ 2.60 GHz
GPUNVIDIA GeForce RTX 4070 Ti SUPER (16GB)NVIDIA GeForce RTX 3090 (24GB)
操作系统ArchLinux / Windows 11Ubuntu 20.04
Python3.8.103.8.10
PyTorch2.1.21.11.0
torchvision0.16.2 (CPU bilinear_interpolate bug)
CUDA12.111.3

1.2 复现项目结构

MDCA-DETR论文复现基于Ultralytics YOLOv8框架(RT-DETR),骨干网络采用了改造版ResNet-18,其中Stage 2/3使用 BasicBlock_DCNv2_Dynamic(即论文中的MCDC模块),Stage 4/5使用 BasicBlock_Attention(即论文中的CA模块)。

Backbone: ResNet-18 改造版
  ├── Stem (ConvNormLayer + MaxPool2d)          标准模块
  ├── Stage2 (P3/8):  BasicBlock_DCNv2_Dynamic  ★ MCDC
  ├── Stage3 (P4/16): BasicBlock_DCNv2_Dynamic  ★ MCDC
  ├── Stage4 (P5/32): BasicBlock_Attention      ★ CA
  └── Stage5 (P5/32): BasicBlock_Attention      ★ CA

1.3 问题触发时序

在处理好依赖和本地化配置后进行实验训练的完整流程如下:

步骤1RTDETR.__init__("rtdetr-attention.yaml")
创建第一个模型 (nc=80, 架构模型), 成功打印 "53.5 GFLOPs"
步骤2model.train(data=..., epochs=300)
触发训练阶段, 内部调用 self.trainer.get_model(cfg=..., nc=10)
步骤3DetectionModel.__init__()BaseModel.__init__()
创建第二个模型 (nc=10, 训练模型)
步骤4self.info()get_flops()
触发CPU上的FLOPs估算, 调用 thop.profile()
步骤5 — Profiler构造 (1, 3, 32, 32) CPU dummy input, 逐层前向传播
走到 Backbone Stage 4/5 的 BasicBlock_DCNv2_Dynamic
步骤6DCNv2_Dynamic.forward()
调用 torch.ops.torchvision.deform_conv2d()
步骤7 — torchvision C++ 路由到 deform_conv2d CPU kernel
调用 bilinear_interpolate<float>()
CRASH — Windows: 0xC0000005 (Access Violation)
ArchLinux: SIGSEGV (Signal 11: Segmentation fault)

两个环境下的报错都指向同一件事:训练开始前的FLOPs估算阶段,torchvision 0.16.2 的CPU版 bilinear_interpolate 发生了内存越界访问,系统内核瞬间切断进程。

二、崩溃定位与分析

2.1 GDB回溯

在ArchLinux环境中使用GDB对Python进程进行调试定位,崩溃现场如下:

Thread 1 "python" received signal SIGSEGV, Segmentation fault.
0x00007ffe78aab0f2 in float vision::ops::(anonymous namespace)::bilinear_interpolate<float>(
    float const*, int, int, float, float) [clone .part.0] ()
   from /mnt/2T2/share/作业/实习/论文文献阅读分享/expre/.venv/lib/python3.8/site-packages/torchvision/_C.so

关键信息:

  • 崩溃点位于 torchvision/_C.so —— torchvision的底层C++编译库
  • 具体函数是 bilinear_interpolate (双线性插值算子)
  • 这是一个 CPU版本的 bilinear_interpolate,位于匿名命名空间(anonymous namespace)中
  • 它是普通C++函数,不是 __device__ 修饰的CUDA kernel

2.2 调用链还原

综合代码审查和GDB分析,完整的崩溃调用链如下:

RTDETR.__init__("rtdetr-attention.yaml")
DetectionModel.__init__()
BaseModel.__init__()
self.info() # 打印模型信息 (参数量 + FLOPs)
get_flops()
thop.profile(model, inputs=(1,3,32,32))
Profiler前向传播 (CPU dummy input, 逐层执行)
BasicBlock_DCNv2_Dynamic.forward() (Stage 4/5)
DCNv2_Dynamic.forward() # block.py:505-525
torch.ops.torchvision.deform_conv2d(x, offset, weight, ...)
torchvision C++ dispatcher
deform_conv2d CPU kernel
bilinear_interpolate<float>()
为什么是CPU kernel?
thop.profile() 构造的 dummy input 是 (1, 3, 32, 32) 的CPU tensor(未指定 device='cuda')。 因此整个前向传播在CPU上执行,deform_conv2d的路由器将调用分发给CPU实现,从而触发了CPU版 bilinear_interpolate 的bug。

三、根本原因:CPU bilinear_interpolate 边界检查缺陷

3.1 漏洞代码分析

torchvision 0.16.2 中CPU版的 bilinear_interpolate 函数存在以下边界检查缺陷:

// torchvision 0.16.2 CPU bilinear_interpolate (简化伪代码)
template <typename T>
static inline T bilinear_interpolate(
    const T* input, int height, int width,
    T y, T x)              // ← y, x 为浮点数
{
    // 边界检查条件
    if (y < -1.0 || y > height || x < -1.0 || x > width) {
        return 0;          // 仅挡 y < -1 或 y > height
    }

    // 强制转换 —— ★ 问题所在
    int y_low = (int)y;    // 将float转为int
    int x_low = (int)x;

    // clamp —— 编译器可能优化掉
    if (y_low >= height - 1) { y_low = height - 1; }
    if (x_low >= width - 1)  { x_low = width - 1;  }

    // ★ 越界访问发生点
    // 当y为极端浮点值(NaN/Inf/被stride&padding放大后的中间值)时,
    // (int)y 的转换在C++中属于未定义行为(UB),
    // y_low可能远超 height-1 的clamp范围
    T v1 = input[y_low * width + x_low];     // ← 越界访问发生点
    ...
}

3.2 漏洞三要素

  1. 边界检查不完整: 仅检查 y < -1.0y > height,但并未覆盖所有导致整数转换出问题的浮点场景(如NaN、Inf、被中间计算放大后的合法范围内的极大值)
  2. 整数转换的未定义行为: int y_low = (int)y 这行转换在 y 为极端浮点值时属于C++标准的未定义行为。例如 NaN 转 int 的结果是未定义的,部分编译器可能产生极大正值
  3. 编译器优化陷阱: 虽然后续有 if (y_low >= height - 1) 的clamp语句,但由于之前的转换已是UB,编译器在优化时可能将整个clamp代码消除或产生不可预期的值,最终导致 input[y_low * width + x_low] 访问越界内存
关键理解: 正常的forward过程中,offset受sigmoid和卷积核范围约束,一般不会产生极端值。 但在FLOPs估算阶段,Profiler使用的是未经训练的随机初始化权重,MPCA注意力模块(DCNv2_Offset_Attention)生成的offset值可能被stride/padding组合放大到异常范围,恰好触发边界检查的盲区。

四、为何第二次模型创建才崩溃

4.1 两次模型创建过程

在整个实验流程中,模型实际上被创建了两次:

次序触发时机nc参数目的是否触发FLOPs
第一次RTDETR.__init__("rtdetr-attention.yaml")80 (默认)创建架构模型,打印参数量和FLOPs触发,打印 53.5 GFLOPs
第二次trainer.get_model(cfg=..., nc=10)10 (数据集类别数)创建训练模型,匹配实际类别数触发,但崩溃

两次模型创建都会触发 self.info()get_flops(),都会在CPU上执行 deepcopy(model) 的前向传播做FLOPs估算。奇怪的是第一次成功了,第二次才崩溃。

4.2 内存布局差异:静默破坏 vs 崩溃

bilinear_interpolate 中的越界访问并不一定会触发系统内核的报错,属于"静默内存破坏"(Silent Memory Corruption)。是否产生内核报错,取决于越界地址是否落在已映射的合法内存页中:

  • 越界到已映射内存: 读取到垃圾数据,程序继续执行,不崩溃(静默破坏)
  • 越界到未映射内存页: 触发SIGSEGV / 0xC0000005,进程被内核终止

PyTorch的 tensor 内存分配器(cpu_allocator)使用内存池(memory pool)策略

  • 分配块通常大于tensor的逻辑大小(预分配缓冲)
  • 相邻tensor的分配、释放顺序都会影响内存池的碎片化状态
  • 第一次创建模型时:堆较空,越界更大可能落在已分配的大块内存页内,只产生静默破坏,但程序不崩溃
  • 第二次创建模型时:内存被分配到与第一次紧邻的区域,可用空间被挤压在不同内存段中,越界时更容易踩到内存池未分配的页,导致内核切断进程

换言之,第一次模型创建过程中的越界访问已经破坏了内存布局(虽然未崩溃),使得第二次模型创建时更容易发生非法访问触发内核保护。

结论: 由于这个bug与内存布局高度相关,崩溃一定只会在第二次模型创建时出现。这是该bug的一个显著特征——可以通过"第一次成功、第二次崩溃"的模式快速定位。

五、为什么FLOPs估算仍能完成

5.1 thop.profile() 工作原理

第一次模型创建时,虽然CPU bilinear_interpolate读取到了垃圾数据,但FLOPs估算(53.5 GFLOPs)仍然正确完成。这是因为:

thop.profile() 的工作原理是在模型各层注册前向hook,拦截每个算子的调用,统计其理论乘加次数(MACs)。统计只依赖于算子类型和输入tensor的shape,与tensor中的具体数值完全无关。

# thop.profile() 的简化工作原理
def profile(model, inputs):
    # 1. 为模型中每个子模块注册 forward hook
    # 2. Hook函数在每次算子执行前捕获输入tensor的shape
    # 3. 根据算子类型(Conv2d, BN, Linear...)和shape查表计算MACs
    # 4. 累加所有算子的MACs → 得到总FLOPs
    # ★ MACs计算只需要 shape,不需要数值结果
    model(*inputs)  # 执行前向传播 (可能产生垃圾数值,但不影响统计)

因此,即便 bilinear_interpolate 越界读取到垃圾数据,只要前向传播流程完整走完(未崩溃),thop.profile() 就能基于各层的shape正确统计出FLOPs。第一次模型创建时恰好落在"越界但不触发内核保护"的内存区域内,所以FLOPs估算成功完成。

六、排查过程

6.1 初步怀疑:AMP混合精度

最早怀疑崩溃与PyTorch自动混合精度(AMP)有关——AMP在某些算子组合下可能导致数值不稳定或精度损失。排查过程如下:

  • ultralytics/cfg/default.yaml 中设置 amp: False
  • 在trainer中确认 torch.cuda.amp.autocast(False) 已禁用混合精度
  • 所有tensor本身就是 float32,.float() 在float32 tensor上是无操作(返回相同tensor)

结论:AMP不是原因。 这也不改变模型结构、参数值、前向计算或梯度流。但它也不能解决CPU bilinear_interpolate的问题

6.2 隔离测试的局限性

在正式训练之前,曾对各算子(DCNv2_Dynamic forward、MCCA注意力、ALPE编码器等)做过独立的可行性测试。这些隔离测试中:

  • 上下文干净(单独创建模型实例,无内存池碎片化)
  • 操作序列简单(单个forward,无两次模型创建)
  • 恰好满足了"越界到已分配页"的条件,未崩溃

因此隔离测试无法暴露该问题——这是典型的"Heisenbug"类问题,只在特定内存布局条件下触发。

6.3 GDB精确定位

最终,通过在ArchLinux下使用GDB attach到Python进程并设置 handle SIGSEGV stop,精确捕获到崩溃时的调用栈,定位到 torchvision/_C.so 中的 bilinear_interpolate 函数,从而确认了真正的根因。

七、解决方案

7.1 方案一:CPU → GPU自动迁移旁路

问题的根本成因是 torchvision 0.16.2 的CPU版 bilinear_interpolate 实现有缺陷。虽然难以直接修正这个C++函数的源码,但可以通过拦截任务并交给GPU执行的方式来旁路这个bug:

# 在 DCNv2_Dynamic.forward() 中, 将 deform_conv2d 调用包装为GPU安全版本

def deform_conv2d_safe(x, weight, offset, mask, bias,
                        stride, padding, dilation,
                        groups, deformable_groups, use_mask):
    """安全版 deform_conv2d: 自动从CPU迁移到GPU以避免torchvision CPU bug"""
    if not x.is_cuda and torch.cuda.is_available():
        # 临时将所有输入迁移到GPU
        x_gpu = x.cuda()
        weight_gpu = weight.cuda()
        offset_gpu = offset.cuda()
        mask_gpu = mask.cuda()
        bias_gpu = bias.cuda() if bias is not None else None

        # 在GPU上执行deform_conv2d
        result = torchvision.ops.deform_conv2d(
            x_gpu, offset_gpu, weight_gpu, bias_gpu,
            stride=stride, padding=padding, dilation=dilation,
            mask=mask_gpu
        )

        # 将结果拷回CPU
        return result.cpu()

    # GPU上或CUDA不可用时直接调用
    return torchvision.ops.deform_conv2d(
        x, offset, weight, bias,
        stride=stride, padding=padding, dilation=dilation, mask=mask
    )

核心思路:当检测到输入在CPU上且CUDA可用时,将 X, offset, mask, weight, bias 临时移至GPU运算,结果再拷回CPU。这样完全旁路了 torchvision 0.16.2 的 CPU bug。

为什么GPU版本没问题?
torchvision的CUDA版 deform_conv2d 和其内部的bilinear_interpolate是独立的CUDA kernel实现(bilinear_interpolate_kernel.cu),拥有正确的边界检查和clamp逻辑,不受CPU版漏洞影响。

7.2 方案二:接口安全化

同时将代码中所有对底层算子的直接调用替换为更安全的Python层接口:

不安全调用安全替代差异
torch.ops.torchvision.deform_conv2d(...)torchvision.ops.deform_conv2d(...)Python层有额外的参数校验和shape检查

torchvision.ops.deform_conv2d() 是Python封装层,在执行C++ kernel之前会进行额外的输入参数验证(如device一致性检查、shape合法性检查),提供了一层额外的安全性。

7.3 最终方案

本次修复中采用了方案一(CPU→GPU 自动迁移旁路)配合方案二(接口安全化)的组合,在不升级 torchvision 版本的前提下解决了问题。由于使用的 RTX 4070 Ti SUPER 需要 CUDA 12.1,而 torchvision 0.21.0+ 依赖更高版本 CUDA/PyTorch 生态系统,升级引入的变量过多,当前组合最为稳定可控。

长期建议: 如果条件允许(如使用 RTX 3090 或更新 CUDA 工具链),升级 torchvision 到 0.21.0+(需同时升级 PyTorch 到 2.6.0+ 和 CUDA 到 12.4+)是最彻底的修复方案。torchvision 0.21.0+cu124 版本在此场景下完全稳定,无需任何 workaround 即可正常完成训练。

八、总结

本篇记录了一次MDCA-DETR论文复现训练过程中遇到的内存越界崩溃问题的完整排查过程。核心发现如下:

  1. 崩溃时机: 模型训练即将开始的那一瞬间,model.info()get_flops() 在CPU上运行 deepcopy(model) 的前向传播来估算FLOPs
  2. 崩溃位置: DCNv2_Dynamic.forward() 调用 torchvision 的 deform_conv2d CPU 实现,其内部的 bilinear_interpolate 函数发生内存越界
  3. 根本原因: torchvision 0.16.2 的CPU版 bilinear_interpolate 边界检查不完整,(int)y 的整数转换在极端浮点值下产生未定义行为,导致内存越界访问
  4. 两次创建的差异: PyTorch内存池分配策略导致两次模型创建时内存布局不同,第一次越界到已分配页(静默破坏),第二次越界到未映射页(系统报错)
  5. FLOPs估算不受影响: thop.profile() 的FLOPs统计基于tensor shape而非数值,越界读到的垃圾数据不影响统计结果
  6. 解决方案: 采用 CPU→GPU 自动迁移旁路 + 接口安全化(torchvision.ops.deform_conv2d 替代 torch.ops.torchvision.deform_conv2d)的组合修复;长期建议升级 torchvision 到 0.21.0+(根本修复)
  7. 跨平台一致性: Windows下的 0xC0000005 (Access Violation) 和 ArchLinux下的 SIGSEGV (Signal 11) 本质上是同一件事——操作系统对越界内存访问的内核级保护响应
教训: 隔离测试的通过不能保证集成环境的稳定。内存类bug(尤其是涉及C++底层算子的bug)往往依赖于运行时的内存布局,具有Heisenbug特性——隔离测试时"恰好"落在安全的越界位置,而集成运行时内存池碎片化才暴露真正的问题。