ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

智能体工作流驱动HPC代码现代化:从Fortran 77到2008的实践

智能体工作流驱动HPC代码现代化:从Fortran 77到2008的实践 1. 项目概述当“老古董”HPC代码遇上智能体工作流在计算化学和材料科学领域GAMESSGeneral Atomic and Molecular Electronic Structure System是一个如雷贯耳的名字。这个诞生于上世纪80年代的量子化学软件包以其强大的功能和开源特性成为了无数科研工作的基石。然而其核心计算模块——特别是用Fortran 77编写的双电子积分Two-Electron Integral核心——在今天看来就像一台需要精心维护的经典老爷车性能卓越但维护和升级的成本极高。我的团队最近接手了一个项目目标就是将这部分的“心脏”代码从陈旧的Fortran 77现代化到Fortran 2008并引入一个Agentic Workflow智能体工作流来系统性地驱动整个过程。这不仅仅是一次简单的语言翻译而是一次涉及代码理解、架构重构、性能优化和验证的深度现代化手术。为什么是双电子积分核心在GAMESS这类进行Hartree-Fock或密度泛函理论DFT计算的程序中双电子积分的计算是绝对的性能瓶颈可能占据超过80%的总计算时间。这部分代码通常经过数十年的手工调优充满了针对特定硬件比如古老的向量机的奇技淫巧代码结构为了极致性能而牺牲了可读性和可维护性。直接重写风险巨大可能引入难以察觉的数值错误放任不管则无法利用现代多核CPU、GPU加速以及更安全的现代语言特性。我们的目标就是在保留其经过验证的数值精度和算法精髓的前提下为其换上现代化的“引擎”和“车身”。而Agentic Workflow就是我们为这次大修量身定制的“自动化手术团队”。它不是一个单一的AI工具而是一个由多个具备不同专长的智能体Agent协同工作的流程。在这个工作流中有的Agent负责代码静态分析和理解有的负责模式识别和转换建议有的负责重构后的性能分析与验证还有的负责协调整个流程和决策。这个项目就是探索如何将这种智能体驱动的工程方法应用于HPC Modernization高性能计算现代化这一传统上高度依赖专家经验的领域。2. 核心挑战与现代化目标解析2.1 剖析Fortran 77遗产代码的“顽疾”在动手之前我们必须彻底理解将要面对的“病人”。一段典型的GAMESS双电子积分Fortran 77代码通常包含以下几类挑战隐式类型与COMMON块Fortran 77中以I-N开头的变量默认为INTEGER其余为REAL。这导致代码中大量变量没有显式声明类型全靠程序员记忆和命名约定。更棘手的是全局数据通过COMMON块共享这些数据块散布在各个子程序中形成了隐晦的、紧耦合的全局状态使得理解数据流和进行单元测试几乎不可能。C 典型的COMMON块和隐式类型 COMMON /SHELL/ KATOM,KTYPE,KNG,KLOC,KMIN,KMAX,NSHELL COMMON /XYZ/ X,Y,Z ... DO 100 I 1, NATOM ! I是整数但NATOM可能来自某个COMMON块 XI X(I) ! X是实数数组来自/XYZ/ COMMON块一个变量XI你无法从声明中知道它是单精度还是双精度它的作用域和生命周期也难以追踪。固定格式与GOTO逻辑代码是固定格式第6列缩进第72列换行注释以C开头。控制流严重依赖GOTO和算术IF语句形成了所谓的“面条代码”Spaghetti Code。追踪一个条件分支可能需要跨越多个文件。IF (SOME_COND) GOTO 200 ... ! 一些代码 GOTO 300 200 CONTINUE ... ! 另一段代码 300 CONTINUE面向硬件的优化“黑魔法”为了在Cray或旧式向量机上榨干性能代码中充满了手工展开的循环、特殊的内存访问模式例如为了向量化而进行的数组重排、以及内联的汇编或特定编译器指令。这些优化在现代CPU上可能无效甚至有害但它们与算法逻辑深度纠缠难以剥离。模块化缺失与巨型子程序功能边界模糊一个子程序动辄上千行承担了计算、I/O、错误处理等多种职责。这使得代码复用和独立测试变得异常困难。2.2 定义现代化目标不止于语法升级我们的现代化目标有清晰的层次绝非简单的f77到f08的语法转换功能等价性最高优先级现代化后的代码在给定相同的输入时必须产生比特级相同或在误差容限内数值等价的输出。这是底线任何偏离都意味着项目失败。提升可维护性与可读性显式接口使用MODULE封装数据和过程用USE语句进行显式导入消灭COMMON块。现代控制流用DO...END DO、SELECT CASE、IF...THEN...ELSE...END IF取代GOTO。显式变量声明强制使用IMPLICIT NONE所有变量必须显式声明其类型、种类KIND和意图INTENT。提升模块化将巨型子程序按功能拆分为更小、职责单一的模块和过程。为未来性能优化奠基数据抽象使用派生类型TYPE封装相关数据为未来引入更复杂的数据结构做准备。数组语法与内在函数利用Fortran 90/95/2003的数组操作和MATMUL、DOT_PRODUCT等内在函数这些通常能被编译器高效优化。暴露并行化机会通过代码重构让循环依赖和数据访问模式更清晰为后续添加OpenMP或OpenACC指令创造条件。启用现代工具链支持现代化后的代码应能更好地与现代版本控制系统Git、构建系统CMake、调试器、性能分析器如Intel VTune, gprof和静态分析工具协同工作。3. Agentic Workflow 的设计与智能体分工传统的代码迁移依赖资深工程师人工进行耗时耗力且容易出错。我们设计的Agentic Workflow旨在将这个过程系统化、自动化并融入领域知识。整个工作流由多个智能体协同完成它们并非通用大语言模型而是针对特定任务进行了提示工程Prompt Engineering和工具链集成的专用“数字员工”。3.1 工作流总体架构我们的工作流是一个分阶段的、迭代的管道每个阶段由主导智能体负责并可以调用其他智能体或外部工具辅助。核心阶段包括分析 - 转换 - 重构 - 验证。[原始F77代码库] - [分析Agent] - [代码理解报告 映射清单] | v [转换Agent] - [初始F08版本] - [重构Agent] - [优化后F08版本] | | v v [验证Agent] ------------------------------ [持续验证] | v [最终现代化代码 完整报告]3.2 核心智能体角色与任务分析Agent代码考古学家输入整个Fortran 77源代码树。工具静态分析工具如fortran-syntax解析器、自定义脚本、知识图谱构建器。核心任务绘制调用图解析所有CALL语句和FUNCTION引用建立子程序/函数之间的调用关系。追踪数据流识别所有COMMON块分析哪些变量在哪些子程序中被读写构建全局数据依赖图。这是最繁琐也最关键的一步。识别模式与异味标记出所有GOTO、算术IF、隐式类型变量、固定格式循环等需要转换的模式。生成“地图”输出一份详细的报告包括需要创建的Module列表每个COMMON块对应一个或多个Module、需要拆分的巨型子程序、潜在的并行化热点循环。实操心得我们让分析Agent优先处理那些被频繁调用的核心计算例程。对于某些极其复杂的GOTO链Agent会将其可视化成一个有向图帮助人类工程师理解其真实意图是循环控制、错误处理还是状态机。转换Agent语法翻译官输入分析Agent的报告 单个F77源文件。工具基于规则的代码转换引擎如f2f或自定义的AST转换工具、Fortran语言规范。核心任务基础语法转换将固定格式转换为自由格式将REAL*8等非标准声明转换为REAL(KIND8)或使用iso_fortran_env中的REAL64将DO循环标签转换为DO...END DO结构。初步结构转换根据分析Agent的“地图”将COMMON /BLOCK/ A, B, C初步转换为一个Modulemodule_block包含变量A, B, C并在原位置添加USE module_block。注意这只是机械的一对一转换还未进行逻辑重构。处理简单GOTO将一些明显的、结构简单的GOTO循环或条件分支转换为DO或IF语句。注意事项转换Agent必须非常保守。对于无法确定意图的复杂控制流或模糊的数据依赖它不应强行转换而是高亮标记出来留给重构Agent或人类处理。“不破坏”比“全转换”更重要。重构Agent软件设计师输入经转换Agent初步处理后的F08代码 分析Agent的深度分析报告。工具代码重构引擎、领域知识库关于双电子积分算法、量子化学概念。核心任务逻辑重构处理转换Agent留下的“硬骨头”。例如将一个用GOTO实现的复杂状态机重构成基于SELECT CASE和状态变量的清晰结构。模块化设计合并相关的、小的Module将大子程序按功能拆分成多个更小的子程序或内部函数引入派生类型来封装逻辑上属于一体的数据如将一个基组shell的所有信息封装进一个type shell_info。接口现代化为所有子程序添加明确的INTENT(IN),INTENT(OUT),INTENT(INOUT)属性将频繁传递的大型参数列表考虑改为传递派生类型实例。引入现代特性在合适的地方用数组切片操作替换显式循环用内置函数替换手写的计算函数如用NORM2计算范数。实操心得重构Agent需要与领域知识紧密结合。我们为其灌输了双电子积分计算中常见的算法模式如“壳层Shell循环”、“收缩高斯函数Contracted Gaussian处理”等使其能识别这些模式并给出更合理的重构建议而不是进行盲目的语法变换。验证Agent质量守门员输入原始F77代码 现代化后的F08代码 一组测试用例单元测试和集成测试。工具测试框架如pFUnit、编译脚本、数值比较工具、性能分析工具。核心任务编译验证确保新代码能用现代Fortran编译器如Intel ifort, GNU gfortran, NVIDIA nvfortran无错误、无警告地编译。功能等价性测试这是核心。运行相同的测试输入比较新旧代码的输出。对于浮点数我们设定严格的相对误差和绝对误差容限例如1.0E-12。验证Agent需要运行大量的小型测试针对单个积分函数和几个完整的中型计算案例。性能回归测试在相同的硬件和编译优化选项下比较关键函数的执行时间。现代化初期性能持平或略有下降是可接受的但需明确记录。生成验证报告详细列出所有测试通过/失败的情况对数值差异进行统计分析并定位到可能出错的代码区域反馈给重构Agent进行下一轮迭代。注意事项测试用例的构建至关重要。我们除了使用GAMESS自带的测试集还额外生成了大量针对边界条件小距离、大角动量量子数的测试以确保数值稳定性。协调Agent项目经理角色这个智能体不直接修改代码而是管理整个工作流。它接收验证Agent的报告决定是进入下一轮重构迭代还是将问题上报给人类工程师。它管理代码版本确保每次迭代都在可控范围内。4. 实操过程以双电子积分核心模块为例下面我以一个简化的双电子积分计算循环为例展示工作流如何逐步将其现代化。假设我们有一个核心子程序CALC_2E_INT它计算两个电子壳层shell之间的所有双电子积分。4.1 阶段一分析Agent的发现原始F77代码片段极度简化SUBROUTINE CALC_2E_INT(ISH, JSH, KSH, LSH, INTS, NINT) C 计算壳层ISH, JSH, KSH, LSH之间的双电子积分 IMPLICIT REAL*8 (A-H,O-Z) COMMON /BASIS/ X(100), Y(100), Z(100), EXPON(100), COEF(100) COMMON /SHELL/ KATOM(50), KTYPE(50), KSTART(50), KNG(50) DIMENSION INTS(*) ... C 获取壳层信息 I KSTART(ISH) J KSTART(JSH) NI KNG(ISH) NJ KNG(JSH) ... C 四重循环计算原始高斯函数积分 DO 100 IP 1, NI DO 100 JP 1, NJ DO 100 KP 1, NK DO 100 LP 1, NL ... ! 复杂的积分计算涉及大量GOTO和中间变量 INTS(INDEX) VALUE 100 CONTINUE ... IF (SOME_ERR) GOTO 999 RETURN 999 WRITE(6,*) ERROR IN 2E INTEGRAL STOP END分析Agent报告指出使用了两个COMMON块/BASIS/和/SHELL/传递全局数据。隐式类型REAL*8变量I, J, NI, NJ等未声明。四重嵌套的固定格式DO循环使用标签100。简单的错误处理通过GOTO 999跳转到错误输出并停止。子程序参数INTS和NINT用途不明INTS似乎是一个输出数组。4.2 阶段二转换Agent的初步处理转换Agent根据规则进行一对一转换生成初步的F08代码module basis_data implicit none real(kind8), save :: x(100), y(100), z(100), expon(100), coef(100) end module basis_data module shell_data implicit none integer, save :: katom(50), ktype(50), kstart(50), kng(50) end module shell_data subroutine calc_2e_int(ish, jsh, ksh, lsh, ints, nint) ! 计算壳层ish, jsh, ksh, lsh之间的双电子积分 use basis_data use shell_data implicit none integer, intent(in) :: ish, jsh, ksh, lsh, nint real(kind8), intent(out) :: ints(nint) integer :: i, j, ni, nj, nk, nl, ip, jp, kp, lp, index real(kind8) :: value ! ... 其他局部变量 ! 获取壳层信息 i kstart(ish) j kstart(jsh) ni kng(ish) nj kng(jsh) ! ... ! 四重循环计算原始高斯函数积分 do ip 1, ni do jp 1, nj do kp 1, nk do lp 1, nl ... ! 复杂的积分计算 ints(index) value end do end do end do end do ! ... if (some_err) then write(6,*) ERROR IN 2E INTEGRAL stop end if end subroutine calc_2e_int转换Agent的成果语法现代化了COMMON块变成了Module循环结构清晰了变量有了显式声明和意图属性。但代码结构依然臃肿错误处理粗暴数据传递方式原始。4.3 阶段三重构Agent的深度优化重构Agent介入它拥有算法知识认为这个子程序承担了太多职责数据获取从全局模块中提取壳层信息。积分核心计算四重循环。错误处理。重构Agent提出并执行以下重构数据抽象创建派生类型shell_type来封装一个壳层的所有信息。module shell_type_mod use iso_fortran_env, only: real64 implicit none private public :: shell_type type :: shell_type integer :: atom_index integer :: angular_momentum integer :: start_index ! 在全局基函数中的起始位置 integer :: num_primitives real(real64), allocatable :: exponents(:) real(real64), allocatable :: coefficients(:) real(real64) :: center(3) end type shell_type end module shell_type_mod职责分离将庞大的calc_2e_int拆分成更小的、专注的子程序。compute_shell_quadruplet: 协调四个壳层的积分计算。compute_primitive_quartet: 计算四个原始高斯函数的积分最内层核心。contract_primitives_to_shells: 将原始积分收缩到壳层。改进接口和错误处理使用派生类型作为参数用返回错误码代替STOP。subroutine compute_shell_quadruplet(shell_i, shell_j, shell_k, shell_l, integrals, error) use shell_type_mod, only: shell_type, real64 implicit none type(shell_type), intent(in) :: shell_i, shell_j, shell_k, shell_l real(real64), intent(out) :: integrals(:) integer, intent(out) :: error ! 0表示成功非0为错误码 integer :: nints_expected ! 检查输入合法性 if (.not. valid_shell_pair(shell_i, shell_j) .or. ...) then error 1 ! 无效壳层输入 return end if nints_expected number_of_integrals(shell_i, shell_j, shell_k, shell_l) if (size(integrals) nints_expected) then error 2 ! 输出数组太小 return end if error 0 ! ... 调用更底层的计算例程 end subroutine compute_shell_quadruplet引入现代特性在合适的地方用数组操作简化代码。例如计算两个壳层中心的距离! 旧方式 rij2 (xi - xj)**2 (yi - yj)**2 (zi - zj)**2 ! 新方式 rij_vec shell_i%center - shell_j%center rij2 dot_product(rij_vec, rij_vec)4.4 阶段四验证Agent的严格把关验证Agent会为重构前后的代码运行相同的测试套件单元测试针对新的小函数如compute_primitive_quartet提供固定的、小规模的输入验证其输出与从原代码中“剥离”出的对应部分是否一致。集成测试调用新的compute_shell_quadruplet与旧的CALC_2E_INT对比计算一系列不同角动量、不同距离的壳层对积分确保所有积分值在1.0E-14的相对误差内。性能测试在相同的单核上运行新的模块化代码可能因函数调用开销而稍慢这是可接受的。验证Agent会记录这个基线性能供后续优化参考。如果验证失败协调Agent会将问题区域例如某个特定壳层组合的积分误差超标反馈给重构Agent触发新一轮的分析-重构-验证循环。5. 经验总结、避坑指南与未来展望经过这个项目的锤炼我对Agentic Workflow在HPC现代化中的应用有了深刻体会。5.1 核心经验与心得智能体是“副驾驶”不是“自动驾驶”最成功的模式是人机协同。分析Agent生成的依赖图需要工程师复核重构Agent提出的方案需要基于领域知识进行判断验证Agent报出的差异需要人工甄别是数值误差还是逻辑错误。工程师负责制定策略、审核关键决策和解决模糊问题。测试用例是生命线没有全面、精准的测试用例现代化工程就是盲人摸象。必须在项目开始前投入大量精力构建一个强大的测试套件包括单元测试、集成测试和端到端测试。这些测试不仅是验证工具也是定义“功能等价”的契约。增量式、迭代式推进不要试图一次性现代化整个百万行代码库。我们的策略是“分而治之”垂直切片先选择一个独立的、功能边界清晰的模块如一个特定的积分算法进行全流程现代化打通工具链。水平扩展在此基础上逐步处理其他模块同时建立公共的现代化基础设施如公共数据类型Module。每完成一个切片就立即集成和测试确保主干始终是可工作的。保留性能优化的灵活性初期重构以清晰化为目标不要过早追求性能。清晰的代码结构更容易进行性能分析和优化。例如将四重循环的核心计算独立成函数后我们后续可以更容易地尝试在该函数内添加OpenMP指令或将其移植到GPU上。5.2 常见问题与排查技巧数值差异问题现象验证测试失败新旧代码结果有微小差异。排查检查种类KIND一致性确保所有REAL变量具有相同的KIND如REAL64。旧代码中的REAL*8可能与新代码中的REAL(KIND8)不完全等价取决于编译器。使用iso_fortran_env中的REAL64最安全。检查运算顺序Fortran不严格规定求值顺序。数组操作A B C * D与显式循环可能因顺序不同产生细微差异。如果差异可接受可以放宽误差容限。如果不可接受可能需要暂时保留显式循环。检查函数/子程序参数意图错误的INTENT(INOUT)可能导致输入被意外修改。工具使用调试器逐行对比或插入打印语句输出中间计算结果。编译警告与兼容性现象新代码编译通过但有大量警告。处理严肃对待每一个警告。常见的有未使用变量、隐式类型转换、过时特性等。消除警告是提升代码质量的重要一步。可以设置编译器选项将警告视为错误如-Werrorin gfortran。性能下降现象新代码正确但运行变慢。分析函数调用开销过度模块化可能导致大量小函数调用。对于最内层热循环考虑使用inline提示如果编译器支持或将关键部分内联。数组临时变量复杂的数组表达式A matmul(B, C) D可能创建临时数组。对于性能关键部分使用显式循环或do concurrent可能更优。内存访问模式重构可能改变了数据布局影响缓存利用率。使用性能分析工具如perf、VTune定位热点和缓存未命中。策略先保证正确和清晰再针对性能瓶颈进行有依据的优化。5.3 未来工作方向这次项目验证了Agentic Workflow在复杂遗产代码现代化中的可行性。展望未来有几个方向值得深入更智能的重构Agent结合深度学习让Agent不仅能识别语法模式还能理解更高层次的算法意图提出更优的重构方案例如自动识别并应用常见的HPC优化模式。与形式化验证结合对于航天、核能等安全关键领域的HPC代码是否可以结合形式化方法用智能体辅助生成部分代码的验证条件Proof Obligation多目标优化让工作流在现代化过程中同时考虑性能、功耗、数值稳定性等多个目标进行自动化的权衡探索。知识库的持续积累将本次项目中形成的关于Fortran现代化、双电子积分计算的知识沉淀到智能体的知识库中使其能更好地处理下一个类似的量子化学代码。现代化之旅从未轻松但有了智能体工作流作为“倍增器”我们能够更系统、更自信地对那些至关重要的“老古董”代码进行手术让它们在新的计算时代继续发挥核心价值。这个过程本身也是将领域专家经验转化为可重复、可扩展的自动化流程的宝贵实践。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进