EDIF_Part 模块集成指南

本页集中说明 Partition、Routing 与 TDM 三个模块的集成方式。三个模块按执行顺序组织为同级章节:

  • Partition:构造超图与平台,生成 FPGA/Die 划分结果;

  • Routing:补齐割线、端口、时序和容量数据,生成物理布线树;

  • TDM:在布线树上执行连续优化、离散化和物理通道合法化。

各模块算法细节见独立技术手册;issues 页面仅收录 Partition 模块的问题记录。


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>

所有节点

每个 node 设置 weightresources(资源用量向量,如 [LUT数, FF数, DSP数, BRAM数]

nets

vector<net>

所有超边(线网)

每个 net 设置 weight(权重,默认 1.0)

incident_nodes

vector<int>

超边的端点展平数组

按顺序拼接每条 net 连接的 node ID,通过 nets[i].beginnets[i].size 索引

incident_nets

vector<int>

节点的关联超边展平数组

按顺序拼接每个 node 连接的 net ID,通过 nodes[i].beginnodes[i].size 索引

fixed_assign

vector<int>

固定分配(可选)

初始化为全 -1;若节点 i 必须分配到特定硬件块,设 fixed_assign[i] = 块号

candidates

vector<set<int>>

区域约束(可选)

若节点 i 只能放在某些块中,设 candidates[i] = {允许的块号集合}

community

vector<int>

初始划分提示(可选)

如果有初始分配建议,设 community[i] = 建议块号

构造示例(3 个节点、2 条超边的简单图):

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<VectorXi>

每个硬件块的资源容量

resources[i] 是第 i 个块的容量向量,维度与 node.resources 一致

topology

vector<vector<int>>

连通性矩阵

topology[i][j] 非零表示块 i 和块 j 直连

dist

vector<vector<int>>

最短跳数矩阵

dist[i][j] 是块 i 到块 j 的最少跳数

maxDist

vector<int>

每个块允许的最大跳数

dist 行最大值推导

upper_resources

vector<VectorXi>

资源上限(可选)

启用 bound_constraint 时需要填

lower_resources

VectorXi

资源下限(可选)

启用 bound_constraint 时需要填

构造示例(4 个硬件块、环形拓扑):

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 函数签名

int partition(
    graph &finest,                              // [输入] 你的网表
    fpga &fpgas,                                 // [输入] 硬件平台
    vector<int> &parts,                          // [输出] 划分结果 ★核心输出★
    PartitionParams &para,                       // [输入/输出] 算法参数
    flat_hash_map<string, fixInfo> &fixed_assignment,  // [输入] 固定分配约束
    unordered_map<string, vector<string>> &group_assignment,  // [输入] 组约束
    flat_hash_map<string, int> &name_map,        // [输入] 名称到节点ID的映射
    shared_ptr<timingEvaluator> &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:最简场景(纯图分割,无时序,无约束)

#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<int> parts;                                     // 输出:划分结果
flat_hash_map<string, fixInfo> fixed_assignment;       // 空:无固定约束
unordered_map<string, vector<string>> group_assignment; // 空:无组约束
flat_hash_map<string, int> name_map;                   // 空:不需要名字映射
shared_ptr<timingEvaluator> 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:有固定分配约束

某些节点必须放在特定硬件块上,不能被移动:

// 假设节点名叫 "cpu_core_0",必须放在块 2
flat_hash_map<string, fixInfo> fixed_assignment;
name_map["cpu_core_0"] = 42;              // 名字到 ID 的映射
fixed_assignment["cpu_core_0"] = {2, {}}; // fpgaNo=2 表示固定到块 2

para.has_fix = true;  // 必须开启这个开关

示例 3:有区域约束

某些节点只能在指定的几个块中选择:

// 节点 "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:有组约束

某些节点必须被分到同一个块:

// "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:使用时序数据

// 声明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.resourcesfpga.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_topohas_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: 如何验证划分结果的正确性?

// 计算割边权重(越小越好)
vector<vector<int>> 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 模块技术手册

一、30 秒速览:最小调用流程

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 前确认相关开关

simpleTimingspFlow

时序模式必填

通常由 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_fpgafpga_hierarchy_paths

DelayLibrary delayLib

必须提供对象

直接声明 DelayLibrary delayLib;设置 delay_lib_path 后由 routingFlow() 自动加载,也可解析 JSON 后调用 buildFromJson()

cut_netsroute_treescut_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 的路径可以这样填写:

vector<SimpleTiming> simpleTimings;
simpleTimings.emplace_back(
    vector<int>{0, 1, 2},      // path_node:从 source 到 sink 的 graph node ID
    vector<int>{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

  • slackperiod 虽然是字符串,内容仍应是可解析的纳秒数值;

  • 非时序模式直接传空的 vector<SimpleTiming>

已有 pFlow 时,推荐调用 HSFullTiming::HSCutNetFlow::buildFrom(pFlow, parts),再用 convertCutNetPathsToSimpleTimings() 自动生成割线网级路径,不必手填。

4.2 构造可选的 CutConnection

CutConnection 主要为 routing_cut_info.json 提供分区端口和网表连接信息,不负责替代 partscutweights。不需要稳定端口名时直接传空容器。

下面表示从物理分区 0 的 data_out 连接到物理分区 1 的 data_in

vector<CutConnection> 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_partto_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() 的完整示例:

DelayLibrary makeManualDelayLibrary()
{
    DelayLibrary lib;

    // 与 data/delay.json 一致:每个 ratio 都有一个对应的 delay(ns)。
    const vector<double> 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<double> 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<double> mgt_ratio = {62, 124, 248, 496, 992};
    const vector<double> 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<int>(ratio));
    for (double ratio : mgt_ratio)
        lib.mgt_meta.choices.push_back(static_cast<int>(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 后显式设置:

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 的子集,使离散档位能直接命中实测点;

  • rminrmax 覆盖全部 choices,但不要超出硬件支持范围。

方法二:使用 buildFromJson()

data/delay.json 采用最简格式:根对象只包含 giomgt,每个 points 元素都是 [ratio, delay_ns]

{
  "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,它本身不负责打开文件。读取当前文件的完整代码如下:

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. 检查根对象中是否同时存在可用的 giomgt

  2. 将每个二元数组的第一个数解释为 ratio,第二个数解释为 delay(ns);

  3. 对 ratio 排序、合并重复点,并构造 PWL 模型与连续斜率;

  4. 以首尾采样 ratio 推导 min_ratiomax_ratio

  5. 将全部采样 ratio 推导为离散 choices

  6. 将 GIO 的最小采样点 [2, 4.57] 推导为 bypass;

  7. 建立可用的线性回退模型,并解析可选的多层级 scopes

delay_detail.json 中的对象形式采样点也受支持,例如 {"ratio": 8, "delay_ns": 58};根级 bypass 也会被读取。当前 buildFromJson() 不读取 fitfallback_linearlow_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:

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<int> parts = {0, 1, 1};

// 无 Die 模式可传空;这里显式填写,便于统一节点映射。
vector<pair<int, int>> 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<vector<int>> cutweights;
fpgas.computeCutWeightsOld(finest, parts, cutweights);

vector<SimpleTiming> simpleTimings;
simpleTimings.emplace_back(
    vector<int>{0, 1, 2}, vector<int>{0, 1},
    "-0.25", "10.0", "clk_main", 0);

vector<CutConnection> 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:

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<int> parts = {0, 0, 1};
vector<pair<int, int>> 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<vector<int>> cutweights;
fpgas.computeCutWeightsOld(finest, parts, cutweights);

vector<SimpleTiming> simpleTimings;
simpleTimings.emplace_back(
    vector<int>{0, 1, 2}, vector<int>{0, 1},
    "-0.25", "10.0", "clk_main", 0);

vector<CutConnection> cutConnections;
DelayLibrary delayLib = makeManualDelayLibrary();

实际工程中通常调用 partDie() 生成 die_parts,并由 FIT/socket 解析流程生成 die_connectionsfailed_channels 中填写不可用的物理通道编号;可用通道数按 total_channels - failed_channels.size() 计算。

4.6 示例三:多层级模式

假设 hierarchy_root 已由平台解析流程构造,node_paths[i] 是节点 i 的叶级层次路径:

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<vector<int>, 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<int> 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<pair<int, int>> die_parts(parts.size());
for (size_t i = 0; i < parts.size(); ++i) {
    die_parts[i] = {parts[i], 0};
}

vector<vector<int>> cutweights;
fpgas.computeCutWeightsOld(finest, parts, cutweights);

vector<SimpleTiming> simpleTimings;
// 按 4.1 节填写;其中仍使用 finest 的 node/net ID。

vector<CutConnection> cutConnections;
// 若需要端口信息,from_part/to_part 使用 path_to_fpga 的全局 ID。

DelayLibrary delayLib = makeManualDelayLibrary();

如果需要层级延迟,可以在手工基础库上为不同范围增加固定偏移:

void setScopeOffset(DelayLibrary &lib, DelayScope scope, double offset_ns)
{
    auto &dst = lib.scoped_models[static_cast<int>(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 中任意一组输入后,调用方式相同:

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 引用 finestdie_partsfpgasDelayLibrary,这些对象必须至少存活到 TDM 执行结束。


五、执行流程说明

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。

注意:编号不应混用。 partsdie_partscutweightsrouting_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 输出

提供 partsdie_parts 和割线统计

Eigen3、TBB

数据计算与并行执行

spdlog

日志输出

nlohmann/json

延迟库和结果 JSON


TDM 模块集成指南

本指南面向已经完成 Routing、需要继续分配时分复用比例和物理通道的开发者。阅读完本文后,你可以:

  • 知道调哪个函数tdm::executeTDM()

  • 知道需要提供哪些 Routing 结果和参数

  • 知道从哪些文件读取最终 TDM 结果

所有 API 的详细参数说明见 src/tdm/tdm.hsrc/tdm/delay_lib.h 中的注释;算法细节见 TDM 模块技术手册

一、30 秒速览:最小调用流程

1. 完成 routing_flow::routingFlow()
2. 确认 Routing 的布线树、容量和延迟库有效
3. 设置 TDMParams
4. 调用 tdm::executeTDM()
5. 从 tdmDelayReport.json 和 routing.out 读取结果

二、你需要准备的四个输入

输入

必填性

怎么准备

Routing router

必须

先按上一节构造 Routing,再调用 routing_flow::routingFlow();该函数会填充 cut_netsroute_treescut_mat

fpga fpgas

必须

直接复用 Routing 使用的平台对象;多层级流程使用 flattenHierarchyToFpgasForRouting() 生成的 routing_fpgas

params para

必须

直接复用 Routing 的顶层配置对象,并在调用前确认 tdmParamshas_die 和时序开关

DelayLibrary delayLib

必须

在 Routing 前声明一次并传给两个模块;routingFlow()executeTDM() 会按 delay_lib_path 自动加载,也可调用 buildFromJson() 预加载

timing_edgestdm_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() 的四个参数在三种平台模式下相同,区别在于 routerfpgas 中保存的编号、物理连接与层级信息。不要手工构造内部 Tdmtiming_edgestdm_groups

4.1 函数签名

void executeTDM(
    Routing &router,
    const fpga &fpgas,
    const params &para,
    DelayLibrary &delayLib);

4.2 示例一:无 Die 模式

接续 Routing 4.4 和 4.7 的 routerfpgasdelayLib

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_treesrouter.cut_netsrouter.cut_mat 自动建立容量组。partsdie_parts 不需要再次传入。

4.3 示例二:有 Die 模式

接续 Routing 4.5 和 4.7。只要 para.has_dierouter.die_partsfpgas.dies_per_fpgafpgas.die_connections 已经一致,调用本身没有变化:

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

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=truefpga_hierarchy_paths 非空时,Routing 和 TDM 会自动按 BoardInterBoardInterClusterInterRack 选择延迟模型。

4.5 使用文件自动加载延迟库

如果不希望在 C++ 中填写 ratio–delay 点,可以让流程读取 JSON:

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 报告,配置、容量或合法化失败信息通过日志输出。


五、执行流程说明

Routing 结果
    |
    v
构造时序边和容量组
    |
    v
连续分配 TDM ratio
    |
    v
映射到离散档位并选择 GIO/MGT
    |
    v
检查物理通道合法性
    |
    v
重新计算时序并导出结果

连续阶段使用拉格朗日松弛在共享容量约束下分配 ratio;离散档位和物理通道检查在后续阶段完成。更详细的推导和选择原因见 TDM 模块技术手册


六、输出与结果读取

输出

主要内容

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

延迟库与结果文件