Prhub

#1950 Support non-finite floats across the wire

原始 PR 作者 fzyzcjy 合并时间 2026-09-04 13:27 文件变更 4 提交数 1 评论 1 代码增减 +336 / -6

执行摘要

引入 _NonFiniteFloatCodec,修复 NaN/Inf 在 RPC 线上丢失的问题。

PR 标题为 'Support non-finite floats across the wire',PR body 为空。结合实现上下文(如测试用例 test_non_finite_result_survives_the_wire),核心动机是修复 RPC 通信中因 JSON 不允许 NaN/Inf 导致的特殊浮点值丢失问题,确保如损失值、梯度范数等训练指标在 Worker 和客户端间传输的完整性。

该 PR 值得精读。其核心设计——通过引入一个轻量级递归编解码器来处理 JSON 序列化的固有缺陷——清晰、有效且可扩展。实现集中、测试完备,是处理类似数据完整性问题的优秀范例。关注点应放在 _NonFiniteFloatCodec 的递归实现和其在 RpcSerializer 中的无缝集成上。

讨论亮点

Review 审核由 guapisolo 在 stack 上下文下完成,通过了 safe-merge checkpoint #2176。评审意见为:'The serialization design holds, and no end-to-end-backed blocker remains. LGTM.' 这表明评审认可了序列化设计,并确认没有端到端测试覆盖的阻塞性问题。本次 PR 未发现公开的争议或设计权衡讨论。

实现拆解

  1. 引入非有限浮点数编解码器:在 miles/utils/workers/rpc/common/serialization.py 中新增 _NonFiniteFloatCodec 类。该类提供 encode 和 decode 两个类方法,分别负责在序列化前将非有限浮点数(NaN, Inf, -Inf)递归转换为特定字典格式(如 {'__miles_non_finite_float__': 'nan'}),并在反序列化后还原。原因:标准 JSON 库的 allow_nan=False 无法处理这些值,此编解码器提供了安全、递归的转换层。影响:为 RPC 序列化核心路径增加了数据保真度。
  2. 更新 RPC 序列化器:修改 RpcSerializer 的 encode_query、decode_query、encode_result、decode_result 四个方法,在原有的 Pydantic 模型序列化/反序列化步骤前后,分别调用 _NonFiniteFloatCodec.encode 和 _NonFiniteFloatCodec.decode。原因:确保所有进出 RPC 系统的负载都经过编解码器处理,无论其类型注解如何。影响:这是变更的核心集成点,将编解码器应用于所有 RPC 通信。
  3. 调整 Pydantic 序列化配置:将全局 _WIRE_CONFIG 增加 ser_json_inf_nan='constants' 配置项。原因:配合 _NonFiniteFloatCodec,确保 Pydantic 层面也能识别这些特殊值,而非在 dump 阶段就出错。影响:配置与编解码器协同工作,共同完成对非有限浮点数的支持。
  4. 增加单元测试与端到端测试:在 tests/fast/utils/workers/rpc/common/test_serialization.py 中新增 TestNonFiniteFloatRoundtrip、TestNonFiniteFloatValues 等测试类,覆盖各种数据结构(标量、字典、列表、模型、数据类)中的非有限浮点数往返。在 tests/fast/utils/workers/e2e/test_complex_types.py 中新增 TestNonFiniteFloats 测试类,通过模拟真实的 RPC Worker 通信验证端到端正确性。原因:确保新机制的正确性和鲁棒性。影响:提供了坚实的测试保障。
  5. 扩展端到端测试工作器:在 tests/fast/utils/workers/e2e/e2e_worker.py 中新增 Metric 模型和一系列演示方法(如 demo_nan_result, demo_float),为端到端测试提供支持。原因:为端到端测试用例提供必要的 Worker 端逻辑。影响:使集成测试成为可能。
文件 模块 状态 重要度
miles/utils/workers/rpc/common/serialization.py 序列化 modified 7.81
tests/fast/utils/workers/rpc/common/test_serialization.py 序列化测试 modified 7.77
tests/fast/utils/workers/e2e/test_complex_types.py 复杂类型测试 modified 7.06
tests/fast/utils/workers/e2e/e2e_worker.py 测试工作器 modified 6.68

关键符号

_NonFiniteFloatCodec.encode _NonFiniteFloatCodec.decode RpcSerializer.encode_query RpcSerializer.decode_query RpcSerializer.encode_result RpcSerializer.decode_result

关键源码片段

miles/utils/workers/rpc/common/serialization.py dependency-wiring

核心变更文件,引入 _NonFiniteFloatCodec 并集成到 RpcSerializer,直接修复非有限浮点数传输问题。

class _NonFiniteFloatCodec:
    """递归编解码器,用于在 JSON 序列化中安全地传输非有限浮点数 (NaN, Inf, -Inf)。"""
​
    @classmethod
    def encode(cls, value: Any) -> Any:
        """递归编码:将数据结构中的非有限浮点数替换为特定字典标记。"""
        if isinstance(value, float) and not math.isfinite(value):
            # 将 NaN/Inf/-Inf 映射到对应的字符串 token,并包装在特定字典中
            return {NON_FINITE_FLOAT_TAG: _TOKEN_BY_FLOAT.get(value, "nan")}
        if isinstance(value, dict):
            # 保留键冲突检测:如果用户数据中包含标记键,则抛出异常
            if NON_FINITE_FLOAT_TAG in value:
                raise ValueError(f"rpc payloads must not contain the reserved key {NON_FINITE_FLOAT_TAG!r}")
            # 递归编码字典中的每个值
            return {key: cls.encode(item) for key, item in value.items()}
        if isinstance(value, list):
            # 递归编码列表中的每个元素
            return [cls.encode(item) for item in value]
        # 其他类型(如 int, str, None)直接返回,无需处理
        return value
​
    @classmethod
    def decode(cls, value: Any) -> Any:
        """递归解码:将特定字典标记还原为对应的非有限浮点数。"""
        if isinstance(value, dict):
            token = value.get(NON_FINITE_FLOAT_TAG)
            # 判断当前字典是否恰好是标记格式:只有一个键且键值是有效 token
            if len(value) == 1 and token in _FLOAT_BY_TOKEN:
                return _FLOAT_BY_TOKEN[token]
            # 否则,递归解码字典中的每个值
            return {key: cls.decode(item) for key, item in value.items()}
        if isinstance(value, list):
            # 递归解码列表中的每个元素
            return [cls.decode(item) for item in value]
        # 其他类型直接返回
        return value

评论区精华

PR 审核 other

guapisolo 在 stack 上下文下批准了 PR,确认序列化设计有效且无端到端阻塞问题。

结论:PR 被批准并合并。 · 已解决

风险与影响

  1. 性能开销:_NonFiniteFloatCodec.encode/decode 在每次 RPC 编解码时都会递归遍历整个数据结构。对于包含大量基本类型的大型负载,这可能引入额外的性能开销。具体文件/逻辑:serialization.py 中的递归遍历逻辑。
  2. 保留键冲突:编解码器使用固定键 __miles_non_finite_float__ 作为标记。如果用户数据中意外包含此键(尽管概率低),编解码器会抛出 ValueError。具体文件/逻辑:encode 方法中的冲突检测逻辑。
  3. 与现有逻辑的兼容性:此次变更为 RPC 序列化的全局行为引入了新的处理层,可能影响所有使用 RpcSerializer 的路径,需要确保无回归。具体文件/逻辑:RpcSerializer 的四个方法。

对用户:确保了通过 RPC 传输的训练指标(如 NaN/Inf 损失值)能被正确接收和处理,避免了数据静默丢失。对系统:这是 RPC 序列化模块(miles.utils.workers.rpc.common.serialization)的核心变更,直接影响 Worker 与客户端间所有类型化数据的传输,影响范围广但精确。对团队:引入了新的设计模式(编解码器递归处理),需要团队成员理解并维护;测试覆盖全面,降低了引入缺陷的风险。

序列化路径变更 递归性能开销 保留键冲突

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论