FPGA仿真如何极致压榨CPU性能

描述

以下文章来源于AdriftCoreFPGA芯研社,作者CNL中子

对于许多FPGA/IC工程师而言,设计实现游刃有余,验证仿真却常成短板——传统验证方法面临两难困局:学习UVM需投入大量时间成本,而纯Verilog自仿又会陷入重复造轮子的低效循环。以通信协议仿真为例,仅报文解析就需要重写整套解析逻辑,相当于用Verilog再实现一次协议栈,耗时费力。
此时,Python的生态优势便锋芒尽显。其丰富的字符串处理库可直接解析报文,配合Cocotb框架,仅需少量Python代码即可构建高效测试平台,将验证工作量压缩70%以上。Cocotb的独特价值正在于此:用Python解放验证生产力,让工程师专注于设计创新而非重复劳动

• 前言

• cocotb+pytest

• 开源cocotb仿真环境介绍

前言

在日常开发中,我们的仿真一般是串行执行的。如果想批量做回归测试,就会明显感受到速度之慢:当我们焦急等待仿真结果时,CPU 却在“摸鱼”,只有一个核心在跑,其余核心几乎闲置。这时难免会想——我花了几千块钱搭的配置,不是让CPU在这里摸鱼的

能不能把 CPU 的每一个核心都压榨起来呢?

答案是肯定的。借助 cocotb + pytest,我们可以轻松实现多核并行仿真,最大化利用 CPU 资源。

如下图所示,图一为普通串行仿真,共 9 个 case,耗时 358.90 s;而使用 6 核并行仿真,仅用 117.03 s,速度提升足足 3 倍。乍看只是节省了 4 分钟,但这还只是一个简单的测试用例集。若你的用例规模很大,多核可能只需 1 小时,而单核却要 3 小时甚至更久,可见效率差距巨大。

下面我将介绍如何充分压榨 CPU 的性能,并顺带介绍一下本公众号开源的 AFX-Cocotb-Pytest 库,让你无需繁琐配置即可开箱即用,实现真正意义上的高效并行仿真。

VerilogVerilog

介绍

pytest-xdist

pytest-xdist 是 pytest 的增强插件,通过 并行执行测试用例 与 分布式运行测试 来显著加速整个测试流程。

借助多进程并行、任务负载均衡以及跨机器分布式执行,它能够最大化利用 CPU 资源,特别适用于包含大量测试的大型工程。

如果希望在仿真环境中充分压榨多核性能,就需要在我们的 cocotb 测试框架中引入并配置 pytest-xdist,以实现高效的并行仿真执行。

安装插件

 

pip install pytest-xdist

 

并行运行所有测试

 

pytest -n auto

 

-n auto 会自动根据 CPU 核心数分配 worker 数量。

你也可以手动指定 worker 数:

 

pytest -n 8

 

cocotb+pytest

pytest

在 cocotb 进阶专栏的《用 Python 给 Verilog 设计自仿进阶(二):实现仿真平台 Linux/Win 多平台兼容》以及《用 Python 给 Verilog 设计自仿进阶(三):FPGA 仿真,如何兼容 VCS、Iverilog、Modelsim 等多平台仿真器》中,我们已经借助 pytest 实现了跨系统、跨仿真器的批量自动化仿真。

然而,这些批量仿真仍未真正发挥出多核 CPU 的计算潜力。接下来,我将介绍如何将 pytest-xdist 融入现有 pytest 测试体系,从而实现更高效的并行仿真加速。

pytest-xdist

要实现多核并行仿真,关键在于正确管理仿真过程中使用的文件资源。在单核环境下,所有测试用例共用同一套文件进行仿真,彼此之间不存在竞争关系,因此不会发生读写冲突。然而,当启用多核并行时,多个 worker 可能会同时对同一文件进行增删查改,从而导致仿真过程出现竞争条件,最终引发报告错误。

对于 Verilog 仿真而言,最核心的文件包括 filelist 和 sim_build,其中 sim_build 是仿真的实际工作目录。因此,为了保证并行仿真的独立性,我们需要根据 worker 的 ID(即每个 CPU 核心对应的 worker 进程)为它们分别创建并访问独立的 filelist 和 sim_build,从而避免任何文件级冲突。

首先获取当前worker进程的id

 

worker_id = os.getenv("PYTEST_XDIST_WORKER", "gw0")

 

filelist

基于 worker ID 创建独立的 filelist 时,这里的 filelist 是通过 指定 RTL 根目录,由 Python 自动遍历所有 .v  .sv 文件生成的。随后,我们根据生成的 filelist 获取文件路径,并将其传递给 cocotb_run 的 verilog_sources。
这样做的好处在于:

1. 无需手写 filelist
只需将 RTL 文件放入指定目录,即可自动收集所有 Verilog/SV 模块,降低维护成本。

2. 生成的 filelist 可直接供波形工具使用
例如 VCS、ModelSim、Verdi 等工具可以利用 filelist 实现源码追踪(trace),无需重复配置路径。

3. 并行仿真时便于为每个 worker 创建独立 filelist
避免文件竞争问题。

如果你的项目不是通过 filelist 动态生成 verilog_sources,而是手动指定 RTL 文件列表,那么你可以忽略这一机制。

 

filelist_path = Path(filelist+worker_id+".f").resolve()

 

sim_build

在并行仿真中,每个 worker 需要使用独立的仿真工作目录(sim_build),以避免多个进程同时读写同一目录而导致文件冲突。基于 worker ID 创建独立的 sim_build 可以这样实现:

 

sim_build = os.path.join(sim_path, "sim_build", f"{safe_test_name(request.node.name)}_{worker_id}")

 

随后,将生成的 sim_build 传递给 cocotb_run 即可让每个 worker 使用独立的仿真空间:

 

cocotb_test.simulator.run(
    python_search=[str(tb_files)],
    verilog_sources=verilog_sources,
    toplevel=toplevel,
    module=module,
    compile_args=compile_args,
    sim_args=sim_args,
    parameters=parameters,
    sim_build=sim_build,
    extra_env=extra_env,
    waves=waves
)

 

这样即可确保每个并行任务都在自己的仿真目录中独立运行,避免竞争条件,提高并行执行的稳定性。

开源cocotb仿真环境介绍

当然,如果你觉得上述配置流程较为繁琐,也可以直接使用我提供的 afx_cocotb_test_run 库。该库的目标是简化 pytest + cocotb 仿真环境的代码编写与配置复杂度
只需将该文件放在你的 Python 仿真脚本所在目录,并直接调用即可完成仿真运行。

目前该库已支持 VCS 和 Icarus 等主流仿真器。如果你需要支持其他仿真器,也可以参考我提供的 VCS 与 Icarus 示例进行扩展,非常方便。

 

from afx_cocotb_test_run import *

 

在自己的Python代码末尾加上类似下面代码的模板即可

 

################################################################### RUN TEST ###################################################################
import os
import pytest

@pytest.mark.parametrize("cycle", [10,30,40,80])
@pytest.mark.parametrize("a", [3,5])
def test_run(request,cycle,a):
    simulator = os.environ.get("SIM", "")
    waves = os.environ.get("WAVES", "")

    parameters = {k.upper(): v for k, v in request.node.callspec.params.items()}
    afx_test_run(request,ctb="cocotb_top",tc="tb_top",wave=waves,sim=simulator,parameters=parameters)

 

在使用 afx_cocotb_test_run 进行仿真时,我们可以通过 afx_test_run 提供的多种参数灵活配置整个运行环境。
这些参数覆盖了仿真器选择、波形开关、测试平台路径、RTL 文件路径以及可选的参数传递等内容,使得 cocotb + pytest 的仿真流程高度可控、易扩展。

以下是 afx_test_run 支持的主要配置项:

 

sim: str = "vcs",                # 指定使用的仿真器,如 "vcs" 或 "icarus"
wave: str = "1",                 # 波形开关,1 表示生成波形,0 表示关闭
ctb: str = "cocotb_top",         # cocotb Python 顶层模块名
tc: str = "tb_top",              # Verilog/SystemVerilog 顶层 testbench 模块名
filelist: str = '../sim/filelist',  # 自动生成或指定的 filelist 路径
tests_dir: str = '../sim/',      # 工作空间路径
include_list: str = '../../design/incl',  # include 头文件目录
tb_list: str = '../../verify/tb',         # Verilog/SV/Python testbench文件路径
pkg: str = '../../design/pkg',            # package 所在目录
rtl: str = '../../design/rtl',            # RTL 源码目录
sim_ip: str = '../ip',                    # 仿真依赖的 IP 文件目录
parameters: dict[str, int] = {}           # Verilog/SystemVerilog 顶层的可配置参数

 

这些参数的设计使得用户可以根据自己的工程结构轻松适配,无需修改大量路径或脚本,即可快速构建 cocotb 的跨平台仿真环境,同时让 pytest-xdist 并行加速的配置更自然、易于集成。

以上述模板为例,我们将仿真器类型和是否生成波形配置为环境变量,在使用 pytest 时可以灵活指定。

 

WAVE=1 SIM=vcs pytest cocotb_top.py 

 

测试用例参数

同时,我们可以传入 RTL 顶层参数(如 parameter A)以及测试环境参数(如表示仿真时钟频率的 cycle)。

对于测试环境参数,需要特别注意:cycle 并不是直接传入 RTL,而是作为测试用例的参数,用于控制仿真时的行为,例如仿真时钟频率、数据流中反压的概率,或者其他与特定测试场景相关的设置。

例如,如果我们在测试用例中使用:

 

# @pytest.mark.parametrize("a", [3,5])
@pytest.mark.parametrize("cycle", [10,20])
@pytest.mark.parametrize("press", [0.3,0.8])
def test_run(request,cycle,press):
    parameters = {k.upper(): v for k, v in request.node.callspec.params.items()}
    afx_test_run(request,ctb="cocotb_top",tc="tb_top",wave=waves,sim=simulator,parameters=parameters)

 

则会生成 4 个不同的测试 case:

1. 时钟频率 10,反压概率 0.8

2. 时钟频率 10,反压概率 0.3

3. 时钟频率 20,反压概率 0.8

4. 时钟频率 20,反压概率 0.3

在 Python 脚本中,需要配合从环境变量获取参数,例如:

 

@cocotb.test()
async def dff_simple_test(dut):
    cycle = int(os.getenv("PARAM_CYCLE", "20"))
    press = float(os.getenv("PARAM_PRESS", "0.3"))
    # clock
    await cocotb.start(generate_clock(dut,cycle,"ns",0))

 

这样就可以保证测试用例能够根据不同参数自动生成,并与仿真环境动态关联。

波形查看

在Python仿真文件夹下输入命令即可仿真:

 

WAVES=1 pytest cocotb_top.py -v -s -n auto

 

在sim_build产生四个仿真结果

 

AdriftCore@AdriftCore:/sim_build$ ls
test_run_0_3_10_gw0  test_run_0_3_20_gw1  test_run_0_8_10_gw2  test_run_0_8_20_gw3

 

每个仿真结果都会有其对应的波形

 

AdriftCore@AdriftCore:/sim_build/test_run_0_3_10_gw0$ ls
cocotb_top  cocotb_top.daidir  cocotb_top_ucli.do  com.log  Makefile  novas_dump.log  pli.tab  simulate.log  tb_top.fsdb  ucli.key  um8pni2k_results.xml  vcs_lib

 

链接

AFX-CocotbSIMENV 的功能不仅限于 pytest 的并行仿真,这只是其中的一部分。由于篇幅所限,这里无法一一展开说明。后续版本中,AFX-CocotbSIMENV 会将实际仿真中常用的功能封装为函数或方法集成到库中,从而减少重复操作,提高仿真效率。

 

https://github.com/AdriftXCore/CocotbSIMENV

https://github.com/AdriftXCore/CocotbSIMENV/blob/master/verify/tb/afx_cocotb_test_run.py

 

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分