持久化增量 Csrc 构建缓存¶
概述¶
vLLM Ascend 会构建原生第三方库以及众多 AscendC 自定义算子。精确的最终产物缓存在命中时速度很快,但任何源码变更都会使整个产物失效。持久化增量 csrc 缓存增加了第二层,可在 CI 任务、工作区以及重复的本地源码构建之间复用单个编译动作。
这两级缓存具有不同的职责:
源码构建仍然是权威来源。L1 仅使用先前已验证的产物替换等价的动作。
目标与非目标¶
目标:
- 在 CI 任务之间复用原生构建工作;
- 在重复的本地源码构建之间复用原生构建工作,且无需远程传输;
- 仅使语义输入发生变化的动作失效;
- 在等价的检出根目录与构建根目录之间复用条目;
- 通过仓库的缓存传输机制持久化本地条目;
- 在并发构建下安全地发布产物;以及
- 当可选缓存层不可用时降级为普通构建。
非目标:
- 替换编译器依赖跟踪;
- 替换 L0 最终产物缓存;
- 跨物理节点共享文件锁;
- 要求历史源码快照包含缓存引擎;或
- 就地迁移旧版缓存模式。
架构¶
workflow
|
+-- restore L1 snapshot
|
+-- CMake adapter
| |
| +-- action identity
| +-- entry lookup/validation
| +-- restore or compile
| +-- local entry save
| `-- custom-operator publication
|
`-- publish changed L1 snapshot (writers only)
build_cache.py 负责本地动作标识、条目、加锁、恢复与发布。CMake 声明语义输入并调用该引擎。复合动作负责持久化快照键与 OBS 传输。工作流仅决定从何处恢复以及哪些受信任的构建角色可以发布。
这两级缓存具有各自独立的正确性边界。L0 是外层的精确最终产物快照:只有当完整的产物键匹配时,命中才能跳过原生构建。L1 是本地动作缓存的外层持久化快照。恢复 L1 快照仅使候选条目可用;内部引擎在接受条目之前仍会校验每个清单、动作键、模式、产物模型以及产物内容。因此,L1 命中绝不会覆盖内部正确性,L1 未命中也不会改变普通源码构建的正确性。
现有的镜像构建 L0 键使用架构、基础镜像标签以及受跟踪的 csrc 哈希。由于 L0 命中会跳过编译,从而跳过 L1 条目校验,因此该键假定编译器镜像仓库固定且标签不可变。更改仓库或重新发布内容不同的标签,需要新的 L0 键命名空间或在键中包含完整的镜像标识;L1 标识检查无法使已被接受的 L0 产物变得安全。本 PR 不改变镜像构建的 L0 行为,也不为 Docker 构建添加 L1。
构建流程¶
冷构建¶
L0 miss
-> L1 miss
-> normal source build starts
-> action MISS
-> compiler runs
-> verified local entry save
-> .updated marker
-> writer publishes L1
-> optional L0 publication
热构建¶
L0 miss
-> compatible L1 snapshot restored
-> .updated marker reset
-> normal source build starts
-> action identity calculated
-> entry validated
-> action HIT
-> artifacts restored
-> compiler action skipped
全部命中的构建不会重新创建 .updated,因此不会发布另一个唯一的远程快照。
缓存标识¶
每个动作键是三个独立标识的规范哈希:
prepared_input_hash
what is compiled
recipe_hash
how it is compiled
compiler_environment_hash
which toolchain compiles it
对于自定义算子,operator_text_hash 提供算子命名空间。它不会替换三个动作键组成部分中的任何一个。
条目清单存储哈希以及该键所使用的确切产物模型。命中要求模式、域、动作键、产物模型以及已验证的产物内容均符合预期。
预处理输入契约¶
预处理输入包含所有编译器可见的语义内容,包括:
- 生成的算子源码;
- 依赖的生成源码;
- 共享内核源码;
- 编译器可见的配方文件;以及
- 共享兼容性头文件,例如
cann_compat.h。
物理检出根目录与构建根目录不属于语义标识。因此,UTF-8 文本可以规范化显式声明的根目录。仅当通过某个根目录引用的每个语义对象都被动作标识独立覆盖时,该根目录才可以被规范化。
例如,生成的适配器可以包含:
临时根目录会被规范化,而 cann_compat.h 内容会作为预处理输入进行哈希。移动工作区仍为命中;更改该头文件则为未命中。
规范化契约被刻意限定得很窄:
- UTF-8 文本仅在路径组件边界处替换显式的规范化根目录;
- 源码、转义以及正则表达式中无关的反斜杠仍保持字节敏感;
- 二进制输入仍保持原始字节敏感;
- 显式根目录之外的路径仍保持敏感;以及
- 符号链接标识与解析后的语义内容均参与哈希。
不可读的显式语义输入绝不会被静默地从标识中省略。
配方与编译器环境¶
配方标识在经过相同的根目录规范化契约后,涵盖编译器命令、配方文件以及显式配方值。--set-env 编译器覆盖项也参与其中,并使用其确切值:某个覆盖项可能指向未被预处理输入标识覆盖的输入。
编译器环境标识涵盖主机平台、所选的编译器配置、工具版本输出、CANN 元数据以及显式环境值。当所报告的工具标识相等时,编译器的绝对安装路径不具有语义。
CMake 集成契约¶
vllm_ascend_build_cache_command() 是一个对正确性敏感的边界,而不仅仅是命令包装器。更改编译器可见输入的调用方必须在同一次变更中更新匹配的标识组:
PREPARED_INPUT描述源内容、生成内容、依赖内容以及共享内容;RECIPE_FILE和RECIPE_VALUE描述这些内容是如何构建的;SET_ENV会改变编译器进程环境,只要其值可能影响输出,就必须保留在 recipe 标识中;ENVIRONMENT_FILE、ENVIRONMENT_VALUE和ENVIRONMENT_TOOL描述 编译器/工具链环境;以及NORMALIZE_PATH仅移除那些其引用的语义内容已被上述某个标识组覆盖的物理位置。
移除或省略某个标识参数可能导致误命中(false HIT)。添加非语义的物理路径可能导致本可避免的跨工作区未命中。因此,对自定义算子生成输入或 protobuf
ExternalProject_Add() 命令的更改都需要进行缓存标识审查。
Schema 与快照兼容性¶
条目清单使用 SCHEMA_VERSION = 4。Schema 4 引入了安全的文本化
预处理输入规范化,以及对规范化路径的完整语义覆盖。较旧的条目自然会未命中,因为持久化键中包含 schema,且条目校验会拒绝不同的 schema。不存在原地
迁移:首次 schema-4 构建为冷构建,后续构建为热构建。
Schema 4 是该缓存的首个生产命名空间。由更早的 schema-4 候选版本写入的预发布本地或 验证快照与生产环境不兼容,必须与生产命名空间保持隔离。任何未来可能将旧的语义输入映射为不同含义的 更改,都需要进行 schema 或标识修订,而不能复用那些条目。
PUBLISH_STATE_SCHEMA 是独立的,因为构建树产物所有权与持久化条目标识是
不同的格式。
快照键命令只有一种兼容性模型:
schema
+ architecture
+ canonical SOC
+ explicit compiler-image identity for a nested container build
or CANN metadata + runtime OS/libc identity for a direct build
恢复操作会添加被跟踪的 csrc 哈希以及唯一的发布后缀。 因此,生产者键和消费者键都来自同一实现。 显式的编译器镜像优先于外层 runner 环境, 因此 Docker 构建以实际编译 csrc 的环境作为键。 直接构建会包含运行时 OS/libc 标识,以防止主机构建的产物 跨越不兼容的系统头文件或 ABI 边界。
准备、恢复和保存操作¶
prepare_csrc_l1_restore.py 是共享的键准备辅助脚本。它会检查
被测源码根目录是否包含缓存引擎,在调用方未提供时推导被跟踪的 csrc
哈希,计算快照键,并导出缓存目录和事件日志路径。不受支持或无法识别的源码
快照会返回 supported=false,以便调用方继续执行正常的
构建。
.github/actions/csrc-l1-restore/action.yaml 会调用该辅助脚本,使用兼容性前缀恢复
带键的外层快照,并在构建前移除 .updated。.github/actions/csrc-l1-save/action.yaml 仅在
调用方提供主键、本地缓存包含 .updated 且存在受信任的
写入者凭据时才会发布。本地引擎仅在条目被新保存或替换后才创建 .updated;因此全命中的构建不会
重新发布未更改的快照。恢复和保存的传输失败属于
可用性失败,保持 fail-open。清单缺失或无法解析,
或清单对象未通过 schema 或产物校验,均视为 MISS 并
重新构建。可能破坏共享产物发布或
同步的冲突会导致构建失败。顶层为非对象的 JSON 清单
目前在验证期间会抛出异常;它不在 MISS 回退的覆盖范围内。
工作流代码与被测源码是有意分离的。持久化操作
引用使用 uses: $/.github/actions/csrc-l1-restore 和
uses: $/.github/actions/csrc-l1-save,其中 $/.github 会从工作流修订版本解析操作
实现。因此,调用方的仓库和
source_ref 可以是 fork 或历史快照,而不会选择
不兼容的操作实现。调用方输入仍然描述
被测检出:source-root、缓存目录、架构、SOC、工具链
镜像以及可选的 csrc 哈希。
条目与产物生命周期¶
对于每个操作,引擎会:
- 对预处理输入、recipe 和编译器环境进行哈希;
- 推导内容寻址的条目路径;
- 获取条目锁和操作锁;
- 校验并恢复匹配的条目;
- 否则运行原始构建命令;
- 发现并验证生成的产物;
- 原子地保存本地条目;
- 将本地 L1 快照标记为已更新;以及
- 将自定义算子产物发布到共享构建输出中。
条目清单是正确性记录。查找或失效不需要单独的 可变索引。
并发模型¶
三种锁作用域保护不同的不变量:
- 条目锁允许一个内容寻址条目只有一个创建者;
- 操作锁保护单个操作的私有暂存目录;以及
- 发布锁串行化共享自定义算子输出的发布。
自定义算子会编译到私有暂存目录中。发布会跟踪 产物所有权并使用原子替换,防止部分输出 变得可见。同一相对产物存在冲突的所有者属于硬 错误,而不是不安全的覆盖。
多节点任务使用节点作用域的缓存目录:
该设计不依赖跨节点的 flock 行为。
持久化传输与信任¶
restore 和 save 复合 action 负责固定的 OBS 传输配置。缓存引擎不具备存储 API 或凭据相关的知识。
读取访问遵循运行器现有的 OBS 授权。共享写入需要同时提供 HW_OBS_AK 和 HW_OBS_SK;持有这些密钥是唯一的写入授权边界。保存操作从受信任的调用方显式接收凭据,当任一凭据缺失时跳过发布。
在此集成中,只有中央 csrc 生产者会发布 L1。镜像和发布 wheel 的 Docker 构建保持其现有行为,不会恢复、导出或保存持久化 L1。
消费源码的任务是只读的 L1 消费者:它们可以在现有源码安装之前恢复兼容的快照,但不会发布。精确产物消费者使用中央 L0 输出;L0 未命中时会继续执行普通源码构建,而不会将持久化 L1 传输作为前提条件。本地编译仍然正确,但不会创建共享快照。
写入者仅在成功构建创建或替换本地条目后才会发布。OBS 恢复和保存失败仅属于性能退化。
失败行为¶
| 条件 | 行为 |
|---|---|
| 历史源码没有缓存引擎 | 报告不支持并继续执行普通构建。 |
| 无法对快照环境进行指纹识别 | 跳过 L1 恢复并继续执行普通构建。 |
| 快照恢复失败 | 使用空的本地 L1 继续。 |
| 条目缺失、无法解析,或对象清单验证失败 | 编译并替换它。 |
| 清单是有效的 JSON 但顶层不是对象 | 目前在验证期间会抛出异常;这是缓存引擎的一个未修复缺陷。 |
| 条目锁不可用 | 绕过该条目并编译。 |
| 本地条目保存失败 | 发出警告并保留成功的构建输出。 |
| 快照保存失败 | 保留成功的构建或已验证的 L0 产物。 |
| 编译器命令失败 | 使构建失败。 |
| 产物所有权冲突 | 失败,而不是覆盖另一个操作的输出。 |
| 操作或发布同步失败 | 当继续执行可能损坏共享输出时失败。 |
运维 JSONL 事件仅限于缓存结果、警告和锁争用。可观测性为尽力而为,且绝不会串行化编译。
集成模式¶
直接源码消费者¶
消费源码的任务在现有源码安装之前恢复 L1,且不发布。精确产物任务使用规范生产者的 L0 输出;L0 未命中时会通过其现有源码构建继续,而不会进行第二次直接 L1 回退,除非调用方显式启用可选的共享 L1 回退。选定的测试任务在 L0 未命中后使用该可选回退;其他 L0 消费者(包括定时的上游 E2E)保留其调用方特定的源码构建策略。
本地源码构建¶
普通的本地源码构建使用仓库中被忽略的 csrc/build_cache 目录下的同一 action 缓存。这只是本地加速:不涉及 OBS 凭据或远程快照传输。将 VLLM_ASCEND_BUILD_CACHE_DIR 设置为绝对路径可覆盖此本地默认值。CI 使用相同的相对于工作区的 csrc/build_cache 传输路径,restore 辅助程序会将其解析为用于构建的绝对目录。删除本地目录会导致冷构建;它不会改变编译后的源码或 CI 的持久化快照。
csrc/build.sh keeps two operator_text_hash versions per SOC/operator after
a successful csrc build, retaining all actions inside each version. Configure
the script-local BUILD_CACHE_MAX_OPERATOR_VERSIONS beside BUILD_CACHE_DIR;
nonpositive values disable cleanup. No project-level setting is introduced.
HITs and saves refresh recency without deleting versions, so a third version
can coexist until the entire csrc build succeeds. Failed builds do not prune.
Cleanup runs at the end of build.sh, after its foreground build commands
(including their parallel compilation tasks) have succeeded. No cleanup lock
is added; the cache directory must not be used by another concurrent build or
prune operation. Cleanup errors never fail the successful build.
.updated marks compaction for the next trusted snapshot save.
Published outputs are never removed. Schema 4 and action identity are unchanged.
Standalone action or CMake users can explicitly run
python3 csrc/scripts/build_cache.py prune --cache-root csrc/build_cache after a
successful complete build. --max-operator-versions N changes the default of 2.
This bounds operator source-version history, not disk bytes, third-party entries, action-key variants within a retained version, or historical OBS snapshot counts. Separate concurrent builds must use separate cache directories, or the caller must arrange a maintenance window after all builds finish.
中央生产者¶
可复用的生产者恢复 L1,执行普通源码构建,验证最终原生产物,发布已更改的 L1 状态,然后发布精确的 L0 产物。
所有 L1 生产者和消费者都必须向 restore/save action 传递相同的字面相对路径 cache-dir: csrc/build_cache。runs-on/cache 会在其远程缓存版本中包含该路径:在一个 runner 上使用 /tmp、在另一个 runner 上使用 runner.temp,会将原本兼容的键分隔到不同的 OBS 命名空间中。相对归档布局也会在每个消费者自己的工作区下恢复,而不是生产者所在的绝对目录。生产者初始设置 VLLM_ASCEND_BUILD_CACHE_DIR=csrc/build_cache。Restore 通过 GITHUB_ENV 导出其绝对等价路径以供编译使用,并在其 cache-dir 输出中保留原始写法。Save 使用该输出,而不是更新后的环境变量:将绝对构建目录作为 save 的输入会再次拆分 OBS 命名空间。restore/save 传输失败仍然是非致命的;普通源码编译仍是回退方案。旧路径命名空间下的现有快照不会被迁移,因此使用共享路径的首次构建可能是冷构建。
历史源码¶
Main2Main 和 bisect 风格的构建将当前工作流辅助工具与选定的源码树分开。没有 build_cache.py 的历史源码树会报告 supported=false 并正常构建。
Docker 构建¶
镜像和发布 wheel 工作流在此 PR 中不是 L1 消费者或写入者。它们现有的 Docker 构建、源码编译和镜像 L0 行为保持不变。后续集成必须验证 Docker 边界,而不能将持久化 L1 嵌入已发布的镜像中。
验证摘要¶
| 能力 | 证据 |
|---|---|
| 操作标识、条目和失败行为 | 缓存引擎单元测试 |
| 并发所有权和锁行为 | 并发单元测试 |
| 直接源码复用 | A2、A3 和 310P 冷/热运行 |
| 选择性失效 | 算子本地变更且存在无关 HIT |
| 与根无关的语义标识 | schema-4 CASE-C 回归 |
| 持久化生产者 | 冷/热生产者运行 |
某些精确的生产调用方仍然需要发布凭据、调用方注册或多节点分配。结构和组件证据不能替代这些调用方特定的运行时门控。
运维调试¶
对于意外的完全 MISS,请比较条目清单中的 schema、快照兼容性、prepared_input_hash、recipe_hash 和 compiler_environment_hash。对于大范围的选择性失效,请检查准备好的输入集和算子命名空间。对于历史源码,请检查恢复操作的 supported 输出。
实现映射¶
| 职责 | 源码 |
|---|---|
| 缓存标识、条目、锁定和发布 | csrc/scripts/build_cache.py |
| CMake 到引擎适配器 | csrc/cmake/build_cache.cmake |
| 算子语义输入 | csrc/cmake/func.cmake |
| 第三方语义输入 | csrc/cmake/third_party/ascend_protobuf.cmake |
| 持久化恢复与密钥编排 | .github/actions/csrc-l1-restore/action.yaml |
| 持久化变更快照发布 | .github/actions/csrc-l1-save/action.yaml |
| 中央生产者 | .github/workflows/_build_csrc_cache.yaml |