PyTorch Blog

From Upstream Changes to Downstream Confidence: Inside Torch Spyre’s Integration with PyTorch CRCR

6.9内容质量
From Upstream Changes to Downstream Confidence: Inside Torch Spyre’s Integration with PyTorch CRCR

TL;DR · AI 摘要

Title: From Upstream Changes to Downstream Confidence: Inside Torch Spyre’s Integration with PyTorch CRCR URL Source: Pu...

核心要点

  • 主题聚焦:From Upstream Changes to Downstream Confidence:
  • 来源:PyTorch Blog,建议结合原文判断细节。
  • AI 分析暂不可用,本条为保底评分与摘要。
#AI#编程#后端#安全#开源
打开原文

特色项目

简要说明

PyTorch的跨仓库CI中继(CRCR)为树外加速器提供了一种清晰且可扩展的方式接入上游CI系统。该机制允许每个后端自主决定PyTorch数以万计的测试中哪些与自身硬件相关,并随着PyTorch、后端和测试套件的演进动态维护这一判断。本文将深入解析Torch Spyre如何基于该基础架构实现CRCR的L2集成:通过智能流水线从代码和执行证据中筛选并重新分类测试,采用声明式YAML框架在不修改上游测试的前提下适配它们,并构建了一套适用于大规模场景的可靠性与可复现性工作流模式。这些方案对任何私有使用1类设备均具有通用性,我们期待与PyTorch社区合作,推动其在适用场景下的复用和上游集成。

介绍

PyTorch的跨仓库CI中继(CRCR)通过标准化机制弥合PyTorch与树外(OOT)加速器仓库间的协作鸿沟。该系统可触发上游变更对应的下游CI,将结果直接呈现给PyTorch评审人员,并通过PyTorch CI CRCR HUD将回归问题统一展示。具体实现上,OOT加速器仓库只需在pytorch/pytorch接收调度指令,并通过包含白名单条目、监听repository_dispatch事件的工作流以及复合回调动作的最小集成方案,将结果反馈至HUD。进一步地,CRCR提供四个集成层级,使下游项目可逐步从接收上游调度通知开始,进阶到通过HUD提交结果,最终参与非阻塞或阻塞式的上游PR验证流程。

技术挑战真正开始于下一步,即中继器为每个后端留下的决策:哪些是测试候选对象,哪些调度值得构建,PyTorch数以万计的测试中哪些在你的硬件上具有实际意义,如何在不分支CUDA测试代码的前提下适配这些测试,以及"绿色"(成功)的定义可以接受哪些范围。这些是必须由后端自行决定的正确决策,因为只有后端才能做出这些判断。本文描述了我们构建的实现机制,包括智能测试选择流水线、声明式测试复用框架和可靠的流程系统。这些机制共同推动Torch Spyre实现了L2集成。从设计上讲,这些机制并非Spyre专属,加速器配置位于CI逻辑之外,因此我们不希望让每个后端重复实现这些模式,而是计划与PyTorch团队合作将可复用部分上游化,PyTorch OpenReg将成为自然的参考基准。

三个持续演进的测试候选对象:外部加速器PyTorch后端、PyTorch核心、测试套件

_Image 2_

_图1:PyTorch与Torch Spyre的测试范围_

挑战1:确定测试内容:三个动态目标及组合选择

通常,外部加速器在从PyTorch CRCR接收到调度后,需要处理三个持续演进的测试候选对象。首先,是外部加速器PyTorch后端代码库,例如Torch Spyre。作为IBM Spyre加速器的PyTorch后端,Torch Spyre依赖于通过PyTorch外部扩展接口的深度集成。这暴露了从PyTorch核心钩子中广泛测试的表面,包括运算符和运行时行为。第二个候选对象是PyTorch核心本身,核心运行时组件或其他核心功能的变更可能引发外部后端的回归问题。最后是持续演进的测试套件,测试可能被修改,从而暴露潜在的回归问题。这三个候选对象中的任何一个都可能引入回归问题。通过PyTorch测试套件对外部加速器后端进行测试,有助于发现所有三个候选对象中的回归问题。

明确三个候选对象后,直接引出下一个决策:实际运行哪些组合。当调度到达时,外部加速器后端可能自上次运行后已取得进展,PyTorch核心可能有新提交,测试套件本身也可能发生变化。主要组合是三个候选对象中最新版本的组合:最新的后端代码、最新的PyTorch核心以及调度时当前的测试套件。这种组合能够明确归因于上游变更的回归问题:当自上次绿色运行以来唯一变化的是上游代码时,新的失败指向上游问题。

可选地,可以扩展为包含其他有用的组合,例如将一系列外部加速器后端版本与动态演进的PyTorch核心和测试套件进行测试。这有助于发现每个外部加速器后端版本的前向和后向兼容性问题。此外,每个组合可作为独立任务上报至HUD,使每个组合对应的结果和回归问题更易于追踪。

驯服 PyTorch 测试套件

挑战 2:识别适用于你的 OOT 加速器后端的候选测试

_Image 3_

_图 2:测试选择的智能代理工作流_

PyTorch 拥有数以万计的测试用例。手动识别哪些测试与你的后端相关既不高效,随着 PyTorch 和你的后端不断发展,保持测试选择的时效性更是难上加难。因此,我们构建了一个智能代理流程来解决这个问题。

该流程分为四个阶段(参见图 2)。前两个阶段用于构建上下文;后两个阶段进行决策:首先基于代码,然后基于执行结果。

  1. 高级筛选。第一个代理通过两个输入将搜索范围从整个测试树缩小到值得分析的文件夹和顶级文件:后端实际挂钩的 PyTorch 扩展点,以及关于范围的自然语言偏好。如果后端未挂钩 torch.dynamo 或尚未准备处理 autograd,会在此处明确说明并完全跳过相关测试树。
  2. 仓库记忆。仓库记忆生成器将该短名单转换为可查询的索引,包含符号、文件、LLM 生成的摘要和每项测试的嵌入向量。这使下一阶段的选择代理能够一次性对整个候选集进行推理,而不再受限于单个上下文窗口的容量。
  3. 低级筛选。第二个代理随后分析后端自身的代码库、文档和元数据(例如支持的操作符),然后查询仓库中的精确测试用例,并将它们归类为 mandatory_success 或 skip。每个决策都会记录下其背后的推理过程。
  4. 基于真实执行的优化。所选测试在真实硬件上运行。代理通过执行日志进行二次分析,从而捕捉静态分析遗漏的运行时故障和数值差异。输出是一个修正后的测试集,重新归类并更新了推理依据。

该流程目前为每个上游测试文件生成一个文件级配置,覆盖数千个独立命名的测试用例,而合并运行则在真实硬件上执行这些测试。覆盖率会自动增长,使后端新增操作符时能自动启用所有依赖它的测试,无需手动维护列表。

可审计性至关重要:每个分类决策都会以注释形式写回配置文件,说明原因。当有人询问“为什么跳过这个测试?”时,答案在文件中而非模型记忆中。代理提出建议;配置文件是可审查的正式记录。

此外,该智能代理流程可根据特定用例进行定制。以下讨论两个典型用例。

用例 A:启用新操作符

_当后端启用新操作符时,应该开启哪些测试?_

这需要准备测试到操作符的映射表,对每个测试记录其使用的操作符,并构建反向索引将每个操作符映射到所有使用它的测试列表。仓库记忆生成器在一次性执行过程中捕获这些元数据:

  1. 即时执行路径:通过 TorchDispatchMode 捕获
  2. 编译路径:通过 TORCH_LOGS 捕获

然而,通过此过程捕获的测试无法直接添加到测试套件中。尽管某个测试可能覆盖了目标操作符,但也可能涉及与OOT加速器无关或当前不支持的其他PyTorch功能。因此,操作符信息连同识别出的测试文件会被传递给低级选择代理。该代理利用这些信息进一步筛选候选对象,生成可添加到测试套件的最终测试和文件集合。

结果:启用一个操作符即可自动呈现所有相关测试,无需人工筛选。

使用场景B:升级PyTorch版本

_当升级到新版本PyTorch时,如何在不重新处理所有内容的情况下更新测试选择?_

随着PyTorch版本的演进,测试套件会因新增测试或现有测试的修改/删除而发生变化。这些变更可以记录到仓库内存中以反映更新后的测试套件。随后只需运行低级选择代理处理新增和修改的测试差异部分,即可识别与OOT加速器相关的变更并生成修订后的测试套件。这避免了重新处理整个测试套件,同时保持所选测试与更新后的PyTorch版本保持一致。

结果:版本升级转化为配置差异而非全量重新处理。例如,从PyTorch 2.13升级到2.14时,只需评估约4000个变更测试,而非数万个。

挑战3:适配候选测试

PyTorch测试通常跨数据类型、形状和操作符进行参数化。由于某个数据类型不被支持而跳过整个测试过于粗略,在这种情况下应仅排除该数据类型而保留其他测试运行。更深入的适配(如移除CUDA特定的硬编码)传统上需要上游补丁。为避免此问题,Torch Spyre维护了一个声明式测试复用框架,通过YAML配置表达所有适配规则。

该框架提供三种控制方式:

  1. 参数级控制:子选择特定数据类型,排除特定形状,为关注的数据类型添加覆盖范围。
  2. 结果分类:将每个测试分配到mandatory_success(必须通过)、xfail(预期失败)、xfail_strict(预期失败且意外通过视为失败)或skip(跳过)。
  3. 能力驱动包含:全局声明支持的操作符/数据类型;启用新操作符会自动允许所有等待该操作符的测试。

具体来说,声明式接口的实践形式如下:文件条目为未明确列出的内容设置默认策略,然后按分类列出测试,通过edits:块适配个别案例而无需修改上游源码:

code
test_suite_config:
  labels: [trunk]
  files:
    - path: ${TORCH_ROOT}/test/test_view_ops.py
      unlisted_test_mode: skip        # 默认的未列出测试策略
      tests:
        # 基本张量元数据操作,在Spyre上完全支持。
        - names:
            - TestOldViewOps::test_broadcast_tensors
            - TestViewOps::test_contiguous_self
          mode: mandatory_success
        # bool/int64失败仅因为测试设置使用了aten::random_.from。
        - names:
            - TestOldViewOps::test_broadcast_to
          mode: mandatory_success
          edits:
            dtypes:
              exclude:
                - name: bool
                - name: int64
  global:
    supported_dtypes: [{name: bfloat16}, {name: float16}, {name: float32}]
    supported_ops:
      - name: _scaled_mm
        dtypes: [{name: bfloat16}]

通过这些额外的用户控制选项,智能代理工作流程可以扩展为生成相应的配置文件,这些配置文件可以被OOT测试复用框架使用。低级代理和测试仓库内存可以捕获测试结果(例如mandatory_success),同时识别支持的数据类型和其他测试参数,从而在单个测试用例级别对测试进行分类。

该模式的四个属性承担了大部分工作:

  1. 声明的默认值。unlisted_test_mode为未列出的测试设置结果,因此上游明天添加的测试将具有定义的结果,而不是在提交当天破坏CI。
  2. 四个结果分类。mandatory_success、xfail、xfail_strict和skip。严格变体有其存在的意义:意外通过会发出信号以提升测试,而不是静默处理。
  3. 每个测试案例的自适应。edits:适用于单个数据类型、操作和模块,因此不支持的数据类型只会损失一个数据类型,而不是整个测试。
  4. 一次性声明能力。global.supported_ops和supported_dtypes按文件声明,因此在后端启用操作符只需一行更改,即可启用所有等待该操作符的测试。

工作原理:框架在收集时修补上游的@ops、@modules和@dtypes装饰器,生成pytest标记(op__、dtype__)。这意味着相同的配置同时驱动自适应和选择:-m op__add会精确运行测试该操作符的测试。

关键保证:上游测试树保持原样。不修改PyTorch的测试。版本升级变为配置差异,而不是合并冲突。框架针对通用私有设备privateuse1,因此没有任何硬件特定内容。

CRCR集成到现有工作流程中

_Image 4_

_图3:Torch Spyre中集成CRCR的工作流程_

挑战4:消费分发:根据负载进行门控,自行解析SHA

| 分发字段 | 用途 | | --- | --- | | SHA | 识别要验证的上游提交的确切哈希值。 | | PR编号 | 将下游测试结果与特定上游PR相关联。 | | 操作 | 指定PR是打开、更新(同步)还是关闭/合并。 | | 基分支(pull_request.base.ref) | 识别目标分支并启用对主分支和发布分支(如release/{version})的过滤。 | | PR标签(pull_request.labels) | 与拉取请求一起使用的标签。可用于通过标签“Merged”识别已合并的PR。 |

_表1:关键分发字段_

集成 CRCR 从消费 PyTorch 的分发开始。分发负载包含如表 1 所示的关键信息。可以通过这些信息对分发进行过滤以满足不同使用场景的需求。针对已合并到 main 分支的拉取请求进行过滤时,需要满足关闭操作带有合并信号且目标分支为 main 的条件。合并信号是间接的。PyTorch 不使用 GitHub 的合并按钮,这意味着 PR 会被关闭后,由 PyTorchBot 进行压缩合并并添加“Merged”标签。因此检测已落地的 PR 需要检查该标签:

code
HAS_MERGED=$(echo "$PAYLOAD_JSON" | jq -r '.payload.pull_request.labels // [] | map(.name) | index("Merged") // ""')

特殊情况:竞态条件可能导致标签在分发后才被应用,或者人工合并可能完全跳过标签。对于关键工作流可能需要防御性处理(例如轮询或备用启发式方法)。

除了这种由分发驱动的使用场景外,还有两种其他使用场景可能对 OOT 加速器有价值:针对 PyTorch 每日构建进行测试,以及针对 PyTorch 发布版本进行测试。

使用场景 C:每日构建测试

CRCR 不会对每日构建进行分发,但 HUD 支持报告每日构建结果。工作流按照计划运行,直接解析 SHA 的方式如下:

code
NIGHTLY_SHA=$(curl -fsSL "https://api.github.com/repos/pytorch/pytorch/commits?sha=nightly&per_page=1" | jq -r '.[0].sha // empty')

if [ -z "${NIGHTLY_SHA}" ]; then
  echo "::error::Could not resolve HEAD for pytorch/pytorch/nightly"
  exit 1
fi

COMMIT_MSG=$(curl -fsSL "https://api.github.com/repos/pytorch/pytorch/commits/${NIGHTLY_SHA}" | jq -r '.commit.message')

SOURCE_SHA=$(echo "$COMMIT_MSG" | grep -oP '\(([a-f0-9]{40})\)' | tr -d '()')

if [ -z "${SOURCE_SHA}" ]; then
  echo "::error::Could not extract source SHA from latest nightly commit"
  exit 1
fi

echo "Resolved pytorch/pytorch@nightly -> source ${SOURCE_SHA}"

使用场景 D:发布版本测试

PyTorch 发布版本时不会发送分发。发布版本测试需要通过 workflow_dispatch 手动触发,并传递发布分支(例如 release/v2.14)。SHA 解析方式与每日构建相同。

注意:HUD 目前没有专门的视图用于展示发布版本测试结果。

挑战 5:提升工作流效率:测试拆分、并行化和一次构建多次测试

数千个测试需要快速运行。拆分发生在三个层面:

  1. 功能层面:按区域(Inductor、运算符、eager 等)对测试进行分组
  2. 时长限制:将每个功能组进一步细分以确保不超过最大任务时长(例如 30 分钟)
  3. 均衡分桶:将单个测试分配到不同拆分中以避免拖后腿的任务

构建此系统需要一次遍历的计时运行来测量每个测试和每个功能的持续时间。结果:并行任务可以同时完成,而不是一个任务阻塞其他任务。

一次构建多次测试:构建 PyTorch 和后端轮子一次,发布为工作流制品,供所有测试拆分使用。这可以避免冗余构建并保证所有任务测试相同的制品。

挑战 6:提升工作流可靠性

_Image 5_

_图 4:工作流弹性的重试模式_

在HUD中,工作流的最终状态应重点关注由不断演进的PyTorch和OOT加速器代码库引入的回归问题,而非工作流稳定性失败(如可能对更广泛社区无实际意义的临时基础设施问题)。

尽管基础设施和平台层可以配备多种可靠性措施以提升工作流稳定性,但开发者可通过采用额外的可靠性模式进一步增强工作流,如表2所示。此处列出的模式并非穷尽所有可能性,但为OOT加速器构建可靠的CRCR工作流提供了实用基准。

| 可靠性维度 | 模式 | 对CRCR的影响视角 | 实施细节 | | --- | --- | --- | --- | | 容错性 | 并行隔离执行模式 | 即使某个作业失败,非故障测试作业仍能继续完成并报告状态 | 在平台层使用GitHub Actions矩阵作业,通过Pod/容器实现执行隔离 | | 容错性 | 错误继续模式 | 非关键路径的失败不会导致HUD中整体工作流状态被标记为失败 | 对非关键作业或步骤使用GitHub Actions的continue-on-error功能 | | 弹性 | 重试模式 | 临时故障可通过重试恢复,无需在HUD中暴露持续性故障 | 如图4所示,基于测试日志和故障启发式方法,使用工作流级重试和更细粒度的作业内重试 | | 弹性 | 日志记录与故障分类 | 区分可操作的测试回归与无法通过重试模式恢复的临时基础设施/平台故障 | 通过详细日志和摘要信息可对故障(已通过重试模式处理)进行分类并采取恢复措施 | | 一致性 | 一次构建多次使用 | 并行作业测试相同的PyTorch和OOT加速器构件,减少跨拆分不一致性 | 构建一次轮子(wheel),作为GitHub工作流构件发布,并在所有测试作业中使用相同构件 |

_表2:工作流可靠性模式_

挑战7:矩阵作业的CRCR回调

GitHub允许基于变量组合运行一组作业,称为矩阵策略(matrix strategy)。矩阵策略中的单个作业称为矩阵腿(legs)。

约束条件:匹配的in_progress/completed回调对必须来自同一作业。CRCR使用check_run_id跟踪重试,每个矩阵腿都会获得独立的check_run_id。这意味着无法从生成矩阵的父作业发送回调。

两种变通方案:

方案A:按腿回调。每个矩阵作业发送自己的in_progress和completed状态。简单直接,但对大型矩阵可能导致HUD信息过载,且可能不适用于动态作业。

方案B:调度+轮询。父作业调度包含矩阵的独立工作流,然后轮询完成状态。此模式同样适用于异构CI场景(如从GitHub Actions父作业触发Jenkins或其他平台)。

挑战8:数周后实现运行可重复性

调试两周前的故障需要准确知道当时运行了什么。通过将以下元数据作为工作流构件进行捕获:

  1. 触发负载(PR编号、SHA、操作)
  2. PyTorch和后端轮子版本(或提交SHA)
  3. 运行时环境(Python、CUDA、OS、CPU架构)
  4. 每项测试结果(通过/失败/跳过,耗时)

结合一次性构建产物,这可确保任何运行记录数周后仍可被重现或进行二分查找。

结论

CRCR 负责分发和报告。它留给下游仓库的决策权是正确的归属:哪些调度需要关注、哪些测试具有实际意义,以及在特定平台上“绿色”状态的具体定义。

掌控这些决策意味着需要同步三个动态目标:后端、PyTorch 核心和测试套件。本文的工程实践正是围绕这些目标展开:智能测试选择流水线、声明式测试复用框架、一次性构建多环境推广机制,以及分层重试机制。

这些方案与硬件无关。测试复用框架针对通用的 privateuse1 设备进行设计。我们计划与 PyTorch 团队合作将其上游化——最自然的整合方式是与 Torch OpenReg 一同推进,这是加速器作者已用于学习扩展点的树内参考后端。

如果这些模式对您的集成工作有帮助,我们希望在制定方案前了解哪些模式最为关键。请通过 torch-spyre/torch-spyre 开启 issue 与我们交流。

致谢

我们要感谢 IBM Spyre 团队在该项目中的支持。