news 2026/10/18 3:14:41

把LLM当作编译器:直接生成PTX绕开后端

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
把LLM当作编译器:直接生成PTX绕开后端

把 LLM 当成编译器、直接生成 PTX、把整个编译器后端绕开——这套说法最早吸引我,是因为它戳中了 GPU 编程工具链里最没存在感又最麻烦的一段。大部分人写 CUDA 时根本不会关心 PTX,只知道 nvcc 把核函数编译成能在 GPU 上跑的东西;至于编译器后端(ptxas 那一大坨优化逻辑)到底在干什么,更没几个人说得清。这篇论文做的,恰恰就是把这段“黑盒子”拿掉,换成一个大模型。

下面我会从编译原理、GPU 编程、LLM 工程三个角度把它拆开讲。文章想覆盖三件事:PTX 和编译器后端在传统工具链里扮演什么角色;为什么让 LLM 直接写 PTX 在原理上可行;以及如果你想复现或者借鉴这个思路,有哪些环节和坑。适合对 CUDA 有基本了解、同时对“LLM 在编程工具里到底能渗透到多底层”感兴趣的读者。如果你只是听说过 PTX、没亲手写过,也没关系,我会尽量把概念拉回地面,用类比讲清楚。

1. 先把概念对齐:PTX、编译器后端、LLM 各自的位置

1.1 传统 CUDA 编译流水线到底在做什么

我在带团队做 GPU 加速的时候,最常见的现象是:大家把 nvcc 当成一个“魔法盒子”,写好的核函数丢进去,出来一个 .cubin 或者直接被驱动程序加载执行。很少有人真的去拆解中间发生了什么,但恰恰是这段“没人关心的中间过程”,决定了你写的代码最终能不能把 GPU 吃满。

标准的 CUDA 编译流水线大概是这样的:

  1. nvcc 前端(基于 LLVM/Clang 改的)把 CUDA C++ 源码解析、降级成 NVVM IR,这相当于 LLVM IR 的一个变体。
  2. NVVM IR 会被转成 PTX(Parallel Thread Execution),这是 NVIDIA 定义的一种虚拟指令集,你可以把它理解成“GPU 的汇编语言”,但它不绑定具体某代硬件。
  3. PTX 之后交给 ptxas,也就是常说的编译器后端/汇编器,做寄存器分配、指令调度、内存访问合并、分支优化等,最终生成 SASS——这才是某代 GPU 芯片真正执行的机器码。

一句话概括:PTX 是中间的联络语言,编译器后端负责把联络语言变成实打实的机器码,并且在这个过程中疯狂优化。

很多人会拿 Java 做类比,PTX 类似字节码,SASS 类似 JVM 最终编译出来的本地机器码。区别在于 CUDA 的这个“JIT”过程分散在两处:写应用的人会在编译期生成 PTX,驱动程序加载 cubin 时再把它转成 SASS。也就是说,传统工具链里的“编译器后端”并不是独立存在的一层,它同时涉及离线 ptxas 汇编和驱动里的 JIT 编译。

1.2 后端为什么是“深水区”

我说后端是深水区,不是因为它神秘,而是因为它积累了太多启发式规则。寄存器分配本身是个 NP 难问题,指令调度要同时照顾数据依赖、流水线延迟、bank conflict、occupancy,内存访问还要考虑合并与对齐。这些规则是过去二十年一点一点堆起来的,每一代新架构(Volta、Turing、Ampere、Hopper、Blackwell)都会带来新的约束,后端就得跟着打补丁。

打个比方:传统编译器后端像一个经验丰富但按手册办事的老技师,它知道怎么做是“平均最优”,但面对一个特别偏门的 kernel 形态,它不会灵活变通。因为它的一切决策来自固定算法和成本模型,而不是“见过类似写法”。

这也是论文选择 PTX 作为切入点的原因之一。PTX 是一个相对干净、指令集收敛、格式高度结构化的目标语言,比直接生成 SASS 容易得多,比生成 LLVM IR 又更贴近最终执行效果。对 LLM 来说,这种“输入一段描述或者一段源码,输出一段 PTX 代码”的任务,本质上是离散符号序列的翻译问题,恰好是它的主场。

1.3 “AI 就是编译器”这个判断的由来

论文标题里这个判断很重:“AI 就是编译器”。它不是比喻,而是在重新定义编译器的功能边界。传统编译器做的是“源代码 → 经过多趟优化 → 目标代码”的确定性变换;而 LLM 做的是“根据训练中学到的模式,预测出最可能的、质量最高的目标代码”。两者输入输出一致,只是实现路径不同。

这里必须澄清一个常见的误解:论文并不是说 LLM 能完全取代 ptxas,让 PTX 直接变成 SASS 之后再也没编译器的事。绕开后端的核心,是让 LLM 承担传统后端里“从语义到优化后指令序列”的决策工作,由 LLM 直接给出接近最终质量的 PTX;最后那一步 PTX→SASS 的汇编依然存在,但它退化成近乎机械的翻译。换句话说,最需要“智能”的部分被 LLM 接管了,剩下的体力活留给工具。

我第一次读完这个思路,第一反应是“激进”,第二反应是“其实有道理”。因为后端优化的瓶颈从来不是体力,而是启发式规则面对未知模式时的盲区。LLM 恰恰擅长从海量真实 kernel 里学会“这类代码应该怎么写才快”。这个视角,和传统编译器完全不在一个维度上。

2. 核心思路拆解:为什么让 LLM 直接写 PTX 是可行的

2.1 PTX 的语法面相对收敛,正好匹配 LLM 的生成能力

如果你让 LLM 直接写 C++ 或者 CUDA C++,要处理的东西太杂了:模板、头文件、指针别名分析、内存模型、异常处理,这些跟最终性能关系不大,但会占掉大量生成难度。PTX 不一样,它的指令就那么几百条,寄存器是编号的,类型系统简单,没有对象、没有继承、没有模板。

以最基础的向量加法为例,你不用告诉模型“这是一个global函数,用 gridDim、blockDim、threadIdx 算索引,然后考虑边界”,你要它输出的只是一个结构清晰的 PTX 块:

.version 8.0 .target sm_80 .address_size 64 .visible .entry vecAdd( .param .u64 .ptr .global .rdonly a, .param .u64 .ptr .global .rdonly b, .param .u64 .ptr .global c, .param .u32 n ) { .reg .pred %p<2>; .reg .b32 %r<6>; .reg .f32 %f<4>; .reg .u64 %rd<5>; ld.param.u64 %rd1, [a]; ld.param.u64 %rd2, [b]; ld.param.u64 %rd3, [c]; ld.param.u32 %r5, [n]; mov.u32 %r1, %ctaid.x; mov.u32 %r2, %ntid.x; mov.u32 %r3, %tid.x; mad.lo.u32 %r4, %r1, %r2, %r3; setp.ge.u32 %p1, %r4, %r5; @%p1 bra $L_exit; mul.wide.u32 %rd4, %r4, 4; add.u64 %rd1, %rd1, %rd4; add.u64 %rd2, %rd2, %rd4; add.u64 %rd3, %rd3, %rd4; ld.global.nc.f32 %f1, [%rd1]; ld.global.nc.f32 %f2, [%rd2]; add.f32 %f3, %f1, %f2; st.global.f32 [%rd3], %f3; $L_exit: ret; }

这是示意性的简化版本,不保证和 nvcc 的逐行输出完全一致,但它说明了 PTX 的重要特征:一切都是显式的、扁平的、结构化的。没有类,没有重载,没有隐式转换。这种语言对 LLM 来说,约束强、噪声少,生成难度比 C++ 低一个数量级。论文选择 PTX 而不是 SASS 作为生成目标,也有工程上的现实考量——PTX 的语法和语义是公开文档化且相对长期稳定的,直接生成 SASS 则需要为每一代芯片单独维护电码表,根本不现实。

2.2 跳过后端的收益与代价

收益要从三个层面看。

第一,优化可以脱离启发式的局限。传统后端的优化规则是工程师总结的“常见模式”,它擅长处理矩阵乘、卷积、归约这类高频形态,但遇到非典型数据流或者新指令组合时就抓瞎。LLM 在训练时见过 GitHub 上大量的手工调优 kernel、厂商的样例代码、论坛里贴出来的怪招,它能把这些“经验”直接映射到生成结果里,不需要工程师把经验写成死板的 pass。

第二,可以针对具体场景定制指令序列。传统后端要照顾所有用途,所以它产出的代码是“平均最优”;LLM 可以根据你给的额外约束(比如“这个 kernel 的 block 大小固定为 128”“共享内存只用 16KB”“必须让每个线程处理连续两个元素”)输出针对性更强的 PTX。这在传统工具链里往往需要你手动内联汇编或者修改编译选项,门槛很高。

第三,摆脱工具链的“版本锁”。写一次 PTX,理论上只要最终能通过 ptxas 的语法检查,就能被后续驱动 JIT 成新架构的 SASS,不再受 nvcc 大版本升级时的行为变动困扰。

代价同样不能装看不见。最直接的是正确性风险:后端虽然优化死板,但它经过了几十年的验证,很少生成错代码;LLM 生成的 PTX 哪怕语法正确,也可能在边界条件、内存对齐、barrier 同步上有微妙的语义错误。“能编译通过”和“结果正确”之间的距离,比很多人想象的要大得多。这一点我在后面避坑部分会展开。

2.3 方法路线:数据、训练、验证闭环

据我对这类工作的常见做法总结(这也是复现时的标准路线),论文的方法框架大概是三条线并行:

  1. 数据构造:从 CUDA Samples、开源 GPU 计算库、GitHub 大规模 kernel 代码里抽取(源码,PTX)配对数据。没有现成配对的,就自己用 nvcc 编译源码生成 PTX 来造语料。这一步的工程量最大,数据质量直接决定模型水平。
  2. 模型训练:用这些配对数据对基座模型做监督微调(SFT),让模型学会“看到某种计算意图,输出对应的 PTX 片段”。之后再引入偏好优化,比如让模型生成多个候选,按运行正确性和性能排序,构造偏好对做强化学习或 DPO。
  3. 验证闭环:生成出来的 PTX 不能靠人眼看,要能自动验证。做法是把 PTX 交给 ptxas 汇编成 cubin,再通过 CUDA Driver API 加载执行,比较输出结果和参考实现,同时记录执行耗时。用验证结果作为反馈信号,继续迭代模型。

这条闭环里最妙的是:编译器工具链没有被扔掉,而是从“生成者”变成了“裁判员”。ptxas、compute-sanitizer、Nsight 这些工具全部复活成验证器。LLM 负责提出候选方案,传统工具负责告诉它“你错了”或者“你这次性能不错”,形成一个人机协作的社交流程。我认为这是整篇论文最有工程价值的地方。

3. 实操视角:如果我想复现或上手,怎么做

3.1 搭建实验环境

先说明,以下步骤是我根据常见实践补充的落地流程,不是论文原文的实验细节;但如果你想快速验证“LLM 生成 PTX”这条路是否值得投入,按这个流程走基本不会错。

环境准备分两层:GPU 计算环境和 LLM 推理环境。

GPU 侧至少需要一块能跑的 NVIDIA 显卡(建议 Turing 以上架构),装上 CUDA Toolkit。重点是拿到两个工具:

  • ptxas:把 PTX 汇编成 cubin 的官方汇编器,也是我们用来验证 LLM 输出“能不能过”的第一道关卡。
  • nvcc:用来从传统源码生成 PTX 作为训练语料和对照基准。

验证流程里最常用的命令是这组:

# 从 CUDA 源码生成 PTX nvcc -arch=sm_80 -ptx vector_add.cu -o vector_add.ptx # 把 PTX 汇编成 cubin ptxas -arch=sm_80 -o vector_add.cubin vector_add.ptx # 反转:从 cubin 反汇编出 SASS,用来观察最终机器码 cuobjdump -sass vector_add.cubin # 运行时检查内存错误 compute-sanitizer --tool memcheck ./run_vector_add

LLM 侧就看你有什么卡。如果只是小规模实验,用 7B 到 13B 的开源模型(Qwen、Llama 3 这类)在本地做 LoRA 微调完全够用;如果目标是想在复杂 kernel 上接近 ptxas 的水平,那需要更大基座,这时候通常要接 vLLM 或 TGI 这类推理框架,把生成吞吐打上去。

3.2 构造训练语料与评估基准

语料是这条路线上最脏最累的活,但也是决定上限的环节。我的建议是分三档来做:

  • 第一档:官方样例。CUDA Toolkit 自带的 Samples 目录里有几十个经典 kernel,直接编译生成对应的 PTX,配对作为“标准答案”。这些数据干净、许可友好,适合做 SFT 的冷启动。
  • 第二档:开源库。把一些小型的 GPU 库或内核集合(比如一些单文件 kernel 仓库)拉下来,过滤掉明显非法的代码后批量编译。注意这些代码的许可证,如果是 GPL 等强 copyleft,训练后商用会有麻烦;建议优先选 MIT/Apache 协议的。
  • 第三档:自写模板。针对你要解决的特定问题(比如注意力机制、flash attention、稀疏矩阵乘),自己手写一批“意图描述 + 参考 PTX”。目的不是让模型背答案,而是让它学会把“计算意图”映射成“指令序列”。

评估基准我建议分级。第一级是“正确性”:固定输入输出,跑出来的结果和 CPU 参考实现一致,通过率要接近 100%。第二级是“性能对齐”:和ptxas -O3生成的 baseline 对比,运行时间不能差太多。第三级是“复杂度梯度”:从向量加这种 20 行 PTX 能搞定的,到带共享内存、同步、归约的 kernel,再到多 block 协作的复杂 kernel,逐级提升难度。这样你能清楚看出模型的薄弱环节在哪个层级。

3.3 典型生成样例与验证流程

简单跑通一个 end-to-end 流程,比讨论一万句理论都来得实在。以向量加法为例,完整链路是这样的:

  1. 给 LLM 一段自然语言描述或者一段简单 CUDA 源码,要求它输出完整的 PTX。
  2. 把输出保存成generated.ptx,先跑ptxas检查语法和目标架构是否匹配。
  3. 写一个最小测试程序,用cuModuleLoad加 cubin,用cuLaunchKernel启动,把结果拷回主机比对。
  4. 跑compute-sanitizer查越界、竞争、未初始化内存问题。
  5. 用cuEvent或clock64()计时代,和传统编译路径生成的 kernel 做对照。

这里我要强调一个实操细节:不要只测一个数据规模。我最开始在复现类似思路时,只测了 n=1048576 这种友好的规模,结果模型生成的 PTX 在大规模下表现惊人,我差点以为 LLM 已经打败后端了。后来换成 n=1000003(一个很刁钻的质数规模),当场翻车——边界判断写错,越界访问被 compute-sanitizer 当场逮住。LLM 生成代码的“平均质量”可能不差,但“极端情况下的鲁棒性”离传统编译器还有明显距离。测试数据规模必须包含非对齐、非整除、线程数大于数据量这些边界场景。

4. 避坑实录:生成式编译器常见的几个大坑

4.1 正确性验证别只靠“能跑”

这条我吃过亏,必须放在最前面。LLM 生成的 PTX 和传统编译器输出的最大区别是:传统编译器不会在结果里留下“看起来很对但实际有错”的代码,LLM 会。原因是模型学习的是概率分布,它知道“向量加法的 PTX 大概长什么样”,但它不理解每条指令在这个特定上下文里的语义后果。

容易踩的具体问题包括:

  • 边界判断用了setp.ge还是setp.gt,在数据规模恰好是 block 整数倍时两者结果一样,一旦不是整数倍就分出了胜负。
  • 全局内存访问是否按 4/8/16 字节对齐,ld.global.f32要求 4 字节对齐,地址算错一位可能不报错,只是结果错。
  • .rdonly修饰符有没有写对,写错在某些场景下会绕过错觉优化,性能直接掉一半。
  • 共享内存的读写顺序和.bar.sync是否匹配,多 block 协作时尤其容易翻车。

所以验证必须用“输入多样化 + 工具链多层拦截”的思路。输入上覆盖小规模、大规模、非对齐、非整除、空数据;工具链上除了ptxas,必须跑compute-sanitizer,有条件再加数值断言,不要只对输出做一次全等比较。

4.2 性能指标怎么定才公平

第二个大坑是性能对比的公平性。我见过不少被媒体标题带偏的案例:“LLM 生成的代码比编译器快 XX%”——细看发现对比的是-O0或者拿手写次品做 baseline。论文要想让人信服,对照组必须是同一 PTX 在ptxas -O3下产出的 SASS,再比运行时间,而不是拿 LLM 和“没优化的编译器”比。

几点实操建议:

  • 跑性能前先 warmup,取多轮 median,不要用第一轮时间。
  • 关注端到端时间:LLM 生成 PTX + ptxas 汇编 + 加载 cubin 的时间也要算进去。如果每次都用 LLM 现场生成,这个固定开销可能比 kernel 本身还大;实际工程里通常会把高频 kernel 的 PTX 缓存起来。
  • 注意 ptxas 的输出不一定是最优的,但驱动在加载 cubin 时仍会做一次 JIT 优化。所以“LLM 生成的 PTX 性能”要放在完整链路里测,单独测 PTX 本身没意义。

我还想多说一句:性能对齐测试里要特别留意寄存器溢出(spilling)。ptxas 做寄存器分配时如果压力过大,会把寄存器 spill 到 local memory,性能骤降。LLM 生成的 PTX 如果频繁使用高阶寄存器,很可能在看似合理的代码里埋下这个隐患。看 SASS 时重点搜local相关的 load/store,有大量出现基本可以断定 spilling 了。

4.3 安全、兼容性与工程化问题

最后一个坑,偏工程化,但对落地至关重要。

  • 沙箱执行:LLM 生成的代码没有经过人工审查前,绝对不能直接跑在共享集群或生产机器上。它可能包含恶意逻辑、死循环、或者超出卡权的资源申请。正确做法是隔离执行,至少用compute-sanitizer和资源限制先把危险操作挡掉。
  • 架构兼容:PTX 带.target元数据,生成时写sm_80的代码不能直接在sm_90上加载。如果你的集群混用多代卡,需要在生成时显式指定架构,或者让加载层做架构检测后重新生成。
  • 可维护性:团队里没人读得懂 LLM 生成的 PTX,这本身是个现实问题。传统编译器的输出是确定性的,出问题可以回溯;LLM 生成的结果每次可能不同,排查起来会更费劲。工程上建议在生成时固定随机种子、缓存生成结果、并且把“生成用的输入描述”和“PTX 输出”一起入库管理,否则出了问题根本不知道当时的 prompt 是什么。

下面把常见问题整理成一张速查表:

现象原因排查思路
编译通过但结果错边界、对齐、修饰符语义问题compute-sanitizer + 多规模输入测试
大规模正常小规模翻车边界判断写错用非整除的 n 测试
性能忽高忽低寄存器 spilling 或驱动 JIT 行为cuobjdump 查看 SASS 中的 local memory 访问
换卡后无法加载.target 架构不匹配生成时指定架构,或加载时检测
死循环或资源超限生成代码包含非法控制流沙箱执行,设置最大运行时间和内存限制

5. 我的实战体会与下一步可以怎么玩

5.1 对编译器工程师的意义

这篇论文最有价值的不是“LLM 能写汇编”这个噱头,而是它提供了一个更务实的身份转换:编译器工程师的产出,从“写优化 pass”变成“构造验证闭环”。传统后端不会消失,但它会逐渐从舞台中央退到裁判席和安全网的位置。我认识的编译器团队里,已经有人在认真做“LLM 生成候选指令序列 + 传统工具自动验证”的混合流程。这个方向的潜力不在取代,而在互补。

5.2 对应用开发者意味着什么

对写 CUDA 的应用开发者来说,短期内最现实的变化是:你不需要手写 PTX,但你需要理解 PTX 级别的性能语义。因为你迟早要在“LLM 生成的 kernel 是快还是慢”这个问题上做出判断。过去你只要会调 nvcc 的参数,未来你可能需要会审一段模型输出的指令序列——像 code review 一样审代码。这要求你把底层的执行模型补上,至少知道寄存器、共享内存、bank conflict、occupancy 这些概念是怎么影响最终性能的。

5.3 后面可以怎么扩展

这篇论文把“编译器后端被绕开”这件事做出来了,那下一步自然要问:能不能不止绕开后端?

我看到的几个值得继续跟的方向:

  • 把 LLM 生成目标从 PTX 延伸到 MLIR 或者 LLVM IR。理由和选 PTX 相似——它们也是结构化、语义清晰的中间表示,而且跨硬件背景更强,同一套模型有可能同时服务 NVIDIA、AMD 和 CPU 后端。
  • 把验证信号做得更细。目前大多数验证只停留在“结果对没对、跑得快不快”,下一步可以做“逐指令级的性能归因”,告诉模型是哪条指令导致性能差了 20%,这对模型迭代效率的提升会非常明显。
  • 混合路由架构:简单 kernel 走传统编译器,复杂 kernel 或特殊需求走 LLM,中间设一个成本-质量路由。这可能是最快能落到产品里的形态。

最后分享一个我实际踩过坑之后的体会。刚开始复现这类方案时,我总想让模型一步到位生成“惊世骇俗的超级 kernel”,结果模型经常给我生成一个看起来工整、跑起来灾难的 PTX。后来学乖了:先让它严格模仿传统编译器的输出,确保正确性和性能对齐,再逐步放开手脚去做优化。在生成式编译器这个领域,先学会抄写标准答案,再谈创新,这条路径比一上来就追求“超越 ptxas”要稳妥得多。智能化的工具链从来不缺想象力,缺的是能把想象力稳稳接住的那层验证逻辑。你想把编译器交给 AI,第一步永远是给 AI 配好一个不会撒谎的考官。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 10:04:30

SC120AT车规CMOS调试指南:从上电时序到HDR配置的工程实践

简介&#xff1a;这份资源是思特威SC120AT数字CMOS图像传感器的官方数据手册&#xff0c;面向汽车电子视觉系统开发、车载摄像头选型与图像算法调试的工程师&#xff0c;以及需要了解车规级传感器规格的硬件设计人员。SC120AT符合AEC-Q100 Grade 2标准&#xff0c;最高支持1288…

作者头像 李华
网站建设 2026/10/18 9:00:11

Ubuntu换源实战指南:从加速下载到APT深度管控

1. 为什么改软件源不是“折腾”&#xff0c;而是Ubuntu日常运维的必修课你刚装好一台Ubuntu&#xff0c;兴冲冲打开终端敲下sudo apt update&#xff0c;结果卡在0% [Connecting to archive.ubuntu.com]——等了三分钟&#xff0c;进度条纹丝不动。又或者&#xff0c;apt insta…

作者头像 李华
网站建设 2026/10/18 0:24:59

PowerShell编码设置:解决UTF-8乱码问题

1. 为什么需要修改PowerShell编码为UTF-8在Windows系统下工作的开发者经常会遇到这样的场景&#xff1a;当你用PowerShell查看日志文件时&#xff0c;中文字符显示为乱码&#xff1b;当你把PowerShell的输出重定向到文件后&#xff0c;用其他编辑器打开发现编码错误。这些问题的…

作者头像 李华
网站建设 2026/10/16 19:13:03

基于YOLOv11的稻田杂草检测实战:从数据集到部署全解析

田间地头的杂草问题&#xff0c;搞农业的人都知道有多头疼。尤其是水稻田里的稗草&#xff0c;跟秧苗长得那叫一个像&#xff0c;人工拔草费时费力&#xff0c;除草剂撒多了又怕伤苗、怕残留。我去年接到一个智慧农业相关的项目需求&#xff0c;就是要做一套能自动识别稻田杂草…

作者头像 李华
网站建设 2026/10/18 0:41:38

医疗信息化系统开发实战:SpringBoot医院管理系统架构解析

1. 项目概述这个医院医生管理系统后端项目&#xff0c;是我在医疗信息化领域摸爬滚打多年后&#xff0c;总结出的一个轻量级解决方案。它麻雀虽小五脏俱全&#xff0c;包含了医生排班、患者管理、处方开具等核心功能模块&#xff0c;特别适合中小型医疗机构快速搭建自己的业务系…

作者头像 李华
网站建设 2026/10/16 19:13:06

Codex 遇 EDA:从 BOM 脚本到封装库整理的自动化实战

最近几个 EDA 交流群里的画风跟年初完全不一样了。以前大家聊的是哪家封装库全、哪个版本又更新了差分线间距、Gerber 命名怎么统一&#xff0c;现在冒出来一堆新帖子&#xff1a;Codex 怎么装、登录不上怎么处理、能不能用 Codex 把那个 BOM 处理脚本重写一遍、拿 Codex 读嘉立…

作者头像 李华