Тесты PyTorch: почему CI показывает имена, которых нет в исходнике
В логе PyTorch CI легко встретить тест с именем TestLinalgCUDA.test_matmul_cuda_float32, хотя в исходнике есть только test_matmul. Попытка запустить pytest test/test_torch.py::TestTorch::test_matmul часто заканчивается сообщением no tests collected. Причина не в потерянном тесте. PyTorch создаёт конкретные варианты во время импорта файла.
Так один шаблон покрывает несколько устройств и типов данных. Когда знаешь правило генерации имён, падение из CI можно воспроизвести локально без гадания.
Почему pytest не находит шаблонный класс
Во многих тестовых файлах класс из исходника служит шаблоном. Функция instantiate_device_type_tests() создаёт из него классы для доступных устройств: TestTorchCPU, TestTorchCUDA, TestTorchMPS, TestTorchXPU. Параметризация по dtype добавляет к имени метода устройство и тип данных.
Метод test_matmul может превратиться в test_matmul_cuda_float32, test_matmul_cpu_float64 и test_matmul_mps_float16. После инстанциации исходный TestTorch уже не является отдельным discoverable-классом, поэтому полный путь к шаблону не работает.
Конкретный вариант удобнее запускать через фильтр:
pytest test/test_torch.py -k "test_matmul_cuda_float32" -x
Все варианты одного теста можно получить более широким запросом:
pytest test/test_torch.py -k "test_matmul"
Pytest фильтрует уже сгенерированные имена, которые видит после импорта модуля.
Один тест для разных устройств
PyTorch поддерживает CPU, CUDA, MPS, XPU и другие бэкенды. У каждого есть свой набор dtype. Ручная копия теста для каждой комбинации быстро превратилась бы в большой и плохо синхронизируемый набор почти одинакового кода.
Device-generic тест получает device и dtype как аргументы:
def test_basic(self, device, dtype):
x = torch.randn(3, 4, device=device, dtype=dtype)
y = x.matmul(x.T)
self.assertEqual(y.shape, (3, 3))
Во время импорта instantiate_device_type_tests() создаёт подходящие варианты. Общая схема имени выглядит как <ClassName><DEVICE>.<method>_<device>_<dtype>.
Схема экономит строки и упрощает поддержку новых бэкендов. После регистрации устройство получает значительную часть существующего покрытия. Контрибьютору не приходится копировать каждый старый тест вручную.
Как работают OpInfo
Для операторов PyTorch использует ещё один уровень параметризации. Объект OpInfo описывает оператор: его имя, варианты вызова, поддерживаемые dtype, sample inputs, ожидаемые пропуски, tolerances и декораторы. Основной реестр op_db находится в common_methods_invocations.py.
Обобщённые тесты из файлов вроде test_ops.py читают эти метаданные и проверяют разные операторы одним и тем же кодом. Среди проверок есть forward pass, градиенты, работа с torch.compile и совместимость с Meta или FakeTensor.
Имя TestCommonCUDA.test_variant_consistency_eager_torch_matmul_cuda_float32 можно читать буквально. Это тест согласованности вариантов вызова torch.matmul на CUDA с dtype float32.
Декоратор @ops(...) подключает OpInfo-параметризацию. Для случаев, не связанных с операторами, есть @parametrize(...). Он разворачивает тест по заданным режимам, формам или конфигурациям.
Где искать нужный код
Тестовая инфраструктура разделена на несколько уровней.
test/run_test.py управляет запуском файлов, шардированием и выбором затронутых тестов. Переменная PYTORCH_TESTING_DEVICE_ONLY_FOR ограничивает набор устройств. PYTORCH_TEST_WITH_SLOW=1 включает медленные тесты, а PYTORCH_TEST_WITH_DYNAMO=1 добавляет проверки torch.compile.
Контрибьюторы чаще работают с instantiate_device_type_tests, декораторами @dtypes, @onlyCUDA, @onlyCPU, @onlyAccelerator, @ops, @parametrize и @modules. В этом же слое находятся OpInfo и генераторы входных данных вроде make_tensor.
Базовые утилиты лежат в torch.testing._internal.common_utils.py. Там определены TestCase, run_tests(), load_tests, общие флаги и часть параметризации.
Если нужен конкретный файл, начните с четырёх мест:
common_device_type.pyдля device-specific декораторов и инстанциации;opinfo/core.pyдля устройства OpInfo;common_methods_invocations.pyдля реестраop_db;test/run_test.pyдля запуска и шардирования.
Как воспроизвести падение из CI
Сгенерированное имя обычно уже содержит тест, устройство, dtype и, для OpInfo, оператор. Сначала откройте сводку Dr. CI, затем полный лог задачи в hud.pytorch.org. Найдите имя теста и номер шарда.
После этого запустите узкий фильтр локально:
pytest test/test_torch.py -k "test_matmul_cuda_float32" -x
Для OpInfo-теста проверьте генерацию sample inputs на нужном dtype. Распространённая ошибка состоит в использовании torch.randn в dtype-generic тесте. Эта функция подходит для floating-point и complex типов, но не для integer или boolean. make_tensor умеет работать со всеми этими категориями.
Проверьте и зависимость от порядка выполнения. Тест не должен оставлять изменённый OpInfo, зарегистрированный хук или другое глобальное состояние. При шардировании соседние тесты и порядок запуска отличаются от локального прогона, поэтому скрытое состояние быстро проявляется.
Сбои также возникают из-за версии CUDA, драйвера и различий между GPU-архитектурами. Числовой tolerance, который проходит на одной карте, может оказаться слишком строгим на другой.
Отдельно стоит помнить про EXPECTTEST_ACCEPT. Некоторые тесты сравнивают вывод с сохранёнными snapshots. Значение EXPECTTEST_ACCEPT=1 обновляет ожидаемый вывод. Перед коммитом такой diff нужно прочитать вручную, иначе обновлённый snapshot может спрятать настоящую регрессию.
Зачем PyTorch генерирует столько вариантов
Один метод может проверяться на четырёх устройствах и шести dtype, что даёт больше двадцати конкретных тестов. OpInfo добавляет к этой матрице сотни операторов и несколько общих проверок. В результате десятки тысяч исходных методов превращаются в гораздо большее число запускаемых случаев.
Поэтому CI состоит из множества шардов на машинах с разными GPU. test/run_test.py старается распределить работу так, чтобы воркеры заканчивали примерно одновременно.
Эта механика объясняет странные имена в логах. Они не случайны: имя фиксирует конкретную точку в тестовой матрице.
Памятка контрибьютору
Используйте переданный аргумент device вместо жёсткого device="cuda". Для dtype-generic тестов берите make_tensor, если набор включает integer, boolean или другие типы помимо float и complex. Локально фильтруйте сгенерированное имя через pytest -k.
Если тест проходит отдельно, но падает в полном файле, ищите test pollution. Сравните изолированный запуск с pytest test/test_torch.py. Для параметров используйте @parametrize и @dtypes, а не ручные копии методов. Декоратор @onlyCUDA имеет смысл только тогда, когда тест действительно не должен работать на CPU.
Часто задаваемые вопросы
Почему имя в CI отличается от имени в исходнике?
PyTorch создаёт тесты во время импорта. Шаблонный test_matmul разворачивается в методы вроде test_matmul_cuda_float32 и test_matmul_cpu_float64. CI показывает уже созданный вариант.
Как запустить один вариант?
Используйте pytest -k с частью сгенерированного имени:
pytest test/test_torch.py -k "test_matmul_cuda_float32"
Что такое OpInfo?
Это набор метаданных об операторе: имя, варианты вызова, dtype, sample inputs, skips и tolerances. Благодаря OpInfo один обобщённый тест применим к большому числу операторов.
Почему torch.randn ломает dtype-generic тест?
torch.randn генерирует значения из нормального распределения и не поддерживает integer или boolean dtype. Для общей параметризации используйте make_tensor с явными device и dtype.
Короткий вывод
При падении PyTorch CI читайте сгенерированное имя справа налево: dtype, устройство, оператор и исходный тест. Затем воспроизведите нужный вариант через pytest -k. Обычно этого достаточно, чтобы перейти от сообщения no tests collected к реальной ошибке.