FPGA实践者
最后提一点:文档和回归。设计验证平台时要考虑可维护性,所有测试用例和覆盖点都要有文档记录。建立自动化的回归测试,每次代码更新都要跑一遍核心测试集。重点验证的功能点要对应到具体的测试用例,确保追踪性。这样整个验证过程才是完整和可信的。
最后提一点:文档和回归。设计验证平台时要考虑可维护性,所有测试用例和覆盖点都要有文档记录。建立自动化的回归测试,每次代码更新都要跑一遍核心测试集。重点验证的功能点要对应到具体的测试用例,确保追踪性。这样整个验证过程才是完整和可信的。
如果时间允许,我会考虑做硬件/软件协同验证。用 C/C++ 写一个驱动,在 FPGA 原型或者仿真器上跑真实的神经网络模型(比如一个小型的 MNIST 分类网络),看最终分类结果是否正确。这是最贴近实际应用的验证,能发现一些纯粹模块级验证发现不了的集成问题。当然,这通常是后期才做的。
从面试角度,你可以谈谈如何应对挑战。比如,可以说“面对巨大输入空间,我会优先验证最常用的配置组合(比如 3x3 conv, stride 1, padding same),再通过约束随机覆盖长尾组合。同时,我会设计断言(assertions)在仿真中实时捕捉错误,比如检查输出数据是否在理论范围内。” 这样既展示了方法,也体现了你的实战思维。
对于溢出/饱和处理,设计测试用例时可以分三步:1. 正常范围数据,确保功能正确。2. 边界数据,比如输入是最大值,权重也是最大值,累加后刚好在饱和边界。3. 超范围数据,强制溢出,检查饱和逻辑是否按规格执行(比如是饱和到最大值还是绕回)。验证平台里,checker 要根据不同的数据位宽和饱和配置来调整比对策略。这点很重要。
别忘了验证可测试性设计(DFT)和初始化。加速器上电后的初始状态是什么?寄存器复位值是否正确?有没有扫描链?这些虽然偏后端,但验证工程师也要考虑。功能点方面,再提一个:多上下文切换。如果加速器支持处理多个不同的神经网络层,那么切换上下文(配置和数据)时不能出错。测试用例就要模拟这种快速切换的场景。覆盖点可以加一个上下文切换的覆盖组。
我觉得可以更关注于‘如何高效验证’。输入空间大,所以要用智能的随机约束。比如,卷积参数不是完全独立随机的,padding 太大可能让输出尺寸为负,这种无效组合要在约束里排除。验证平台里最好能实时监控覆盖率,并动态调整随机权重(如果支持)。重点功能点:除了计算,还有数据预处理和后处理(如果需要),比如数据对齐、格式打包。测试用例可以分层次:单元测试(验单个卷积核)、集成测试(验整个数据流)、系统测试(配合 CPU/DDR 验)。
分享个经验:验证这种模块,数据比对是个挑战。因为中间结果可能很多,或者输出是经过量化/截断的。我们的做法是在 reference model 里模拟整个数据流,包括量化舍入和饱和逻辑,然后与 DUT 的最终输出比对。对于 INT8,要特别注意累加器(通常是更高位宽,比如 INT32)的溢出和后续的饱和到 INT8 是否正确。测试用例要专门设计让累加和超过 INT32 范围的数据(虽然难,但可以构造)。覆盖点一定要包括“饱和事件”是否被触发。
从场景题回答技巧看,要展现结构化思维。我会这样答:第一步,理解规格,列出所有需要验证的特性(Feature List)。第二步,根据特性制定验证计划,包括环境架构、测试用例分类、覆盖模型。第三步,搭建环境,重点实现可重用的 sequence(用于生成不同参数的数据)、reference model 和 scoreboard。第四步,执行仿真,分析覆盖,查漏补缺。重点验证功能点:1. 标准功能(各种卷积池化)。2. 配置和错误处理。3. 性能指标(吞吐、延迟)。4. 功耗相关特性(如果有)。测试用例就围绕这些点展开,随机为主,定向为辅。
我可能会先问面试官这个加速器的架构细节,因为不同架构验证重点不同。比如是脉动阵列还是向量处理器?数据是逐层处理还是全流水?了解清楚后再谈验证计划。通用重点:计算单元功能、数据搬运、控制流。测试用例设计上,采用灰盒验证,利用一些内部可观测点(比如 FIFO 深度、状态机状态)来指导测试生成。比如当看到 FIFO 快满时,就施加背压,验证流控机制。覆盖点除了功能,还要考虑性能覆盖,比如是否覆盖了各种数据复用模式下的时序场景。
补充一个角度:形式验证。对于这种控制逻辑相对规整,但数据路径复杂的模块,可以结合形式验证和仿真。用形式验证来证明控制逻辑(比如状态机、FIFO 控制)永远不会死锁或出错。用仿真来验证数据路径的计算正确性。这样分工,效率更高。验证功能点方面,形式验证可以覆盖所有可能的控制流序列,这是仿真很难穷尽的。仿真的重点就放在数据计算和边界值上。
简单直接点说。环境:UVM,或者用 cocotb 搭个 Python 环境也行。重点验:1. 单个操作的正确性(卷积、池化)。2. 多个操作组合起来的正确性(比如 conv+relu+pool)。3. 数据精度和溢出处理,这是最容易出 bug 的地方。4. 内存一致性,确保读写的地址和数据没错。测试用例:多随机,但也要手写一些关键的,比如全零数据、全最大/最小值数据、随机但可预测模式的数据(比如递增数列)。覆盖点:把规格书里的每个可配置参数都作为覆盖点,还要覆盖数据路径上的关键信号(比如累加器溢出标志位)。
我觉得关键点在于如何管理巨大的输入空间。一个实用方法是使用覆盖率驱动验证(CDV)。定义清晰的覆盖组:比如 cov_data_type: INT8, FP16; cov_conv_params: kernel sizes (1x1, 3x3, 5x5...), strides (1,2), paddings (valid, same, custom); cov_corner_cases: zero_input, all_ones, max/min_values。然后在 test 里用约束随机去 hit 这些覆盖点。验证平台里 checker 要设计得灵活,对于 FP16 需要比较浮点误差是否在可接受范围内(比如相对误差小于 1e-3)。另外,不要忘了验证配置接口,比如动态重配置(在运行中改变卷积参数)是否工作正常。