毕业论文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():
# 运行测试
其中 fn 和 buid_inputs 是直接提取的,前面的引入依赖和后边的执行算是模板代码。
fn 和 build_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 参数的替换。要想修改图结构,我个人的感觉是有三个方面的做法:
- 基于规则变异/生成。大概就是 reborn2 以及 NNSmith 那套做法,就没有大模型什么事儿了。
- 种子就足够复杂,这样就不变异结构了,只替换算子或者改参数。复杂的种子可以靠其他的生成器生成?比如 reborn2?通过修改他们的参数,得到较为复杂的种子程序。或者靠官方的复杂测试用例?
- 参照模板插入。这个大概就是我们设计一些变异模板,比如某个
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
这样似乎更乱了
实验设计
比较几个工作时,如果要比较发现缺陷的能力,可以用已经被确定的缺陷为”考卷“,看看其他工作能不能发现这些问题,以及我的工作能不能发现这些问题。
一个方法可能在种子和变异规则上都有一些工作,那么可能就要分别做实验去评定,种子质量和变异规则分别贡献多少。
如果害怕测不出来 bug,可以对 DL 框架本身进行变异,然后看自己的方法能否找到若干变异体里的 bug,需要和未变异的框架进行差分测试。这种变异测试也是检验自己生成的测试用例的质量的标准之一。
但是这个思路要产生很多个版本的 DL 框架,都需要编译,这可能会导致很慢且很浪费空间,这是需要解决的问题。并且,普通的变异可能 level 太低了,自己的方法未必能测出来,测出来也未必是自己的方法起的作用。
预先生成大量 DL 框架的变异体,预先生成一组测试用例,然后看 bug 的检测能力,这是可行的。
也可以对一个 DL 框架变异体一直 fuzzing,直到找到 bug,这个事情也是可行的,并且如果你是覆盖率引导的,那也只能这样去测,我们关心的是用了多少用例或者多久,测出来的 bug。
对 DL 框架本身进行变异根本没人做,可能工程上实现难度比较大吧,这个应该是不可做。