Transformers 文档
测试
并获得增强的文档体验
开始使用
测试
让我们来看看 🤗 Transformers 模型是如何进行测试的,以及如何编写新的测试和改进现有测试。
仓库中有 2 个测试套件:
tests
— 通用 API 的测试examples
— 主要用于不属于 API 的各种应用程序的测试
Transformers 如何进行测试
一旦提交 PR,它将通过 9 个 CircleCi 任务进行测试。该 PR 的每次新提交都会重新测试。这些任务在此配置文件中定义,因此如果需要,您可以在自己的机器上重现相同的环境。
这些 CI 任务不运行
@slow
测试。有 3 个任务由 github actions 运行
torch hub 集成:检查 torch hub 集成是否有效。
自托管(推送):仅在
main
分支的提交上运行 GPU 上的快速测试。仅当main
分支上的提交更新了以下文件夹之一中的代码时才会运行:src
、tests
、.github
(以防止在添加的模型卡、笔记本等上运行)自托管运行器:在
tests
和examples
中运行 GPU 上的正常测试和慢速测试
RUN_SLOW=1 pytest tests/ RUN_SLOW=1 pytest examples/
结果可以在这里查看。
运行测试
选择要运行的测试
本文档详细介绍了如何运行测试。如果阅读完所有内容后,您需要更多详细信息,您可以在这里找到它们。
以下是一些最常用的运行测试的方法。
全部运行
pytest
或者
make test
请注意,后者定义为
python -m pytest -n auto --dist=loadfile -s -v ./tests/
这告诉 pytest:
- 运行与 CPU 核心数相同的测试进程(如果您没有大量 RAM,这可能会太多!)
- 确保同一文件中的所有测试都将由同一测试进程运行
- 不捕获输出
- 在详细模式下运行
获取所有测试列表
测试套件的所有测试
pytest --collect-only -q
给定测试文件的所有测试
pytest tests/test_optimization.py --collect-only -q
运行特定的测试模块
要运行单个测试模块
pytest tests/utils/test_logging.py
运行特定测试
由于大多数测试中都使用了 unittest,因此要运行特定的子测试,您需要知道包含这些测试的 unittest 类的名称。例如,它可以是
pytest tests/test_optimization.py::OptimizationTest::test_adam_w
这里
tests/test_optimization.py
- 包含测试的文件OptimizationTest
- 类的名称test_adam_w
- 特定测试函数的名称
如果文件包含多个类,您可以选择仅运行给定类的测试。例如
pytest tests/test_optimization.py::OptimizationTest
将运行该类中的所有测试。
如前所述,您可以通过运行以下命令查看 OptimizationTest
类中包含哪些测试:
pytest tests/test_optimization.py::OptimizationTest --collect-only -q
您可以通过关键字表达式运行测试。
要仅运行名称中包含 adam
的测试:
pytest -k adam tests/test_optimization.py
逻辑 and
和 or
可用于指示所有关键字是否应匹配或其中一个。not
可用于否定。
要运行所有测试,但名称中包含 adam
的测试除外:
pytest -k "not adam" tests/test_optimization.py
您可以将这两个模式组合在一起
pytest -k "ada and not adam" tests/test_optimization.py
例如,要同时运行 test_adafactor
和 test_adam_w
,您可以使用
pytest -k "test_adafactor or test_adam_w" tests/test_optimization.py
请注意,这里我们使用 or
,因为我们希望任一关键字匹配以同时包含两者。
如果您只想包含同时包含两个模式的测试,则应使用 and
pytest -k "test and ada" tests/test_optimization.py
运行 Accelerate 测试
有时您需要在模型上运行 accelerate
测试。为此,您只需在命令中添加 -m accelerate_tests
,例如,如果您想在 OPT
上运行这些测试,请运行
RUN_SLOW=1 pytest -m accelerate_tests tests/models/opt/test_modeling_opt.py
运行文档测试
为了测试文档示例是否正确,您应该检查 doctests
是否通过。例如,让我们使用 WhisperModel.forward
的文档字符串
r"""
Returns:
Example:
```python
>>> import torch
>>> from transformers import WhisperModel, WhisperFeatureExtractor
>>> from datasets import load_dataset
>>> model = WhisperModel.from_pretrained("openai/whisper-base")
>>> feature_extractor = WhisperFeatureExtractor.from_pretrained("openai/whisper-base")
>>> ds = load_dataset("hf-internal-testing/librispeech_asr_dummy", "clean", split="validation")
>>> inputs = feature_extractor(ds[0]["audio"]["array"], return_tensors="pt")
>>> input_features = inputs.input_features
>>> decoder_input_ids = torch.tensor([[1, 1]]) * model.config.decoder_start_token_id
>>> last_hidden_state = model(input_features, decoder_input_ids=decoder_input_ids).last_hidden_state
>>> list(last_hidden_state.shape)
[1, 2, 512]
```"""
只需运行以下行即可自动测试所需文件中的每个文档字符串示例
pytest --doctest-modules <path_to_file_or_dir>
如果文件具有 Markdown 扩展名,则应添加 --doctest-glob="*.md"
参数。
仅运行修改过的测试
您可以使用 pytest-picked 运行与未暂存文件或当前分支(根据 Git)相关的测试。这是一种快速测试您的更改是否破坏任何东西的好方法,因为它不会运行与您未触及的文件相关的测试。
pip install pytest-picked
pytest --picked
所有测试都将从已修改但尚未提交的文件和文件夹中运行。
源文件修改时自动重新运行失败的测试
pytest-xdist 提供了一个非常有用的功能,即检测所有失败的测试,然后等待您修改文件并持续重新运行这些失败的测试,直到它们通过,同时您修复它们。这样您在修复后就不需要重新启动 pytest。这将重复进行,直到所有测试都通过,之后会再次执行完整运行。
pip install pytest-xdist
要进入该模式:pytest -f
或 pytest --looponfail
文件更改通过查看 looponfailroots
根目录及其所有内容(递归)来检测。如果此值的默认设置不适合您,您可以通过在 setup.cfg
中设置配置选项来更改它。
[tool:pytest]
looponfailroots = transformers tests
或 pytest.ini
/tox.ini
文件
[pytest]
looponfailroots = transformers tests
这将导致仅在相应目录中查找文件更改,这些目录相对于 ini 文件目录指定。
pytest-watch 是此功能的另一种实现。
跳过测试模块
如果您想运行所有测试模块,除了少数几个,您可以通过提供要运行的测试的显式列表来排除它们。例如,要运行除 test_modeling_*.py
测试之外的所有测试:
pytest *ls -1 tests/*py | grep -v test_modeling*
清除状态
CI 构建以及隔离重要(与速度相比)时,应清除缓存
pytest --cache-clear tests
并行运行测试
如前所述,make test
通过 pytest-xdist
插件(-n X
参数,例如 -n 2
运行 2 个并行作业)并行运行测试。
pytest-xdist
的 --dist=
选项允许控制测试如何分组。--dist=loadfile
将一个文件中的测试放在同一个进程上。
由于执行测试的顺序不同且不可预测,如果使用 pytest-xdist
运行测试套件导致失败(这意味着我们有一些未检测到的耦合测试),请使用 pytest-replay 以相同的顺序重播测试,这应该有助于将失败序列减少到最小。
测试顺序和重复
重复测试多次,按顺序、随机或按组重复,以检测任何潜在的相互依赖性和状态相关错误(拆除)。而直接的多次重复只是为了检测一些深度学习随机性揭示的问题。
重复测试
pip install pytest-flakefinder
然后多次运行每个测试(默认为 50 次)
pytest --flake-finder --flake-runs=5 tests/test_failing_test.py
此插件不适用于 pytest-xdist
的 -n
标志。
还有另一个插件 pytest-repeat
,但它不适用于 unittest
。
随机顺序运行测试
pip install pytest-random-order
重要提示:pytest-random-order
的存在将自动随机化测试,无需配置更改或命令行选项。
如前所述,这允许检测耦合测试 - 其中一个测试的状态会影响另一个测试的状态。安装 pytest-random-order
后,它将打印该会话使用的随机种子,例如
pytest tests [...] Using --random-order-bucket=module Using --random-order-seed=573663
这样,如果给定特定序列失败,您可以通过添加该精确种子来重现它,例如:
pytest --random-order-seed=573663 [...] Using --random-order-bucket=module Using --random-order-seed=573663
它只有在您使用完全相同的测试列表(或根本不使用列表)时才能重现精确顺序。一旦您开始手动缩小列表范围,您就不能再依赖种子,而必须手动按失败的精确顺序列出它们,并告诉 pytest 不要随机化它们,而是使用 --random-order-bucket=none
,例如:
pytest --random-order-bucket=none tests/test_a.py tests/test_c.py tests/test_b.py
要禁用所有测试的随机化:
pytest --random-order-bucket=none
默认情况下,--random-order-bucket=module
被隐式使用,它将在模块级别打乱文件。它也可以在 class
、package
、global
和 none
级别进行打乱。有关完整详细信息,请参阅其文档。
另一种随机化替代方案是:pytest-randomly
。此模块具有非常相似的功能/界面,但它没有 pytest-random-order
中可用的桶模式。它存在一旦安装就会强制执行的相同问题。
外观变体
pytest-sugar
pytest-sugar 是一个插件,可改善外观,添加进度条,并即时显示失败的测试和断言。安装后它会自动激活。
pip install pytest-sugar
要不使用它运行测试,请运行
pytest -p no:sugar
或卸载它。
报告每个子测试的名称和进度
对于通过 pytest
运行的单个或一组测试(在 pip install pytest-pspec
之后)
pytest --pspec tests/test_optimization.py
即时显示失败的测试
pytest-instafail 可即时显示失败和错误,而无需等待测试会话结束。
pip install pytest-instafail
pytest --instafail
用 GPU 还是不用 GPU
在启用 GPU 的设置中,要在仅限 CPU 的模式下测试,请为 CUDA GPU 添加 CUDA_VISIBLE_DEVICES=""
CUDA_VISIBLE_DEVICES="" pytest tests/utils/test_logging.py
或者,如果您有多个 GPU,您可以指定 pytest
使用哪个 GPU。例如,如果您有 GPU 0
和 1
,要仅使用第二个 GPU,您可以运行
CUDA_VISIBLE_DEVICES="1" pytest tests/utils/test_logging.py
对于 Intel GPU,请在上述示例中使用 ZE_AFFINITY_MASK
而不是 CUDA_VISIBLE_DEVICES
。
当您想在不同的 GPU 上运行不同的任务时,这很方便。
某些测试必须仅在 CPU 上运行,其他测试可以在 CPU、GPU 或 TPU 上运行,还有一些测试可以在多个 GPU 上运行。以下跳过装饰器用于设置 CPU/GPU/XPU/TPU 测试的要求:
require_torch
- 此测试仅在 torch 下运行require_torch_gpu
- 同require_torch
,但需要至少 1 个 GPUrequire_torch_multi_gpu
- 同require_torch
,但需要至少 2 个 GPUrequire_torch_non_multi_gpu
- 同require_torch
,但需要 0 或 1 个 GPUrequire_torch_up_to_2_gpus
- 同require_torch
,但需要 0 或 1 或 2 个 GPUrequire_torch_xla
- 同require_torch
,但需要至少 1 个 TPU
让我们在下表中描述 GPU 要求
N个GPU | 装饰器 |
---|---|
>= 0 | @require_torch |
>= 1 | @require_torch_gpu |
>= 2 | @require_torch_multi_gpu |
< 2 | @require_torch_non_multi_gpu |
< 3 | @require_torch_up_to_2_gpus |
例如,这里有一个测试,它只有在有 2 个或更多 GPU 可用且安装了 pytorch 时才能运行:
@require_torch_multi_gpu
def test_example_with_multi_gpu():
这些装饰器可以堆叠。例如,如果一个测试很慢并且在 pytorch 下需要至少一个 GPU,这里是如何设置它:
@require_torch_gpu
@slow
def test_example_slow_on_gpu():
某些装饰器(如 @parametrized
)会重写测试名称,因此 @require_*
跳过装饰器必须最后列出,才能正常工作。以下是正确用法的示例:
@parameterized.expand(...)
@require_torch_multi_gpu
def test_integration_foo():
这种顺序问题在 @pytest.mark.parametrize
中不存在,您可以将其放在第一位或最后一位,它仍然有效。但它只适用于非单元测试。
在测试内部
- 有多少个 GPU 可用
from transformers.testing_utils import get_gpu_count
n_gpu = get_gpu_count() # works with torch and tf
使用特定 PyTorch 后端或设备进行测试
要在特定 torch 设备上运行测试套件,请添加 TRANSFORMERS_TEST_DEVICE="$device"
,其中 $device
是目标后端。例如,要仅在 CPU 上进行测试:
TRANSFORMERS_TEST_DEVICE="cpu" pytest tests/utils/test_logging.py
此变量对于测试自定义或不常见的 PyTorch 后端(例如 mps
、xpu
或 npu
)很有用。它还可以通过指定特定 GPU 或在仅限 CPU 模式下进行测试来达到与 CUDA_VISIBLE_DEVICES
相同的效果。
某些设备在首次导入 torch
后需要额外的导入。这可以通过环境变量 TRANSFORMERS_TEST_BACKEND
指定。
TRANSFORMERS_TEST_BACKEND="torch_npu" pytest tests/utils/test_logging.py
替代后端可能还需要替换设备特定功能。例如,torch.cuda.manual_seed
可能需要替换为设备特定种子设置器,如 torch.npu.manual_seed
或 torch.xpu.manual_seed
,以便在设备上正确设置随机种子。要在运行测试套件时指定具有后端特定设备功能的新后端,请创建 Python 设备规范文件 spec.py
,其格式如下:
import torch
import torch_npu # for xpu, replace it with `import intel_extension_for_pytorch`
# !! Further additional imports can be added here !!
# Specify the device name (eg. 'cuda', 'cpu', 'npu', 'xpu', 'mps')
DEVICE_NAME = 'npu'
# Specify device-specific backends to dispatch to.
# If not specified, will fallback to 'default' in 'testing_utils.py`
MANUAL_SEED_FN = torch.npu.manual_seed
EMPTY_CACHE_FN = torch.npu.empty_cache
DEVICE_COUNT_FN = torch.npu.device_count
此格式还允许指定所需的任何额外导入。要使用此文件替换测试套件中的等效方法,请将环境变量 TRANSFORMERS_TEST_DEVICE_SPEC
设置为规范文件的路径,例如 TRANSFORMERS_TEST_DEVICE_SPEC=spec.py
。
目前,仅支持 MANUAL_SEED_FN
、EMPTY_CACHE_FN
和 DEVICE_COUNT_FN
进行设备特定分发。
分布式训练
pytest
无法直接处理分布式训练。如果尝试这样做,子进程将无法正确执行,并最终认为它们是 pytest
并开始循环运行测试套件。但是,如果一个进程生成多个 worker 并管理 IO 管道,那么它就可以正常工作。
以下是一些使用它的测试:
要直接跳到执行点,请在这些测试中搜索 execute_subprocess_async
调用。
您需要至少 2 个 GPU 才能看到这些测试的实际效果。
CUDA_VISIBLE_DEVICES=0,1 RUN_SLOW=1 pytest -sv tests/test_trainer_distributed.py
输出捕获
在测试执行期间,任何发送到 stdout
和 stderr
的输出都将被捕获。如果测试或设置方法失败,其相应的捕获输出通常会与失败回溯一起显示。
要禁用输出捕获并正常获取 stdout
和 stderr
,请使用 -s
或 --capture=no
pytest -s tests/utils/test_logging.py
将测试结果发送到 JUnit 格式输出
pytest tests --junitxml=result.xml
颜色控制
无颜色显示(例如,白色背景上的黄色不可读)
pytest --color=no tests/utils/test_logging.py
将测试报告发送到在线 Pastebin 服务
为每次测试失败创建一个 URL
pytest --pastebin=failed tests/utils/test_logging.py
这将把测试运行信息提交到远程 Paste 服务,并为每个失败提供一个 URL。您可以像往常一样选择测试,或者添加例如 -x,如果您只想发送一个特定的失败。
为整个测试会话日志创建一个 URL
pytest --pastebin=all tests/utils/test_logging.py
编写测试
🤗 transformers 测试基于 unittest
,但由 pytest
运行,因此大多数情况下可以使用两个系统的功能。
您可以在此处阅读支持的功能,但需要记住的重要一点是,大多数 pytest
fixture 都不起作用。参数化也不行,但我们使用以类似方式工作的模块 parameterized
。
参数化
通常,需要多次运行相同的测试,但使用不同的参数。这可以在测试内部完成,但这样就无法仅针对一组参数运行该测试。
# test_this1.py
import unittest
from parameterized import parameterized
class TestMathUnitTest(unittest.TestCase):
@parameterized.expand(
[
("negative", -1.5, -2.0),
("integer", 1, 1.0),
("large fraction", 1.6, 1),
]
)
def test_floor(self, name, input, expected):
assert_equal(math.floor(input), expected)
现在,默认情况下,此测试将运行 3 次,每次将 test_floor
的最后 3 个参数分配给参数列表中相应的参数。
你可以只运行 negative
和 integer
参数集,使用:
pytest -k "negative and integer" tests/test_mytest.py
或者所有子测试,除了 negative
,使用:
pytest -k "not negative" tests/test_mytest.py
除了使用前面提到的 -k
过滤器外,您还可以找出每个子测试的精确名称,并使用其精确名称运行其中任何一个或所有这些子测试。
pytest test_this1.py --collect-only -q
它将列出
test_this1.py::TestMathUnitTest::test_floor_0_negative test_this1.py::TestMathUnitTest::test_floor_1_integer test_this1.py::TestMathUnitTest::test_floor_2_large_fraction
现在您只运行 2 个特定子测试:
pytest test_this1.py::TestMathUnitTest::test_floor_0_negative test_this1.py::TestMathUnitTest::test_floor_1_integer
模块 parameterized 已在 transformers
的开发依赖项中,适用于 unittests
和 pytest
测试。
但是,如果测试不是 unittest
,则可以使用 pytest.mark.parametrize
(或者您可能会在某些现有测试中看到它被使用,主要是在 examples
下)。
这里是相同的例子,这次使用 pytest
的 parametrize
标记
# test_this2.py
import pytest
@pytest.mark.parametrize(
"name, input, expected",
[
("negative", -1.5, -2.0),
("integer", 1, 1.0),
("large fraction", 1.6, 1),
],
)
def test_floor(name, input, expected):
assert_equal(math.floor(input), expected)
与 parameterized
一样,使用 pytest.mark.parametrize
可以对要运行的子测试进行精细控制,如果 -k
过滤器不起作用。但是,此参数化函数为子测试创建了一组略有不同的名称。它们看起来像这样:
pytest test_this2.py --collect-only -q
它将列出
test_this2.py::test_floor[integer-1-1.0] test_this2.py::test_floor[negative--1.5--2.0] test_this2.py::test_floor[large fraction-1.6-1]
现在您只需运行特定测试
pytest test_this2.py::test_floor[negative--1.5--2.0] test_this2.py::test_floor[integer-1-1.0]
如前一个例子。
文件和目录
在测试中,我们经常需要知道文件相对于当前测试文件的位置,这并非易事,因为测试可能从多个目录调用,或者可能位于不同深度的子目录中。辅助类 transformers.test_utils.TestCasePlus
通过整理所有基本路径并提供对它们的轻松访问器来解决此问题。
pathlib
对象(全部完全解析)test_file_path
- 当前测试文件路径,即__file__
test_file_dir
- 包含当前测试文件的目录tests_dir
-tests
测试套件的目录examples_dir
-examples
测试套件的目录repo_root_dir
- 仓库的根目录src_dir
-src
目录(即transformers
子目录所在的位置)
字符串化路径——与上述相同,但这些返回路径为字符串,而不是
pathlib
对象test_file_path_str
test_file_dir_str
tests_dir_str
examples_dir_str
repo_root_dir_str
src_dir_str
要开始使用这些,您只需确保测试位于 transformers.test_utils.TestCasePlus
的子类中。例如:
from transformers.testing_utils import TestCasePlus
class PathExampleTest(TestCasePlus):
def test_something_involving_local_locations(self):
data_dir = self.tests_dir / "fixtures/tests_samples/wmt_en_ro"
如果您不需要通过 pathlib
操作路径,或者只需要一个字符串形式的路径,则可以始终对 pathlib
对象调用 str()
,或者使用以 _str
结尾的访问器。例如:
from transformers.testing_utils import TestCasePlus
class PathExampleTest(TestCasePlus):
def test_something_involving_stringified_locations(self):
examples_dir = self.examples_dir_str
临时文件和目录
使用唯一的临时文件和目录对于并行测试运行至关重要,这样测试就不会互相覆盖数据。此外,我们希望在创建它们的每个测试结束时删除临时文件和目录。因此,使用 tempfile
等包来满足这些需求至关重要。
然而,在调试测试时,您需要能够查看临时文件或目录中的内容,并且您想知道它的确切路径,而不是在每次测试重新运行时都将其随机化。
辅助类 transformers.test_utils.TestCasePlus
最适合用于此目的。它是 unittest.TestCase
的子类,因此我们可以在测试模块中轻松地从它继承。
以下是其用法示例
from transformers.testing_utils import TestCasePlus
class ExamplesTests(TestCasePlus):
def test_whatever(self):
tmp_dir = self.get_auto_remove_tmp_dir()
此代码创建一个唯一的临时目录,并将 tmp_dir
设置为其位置。
- 创建唯一的临时目录
def test_whatever(self):
tmp_dir = self.get_auto_remove_tmp_dir()
tmp_dir
将包含创建的临时目录的路径。它将在测试结束时自动删除。
- 创建我选择的临时目录,确保它在测试开始前为空,并且在测试后不清空。
def test_whatever(self):
tmp_dir = self.get_auto_remove_tmp_dir("./xxx")
这对于调试非常有用,当您想要监视特定目录并确保之前的测试没有在那里留下任何数据时。
您可以通过直接覆盖
before
和after
参数来覆盖默认行为,从而导致以下行为之一:before=True
:临时目录将在测试开始时始终被清除。before=False
:如果临时目录已经存在,则任何现有文件都将保留在那里。after=True
:临时目录将在测试结束时始终被删除。after=False
:临时目录将在测试结束时始终保持不变。
为了安全地运行相当于 rm -r
的命令,如果使用显式 tmp_dir
,则只允许项目仓库检出中的子目录,以免错误地删除 /tmp
或文件系统中的其他重要部分。即,请始终传递以 ./
开头的路径。
每个测试都可以注册多个临时目录,除非另有要求,否则它们都将自动删除。
临时 sys.path 覆盖
如果您需要临时覆盖 sys.path
以从其他测试导入,例如,您可以使用 ExtendSysPath
上下文管理器。示例:
import os
from transformers.testing_utils import ExtendSysPath
bindir = os.path.abspath(os.path.dirname(__file__))
with ExtendSysPath(f"{bindir}/.."):
from test_trainer import TrainerIntegrationCommon # noqa
跳过测试
当发现 bug 并编写新测试,但 bug 尚未修复时,这很有用。为了能够将其提交到主仓库,我们需要确保在 make test
期间跳过它。
方法
跳过 (skip) 意味着您期望您的测试仅在满足某些条件时才能通过,否则 pytest 应该完全跳过运行测试。常见的例子是在非 Windows 平台上跳过仅限 Windows 的测试,或者跳过依赖于当前不可用的外部资源(例如数据库)的测试。
xfail (xfail) 意味着您希望测试因某种原因失败。一个常见的例子是针对尚未实现的功能或尚未修复的 bug 的测试。当测试通过但预期会失败(标记为 pytest.mark.xfail)时,它是一个 xpass,并将在测试摘要中报告。
两者之间的一个重要区别是 skip
不运行测试,而 xfail
会运行。因此,如果有问题的代码导致一些不良状态会影响其他测试,请不要使用 xfail
。
实现
- 以下是如何无条件跳过整个测试
@unittest.skip(reason="this bug needs to be fixed")
def test_feature_x():
或通过 pytest
@pytest.mark.skip(reason="this bug needs to be fixed")
或 xfail
方式
@pytest.mark.xfail
def test_feature_x():
以下是根据测试内部检查跳过测试的方法
def test_feature_x():
if not has_something():
pytest.skip("unsupported configuration")
或整个模块
import pytest
if not pytest.config.getoption("--custom-flag"):
pytest.skip("--custom-flag is missing, skipping tests", allow_module_level=True)
或 xfail
方式
def test_feature_x():
pytest.xfail("expected to fail until bug XYZ is fixed")
- 以下是如何在缺少某些导入时跳过模块中的所有测试
docutils = pytest.importorskip("docutils", minversion="0.3")
- 根据条件跳过测试
@pytest.mark.skipif(sys.version_info < (3,6), reason="requires python3.6 or higher")
def test_feature_x():
或者
@unittest.skipIf(torch_device == "cpu", "Can't do half precision")
def test_feature_x():
或跳过整个模块
@pytest.mark.skipif(sys.platform == 'win32', reason="does not run on windows")
class TestClass():
def test_feature_x(self):
更多详细信息、示例和方法请参见此处。
慢速测试
测试库不断增长,一些测试需要几分钟才能运行,因此我们无法在 CI 上等待一个小时才能完成测试套件。因此,除了基本测试的一些例外,慢速测试应按以下示例进行标记:
from transformers.testing_utils import slow
@slow
def test_integration_foo():
一旦测试被标记为 @slow
,要运行此类测试,请设置 RUN_SLOW=1
环境变量,例如:
RUN_SLOW=1 pytest tests
某些装饰器(如 @parameterized
)会重写测试名称,因此 @slow
和其余跳过装饰器 @require_*
必须最后列出,才能正常工作。以下是正确用法的示例:
@parameterized.expand(...)
@slow
def test_integration_foo():
正如本文档开头所解释的,慢速测试会按计划运行,而不是在 PR CI 检查中运行。因此,PR 提交期间可能会遗漏一些问题并被合并。此类问题将在下次计划的 CI 任务中被捕获。但这也意味着在提交 PR 之前在您的机器上运行慢速测试很重要。
以下是选择哪些测试应标记为慢速的粗略决策机制:
如果测试专注于库的内部组件之一(例如,建模文件、分词文件、管道),那么我们应该在非慢速测试套件中运行该测试。如果它专注于库的其他方面,例如文档或示例,那么我们应该在慢速测试套件中运行这些测试。然后,为了完善这种方法,我们应该有一些例外:
- 所有需要下载大量权重或大于约 50MB 的数据集的测试(例如,模型或分词器集成测试、管道集成测试)都应设置为慢速。如果您要添加新模型,则应创建并上传到 hub 它的微小版本(带随机权重)用于集成测试。这将在以下段落中讨论。
- 所有需要进行未特别优化以实现快速训练的测试都应设置为慢速。
- 如果其中一些“本应是非慢速”的测试异常慢,我们可以引入例外,并将其设置为
@slow
。自动建模测试就是很好的例子,它们会将大文件保存和加载到磁盘,因此被标记为@slow
。 - 如果一个测试在 CI 上在 1 秒内完成(包括下载,如果有的话),那么它应该是一个正常的测试。
总的来说,所有非慢速测试都需要完全覆盖不同的内部组件,同时保持快速。例如,可以通过使用特制的带有随机权重的微型模型进行测试来实现显著的覆盖率。这些模型具有最少的层数(例如 2)、词汇量大小(例如 1000)等。然后,@slow
测试可以使用大型慢速模型进行定性测试。要查看这些模型的使用,只需查找带有以下内容的微型模型:
grep tiny tests examples
以下是一个脚本示例,它创建了微型模型 stas/tiny-wmt19-en-de。您可以轻松地将其调整为您的特定模型架构。
如果存在下载大型模型的开销,则很容易错误地测量运行时,但如果您在本地测试,则下载的文件将被缓存,因此不会测量下载时间。因此,请查看 CI 日志中的执行速度报告(pytest --durations=0 tests
的输出)。
该报告还有助于查找未标记为慢速或需要重写以提高速度的慢速异常值。如果您注意到测试套件在 CI 上开始变慢,此报告的顶部列表将显示最慢的测试。
测试 stdout/stderr 输出
为了测试写入 stdout
和/或 stderr
的函数,测试可以使用 pytest
的 capsys 系统访问这些流。以下是如何实现:
import sys
def print_to_stdout(s):
print(s)
def print_to_stderr(s):
sys.stderr.write(s)
def test_result_and_stdout(capsys):
msg = "Hello"
print_to_stdout(msg)
print_to_stderr(msg)
out, err = capsys.readouterr() # consume the captured output streams
# optional: if you want to replay the consumed streams:
sys.stdout.write(out)
sys.stderr.write(err)
# test:
assert msg in out
assert msg in err
当然,大多数时候,stderr
会作为异常的一部分出现,因此在这种情况下必须使用 try/except
def raise_exception(msg):
raise ValueError(msg)
def test_something_exception():
msg = "Not a good value"
error = ""
try:
raise_exception(msg)
except Exception as e:
error = str(e)
assert msg in error, f"{msg} is in the exception:\n{error}"
捕获 stdout 的另一种方法是通过 contextlib.redirect_stdout
from io import StringIO
from contextlib import redirect_stdout
def print_to_stdout(s):
print(s)
def test_result_and_stdout():
msg = "Hello"
buffer = StringIO()
with redirect_stdout(buffer):
print_to_stdout(msg)
out = buffer.getvalue()
# optional: if you want to replay the consumed streams:
sys.stdout.write(out)
# test:
assert msg in out
捕获 stdout 的一个重要潜在问题是它可能包含 \r
字符,这些字符在正常 print
中会重置到目前为止已打印的所有内容。pytest
没有问题,但是使用 pytest -s
这些字符会被包含在缓冲区中,因此为了使测试能够在有和没有 -s
的情况下运行,您必须对捕获的输出进行额外的清理,使用 re.sub(r'~.*\r', '', buf, 0, re.M)
。
但是,我们有一个辅助上下文管理器包装器,可以自动处理所有这些问题,无论它是否包含一些 \r
,所以它很简单
from transformers.testing_utils import CaptureStdout
with CaptureStdout() as cs:
function_that_writes_to_stdout()
print(cs.out)
这是一个完整的测试示例
from transformers.testing_utils import CaptureStdout
msg = "Secret message\r"
final = "Hello World"
with CaptureStdout() as cs:
print(msg + final)
assert cs.out == final + "\n", f"captured: {cs.out}, expecting {final}"
如果您想捕获 stderr
,请改用 CaptureStderr
类
from transformers.testing_utils import CaptureStderr
with CaptureStderr() as cs:
function_that_writes_to_stderr()
print(cs.err)
如果您需要同时捕获两个流,请使用父 CaptureStd
类
from transformers.testing_utils import CaptureStd
with CaptureStd() as cs:
function_that_writes_to_stdout_and_stderr()
print(cs.err, cs.out)
此外,为了帮助调试测试问题,默认情况下,这些上下文管理器会在退出上下文时自动重播捕获的流。
捕获日志流
如果需要验证日志记录器的输出,可以使用 CaptureLogger
from transformers import logging
from transformers.testing_utils import CaptureLogger
msg = "Testing 1, 2, 3"
logging.set_verbosity_info()
logger = logging.get_logger("transformers.models.bart.tokenization_bart")
with CaptureLogger(logger) as cl:
logger.info(msg)
assert cl.out, msg + "\n"
使用环境变量进行测试
如果想为特定测试测试环境变量的影响,可以使用辅助装饰器 transformers.testing_utils.mockenv
from transformers.testing_utils import mockenv
class HfArgumentParserTest(unittest.TestCase):
@mockenv(TRANSFORMERS_VERBOSITY="error")
def test_env_override(self):
env_level_str = os.getenv("TRANSFORMERS_VERBOSITY", None)
有时需要调用外部程序,这需要在 os.environ
中设置 PYTHONPATH
以包含多个本地路径。辅助类 transformers.test_utils.TestCasePlus
可提供帮助
from transformers.testing_utils import TestCasePlus
class EnvExampleTest(TestCasePlus):
def test_external_prog(self):
env = self.get_env()
# now call the external program, passing `env` to it
根据测试文件是在 tests
测试套件下还是在 examples
下,它会正确设置 env[PYTHONPATH]
以包含这两个目录中的一个,以及 src
目录,以确保测试是针对当前仓库进行的,最后还会包含测试调用前已设置的 env[PYTHONPATH]
(如果存在)。
此辅助方法会创建 os.environ
对象的副本,因此原始对象保持不变。
获得可复现的结果
在某些情况下,您可能希望消除测试的随机性。要获得相同的可复现结果集,您需要固定随机种子。
seed = 42
# python RNG
import random
random.seed(seed)
# pytorch RNGs
import torch
torch.manual_seed(seed)
torch.backends.cudnn.deterministic = True
if torch.cuda.is_available():
torch.cuda.manual_seed_all(seed)
# numpy RNG
import numpy as np
np.random.seed(seed)
调试测试
要在警告点启动调试器,请执行此操作
pytest tests/utils/test_logging.py -W error::UserWarning --pdb
使用 GitHub Actions 工作流
要触发自推工作流 CI 作业,您必须
- 在
transformers
原始仓库(而非派生仓库!)中创建一个新分支。 - 分支名称必须以
ci_
或ci-
开头(main
也会触发,但我们不能在main
上创建 PR)。它也只针对特定路径触发——您可以此处的 push: 下找到最新定义,以防自本文档编写以来有所更改。 - 从该分支创建 PR。
- 然后您可以在此处看到作业出现。如果存在积压,它可能不会立即运行。
测试实验性 CI 功能
测试 CI 功能可能会有问题,因为它可能干扰正常的 CI 功能。因此,如果要添加新的 CI 功能,应按以下方式进行。
- 创建一个新的专用作业来测试需要测试的内容
- 新作业必须始终成功,以便我们获得绿色勾号(详情如下)。
- 让它运行几天,观察各种不同类型的 PR 是否能在其上运行(用户派生分支、非派生分支、源自 github.com UI 直接文件编辑的分支、各种强制推送等——类型繁多),同时监控实验性作业的日志(而非整体作业的绿色状态,因为它是特意设置为始终绿色的)
- 当一切都稳定后,将新更改合并到现有作业中。
这样,对 CI 功能本身的实验就不会干扰正常的工作流程。
那么,在开发新的 CI 功能时,我们如何才能让作业始终成功呢?
一些 CI(如 TravisCI)支持忽略步骤失败并会将整体作业报告为成功,但截至本文撰写时,CircleCI 和 Github Actions 尚不支持此功能。
因此,可以使用以下变通方法
- 在运行命令的开头使用
set +euo pipefail
来抑制 bash 脚本中大多数潜在的失败。 - 最后一个命令必须是成功的:
echo "done"
或仅true
即可
这里是一个例子
- run:
name: run CI experiment
command: |
set +euo pipefail
echo "setting run-all-despite-any-errors-mode"
this_command_will_fail
echo "but bash continues to run"
# emulate another failure
false
# but the last command must be a success
echo "during experiment do not remove: reporting success to CI, even if there were failures"
对于简单的命令,您也可以这样做
cmd_that_may_fail || true
当然,一旦对结果满意,请将实验步骤或作业与其余的正常作业集成,同时删除 set +euo pipefail
或您可能添加的任何其他内容,以确保实验作业不会干扰正常的 CI 功能。
如果我们可以为实验步骤设置类似 allow-failure
的东西,并允许它失败而不影响 PR 的整体状态,那么整个过程会容易得多。但如前所述,CircleCI 和 Github Actions 目前不支持此功能。
您可以在这些 CI 特定线程中投票支持此功能并查看其进展
DeepSpeed 集成
对于涉及 DeepSpeed 集成的 PR,请注意我们的 CircleCI PR CI 设置没有 GPU。需要 GPU 的测试在不同的 CI 夜间运行。这意味着如果您的 PR 中通过了 CI 报告,并不意味着 DeepSpeed 测试通过。
要运行 DeepSpeed 测试
RUN_SLOW=1 pytest tests/deepspeed/test_deepspeed.py
对建模或 PyTorch 示例代码的任何更改也需要运行模型库测试。
RUN_SLOW=1 pytest tests/deepspeed