毕业论文idea随手记

毕业论文idea随手记

工作量思路

历史特征引导

覆盖率引导(这个可能不太好搞)

大模型生成


20260516

最近读了 AlignGuard 这篇工作,该工作分析了 torch.compile 的一百多个 issue,总结了一些容易致错的模式,将它们转化成了 (动作, 条件) 对(结构化自然语言),利用这个东西去引导对种子程序进行变异,使得变异后的程序尽量包含致错模式。其中,种子程序是从 Pytorch 官方的测试代码中扒出来的,这部分代码有 1.2w 行,总量其实是很大的。

这给我们一些启发,首先就是基于致错模式去生成类似的测试用例,是真的能再测出 bug 的。然后,如果你的变异器或者生成器是基于 LLM 的,那么变异规则用自然语言描述其实是自然的,特征用自然语言描述也是自然的;如果你的变异器/生成器是基于其他自己写的模型,那么引导信号可能就得用特征向量了,变异规则以及触发条件可能也要用一些可计算的量来表示。

目前,参照 AlignGuard 的做法,我从 pytorch 官方的测试代码中提取出了一个索引,并且实现了根据索引去具体拿里面某个测试用例,不过这个测试用例如果单拿出来这几行是没法跑的,我其实很好奇 AlignGuard 是怎么把提取出的测试用例变得独立可跑的,难道是手动改的吗?

pytorch 的 issue 我爬取了 1000 条和 torch.compile 相关的,通过看这些 issue 中是否包含我关心的某些信息,给这些 issue 进行了打分,满分 7 分,5 分以及以上的我筛出来 200+ 个,并使用 LLM 进行了初步分析和标注,后边会对这些 issue 进行逐个分析,整理成结构化的东西,我看了看前几个,觉得这几个 issue 确实挺不错的。

FreeFuzz 是给所有的调用外面套了一个装饰器,从而能够采集到代码运行时的调用信息,比如某个算子的所有参数的形状之类的,从而得到的所谓的”真实程序的调用轨迹“。每次挑选一个真实的调用轨迹,这个东西当成是种子,然后对里面的参数做变异(比如从别的轨迹里面找一个名字一样的参数,把它的值拿过来),以及类型变异之类的,对单个 API 测试,也能测出来不少问题。它的代码里面处理了很多脏活累活,可以作为参考。


20260528

目前已经提取了 test_torchinductor.py 这个测试代码中的 200+ 个可以单独执行的测试用例了,测试用例大概的格式是:

引入各种依赖

# 定义结构
def fn():
	...
# 定义参数
def build_inputs():
	...
	
def run():
	# 运行两种模式并比对,手写了一套比对逻辑
	
def main():
	# 运行测试

其中 fnbuid_inputs 是直接提取的,前面的引入依赖和后边的执行算是模板代码。

fnbuild_inputs 直接提取也未必能用,我先分析了直接提取之后的里面,那些不能跑的到底报了什么错,然后发现名字未定义的错误最多,就先写了一个只解决这个问题的修复脚本。我是觉得这种报错原因应该比较有限,可以分成几个类,所以分类去分别修复就行,可能用不到大模型去做。

和 DeepSeek 聊了聊,目前得到一个初步的对测试用例进行变异的提示词模板,非常粗糙,我们后边需要仔细去打磨一下,看看提示词怎么写:

你是一个深度学习框架测试用例变异器。给定一个PyTorch测试用例,请按照以下要求生成变异版本:

1. **原始代码**:
​```python

​```

2. **变异规则**(按优先级选择1-2个应用):
   - 算子替换:将当前算子替换为功能相似但数值特性不同的算子(如 avg_pool -> max_pool,或 adaptive_avg_pool1d -> avg_pool1d)
   - 参数变异:修改算子参数(如 output_size、kernel_size、stride等),在保持合理取值范围的情况下,可以尝试极限值和边界值
   - 张量形状变异:调整输入张量的某维度大小(如 4x4x3 -> 8x4x3),保持维度兼容
   - 数据类型变异:改变dtype(float64 -> float32 或 bfloat16)
   - 插入无关操作:添加 view/reshape/contiguous 等不影响最终结果但可能改变内存布局的操作
   - 操作顺序变异:调整`fn`内部语句顺序(若语义允许)
   - 等价语句替换:如 torch.argmax(x) -> torch.max(x)[1]

3. **约束**:
   - 生成的代码必须是完整、可直接运行的Python脚本,不能有语法错误
   - 保持 `fn` 的输入输出类型一致性
   - 变异后的 `fn` 应当与原函数**在数学上不完全等价**(目标:暴露框架差异)
   - 保持 `build_inputs` 返回的 tensor shape 与 `fn` 期望的输入兼容

4. **输出格式**:
   - 只输出变异后的完整代码,不要添加解释
   - 代码开头添加注释说明应用了哪些变异规则

请开始变异。

通过讨论,我们觉得需要构建一个算子语义知识库,辅助 LLM 选择变异后还能正常运行的算子(最稳妥的可能是选一个性质差不多的算子),不能有语法错误或者明显的语义错误。

为了引入历史的经验,还需要构建致错模式知识库,使用这个知识库可以在变异时朝着特定的模式去变异。

由于大语言模型是语言模型,所以用自然语言去指导它可能是更好的,扔给它结构化数据可能并非很好的策略。我们存知识库的时候是结构化的存的,但是每条最好都给一个固定的自然语言描述,投喂给大模型用的是这个自然语言描述,但知识库检索可以检索结构化的那部分。

用自然语言引导 LLM 做事这一步我们都知道,由于我们不练模型,所以我们能做的就是设计好的提示词就可以了。

现在的问题就是,怎样在知识库里检索到要用什么知识呢?这大概需要我们算一些即将给大模型的测试用例的度量,比如是否包含某类的算子、算子个数、连接方式、参数边界特征、形状特征、是否 in-place 之类的。用这样一个“特征向量”去查库,找到适合使用的知识,然后构建到提示词里去做变异。

如何算这个“特征向量”呢?有一些是可以静态分析的,有一些可能不太好搞。

特征向量算好了,如何匹配呢?这个就是要研究匹配算法了。

上面有一个问题可能是工程味道太浓了,写文章不能这样写。deepseek 给出了一个可能的写文章的目录:

3.1 问题定义与形式化(2页)
   3.1.1 测试用例变异问题形式化
   - 定义测试用例 T = (f, I, oracle)
   - 定义变异操作 M: T → T'
   - 定义变异有效性:语义改变 & 语法正确
   
   3.1.2 模式驱动变异的形式化
   - 定义模式 P = (trigger, action, weight)
   - 定义模式感知变异:T' = M(T | P)
   
   3.1.3 研究问题分解
   - RQ1: 如何构建可被LLM理解的知识库?
   - RQ2: 如何为给定测试用例检索相关知识?
   - RQ3: 如何利用检索到的知识引导LLM变异?

3.2 方法概述(2页)
   3.2.1 整体框架
   - 一张大图:离线阶段 vs 在线阶段
   - 四个模块:知识库构建、特征提取、知识检索、LLM变异生成
   
   3.2.2 工作流程
   - 步骤1-5的文字描述 + 数据流图
   
   3.2.3 设计原则
   - 分离存储与使用
   - 规则驱动检索,LLM驱动生成
   - 闭环反馈优化

3.3 双知识库的离线构建(4页)
   3.3.1 知识表示设计
   - 结构化表示(用于检索):算子字段、触发条件、权重
   - 自然语言表示(用于LLM):指令模板、代码示例
   - 两种表示的映射关系
   
   3.3.2 算子语义知识库构建
   - 知识来源(PyTorch文档、源码、社区bug报告)
   - 知识抽取方法(规则抽取 or LLM辅助标注)
   - 知识条目示例与统计
   - 质量保证(人工校验、交叉验证)
   
   3.3.3 致错模式知识库构建
   - 模式来源(历史bug分析、文献调研)
   - 模式分类体系(算子连接/参数边界/数值不稳定/动态shape陷阱)
   - 模式优先级与权重初始化
   - 知识条目示例与统计
   
   3.3.4 知识库规模与覆盖度分析
   - 覆盖了多少PyTorch算子
   - 覆盖了多少已知bug类型

3.4 测试用例特征提取(3页)
   3.4.1 特征设计
   - 特征分类:算子级、序列级、参数级、数值级、动态级
   - 特征向量定义 F = (f1, f2, ..., fn)
   - 特征取值空间(布尔/枚举/数值)
   
   3.4.2 静态分析提取方法
   - AST遍历算法
   - 算子识别规则表
   - 参数边界检测规则
   - 连接顺序提取算法
   - 动态shape变量识别
   
   3.4.3 复杂特征处理策略
   - 数据流依赖的处理(可选,可降级)
   - 跨函数调用的处理(可忽略)
   - 准确性与效率的权衡
   
   3.4.4 特征提取示例
   - 用一个具体测试用例,展示提取出的特征向量

3.5 基于特征的知识检索与匹配(4页)
   3.5.1 检索问题形式化
   - 给定特征向量F和知识库K,找到最相关的知识子集K* ⊆ K
   - 目标函数:max relevance(K*, F)
   
   3.5.2 两阶段匹配算法
   - 第一阶段:布尔条件过滤(粗筛)
   - 第二阶段:加权打分排序(精排)
   - 算法伪代码(Algorithm 1)
   
   3.5.3 打分函数设计
   - 匹配度计算:score = Σ(wi × I(cond_i satisfied))
   - 历史权重融合:final_score = α·match_score + (1-α)·historical_weight
   - 参数α的确定方法
   
   3.5.4 检索结果多样性保证
   - 避免返回相似度过高的知识
   - Top-K策略(K=2或3)
   - 多样性惩罚项(可选)

3.6 基于检索增强的LLM变异生成(3页)
   3.6.1 变异生成流程
   - 流程图:检索结果 → 提示词构建 → LLM生成 → 验证 → 反馈
   
   3.6.2 提示词模板设计
   - 模板结构(角色/任务/知识/约束/输出格式)
   - 知识注入方式(自然语言+示例)
   - 不同知识类型的提示词变体
   
   3.6.3 执行反馈与迭代修复
   - 验证器设计(编译/运行/等价性检查)
   - 错误分类与反馈消息生成
   - 修复策略与重试上限
   
   3.6.4 变异生成示例
   - 输入测试用例 → 检索到知识 → 构建提示词 → LLM输出 → 验证通过

3.7 反馈驱动的知识库权重更新(3页)
   3.7.1 更新问题形式化
   - 目标:让更有效的模式获得更高权重
   - 在线学习框架
   
   3.7.2 基于Beta分布的贝叶斯更新
   - Beta分布建模模式有效性
   - 更新公式:α ← α + success, β ← β + failure
   - 权重计算:weight = α/(α+β)
   
   3.7.3 探索与利用的平衡
   - ϵ-greedy策略
   - 新模式的探索机制
   
   3.7.4 收敛性与稳定性分析
   - 权重更新曲线示例
   - 避免过拟合的策略

3.8 算法总结与复杂度分析(2页)
   3.8.1 主算法伪代码
   - 完整的变异生成算法(Algorithm 2)
   
   3.8.2 复杂度分析
   - 特征提取:O(|AST|)
   - 知识检索:O(|K|)
   - LLM生成:O(L)
   - 总体复杂度
   
   3.8.3 方法局限性讨论
   - 静态分析的局限
   - LLM的不确定性
   - 适用场景边界
   
   
第4章 实验验证

4.1 实验设置
   - 数据集:从PyTorch测试套件中选取200个种子用例
   - 基线:随机变异、零样本LLM
   - 评价指标:生成成功率、语义差异率、覆盖率提升、平均耗时

4.2 研究问题
   RQ1:我们的方法能否生成更多可运行的变异用例?
   RQ2:我们的方法能否产生更大比例的语义差异?
   RQ3:我们的方法能否提升测试覆盖率?
   RQ4:知识库各组件(算子库、模式库)的贡献如何?

4.3 RQ1结果:生成成功率
   - 结果表格:方法 vs 基线
   - 分析:为什么我们的方法更好(知识库避免语法错误)

4.4 RQ2结果:语义差异率
   - 结果表格 + 典型案例展示
   - 分析:模式库定向变异的效果

4.5 RQ3结果:覆盖率提升
   - 分支覆盖率/行覆盖率对比
   - 分析:哪些类型的代码被新覆盖

4.6 RQ4结果:消融实验
   - 去掉算子库 vs 去掉模式库 vs 完整方法
   - 分析:两个库各自贡献

4.7 案例研究
   - 展示2-3个成功触发已知bug的案例

4.8 讨论与局限性
   - 方法的适用范围
   - 为什么没有发现新bug(可能:数据集小、时间有限、PyTorch已经很稳定)

关于如何尽可能增大测出 bug 的概率,和 deepseek 聊过之后,有下面一些实验上的建议。首先是看看有没有现成的 bug 数据集,看看能不能测出来已知的 bug。然后是可以聚焦在某个小集合的算子上,专门去测这些,可能会更好。还有就是选好的种子,比如 PyTorch 官方的单元测试代码,目前只搞了 test_torchinductor.py 里的种子,其实还可以挖。

变异规则可以是偏宽泛的通用的规则(请修改xx参数),也可以是具体的规则(请将xx参数改成 1),具体的规则更可能触发已有的 bug,可能实验效果上会更好。

目前这个提示词,明显无法增加子图结构,充其量是做一些算子的替换以及 API 参数的替换。要想修改图结构,我个人的感觉是有三个方面的做法:

  1. 基于规则变异/生成。大概就是 reborn2 以及 NNSmith 那套做法,就没有大模型什么事儿了。
  2. 种子就足够复杂,这样就不变异结构了,只替换算子或者改参数。复杂的种子可以靠其他的生成器生成?比如 reborn2?通过修改他们的参数,得到较为复杂的种子程序。或者靠官方的复杂测试用例?
  3. 参照模板插入。这个大概就是我们设计一些变异模板,比如某个 f(a, b, c) 可以通过怎样的 a = g(x, y), b = h(z, w), c = 1 之类的东西拼出来,然后 x, y, z, w 之类的再递归下去,看看是直接赋值还是再调用算子。这个模板的意义在哪里?在于让大模型知道这样变异是合法的?

如果要有变异模板的话,那么就应该有一个类似子图模式库的东西了。这样大概需要按照下面的方式去协作:

flowchart TB
    subgraph Input["输入"]
        CODE[原始测试用例]
    end

    subgraph Phase1["阶段1:代码理解"]
        FE[特征提取]
        OS[算子语义库]
        FE --> OS
    end

    subgraph Phase2["阶段2:策略决策"]
        FPM[致错模式库]
        DECIDE{选择变异方向}
        FE --> FPM
        FPM --> DECIDE
    end

    subgraph Phase3["阶段3:代码生成"]
        TEM[子图模式库]
        LLM[大语言模型]
        DECIDE --> TEM
        DECIDE --> LLM
        OS --> LLM
    end

    subgraph Output["输出"]
        RESULT[变异代码]
    end

    TEM --> RESULT
    LLM --> RESULT

三个库各自的定位:

flowchart TB
    subgraph L1["第1层:算子语义知识库"]
        O1["算子功能"]
        O2["输入输出shape规则"]
        O3["参数类型/范围"]
        O4["dtype兼容性"]
        O5["可替换候选"]
    end

    subgraph L2["第2层:致错模式库"]
        F1["算子连接型:池化→索引"]
        F2["参数边界型:output_size=1"]
        F3["数值不稳定型:小除数"]
        F4["动态shape陷阱型:ceil边界"]
    end

    subgraph L3["第3层:子图模式库"]
        S1["残差连接插入模板"]
        S2["算子重排模板"]
        S3["激活函数替换链模板"]
        S4["Conv-BN融合模板"]
    end

    O1 --> F1
    O2 --> F1
    O3 --> F2
    O5 --> S1
    
    F1 --> S1
    F2 --> S2
    F3 --> S3
    F4 --> S4

    style L1 fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
    style L2 fill:#fff3e0,stroke:#e65100,stroke-width:2px
    style L3 fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px

这样似乎更乱了


20260625

特征库 x 种子库 = 变异产物,所以特征库和种子库都要扩大,然后特征库与种子库的配对算法也要好一些。

目前是有 cuda 环境的,但是之前做的实验全是纯 CPU 的,这个事情后边可以去做一下。

目前只是用 LLM 去对种子库 x 缺陷特征库触发问题这个事情进行摸底。

特征库想要施加在种子库上,是想先看到种子库的某些残缺的特点,匹配上了再施加,但问题是在种子库里似乎找不到这种残缺的特点。200 种子和 100 特征,最后只能匹配生成出来 49 个用例,未免也太少了。

生成出来的东西,有的是照抄了特征里的复现代码,有的稍有泛化性。

agent 可以极大地加速实现代码,所以现在的关键在于,想清楚你想做什么,想实现什么代码,想清楚了之后再让 agent 去干,不然你的研究会很乱,不知道在干什么。


20260701

已经到 7 月了,我们希望在接下来半年里完成这个工作,并把师兄的工作投出去,一定要在 2026 年完成这个事情,2027 年我就专注于搞竞赛事业了。

不管是从 0 开始生成,还是在种子上改,我们的目的在于,生成出我们想要的包含预期特征的测试用例,所以不用拘泥于种子。改种子或者从 0 开始组合,是手段,不是目的。

我们应该考虑特征建模这个事情了。Taxonomy(分类体系)

特征建模,其实就是对缺陷机理的编码。

编码范围是有限的,每种编码可能能够产生不同实例,但不是每种编码都有意义。

一个 bug,它可能有很多特征,组成一个特征向量,只有确切地满足这个特征,才会走到某个位置,触发某个 bug。

特征建模要考虑哪些方面呢?

我的题目提到了计算图缺陷特征,所以肯定要从计算图结构的角度说一说,比如算子的组合,然后单个算子可能也会有那种比较有说法的,或者说我们统计到缺陷中出现某个算子时可能就容易出问题(可能主要还是组合的事情)

参数的角度肯定也要说一说,首先是参数类型,比如一些精度问题好像就是参数类型导致的,然后就是参数的具体值,比如特殊值,极限值之类的

执行后端上肯定也有关系,比如 CPU,不同型号的 GPU,国产的可能是一个突破口呢。

执行模式,eager 或者 compile,走的底层路径是不一样的。

我相信只靠这几种特征肯定是有一定用处的,但我们还是要再多想一些特征。

我们似乎还要为 pytorch 的算子做一个分类

我们来仔细研究一些用例吧,先从师兄之前做好的用例看起:

import torch

print(torch.__version__)

sym_0 = (8, 2, 1, 1)
sym_1 = torch.float32
sym_2 = torch.device("cpu")
sym_3 = 0
sym_4 = True
sym_5 = (9223372036854775807, 5868783964474102731)
sym_6 = (-1, 3010182406857593769)
sym_7 = (0,)
sym_8 = True
sym_9 = True
sym_10 = 33554427

var_546 = torch.randn(size=sym_0, dtype=sym_1, device=sym_2)
var_124 = torch.ops.aten.alias(var_546)
var_360 = torch.argmax(var_124, dim=sym_3, keepdim=sym_4)
torch.nn.functional.avg_pool2d(var_360, kernel_size=sym_5, stride=sym_6, padding=sym_7, ceil_mode=sym_8, count_include_pad=sym_9, divisor_override=sym_10)

对于这个用例,师兄的解释是,sym_5 的值超过了 int 范围,这个事情在 python 中是合法的,但是在 C++ 中存不下,溢出成了负数,也没有校验负数,所以会出问题。

这个用例的核心在于 sym_5 太大了,并非是因为计算图本身很复杂。

再看一个:

import torch

def f(*args):
    sym_5, sym_6, sym_7 = args

    var_279 = torch.quantize_per_tensor(torch.randn((100,)), scale=sym_5, zero_point=sym_6, dtype=sym_7)
    var_374 = torch.flip(var_279, dims=(0,))
    return var_374

res = f(3., 10, torch.quint2x4)
print(res)

师兄说,flip 算子底层不支持 quint2x4 或者 quint4x2 这种类型的张量。这个我的理解就是数据类型与算子组合共同作用下导致的,这个数据类型的精度很低,内存排布较为特殊,所以 flip 算子可能并不支持这种数据类型的。

通过和 LLM 交流,他给了一些可能值得关注的特殊数据类型:

从历史经验和实现原理来看,以下几类数据类型有可能出现问题,需要进一步验证。

1. 打包格式(Packed Formats)

这类数据多个元素挤在一个存储单元里,最危险:

quint2x4(2-bit,每字节4个):你刚遇到的案例

quint4x2(4-bit,每字节2个):类似问题

qint4x2(有符号版):同理

其他自定义的低比特打包格式

为什么危险:绝大多数视图操作(flip、slice、transpose、permute)都假设"一个元素占据连续的固定字节数",遇到打包格式就直接崩。

2. 复数/向量类型

complex64/ complex128:实部虚部捆绑在一起,有些算子只当成两个独立的 float 处理

torch.quaternion​ 等自定义复合类型:类似问题

3. 稀疏格式相关

稀疏 COO 格式的索引:indices和 values分开存储,常规算子不知道要同时处理两部分

CSR/CSC 格式:三个数组耦合,切片或翻转时容易只动其中一个

4. 特殊步长(Strided)的非连续张量

虽然不是新数据类型,但非连续内存布局也容易踩坑:

t.T转置后的张量(步长反序)

expand扩展后的张量(步长为0)

as_strided手动构造的奇怪步长

为什么危险:很多算子只在连续内存上做过充分测试,遇到非连续布局就走到了未覆盖的分支。

5. 混合精度组合

AMP(自动混合精度)​ 场景下:同一个计算图中 float16 和 float32 混用,某些算子只对单一精度做了优化

量化 + 浮点混合:比如量化卷积后面接浮点激活函数,边界处的类型转换可能有 bug

我们后边在测的时候,其实可以考虑优先生成上面这种数据类型的用例。

下一个用例:

import torch

@torch.compile
def f(input):
    var_17 = torch.slice_copy(input, dim=0, start=449, end=None, step=9223372036854775807)
    return torch.reciprocal(var_17)

input = torch.randn((875,))
res = f(input)
print(res)

这个师兄说因为 step 非常大,导致了 slice_copy 在内部实现中,进行涉及到张量形状计算的代码时溢出,导致出现负数了,这导致 eager mode 和 compile mode 执行不一致,前者是空张量,后者是段错误,修复方案是计算形状时小心避免溢出。

这个用例给我们的启示是,pytorch 内部实现时可能确实存在很多这种溢出的问题,它可能事先觉得不会传那么大的数,所以实现时不够小心,才会遗留这样的问题。

下一个用例:

import torch

@torch.compile
def f(*args):
    sym_0, sym_1 = args
    return torch.randint(high=sym_0, size=sym_1)

res = f(0, (3960,))

师兄说,randint 用于生成 [from, to) 的范围的随机整数,此处传入的 from = to = 0,是未定义的,框架没有对 from >= to 的情况进行检查,所以造成了这个问题,这个问题在 eager mode 下会 runtime error exception,但是在 compile mode 下会框架内部崩溃,也就是说 compile mode 没有正确处理。

但我有一个疑惑,为啥 eager mode 能拦截,compile mode 拦截不住呢?

LLM 的解释是:eager 能拦截,是因为 ATen 在执行路径上有 runtime check;compile 往往走 Inductor 生成的另一条路,没有把同等校验保留进编译图或内核,非法输入就从可预期的异常变成未定义行为/崩溃
这不是 randint 特例,而是 compile 边界处理一整类问题的缩影。

这个还是很有启发的,这揭示了一类 bug 的出现原因。

我们再看一下下一个用例:

import torch

@torch.compile
def f(*args):
    var_92, sym_1 = args
    return torch.bucketize(sym_1, var_92)

var_92 = torch.randn(size=(1000,))
res = f(var_92, 1)
print(res)


import torch

@torch.compile
def f(*args):
    boundaries, value = args
    sorted_boundaries = torch.sort(boundaries).values
    return torch.bucketize(value, sorted_boundaries)

var_92 = torch.randn(1000)
res = f(var_92, 1)
print(res)

首先我可能要理解一下这个用例,这个用例是说生成了 1000 个随机数,排序后建立桶,然后问 1 这个数在哪个桶里,这个用例在 compile mode 下会递归超过 Python 最大限制而崩溃,eager mode 下正常。

改了一下这个代码,如果 size 小一点,compile mode 就能跑出来。

这说明了 compile mode 的优化算法,可能没有考虑到优化本身的时空限制,以至于优化过程会被极端情况卡爆,优化算法本身的鲁棒性也很重要。

然后我发现,利用 Agent,其实可以在用例上进行进一步的改造,比如改改某些参数,改改类型,去尝试到底是什么情况下导致的这个 bug。这其实是一个好东西的,假如我们设计出这样一个本质特征提取 agent,可以省很多事情,不用手动设计 delta debugging 策略了,agent 直接无敌了。

pytorch 不断更新,我们其实也可以看加了什么新的 feature,然后猛测新增的这些东西。

pytorch 多版本,我们现在用 v2.5.1 版本,目前还是用的人很多的,不用过于新的版本,防止复现不出来 bug。

我们接下来看师兄重点分析的最后一个 bug:

import torch

@torch.compile
def f(*args):
    sym_0, sym_1, sym_2 = args
    
    var_540 = torch.ones(size=sym_0)
    return torch.diff(var_540, n=sym_1, dim=sym_2, prepend=None, append=None)

res = f((3505,), 30, 0)
print(res)

对一个张量进行差分时,如果是在 compile mode,且差分阶数比较高(比如 30)时,会跑非常久,比如 30 时跑 500 秒都出不来结果,20 时需要跑 4 秒,但 eager mode 很快,这是一个显著的算子性能下降问题。对这个 issue 的修复的 commit 非常复杂,我暂时还看不出来这个 bug 的根因,以及到底怎么改的。

我们其实是不知道这个 bug 的根因的,但我们还是要从这个用例中提取一些有用的特征,这个应该如何做呢?

LLM 对于特征,进行了分层:

层级 问什么 不知道根因时能做吗
L0 现象 eager/compile 各怎样?慢/崩/数值错? ✅ 必做
L1 触发面 动哪个 API、哪个参数、什么规模? ✅ 必做
L2 抽象 pattern 用 taxonomy 标签概括(不含实现细节) ✅ 能做,允许带 ?
L3 根因 哪条 pass、哪段 C++、怎么修的 ❌ 可后置,用 issue/PR 补

通过看别人的 issue,我发现精度这块问题很严重啊,尤其是低精度下差距比较大。

然后还看到一些测出了编译器在优化广播语义时出现的问题。

与 LLM 交流了解了一下广播:广播就是让小张量在运算时"假装"变大,省去手动复制的麻烦,但也给编译器留下了犯错的空间。

广播是隐式的

  1. 计算图中没有显式的 expand节点
  2. 编译器需要自己推断哪些维度被广播了
  3. 当广播与条件分支(如 where)结合时,推理变得更加复杂
  4. 编译器可能错误地认为某些维度不需要广播,或者搞错了广播的方向

所以它容易犯错。

这种错误犯了之后一般就是逻辑错误了。

为了触发它,一般要构造能触发这个优化的代码。

有些 eager 和 compile 不一致,可能并非 bug,nn.Hardswish produces wrong output under torch.compile(backend='inductor') · Issue #181693 · pytorch/pytorch,也可能是测试用例设计的不好。

我们需要知道大模型时代 pytorch 哪部分变化幅度很大,然后尽量去测那些部分,这也是一个假设,我们假设这部分因为更新频繁所以 bug 很多。

浮点数相加,交换顺序结果可能不一样,这种不能算是 pytorch 的 bug,你在判断的时候需要注意 oracle 不能太严格了。

Eager 用一种顺序累加(顺序求和),Compiled 用另一种顺序累加(分块求和),这两种都是合法的 float32 计算,PyTorch 目前不承诺让它们产生比特级一致的结果

但是你人工强转精度导致的问题,可能确实是问题。

求和很多的话浮点数会有精度问题,算法竞赛里也有一种浮点数求和方法,加起来再除回去其实是一个可能的 pattern。

索引、切片、掩码类算子似乎很容易出问题呢

用 LLM 稍微总结了一下,我们得到以下的测试假设:

优先级 算子类别 典型代表 bug 类型
🔴 P0 索引/散射/切片写入 slice_scatter, scatter, index_put, gather 梯度全零 / 流向错
🔴 P0 条件分支含广播 where(cond_broadcast, ...) mask stride 错 / 分支反
🟠 P1 masked_fill / masked_select masked_fill mask 常量折叠
🟠 P1 unbind / split / flip / roll unbind, flip 坐标逆映射错
🟡 P2 低精度截断 + 融合 .to(bf16), hardswish 精度差异大(通常非逻辑 bug)
🟢 安全 matmul / conv / 纯逐元素 linear, relu 极少 Inductor 反向 bug

stride 参数好像很重要,stride 参数描述的是跳到下一个元素/下一行/某维的下一个,要在内存里移动多少步。如果某些原因导致 stride 里的值错了,或者你用错 stride 的值了,都会导致出问题。

多个 batch,但是只测第一个 batch,可能测不出来偏移量错误。

[inductor] max returns incorrect indices when compiling a function includes (a + b).max(dim=1) · Issue #178964 · pytorch/pytorch 非连续张量+算子融合似乎很容易测出问题啊?

我们对之前抽取的内容的部分字段进行了词频统计:

=== trigger_ops top 20 ===
  23  torch.add
  17  torch.compile
  12  torch.matmul
  10  torch.mul
   8  flex_attention
   7  torch.einsum
   5  torch.nn.Conv2d
   5  create_block_mask
   4  Tensor.to
   4  torch.div
   4  F.linear
   4  torch.clamp
   4  __getitem__
   4  reshape
   4  nn.Conv2d
   4  torch.randn_like
   4  torch.cat
   4  torch.norm
   4  nn.Linear
   4  nn.LSTM

=== trigger_pattern_coarse top 15 ===
   2  torch.slice_scatter backward under torch.compile
   1  float16 cast before elementwise add
   1  torch.where with broadcast shapes
   1  nn.Hardswish under torch.compile(backend='inductor')
   1  nn.RReLU in eval mode under torch.compile
   1  matmul -> to(bfloat16) -> fp32 add
   1  single-head attention with matmul->div->softmax->dropout->matmul (SFDP pattern 11)
   1  multiple torch.randint calls with heterogeneous shapes/ranges/dtypes
   1  batched einsum with batch-specific weight parameters
   1  multiple F.linear with same input
   1  log2 -> ceil -> clamp -> uint8 quantization pipeline
   1  select → unsqueeze → expand → batch pointwise fusion
   1  explicit fp32->bfloat16->fp32 round-trip cast
   1  (a + b).max(dim=1) with transposed a
   1  einsum with element-wise addition
你对 A1 目前的理解是对的,我们要的就是少量抽象机制类(十几到几十个)+ 多数 issue 映射到已有类。A2 你说的用几个布尔值去描述是否存在此类结构,我觉得是可以的,且符合我之前自己的设想,但就怕这样表达能力不够, A2 肯定是要保留的。

A 和 B 的话,你原来的做法其实有个好处,就是你记录的信息基本上没有什么损失,都是直接从 issue 中提取出来的,现在如果拆开变成只记出现了什么算子,出现了什么类型,感觉会损失一些信息,我不确定这样会不会导致后边使用这个特征时测试结果不好。

我的理解,特征向量就是服务于生成的,FGH 确实不该出现在特征向量中,但是 FGH 这些信息还是要获得的,就像你说的,可能要放别的地方。

你最后给出来的这个特征向量的样子,我个人觉得合理多了。

20260702

我们今天通过和 LLM 聊天,做出了一个重要修正。

我的研究并不是要发现新 bug,而是研究我能不能设计出一种相对全面且相对简洁的测试用例的缺陷特征模型,根据这个模型,我能够刻画绝大部分已有的触发缺陷的测试用例的特征,并利用这些特征去批量地生成测试用例,这些测试用例大概率能够触发已知的 bug(我们称之为稳定触发)。我们不强调能不能发现新 bug,如果最后真测出新 bug 了,我们可以再吹这个事情。

LLM 说,这种总结模式 -> 基于模式生成 -> 证明生成出的东西也能触发问题的方法论是通的,有很多工作是这样研究的,DL 框架测试也有这样搞的,比如有总结优化模式(并非 bug 模式)然后测试的,它那个还测出新 bug 了。我之前文献调研的时候,把这部分给漏掉了,我当时老是想着测出新 bug 了。

为了证明你的方法有效,你当然可以写一个不基于 LLM 的生成器去做,也可以写一个基于 LLM 的,但你最后都得稳定触发那些 bug。另外,如果真是基于 LLM 的,你要证明,不用这套特征,直接让 LLM 瞎生成,它大概率无法再测出来。

LLM 说我们的方法还要做消融,比如把特征向量改了,然后发现测不出 bug 了,说明这一维特征是重要的。

我今天还使用 LLM,理解了一下 reborn2,从中得到了一些设计上的借鉴,总结成了文档,但我还没仔细看。这个事情需要在设计通用的生成逻辑时去看。

一个通用的生成器,它可以通过输入缺陷特征向量,输出一系列可独立运行的 pytorch 测试脚本,这些脚本具有这个特征。特征向量的每一维,都是生成决策的一部分。

通用生成器的背后是我们设计的特征体系。这个东西的价值在于,能够根据我们设计的特征体系,刻画大部分缺陷的特征,并稳定复现大部分缺陷。并且,它能够在一个谓词约束下,做放大测试,可能会有一定的泛化性,这意味着可能能测出来同一个 bug 机制模式下的其他 bug。

这个东西的难点在于,特征体系设计困难,可能会出现设计的特征不好量化取值,或者无效,或者太粗太细,太粗可能复现不了,太细像背答案。生成出的用例的特征完全 100% 特征相同可能较为困难,但至少保证大概率是特征相同的。另外生成的用例本身不能有语法语义问题也是重要的,不然是无效用例。


20260703

机理知识的自动收集,是否也是一个可以做的东西?

漏洞样本生成?


20260730

又过了好久没写这个了,前段时间主要是去忙小论文投递了,现在至少是投出去了,9 月 14 号出结果,希望能中。如果中了,那是最好的,如果没中,下次投递最好是能够一下子投递两篇。

投稿期间,其实有一些调研和思考,现在的 DL 框架测试基本都是基于 LLM 的了,并且基本都没做微调,只是通过各种方式,把一些引导性的信息塞到提示词里,把 LLM 当成一个一次性生成器,生成出来不合法就 retry 几次,合法就不管用例到底是不是真的符合提示词的描述了。

吴老师说,各种各样的信息,每种信息都是一种视图,我们相当于在不同视图里选一个点,这些点相当于高维空间里某个维度的坐标,整个组成测试用例高维空间里的一个点。

把所有已知对测试有帮助的信息(API文档、TORCH_CHECK约束、GitHub Issues、相似API已知bug、代码注释、跨框架对应关系、crash trace、commit message、各类覆盖率(API、python、c++、edge))梳理出来,包括它们之间的相互关系,建一个类似知识图谱/模型的东西。然后研究哪种信息在什么场景下对找 bug 有帮助,需要实验+统计分析来研究。由于信息并不都能完全满足,所以需要一种动态采样模型里的某些特征,来构造测试用例,而不是把所有信息一股脑编码进提示词(可能根本生成不了)。


20260801

看到了一句很有启发性的话:开环把测试当生成问题,闭环把测试当搜索问题

开环的隐含假设:测试生成质量由生成器的先验知识决定。LLM见过的API文档、代码模式、bug历史足够多,生成一次就能覆盖大部分有价值的测试空间。反馈不必要,因为"生成器已经知道该生成什么"。

闭环的隐含假设:静态先验知识有上界。测试过程本身会产生信息(哪些API组合容易crash、哪些参数区间覆盖率低、哪些模式已经试过),这些信息能让后续生成更有针对性。反馈必要,因为"单靠先验不够,过程信息本身是有价值的"。

我的工作,第一步是把先验融合都用上,提升开环的效果,然后,再考虑如何增强闭环的效果,使得整个的效果更好。这样就很有衔接性了。

第一步要做的事情,大概就是梳理清楚多源知识有哪些,各自的功效,知识之间的关系,建立起来一个多源知识模型,并检验开环的情况下这个东西的效果。这里的多源知识,可能是我们之前想的静态的那层知识。

第二步要做的事情,就是将上一个工作改成闭环的,使用各种各样的反馈信息,去调整上一个工作中,特征空间里采样的策略或者权重,实现自适应,从而更能够提高效果。

关于多源知识,我们之前已经做了很多的调研,现在脑子里已经有一个相对清晰的了解了。

对于闭环这个事情,我需要先做一个调研,一方面是看 DL 框架的测试的闭环情况,我的印象中应该还是开环为主,是大家没注意到能闭环吗?还是这个事情本身和其他测试对象不太一样,导致不容易闭环呢?另一方面,看一下其他的测试对象或者传统的软件测试,是如何做闭环这个事情的。

我们先来看一下传统软件测试中的闭环的情况。

首先,闭环的定义,我们认为必须得是测试的过程信息被用来调整测试策略。有一些工作可能是使用闭环修复测试产物(比如让 LLM 生成的测试用例能 run 起来),这种不算是闭环。

最成熟的是覆盖率反馈,新命中的边是高价值的,有价值的输入被放入队列作为种子,后续会对种子使用某些规则进行排序,从而确定优先选哪个种子,以及,还得确定每个种子的变异步数(比如,越少覆盖的给予更多的变异次数)。该闭环中,我们用的是覆盖率去反馈种子选择优先级、选择哪个变异规则变异多少次。覆盖率反馈直接改变了"下一次用什么种子变异"和"变异多少次"这两个核心策略决策。

还有一个比较符合直觉的自适应的随机测试,大概意思是,我之前测到过的没问题的地方,这个地方的附近可能也大概率没有问题,所以我要尽量测更远的地方。一个工作中具体是这样干的,每次随机生成一堆用例,然后计算这些用例和已经测过的用例的距离,选最小距离最大的那个去执行。在这个方案中,是利用测试的过程信息(测试用例的空间分布)调整测试策略。

第三类是把测试用例生成问题,去形式化成一个搜索问题。什么意思呢?感觉突然不太能理解了。

第四类是“具体/符号执行”,大概意思是先运行一个测试用例,看执行轨迹所满足的一系列约束条件,然后考虑把轨迹中的某个约束取反,使用 SMT 求解得到另一组参数,再执行这个用例,就能走到不一样的分支了。需要决策的点在于,选哪个分支条件取反。另外,这次执行得到的约束条件,就是你采集到的全部的信息了。

第五类是自适应蜕变测试,我其实没看懂是啥意思。后边再继续追问了解一下。

上面的闭环都完全没用 LLM,下面我们来看一下 LLM 如果加入到闭环中,大概是啥样的。

首先是一种比较简单的情况,有人是搞覆盖率反馈的,当覆盖率的增长出现瓶颈时,看一下哪些 API 没测到,让 LLM 介入,生成这种用例,加入到种子队列中继续参与测试,这样后边就能测到了。这种方法相当于 LLM 做了一个测试空间的传送门,能从一个区域启发式地跳到另一个没被测过的区域继续探索。

吴老师前几天给推荐了一篇文章,它是里面有多个闭环,一个是我们最开始说的只修复产物的假闭环,用知识图谱作为引导,去修复测试用例无法运行的问题;另一个是真闭环,根据覆盖率,找到低覆盖率的 API 列表,然后基于这个低覆盖率列表去生成或者改写用例,这里 LLM 参与的是直接调整测试用例 API 组合。

还有一个类似的工作,是基于一段时间内的覆盖率情况报告,让 LLM 生成为了覆盖建议,从而调整 API 选择的概率。这个就是让 LLM 先生成概率,从而间接地引导生成出的用例。

下一个工作比较有意思,也比较激进,它让 LLM 根据测试的过程信息,去直接改生成器的代码。相比于上一个工作是改生成概率,这个直接把代码都改了,没有参数化这一套了。

也有根据反馈,去调整生成提示词的(把 LLM 本身当成生成器)。

下面还有两个没看懂的工作。

最后做一个总结,传统的测试中,闭环模式大概可以这样分:

  • 指标反馈→权重/概率调整
  • 指标反馈→LLM分析→语义策略调整
  • 指标反馈→LLM分析→自主决策
  • 多回路并行

通用架构:

[执行测试] → [收集反馈] → [分析反馈] → [调整策略] → [生成下一批测试] → [执行测试] → ...
                ↑                          ↑
                |                          |
           反馈信号类型              策略调整方式
           (覆盖率/crash/             (参数化/语义化/
            距离/约束/适应度)           自主决策)

这里需要注意,调整策略和调整个体不一样。调整个体指的是下一个种子选啥,调整策略指的是咋生成下一个用例。

loop engineering?

如何去排除荷尔蒙的因素:

  • 我平常的时候的行为,和对她的行为,是不是一样的?
  • 我是不是并非想借这个机会填补空缺?我想和她在一起,是因为我觉得在一起我们都会变得更好(相比于不在一起时自己成长和发展)?

根据之前的恋爱经验,后边可能会爆雷的点:

  • 家庭不匹配,家里人的阻拦
  • 规划不一样
  • 交往方式,特别是处理矛盾的方式
  • 生活习惯
  • 能不能持续性地互相理解对方,能换位思考
  • 只能浅层交往,没法交流比较深的事情,或者交流了这些就会不愉快
  • 会不会因为在一起了,就默认要求对方更怎么样,或者要求对方因为自己要做出什么改变
  • 在一起后双方的个人时间空间问题

实验设计

比较几个工作时,如果要比较发现缺陷的能力,可以用已经被确定的缺陷为”考卷“,看看其他工作能不能发现这些问题,以及我的工作能不能发现这些问题。

为了证明你的工作有用,你需要证明你的方法能够稳定复现或者触发一些 bug,要么是比你自己的基线相比能稳定复现,要么是相比别人的,你能稳定复现。

一个方法可能在种子和变异规则上都有一些工作,那么可能就要分别做实验去评定,种子质量和变异规则分别贡献多少。

如果害怕测不出来 bug,可以对 DL 框架本身进行变异,然后看自己的方法能否找到若干变异体里的 bug,需要和未变异的框架进行差分测试。这种变异测试也是检验自己生成的测试用例的质量的标准之一。

但是这个思路要产生很多个版本的 DL 框架,都需要编译,这可能会导致很慢且很浪费空间,这是需要解决的问题。并且,普通的变异可能 level 太低了,自己的方法未必能测出来,测出来也未必是自己的方法起的作用。

预先生成大量 DL 框架的变异体,预先生成一组测试用例,然后看 bug 的检测能力,这是可行的。

也可以对一个 DL 框架变异体一直 fuzzing,直到找到 bug,这个事情也是可行的,并且如果你是覆盖率引导的,那也只能这样去测,我们关心的是用了多少用例或者多久,测出来的 bug。

对 DL 框架本身进行变异根本没人做,可能工程上实现难度比较大吧,这个应该是不可做。

赞赏