
第一次把自研AI芯片的软件栈画成一张地图是在那台编译服务器连续三次内存溢出之后。硬件那边流片早就回来了测试芯片能跑引导程序、能点灯芯片团队觉得胜利在望软件团队打开仓库却有点沉默驱动、运行时、编译器、Python推理框架、模型转换工具加起来几十万行代码分布在十几个仓库再加上三四套构建系统和跨语言的胶水层没有任何一份文档能把“一行PyTorch模型从Python层走到芯片执行”的全过程说清楚。我就是在那时候决定用Agent把这块芯片的全栈软件地形自动测绘出来让新人也能够在入职三小时内知道去哪里改代码让做芯片架构的同事也能在白板上指着某个NPU核讲清楚它的软件支撑到底依赖什么。这是我“用Agent做一块AI芯片”系列的第二篇但即使你没看过第一篇也不影响阅读——这一篇的核心是软件栈的“地图化”方法以及Agent在这个过程中到底怎么干活。先说结论所谓全栈软件地图不是画一张漂亮的架构图。它要回答三个问题——一个模型请求从哪进来经过哪些模块、结构体和接口转换最终在哪颗计算核上以什么格式执行。把这三个问题的答案变成可检索的索引、可维护的文档和清晰的依赖拓扑你的软件栈才算真正“可导航”。这篇文章适合两种人一种是我这种正在做AI芯片或专用加速芯片软件栈的需要快速梳理并行团队堆起来的代码另一种是手里有几十万行C/Python混合项目、想用Agent自动分析依赖关系并持续维护项目地图的。后面你在单测、代码评审、故障排查甚至文档生成上遇到的场景跟它是同一套手法的延伸。1. 为什么一张“软件地图”比芯片本身更值钱1.1 AI芯片的死法不是死在流片是死在“软件跑不起来”聊AI芯片的人总爱把流片当作分水岭好像芯片回来后一切都会自然发生。实际上流片成功只是入场券。一块AI芯片要被人真正用起来必须让PyTorch模型、ONNX模型或TFLite模型跑出可复现的结果而且性能还有指标要求。为了这个“跑起来”你可能得同时打通驱动、内核模块、用户态运行时、编译器、算子库、图优化器和框架适配层。任何一个环节断了整个芯片在用户眼里就是一块废硅。我见过太多项目死在“软件跑不起来”上驱动能加载但Host和Device间的一次DMA搬运就要几毫秒算子库有几百个kernel但跑第一个ResNet全模型时发现关键算子没有实现编译器能编出汇编但生成的指令流在Cache策略上完全没对齐芯片的硬件设计。这些问题想快速定位靠的往往不是逐行读代码而是先有一张“地图”——知道问题大概出在哪个层、哪条链路上再进去细看。全栈软件地图最大的价值就在这里它不是用来给老板汇报的PPT而是用来回答“哪里最痛、哪里最短、哪两个模块看似无关实则强耦合”的工程工具。1.2 “全栈”到底指哪几个栈算力栈、工具链栈、应用栈很多人一听到“全栈”下意识以为是把AI芯片的所有代码都看完。这个想法既有误导性又无可行性——几十万行代码塞进任何人的脑子或上下文结果都是一团浆糊。我习惯把AI芯片的软件体系拆成三个栈算力栈指芯片计算单元的直接软件支撑包括驱动、运行时、内存管理、调度器、算子和底层加速指令。工具链栈指把模型转换成芯片可执行格式的编译器、图优化器、量化校准工具和性能剖析器。应用栈指面向用户的一层——Python框架绑定、推理服务SDK、模型仓库适配以及上层Agent/应用调用推理能力时的API边界。三个栈的分界要画清楚因为不同团队维护不同栈Agent在测绘时也要分开采样、分开建模否则会把编译器的LLVM Pass和运行时里的调度队列混在一张图上越画越乱。我们这篇文章重点谈算力栈和工具链栈的“地图化”应用栈留到结尾讲和Agent的衔接。2. 拆开一块AI芯片的软件栈从裸硅到模型服务一共有几层2.1 硬件抽象层寄存器、DMA和计算单元的“国家边界”任何AI芯片的软件地图最底层一定是硬件抽象层。它定义了软件和芯片之间的“国界”哪些寄存器是可写的、哪些只能读、中断号怎么分配、MMIO地址范围在哪。以我们自研芯片为例硬件抽象层覆盖这些实体计算核我们叫NPU Core的寄存器组包括控制状态寄存器、启动寄存器、中断状态寄存器。Global Memory和Local SRAM的地址窗口DMA搬运的目标和源地址都要映射进来。片间互联NoC/总线的端口配置多核访问全局内存时要走的路径。同步与事件机制几个核同时完成一阶段计算后怎么互相通知。Agent测绘这一层的核心任务是识别两类东西一类是物理硬件的“暴露面”比如mmio_base.h头文件里的地址宏定义另一类是软件访问这些暴露面的唯一入口比如hw_reg_read(reg_t addr)的封装函数。一份好的地图要把“哪些代码直接摸了寄存器”标出来——这些代码如果写坏了整块芯片就可能卡死或跑飞它们是代码评审和测试时最需要关注的地方。实际测绘时我让Agent重点检索这些模式#define地址常量或REG_BASE这类宏readl/writel/ioremap/mmap设备寄存器设备树文件.dts/.dtsi中描述的中断号和地址范围。Agent找到这些之后会把它们记成一张“硬件接触面表”后续每一层软件向下依赖这张表谁乱动它谁的依赖就会断裂。2.2 编译层从计算图到指令流的“翻译官”编译层是AI芯片软件栈里最复杂的部分也是外界最难理解的。芯片不认PyTorch模型只认自己定义的指令或微码。中间的翻译落在编译器身上。编译器收到模型之后要经历这些步骤接收计算图PyTorch的TorchScript、ONNX或TFLite FlatBuffer。做算子分解和融合比如把Conv、BatchNorm、ReLU合并成一个融合算子减少内存访问次数。图优化常量折叠、死代码消除、布局转换NCHW转NHWC或芯片特殊的tile布局。算子选择把融合后的算子映射到芯片已有的预编译内核或可编程指令模板上。内存规划把张量安排到Global Memory、Local SRAM、寄存器文件三级存储中。指令生成和调度产出芯片指令流并尽可能做双缓冲、流水线重叠。Agent测绘编译层时最容易抓狂因为编译器的抽象层次极多IR中间表示就有好几种Pass链从IR进来再回到IR中间还有各种方言。我让Agent按“输入格式—IR中间态—Pass链—代码生成后端”四个小结构去扫而不是期望它把整个编译器的控制流都理解清楚。举个例子我们的编译器里有这样一层代码专门把ONNX的Conv节点翻译成芯片的卷积指令模板struct ConvLowering : public OpLowering { LowerResult lower(const onnx::NodeProto node, const MappingContext ctx) override { // 从ONNX属性里读取kernel_shape、strides、pads auto kernel node.attr(kernel_shape); auto strides node.attr(strides); // 查算子库元数据拿到对应NPU Core指令模板 auto tpl op_lib_-find(npu.conv2d, kernel, strides); if (!tpl) return LowerResult::unsupported(node); // 做内存布局转换把输入tensor重排成tile格式 auto layout tpl-required_layout(); auto input ctx.rewrite_layout(node.input(0), layout); // 输出指令候选 return LowerResult::success(tpl-emit(input, node.output(0))); } };这一层的地图重点不是画出每一个Pass而是画出“模型入口—Pass主链—后端出口”的骨干并标清楚哪些算子支持、哪些走fallback、哪些会报Unsupported。有这张表模型移植时一遇到跑不起来的算子你就能马上判断到底是编译层的算子选择失败还是运行时缺实现。2.3 运行时与驱动层内存、调度、并发都在这里兜底编译层把模型翻译成指令流运行时和驱动负责把这串指令流真正送进芯片并管理芯片上的资源。运行时层有两套最常见的架构Host侧Runtime运行在CPU上负责申请内存、创建执行流Stream、提交任务、等待完成。类似CUDA的运行时API。设备侧Firmware运行在芯片内部负责解释Host下发的指令队列、调度计算核、处理中断和错误。Agent测绘这层的时候我安排它优先追踪这些关键对象Device、Context、Stream、Event这些核心类之间的生命周期关系。内存管理malloc_device、memcpy、free_device的调用链以及内存池的实现位置。任务提交语义是同步sync提交还是异步streamevent的依赖关系。错误处理路径芯片报错后错误码从哪里产生、传到哪里、用户API在哪个位置能拿到。这里有个特别容易忽略的地方也是我建议Agent必须采集的同步原语。多颗NPU Core并行计算时谁等谁、谁通知谁全部靠运行时层的同步机制。如果地图里没有把这些信号量、栅栏、事件对象标出来后续做性能调优时你连并行瓶颈在哪都找不到。2.4 框架与应用层云边端模型的入口和出口框架适配层决定了用户能不能把模型喂给你的芯片。常见做法是提供ONNX Runtime的ExecutionProvider或者PyTorch的Custom Backend。这层代码量通常不大但极考“耐心”你要对接PyTorch复杂的前端逻辑还要处理Tensor在框架和设备之间的拷贝、DFS和自动微分钩子有些模型算子会直接走CPU fallback性能突然掉一个数量级。Agent在这层的主要任务是画出“框架入口—张量搬运—设备侧执行—返回结果”的生命周期。我让它重点找这些内容torch::Tensor到设备张量的转换函数例如to_blob或from_blob。框架调用设备算子的适配器例如at::native::里的自定义函数。ONNX Runtime里的GetCapability()和Compile()接口实现。另外这层是未来AI应用接入最频繁的地方。如果你想让上层Agent模型编排、自动调参、推理服务网关调用芯片算力它们在框架API层看到的就是你的执行入口和鉴权接口。地图上把这层标注得越清楚上层系统的开发速度越快。3. 让Agent当测绘员我用什么架构把软件栈画成地图3.1 Agent在这个任务里到底干哪些活让Agent画地图不是把几十万行代码丢给一个聊天窗口然后问“帮我总结一下”那是幻觉制造机。我用的方式是把Agent当作“测绘员团队”给它划分明确的岗位侦察员Sampler负责全局扫描用代码检索工具找关键符号、文件和依赖。分析师Analyzer负责深度解析单个模块读取特定文件并提炼结构。验证员Verifier负责检查分析结果例如确认头文件是否存在、符号是否真的被引用。制图员Cartographer负责把前几位的产出汇总成层级结构、依赖索引和Markdown文档。这种角色划分的好处是每个Agent的任务边界清晰上下文窗口可以控制得很小幻觉的扩散空间被压缩。坏处是你要多花时间做编排但实测下来产出质量比单一大模型高很多。3.2 整体流程采样代码 — 解析依赖 — 验证拓扑 — 产出地图我的Agent测绘流程固定为四步每一步都对应明确的输入和输出采样代码用rgripgrep全仓库搜索关键pattern比如REG_BASE、ONNX、torch::Tensor、Build目录下的CMakeLists.txt、BUILD.bazel。按栈的层次切分搜索范围避免一次搜全仓库。解析依赖读取各类构建文件提取目标、源码文件列表、头文件路径和依赖项。对C代码用tree-sitter或clang提取符号引用对Python代码用ast解析import关系和函数调用关系。验证拓扑让Agent随机选一批“关键路径”做交叉验证。比如从某个算子入口出发追踪它的实现和调用者确认地图上的链路与实际代码一致。凡是验证不出来的依赖边一律标记为“待人工确认”绝不写进最终地图的“确定”区域。产出地图按层次组织输出Markdown文档和JSON拓扑文件。JSON拓扑文件是给机器用的后续增量更新和性能分析都在它基础上做。3.3 工具和提示词我实际用的那套组合工具选择是个门槛选错了Agent再怎么调也白搭。我用的是这套组合用途工具说明代码搜索ripgrep /rg --json全仓库快速搜索输出结构化结果Agent可直接消费符号提取tree-sitter CLI、clang AST dump解析C/C/Python语法提取函数、类、引用关系构建依赖cmake-file-api、bazel query从构建系统拿目标级依赖比静态扫描准确文件解析grep jq 小型Python脚本轻量处理JSON、日志、头文件宏定义交互框架Agent框架 自定义工具封装让Agent能调用上面这些命令而不是把代码塞进上下文给侦察员Agent的提示词我经过多轮迭代后基本定格成了这样你是AI芯片软件栈的代码侦察员。现在需要你完成以下任务 1. 在指定目录下搜索所有包含 NPU_REG_BASE 或 REG_BASE 的代码文件。 2. 对每个结果输出文件路径、行号、宏名和附近代码片段。 3. 如果搜索结果超过50条按目录聚合后再输出。 不要做任何推测不要解释代码逻辑只报告你看到的事实。 输出格式Markdown表格。看到没约束Agent“不做推测、只报告事实”是减少幻觉的关键。很多Agent任务翻车不是因为模型不够聪明而是因为提示词没告诉它哪些事情不要做。4. 实测记录Agent给我画出的第一版地图4.1 规划阶段给Agent定的边界和验收标准在让Agent正式跑之前我先花了半天定义边界否则Agent会失控。我最终圈定的范围是只覆盖算力栈和工具链栈不碰芯片验证的UVM平台不碰上层云编排。代码仓库限定为device-driver、runtime、compiler、oplib、framework-adapter。验收标准地图能让一个刚入职的工程师在30分钟内找到“运行一个ResNet18模型”所涉及的全部核心文件和关键路径。同时我给了Agent一个“禁区清单”不生成流程图、不生成架构美化图、不输出性能推测、不给出优化建议只做事实测绘。这一步很重要因为Agent一旦开始自由发挥你得到的就是一篇看起来很有逻辑但没法用的“AI幻觉纪要”。4.2 执行阶段Agent的代码采样、追踪和归档步骤整个执行跑了两轮第一轮花了大约四个小时第二轮用来修正错漏又花了两个小时。第一轮的主要步骤全局扫描侦察员Agent对五个仓库分别跑rg搜索几十组pattern包括驱动入口、寄存器宏、DMA描述符、算子注册宏、ONNX Exporter、PyTorch Backend等。构建依赖提取用cmake-file-api和bazel query分别生成目标视图再让Agent把目标视图里每个文件映射到它分析出的模块上。深度追踪分析师Agent从model_inference入口函数开始往下追记录每一层调用的函数、所在文件和头文件依赖生成调用栈快照。交叉验证验证员Agent随机抽了20条关键路径对每一条路径从反方向从底层硬件抽象层向上重新追踪把不一致的依赖边全部丢到“待确认”列表。最终产出物是四个文件software_stack_architecture.md人类可读地图dependency_graph.json机器可读拓扑hardware_contact_surface.csv硬件接触面清单pending_review_list.md待人工确认清单这份地图的最大价值是让我第一次完整看到了“一个模型请求”从ONNX文件进入编译器、经过四个铁律类Pass、在算子库命中一个NPU卷积模板、再经过运行时内存规划和DMA调度最后落到NPU Core指令发射的完整旅程。将近几十个人合作写了两年多的代码第一次像一本地图册一样摊开在眼前。4.3 结果复盘哪些地方画对了哪些地方在瞎编复盘结果让我挺意外的。Agent对构建依赖和符号引用的追踪准确率很高对宏定义和底层地址映射的挖掘也很可靠。但有三类问题是明显瞎编或者至少不准确的把同名不同作用域的变量连成了依赖边比如两个不同文件里都有input_成员Agent容易画出一条根本不存在的引用关系。把注释里的老接口当成活跃代码导致地图里出现一些早就废弃的模块。对“运行在Host侧还是设备侧”判断不清有些类明明跑在设备固件里Agent画成了Host运行时的一部分这会误导性能分析。所以我一直强调验证阶段不能省。第一版地图里Agent自己列出的“待确认依赖边”有四十多条这不算丢人——它知道自己不确定哪些比假装确定强得多。我建议你无论用哪个Agent框架都要保留一个机制让Agent在置信度不足时输出UNKNOWN而不是生造一个可能性出来。5. 地图之后从“静态地形”变成“动态导航”5.1 把git提交和CI当作心跳让地图自动更新静态地图发布几天后就会过期这是必然的。仓库每天都在变新增算子、改接口、迁移模块任何变动都会让地图失真。所以地图必须从一次性测绘变成可持续更新的“动态导航”。我目前的方案是给Agent接入了两个“心跳”信号git提交事件每次merge到主干时Agent把变更文件列表和地图对应层做比对。如果变更涉及某一层的核心模块就标记该层为“待更新”。CI构建事件当CI失败或特定模板测试开始跑时Agent自动在对应层次追加一条“构建失败提示”让地图不仅能导航还能预警。增量更新不用每次全量跑Agent只需要对变化文件做局部分析。比如一个算子库新增了某个NPU Kernel就只更新算子库节点及它与编译层的依赖边其余部分不动。更新后的JSON拓扑按时间戳存档方便回溯“地图的某个判断是何时被修正的”。5.2 软件地图怎么反哺芯片架构和软硬件协同地图画出来之后最大的收益之一是它能反哺芯片下一版的架构决策。我们不到一个星期就用地图找到了一个重要瓶颈从编译器到运行时所有的张量描述符都要经过一个极长的序列化/反序列化过程导致一个小模型的一次推理光搬运描述符就要浪费大量时间。这个结论放到地图上非常直观编译器生成的“tensor descriptor”先转成protobuf消息运行时再解析成内部结构最后驱动又把它转成固件能认的二进制定点结构。三次转换在三个层里分别进行每层都觉得自己设计合理但连起来看这个链路明显可以砍掉一次或两次转换。于是我们提给芯片架构团队一个建议下一版在硬件里增加一个计算描述符的专用寄存器区域减少CPU侧转换。这个建议没有地图根本没法快速定位到。所以软件地图不只是拿来“给新人看”的它更是一张瓶颈地图每个层次的边界、每次跨层调用的代价、每个序列化/反序列化点都会跃然纸上。软硬件协同优化的切入点往往就藏在这些边界角落里。5.3 多Agent按层协作时的任务切分方式既然一张软件栈天然分成了多个层次多Agent协作时就可以按层切分任务而不是按仓库切分。我建议切分方式如下硬件层Agent负责寄存器、DMA、中断、地址映射。编译层Agent负责Pass链、IR转换、算子选择、后端代码生成。运行层Agent负责内存池、执行队列、同步机制、设备固件交互。框架层Agent负责PyTorch/ONNX Runtime适配、张量生命周期管理。层与层之间通信靠一个共享的“接口边界表”完成。比如编译层输出一个EmittedKernel结构运行层Agent不需要理解编译器的Pass逻辑只要知道这个结构的字段含义和生命周期就可以了。这样每个Agent的上下文窗口都很小既能深入分析又不会相互干扰。这种按层切分还有一个好处评审和验收也按层做。硬件工程师评审硬件层的地图编译器团队评审编译层的地图而不是让一个不懂驱动的人去确认整个系统图。多Agent系统要真正可落地职责边界比模型选型更重要。6. 这次测绘踩过的坑比收获还多6.1 上下文窗口塞爆给Agent喂全仓库是个馊主意最早我犯过一个非常典型的错误为了让Agent“全面理解系统”我把整个仓库的代码摘要甚至关键文件全文都塞进它的上下文里结果几分钟后Agent就开始胡说八道——不是它笨而是上下文太长之后注意力被大量无关文件占据关键依赖反而抓不住。后来我把策略改成了“工具存取按需注入”Agent想读哪个文件就通过工具去读读完的结论存进工作区不占用长期上下文Agent要横向搜索就调用rg或bazel query拿结构化结果。上下文里只保留当前子任务的局部信息。这个调整的收益立竿见影地图的质量直接上了一个台阶。一句话总结让Agent当测绘员是靠它手里的工具包取胜不是靠它把整个城市的地图背下来。6.2 幻觉模块如何识别和拦截不存在的依赖Agent最大的毛病是会在依赖图里“编”出一些看起来合理但根本不存在的模块。例如某个头文件路径如果Agent只根据记忆里的模式判断可能直接写出一个include/npu/core/conv2d_engine.h但实际这个路径在仓库里并不存在。我的拦截方法是三重验证第一重工具验证所有路径、宏、符号Agent必须调用ls/rg/cmake query确认存在。第二重反向追踪从地图上的依赖边B反向查回A看A的调用栈里是否真的出现了B。第三重编译验证对于最关键的头文件和依赖关系直接用编译器的依赖扫描工具生成精确的#include关系做比对。经过三重验证后幻觉模块的比例会降到很低。但你要接受一个现实不可能降到零。所以地图上要保留“置信度”字段低于某置信度的依赖边不参与后续的自动化分析只作为参考信息。6.3 版本漂移地图生出来那天就已经过时了最后一条经验是关于“更新成本”的。第一版地图用了不少精力但三天后我再看它已经有一些细节过时了——某个目录被重命名某个接口增加参数某个Pass链新增了一个优化步骤。所以我现在反复跟团队强调地图不是文档地图是“可更新的数据库”。我们给JSON拓扑文件配置了结构化校验定期跟每个仓库的主干分支做一次差异比对每次大版本变更触发一次局部更新。Agent看起来像一次性的美差做多了其实是在维护一个不断生长的软件地形数据库。这套东西跑通之后团队里一句很常见的话是“帮我查一下这个报错顺着调用链是哪一层抛出来的”以前摸半天现在可以直接用地图索引定位然后把相关代码拎出来看。这才是做全栈软件地图真正让人爽到的时刻。这次测绘给我的体会是AI芯片的量产门槛从来不只是晶圆和封装软件栈的“可知性”可能更致命。Agent作为测绘员本意不是替代软件工程师而是把工程师从“在几十个仓库里大海捞针”的体力劳动里解放出来把精力放到真正需要判断力的地方。下一篇我会谈谈在这个全栈软件地图的基础上怎么让Agent进一步辅助算子库的性能分析和回归测试。