尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

PyTorch Operator Upgrader 完整指南:为 BC-breaking 算子变更编写版本升级器(operator_upgraders 源码解析)

PyTorch Operator Upgrader 完整指南:为 BC-breaking 算子变更编写版本升级器(operator_upgraders 源码解析) PyTorch Operator Upgrader 完整指南为 BC-breaking 算子变更编写版本升级器operator_upgraders 源码解析【免费下载链接】pytorchTensors and Dynamic neural networks in Python with strong GPU acceleration项目地址: https://gitcode.com/GitHub_Trending/py/pytorchPyTorch 的算子operator在演进中可能因修复 bug、改善易用性等原因发生破坏性变更导致旧程序在新运行时BC breaking或新程序在旧运行时FC breaking行为不一致。本文基于torch/csrc/jit/operator_upgraders/模块的官方开发者指南README.md系统讲解Upgrader升级器的完整设计何时需要编写、如何命名、如何注册到版本映射表、如何生成测试模型并验证并穿插仓库源码级佐证。读完本文你将具备独立完成一次BC-breaking 算子变更 配套 upgrader 测试 版本号提升全流程的能力。背景为什么算子变更需要版本化PyTorch 算子会因各种原因被修改例如提升可用性或修复 bug。这些修改可能带来两类破坏向后兼容BC破坏旧程序在新版 PyTorch 运行时不再按预期工作old program / new runtime problem向前兼容FC破坏新程序在旧版 PyTorch 上无法运行new program / old runtime problem。官方指南聚焦于维护向后兼容性的要求并为此引入upgrader概念一种用于将新算子适配为旧算子行为的方法。当新运行时读取一个包含旧算子定义的旧程序时upgrader 会把旧算子定义适配成符合新算子实现的形式。显然upgrader 只会在遇到旧算子定义时被应用——如果程序里没有旧算子就完全不会触发 upgrader。更完整的动机说明见 PyTorch 官方 RFC-0017PyTorch Operator Versioning本仓库的模型生成脚本 test/jit/fixtures_srcs/generate_models.py 也在文件头注释中引用了该 RFC。判断变更是否 BC-breaking 的标准很简单运行python test/forward_backward_compatibility/check_forward_backward_compatibility.py如果失败就说明你的改动是 BC-breaking 的必须编写 upgrader。典型 BC-breaking 变更示例变更类型旧 schema新 schema说明返回类型更泛化foo(Tensor self, int a) - intfoo(Tensor self, int a) - Scalar返回值类型放宽参数类型更具体foo(Tensor self, Scalar a) - intfoo(Tensor self, int a) - int入参类型收窄新增参数无默认值foo(Tensor self, int a) - intfoo(Tensor self, int a, int b) - int旧程序无法提供新参数内部实现变更schema 不变schema 不变语义发生变化也属于破坏弃用deprecate算子——直接移除视为破坏需要特别区分的是给算子新增带默认值的参数并不构成 BC-breaking因此不需要 upgrader。例如def foo(x, y)变成def foo(x, y, z100)是向后兼容的。这一条也在官方指南末尾的 NOTE 中单独强调。整体工作流概览官方指南把整个流程拆成两大步每步再细分若干子步骤准备阶段在改动算子前先构建 PyTorch 源码版本、编写测试模块、生成历史版本测试模型并提交 PR实施阶段修改算子、编写 upgraderTorchScript 形式、提升版本号、更新版本映射表、自动生成移动端 upgrader 代码、编写测试并提交单个 PR。为什么要先准备模型、再改算子因为一旦算子被修改新运行时将再也无法导出包含历史算子定义的模型也就无法再测试 upgrader。所以必须在改动之前把旧模型固化下来。准备阶段生成历史版本测试模型1. 在 fixtures_src.py 中添加测试模块在 test/jit/fixtures_srcs/fixtures_src.py 中添加一个使用被变更算子的torch.nn.Module。官方指南给出的示例class TestVersionedLinspaceV7(torch.nn.Module): def __init__(self) - None: super().__init__() def forward(self, a: Union[int, float, complex], b: Union[int, float, complex]): c torch.linspace(a, b, steps5) d torch.linspace(a, b) return c, d需要注意的约束模块必须实际使用被变更的算子例如torch.linspace命名遵循TestVersioned{${OpnameOverloadedname}}V${kProducedFileFormatVersion}规范其中kProducedFileFormatVersion定义在 caffe2/serialize/versions.h算子用法参考官方 PyTorch Docs如torch.linspace。本仓库的 fixtures_src.py 中已按此规范积累了多个版本的历史模块例如TestVersionedDivTensorExampleV7、TestVersionedLinspaceV7、TestVersionedLinspaceOutV7、TestVersionedLogspaceV8、TestVersionedGeluV9、TestVersionedRandomV10等可以作为命名与写法的直接参照。2. 在 ALL_MODULES 中注册模块与算子在 generate_models.py 的ALL_MODULES字典中用模块实例作为 key、被变更算子名作为 value 注册# key: test module instance, value: changed operator name ALL_MODULES { TestVersionedLinspaceV7(): aten::linspace, }这样能保证导出的测试模型覆盖所有需要的内容。如果模型未覆盖被变更的算子导出过程会失败。3. 导出模型到 fixtures 目录运行python test/jit/fixtures_src/generate_models.py将模型导出到test/jit/fixtures。注意仓库实际路径为test/jit/fixtures_srcs/复数srcs以仓库现有目录为准。4. 提交变更并创建 PR在改动运行时之前先把旧模型 注册逻辑合入主干。这一步非常关键等改动合并后再想切回旧版本源码重新生成模型会非常困难所以一定要在改动前提交一个有效的测试模型。实施阶段修改算子并编写 upgrader1. 做出算子变更这是你的实际业务改动例如把linspace的steps从可选参数改为必填参数。2. 在 upgraders_entry.cpp 中编写 upgraderupgrader 本体写在 torch/csrc/jit/operator_upgraders/upgraders_entry.cpp 的kUpgradersEntryMap映射中key 为 upgrader 名称value 为一段TorchScript 源码字符串。命名规范软性强制operator_name_operator_overload_start_end。其中start和end表示当 全局算子版本号 落在区间[start, end]内时该 upgrader 会被应用到对应时期导出的算子。例如linspace_0_7表示linspace算子在第 07 版导出的模型需要使用这个 upgrader。以linspace的outoverload 为例先检查 upgrader 是否已存在于upgraders_entry.cpp若不存在upgrader 名可直接取linspace_out_0_{kProducedFileFormatVersion}若已存在例如已有linspace_out_0_7表示算子版本从 7 升到 8 时linspace.out发生了变化如果能在版本升到 8 之前写出对所有linspace版本都有效的 upgrader就写linspace_out_0_{kProducedFileFormatVersion}如果无法写出跨版本的 upgrader则查看 versions.h 中版本升到 8 的日期若已过去180 天可以写linspace_out_8_{kProducedFileFormatVersion}并弃用旧的 upgrader若未满 180 天则等待满 180 天后再执行同样的操作。这个 180 天策略的目的是保证旧 upgrader 的覆盖区间与新 upgrader 的起始区间之间始终有足够长的重叠保护期避免出现既不被旧 upgrader 也不被新 upgrader 覆盖的模型版本。以 linspace 为完整示例当linspace版本升到 8 时变更内容是把step实为steps从可选参数改为必填参数。旧 schema 为linspace(start: Union[int, float, complex], end: Union[int, float, complex], steps: Optional[int], dtype: Optional[int], layout: Optional[int], device: Optional[Device], pin_memory: Optional[bool]):新 schema 为linspace(start: Union[int, float, complex], end: Union[int, float, complex], steps: int, dtype: Optional[int], layout: Optional[int], device: Optional[Device], pin_memory: Optional[bool]):upgrader 只作用于旧模型新模型不会触发。先用伪 Python 描述修复逻辑当旧模型里steps缺省None时按新语义补默认值100再调用新算子def linspace_0_7(start: Union[int, float, complex], end: Union[int, float, complex], steps: Optional[int], *, dtype: Optional[int], layout: Optional[int], device: Optional[Device], pin_memory: Optional[bool]): if (steps is None): return torch.linspace(startstart, endend, steps100, dtypedtype, layoutlayout, devicedevice, pin_memorypin_memory) return torch.linspace(startstart, endend, stepssteps, dtypedtype, layoutlayout, devicedevice, pin_memorypin_memory)实际的 upgrader 必须以TorchScript编写下面就是仓库中linspace0~7 版本导出的真实注册代码见 upgraders_entry.cppstatic std::unordered_mapstd::string, std::string kUpgradersEntryMap( { {linspace_0_7, RSCRIPT( def linspace_0_7(start: Union[int, float, complex], end: Union[int, float, complex], steps: Optional[int], *, dtype: Optional[int], layout: Optional[int], device: Optional[Device], pin_memory: Optional[bool]): if (steps is None): return torch.linspace(startstart, endend, steps100, dtypedtype, layoutlayout, devicedevice, pin_memorypin_memory) return torch.linspace(startstart, endend, stepssteps, dtypedtype, layoutlayout, devicedevice, pin_memorypin_memory) )SCRIPT}, }linspace.out的 upgrader 也以同样的方式注册{linspace_out_0_7, RSCRIPT( def linspace_out_0_7(start: Union[int, float, complex], end: Union[int, float, complex], steps: Optional[int], *, out: Tensor): if (steps is None): return torch.linspace(startstart, endend, steps100, outout) return torch.linspace(startstart, endend, stepssteps, outout) )SCRIPT},应用时机当新运行时加载旧模型时会先检查旧模型的算子版本。若旧模型版本低于当前运行时版本就把旧模型中的算子替换为上述 upgrader。仓库中已有的其他 upgrader 一览upgraders_entry.cpp 中目前注册了以下 upgrader可作为不同变更类型的参考div_*_0_3div.Tensor、div.Scalar、div.out及对应 inplace 变体语义变更——整数除法行为改变。当任一操作数为浮点时走true_divide否则用rounding_modetrunc的divide复现旧行为full_0_4/full_out_0_4语义变更——不再从 bool/int 填充值推断浮点 dtypeupgrader 在dtype is None时先把fill_value转成 floatlinspace_0_7/linspace_out_0_7、logspace_0_8/logspace_out_0_8参数语义变更——steps变为必填缺省时补 100gelu_0_9/gelu_out_0_9新增approximate参数upgrader 显式传approximatenone复现旧行为。upgrader 如何变成可执行的 GraphkUpgradersEntryMap只是字符串源。真正执行时仓库通过create_upgrader_graph把 TorchScript 字符串编译成Graphstd::shared_ptrGraph create_upgrader_graph( const std::string upgrader_name, const std::string upgrader_body) { auto cu std::make_sharedCompilationUnit(); cu-define(std::nullopt, upgrader_body, nativeResolver(), nullptr); Function jitFunc cu-get_function(upgrader_name); GraphFunction graphFunction toGraphFunction(jitFunc); return graphFunction.graph(); }generate_upgraders_graph()遍历整个 map 逐个编译populate_upgraders_graph_map()则在首次使用时一次性填充全局 upgrader graph 表。这意味着upgrader 的运行时形态是 JIT 编译后的 Graph与 TorchScript 模型加载链路完全打通。3. 提升文件格式版本号同时把 caffe2/serialize/versions.h 中的kMaxSupportedFileFormatVersion和kProducedFileFormatVersion各加 1并在该文件的历史注释区补充原因。当前仓库中该文件已经历多次提升注释完整记录了历次变更constexpr uint64_t kMaxSupportedFileFormatVersion 0xAL; // We describe new operator version bump reasons here: // 1) [01/24/2022] // We bump the version number to 8 to update aten::linspace // and aten::linspace.out to error out when steps is not // provided. (see: https://github.com/pytorch/pytorch/issues/55951) // 2) [01/30/2022] // Bump the version number to 9 to update aten::logspace and // and aten::logspace.out to error out when steps is not // provided. (see: https://github.com/pytorch/pytorch/issues/55951) // 3) [02/11/2022] // Bump the version number to 10 to update aten::gelu and // and aten::gelu.out to support the new approximate kwarg. // (see: https://github.com/pytorch/pytorch/pull/61439) constexpr uint64_t kProducedFileFormatVersion 0xAL;versions.h还解释了版本化机制的关键设计见文件中的 Dynamic Versions and torch.jit.save vs. torch.save 注释采用生产文件格式版本号描述归档的读取方式归档中写入的版本至少等于当前生产版本但如果包含某些符号则可能更高这些条件版本称为动态版本动态版本的价值在于torch.div语义改变时被赋予动态版本 4保存使用torch.div的模块时归档也至少带上版本 4从而阻止旧版 PyTorch 误用错误的除法语义不使用这些算子的程序可以只写生产版本号从而在旧版本上照常运行对比之下torch.save类似 Python pickle不保留算子语义、忽略动态版本——torch.save/torch.load跨版本加载时行为可能不同而torch.jit.save会尽力保留算子语义。注意本文引用的版本号0xAL、kProducedFileFormatVersion 0xAL以当前仓库为准较 README 中的示例0x9L更新实际开发时一律读取 versions.h 中的现值。4. 更新 version_map.cpp 版本映射表在 torch/csrc/jit/operator_upgraders/version_map.cpp 中为算子注册版本映射条目格式如下且必须按 bump 到的版本号排序{{${operator_name.overloaded_name}, {{${bump_to_version}, ${upgrader_name}, ${old operator schema}}}},对于linspace若存在两次版本提升一次升到 8、一次升到 12排序后的结果是{{aten::linspace, {{12, linspace_0_11, aten::linspace(Scalar start, Scalar end, int? stepsNone, *, ScalarType? dtypeNone, Layout? layoutNone, Device? deviceNone, bool? pin_memoryNone) - Tensor}}}, {{8, linspace_0_7, aten::linspace(Scalar start, Scalar end, int? stepsNone, *, ScalarType? dtypeNone, Layout? layoutNone, Device? deviceNone, bool? pin_memoryNone) - Tensor}}},version_map.cpp中实际存储的是std::unordered_mapstd::string, std::vectorUpgraderEntry operatorVersionMap并在首次访问时通过get_operator_version_map()对每个算子的条目按bumped_at_version降序排序见该文件中的std::sort逻辑保证查找时优先命中最新版本的 upgrader。文件中还提供了test_only_add_entry、test_only_remove_entry、test_only_reset_flag等测试专用接口以及calculate_package_version_based_on_upgraders/get_version_calculator_flag用于按 upgrader 计算 package 版本的开关。当前仓库中已注册的算子版本映射包括算子bump 版本upgrader 名旧 schema 摘要aten::linspace8linspace_0_7stepsNone可选aten::linspace.out8linspace_out_0_7stepsNone可选aten::logspace/.out9logspace_0_8/logspace_out_0_8stepsNone可选aten::div.*8 个变体4div_*_0_3旧整数除法语义aten::full/.out5full_0_4/full_out_0_4旧 dtype 推断aten::gelu/.out10gelu_0_9/gelu_out_0_9无approximate参数5. 自动生成移动端 upgrader 代码重新从源码构建 PyTorch 后运行python pytorch/torchgen/operator_versions/gen_mobile_upgraders.py该脚本会自动更新 torch/csrc/jit/mobile/upgrader_mobile.cpp把upgraders_entry.cpp与version_map.cpp的内容同步到移动端lite interpreter使用的代码中。官方建议的构建方式是pip install -e . --no-build-isolation。6. 编写测试利用步骤 1 生成的旧模型在test/test_save_load_for_op_versions.py中添加测试。仓库中对应文件为 test/jit/test_save_load_for_op_version.py测试类TestSaveLoadForOpVersion(JitTestCase)其中已包含test_versioned_div_scalar、test_versioned_div_scalar_reciprocal、test_versioned_div_scalar_inplace等大量用例。官方指南给出的测试模板settings(max_examples10, deadline200000) # A total of 10 examples will be generated given( sample_inputst.tuples(st.integers(min_value5, max_value199), st.floats(min_value5.0, max_value199.0)) ) # Generate a pair (integer, float) example((2, 3, 2.0, 3.0)) # Ensure this example will be covered def test_versioned_div_scalar(self, sample_input): # Step 1. Write down the old behavior of this operator, if possible def historic_div_scalar_float(self, other: float): return torch.true_divide(self, other) # Step 2. Write down how current module should look like class MyModuleFloat(torch.nn.Module): def __init__(self) - None: super().__init__() def forward(self, a, b: float): return a / b try: # Step 3. Load the old model and it will apply upgrader v3_mobile_module_float _load_for_lite_interpreter( pytorch_test_dir /jit/fixtures/test_versioned_div_scalar_float_v2.ptl) v3_server_module_float torch.jit.load( pytorch_test_dir /jit/fixtures/test_versioned_div_scalar_float_v2.ptl) except Exception as e: self.skipTest(Failed to load fixture!) # Step4. Load the new model and it wont apply the upgrader current_mobile_module_float self._save_load_mobile_module(MyModuleFloat) current_server_module_float self._save_load_module(MyModuleFloat) for val_a, val_b in product(sample_input, sample_input): a torch.tensor((val_a,)) b val_b def _helper(m, fn): m_result self._try_fn(m, a, b) fn_result self._try_fn(fn, a, b) if isinstance(m_result, Exception): self.assertTrue(fn_result, Exception) else: self.assertEqual(m_result, fn_result) # Ensure the module loaded from the old model with upgrader # has the same result as the module loaded from the new model _helper(v3_mobile_module_float, current_mobile_module_float) _helper(v3_mobile_module_float, current_server_module_float) # Ensure the module loaded from the new model with upgrader # has the same result as the module loaded from the new model _helper(current_mobile_module_float, torch.div) _helper(current_server_module_float, torch.div)测试的核心验证逻辑分为四步描述旧行为写出历史版本下算子的等价实现如torch.true_divide描述当前模块形态定义当前新语义下的nn.Module加载旧模型用_load_for_lite_interpreter和torch.jit.load分别加载旧.ptl模型此时会自动应用 upgrader交叉比对结果旧模型经 upgrader的输出必须与当前模型、以及直接调用新算子的输出保持一致同时验证新模型加载时不会应用 upgrader。仓库中 test/jit/test_save_load_for_op_version.py 的既有用例还覆盖了 int/float 两种标量、inplace 变体、reciprocal 变体等更多场景可继续参照。7. 提交单个 PR把第 2 步的所有改动放在一个PR 中提交。官方指南还给出两个参考 PR新增logspace测试模块的 PR、更新logspace算子的 PR用于整体感受改动的完整形态。关于 FC-breaking 的说明官方指南明确指出FC-breaking 变更的解决方案目前还不存在。如果你遇到如下 FC 破坏场景且希望得到支持请到 PyTorch Forum 或 GitHub 上报官方会据此排定优先级新增默认参数在非末尾位置非 out 参数区之前插入新的默认参数例如foo(Tensor self, int a, int b1, Tensor(a!) out)变为foo(Tensor self, int a, int c1, int b1, Tensor(a!) out)在 schema 非末尾位置新增 out 参数新增容器类型ListType/DictType的默认参数如int[2] c1修改默认参数名仅当该参数总是使用默认值、序列化时会忽略它时才可行其他情况都会失败修改默认参数的默认值新运行时若以默认值保存该参数旧运行时会用旧默认值导致错误输出新增算子。总结与自查清单完成一次 BC-breaking 算子变更的完整动作清单改动前在 test/jit/fixtures_srcs/fixtures_src.py 添加TestVersioned{Op}{Overload}V{version}模块在 test/jit/fixtures_srcs/generate_models.py 的ALL_MODULES注册运行python test/jit/fixtures_src/generate_models.py导出旧模型先提交模型相关 PR改动后修改算子在 torch/csrc/jit/operator_upgraders/upgraders_entry.cpp 以 TorchScript 编写命名规范为op_overload_start_end的 upgrader在 caffe2/serialize/versions.h 提升kMaxSupportedFileFormatVersion与kProducedFileFormatVersion并写明原因在 torch/csrc/jit/operator_upgraders/version_map.cpp 添加按版本排序的映射条目重建后运行python pytorch/torchgen/operator_versions/gen_mobile_upgraders.py同步移动端代码在 test/jit/test_save_load_for_op_version.py 添加测试最后把所有改动合成一个 PR提交。核心原则一句话upgrader 只服务于旧模型 新运行时这一种组合——新模型永远不会触发 upgrader旧模型在旧运行时也不需要 upgrader。把握好这一点再配合版本映射表与 180 天策略就能在 PyTorch 生态中安全、可追溯地演进算子语义。【免费下载链接】pytorchTensors and Dynamic neural networks in Python with strong GPU acceleration项目地址: https://gitcode.com/GitHub_Trending/py/pytorch创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表