第四次阶段性汇报 · 讲稿
(SK2 Decompile 复现 · 模型/数据迁移与 CPU 评测)
P1(封面)
各位老师、同学好,下面由我来做本次的阶段性汇报。汇报内容主要围绕 SK2 Decompile 两阶段反编译框架的复现展开,重点是上一阶段之后这一段时间里,我在「模型迁移到 → 服务器上拉取并评测」这条流水线上做的事情,以及其中遇到的问题、定位过程和后续的规划。
P2(本期工作总览)
先给大家一个总览的视图。这一段时间我主要在做两件比较大的事情。
第一件,是把上一阶段已经在 CPU 上训练好的两阶段模型——也就是「结构恢复(pseudo2norm)」和「标识符命名(norm2code)」这两个 checkpoint——推到上,然后在云服务器上拉下来做进一步的处理。之所以要走这一步,是因为后续要进入评测环节时,GPU 算力服务器的库存持续开不起来(这一点我后面 P10 的截图会具体讲到),所以只能先在 CPU 服务器上完成数据归一化和评测脚本的验证,等 GPU 有库存的时候再切回去。
第二件,是 CPU 端的评测。在迁移完成之后,我尝试直接按官方仓库的说明跑评测脚本,但发现命令跑通之后输出是空的,于是开始排查 JSON 字段和代码的对应关系。
这两件工作,最后都走完了主要环节,但也都碰到了比较硬的环境/依赖问题,因此本期汇报的最后,我会给出下一步的规划,主要思路是「先用 huggingface 上的开源模型权重跑通评测,把评测流程先稳定下来」,再去啃两阶段 RL 训练这条更难的路。下面按页面顺序展开。
P3(模型上传:触发问题)
先说模型上传。这一页是「上传」这个动作第一次出现报错的状态。
上一阶段结束后,本地 CPU 服务器上的 saves 目录里已经存了训练好的两个 checkpoint,分别是 pseudo2norm-example 和 norm2code-example,每个 checkpoint 都包含 model-0000X-of-00006.safetensors 这六个分片,以及一份 optimizer.pt。这一份资产大约 51G,是后续评测的关键输入。
我先按 Git LFS 的常规做法,在仓库里配置好 LFS、track 了 *.safetensors,然后 git push。但 push 的时候,Git 报了 LFS 错误:
也就是说,单个 LFS 对象超过了 5GB(5120MB)的限制,直接拦截了这次推送。
于是我先用 du -h 找出所有超过 5GB 的文件,定位到大头主要是两处:
我注意到一件有意思的事情:上面列出的 4.6GB 的 safetensors 文件,单看大小是「低于」5GB 的,按理说不应该被 LFS 拦截。但LFS 是按字节严格校验的,safetensors 在 LFS 传输时会附带元数据和校验信息,最后生成的 LFS 对象会刚好触到 5120MB 这个阈值,所以也被拒了。这意味着光删 optimizer.pt 还不够,分片文件本身也要处理。
思路是:先把不需要的 optimizer.pt 之类的大文件删除,再用自己写的 Python 程序,把 4.6GB 的分片文件切成更小的片段,再走 LFS。
P4(模型上传:旧仓库清理 + 自动化脚本)
在做切分之前,我又发现了一个更棘手的问题:哪怕我把上面那些大文件从工作区里删了,旧 Git 历史里依然残留着 51G 的 LFS 对象记录。它们已经 commit 进历史了,靠 git lfs prune、filter-branch 这些常规手段怎么清都清不干净,最后会随着 push 一起被重新生成出来。
权衡之下,我决定彻底抛弃旧历史,初始化一个全新的仓库。具体的做法是:在原 saves 目录的同级,新建一个空的 saves 文件夹,重新 init 一个仓库,只 track 我真正需要上传的那两个 checkpoint 目录和 LFS 配置,其他一概不带。这样等于从源头上让历史里没有 51G 的大文件。命令大致是:
为了把 4.6GB 的分片文件压到允许的阈值以下,我还写了一个自动化的拆分/上传脚本(auto_push.py),它会先把每个分片切到 LFS 单文件阈值以下,再按照 .gitattributes 里登记的规则走 LFS 通道上传,最后做一次 push 校验。这一步之后,SK2 仓库里就有了两个干净可拉取的 checkpoint。
P5(服务器下载模型:拉取 + 合并分片)
模型有了之后,下一步就是到云服务器上把模型拉下来。
我在 CPU 服务器上 git clone 了 SK2 仓库,又用 git lfs pull 把 LFS 对象真正取到本地。因为在客户端这边为了规避阈值做了切分,所以拉下来的 safetensors 是被拆成若干 .part1.safetensors、.part2.safetensors 这样的分片的,没法直接用 HuggingFace 的 from_pretrained 加载。
所以我又写了一个 merge_model.py 的小程序,思路很简单:用一个正则 (.*)\.part(\d+)\.safetensors 匹配到所有分片文件,按 part 号排序后用 shutil.copyfileobj 顺序拼回原始的 .safetensors 文件;拼完再把分片删掉,腾出空间。脚本是递归遍历子目录的,所以不管是 pseudo2norm-example 根目录下的分片,还是 checkpoint-2 下面的分片,都能一次性合并干净。
调用方式:
python merge_model.py ~/SK2/pseudo2norm-example
python merge_model.py ~/SK2/norm2code-example
合并完成之后,模型权重就从「分片 + 备份」恢复成了 HuggingFace 标准格式,下一步就可以走评测脚本了。
P6(服务器下载数据:评测数据集的另一半)
模型下载完只是第一步,评测还需要「输入」——也就是反编译的输入数据。这一页想跟大家分享一个比较尴尬的发现:
我在云 CPU 服务器上尝试拉评测用的 dataset(humaneval 那一份反向数据),拉了很久,最后进度卡在 50% 就下不动了。
进一步排查发现,根本原因是这台服务器的内存不够。前面 merge_model.py 合并分片时,六个分片大约 27G 已经被吃掉了,optimizer.pt 之类的辅助文件虽然不参与推理,但留在磁盘上也会争抢空间;更要命的是,评测需要的环境配置(比如一些体积比较大的 wheel 依赖、huggingface 缓存等)在内存/磁盘双双吃紧的情况下,根本下载不下来。
所以这一阶段的结论是:CPU 服务器上只完成了「模型拉取+合并」这一半的工作,评测数据集和环境配置这一半,因为存储和内存的限制,没法和模型共存。
这个 50% 的状态也直接决定了,后面我必须先把评测数据集和环境问题解决掉,再回来做模型推理。
P7(CPU 评测:首次跑 normalize_pseudo 输出为空)
在模型合并完成、临时清理出一部分存储之后,我先不急着跑模型,而是按官方仓库的说明,先把数据归一化这一关跑通——这一步对应的是论文里 IR 生成流程的反向过程,需要把原始样本里的伪代码字段统一成一种格式,供后续反编译使用。
官方仓库里提供的样例是 reverse_sample.json,按照文档给的命令直接跑:
python normalize_pseudo.py \
--input_json reverse_sample.json \
--output_json reverse_sample.json
命令本身没有报错,进程正常退出,但打开 reverse_sample.json 一看——输出是空的,是一个空数组。
我立刻觉得不对劲,于是没有急着换工具,先看代码和样例 JSON 的实际字段。这件事在下一页定位。
P8(CPU 评测:定位 key_name + 后续依赖问题)
定位的过程分两步。
第一步,定位 key 不匹配。
我把 normalize_pseudo.py 的关键读取逻辑和样例 JSON 一起看:
1) normalize_pseudo.py 在解析每条样本时,会从命令行参数 --key_name 拿到一个字符串,默认值是 "pseudo"。
2) 代码里实际是 entry.get("pseudo", "") 来取伪代码字段;如果该字段不存在或为空串,下游处理就退化成空字符串,最后被过滤掉。
3) 而 reverse_sample.json 里,伪代码字段的名字是 "ida_pseudo",不是 "pseudo"。
也就是说,文档里给出的「直接照搬命令」对这份样例 JSON 是不 work 的——它读到的是空字符串,所以输出是空。
于是我把运行命令改成了:
python normalize_pseudo.py \
--input_json reverse_sample.json \
--output_json reverse_sample_norm.json \
--key_name ida_pseudo \
--workers 8 \
--remove 0
重新跑后,输出文件里就有了非空条目,第一道工序算是正式跑通。这一步也让我意识到,官方仓库的 quick start 文档对样例 JSON 的字段名是有一个隐含假设的,不读代码是发现不了的。
第二步,定位后续依赖问题。
数据归一化跑通之后,我继续往后走评测流程:先跑 inference(按 huggingface 路径拉开源权重),再跑 evaluate。但 inference 这边频繁报环境和库依赖的错——包括 vllm、transformers、numpy、scipy、pydantic 这一系列大版本的相互约束,再加上一些 mock/fake tensor 相关的报错,频繁修改 requirements 之后依然没法稳定复现。
我自己也意识到,这种修修补补的方式在 CPU 上很难做到「评测结果可以与论文原始数据对比」这种量级的可靠性,因为版本组合稍微一变,量化指标的数值就会有偏差。所以这一阶段的后期,我把决策点提了一下:
· 把 CPU 上的 inference/evaluate 暂时挂起来,不再继续死磕依赖;
· 后续评测统一切到 GPU 平台去做;
· 评测环境也以 huggingface 上的开源权重为基准,保证与原论文的对比口径一致。
这是这一页最后给出的方向,也直接连到了下一页的「之后规划」。
P9(之后规划)
下面说一下后续的工作规划,分三条线。
第一条线:清理 GPU 服务器。
目前云 GPU 服务器里还残留着一些之前装的依赖和下载的中间数据(pytorch、transformers、vllm 的历史版本,以及一些失败的 evaluation 缓存),这都占着宝贵的存储和内存。后面我会先做一次大扫除——conda 环境按需重建,pip 缓存清空,没用的 inference 结果、临时数据集都删掉——为下一步「跑评测」腾出干净的运行环境。
第二条线:评测优先,本地训练延后。
上一阶段卡壳的两阶段 RL 训练,会对 GPU 算力、显存、存储都有比较高的要求(论文中报告的训练资源是 8×100 GPU·年这种量级;我们复现环境远远达不到)。所以我打算把「评测」和「训练」拆开,先把评测跑通、再去啃训练。本期汇报里 merge_model.py 和数据归一化这一关,其实就是在为这一步铺路。
第三条线:用 humaneval 和 bringup-bench 两条评测线并行,方便与论文做对比。
具体来说:
· humaneval 走 normsrcpseudo 这一份,调用 huggingface 上的 sk2decompile-struct-6.7b(结构恢复)和 sk2decompile-ident-6.7b(标识符命名)两个模型,对应仓库里 sk2decompile_inf.py 的接口。命令大致是:
· bringup-bench 走另一份 binary-level 的样本,对应仓库 evaluation/bringupbench/ 目录,调用 eval_infer_out.py 做汇编级评估:
这两条线一份偏源代码级、一份偏二进制级,覆盖了论文评估体系的主要维度。评测脚本如果能稳定跑通,就可以和原论文里 humaneval 的 re-execution rate、bringup-bench 的 replacement_failed 比例等指标做直接对比。
总结一下这一段规划:用「huggingface 上的开源权重 + 官方评测脚本」作为基线,先把评测流程跑通、跑稳;等 GPU 服务器清理干净、环境一致性问题解决之后,再回头去啃两阶段 RL 训练这条硬骨头。
P10(GPU 服务器开不起来)
这一页插一张截图,是我们在上尝试开 GPU 算力服务器时弹出的提示。
可以看到右上角的红色提示:「指定实例类型的库存小于指定的购买数量」。
这件事直接决定了为什么这一段时间评测主要在 CPU 上做、为什么在环境上要绕一些路。我也会持续盯一下库存,看到有货就尽快把 GPU 拿下来,把规划里的两条评测线在 GPU 上正式跑起来。
P11(汇报结束页)
最后做一个简单的收尾。
本期主要做了三件事:一是把上一阶段的 CPU 训练模型推到,再在云服务器上拉下来合并;二是按官方命令首次跑 normalize_pseudo 时定位了 ida_pseudo / pseudo 的字段不一致问题,并把数据归一化这一关跑通;三是给下一步评测工作画了路线图:先清理 GPU 服务器、用 huggingface 开源权重跑通 humaneval 和 bringup-bench 的评测,再回到两阶段 RL 训练。
中间最值得复盘的一个经验是:开源仓库的 quick start 文档对样例数据是有隐含假设的,命令跑通 ≠ 正确,第一次跑出「空结果」时一定要回去对代码,而不是怀疑工具链。
以上就是本期的汇报内容,请各位老师和同学批评指正。