跳到主要内容

路由模块设计规格

版本:1.3.2 状态:Accepted 创建日期:2026-04-26 最后更新:2026-05-23 作者:xiang.li

版本号规则 (SemVer):

  • major (X.0.0):架构变更、接口不兼容、公式体系重构
  • minor (1.X.0):新增模型/算法、新增章节、扩展接口(向后兼容)
  • patch (1.0.X):修正笔误、补充说明、更新 Implementation Notes

变更历史

版本日期变更说明
1.3.22026-05-23支持 per-flowlet Adaptive Routing 算法实现(fat-tree 等价多路径间的 flowlet 级负载均衡,LetFlow style 纯 hash 切换,无拥塞反馈):(1) Config attrs schema 新增 flowlet_gap_ns: float(同一 flow 两 packet 间隔超过该阈值视为新 flowlet,触发重新选路);(2) 新增第 6 类 Layer 2 Policy PerFlowletAdaptivePolicy——cache key (flow_id, flowlet_id) → path_index,flowlet_id 由 now - last_pkt_ts > flowlet_gap_ns 单调递增;(3) @tbl-22 Strategy↔Policy 映射新增 (first_hop, multi) → PerFlowletAdaptivePolicy 行(与既有 (first_hop, distribution) → FirstHopAdaptivePolicy 区分);(4) @tbl-23b descriptor schema 新增 per_flowlet_adaptive 行(含 flowlet_gap_ns 必填字段);(5) 新增 L2-C7 约束——flowlet 缓存语义(同 flowlet 内严格粘滞 / flowlet 边界处允许换路径),明确与 L2-C1 "FirstHopAdaptive 全 flow 粘滞"的区分;(6) @tbl-13a 决策频次 first_hop 行典型算法补 per-flowlet AR + 表注澄清 "首跳期或 flowlet 边界期";(7) §业界参考补 NVIDIA Quantum ar_ftree / LetFlow / CONGA 引用。SemVer 决策:本次新增 1 类 Layer 2 Policy + 1 行 descriptor schema + 1 条 L2 约束,按 §风险与限制 "5 类 Policy 覆盖不够 → 新增 Policy 类需 bump spec minor 版本" 严格判定本应 minor v1.4.0;用户裁定 patch v1.3.2,理由:既有 first_hop decision_frequency 类目下加变体(hash 触发 vs queue-aware 触发),不引入新核心抽象、不修改 Goals、不破坏 L2-C1 / L2-C3 既有约束。变更历史记账保留 SemVer 决策痕迹
1.3.12026-05-23支持 D-mod-k 算法实现(Fat-Tree 专用,static + single,复用 StaticTablePolicy):(1) Edge attrs schema 新增 level: int + port_idx: int(D-mod-k 消费,标识 fat-tree 边层级 + 显式物理端口号,决策 1 选 EdgeSpec attrs 而非字典序隐式契约);(2) Config attrs schema 新增 w_levels: list[int] + p_levels: list[int](PGFT 通用参数,标准 k-ary n-tree 是特例 w_l=k, p_l=1);(3) §Non-Goals 新增 NG9 "不处理 fat-tree 故障 / 降级拓扑"——D-mod-k 均匀性依赖完整对称性(RLFT 等分带宽),降级下 Lemma 3 整除性失效;(4) §业界参考补 D-mod-k 闭式公式选路特例段;(5) §未来方向加 Dmodc(降级 fat-tree 改进);(6) §风险与限制加"邻居命名约束已通过 port_idx 显式化"决策痕迹。SemVer 决策:本次升 patch v1.3.1 而非 minor,理由是 NG9 仅明确既有限制(D-mod-k 算法本身依赖 fat-tree 对称性是业界常识),不引入新范围契约;与 v1.3.0 加 NG8 配合新算法 + spec 大改升 minor 不同质
1.3.02026-05-23(1) Config attrs schema 新增 congestion_aware: bool key(PacketSpray 必填),用于支持 Layer 1 PacketSprayStrategy 实现(per_packet + multi);(2) §Non-Goals 新增 NG8"不建模报文乱序 / 重排序"——乱序是 packet 级路径分散的直接语义后果,单独成项以明确范围契约(spec review F001 决策);(3) §业界参考补 Packet Spraying 选路权重特例段;(4) §未来方向"Packet Spraying / DQPLB"行标 Packet Spraying 接口已就绪;(5) §验收标准 加 congestion_aware schema 校验单测一行;(6) 复用 @tbl-22 既有 per_packet → PacketSprayPolicy 映射,不新增行。属新增算法接口 + 范围扩展,minor bump
1.2.12026-05-22修复 v1.2.0 的 5 处内部矛盾 / 漏洞:(1) 修正 §decision_frequency 枚举语义表中 ECMP 描述(归 static 与 §Strategy↔Policy 映射表对齐);(2) minimal_count 改为 Config attrs schema 配置项(类属性降级为缺省值),消解"类属性硬编码 = 1 但 UGAL 变体需不同值"矛盾;(3) 新增 PolicyDispatcher 抽象(Layer 2 包装层)承载 L2-C2 "NetworkState 未声明字段保护"约束,trait 层无法 enforce 的 issue 在 dispatcher 层解决;(4) 新增 §Policy Descriptor Schema 表,固定 Layer 1 → Layer 2 启动期桥接协议字典,消除 v1.2.0 启发式推断;(5) §NetworkState 字段表加"最低精度要求"列,禁止 link_load / flow_count_on_link 常返 0
1.2.02026-05-20引入双层架构:Layer 1 RoutingStrategy(Python,全局路径计算)+ Layer 2 ForwardingPolicy(Rust,G5 仿真期每跳决策);新增 NetworkState 观测接口契约(adaptive 算法消费运行时状态);RoutingStrategy 增 2 类属性 decision_frequency / requires_runtime_state;明确 PathSet 在不同 output_form 下的语义(等代价 / minimal-valiant 分段);spec 范围扩展到 Rust 侧仿真器集成
1.1.02026-04-27选路权重改为带宽倒数(对齐 OSPF RFC 2328);路径延迟与选路权重解耦;删除 tier 分解抽象(边 type 即层级标识)
1.0.02026-04-26初版:定义路由模块的概念模型、四个核心抽象的接口契约,以及"边可选属性 + 算法显式声明消费"的族特征载体设计

@tbl-spec-route-01 文档变更历史


背景与目标

概述

路由模块为 G5 事件驱动仿真提供 (通信请求 → 物理路径) 的服务。模块以族无关的通用图为输入;族特定信息(维度归属、链路角色等)作为边的可选属性表达;路由算法显式声明它消费哪些属性,从而支持任意拓扑族而不必引入族抽象。模块内部由四个稳定抽象组成:NetworkGraph(图)、RoutingRequest(请求)、RoutingStrategy(算法)、RoutingResult(结果)。

G5 仿真对路由模块的能力需求

G5 是事件驱动仿真器,每个通信事件需要决定"走哪条/哪些物理路径",并基于路径推进后续 switch 队列、链路争用等事件。路由模块要回答四类问题:

问题用途
给定 (src, dst,流量元信息),使用哪些物理路径?报文级转发决策
路径上每条物理边承担多少流量?链路争用建模
通信的累计延迟和瓶颈带宽是多少?时间推进
当算法是逐报文随机或动态决策时,决策结果是分布而非单条路径表达 Packet Spraying / UGAL 等算法

@tbl-spec-route-02 G5 仿真对路由模块的能力需求:问题,用途

多拓扑族的差异化路由需求

13 个拓扑族(ring / fat-tree / dragonfly / dragonfly+ / torus-1d/2d/3d / mesh / hyperx / hypercube / slimfly / polarfly / single-switch / full-mesh)的最优路由算法不同:fat-tree 用 ECMP,torus 用 DOR(维度顺序),dragonfly 用 UGAL(最短路与 Valiant 路混合),spraying 类需要逐报文随机化。模块必须支持算法插拔,且新增算法不应改动图模型或现有算法的代码。

通用图描述与族特征的张力

项目的拓扑 YAML 已设计为族无关的通用图描述network.links 段是通用边列表,无 topo_kind 字段)。这一设计哲学的好处是拓扑生成器、校验器、可视化对所有族同构。但 DOR/UGAL 等族特定算法需要"维度归属""组身份"等族特征 — 这些特征必须以不破坏族无关图描述的方式进入算法。

需要的能力可以归结为:图模型族无关 + 算法对族特征显式声明 + 多种路径输出形态统一表达。

名词

名词定义区分
NetworkGraph节点 + 边 + 节点物理属性 + 边物理属性 + 边可选 attrs仅描述图结构与物理属性,不含族标签
RoutingRequest单次通信请求的输入对象(src、dst、流量元信息)G5 视角下"一次通信"的边界
RoutingStrategy路由算法实现通过 consumes_edge_attrs 声明信息依赖
RoutingResult路径决策的结果对象提供路径、边占用、属性查询接口
Path单条物理路径(节点序列 + 边序列)RoutingResult 的内部基本元素
attrs边上的可选字典属性族特征的载体;合法 key 由模块 schema 集中维护
输出形态路径决策的形式:single(单路径) / multi(路径集合) / distribution(路径分布)由 Strategy 声明,与算法的"路径多样性"标签一致
算法注册中心已知 RoutingStrategy 的命名表,工厂据此实例化与 attrs schema 是两个不同的注册表
ForwardingPolicyLayer 2 Rust 侧 trait,承接 Layer 1 PathSet 做仿真期每跳决策与 Strategy 的生命周期不同:Strategy 启动期算路径、Policy 运行期选下一跳
NetworkStateLayer 2 只读 trait,仿真器暴露给 Policy 的运行时观测接口(链路负载、队列深度等)只读;按 requires_runtime_state 声明字段访问
PathSetRoutingResult 中的路径集合(同 paths() 迭代器内容)single / multi / distribution 三种形态有不同语义(见 §PathSet 语义)
minimal_countdistribution 形态算法的额外类属性,标记 PathSet 中前 N 条为 minimal 路径仅 distribution 必填,其他形态算法为 None
decision_frequencyRoutingStrategy 类属性,标记算法决策时机(static / first_hop / per_hop / per_packet)决定 G5 加载期挑选哪种 ForwardingPolicy
PolicyDispatcherLayer 2 包装层,持有 ForwardingPolicy + Strategy 声明的 requires_runtime_state,在每次 next_hop 调用前后审计 Policy 的 NetworkState 字段访问在 trait 层无法 enforce 的"未声明字段保护"(L2-C2)由该层运行时拒绝;不参与算法语义,仅做契约校验
flow 首跳flow 第一个 packet 经过的首个 switch hop——即 FirstHopAdaptive 与 EcmpHash 类算法的决策时机与 flowlet 首跳 区分(前者粒度 = flow,后者粒度 = flowlet)
flowlet 首跳flowlet 起始 packet 经过的首个 switch hop——即 PerFlowletAdaptive 算法的决策时机;同一 flow 内每个 flowlet 各有一次"首跳"当 flow 内仅 1 个 flowlet 时,flowlet 首跳 == flow 首跳
flowlet同一条 flow 中的一段连续突发,由两 packet 间隔超过 flowlet_gap_ns 阈值切分;同一 flowlet 内 packet 必须沿用同一路径(L2-C7.c)flow 是路由模块抽象的最小流标识(src/dst/qp_id 等),flowlet 是 flow 的子粒度划分

@tbl-spec-route-03 名词

目标

Goals

  • G1:以单一统一接口为 G5 提供 (请求 → 路径) 服务,调用方面对 RoutingResult 一种契约消费三种输出形态
  • G2:图模型族无关 — 任何拓扑族都用同一份 NodeSpec / EdgeSpec schema 表达;族特征通过边的 attrs 字段流入
  • G3:算法插拔 — 新增算法只需实现 RoutingStrategy 接口、注册到工厂、声明属性消费;不需要改图模型或既有算法
  • G4:算法显式声明 attrs 消费consumes_edge_attrs / consumes_config_attrs 列出 key 集合;加载期对 graph 与 routing.attrs 双层校验;零代码默认值(遵守 .claude/rules/config-loading.md
  • G5:三种输出形态(single / multi / distribution)统一在 RoutingResult 接口下表达,避免 isinstance 分支
  • G6:模块边界严格 — 路由模块的输出止于 RoutingResult;如何被通信延迟模型 / 争用模型 / 仿真器消费不在本 spec 范围
  • G7:在不引入 per-hop Python ↔ Rust 回调成本的前提下,把 G5 仿真期每跳决策、运行时状态消费、5 类常见路由模式的语义统一约束在 spec 内(v1.2.0 引入:Layer 1 RoutingStrategy + Layer 2 ForwardingPolicy + NetworkState)

不涉及范围

  • NG1:不实现完整闭环 SDN 反馈控制——Layer 2 NetworkState 仅暴露只读观测,policy 不能修改仿真状态;仿真器内部并发控制 / 跨进程协议不在本 spec 范围。运行时反馈(出口队列深度、链路负载等)通过 NetworkState 暴露
  • NG2:不为 Math 代数模型做专属优化(接口以 G5 需求为主;Math 接入沿用相同接口即可)
  • NG3:不引入 SDN / SRv6 / 中央控制器
  • NG4:不解决跨进程的路由决策(路由模块仍是进程内库)
  • NG5:不做拓扑发现 / 故障恢复 / 链路抖动建模
  • NG6:不引入族抽象(TopologyDescriptor / 子类化 EdgeSpec);族信息只以边 attrs 形式存在
  • NG7:不基于图特征隐式推断算法;routing.algorithm 必须由用户在 YAML 显式声明。算法选择是实验意图,由模块自动选会破坏对比实验、损害可重复性、引入图特征歧义
  • NG8:不建模报文乱序 / 重排序。Packet Spraying 等 per-packet 算法在物理上必然产生乱序到达,上层协议(RDMA / TCP)需重排序缓冲恢复有序——本模块仿真侧仅产出"哪些报文走哪条路径 / 何时到达",不建模重排序延迟、缓冲占用、丢包重传等上层语义。需要时另立 spec 处理
  • NG9:不处理 fat-tree 故障 / 降级拓扑。D-mod-k 算法的无阻塞性质依赖完整 fat-tree 对称性(RLFT 等分带宽约束 $m_l \cdot p_l = w_{l+1} \cdot p_{l+1}$),任一链路 / switch 故障破坏 Lemma 3 整除性后,D-mod-k 在 Shift 流量下的均匀性失效。本模块不建模故障 / 降级;需要降级场景适配时另立 spec 引入 Dmodc(Gliksberg et al., IEEE MICRO 2023)等改进算法。与 NG5 的区分:NG5 排除"建模故障事件本身"(仿真侧能力边界——不模拟链路抖动 / 拓扑发现),NG9 进一步排除"对降级拓扑下算法性质失效的补偿"(算法正确性前提——D-mod-k 需要对称性);两者语义不冲突且互补。NG9 作为范围契约:未来引入 Dmodc 取消 NG9 时是 minor bump 触发条件(与新算法接口绑定)

业界参考

业界路由建模的"族 vs 通用图"张力

工具族信息载体算法切换方式抽象代价链接
BookSim 2按族分代码文件(fattree.cpp / dragonfly.cpp / torus.cpp编译期符号绑定13 族 ≈ 13 套独立路由代码https://github.com/booksim/booksim2
SST/macro拓扑工厂对象(class FatTreeclass Dragonfly拓扑工厂驱动算法拓扑与算法绑死,复用性低https://github.com/sstsimulator/sst-macro
ASTRA-sim 2.0network backend 内部隐藏backend 选择(NS-3 / Garnet / Analytical)路由不是一等公民https://github.com/astra-sim/astra-sim
SimGridXML 平台描述 + 算法属性(Floyd / Dijkstra / Cluster)XML 声明表达力有限,复杂算法需 C++ 扩展https://simgrid.org
HTSim路径选择嵌在 Pipe / Queue 层C++ PathChooser不支持异构拓扑混合https://github.com/Broadcom/csg-htsim

@tbl-spec-route-04 业界路由建模的"族 vs 通用图"张力:工具,族信息载体

共同模式:图 + 算法 + 路径结果的三层结构是普遍设计;本模块继承这一模式但额外增加 RoutingRequest 抽象以承载流量元信息(QP id、消息类型)。

本模块的差异化设计:族特征以"边的可选属性 + 算法显式声明消费"形式融入通用图,避免:

  • 子类化方案的类爆炸(13 族 × 多算法)
  • 族抽象渗透到接口(违反 YAML 的"通用图描述"哲学)
  • 算法图反推的歧义(部分图特征不足以唯一确定族)

选路权重的业界实践

路由算法的"权重"(决定路径选择的代价度量)在不同算法中有截然不同的定义:

算法选路权重来源
OSPF/IS-IS → ECMP带宽倒数(Cost = 参考带宽 / 链路带宽RFC 2328 (OSPF Version 2)
DOR无权重,坐标确定性转发Dally & Seitz, 1987
UGAL动态代价(队列深度 + 跳数 × w_hop)Kim, Dally, Abts, ISCA 2008
BookSim / KSP跳数

@tbl-spec-route-05 选路权重的业界实践:算法,选路权重

关键结论:业界主流的静态选路权重是带宽倒数(OSPF),不是延迟或跳数。带宽倒数的语义是"优先走高带宽链路",对 AI 集群的大块集合通信场景尤为合适。本模块 shortest_path / ECMP 采用带宽倒数作为选路权重,与 OSPF 对齐。

报文级散布的特例:Packet Spraying / DRB 不采用"权重"概念,而是以报文 ID 或 EV(Entropy Value)字段做哈希散布到等代价多路径,散布的均匀性来自统计分布而非权重选优;详见 3.7 Packet Spraying

闭式公式选路的特例:D-mod-k(Zahavi, CCIT #776, 2010)以目的节点编号的位段做模运算选择上行端口,PGFT 通用形式:

$$\begin{equation} q_l(j) = \left\lfloor \frac{j}{\prod_{k=1}^{l} w_k} \right\rfloor \bmod (w_{l+1} \cdot p_{l+1}) \label{eq:spec-route-dmod-k-quotient} \end{equation}$$

标准 k-ary n-tree 是特例($w_l = k, p_l = 1$)退化为 port = floor(d/k^l) mod k。给定 PGFT 参数(@tbl-spec-route-11 的 w_levels / p_levels)即可闭式计算路径,不需要 Dijkstra / 权重;OpenSM ftree 路由引擎是其工程实现。与权重路由不同,D-mod-k 的"最优性"来自代数结构(Lemma 1-3 整除性 + RLFT 等分带宽)而非启发式优化,需要拓扑严格对称(NG9)。详见 3.3 D-mod-k

选路权重与路径延迟的分离:选路权重决定"走哪条路",路径延迟是"走完这条路要多久"。两者是独立关注点 — 带宽倒数用于路径选择(公式 $\eqref{eq:rt-edge-weight}$),延迟用于结果计算(公式 $\eqref{eq:rt-path-latency}$)。不同算法可能使用不同权重(DOR 无权重、UGAL 用动态代价),但路径延迟计算公式对所有算法通用。

路由算法选型空间

../interconnect/03-路由算法/ 已 survey 7 种路由算法(ECMP、Adaptive Routing、DOR、UGAL、Packet Spraying、KSP、选型指南)。本 spec 引用其结论作为算法能力空间的描述背景,不重复算法细节。每个算法所需的图特征(坐标、组身份、链路角色等)映射为 attrs 中的若干 key。

项目内引用

使用示例

G5 仿真器一次典型路由调用:

# 1. 模块加载期
yaml_data = load_yaml("configs/topologies/torus-3d-C64.yaml")
graph = NetworkGraph.from_yaml(yaml_data)

# 工厂构造期校验 routing.attrs 是否齐全(模块级 + dor 的 consumes_config_attrs)
# 缺失 raise MissingConfigAttr,类型错 raise InvalidConfigAttrType
strategy = create_routing_strategy(
name=yaml_data["network"]["routing"]["algorithm"], # "dor"
attrs=yaml_data["network"]["routing"]["attrs"], # 直接透传,不做默认值填充
)

# bind 期校验:strategy.consumes_edge_attrs = {"dimension", "wraparound"}
# 检查 graph 所有边的 attrs 是否包含这两个 key 且类型正确;缺失 raise IncompatibleStrategyError
strategy.bind_graph(graph)

# 2. 仿真运行期,每次通信
request = RoutingRequest(src="c0", dst="c63", bytes=1024, kind="all-reduce")
result = strategy.compute(request)

# 3. G5 消费 result(接口统一)
for path in result.paths(): # iter[Path],与 output_form 无关
for edge_id in path.edges:
...
result.edge_load("e_42") # 用于争用建模
result.bottleneck_bandwidth() # 用于带宽估计
result.latency()

YAML 描述对所有族同构,族特征通过边的 attrs 字段表达:

network:
link_types:
c2c: { bandwidth_gb_per_s: 400, latency_us: 0.2 }
links:
# 通用边:通用算法看 from/to/type 即可工作
- {from: c0, to: c1, type: c2c}
# 带族特征的边:DOR / UGAL 等算法读 attrs
- {from: c2, to: c3, type: c2c, attrs: {dimension: x, wraparound: false}}
- {from: r0, to: r4, type: c2c, attrs: {role: global}}
switches: [...]
routing:
algorithm: dor # 必填
attrs: # 必填字典;模块级 + 算法级 key 都必填
bandwidth_utilization: 0.9 # 模块级
dimension_order: [x, y, z] # DOR 算法级
dateline_vc: true # DOR 算法级

通用算法(shortest_path / ECMP)忽略 attrs 段;族特定算法(DOR / UGAL)按声明读取。

设计详述

概念模型

模块由四个稳定抽象组成:

   ┌────────────────┐    ┌─────────────────┐    ┌──────────────┐
│ NetworkGraph │ │ RoutingRequest │ │ RoutingResult│
│ (图与物理属性) │ │ (一次通信请求) │ │ (决策结果) │
└────────┬───────┘ └────────┬────────┘ └──────▲───────┘
│ │ │
│ bind_graph() │ compute(request) │
│ │ │
└────────┐ ┌───────┘ │
▼ ▼ │
┌─────────────────┐ │
│ RoutingStrategy │────────────────────┘
│ (算法实现) │
└─────────────────┘

│ create_routing_strategy(name, config)

┌──────┴──────┐
│ 算法注册中心 │ + attrs schema(独立注册)
└─────────────┘

责任边界

抽象层级职责不负责
NetworkGraphLayer 1 (Python)节点和边的图论表示,节点物理属性、边物理属性、边可选 attrs不感知拓扑族;不缓存路由结果;不感知算法配置
RoutingRequestLayer 1 (Python)描述单次通信请求的输入:源、目、字节数、通信类型、QP id 等不携带运行时网络状态;不与具体算法耦合
RoutingStrategyLayer 1 (Python)实现具体路由算法的全局路径计算;声明属性消费 + 决策频率 + 运行时状态需求;提供 bind 与 compute 两阶段接口不做 per-hop 决策;不感知运行时网络状态;不持久化结果
RoutingResultLayer 1 (Python)路径决策结果的统一查询接口(PathSet + 派生指标)不重新执行算法;不修改自身内容
ForwardingPolicyLayer 2 (Rust)G5 仿真期的每跳决策:从 Layer 1 计算出的 PathSet 中选择下一跳;可选消费 NetworkState不计算路径(路径来源于 Layer 1 的 RoutingResult);不修改图与算法配置
NetworkStateLayer 2 (Rust)暴露仿真期可观测的网络状态(link 负载、队列深度等),供 adaptive 类 Policy 消费只读;不写仿真状态;不感知算法
PolicyDispatcherLayer 2 (Rust)包装 ForwardingPolicy + Strategy 声明的 requires_runtime_state,向 Policy 暴露受限 NetworkState 视图;运行时审计字段访问 (L2-C2)不实现算法语义;不修改 state;不缓存路径

@tbl-spec-route-06 责任边界:抽象,职责

设计原理

为什么族特征用边的可选属性?

族特征(维度归属、组身份、链路角色)本质是图上的"标签",不是节点边的物理本质。将其作为边的 attrs:

  • graph 模型保持族无关 — 与项目通用 YAML 描述哲学一致
  • 通用算法不读这些字段也能工作 — shortest_path 不感知 attrs
  • 族特定算法显式声明消费 → 类型 / 拼写错误在加载期暴露,不在运行时沉默

否决的替代方案

  • TopologyDescriptor / 族子类化:把族抽象升级为接口一等公民,违反 YAML 设计哲学,且导致 13 族类爆炸
  • 算法从图反推族特征:部分图特征不足以唯一确定族(4×4×4 vs 8×8×4 Torus 边数差异微妙),实现复杂且歧义

为什么 Strategy 显式声明 consumes_attrs?

如果不声明,attrs 字典会成为"什么都能塞"的杂物间,长期会出现:拼写错误(dim vs dimension)在运行时沉默忽略;不同算法对同一 key 的语义假设不一致;attrs schema 漂移无人维护。

显式声明 + 加载期校验带来三个好处:

  1. 错误前置:拼错 key 在 bind_graph 时立即 raise,不到 compute 才发现
  2. schema 可枚举:模块用到的所有 attrs key 是有限集合,可在文档与单测中穷举
  3. 算法-图兼容性显式:用户看到 Strategy 的 consumes_edge_attrs 就知道它对图的要求

为什么 RoutingResult 不强制全网预计算?

不同算法的"路径"语义截然不同:

  • DOR:闭式公式(给定 src, dst 立即计算),全表存储是浪费
  • ECMP:路径集合(适合预计算缓存)
  • UGAL:流量分布(每次决策动态加权,预计算意义不大)
  • Packet Spraying:逐报文随机(路径不是确定值,是分布)

强制 N×N 预计算表会让闭式算法浪费内存、动态算法失去精度。RoutingResult 是接口,实现可选 lazy / 闭式 / 全表,由具体 Strategy 内部决定。

为什么 Strategy 用 bind_graph + compute 两阶段?

  • bind_graph 是绑定时机:算法在此期校验 graph 是否提供它声明的 attrs,并完成预计算(如 ECMP 全网最短路)
  • compute 是查询时机:每次仅需 request,graph 已绑定

分离两者:避免每次 compute 重复校验或预计算;让"算法 - 图"绑定关系显式(一个 Strategy 实例对应一个 graph 绑定)。

为什么 RoutingResult 既提供 paths() 也提供 edge_load()?

paths() 服务于"我要看具体走哪条路"的用例(G5 报文级转发);edge_load() 服务于"这条边承担多少流量"的用例(争用建模)。两者是同一决策的不同视图

  • 单路径算法:edge_load 退化为 0/1
  • 多路径算法:edge_load 是流量在路径上的均分比例
  • 分布算法:edge_load 是路径分布在该边上的边缘概率

调用方按用途选择视图,不需关心算法形态。

为什么把路由拆成 Layer 1(Strategy)+ Layer 2(ForwardingPolicy)?

事件驱动仿真有两种根本不同的决策时机:

  • 全局路径计算(static):给定 (src, dst),"可能走哪些路径"——一次性回答,启动期完成
  • 每跳前进决策(runtime):给定 current_node 和实时状态,"下一跳去哪"——仿真期高频调用

单层方案(让 Strategy 同时承担两者)的问题:

  1. 性能:Python ↔ Rust 跨语言调用成本高;如果每跳决策都回调 Python,per-packet 仿真性能崩塌
  2. 职责混杂:static 算法的"全局视图"和 adaptive 算法的"运行时视图"塞进同一接口,复杂度向 max 对齐
  3. 不可扩展:未来加入 Q-routing / TE-CCL 等真正动态算法时,必须改 Strategy 接口

双层方案的取舍:

  • Layer 1 留在 Python:算法设计灵活、生态成熟、与 Math 评估器复用
  • Layer 2 在 Rust:per-hop 决策零跨语言开销;适合 trait 化承接 5 类算法
  • 桥接面薄:仅启动期一次传 PathSet + Policy descriptor,运行期 Layer 2 自洽

代价:spec 跨语言;新增算法需同时在 Layer 1 实现路径计算和 Layer 2 实现选路逻辑(但 5 类 Policy 已覆盖常见模式,多数新算法可复用现有 Policy)。

为什么 spec 必须覆盖 Rust 侧契约?

历史上 spec 只描述 Python 侧;但 Layer 2 引入了算法语义跨语言的强约束——例如"FirstHopAdaptivePolicy 的后续跳必须沿用首跳决策"是算法正确性约束,不是实现细节。若 spec 不覆盖 Rust 侧,则 Layer 2 实现的算法语义无外部参考,容易与 Layer 1 漂移。

spec 跨 Python / Rust 边界后:

  • 接口契约:在 spec(语义层描述 trait、字段、约束)
  • 具体实现:在 plan + code(Rust 文件路径、struct 字段、错误处理)

边界保持清晰:spec 用伪代码 + 语义描述,不写具体 Rust 语法 / 文件路径 / 函数实现

数据契约

NetworkGraph

字段类型必选含义
nodesdict[NodeId, NodeSpec]节点字典
edgesdict[EdgeId, EdgeSpec]边字典
adjacencydict[NodeId, set[EdgeId]]邻接索引(构造期生成)

@tbl-spec-route-07 NetworkGraph:字段,类型

NodeSpec(纯物理属性):

字段类型必选含义
idNodeId唯一标识
kind"chip" | "switch"节点类型
port_countint | Noneswitch 必选端口数
forwarding_latency_nsfloatswitch 必选转发延迟(纳秒,与 YAML schema 及 Layer 2 NetworkState 的 _ns 命名统一)

@tbl-spec-route-08 NetworkGraph:字段,类型

EdgeSpec(物理属性 + 可选 attrs):

字段类型必选含义
idEdgeId唯一标识
srcNodeId源端点
dstNodeId目标端点
bandwidth_gb_per_sfloat物理带宽(单方向)
latency_usfloat物理延迟
typestr链路类型(如 c2c / b2b / r2r / p2p)
attrsdict[str, Any]可选族特征载体;合法 key 见 edge attrs schema;缺省视为该边无族特征(不参与族算法消费)

@tbl-spec-route-09 NetworkGraph:字段,类型

attrs Schema(双层中央维护)

模块维护两份 attrs schema,分别约束图层与配置层的字典。每个 key 声明类型与含义。

Edge attrs schema(约束 EdgeSpec.attrs,描述图上的族特征):

key类型含义典型消费算法
dimensionstr边所属逻辑维度(如 "x" / "y" / "z")DOR
wraparoundbool是否环绕链路(用于 Torus dateline VC 标记)DOR (Torus)
role"local" | "global" | "intra"链路在族结构中的角色UGAL
levelint ∈ [0, h-1]fat-tree 边层级(0=chip→leaf-switch、1=leaf→spine、2=spine→core 等);h = len(w_levels) 树层数;图中所有 fat-tree 边的 level 取值范围与 w_levels 长度对齐(约束 C7)D-mod-k
port_idxint ≥ 0边在 src 节点的物理端口编号(与 D-mod-k 公式产出的端口编号 q 对齐;Strategy 直接读 attrs 找邻居 switch,避免依赖邻居名字典序的隐式契约)D-mod-k

@tbl-spec-route-10 attrs Schema(双层中央维护):key,类型

Config attrs schema(约束 routing.attrs,描述路由运行参数):

key类型含义消费方
bandwidth_utilizationfloat ∈ (0, 1]多路径聚合带宽折扣,作用于公式 \eqref{eq:rt-aggregate-bw}模块层(所有 Strategy 共用)
dimension_orderlist[str]维度路由顺序(如 ["x", "y", "z"])DOR
dateline_vcbool是否启用 Torus dateline 虚通道DOR (Torus)
max_pathsint多路径算法最多返回的路径数ECMP / KSP / PacketSpray
biasfloatUGAL 最短路与 Valiant 路决策偏置UGAL
valiant_kintValiant 中间组候选数UGAL
minimal_countint ≥ 1distribution 形态 PathSet 中前 N 条为 minimal 路径、其余为 valiant 候选UGAL 及其他 distribution 算法
congestion_awareboolPacket Spraying 拥塞感知模式开关:false=纯哈希散布(path_idx = hash(packet_id) mod N),true=对每个候选路径查首跳 link_load 后选最低者(argmin)PacketSpray (必填)
w_levelslist[int ≥ 1]PGFT 各层上行链路数 $w_l$(标准 k-ary n-tree 特例:$w_l = k$ 各层常数);长度 = fat-tree 层数 h;与 p_levels 等长;与图中 max(edge.level) + 1 一致(约束 C7)D-mod-k (必填)
p_levelslist[int ≥ 1]PGFT 各层 parent group 大小 $p_l$(标准 k-ary n-tree 特例:$p_l = 1$ 各层常数);长度同 w_levels(约束 C7)D-mod-k (必填)
flowlet_gap_nsfloat > 0同一 flow 两 packet 间隔阈值(仿真时刻差 now - last_pkt_ts),超过该值视为新 flowlet 起点,触发重新选路;业界典型 50000–2000000 ns(即 50 μs - 2 ms),分负载场景:轻负载 50000–100000 ns / 中等 100000–500000 ns / 重负载(AI AllReduce 突发)500000–2000000 ns;详见 ../interconnect/03-路由算法/04-自适应路由.md @tbl-route-ar-flowlet-gap-valuesPerFlowletAdaptive (必填)

@tbl-spec-route-11 attrs Schema(双层中央维护):key,类型

Schema 维护规则

  • 新算法引入新 attrs key → 扩展对应 schema 表(增加一行)
  • 边 attrs 含未注册的 key → 加载期 warning(约束 W1)
  • routing.attrs 含未注册的 key → 加载期 warning(同 W1)
  • routing.attrs 缺失被消费的 key → raise(见约束 C2、C6)

每个 RoutingStrategy 通过 consumes_edge_attrs / consumes_config_attrs 显式声明它消费哪些 key;模块层固定消费 config attrs 中的 bandwidth_utilization(所有算法共用)。

RoutingRequest

字段类型必选含义
srcNodeId源节点
dstNodeId目标节点
bytesint | None可选消息字节数(用于报文级算法)
kind"p2p" | "all-reduce" | "all-to-all" | "all-gather"可选通信类型 hint
qp_idint | None可选QP 标识(E-ECMP 类算法的哈希维度)

@tbl-spec-route-12 RoutingRequest:字段,类型

RoutingStrategy

类属性(声明):

属性类型必选含义
namestr算法名(工厂注册键)
output_form"single" | "multi" | "distribution"输出形态
consumes_edge_attrsset[str]算法读取的边 attrs key 集合(通用算法声明为空集 set()
consumes_node_attrsset[str]算法读取的节点 attrs key 集合(通用算法声明为空集 set(),保留扩展)
consumes_config_attrsset[str]算法读取的 routing.attrs key 集合(通用算法声明为空集 set()
decision_frequency"static" | "first_hop" | "per_hop" | "per_packet"决策时机分类——决定 G5 加载期挑选哪种 ForwardingPolicy(见 §Strategy ↔ Policy 映射)
requires_runtime_stateset[str]运行时状态需求 key 集合,G5 加载期据此校验仿真器已暴露对应 NetworkState 字段(静态算法声明为空集 set()
minimal_countint | Nonedistribution 必填缺省值:当用户未在 routing.attrs.minimal_count 显式提供时,Strategy 使用此类属性值;显式提供时以 attrs 为准。仅 output_form="distribution" 算法必填类属性(≥ 1 的 int);其他形态算法声明为 None。运行时真实值RoutingStrategy.minimal_count 实例属性暴露(构造期由工厂从 attrs.minimal_count 或类属性缺省取值后赋值)

@tbl-spec-route-13 RoutingStrategy:属性,类型

decision_frequency 枚举语义(命名易混淆,单列澄清):

何时做决策是否查 NetworkState后续 hop典型算法
static启动期一次性确定路径(含 hash 选路这种"对 flow_id 的纯函数"决策)沿用启动期决策DOR / D-mod-k / shortest_path / ECMP
first_hop首跳期或 flowlet 边界期查 NetworkState 或 sim_time 选路是(NetworkState 字段)或仅查 sim_time沿用当前 flow / flowlet 的首跳决策(缓存语义见 L2-C1 / L2-C7)UGAL / Valiant + queue-aware / per-flowlet AR
per_hop每跳查 NetworkState 重新决策不缓存Q-routing / Adaptive bubble
per_packet每报文决策可选不缓存Packet Spraying / DRB

ECMP 归 static 而非 first_hop 的理由:ECMP 的路径选择是 f(flow_id) → path_index纯函数(hash + 模运算),不读 NetworkState 任何字段。first_hop 的定义性特征是"首跳期消费运行时状态",ECMP 不符合。本表与下方 §Strategy ↔ Policy 映射表(static + multi → EcmpHashPolicy)严格对齐。

注:per-flowlet 自适应(业界 AR 常见粒度)归 first_hop——flowlet 起始期等价于"该 flowlet 的首跳"(见 §名词 "flowlet 首跳")。本 spec v1.3.2 内置的 per-flowlet AR 采用 LetFlow style 纯 hash 切换(不查 NetworkState 字段,仅消费 flow.sim_time_ns 判断 flowlet 边界 + hash 选路),requires_runtime_state = set();将来可扩展拥塞感知 flowlet AR 变体(如 CONGA / NVIDIA Quantum IB AR)补 link_load 消费,无需再改决策频次归属。

实例方法

方法入参返回含义
bind_graphgraph: NetworkGraphNone绑定 graph,校验属性消费,完成预计算
computerequest: RoutingRequestRoutingResult单次路由决策

@tbl-spec-route-14 RoutingStrategy:方法,入参

通用算法(如 shortest_path)的 consumes_edge_attrs / consumes_node_attrs / consumes_config_attrs 均为空集合 → 任意图上工作。族特定算法(如 DOR、UGAL)consumes_edge_attrs 非空 → 加载期对 graph 校验;含运行参数的算法(如 ECMP / DOR / UGAL)consumes_config_attrs 非空 → 加载期对 routing.attrs 校验。

RoutingResult

接口(实现可 lazy):

方法入参返回含义
pathsiter[Path]路径迭代器。distribution 形态下,路径权重由 RoutingResult.path_weight(i) 单独查询;前 minimal_count 条为 minimal、其余为 valiant(见 §PathSet 语义)
edge_loadedge_id: EdgeIdfloat ∈ [0, 1]该边在本次结果上承载的流量比例
bottleneck_bandwidthfloat整体瓶颈带宽(公式 \eqref{eq:rt-aggregate-bw})
latencyfloat主路径累计延迟(公式 \eqref{eq:rt-path-latency})
hop_countint主路径跳数

@tbl-spec-route-15 RoutingResult:方法,入参

Path 内部:

字段类型含义
nodeslist[NodeId]节点序列(含 src 与 dst)
edgeslist[EdgeId]边序列

@tbl-spec-route-16 RoutingResult:字段,类型

edge_load 的语义随 output_form 变化

output_formedge_load 含义
single0 或 1
multi1 / 路径数(同一边出现在多条路径时累加)
distribution边上路径分布的边缘概率(含报文级散布的统计平均)

@tbl-spec-route-17 RoutingResult: output_form, edge_load 含义

PathSet 语义(paths() 返回的路径集合在不同 output_form 下的解读):

output_formPathSet 解读Layer 2 消费方式
single唯一确定路径直接 lookup 节点序列
multiK 条等代价并列路径,无主次按 flow 哈希 / 报文 ID 选其中一条
distribution路径带权重;前 minimal_count 条为 minimal,其余为 valiantadaptive policy 根据 NetworkState 在 minimal/valiant 间动态选择

@tbl-spec-route-18 PathSet 语义:output_form,解读

distribution 形态的 PathSet 必须保证:路径列表前 N 条是 minimal(N = RoutingStrategy.minimal_count,已在属性表声明),其余为 valiant 候选。该约定是 Layer 1 与 Layer 2 的硬契约——Layer 2 直接按 index 划分 minimal/valiant,不再额外标注。

Strategy 工厂

入口(语义层):

create_routing_strategy(name: str, attrs: dict[str, Any]) → RoutingStrategy

  • name 必须在算法注册中心内,否则 raise UnknownRoutingAlgorithm
  • attrs 由 YAML routing.attrs 段直接透传(不经过任何代码层默认填充)
  • 工厂构造期校验 attrs 中是否含有 Strategy 声明的 consumes_config_attrs 全部 key;缺失 raise MissingConfigAttr(含字段名)
  • output_form="distribution" 的 Strategy,工厂构造期把 minimal_count 写入实例属性 strategy.minimal_count:若 attrsminimal_count 则取该值,否则回落到类属性缺省值(@tbl-spec-route-13 声明的整数)。规则与 .claude/rules/config-loading.md 一致——"类属性缺省"是 Strategy 类型自身的语义声明(与 dataclass 字段缺省同质),不是配置层 fallback;attrs 显式提供时以 attrs 为准

ForwardingPolicy(Layer 2 / Rust 侧)

G5 仿真器内 trait,承接 Layer 1 计算出的 PathSet,做仿真期每跳决策。

Trait 接口(伪代码,Rust-style 仅为可读性;具体类型 / 错误处理在 plan/code 阶段细化):

trait ForwardingPolicy {
fn next_hop(
ctx: RoutingContext, // src_chip, dst_chip, current_node
flow: FlowMeta, // flow_id, packet_id, sim_time_ns
state: &dyn NetworkState, // 仿真期可观测的网络状态
) -> Option<NodeId>;
}

返回 "NodeId 或 None"(None 表示在当前 PathSet 中无可行下一跳)。

字段语义

入参含义
ctx.src_chip / dst_chip通信端点的 chip 标识(u32)
ctx.current_node当前所处节点的 NodeId(chip 或 switch)
flow.flow_id流标识,static multipath 哈希维度(如 ECMP)
flow.packet_id报文标识,per-packet 算法(如 Packet Spraying)的决策维度
flow.sim_time_ns当前仿真时刻(adaptive 算法可用于时间窗口判断)
state只读 NetworkState 引用,adaptive 类 policy 通过它读队列深度 / 链路负载

返回 None 表示 (ctx, flow) 在当前 PathSet 中无可行下一跳(src 与 dst 不可达 / 路径耗尽)。

@tbl-spec-route-19 ForwardingPolicy.next_hop:入参,含义

内置 6 类 Policy(与 decision_frequency 组合):

Policy服务的 (decision_frequency, output_form)关键逻辑
StaticTablePolicy(static, single)启动期一次性把 PathSet 折叠为查找表,per-hop 仅查表
EcmpHashPolicy(static, multi)hash(flow_id) % K 选 K 条等代价路径之一;首跳决策、后续 hop 沿用
FirstHopAdaptivePolicy(first_hop, distribution)首跳期查 NetworkState(如 minimal vs valiant 队列深度比较);缓存 (flow_id) → path_index;后续 hop 及该 flow 后续 packet 全部沿用首跳决策(L2-C1)
PerFlowletAdaptivePolicy(first_hop, multi)flowlet 边界(now - last_pkt_ts > flowlet_gap_ns)触发新一轮选路;缓存 (flow_id, flowlet_id) → path_index;同一 flowlet 内严格粘滞(L2-C7);选路方式 hash((flow_id, flowlet_id)) % K(LetFlow style,不查 NetworkState)
PerHopAdaptivePolicy(per_hop, single | distribution)每跳查 NetworkState 选下一跳;不缓存
PacketSprayPolicy(per_packet, multi | distribution)按 packet_id 决策(伪随机 / 加权采样 / 拥塞感知散布)

@tbl-spec-route-20 ForwardingPolicy 内置类型

:source-routed 算法(SRv6 等)路径在报文头部携带,等价于 StaticTablePolicy 把路径"内联"到 packet。

NetworkState(Layer 2 / Rust 侧)

只读 trait,G5 仿真器实现并向 ForwardingPolicy 暴露的运行时网络状态。adaptive 类算法通过它消费仿真期实时观测量。

Trait 接口(伪代码,Rust-style 仅为可读性;具体类型 / 错误处理在 plan/code 阶段细化):

trait NetworkState {
fn link_busy_until_ns(src: NodeId, dst: NodeId) -> f64;
fn link_load(src: NodeId, dst: NodeId) -> f64;
fn switch_queue_depth(node: NodeId, out_port: NodeId) -> u32;
fn flow_count_on_link(src: NodeId, dst: NodeId) -> u32;
}

字段单位与语义契约

字段单位 / 类型语义最低精度要求实现选择
link_busy_until_ns绝对仿真时刻 (ns)该链路当前队列结束时刻;当 < 当前仿真时刻表示链路空闲必须精确(直接来自 PhyLink.busy_until_ns)无可选实现
link_load归一化 [0.0, 1.0]0 = 空闲、1.0 = 已饱和必须返回有信号的非常量值:禁止常返 0 或常返固定值。允许实现为"过去 W ns 链路忙时间占比"(滑动平均)或"瞬时 = 当前是否在传输";选定方案必须在 §实现备注 声明窗口长度 W 或瞬时定义滑动平均(推荐 W=link_latency_us × 10)/ 瞬时 / 累计 EWMA 三选一
switch_queue_depth字节数指定 switch 节点的指定出端口当前排队字节数;与 UGAL queue-aware 公式对齐必须精确(来自 switch 内 VOQ 状态)无可选实现
flow_count_on_link流数链路上当前 active flow 数;用于多流竞争因子估算必须维护 active flow 计数器,禁止常返 0。允许精确计数(HashMap<flow_id, refcount>)或哈希估计(Count-Min Sketch);估计方式与误差范围必须在 §实现备注 声明精确 / Count-Min Sketch / HyperLogLog 三选一

@tbl-spec-route-21 NetworkState 观测字段

精度违规判定:返回常量(特别是 0)即视为违反"最低精度要求"。理由:adaptive 算法的决策质量直接取决于 state 信号强度;常返 0 让 UGAL queue-aware 模式退化为纯 cost-based、PacketSpray 拥塞感知模式退化为均匀散布——算法表现上可能"看起来正常"但语义已完全失效,调试时极难定位。

核心约束

  • 只读:Policy 不能修改 state,只观测
  • 按需声明RoutingStrategy.requires_runtime_state 列举该算法消费的 state 字段名,G5 加载期校验仿真器已实现这些字段;未声明的字段算法不得查询(防止隐式依赖)
  • 粒度可换:state 各字段可有精确实现 / 近似实现(如移动平均),由仿真器选择

PolicyDispatcher(Layer 2 / Rust 侧)

trait 层无法 enforce "Policy 只能查询 requires_runtime_state 声明的 NetworkState 字段"(L2-C2)——Rust trait object 无法在方法分发前感知"调用方声明了什么"。PolicyDispatcher 在 Policy 与 NetworkState 之间插入一层包装,承载该约束。

Trait 接口(伪代码,Rust-style 仅为可读性):

struct PolicyDispatcher {
policy: Box<dyn ForwardingPolicy>,
declared_state_fields: HashSet<String>, // 来自 Strategy.requires_runtime_state
}

impl PolicyDispatcher {
fn next_hop(
ctx: RoutingContext,
flow: FlowMeta,
state: &dyn NetworkState,
) -> Option<NodeId> {
let guarded = GuardedState::new(state, &self.declared_state_fields);
let result = self.policy.next_hop(ctx, flow, &guarded);
// guarded 在 drop 时报告 query 集合;若 query ⊄ declared → raise UndeclaredStateAccess
result
}
}

GuardedState 是 NetworkState 的包装实现,每个 trait 方法被调用时记录字段名;若调用未在 declared_state_fields 内的字段 → 立即 raise UndeclaredStateAccess(fail-fast,不等 drop)。

内部可变性约束:NetworkState trait 方法签名是 &self,但 GuardedState 需在调用过程中累积 query 集合并触发 fail-fast 校验——这要求内部可变性(Rust RefCell / Cell / AtomicUsize 等)。GuardedState 实现依赖 L2-C5 的单线程假设:仿真期不会有多个线程同时通过同一 GuardedState 查 state,因此非线程安全的 RefCell 是合法选择。若未来多线程化仿真器,GuardedState 需切换到原子计数或线程局部存储,与 L2-C5 一同 bump 升级。

职责边界

包含不包含
校验字段访问合法性(L2-C2 enforce)不修改 Policy 行为;不改 state 内容
记录访问统计(可选 debug 模式)不参与算法决策
错误信息含字段名 + Policy 类型不感知具体算法语义

性能开销:每次 NetworkState 方法调用增加一次 HashSet lookup。生产模式下可通过条件编译(feature flag)跳过审计,仅在调试 / CI 模式启用。

Strategy ↔ Policy 映射

Layer 1 与 Layer 2 在 G5 加载期通过 decision_frequency 类属性自动匹配:

RoutingStrategy.decision_frequencyoutput_form选定的 ForwardingPolicy
staticsingleStaticTablePolicy
staticmultiEcmpHashPolicy
first_hopdistributionFirstHopAdaptivePolicy
first_hopmultiPerFlowletAdaptivePolicy
per_hopsingle / distributionPerHopAdaptivePolicy
per_packetmulti / distributionPacketSprayPolicy

@tbl-spec-route-22 Strategy ↔ Policy 映射

非法组合(如 static + distribution)在 G5 加载期 raise IncompatibleStrategyError

first_hopmulti vs distribution 的语义区分

  • (first_hop, distribution) → FirstHopAdaptivePolicy:PathSet 前 N 条为 minimal、其余 valiant;Policy 查 NetworkState 在 minimal/valiant 间做有偏选择(UGAL 语义);同一 flow 后续 packet 全部粘滞(L2-C1)
  • (first_hop, multi) → PerFlowletAdaptivePolicy:PathSet K 条等代价无主次;Policy 按 flowlet 边界(sim_time gap)切分 flow 为多个 flowlet,每 flowlet 内粘滞、flowlet 间允许换路径(L2-C7)

两者通过 output_form 严格区分,避免歧义。

同 Policy 不同算法的区分:多个 Strategy 可归同一 (decision_frequency, output_form) 组合复用同一 Policy,差异由 consumes_edge_attrs / consumes_config_attrs 反映。例如 static + single → StaticTablePolicy 同时被 ShortestPath / DOR / D-mod-k 复用:ShortestPath 不读 edge attrs(族无关);DOR 读 {dimension, wraparound}(Torus 维度);D-mod-k 读 {level, port_idx} + {w_levels, p_levels}(fat-tree 层级与端口)。同一 Policy 处理已计算好的 PathSet,算法语义差异在 Layer 1 Strategy 层级体现。

算法 / 公式

选路权重(路径选择)

边权重(shortest_path / ECMP 的 Dijkstra 权重,对齐 OSPF RFC 2328):

$$\begin{equation} w(e) = \frac{1}{\text{bandwidth\_gb\_per\_s}(e)} \label{eq:rt-edge-weight} \end{equation}$$

路径代价(Dijkstra 最小化目标):

$$\begin{equation} C_{\text{path}} = \sum_{e \in \mathcal{E}} w(e) \label{eq:rt-path-cost} \end{equation}$$
  • 语义:优先选择高带宽路径。两条 400 Gbps 边的代价 = 一条 200 Gbps 边的代价,自然平衡跳数与带宽
  • ECMP 的"等代价"定义:$C_{\text{path}}$ 相等的多条路径(在浮点容差 $\epsilon = 10^{-9}$ 内)
  • 交换机转发延迟不参与选路权重,仅参与路径延迟计算(下方公式 $\eqref{eq:rt-path-latency}$
  • 此权重仅适用于 shortest_path / ECMP;DOR 无权重(坐标确定性);UGAL 使用动态代价(队列深度 + 跳数)

路径指标(结果计算)

路径累计延迟(路径选出后的结果计算,所有算法通用):

$$\begin{equation} T_{\text{path}} = \sum_{e \in \mathcal{E}} \text{latency}(e) + \sum_{n \in \mathcal{N}_{\text{interior}}} \text{forwarding\_latency}(n) \label{eq:rt-path-latency} \end{equation}$$
  • $\mathcal{E}$:路径上的边序列(含 chip-to-chip 直连边和 switch-relay 边)
  • $\mathcal{N}_{\text{interior}}$:路径上除源、目以外的中间节点(仅 switch 贡献 forwarding_latency)

路径瓶颈带宽:

$$\begin{equation} B_{\text{bottleneck}} = \min_{e \in \mathcal{E}} \text{bandwidth}(e) \label{eq:rt-bottleneck-bw} \end{equation}$$

多路径聚合带宽(适用于 ECMP / KSP / Packet Spraying 等独立等代价路径):

$$\begin{equation} B_{\text{aggregate}} = u_{\text{routing}} \cdot \sum_{i=1}^{N} B_{\text{bottleneck}}^{(i)} \label{eq:rt-aggregate-bw} \end{equation}$$
  • $N$:等代价路径数
  • $u_{\text{routing}} \in (0, 1]$:路由侧带宽利用率折扣(YAML routing.bandwidth_utilization
  • 严格成立条件:$N$ 条路径独立、等代价、流量均匀分布
  • 非独立路径(共享瓶颈边)应通过 RoutingResult.edge_load 表达,不直接套用此公式

链路层级标识

边的 type 字段(c2c / b2b / r2r / p2p 或自定义)即链路的层级标识。调用方(如通信评估器)可从 RoutingResult 的路径边序列中读取 EdgeSpec.type 以区分链路层级,无需独立的"tier 分解"抽象

路由模块的输出止于 RoutingResult(G6 边界),链路层级的业务语义由调用方自行解释。例如:通信评估器可从路径的瓶颈边 type 判断通信走的是 c2c(板内)还是 r2r(跨 rack),据此选择对应的带宽参数。

约束与规则

正确性约束(违反 → 抛 RoutingError 子类,硬拒绝):

  • C1:路径必须连通;不可达时 compute 返回空路径的 RoutingResult,由调用方决定如何处理
  • C2bind_graph 期间,consumes_edge_attrs 中每个 key 必须在 graph 所有边的 attrs 上存在且类型与 edge attrs schema 一致 — 缺失或类型错 raise IncompatibleStrategyError
  • C3src == dst 视为零跳特殊情况(合法),返回零代价 RoutingResult(hop=0, latency=0, bw=∞)
  • C4:图非连通且 (src, dst) 跨连通分量 → bind_graph 期 raise DisconnectedGraphError
  • C5:request 中的 src 或 dst 不在 graph → compute raise NodeNotFound
  • C6create_routing_strategy 工厂期,consumes_config_attrs 中每个 key + 模块级 key(bandwidth_utilization)必须在 routing.attrs 中存在且类型与 config attrs schema 一致 — 缺失 raise MissingConfigAttr,类型错 raise InvalidConfigAttrType
  • C7(D-mod-k attrs 跨表一致性):bind_graph 期 D-mod-k Strategy 必须校验:(a) len(w_levels) == len(p_levels)(PGFT 两参数等长);(b) len(w_levels) == max(edge.level for edge in fat-tree边) + 1(attrs 列长度 = 树层数 h,与图中边层级取值范围对齐);(c) w_l, p_l ≥ 1 各层正整数。任一违反 raise IncompatibleStrategyError,错误消息含具体不一致字段。该约束保证 PGFT 公式 q_l(j) = floor(j/Π w_k) mod (w_{l+1}·p_{l+1}) 在所有层级上都有定义

可用性约束(违反 → warning,不阻塞):

  • W1:边 attrs 或 routing.attrs 含未在对应 schema 注册的 key → warning(疑似拼写错误),保留 key 不丢弃
  • W2:算法配置(如 max_paths=N)但实际可用路径 < N → warning,截断
  • W3routing.attrs 含已注册但当前 algorithm 不消费的 key(如 algorithm=shortest_path 但 attrs 含 max_paths)→ warning,该 key 不影响行为

边界条件

  • B1:自反请求 (src == dst) → 返回单元素零代价路径(同 C3)
  • B2:算法不兼容当前图(consumes 校验失败) → bind_graph 期 raise IncompatibleStrategyError,错误消息含缺失 key 名
  • B3:YAML 必须显式指定 routing.algorithm 字段;缺失 → 加载期 raise MissingAlgorithmField(错误消息含 YAML 文件路径)。模块不做任何默认推断或 fallback:算法选择是实验意图,必须由用户显式声明;推断会破坏对比实验、损害可重复性、引入图特征歧义
  • B4:YAML 显式指定的 algorithm 不在工厂注册中心 → create_routing_strategy raise UnknownRoutingAlgorithm
  • B5:attrs 值为 None 或缺省 → 视为该边未提供该属性,C2 校验生效

Layer 2(G5 仿真侧)约束

  • L2-C1FirstHopAdaptivePolicy 在首跳决策后必须缓存 (flow_id) → path_index,后续跳沿用首跳决策。这是 first-hop adaptive 算法的语义不变性,违反将导致同一流报文乱序到达

  • L2-C2:Policy 只能查询 RoutingStrategy.requires_runtime_state 已声明的 NetworkState 字段;查询未声明字段 → PolicyDispatcher 拦截并 raise UndeclaredStateAccess。该约束防止 adaptive 算法隐式依赖未声明状态,导致 spec 与实现漂移。承载层:见 §PolicyDispatcher 小节——trait 层本身无法 enforce,必须由 dispatcher 通过包装 NetworkState 实现

  • L2-C3:G5 加载期按 Strategy ↔ Policy 映射表匹配;非法组合(如 static + distribution)raise IncompatibleStrategyError

  • L2-C4PerHopAdaptivePolicy 不得缓存路径选择——其语义就是每跳重新决策;缓存即等价于退化为 FirstHopAdaptive

  • L2-C5NetworkState 实现必须保证 trait 方法的查询语义在单次 next_hop 调用内一致(不允许仿真器在 policy 查询过程中并发更新 state);G5 单线程仿真天然满足,多线程化时需引入快照

  • L2-C6(dispatcher 调用必经性):G5 仿真器对 Policy 的唯一调用入口是 PolicyDispatcher.next_hop;C2CNetwork / event handler / 任何调用方必须持有 PolicyDispatcher,不得持有裸 Box<dyn ForwardingPolicy>。该约束保证 L2-C2 字段访问保护无法被绕过——若调用方直接拿裸 Policy + 真实 NetworkState 调 next_hop,GuardedState 不被构造,未声明字段访问就不被拦截。架构层面通过"裸 Policy 不出现在 dispatcher 外部 API"实现:Policy 工厂只返回 PolicyDispatcher,不暴露内部 policy 字段

  • L2-C7(PerFlowletAdaptive flowlet 缓存语义):PerFlowletAdaptivePolicy 必须按 (flow_id, flowlet_id) 缓存 path_index,遵守以下三条规则:

    • L2-C7.a 初始化:flow 第一次出现时 flowlet_id = 0,按 hash((flow_id, 0)) mod K 选 path_index 并写入缓存
    • L2-C7.b 递增条件:后续 packet 到达时若 now - last_pkt_ts > flowlet_gap_ns,则 flowlet_id += 1 并按新 hash key 重新选路;否则沿用当前 (flow_id, flowlet_id) 缓存的 path_index
    • L2-C7.c 单调性flowlet_id 仅递增、不回退、不在 flow 生命周期内重置;同一 (flow_id, flowlet_id) 内任意后续 packet 与中间跳必须严格粘滞同一 path_index

    与 L2-C1 的区分:L2-C1 是 "FirstHopAdaptive 全 flow 单一路径"(同 flow_id 永不换),L2-C7 是 "PerFlowletAdaptive flowlet 内粘滞、flowlet 间可换"。两者并存,按 Policy 类型分别 enforce。违反 L2-C7.c(同 flowlet 内换路径)会导致同 flowlet 内 packet 乱序到达(与 LetFlow 论文的 flowlet gap 无乱序条件 $\tau_{\text{gap}} \ge d_{\max} - d_{\min}$ 一致)。

集成点

边界输入输出
模块入口YAML 配置 + 节点 ID 集合NetworkGraph 实例 + bind 完成的 RoutingStrategy 实例
模块出口(Layer 1,Python)RoutingRequestRoutingResult
G5 加载期出口(Layer 1 → Layer 2 桥接)RoutingStrategy + NetworkGraph(a) 全对 PathSet(dict[(src,dst), list[Path]])+ (b) ForwardingPolicy 实例(按 decision_frequency 选定)+ (c) 算法附加参数(如 UGAL bias / minimal_count)
仿真期出口(Layer 2,Rust)RoutingContext + FlowMeta + NetworkState下一跳 NodeId

@tbl-spec-route-23 集成点:边界,输入

Layer 1 ↔ Layer 2 数据桥接契约

  • G5 加载期遍历所有 (src, dst) 对,调 RoutingStrategy.compute(RoutingRequest) 拿 RoutingResult,提取 paths() 转为 PathSet(节点序列 + 边序列)传给 Rust
  • decision_frequency + output_form 组合决定 Rust 端实例化哪种 ForwardingPolicy;requires_runtime_state 用于校验仿真器已暴露对应 NetworkState 字段,并传给 PolicyDispatcher 作为字段访问白名单
  • 算法附加参数(如 UGAL bias / minimal_count)通过 RoutingStrategy 的额外属性暴露,桥接层按 §Policy Descriptor Schema 表打包

Policy Descriptor Schema

桥接层(Python 侧)按 (decision_frequency, output_form) 组合显式产出 descriptor 字典,Rust 侧根据 type 字段实例化对应 Policy。禁止启发式推断(如根据"是否有 multi-path"或"是否传 ugal_config"猜测 Policy 类型)——启发式无法覆盖 5 类完整组合,且与 L2-C3 "非法组合 raise" 约束冲突。

(decision_frequency, output_form)descriptor 字典 schema必填字段含义
(static, single){"type": "static_table"}无附加字段;Policy 从 PathSet 单路径建查找表
(static, multi){"type": "ecmp_hash", "max_paths": int}max_paths:K 条等代价路径上限,与 Strategy consumes_config_attrs.max_paths 一致
(first_hop, distribution){"type": "first_hop_adaptive", "minimal_count": int ≥ 1, "bias": float}minimal_count:PathSet 前 N 条为 minimal;bias:minimal vs valiant 决策偏置(参见 UGAL 算法定义)
(first_hop, multi){"type": "per_flowlet_adaptive", "flowlet_gap_ns": float > 0}flowlet_gap_ns:flowlet 边界阈值(仿真时刻差);Policy 在 PathSet K 条等代价路径间按 hash((flow_id, flowlet_id)) mod K 选路,K = PathSet 长度(runtime 推断,无需 descriptor 显式传递);flowlet_id 由 sim_time gap 单调递增(L2-C7)
(per_hop, single | distribution){"type": "per_hop_adaptive"}无附加字段;Policy 每跳查 state 重选下一跳。single 形态:Policy 持当前节点邻接表,每跳对所有出向邻居逐一查 state cost 后选最优(不依赖 PathSet 候选);distribution 形态:Policy 在 PathSet 候选路径的下一跳集合内做 state-driven 决策,等价于"被 Layer 1 约束在 minimal/valiant 候选内的 per-hop 选择"
(per_packet, multi | distribution){"type": "packet_spray", "congestion_aware": bool}congestion_aware:true 时对每个候选路径查首跳 link_load,选最低者(argmin);false 时按 hash(packet_id) mod N 散布。两种模式都不在多包间缓存(per_packet 语义)

@tbl-spec-route-23b Policy Descriptor Schema

Schema 维护规则

  • 新增 Policy 类型必须扩展本表一行 + bump spec minor 版本
  • descriptor 字段值的合法性由 Rust 侧 parse_topology.rs 在加载期校验;缺失必填字段 raise MissingPolicyDescriptorField;type 不在表内 raise UnknownPolicyType
  • 任何不在本表内的 (decision_frequency, output_form) 组合均为非法(L2-C3),桥接层 raise IncompatibleStrategyError

模块内部不感知下游消费方对 RoutingResult 的具体使用(通信延迟模型、争用建模、Math 评估器公式选择 不在本 spec 范围);但 G5 仿真侧 Layer 2 契约在本 spec 范围内。

YAML schema

network:
link_types: # 链路规格表
<type_name>:
bandwidth_gb_per_s: <float>
latency_us: <float>

links: # 边列表(写法 3:物理字段平级 + attrs 集中)
- from: <NodeId | list[NodeId]>
to: <NodeId | list[NodeId]>
type: <link_type_name>
attrs: # 可选;key 必须在 attrs schema 内
<attr_key>: <attr_value>

switches: # 交换机列表(可选)
- id: <NodeId>
type: <"tor" | "spine" | "core">
port_count: <int>
forwarding_latency_ns: <int>
...

routing:
algorithm: <string> # 必填,枚举:shortest_path | ecmp | dor | ugal | ...(已注册算法名)
attrs: # 必填字典;模块级 key + 当前 algorithm 的 consumes_config_attrs 全部必须存在
bandwidth_utilization: <float> # 模块级(所有算法共用),公式 \eqref{eq:rt-aggregate-bw} 使用
# 算法级 key 由 algorithm 决定,例如:
# algorithm=dor 时:dimension_order, dateline_vc
# algorithm=ecmp 时:max_paths
# algorithm=ugal 时:bias, valiant_k, minimal_count
# algorithm=packet_spray 时:max_paths, congestion_aware
# algorithm=dmodk 时:w_levels, p_levels (PGFT 通用;k-ary n-tree 写 [k, k, ...] + [1, 1, ...])
# algorithm=flowlet_ar 时:flowlet_gap_ns (业界 IB AR 典型 50000-500000 即 50-500 μs)

字段透传规则:模块加载期把 routing.attrs 段作为 dict 直接透传给 create_routing_strategy(name, attrs)不做任何代码层默认值填充(遵守 .claude/rules/config-loading.md)。Strategy 在工厂构造期校验消费 key 的存在性与类型;缺失 raise MissingConfigAttr,类型错 raise InvalidConfigAttrType

_template.yaml 与本 schema 同步,按 .claude/rules/sync-templates.md 维护。

引用

外部:

项目内:

备选方案

元数据载体形式

维度方案 A:散落具名字段方案 B:单 dict 无约束方案 C(选定):dict + Strategy 声明方案 D:Tagged 子类方案 E:图算法反推
schema 稳定性差(每加算法改类)差(族子类爆炸)好(图模型不变)
类型安全强(dataclass 字段)弱(Any)中(注册期校验)最强
扩展成本高(改 frozen dataclass)极低低(schema 加一行)高(实现复杂)
错误前置编译期运行期沉默加载期 raise编译期运行期
"杂物间"风险低(声明约束)
YAML 友好中(字段堆叠视觉乱)最好(YAML 极简)
与现有 YAML 哲学对齐破坏(族字段污染节点)对齐对齐(沿用 attrs 风格)严重破坏完全对齐
13 族 × 多算法适配字段爆炸灵活灵活类爆炸反推歧义

@tbl-spec-route-24 元数据载体形式

选择理由:方案 C 兼具方案 B 的扩展灵活性与方案 A 的错误前置;声明机制把"杂物间"负面效应控制住;与项目通用 YAML 描述哲学对齐;不破坏 graph 族无关性。方案 E 看似最纯净,但 Slimfly / Dragonfly+ / 部分 HyperX 变体的图特征反推歧义难解决,且实现复杂度抵消了简洁性收益。

路由配置 YAML 组织

维度方案 α:顶层散字段 + 算法子段嵌套方案 β(选定):routing.attrs 集中字典
形式routing.bandwidth_utilization + routing.<algorithm>.dimension_orderrouting.attrs.bandwidth_utilization + routing.attrs.dimension_order
与 edge.attrs 对称性否(边用 attrs,路由不用)是(双层都用 attrs)
校验机制复用否(顶层散字段需独立处理;算法子段需算法名命中)是(双层共用 schema + Strategy 声明 consumes 模式)
杂物间防护弱(顶层任意字段难校验)强(schema 集中维护)
算法切换时配置位置routing.<old_algo>routing.<new_algo> 段名algorithm 字段,attrs 内容微调
默认值合规性顶层 bandwidth_utilization 易诱导写"选填默认 0.90",违反 config-loadingattrs 整体必填,零默认值天然合规

@tbl-spec-route-25 路由配置 YAML 组织

选择理由:方案 β 与边 attrs 设计完全对称,校验机制(schema + consumes)共用一套实现;天然遵守 .claude/rules/config-loading.md(无字段适合"选填默认"陷阱);新增算法时不需要在 YAML 引入新嵌套子段(attrs 字典扁平加 key 即可)。

Strategy 接口形式

维度方案 a(选定):单一 compute → RoutingResult方案 b:三个接口(SinglePath / MultiPath / Distribution Protocol)
调用方便利性一个接口isinstance 分支
类型表达力output_form 作为元数据Protocol 类型直接区分
新形态扩展成本低(RoutingResult 内部状态)高(新增 Protocol)
13 族 × 多形态简单类型组合膨胀
调用方代码复杂度含分支

@tbl-spec-route-26 Strategy 接口形式

选择理由:调用方(G5 仿真器)的契约是 RoutingResult,不应感知算法形态;统一接口避免每次新增形态都改调用方;output_form 作为声明性元数据已足够告知差异。

族信息载体宏观选型

维度方案 i:TopologyDescriptor方案 ii(选定):边 attrs方案 iii:算法图反推
族抽象渗透高(族成接口一等公民)无(图族无关)
YAML 表达需 family_kind + family_params沿用 links 风格YAML 完全通用
算法实现成本中(每族一 Descriptor 类)低(读 attrs key)高(启发反推有歧义)
与现有 YAML 设计对齐引入新概念沿用 attrs 习惯完全对齐
通用算法成本需理解 GenericDescriptor0(不读 attrs)0

@tbl-spec-route-27 族信息载体宏观选型

选择理由:项目 YAML 已是族无关通用图描述,引入 Descriptor 等于把族概念找回,违反原设计哲学。算法图反推存在歧义且实现复杂。边 attrs 是最低侵入的设计 — 利用 YAML 既有"边贴标签"风格的自然延展。

路由分层架构(v1.2.0 新增)

是否把 Layer 2(per-hop 决策)做成独立抽象?语言放置策略对比 5 个方案:

维度方案 α:单层 Python(Strategy 含每跳)方案 δ:单层全 Rust(连 Strategy 也用 Rust 实现)方案 ε:双层但 Layer 2 也 Python(per-hop 走 PyO3 callback)方案 β(选定):双层 Python + Rust方案 γ:三层(再加 Selector 管 multi-path)
Python ↔ Rust 跨语言开销高(每跳回调)0高(每跳 PyO3 callback)低(启动期一次桥接,运行期 Rust 自洽)低(同 β)
静态算法开销000
Adaptive 算法扩展改 Strategy 接口改 Rust,Python 端不可见加一种 Python policy加一种 Rust Policy加 Policy + Selector
Math 评估器复用接口臃肿失去(Math 在 Python,不能直接调 Rust 算法)直接复用Layer 1 直接复用Layer 1 直接复用
算法迭代速度中(Python 灵活)慢(Rust 改动笨重)中(Python 改 Strategy 快 / 新 Policy 改 Rust 慢)
抽象数11223

@tbl-spec-route-28 路由分层架构选型

选择理由(β)

  • 双层把"全局视图"(Layer 1)和"运行时视图"(Layer 2)分到不同生命周期,复杂度对症
  • per-hop 决策完全在 Rust 内,per-packet 仿真性能不受 Python 影响
  • Layer 1 保留在 Python 让算法迭代快、与 Math 评估器复用零代价(否决 δ)
  • Layer 2 必须在 Rust 才能保证 per-hop 零跨语言开销(否决 α、ε)
  • 5 类内置 Policy 已覆盖业界主流算法模式(static / multipath / first-hop adaptive / per-hop adaptive / packet spray),新算法多数可复用现有 Policy
  • 方案 γ 多出的 Selector 层在 ECMP 选路只是 5 行 Rust 代码,独立成层不值得

D-mod-k 参数表达形式(v1.3.1 新增)

维度方案 a:k: int 简写方案 b(选定):w_levels + p_levels PGFT 通用方案 c:两者并存(别名)
表达能力仅标准 k-ary n-treePGFT / RLFT / slimmed fat-tree 通用通用
用户书写量1 字段2 列表([k,k,k] + [1,1,1])1 字段或 2 列表
Strategy 实现复杂度简单简单(公式直接消费)中(attrs 校验分支:k 在则推导,否则读 list)
Schema 校验简单(int)简单(list 长度 + 元素正数)复杂(一致性互斥校验)
Zahavi CCIT #776 公式对齐k-ary 特例PGFT 通用形式(公式 (3))直接消费通用形式经简写转换
与 §业界参考 D-mod-k 段公式一致是(间接)

@tbl-spec-route-32 D-mod-k 参数表达形式

选择理由(b):PGFT 通用形式直接对齐 Zahavi 原始公式,零额外转换;标准 k-ary n-tree 用户写 w_levels: [4, 4, 4] + p_levels: [1, 1, 1] 仅多 ~30 字符,代价可接受;引入 k 简写别名(方案 c)会让 attrs 校验出现互斥分支(k 与 w_levels/p_levels 不能同时给),与 spec §G4 "attrs 显式声明" 简单清晰原则冲突。

per-flowlet AR 决策模型选型(v1.3.2 新增)

per-flowlet AR 有两个独立维度:选路触发条件(flowlet 边界判定)和选路决策方式(hash vs 拥塞感知)。前者业界基本一致(now - last_pkt_ts > flowlet_gap_ns),后者三种主流方案对比:

维度方案 A1:纯 hash 切换(LetFlow style)方案 A2:拥塞感知 argmin(IB AR / CONGA style)方案 A3:双 mode 配置项
选路方式hash((flow_id, flowlet_id)) mod Kflowlet 边界查 link_load 选 argmin(或带内拥塞反馈)由 attrs flowlet_congestion_aware: bool 决定
requires_runtime_stateset()(不查 NetworkState){"link_load"} 或更多字段两种均支持,按 mode 分支
Layer 2 复杂度低(与 EcmpHash 同复杂度,仅 cache key 多一维)中(增加 state-driven argmin 逻辑)高(两条路径都需实现 + 测试覆盖)
仿真可复现性高(hash 纯函数)低(依赖仿真期 state 演化)
业界对应LetFlow (NSDI 2017)NVIDIA Quantum InfiniBand AR / CONGA (SIGCOMM 2014)
spec patch 范围适配✓ 既有 first_hop 类目下加变体✗ 触发 NetworkState 精度调优、加大 PolicyDispatcher 测试矩阵✗ 引入新 attrs schema 字段 + 多 mode 行为契约
用户配置门槛低(仅配 flowlet_gap_ns)高(必须确保仿真器 link_load 精度满足 spec @tbl-21 要求)

@tbl-spec-route-33 per-flowlet AR 决策模型选型

选择理由(A1)

  • 与 spec v1.3.2 patch 范围最匹配——既有 first_hop decision_frequency 类目下加 LetFlow style 变体,不引入新 NetworkState 字段消费、不改 PolicyDispatcher 校验逻辑
  • 仿真可复现性是测试可靠性的硬要求;A2 的拥塞感知依赖仿真期 state 演化轨迹,相同 YAML 不同跑可能给出不同 path_index 分布,与 G5 既有"同 YAML 同结果"契约有张力
  • LetFlow 论文(Vanini NSDI 2017)证明:在等价多路径拓扑下,纯 hash 切换在 flowlet 粒度上已能达到接近 CONGA 的负载均衡效果,复杂度远低
  • A2 / A3 留作未来方向(见 §未来方向 "拥塞感知 flowlet AR"行)——CONGA / NVIDIA Quantum IB AR 形态在 future 通过扩展 requires_runtime_state 实现,不必先期引入

A2 否决理由:本 spec patch 不应增加 NetworkState 字段消费契约(与既有 v1.2.1 link_load 精度承诺联动),范围超出 v1.3.2 patch 边界。

A3 否决理由:引入第 2 个布尔开关(同 PacketSpray congestion_aware)让 schema 与测试矩阵翻倍;当 A1 与 A2 的语义差异显著(state 消费 / 仿真可复现性),应独立成 Policy 类而非合并为同 Policy 的两 mode。

风险与限制

风险影响缓解措施
attrs 字典缺乏强类型(dict[str, Any])拼写或类型错误可能未被静态工具发现Strategy 显式声明 + 加载期 schema 校验(约束 C2、C6、可用性 W1)把错误提前到 bind_graph 或工厂构造期
attrs schema 分两层(edge + config)文档与代码维护成本翻倍单一 schema 模块(如 attrs_schema.py)同时定义两份;模板文件按 .claude/rules/sync-templates.md 同步
YAML 必填字段过多导致用户写起来啰嗦每个拓扑预设都要列出 attrs 段拓扑生成器为常用族产出完整 YAML(含 attrs);用户手写 YAML 时按 schema 表照填
多算法并存时 attrs schema 膨胀schema 表变长schema 是静态注册,加载期校验复杂度 $O(\|edges\| \times \|attrs\|)$,线性可接受;超大规模可优化为 set 查找
闭式算法 vs 全表存储的实现差异不同 Strategy 内部存储形式不一RoutingResult 接口屏蔽差异;调用方面对统一查询接口(G6 模块边界保证)
edge_load 的语义随 output_form 变化调用方需理解三种语义RoutingResult 在文档中明确每种 output_form 下 edge_load 的含义;提供 helper(如 result.is_distribution()
YAML 写法 3(attrs 嵌套)增加用户书写量简单边写起来啰嗦attrs 字段可省略;通用算法不需要任何 attrs,YAML 与现状一致
Layer 1 / Layer 2 双向漂移(spec 跨语言后,Python 改 Strategy 接口、Rust 改 Policy trait 可能不同步)算法语义不一致接口契约写在本 spec;Layer 1 / Layer 2 任何接口变更都需 bump spec 版本 + 同步审核两侧实现
adaptive 算法的运行时状态依赖Policy 隐式查询未声明状态字段 → 难调试PolicyDispatcher 包装层强制 L2-C2 约束:Policy 只查 requires_runtime_state 声明的字段,未声明字段查询由 GuardedState 拦截并 raise UndeclaredStateAccess(见 §PolicyDispatcher 小节)
Policy 类型覆盖不够新算法需要新 Policy 类(如 in-network aggregation routing)v1.2.0 内置 5 类 + v1.3.2 加 PerFlowletAdaptivePolicy → 6 类;后续新增 Policy 类需 bump spec 版本 + 更新 @tbl-22 映射表 + 更新 @tbl-23b descriptor schema
PerFlowletAdaptive 乱序风险flowlet 边界换路径后,若 flowlet_gap_ns < d_max - d_min(候选路径端到端延迟差),同 flow 跨 flowlet 仍可能乱序——LetFlow 论文给出的无乱序条件是 $\tau_{\text{gap}} \ge d_{\max} - d_{\min}$(详见 04-自适应路由.md §Flowlet Gap 的无乱序条件 公式 eq:route-ar-flowlet-gapspec 不在仿真侧校验 flowlet_gap_ns vs 路径延迟差关系——用户配置责任(业界 IB AR 由 Subnet Manager 经验估配);NG8 已声明不建模报文乱序 / 重排序 → 仿真结果不反映乱序代价
PathSet 在 distribution 形态的 minimal/valiant 分段约定可能歧义UGAL 变体(如 PAR)的 minimal 集合定义不同RoutingStrategy.minimal_count 类属性强制声明;非 distribution 算法无此属性
Packet Spraying 报文乱序对上层协议影响仿真结果可能高估 Packet Spraying 性能(实际重排序缓冲会引入延迟,对小消息更敏感)仿真侧仅测吞吐 / 链路利用率指标,不预测端到端 RDMA 延迟;NG8 已显式声明"不建模报文乱序 / 重排序",需要时另立 spec 处理
D-mod-k 邻居命名约束公式 port = floor(d/k^l) mod k 产出端口编号 $q$,若 Strategy 通过邻居名字典序反推 $q$,则拓扑生成器命名规律改变会让 D-mod-k 失去无阻塞性质(业界 OpenSM 走 LID 等价物,本模块图抽象无 LID 概念)EdgeSpec.attrs 新增 port_idx: int 字段显式标注每条边的物理端口编号,Strategy 直接读 attrs 找邻居。port_idx 替代 LID 的范围:OpenSM LID 同时承载"节点全局标识"+"端口编号"双重职能;本模块 NodeId 已覆盖节点标识,port_idx 仅替代 LID 的"端口编号"维度(即 "src 节点选 q 号端口对应哪条出边" 的映射),不引入全局编号空间。拓扑生成器在 fat-tree edge 输出时必须标注 port_idx(属 plan 范围)

@tbl-spec-route-29 风险与缓解措施

已知限制(设计选择的代价):

  • L1模块不感知运行时网络状态(v1.2.0 已通过 Layer 2 NetworkState 接口解决)
  • L2:单图绑定假设(一个 Strategy 实例对应一个 graph 绑定);多 graph 跨域路由需多实例
  • L3:attrs schema 是模块内中央维护,跨项目复用时需协调命名(如不同项目都用 dimension 但语义不一致)
  • L4:模块只产出"路径 + 路径属性",不产出"流量到路径的分配概率超出 RoutingResult 形态范围"(如 SDN 全局规划留待未来)

验收标准

场景指标目标值测试方法
通用算法在族无关图上工作shortest_path / ECMP 在示例 YAML 上均能产出非空 RoutingResult100% 覆盖端到端单测
选路权重为带宽倒数异构带宽图中,shortest_path 选出的路径优先走高带宽链路(而非低延迟路径)100%单测:构造 c2c(400Gbps,0.2us) + r2r(200Gbps,2us) 异构图,验证选路结果偏向 c2c
选路权重与延迟解耦路径延迟仍按 $\eqref{eq:rt-path-latency}$ 计算(含 switch 转发延迟),不受选路权重影响路径延迟 = 边延迟之和 + switch 转发延迟之和单测对比手算
族特定算法属性校验前置DOR 在缺 dimension 属性的图上 bind_graph 期 raiseraise IncompatibleStrategyError单测
attrs schema 防杂物间YAML 含未注册 key(如 dim 而非 dimension) → 加载期 warning 含 key 名warning 命中单测
算法注册扩展性新增算法仅需实现 Strategy 子类、注册到工厂、可选扩展 schema< 100 行新增;0 行修改既有 Strategy 代码code review
RoutingResult 三种形态统一接口调用方代码消费 single / multi / distribution 三种结果时无 isinstance 分支0 处 isinstance静态扫描
实验意图显式YAML 缺 routing.algorithm 字段时加载期 raise(不 fallback)100%单测:构造缺字段 YAML → raise MissingAlgorithmField
同拓扑切算法同一份 graph YAML 切换 algorithm 字段对应不同 Strategy 实例algorithm 是唯一影响算法的输入单测:同 YAML 用三种 algorithm 加载,得到三种 Strategy
配置加载零默认值(合规 config-loading.md代码中无任何配置参数的默认值(不含 dataclass 零值)grep dict.get(.*,.*) / getattr(.*,.*,.*) 在路由模块代码中无命中静态扫描 + 单测:缺任意 attrs key 时 raise MissingConfigAttr
attrs 必填校验YAML 缺 routing.attrs 段或 attrs 缺被消费 key 时加载期 raise100%单测:分别构造缺 attrs 段、缺模块级 key、缺算法级 key 三种 YAML → raise
公式 \eqref{eq:rt-aggregate-bw} 严格成立条件多路径独立、等代价时聚合带宽误差 < 1%1%单测对比手算
模块边界(G6)RoutingResult 不依赖通信延迟模型 / 争用模型代码0 import 反向依赖静态扫描 import
Strategy 双类属性声明(v1.2.0)所有 Strategy 子类必须声明 decision_frequency + requires_runtime_state100% 子类符合grep 检查;缺失声明 → 工厂期 raise
Strategy ↔ Policy 映射正确(v1.2.0)5 类 decision_frequency 都能匹配到对应 ForwardingPolicy;非法组合 raise100% 覆盖单测:每种组合都跑一遍
静态算法零 NetworkState 开销(v1.2.0)StaticTablePolicy / EcmpHashPolicy 的 next_hop 不查 NetworkState 任何字段0 次 state 查询单测:mock NetworkState 计数查询
FirstHopAdaptive 路径缓存(v1.2.0)同一 flow_id 在后续 hop 必须复用首跳决策100% 一致单测:构造 UGAL 多跳路径 + 故意改变 NetworkState,验证 path_index 不变
PerHopAdaptive 无缓存(v1.2.0)同一 flow 不同 hop 的 NetworkState 不同时,决策应不同100% 响应变化单测:构造队列变化场景,验证 next_hop 输出变化
NetworkState 未声明字段保护(v1.2.0)Policy 查询未在 requires_runtime_state 声明的字段 → raiseraise UndeclaredStateAccess单测:恶意 Policy 查询未声明字段
PathSet distribution 分段约定(v1.2.0)输出 distribution 的 Strategy 必须额外声明 minimal_count;前 N 条路径为 minimal100% 声明覆盖单测:UGAL 构造 distribution 结果,验证前 N 条为 minimal
Policy Descriptor Schema 严格产出(v1.2.1)桥接层按 §Policy Descriptor Schema 表的 5 行精确映射产出 descriptor 字典;禁止启发式推断5 类组合各有 descriptor 单测;非法组合 raise;缺字段 raise单测:(1) 5 种合法组合断言 descriptor 字段集合精确匹配;(2) 模拟用户写 decision_frequency=static + output_form=distribution 应 raise IncompatibleStrategyError;(3) UGAL strategy 缺 bias attrs 应在 Strategy 构造期 raise MissingConfigAttr(不到 descriptor 阶段)
PolicyDispatcher 字段访问保护(v1.2.1,承载 L2-C2)Policy 查询未在 requires_runtime_state 声明的 NetworkState 字段 → dispatcher 拦截 raise UndeclaredStateAccess100% 拦截命中单测:构造 mock Policy 查询未声明字段(如 UGAL 的 requires={link_busy_until_ns, switch_queue_depth},让 mock 查 link_load)→ assert raise
PacketSpray congestion_aware schema 校验(v1.3.0)YAML 含 algorithm: packet_spray 但 attrs 缺 congestion_aware → 工厂期 raise MissingConfigAttr;attrs 含且类型为 bool 时按 §Policy Descriptor Schema 表 packet_spray 行透传100%两个单测:(1) 缺字段 → raise MissingConfigAttr 含字段名;(2) congestion_aware=true 时 descriptor 含 "congestion_aware": true
D-mod-k attrs schema + 跨表一致性 (C7) 校验(v1.3.1)YAML 含 algorithm: dmodk 但 attrs 缺 w_levelsp_levels → 工厂期 raise MissingConfigAttrlen(w_levels) != len(p_levels) 或与图中 max(edge.level)+1 不一致 → bind_graph 期 raise IncompatibleStrategyError,错误消息含具体不一致字段100%三个单测:(1) 缺 w_levels → raise MissingConfigAttr 含字段名;(2) w_levels=[4,4] + p_levels=[1,1,1] (长度不等) → raise IncompatibleStrategyError;(3) w_levels=[4,4,4] + graph 含 level=3 边 → raise IncompatibleStrategyError 含"max(level)+1 != len(w_levels)"
PerFlowletAdaptive flowlet 缓存语义 (L2-C7)(v1.3.2)(a) 同 (flow_id, flowlet_id) 多 packet 必须沿用同一 path_index;(b) now - last_pkt_ts > flowlet_gap_ns 时 flowlet_id+1 并触发新一轮 hash 选路(同 hash key 对不同 flowlet_id 给出不同 path_index 的概率 ≈ 1 - 1/K);(c) 同 (flow_id, flowlet_id) 中间跳全部沿用首跳决策100% 拦截命中三个单测:(1) 构造同 flow 连续 2 packet 时间差 < flowlet_gap_ns,assert path_index 相同;(2) 构造同 flow 两 packet 时间差 > flowlet_gap_ns,assert flowlet_id 单调 +1 且 cache key 必须区分(不强求 path_index 必然变化);(3) 构造单 packet 多跳路径,assert (flow_id, 0) 缓存生成 + 该 packet 沿路径所有中间跳的 next_hop 决策都使用同一 path_index
PerFlowletAdaptive flowlet_gap_ns schema 校验(v1.3.2)YAML 含 algorithm: flowlet_ar 但 attrs 缺 flowlet_gap_ns → 工厂期 raise MissingConfigAttr;attrs 含但类型非 float 或 ≤ 0 → raise InvalidConfigAttrType 含字段名100%两个单测:(1) 缺字段 → raise MissingConfigAttr 含 "flowlet_gap_ns";(2) flowlet_gap_ns=-1.0 → raise InvalidConfigAttrType
PerFlowletAdaptive hash 均匀性 + key 区分(v1.3.2)构造 N=100 个不同 flow_id,每个 flow 跨 flowlet 边界(生成 flowlet_id=0 与 flowlet_id=1 两次决策)。期望 flowlet_id+1 后 path_index 发生变化的 flow 占比应满足 hash 均匀性下的伯努利分布:变化概率 p = (K-1)/K,N 次独立采样的变化数 X ~ Binomial(N, p),验证 `X - N·p≤ 3·sqrt(N·p·(1-p))`(3σ 区间)

@tbl-spec-route-30 验收标准

关键单测场景

  1. 通用图通用算法:32-chip ring(无 attrs) + shortest_path → 路径长度符合环距离
  2. 多路径聚合:32-chip fat-tree + ECMP,max_paths=4 → RoutingResult.paths() 返回 4 条等代价路径
  3. 族特定算法:4×4×4 torus(边 attrs 含 dimension/wraparound) + DOR → 路径长度 = Manhattan 距离
  4. 分布算法:dragonfly(边 attrs 含 role) + UGAL → RoutingResult.output_form == "distribution",含 minimal + valiant 两种
  5. 属性消费校验:构造 DorStrategy 时绑定无 dimension 的 graph → bind_graph raise,错误 message 含 "dimension"
  6. YAML 拼写错误:YAML 写 attrs: {dim: x}(应为 dimension) → 加载期 warning,DorStrategy bind 期 raise
  7. YAML 缺 algorithm:YAML 不写 routing.algorithm → 加载期 raise MissingAlgorithmField,错误 message 含 YAML 路径
  8. YAML algorithm 不存在:YAML 写 algorithm: dor_xy(未注册算法名)→ raise UnknownRoutingAlgorithm
  9. 同拓扑切算法对比:同一份 fat-tree YAML 分别用 algorithm: shortest_path / ecmp / e_ecmp 加载 → 得到三种不同 Strategy 实例,graph 完全相同
  10. 跨连通分量:构造非连通图 + 跨分量 src/dst → bind_graph raise DisconnectedGraphError
  11. routing.attrs 缺模块级 key:YAML routing.attrs 不含 bandwidth_utilization → raise MissingConfigAttr,错误 message 含 "bandwidth_utilization"
  12. routing.attrs 缺算法级 key:algorithm=dor 但 attrs 不含 dimension_order → raise MissingConfigAttr,错误 message 含 "dimension_order"
  13. routing.attrs 含已注册但未消费的 key:algorithm=shortest_path 但 attrs 含 max_paths → warning,不影响行为
  14. edge attrs 拼写错误:YAML 边写 attrs: {dim: x}(应为 dimension)→ 加载期 warning(W1),DorStrategy bind_graph raise IncompatibleStrategyError
  15. 带宽倒数选路:构造异构图(c0-c1: 400Gbps/0.2us, c0-s0: 200Gbps/0.1us, s0-c1: 200Gbps/0.1us),shortest_path 应选 c0→c1 直连(权重 1/400=0.0025)而非 c0→s0→c1(权重 1/200+1/200=0.01),尽管后者延迟更低(0.2us vs 0.2us 相当,但权重差 4 倍)
  16. 边 type 即层级标识:从 RoutingResult 路径边序列读取 EdgeSpec.type,验证 c2c/b2b/r2r 标识正确且与 link_types 定义一致

未来方向

方向优先级前置条件
自适应路由(per-flowlet AR / Q-routing / TE-CCL)per-flowlet AR (LetFlow style) 接口已在 v1.3.2 spec 就绪(新增 PerFlowletAdaptivePolicy + L2-C7 + flowlet_gap_ns attrs);Q-routing / TE-CCL 待补对应 Strategy + 复用既有 Policy 类
拥塞感知 flowlet AR(CONGA / NVIDIA Quantum IB AR)v1.3.2 PerFlowletAdaptivePolicy 已支持 hash 切换 LetFlow 模式;拥塞感知变体需扩展 requires_runtime_state 加入 link_load,在 flowlet 边界查 argmin 替代纯 hash;可通过 attrs flowlet_congestion_aware: bool 复用既有 Policy 框架(同 PacketSpray 的 congestion_aware 设计),不必新增 Policy 类
加权 ECMP(WCMP)对非对称 / 故障拓扑EdgeSpec 加 weight 字段;与故障注入 spec 合并设计
KSP 用于非结构化拓扑(Jellyfish / Xpander)拓扑生成器先支持随机正则图
DQPLB(DRB 变体 / multi-plane)Packet Spraying 算法接口(Strategy + Policy + descriptor schema)已在 v1.3.0 spec 就绪,对应 Layer 1 / Layer 2 实现见 plan;DQPLB 在此基础上扩展多平面路径生成器(v1.2.1 已让 NetworkState 暴露 link_load 满足拥塞感知部分)
Dmodc(降级 fat-tree 适配)D-mod-k 算法接口已在 v1.3.1 spec 就绪;Dmodc(Gliksberg et al., IEEE MICRO 2023)保持闭式算术公式但将固定 $k$ 替换为基于代价的动态分隔数,适应链路 / switch 故障破坏对称性的拓扑——需新 Strategy + 取消 NG9 限制(或单独 spec 覆盖降级场景)
路径稀疏 / 闭式存储优化集群规模 > 1024 chip 后再考虑
路由可视化(前端展示路径热度)RoutingResult 接口稳定后即可
多 graph 路由(异构集群跨域)解除单图绑定假设;引入图域抽象
路由策略评分自检("在该图上算法选得对吗")给定一组 traffic,离线对比多策略产生的瓶颈分布
第 6 类 Policy(in-network aggregation routing)NCCL SHARP / 在网计算等需要 switch 内 reduce 的算法;要扩展 Layer 2 接口语义
Layer 1 / Layer 2 自动一致性检查加载期工具:对每个注册 Strategy 校验 decision_frequency 与 output_form 组合合法
PolicyDispatcher 字段访问统计 / 调优dispatcher 已建好审计能力(v1.2.1);扩展为可导出 per-Policy 的 state query 频次报告,辅助 NetworkState 精度调优

@tbl-spec-route-31 未来方向

实现备注

本节在实现完成后补充,记录 spec 与实际实现的偏差。

v1.2.0 实现偏差(已在 v1.2.1 实现中全部消除)

v1.2.0 时段存在的 5 项 spec ↔ 实现偏差,由 v1.2.1 实现(docs/plans/2026-05-22-routing-v1.2.1-spec-gaps.md)逐项修复。本表保留历史记录,正文以 v1.2.1 实现为准。

偏差点(v1.2.0 时段)已落地修复
Python ↔ Rust 桥接协议启发式推断,PerHopAdaptive / PacketSpray 路径不可达桥接层改为 _build_policy_descriptor 按 spec @tbl-spec-route-23b 5 行严格映射;Rust 端按 descriptor["type"] 分发;5 类 Policy 实例化全部解锁
NetworkState.link_load 常返 0.0,违反精度承诺PhyLink 加滑动平均字段 recent_busy_ns + window_end_ns,窗口 W = base_latency_ns × 10;REQ + RSP 两 VC 都计入
NetworkState.flow_count_on_link 常返 0,违反精度承诺C2CNetwork 加 in_flight_packets_on_link RefCell HashMap;transmit +1 / transmit_ack -1;ObservableView 委托返此值(packet 级是 spec flow 数的合理 proxy)
L2-C2 NetworkState 未声明字段保护未实现引入 PolicyDispatcher + GuardedState:dispatcher 持 declared_state_fields 白名单;feature audit_state_access 开启时通过 GuardedState 拦截未声明字段访问并 panic 携带字段名(默认 off 零开销)
UGAL minimal_count 类属性硬编码 = 1CONFIG_ATTRS_SCHEMA 加 minimal_count: int;registry 工厂期对 distribution 形态 Strategy 用显式 if 分支写入实例属性(避免 dict.get(key, default) 字面违反 config-loading.md)

v1.2.1 实现偏差

偏差点spec 描述实际实现修复时机
NetworkState.flow_count_on_link 多跳语义spec @tbl-21 要求 "active flow 数" 精确语义实现为 "REQ packet 在每跳 +1,ACK 仅最终一跳 -1" 的 in-flight upstream 计数 proxy。多跳路径中间链路 counter 趋于累积偏置。v1.2.1 接受此偏置:当前 adaptive policy(UGAL)只消费 switch_queue_depth + link_busy_until_ns,不读 flow_count_on_link,偏置不影响决策正确性。未来引入消费 flow_count_on_link 的新 policy 时,补每跳 ACK decrement hook(switch 转发期同步减少上游 link 的 counter)。
link_load 衰减实现spec @tbl-21 给三选项(滑动平均 / 瞬时 / 累计 EWMA)v1.2.1 选定线性衰减式滑动平均:每次 transmit 时 decay = (now - window_end) / Wrecent_busy *= (1 - decay),再 += duration;W = base_latency_ns × 10v1.2.1 实现选定参数已登记。若实测 adaptive policy 决策抖动,调到 W = × 20 / × 50 并更新本表。

所有 v1.2.1 验收标准对应单测全 PASS(pytest tests/topology_routing/test_policy_descriptor.py 10/10 + pytest tests/g5/test_multi_chip_regression.py 3/3 + cargo test --features audit_state_access forwarding 25/25 + cargo test forwarding(default off)23/23)。