# EDIF_Part 模块集成指南 本页集中说明 Partition、Routing 与 TDM 三个模块的集成方式。三个模块按执行顺序组织为同级章节: - Partition:构造超图与平台,生成 FPGA/Die 划分结果; - Routing:补齐割线、端口、时序和容量数据,生成物理布线树; - TDM:在布线树上执行连续优化、离散化和物理通道合法化。 各模块算法细节见独立技术手册;`issues` 页面仅收录 Partition 模块的问题记录。 ```{toctree} :hidden: :maxdepth: 2 partition_guide routing_guide tdm_guide api/library_root issues ``` > [Partition 模块技术手册](partition_guide.md) · [Routing 模块技术手册](routing_guide.md) · [TDM 模块技术手册](tdm_guide.md) --- ## Partition 模块集成指南 本指南面向需要在项目中集成超图划分模块的开发者。阅读完本文后,你可以: - 知道**调哪个函数**(`partition()`) - 知道**填哪些参数**、每个参数怎么构造 - 知道**执行完后从哪里取结果** 所有 API 的详细参数说明见 `src/partition/partition.h` 中的注释。 ### 一、30 秒速览:最小调用流程 ``` 1. 构造 graph → 填入你的网表(节点 + 超边) 2. 构造 fpga → 填入目标硬件的资源和拓扑 3. 构造 PartitionParams → 设置算法参数 4. 调用 partition() → 得到划分结果 5. 从 parts[i] 中读取节点 i 被分配到了哪个硬件块 ``` --- ### 二、你需要构造的三个核心数据结构 #### 2.1 `graph` — 你的网表 `graph` 是超图数据结构(定义在 `defs.h`),表示待划分的网络。你需要填写以下字段: | 字段 | 类型 | 含义 | 怎么填 | | ---------------- | ------------------ | ---------------------- | ----------------------------------------------------------------------------------------- | | `nodes` | `vector` | 所有节点 | 每个 node 设置 `weight` 和 `resources`(资源用量向量,如 `[LUT数, FF数, DSP数, BRAM数]`) | | `nets` | `vector` | 所有超边(线网) | 每个 net 设置 `weight`(权重,默认 1.0) | | `incident_nodes` | `vector` | 超边的端点展平数组 | 按顺序拼接每条 net 连接的 node ID,通过 `nets[i].begin` 和 `nets[i].size` 索引 | | `incident_nets` | `vector` | 节点的关联超边展平数组 | 按顺序拼接每个 node 连接的 net ID,通过 `nodes[i].begin` 和 `nodes[i].size` 索引 | | `fixed_assign` | `vector` | 固定分配(可选) | 初始化为全 -1;若节点 i 必须分配到特定硬件块,设 `fixed_assign[i] = 块号` | | `candidates` | `vector>` | 区域约束(可选) | 若节点 i 只能放在某些块中,设 `candidates[i] = {允许的块号集合}` | | `community` | `vector` | 初始划分提示(可选) | 如果有初始分配建议,设 `community[i] = 建议块号` | **构造示例(3 个节点、2 条超边的简单图):** ```cpp graph g; int num_nodes = 3, num_nets = 2; // 节点:每个节点有资源向量 g.nodes.resize(num_nodes); g.nodes[0].resources = VectorXi::Ones(4); // LUT=1, FF=1, DSP=1, BRAM=1 g.nodes[1].resources = VectorXi::Ones(4); g.nodes[2].resources = VectorXi::Ones(4); // 超边 0 连接节点 0,1;超边 1 连接节点 1,2 // incident_nodes = [0, 1, 1, 2] g.incident_nodes = {0, 1, 1, 2}; g.nets.resize(num_nets); g.nets[0] = {1.0, 0, 2}; // weight=1, begin=0, size=2 → 引用 incident_nodes[0..1] g.nets[1] = {1.0, 2, 2}; // weight=1, begin=2, size=2 → 引用 incident_nodes[2..3] // 反向索引:incident_nets // 节点0连接net0, 节点1连接net0和net1, 节点2连接net1 g.incident_nets = {0, 0, 1, 1}; g.nodes[0] = {1, 0, 1, g.nodes[0].resources}; // begin=0, size=1 g.nodes[1] = {1, 1, 2, g.nodes[1].resources}; // begin=1, size=2 g.nodes[2] = {1, 3, 1, g.nodes[2].resources}; // begin=3, size=1 // 默认无固定分配 g.fixed_assign.resize(num_nodes, -1); ``` > **说明:** `incident_nodes` 是 CSR 格式的展平数组。`nets[i].begin` 指向该超边第一个端点在 `incident_nodes` 中的偏移,`nets[i].size` 是端点个数。`incident_nets` 同理,从节点角度索引超边。 #### 2.2 `fpga` — 目标硬件平台 `fpga` 描述划分目标平台的资源与拓扑(定义在 `defs.h`): | 字段 | 类型 | 含义 | 怎么填 | | ----------------- | --------------------- | -------------------- | ------------------------------------------------------------------ | | `resources` | `vector` | 每个硬件块的资源容量 | `resources[i]` 是第 i 个块的容量向量,维度与 `node.resources` 一致 | | `topology` | `vector>` | 连通性矩阵 | `topology[i][j]` 非零表示块 i 和块 j 直连 | | `dist` | `vector>` | 最短跳数矩阵 | `dist[i][j]` 是块 i 到块 j 的最少跳数 | | `maxDist` | `vector` | 每个块允许的最大跳数 | 从 `dist` 行最大值推导 | | `upper_resources` | `vector` | 资源上限(可选) | 启用 `bound_constraint` 时需要填 | | `lower_resources` | `VectorXi` | 资源下限(可选) | 启用 `bound_constraint` 时需要填 | **构造示例(4 个硬件块、环形拓扑):** ```cpp fpga fpgas; int K = 4; int res_dim = 4; // 资源维度:LUT, FF, DSP, BRAM // 每个块的资源容量(例如每个 FPGA 有 10000 LUT、20000 FF、100 DSP、50 BRAM) fpgas.resources.resize(K); for (int i = 0; i < K; i++) fpgas.resources[i] = (VectorXi(4) << 10000, 20000, 100, 50).finished(); // 环形拓扑 fpgas.topology = {{0,1,0,1},{1,0,1,0},{0,1,0,1},{1,0,1,0}}; // 计算最短跳数(用 Floyd-Warshall 或直接手填) fpgas.dist = {{0,1,2,1},{1,0,1,2},{2,1,0,1},{1,2,1,0}}; fpgas.maxDist = {3, 3, 3, 3}; ``` #### 2.3 `PartitionParams` — 算法参数 `PartitionParams`(定义在 `defs.h`)控制算法行为。以下是最常用的参数: | 参数 | 类型 | 默认值 | 含义 | 建议设置 | | ---------------------------- | ---- | ------ | --------------------------------------------------------------------- | ------------------------------------------------ | | `large_net_threshold` | int | 200 | 大规模超边过滤阈值:超过此引脚数的 net 在粗化和细化中被跳过以提升性能 | 通常不用改;设大则包含更多大 net,设小则过滤更多 | | `level` | int | 30 | 最大粗化层数 | 通常不用改 | | `num_initial_solutions` | int | 80 | 生成多少个初始方案 | 越多越可能找到好解,但更慢;建议 32~128 | | `num_best_initial_solutions` | int | 10 | 保留多少个精英解进入细化 | 建议 4~16 | | `has_fix` | bool | false | 是否有固定分配约束 | 有固定节点时设为 true | | `has_region` | bool | false | 是否有区域约束 | 有区域约束时设为 true | | `has_io` | bool | false | 是否有 IO/拓扑约束 | 需要考虑引脚带宽时设为 true | | `force_topo` | bool | false | 是否强制拓扑约束 | 需要严格满足跳数限制时设为 true | | `has_timing` | bool | false | 是否启用时序驱动 | 需要时序优化时设为 true | | `thread` | int | 10 | 并行线程数 | 根据机器配置设置 | | `max_hop` | int | -1 | 最大允许跳数 | -1 表示自动估计 | | `coarsen_method` | int | 1 | 粗化方法 | 1=重边匹配(推荐);0=贪心 | > **注意:** 划分的块数(FPGA 数量)是由 `fpgas.resources.size()` 决定——你给 `fpga` 对象填几个块的资源容量,就会划分成几块。 --- ### 三、调用 partition() — 主入口函数 #### 3.1 函数签名 ```cpp int partition( graph &finest, // [输入] 你的网表 fpga &fpgas, // [输入] 硬件平台 vector &parts, // [输出] 划分结果 ★核心输出★ PartitionParams ¶, // [输入/输出] 算法参数 flat_hash_map &fixed_assignment, // [输入] 固定分配约束 unordered_map> &group_assignment, // [输入] 组约束 flat_hash_map &name_map, // [输入] 名称到节点ID的映射 shared_ptr &evaluator, // [输入] 时序评估器(可为 nullptr) HSFullTiming::HSPartitionFlow &pFlow // [输入] 时序数据流(不用时序时传空对象) ); ``` #### 3.2 返回值 - **返回 1**:划分成功 - **返回 -1**:划分失败(通常是因为资源总量超出硬件容量,无法合法分配) #### 3.3 核心输出:`parts` 函数执行完毕后,**`parts[i]` 就是节点 i 被分配到的硬件块编号**(范围 `[0, fpgas.resources.size()-1]`)。 例如:`parts = [2, 0, 0, 1, 2]` 表示: - 节点 0 → 块 2 - 节点 1 → 块 0 - 节点 2 → 块 0 - 节点 3 → 块 1 - 节点 4 → 块 2 --- ### 四、完整调用示例 #### 示例 1:最简场景(纯图分割,无时序,无约束) ```cpp #include "partition/partition.h" #include "partition/defs.h" using namespace std; // ---- 1. 构造超图 ---- graph finest; // ... 填充 nodes, nets, incident_nodes, incident_nets(见第 2.1 节) // ---- 2. 构造硬件平台 ---- fpga fpgas; // ... 填充 resources, topology, dist(见第 2.2 节) // ---- 3. 配置参数 ---- PartitionParams para; // 注意:划分块数由 fpgas.resources.size() 决定(上面已填了 4 个块) para.num_initial_solutions = 64; // 生成 64 个初始方案 para.thread = 8; // 用 8 个线程 // ---- 4. 准备输出和约束(均为空) ---- vector parts; // 输出:划分结果 flat_hash_map fixed_assignment; // 空:无固定约束 unordered_map> group_assignment; // 空:无组约束 flat_hash_map name_map; // 空:不需要名字映射 shared_ptr evaluator = nullptr; // 空:不使用时序 HSFullTiming::HSPartitionFlow pFlow; // 空:不使用时序数据流 // ---- 5. 调用划分 ---- int ret = partition(finest, fpgas, parts, para, fixed_assignment, group_assignment, name_map, evaluator, pFlow); if (ret == 1) { // 划分成功! // parts[i] 即为节点 i 的归属块号 for (int i = 0; i < parts.size(); i++) { printf("节点 %d → 块 %d\n", i, parts[i]); } } else { printf("划分失败\n"); } ``` #### 示例 2:有固定分配约束 某些节点必须放在特定硬件块上,不能被移动: ```cpp // 假设节点名叫 "cpu_core_0",必须放在块 2 flat_hash_map fixed_assignment; name_map["cpu_core_0"] = 42; // 名字到 ID 的映射 fixed_assignment["cpu_core_0"] = {2, {}}; // fpgaNo=2 表示固定到块 2 para.has_fix = true; // 必须开启这个开关 ``` #### 示例 3:有区域约束 某些节点只能在指定的几个块中选择: ```cpp // 节点 "mem_ctrl" 只能放在块 0 或块 1 name_map["mem_ctrl"] = 7; fixInfo region_info; region_info.fpgaNo = -2; // -2 表示这是区域约束(不是固定分配) region_info.id = {{0, 0, 0}, {0, 0, 1}}; // 每个内层向量的第 3 个元素是候选块号 fixed_assignment["mem_ctrl"] = region_info; para.has_region = true; // 必须开启这个开关 ``` #### 示例 4:有组约束 某些节点必须被分到同一个块: ```cpp // "macro_a"、"macro_b"、"macro_c" 三个节点必须在一起 group_assignment["macro_group"] = {"macro_a", "macro_b", "macro_c"}; name_map["macro_a"] = 10; name_map["macro_b"] = 11; name_map["macro_c"] = 12; ``` #### 示例 5:使用时序数据 ```cpp // 声明pFlow变量,circuitFolder为时序网络文件所在目录路径(一般为--result_netedge所指定目录) HSFullTiming::HSPartitionFlow pFlow; sta::parse(pFlow, circuitFolder); sta::buildNextObjInfo(pFlow); para.partitionParams.has_timing = true; ``` --- ### 五、执行流程说明 调用 `partition()` 后,内部自动执行以下步骤: ``` partition() │ ├─ 1. 处理约束(固定分配、区域约束映射到图节点) │ ├─ 2. 多线程并行调用 multilevel() × N 次 │ │ │ ├─ 2a. 粗化(Coarsening):图逐步缩小 │ ├─ 2b. 初始划分(Initial Partitioning):在最小图上生成多个候选解 │ └─ 2c. 细化(Refinement):解映射回原图的过程中用 FM/PM 等算法优化 │ ├─ 3. [可选] COCP:对候选解做更强的粗化-细化 │ ├─ 4. [可选] V-Cycle:对最优解进行多轮深度优化 │ └─ 5. 从所有候选解中选出综合最优解 → 写入 parts ``` 你只需要调用 `partition()`,这些步骤全自动完成。 --- ### 六、常见问题 #### Q: 资源向量(resources)的维度必须一致吗? 是的。`node.resources`、`fpga.resources[i]`、`fpga.upper_resources` 的维度必须相同,代表同一组资源类型(如 LUT, FF, DSP, BRAM)。 #### Q: 不需要时序优化时,evaluator 和 pFlow 怎么传? 传 `nullptr` 给 evaluator,`pFlow` 传一个默认构造的空对象即可。同时确保 `para.has_timing = false`。 #### Q: name_map 是必须的吗? 如果你不需要固定分配约束(`has_fix=false`)和区域约束(`has_region=false`),name_map 可以为空。如果需要这些约束,name_map 必须包含约束中引用的所有节点名到 ID 的映射。 #### Q: topology 和 dist 需要手算吗? `topology` 是邻接矩阵,需要你提供。`dist` 可以用 Floyd-Warshall 从 `topology` 推导。如果你不需要拓扑约束,可以不填 topology 和 dist,并将 `force_topo` 和 `has_io` 设为 false。 #### Q: 划分结果质量不满意怎么办? 主要调整以下参数: - `para.num_initial_solutions`:增大(如 128、256)以探索更多候选 - `para.V_Cycle_on = 1`:开启 V-Cycle 深度优化 - `para.COCP_on = 1`:开启 COCP 粗化优化 - `para.refine_iters`:增加细化迭代次数 - `para.max_move`:增大单次细化允许的移动步数 #### Q: 如何验证划分结果的正确性? ```cpp // 计算割边权重(越小越好) vector> cut_weights; Metrics m = fpgas.computeCutWeights(finest, parts, cut_weights); printf("Cut = %d, Violation = %d\n", m.cut, m.violation); ``` --- ### 七、模块依赖 | 依赖 | 用途 | 必须 | | ------------------------------------- | ---------------------------- | --------------------- | | Eigen3 | 矩阵/向量运算(资源计算) | 是 | | TBB (Intel Threading Building Blocks) | 并行计算 | 是 | | spdlog | 日志输出 | 是 | | OR-Tools | ILP 初始划分求解(COCP_ILP) | 否(不用 ILP 可跳过) | | nlohmann/json | JSON 序列化 | 是 | | parallel-hashmap | 高性能哈希表 | 是 | --- ## Routing 模块集成指南 本指南面向已经取得 Partition 结果、需要继续完成跨 FPGA/Die 布线的开发者。阅读完本文后,你可以: - 知道**调哪个函数**(`routing_flow::routingFlow()`) - 知道 Partition 后、Routing 前还要补充哪些数据 - 知道哪些参数必须确认,以及从哪里读取布线结果 所有 API 的详细参数说明见 `src/routing/routing.h` 中的注释;算法细节见 [Routing 模块技术手册](routing_guide.html)。 ### 一、30 秒速览:最小调用流程 ```text 1. 取得 Partition 输出的 parts 2. 补充 die_parts、cutweights 和可选的时序/端口数据 3. 构造共享的 DelayLibrary 和 Routing 4. 调用 routing_flow::routingFlow() 5. 从 router.route_trees 等字段读取结果 ``` --- ### 二、你需要准备的数据 #### 2.1 Partition 之前已经存在的数据 | 数据 | 必填性 | 怎么准备 | | --- | --- | --- | | `graph finest` | 必须 | 直接复用 Partition 的网表。ARE/NET 输入可调用 `partUtil::readGraph()`;EDIF 建模结果可调用 `partUtil::constructGraph()` | | `fpga fpgas` | 必须 | 直接复用 Partition 使用的平台对象,不要为 Routing 重新编号 | | `params para` | 必须 | 直接复用顶层配置解析得到的 `params`,进入 Routing 前确认相关开关 | | `simpleTimings`、`pFlow` | 时序模式必填 | 通常由 `modeling::model()` 生成;仅从时序目录构建 `pFlow` 时可调用 `sta::parse()` 和 `sta::buildNextObjInfo()` | | `name_map` | 条件必填 | `partUtil::readGraph()` 或 `partUtil::constructGraph()` 可在构图时同步填充 | #### 2.2 Partition 后、Routing 前补充的数据 | 数据 | 必填性 | 怎么准备 | | --- | --- | --- | | `parts` | 必须 | 直接使用 `partition()` 的输出;外部分区文件可调用 `readPartitionResult()` 读取 | | `die_parts` | Die 模式必填 | 调用 `partDie()` 生成;无 Die 模式可为空或按 `{parts[i], 0}` 构造 | | `cutweights` | 必须 | 完整流程可直接取 `partUtil::checkResults()` 的返回值;单独计算时调用 `fpgas.computeCutWeights()`,非时序模式可用 `computeCutWeightsOld()` | | `cutConnections` | 可选 | `result::computeSubEDIF()` 生成子连接后,调用 `convertSubConnetsToCutConnections()` 转换;不需要端口连接时传空容器 | | 割线网级 `simpleTimings` | 时序模式条件必填 | 调用 `HSFullTiming::HSCutNetFlow::buildFrom(pFlow, parts)`,再用 `convertCutNetPathsToSimpleTimings()` 转换 | | `routing_fpgas` | 多层级模式必填 | 调用 `flattenHierarchyToFpgasForRouting()` 展平叶级 FPGA,并同时取得 `path_to_fpga` 和 `fpga_hierarchy_paths` | | `DelayLibrary delayLib` | 必须提供对象 | 直接声明 `DelayLibrary delayLib`;设置 `delay_lib_path` 后由 `routingFlow()` 自动加载,也可解析 JSON 后调用 `buildFromJson()` | > `cut_nets`、`route_trees` 和 `cut_mat` 是 Routing 的结果,不需要在调用前手工构造。 #### 2.3 Routing 产生的核心结果 | 字段 | 含义 | | --- | --- | | `router.cut_nets` | 跨分区线网及其物理端点 | | `router.cut_timing_paths` | 转换后的割线网时序路径 | | `router.route_trees` | 原始 net ID 对应的物理布线树 | | `router.route_trees_edges` | 带端口和方向信息的布线边 | | `router.cut_mat` | 布线完成后的方向负载 | | `router.delayLibReady` | 延迟库是否成功加载 | --- ### 三、Routing 参数 | 参数 | 必填性 | 默认值 | 建议值或填写方式 | | --- | --- | ---: | --- | | `para.route` | 必须确认 | `true` | 需要执行 Routing/TDM 时保持 `true` | | `para.has_die` | 条件必填 | `false` | 平台包含 Die 时设为 `true`,并提供 `die_parts` | | `para.routing_mode` | 可选 | `false` | 时序驱动布线时设为 `true` | | `para.legacy_non_timing_routing` | 可选 | `false` | 一般保持 `false` | | `routing_cost_factor` | 可选 | `0.5` | 建议从 `0.5` 开始 | | `timing_routing_cost_mode` | 可选 | `critical_minmax_hop` | 一般保持默认值 | | `timing_hop_penalty_multiplier` | 可选 | `0.5` | 建议从 `0.5` 开始 | | `gio_channel_grouping_capacity` | 必须确认 | `23` | 填平台真实值,并保证 Routing 与 TDM 一致 | | `delay_lib_path` | 时序/TDM 模式条件必填 | 空 | 使用与目标平台匹配且已验证的 JSON | --- ### 四、完整调用示例 下面的代码以一个已经完成 Partition 的小型网表为基础。示例中的容量、ratio 和 delay 只用于说明字段含义,移植时应替换为目标平台的实际数据。 #### 4.1 构造可选的 `SimpleTiming` 只有开启时序驱动布线时才需要 `simpleTimings`。一条包含 3 个节点、2 条 net 的路径可以这样填写: ```cpp vector simpleTimings; simpleTimings.emplace_back( vector{0, 1, 2}, // path_node:从 source 到 sink 的 graph node ID vector{0, 1}, // path_net:相邻节点之间经过的原始 net ID "-0.25", // slack:原始裕量,单位 ns "10.0", // period:时钟周期,单位 ns "clk_main", // clock_domain:时钟域名称 0); // pathId:该路径的唯一编号 ``` 填写时应满足: - `path_node[k]` 与 `path_node[k+1]` 由 `path_net[k]` 连接; - 通常有 `path_node.size() == path_net.size() + 1`; - node ID 和 net ID 必须来自传给 Routing 的同一份 `finest`; - `slack`、`period` 虽然是字符串,内容仍应是可解析的纳秒数值; - 非时序模式直接传空的 `vector`。 已有 `pFlow` 时,推荐调用 `HSFullTiming::HSCutNetFlow::buildFrom(pFlow, parts)`,再用 `convertCutNetPathsToSimpleTimings()` 自动生成割线网级路径,不必手填。 #### 4.2 构造可选的 `CutConnection` `CutConnection` 主要为 `routing_cut_info.json` 提供分区端口和网表连接信息,不负责替代 `parts` 或 `cutweights`。不需要稳定端口名时直接传空容器。 下面表示从物理分区 0 的 `data_out` 连接到物理分区 1 的 `data_in`: ```cpp vector cutConnections; CutConnection connection{}; connection.from_part = 0; connection.to_part = 1; CutConnItem item{}; item.from_port = "data_out"; item.to_port = "data_in"; item.bypass = false; // nc 是可选的原网表追踪信息。 item.nc.nc_drv.inst = "u_source"; item.nc.nc_drv.port = "Q"; NC load{}; load.inst = "u_sink"; load.port = "D"; item.nc.nc_loads.push_back(load); connection.connections.push_back(item); cutConnections.push_back(connection); ``` `from_part` 和 `to_part` 必须与最终 `route_trees_edges` 使用的物理编号一致: - 无 Die 模式填写 FPGA ID; - Die 模式填写展平 Die ID,即 `fpga_id * dies_per_fpga + die_id`; - 多层级模式填写 `path_to_fpga` 生成的全局叶级 FPGA ID。 若已经调用 `result::computeSubEDIF()`,可直接使用 `convertSubConnetsToCutConnections(resultContent.subConnets)` 生成,不建议再次手写。 #### 4.3 构造 `DelayLibrary` Routing 和 TDM 必须共用同一个 `DelayLibrary`。ratio 表示一条物理通道上时分复用的逻辑信号数量,delay 表示该 ratio 下测得或标定的端到端延迟,单位为 ns。GIO 与 MGT 的硬件特性不同,必须分别填写。 ##### 方法一:直接在 C++ 中赋值 下面给出一个不调用 `buildFromJson()` 的完整示例: ```cpp DelayLibrary makeManualDelayLibrary() { DelayLibrary lib; // 与 data/delay.json 一致:每个 ratio 都有一个对应的 delay(ns)。 const vector gio_ratio = { 2, 8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120, 128, 136, 144, 152, 160, 168, 176, 184, 192, 200, 208, 216, 224, 232, 240, 248, 256 }; const vector gio_delay = { 4.57, 58, 62, 67, 72, 77, 81, 86, 90, 95, 100, 105, 110, 115, 120, 125, 130, 135, 140, 145, 150, 155, 160, 165, 170, 175, 180, 185, 190, 195, 200, 205, 210 }; const vector mgt_ratio = {62, 124, 248, 496, 992}; const vector mgt_delay = {96, 104, 124, 154, 216}; lib.gio.pwl = PWLModel::fromPoints(gio_ratio, gio_delay); lib.mgt.pwl = PWLModel::fromPoints(mgt_ratio, mgt_delay); if (!lib.gio.pwl.valid || !lib.mgt.pwl.valid) { throw runtime_error("invalid ratio-delay points"); } // 连续优化允许的 ratio 范围。 lib.gio_meta.rmin = gio_ratio.front(); lib.gio_meta.rmax = gio_ratio.back(); lib.gio_meta.has_bounds = true; lib.mgt_meta.rmin = mgt_ratio.front(); lib.mgt_meta.rmax = mgt_ratio.back(); lib.mgt_meta.has_bounds = true; // delay.json 没有单独写 choices,因此这里将全部采样 ratio 作为离散档位。 for (double ratio : gio_ratio) lib.gio_meta.choices.push_back(static_cast(ratio)); for (double ratio : mgt_ratio) lib.mgt_meta.choices.push_back(static_cast(ratio)); // GIO 的直通/低复用特殊点;delay 仍然是 ns。 lib.gio.has_bypass = true; lib.gio.bypass_ratio = 2; lib.gio.bypass_delay = 4.57; return lib; } ``` `PWLModel::fromPoints()` 会按 ratio 排序、合并重复 ratio,并建立分段线性斜率;区间内插值,区间外沿首段或末段外推。每类模型至少需要两个不同的 ratio 点,且 ratio 与 delay 数组长度必须一致。 手工预加载后,`delay_lib_path` 应留空,并在构造 `Routing` 后显式设置: ```cpp DelayLibrary delayLib = makeManualDelayLibrary(); para.tdmParams.delay_lib_path.clear(); // 按 4.7 节构造 Routing 后执行: // router.delayLibReady = true; ``` 只有在 GIO、MGT 模型都有效时才能把 `delayLibReady` 设为 `true`。 移植时应保证: - ratio 是正数,并按硬件支持的复用档位取值; - delay 是该类型物理链路在对应 ratio 下的延迟,单位统一为 ns; - `choices` 最好是采样 ratio 的子集,使离散档位能直接命中实测点; - `rmin`、`rmax` 覆盖全部 `choices`,但不要超出硬件支持范围。 ##### 方法二:使用 `buildFromJson()` `data/delay.json` 采用最简格式:根对象只包含 `gio` 和 `mgt`,每个 `points` 元素都是 `[ratio, delay_ns]`: ```json { "gio": { "points": [ [2, 4.57], [8, 58], [16, 62], [24, 67], [32, 72], [40, 77], [48, 81], [56, 86], [64, 90], [72, 95], [80, 100], [88, 105], [96, 110], [104, 115], [112, 120], [120, 125], [128, 130], [136, 135], [144, 140], [152, 145], [160, 150], [168, 155], [176, 160], [184, 165], [192, 170], [200, 175], [208, 180], [216, 185], [224, 190], [232, 195], [240, 200], [248, 205], [256, 210] ] }, "mgt": { "points": [ [62, 96], [124, 104], [248, 124], [496, 154], [992, 216] ] } } ``` `buildFromJson()` 接收已经解析好的 `nlohmann::json`,它本身不负责打开文件。读取当前文件的完整代码如下: ```cpp ifstream input("data/delay.json"); if (!input.is_open()) { throw runtime_error("cannot open data/delay.json"); } nlohmann::json delayJson; input >> delayJson; DelayLibrary delayLib; if (!delayLib.buildFromJson(delayJson)) { throw runtime_error("delay library is incomplete"); } ``` 该方法会完成以下工作: 1. 检查根对象中是否同时存在可用的 `gio` 和 `mgt`; 2. 将每个二元数组的第一个数解释为 ratio,第二个数解释为 delay(ns); 3. 对 ratio 排序、合并重复点,并构造 PWL 模型与连续斜率; 4. 以首尾采样 ratio 推导 `min_ratio` 和 `max_ratio`; 5. 将全部采样 ratio 推导为离散 `choices`; 6. 将 GIO 的最小采样点 `[2, 4.57]` 推导为 bypass; 7. 建立可用的线性回退模型,并解析可选的多层级 `scopes`。 `delay_detail.json` 中的对象形式采样点也受支持,例如 `{"ratio": 8, "delay_ns": 58}`;根级 `bypass` 也会被读取。当前 `buildFromJson()` 不读取 `fit`、`fallback_linear` 和 `low_range_linear` 字段,而是根据 `points` 自行建立运行时模型。 如果希望流程自动读文件,只需设置 `para.tdmParams.delay_lib_path`,不要提前调用 `buildFromJson()`;`routingFlow()` 会读取文件并在成功后设置 `router.delayLibReady=true`。 #### 4.4 示例一:无 Die 模式 假设 `finest` 有 3 个节点,Partition 输出 `parts = {0, 1, 1}`,目标平台有 2 个 FPGA。下面的矩阵元素表示物理连接数量,不要预先乘以 ratio: ```cpp params para; para.route = true; para.has_die = false; para.routing_mode = true; para.partitionParams.constraints.has_timing = true; para.tdmParams.gio_channel_grouping_capacity = 23; para.tdmParams.delay_lib_path.clear(); // 本例手工预加载 vector parts = {0, 1, 1}; // 无 Die 模式可传空;这里显式填写,便于统一节点映射。 vector> die_parts = { {0, 0}, {1, 0}, {1, 0} }; // fpgas.resources 已由 Partition 填好,这里只展示布线相关字段。 fpgas.topology = { {0, 1}, {1, 0} }; // 0->1 有 2 根 GIO 和 1 根 MGT;反方向相同。 fpgas.hio_channel_assignment = { {0, 2}, {2, 0} }; fpgas.mgt_channel_assignment = { {0, 1}, {1, 0} }; fpgas.cutweights_assignment = { {0, 3}, {3, 0} }; constraint::computeFPGATopo(fpgas); vector> cutweights; fpgas.computeCutWeightsOld(finest, parts, cutweights); vector simpleTimings; simpleTimings.emplace_back( vector{0, 1, 2}, vector{0, 1}, "-0.25", "10.0", "clk_main", 0); vector cutConnections; // 若需要稳定端口名,可按 4.2 节加入 0 -> 1 的连接。 DelayLibrary delayLib = makeManualDelayLibrary(); ``` 这里的 `hio_channel_assignment[0][1] = 2` 表示两根物理 GIO,`gio_channel_grouping_capacity = 23` 表示每根 GIO 可承载 23 个逻辑通道;二者含义不同。 #### 4.5 示例二:有 Die 模式 下面使用 2 个 FPGA、每个 FPGA 4 个 Die。`parts` 仍然保存 FPGA ID,`die_parts` 同时保存 FPGA ID 和 Die ID: ```cpp params para; para.route = true; para.has_die = true; para.routing_mode = true; para.partitionParams.constraints.has_timing = true; para.tdmParams.gio_channel_grouping_capacity = 23; para.tdmParams.delay_lib_path.clear(); vector parts = {0, 0, 1}; vector> die_parts = { {0, 0}, // node 0 -> FPGA 0, Die 0 {0, 1}, // node 1 -> FPGA 0, Die 1 {1, 0} // node 2 -> FPGA 1, Die 0 }; fpgas.dies_per_fpga = 4; fpgas.topology = { {0, 1}, {1, 0} }; fpgas.cutweights_assignment = { {0, 2}, {2, 0} }; constraint::computeFPGATopo(fpgas); DieConnection gio{}; gio.left_fpga = 0; gio.left_die = 1; gio.right_fpga = 1; gio.right_die = 0; gio.left_socket = "J10"; gio.right_socket = "J20"; gio.isMGT = false; gio.left2right.total_channels = 1; gio.right2left.total_channels = 1; DieConnection mgt{}; mgt.left_fpga = 0; mgt.left_die = 1; mgt.right_fpga = 1; mgt.right_die = 0; mgt.left_socket = "MGT0"; mgt.right_socket = "MGT1"; mgt.isMGT = true; mgt.left2right.total_channels = 1; mgt.right2left.total_channels = 1; fpgas.die_connections = {gio, mgt}; vector> cutweights; fpgas.computeCutWeightsOld(finest, parts, cutweights); vector simpleTimings; simpleTimings.emplace_back( vector{0, 1, 2}, vector{0, 1}, "-0.25", "10.0", "clk_main", 0); vector cutConnections; DelayLibrary delayLib = makeManualDelayLibrary(); ``` 实际工程中通常调用 `partDie()` 生成 `die_parts`,并由 FIT/socket 解析流程生成 `die_connections`。`failed_channels` 中填写不可用的物理通道编号;可用通道数按 `total_channels - failed_channels.size()` 计算。 #### 4.6 示例三:多层级模式 假设 `hierarchy_root` 已由平台解析流程构造,`node_paths[i]` 是节点 `i` 的叶级层次路径: ```cpp params para; para.route = true; para.has_hierarchy = true; para.has_die = false; para.routing_mode = true; para.partitionParams.constraints.has_timing = true; para.tdmParams.gio_channel_grouping_capacity = 23; para.tdmParams.delay_lib_path.clear(); fpga fpgas; fpga fpgas0; map, int> path_to_fpga; const int fallback_capacity = 23; if (!flattenHierarchyToFpgasForRouting( hierarchy_root, fpgas, fpgas0, path_to_fpga, fallback_capacity, /*delay_lib_path=*/"")) { throw runtime_error("failed to flatten hierarchy"); } constraint::computeFPGATopo(fpgas); // node_paths 来自层次化 Partition,例如 {rack, cluster, board, fpga}。 vector parts(node_paths.size(), -1); for (size_t i = 0; i < node_paths.size(); ++i) { auto it = path_to_fpga.find(node_paths[i]); if (it == path_to_fpga.end()) { throw runtime_error("node path has no leaf FPGA"); } parts[i] = it->second; } vector> die_parts(parts.size()); for (size_t i = 0; i < parts.size(); ++i) { die_parts[i] = {parts[i], 0}; } vector> cutweights; fpgas.computeCutWeightsOld(finest, parts, cutweights); vector simpleTimings; // 按 4.1 节填写;其中仍使用 finest 的 node/net ID。 vector cutConnections; // 若需要端口信息,from_part/to_part 使用 path_to_fpga 的全局 ID。 DelayLibrary delayLib = makeManualDelayLibrary(); ``` 如果需要层级延迟,可以在手工基础库上为不同范围增加固定偏移: ```cpp void setScopeOffset(DelayLibrary &lib, DelayScope scope, double offset_ns) { auto &dst = lib.scoped_models[static_cast(scope)]; dst.has_gio = true; dst.has_mgt = true; dst.gio = lib.gio; dst.mgt = lib.mgt; dst.gio_meta = lib.gio_meta; dst.mgt_meta = lib.mgt_meta; DelayLibrary::addDelayOffset(dst.gio, offset_ns); DelayLibrary::addDelayOffset(dst.mgt, offset_ns); lib.has_scoped_models = true; } delayLib.initDefaultScopeModels(); setScopeOffset(delayLib, DelayScope::Board, 0.0); setScopeOffset(delayLib, DelayScope::InterBoard, 38.79); setScopeOffset(delayLib, DelayScope::InterCluster, 128.007); setScopeOffset(delayLib, DelayScope::InterRack, 160.0); ``` 偏移量同样是 ns,应来自对应板内、跨板、跨 cluster 和跨 rack 链路的测量结果。也可以在 JSON 的 `scopes` 中为某个范围提供独立 `points`,而不仅是固定偏移。 #### 4.7 三种模式共用的 Routing 调用 完成 4.4、4.5 或 4.6 中任意一组输入后,调用方式相同: ```cpp Routing router( finest, die_parts, fpgas, fpgas.die_connections, para.partitionParams.timing.extra_delay_cut, para.partitionParams.timing.extra_delay_tdm, para.partitionParams.timing.extra_delay_cut_die, para.routing_mode, para.partitionParams.timing.mul_clock_attr, para.has_die, para.routingParams.routing_cost_factor, simpleTimings, &delayLib); router.timing_routing_cost_mode = para.routingParams.timing_routing_cost_mode; router.timing_hop_penalty_multiplier = para.routingParams.timing_hop_penalty_multiplier; router.channel_grouping_capacity = para.tdmParams.gio_channel_grouping_capacity > 0 ? para.tdmParams.gio_channel_grouping_capacity : 23; // 本文示例已经手工构造 delayLib,因此显式标记为可用。 // 如果使用 delay_lib_path 自动加载,不要提前设置该标志。 router.delayLibReady = true; routing_flow::routingFlow( finest, parts, die_parts, fpgas, cutweights, para, router, pFlow, &delayLib, cutConnections); if (router.route_trees.empty() && !router.cut_nets.empty()) { throw runtime_error("Routing produced no route tree"); } ``` `Routing` 引用 `finest`、`die_parts`、`fpgas` 和 `DelayLibrary`,这些对象必须至少存活到 TDM 执行结束。 --- ### 五、执行流程说明 ```text Partition 输出 | v 补齐割线、Die、时序和平台容量 | v 提取 cut_nets 并压缩时序路径 | v 排序并生成初始布线树 | v 按需执行拆线重布 | v 写入 route_trees、cut_mat 和连接结果 ``` 这些步骤由 `routingFlow()` 自动完成,集成层不需要逐个调用内部算法。 #### 5.1 多层级平台 多层级流程先把所有叶级 FPGA 展平为 `routing_fpgas`,再用全局 FPGA ID 执行与普通平台相同的布线。`fpga_hierarchy_paths[id]` 保存该 FPGA 从外层到内层的层级路径,Routing 据此判断连接跨越的层级;同一份展平平台和路径随后交给 TDM。 > **注意:编号不应混用。** `parts`、`die_parts`、`cutweights`、`routing_fpgas` 和层级路径必须使用同一套展平编号。 --- ### 六、结果读取 建议按以下顺序检查: 1. `cut_nets` 是否覆盖所有跨 FPGA/Die 线网; 2. `route_trees` 中每个 sink 是否能回溯到 source; 3. `cut_mat` 是否与布线树的实际负载一致; 4. `route_trees_edges` 的端口和方向是否完整; 5. 时序模式下,`cut_timing_paths` 是否引用有效的局部 cut-net 编号。 常用输出包括: | 输出 | 内容 | | --- | --- | | `routing_cut_info.json` | 端口连接和可用的布线父节点关系 | | `router.route_trees` | 程序内的物理布线树 | | `router.route_trees_edges` | 可用于导出的命名布线边 | --- ### 七、常见问题 #### Q:Partition 成功,为什么没有生成 `cut_nets`? 先检查 `parts` 是否确实包含多个 FPGA ID,以及 `cutweights` 是否非空。如果所有 net 的端点都在同一 FPGA,`cut_nets` 为空是正常结果。 #### Q:无 Die 模式必须填写 `die_parts` 吗? 不是。`has_die=false` 时可以传空容器;如果希望统一节点映射,也可以填写 `{parts[i], 0}`。 #### Q:为什么布线结果不可达? 检查平台拓扑、方向容量、FPGA/Die 编号和 `cutweights` 维度。多层级模式还要检查展平编号与 `fpga_hierarchy_paths` 是否对应。 --- ### 八、模块依赖 | 依赖 | 用途 | | --- | --- | | Partition 输出 | 提供 `parts`、`die_parts` 和割线统计 | | Eigen3、TBB | 数据计算与并行执行 | | spdlog | 日志输出 | | nlohmann/json | 延迟库和结果 JSON | --- ## TDM 模块集成指南 本指南面向已经完成 Routing、需要继续分配时分复用比例和物理通道的开发者。阅读完本文后,你可以: - 知道**调哪个函数**(`tdm::executeTDM()`) - 知道需要提供哪些 Routing 结果和参数 - 知道从哪些文件读取最终 TDM 结果 所有 API 的详细参数说明见 `src/tdm/tdm.h` 和 `src/tdm/delay_lib.h` 中的注释;算法细节见 [TDM 模块技术手册](tdm_guide.html)。 ### 一、30 秒速览:最小调用流程 ```text 1. 完成 routing_flow::routingFlow() 2. 确认 Routing 的布线树、容量和延迟库有效 3. 设置 TDMParams 4. 调用 tdm::executeTDM() 5. 从 tdmDelayReport.json 和 routing.out 读取结果 ``` --- ### 二、你需要准备的四个输入 | 输入 | 必填性 | 怎么准备 | | --- | --- | --- | | `Routing router` | 必须 | 先按上一节构造 `Routing`,再调用 `routing_flow::routingFlow()`;该函数会填充 `cut_nets`、`route_trees` 和 `cut_mat` | | `fpga fpgas` | 必须 | 直接复用 Routing 使用的平台对象;多层级流程使用 `flattenHierarchyToFpgasForRouting()` 生成的 `routing_fpgas` | | `params para` | 必须 | 直接复用 Routing 的顶层配置对象,并在调用前确认 `tdmParams`、`has_die` 和时序开关 | | `DelayLibrary delayLib` | 必须 | 在 Routing 前声明一次并传给两个模块;`routingFlow()` 或 `executeTDM()` 会按 `delay_lib_path` 自动加载,也可调用 `buildFromJson()` 预加载 | `timing_edges`、`tdm_groups` 和离散 ratio 都由 `executeTDM()` 内部生成,不需要手工填写。 --- ### 三、TDM 参数 | 参数 | 必填性 | 默认值 | 建议值或填写方式 | | --- | --- | ---: | --- | | `delay_lib_path` | 时序模式条件必填 | 空 | 使用与平台匹配且已验证的 JSON | | `gio_channel_grouping_capacity` | 必须确认 | `23` | 填平台真实值,并与 Routing 保持一致 | | `max_iters` | 可选 | `100` | 首次集成保持默认值 | | `convergence_threshold` | 可选 | `1e-3` | 首次集成保持默认值 | | `avg_only` | 可选 | `false` | 容量联调时可临时设为 `true` | | `tdm_fast_mode` | 可选 | `false` | 正式结果保持 `false` | | `allow_mgt_on_non_timing_edges` | 可选 | `false` | 一般保持 `false` | | `tdm_opt_mode` | 可选 | `legacy` | 稳定基线用 `legacy`,完整增强流程可用 `v2` | | `tdm_topk` | 可选 | `1` | 日常保持 `1`,调试时可增大 | #### 3.1 `tdm_opt_mode` 的作用 | 取值 | 作用 | | --- | --- | | `legacy` | 使用稳定的默认后处理流程 | | `root_split` | 启用 GIO root 方向拆分 | | `final_polish` | 启用最终局部打磨 | | `v2` | 同时启用 root split 和最终打磨 | | `no_postprocess` | 跳过最终后处理,主要用于调试 | --- ### 四、完整调用示例 `executeTDM()` 的四个参数在三种平台模式下相同,区别在于 `router` 和 `fpgas` 中保存的编号、物理连接与层级信息。不要手工构造内部 `Tdm`、`timing_edges` 或 `tdm_groups`。 #### 4.1 函数签名 ```cpp void executeTDM( Routing &router, const fpga &fpgas, const params ¶, DelayLibrary &delayLib); ``` #### 4.2 示例一:无 Die 模式 接续 Routing 4.4 和 4.7 的 `router`、`fpgas`、`delayLib`: ```cpp para.has_die = false; para.tdmParams.max_iters = 100; para.tdmParams.convergence_threshold = 1e-3; para.tdmParams.avg_only = false; para.tdmParams.tdm_fast_mode = false; para.tdmParams.tdm_opt_mode = "legacy"; para.tdmParams.allow_mgt_on_non_timing_edges = false; para.tdmParams.gio_channel_grouping_capacity = 23; if (!router.delayLibReady) { throw runtime_error("DelayLibrary is not ready"); } tdm::executeTDM(router, fpgas, para, delayLib); ``` TDM 会从 `router.route_trees`、`router.cut_nets` 和 `router.cut_mat` 自动建立容量组。`parts` 或 `die_parts` 不需要再次传入。 #### 4.3 示例二:有 Die 模式 接续 Routing 4.5 和 4.7。只要 `para.has_die`、`router.die_parts`、`fpgas.dies_per_fpga` 和 `fpgas.die_connections` 已经一致,调用本身没有变化: ```cpp para.has_die = true; para.tdmParams.max_iters = 100; para.tdmParams.avg_only = false; para.tdmParams.tdm_opt_mode = "legacy"; para.tdmParams.gio_channel_grouping_capacity = 23; if (router.die_parts.size() != router.finest.nodes.size()) { throw runtime_error("die_parts size mismatch"); } tdm::executeTDM(router, fpgas, para, delayLib); ``` TDM 内部使用 `fpga_id * dies_per_fpga + die_id` 生成展平物理节点,并从 `die_connections` 区分片内、GIO 和 MGT 连接。 #### 4.4 示例三:多层级模式 接续 Routing 4.6 和 4.7。多层级 TDM 必须继续传入展平后的 `fpgas`,不能换回 `hierarchy_root.fpgas`: ```cpp para.has_hierarchy = true; para.has_die = false; para.tdmParams.max_iters = 100; para.tdmParams.avg_only = false; para.tdmParams.tdm_opt_mode = "legacy"; para.tdmParams.gio_channel_grouping_capacity = 23; if (fpgas.fpga_hierarchy_paths.size() != fpgas.resources.size()) { throw runtime_error("hierarchy path count mismatch"); } if (!delayLib.has_scoped_models) { throw runtime_error("hierarchy delay scopes are not configured"); } tdm::executeTDM(router, fpgas, para, delayLib); ``` 当 `delayLib.has_scoped_models=true` 且 `fpga_hierarchy_paths` 非空时,Routing 和 TDM 会自动按 `Board`、`InterBoard`、`InterCluster`、`InterRack` 选择延迟模型。 #### 4.5 使用文件自动加载延迟库 如果不希望在 C++ 中填写 ratio–delay 点,可以让流程读取 JSON: ```cpp DelayLibrary delayLib; para.tdmParams.delay_lib_path = "data/delay.json"; Routing router( finest, die_parts, fpgas, fpgas.die_connections, para.partitionParams.timing.extra_delay_cut, para.partitionParams.timing.extra_delay_tdm, para.partitionParams.timing.extra_delay_cut_die, para.routing_mode, para.partitionParams.timing.mul_clock_attr, para.has_die, para.routingParams.routing_cost_factor, simpleTimings, &delayLib); // 不要在这里手工设置 router.delayLibReady。 routing_flow::routingFlow( finest, parts, die_parts, fpgas, cutweights, para, router, pFlow, &delayLib, cutConnections); if (!router.delayLibReady) { throw runtime_error("failed to load delay library"); } tdm::executeTDM(router, fpgas, para, delayLib); ``` 这里 `routingFlow()` 负责打开文件、解析为 `nlohmann::json`、调用 `buildFromJson()` 并设置 `delayLibReady`。如果直接从 TDM 入口开始,`executeTDM()` 也会在该标志为 `false` 时尝试相同的加载流程。 函数没有返回值。标准流程会写出 TDM 报告,配置、容量或合法化失败信息通过日志输出。 --- ### 五、执行流程说明 ```text Routing 结果 | v 构造时序边和容量组 | v 连续分配 TDM ratio | v 映射到离散档位并选择 GIO/MGT | v 检查物理通道合法性 | v 重新计算时序并导出结果 ``` 连续阶段使用拉格朗日松弛在共享容量约束下分配 ratio;离散档位和物理通道检查在后续阶段完成。更详细的推导和选择原因见 [TDM 模块技术手册](tdm_guide.html)。 --- ### 六、输出与结果读取 | 输出 | 主要内容 | | --- | --- | | `tdmDelayReport.json` | 物理连接、链路类型、ratio、延迟、channel 和 slot | | `routing.out` | 每个线网的 source、sinks、物理路径、ratio 和延迟 | | `pair_tdm_stats.txt` | 条件生成的连接对容量和类型统计 | | `cutTimingPathInfo.txt` | 条件生成的 TDM 前后时序信息 | 建议至少检查: 1. ratio 是否属于延迟库提供的离散 choices; 2. 每个方向是否满足容量约束; 3. GIO channel、socket/root 和 MGT lane/clock 是否可合法装箱; 4. 最终离散延迟下的 WNS 是否符合要求。 --- ### 七、多层级平台 TDM 直接复用 Routing 使用的展平平台和 `fpga_hierarchy_paths`。它根据源、目的 FPGA 的层级路径选择对应的延迟范围,因此不需要集成层再次转换编号。 如果延迟库没有层级 `scopes`,或平台没有提供层级路径,流程会使用基础延迟模型。多层级模式下仍需遵守同一原则: > **注意:编号不应混用。** TDM 输入中的物理节点、容量矩阵和层级路径必须与 Routing 的展平编号一致。 --- ### 八、常见问题 #### Q:为什么连续容量可行,离散结果仍然失败? 连续解只满足共享容量约束;最终结果还必须落在延迟库的离散档位中,并满足 GIO/MGT、socket/root、lane/clock 等物理限制。 #### Q:为什么采用拉格朗日松弛? 它可以把共享容量约束转化为容量价格,使各容量组能够并行更新,同时让关键时序边获得更合适的 ratio。连续解还可以为后续离散化提供较好的起点。 #### Q:为什么 Routing 与 TDM 的延迟不一致? 确认两个模块使用同一个 `DelayLibrary`、相同的 `gio_channel_grouping_capacity`,并检查 `router.delayLibReady`。多层级平台还要确认层级路径已传入。 --- ### 九、模块依赖 | 依赖 | 用途 | | --- | --- | | Routing 输出 | 提供布线树、方向负载和割线时序路径 | | Partition 平台数据 | 提供 FPGA/Die 编号、拓扑和容量 | | TBB、spdlog | 并行执行和日志输出 | | nlohmann/json | 延迟库与结果文件 |