Тесты PyTorch: почему CI показывает имена, которых нет в исходнике
Открываете pull request в PyTorch, смотрите в CI-логи — а там имена тестов, которых вы никогда не писали. TestLinalgCUDA.test_matmul_cuda_float32 — откуда взялся CUDA, почему float32, где ваш TestTorch? Запускаете pytest test/test_torch.py::TestTorch::test_matmul локально — «no tests collected». Знакомо?
Это не баг и не потерянный тест. Это архитектурное решение, которое позволяет PyTorch проверять тысячи комбинаций устройств и типов данных без тысяч рукописных тестов. Разберёмся, как оно работает и как перестать паниковать при виде сгенерированных имён.
Почему «no tests collected» — это нормально
В большинстве тестовых файлов PyTorch класс, который вы видите в исходнике — это шаблон, а не финальный класс. Когда Python импортирует файл, функция instantiate_device_type_tests() разворачивает этот шаблон в конкретные классы для каждого устройства: TestTorchCPU, TestTorchCUDA, TestTorchMPS, TestTorchXPU. Если тест ещё параметризован по dtype, к имени метода добавляется суффикс устройства и типа данных.
Ваш test_matmul превращается в test_matmul_cuda_float32, test_matmul_cpu_float64, test_matmul_mps_float16 и так далее. Один метод в исходнике — десяток реальных тестов в рантайме.
Поэтому pytest test/test_torch.py::TestTorch::test_matmul не находит ничего — класс TestTorch в его исходном виде не существует после инстанциации. Правильный способ запустить конкретный тест — фильтрация по паттерну:
pytest test/test_torch.py -k "test_matmul_cuda_float32" -x
Или, если хотите все варианты test_matmul на всех устройствах:
pytest test/test_torch.py -k "test_matmul"
Это работает, потому что pytest ищет по именам сгенерированных классов и методов, а не по исходным шаблонным именам.
Device-generic тесты: одна функция — все устройства
PyTorch работает на CPU, CUDA, MPS, XPU и других бэкендах. Каждый бэкенд поддерживает десятки dtype: float16, float32, float64, bfloat16, int8, int32, bool и другие. Написать отдельный тест для каждой комбинации — это тысячи строк дублирующегося кода, который невозможно поддерживать.
Решение — тестовые шаблоны. Вы пишете один метод с параметрами 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>. То есть ваш test_basic на CUDA с float32 становится test_basic_cuda_float32, на MPS с float16 — test_basic_mps_float16.
Это не просто удобство. Это способ гарантировать, что каждая операция ведёт себя одинаково на каждом устройстве и с каждым типом данных — без ручного дублирования. Если вы добавляете новый бэкенд в PyTorch, вам не нужно переписывать тесты: достаточно зарегистрировать новое устройство, и вся существующая тестовая матрица автоматически расширится.
OpInfos: метаданные вместо ручных тестов
Операторов в PyTorch сотни — от torch.matmul до torch.nn.functional.scaled_dot_product_attention. Писать тест для каждого оператора вручную было бы абсурдно. Вместо этого PyTorch использует OpInfos — метаданные, описывающие как тестировать каждый оператор.
Каждый OpInfo содержит имя оператора, его варианты (functional, method, in-place), поддерживаемые dtype, примеры входных данных (sample inputs), ожидаемые пропуски (skips), декораторы, правила tolerances и многое другое. Всё это хранится в реестре op_db, определённом в common_methods_invocations.py.
Обобщённые тесты в файлах вроде test_ops.py читают OpInfo и применяют одни и те же проверки к разным операторам: корректность forward pass, проверка градиентов, совместимость с torch.compile, валидация через Meta/FakeTensor. Один тестовый шаблон — десятки операторов, каждый со своим набором устройств и типов данных.
Когда вы видите в CI что-то вроде TestCommonCUDA.test_variant_consistency_eager_torch_matmul_cuda_float32 — это значит, что обобщённый OpInfo-тест проверяет оператор torch.matmul на CUDA с float32, и в данном случае тест проверяет consistency между разными вариантами вызова оператора (функциональный, через метод тензора, in-place).
Декоратор @ops(...) — один из примеров этого паттерна. Для не-операторных случаев есть @parametrize(...), который разворачивает один тест по пользовательским значениям: режимами, формами, конфигурациями. Идея везде одна — один body теста, инфраструктура генерирует комбинации.
Архитектура: кто за что отвечает
Тестовая инфраструктура PyTorch состоит из нескольких слоёв, и понимание их границ помогает быстро найти, куда смотреть при ошибке.
На верхнем уровне — CI-оркестрация. Скрипт test/run_test.py отвечает за запуск файлов, шардирование (разделение тестов между воркерами) и выбор затронутых тестов при pull request. Переменные окружения вроде 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, sample_inputs).
Нижний уровень — базовые утилиты в torch.testing._internal.common_utils.py: класс TestCase, функция run_tests(), хелперы load_tests, parametrize, общие флаги. Это фундамент, на котором построено всё остальное.
Ключевые файлы, которые стоит знать: common_device_type.py содержит instantiate_device_type_tests и все device-специфичные декораторы. opinfo/core.py — ядро OpInfo с метаданными, sample inputs, dtype-поддержкой и tolerance-конфигурацией. common_methods_invocations.py — реестр op_db. test/run_test.py — CI-раннер с шардированием.
Практика: как отлаживать CI-сбой
Когда CI падает, первое, что нужно сделать — прочитать сгенерированное имя теста. Оно сообщает вам четыре вещи: какой тест, на каком устройстве, с каким dtype и (для OpInfo-тестов) какой оператор. Это ключ к воспроизведению.
Базовый flow отладки такой. Открываете Dr. CI — это бот, который суммирует падающие джобы на pull request, группирует повторяющиеся сбои и указывает на релевантные логи. Это не замена чтению полного вывода CI, но хорошая стартовая точка. Далее — открываете failing job на hud.pytorch.org, находите сгенерированное имя теста и шард. Потом воспроизводите локально:
pytest test/test_torch.py -k "test_matmul_cuda_float32" -x
Если тест использует OpInfo-инфраструктуру, убедитесь, что sample inputs генерируются корректно для вашего dtype. Частая ошибка — использовать torch.randn в dtype-generic тестах: randn работает для float и complex, но падает на integer и boolean. Правильный выбор — make_tensor, который обрабатывает все категории dtype.
Ещё одна типичная проблема — тесты, которые зависят от порядка выполнения или глобального состояния. PyTorch ожидает, что тесты атомарны. Если ваш тест оставляет после себя модифицированный OpInfo или зарегистрированный хук, следующий тест в том же файле может упасть непредсказуемо. Это особенно заметно при шардировании: разные шарды выполняют разные подмножества тестов, и порядок может отличаться от локального прогона.
Распространённые CI-специфичные ошибки приходятся на различия в окружении (другая версия CUDA, другой драйвер), числовую точность (разные результаты на разных GPU-архитектурах) и предположения о шардировании (тест ожидает, что все другие тесты уже выполнились, а в другом шарде это не так).
Отдельная история — EXPECTTEST_ACCEPT. Многие тесты PyTorch сравнивают вывод с эталонными «snapshots», сохранёнными в репозитории. Когда поведение операции меняется намеренно (оптимизация, исправление бага), snapshot-тесты начинают падать. Переменная EXPECTTEST_ACCEPT=1 автоматически обновляет ожидаемый вывод, и вы коммитите новый snapshot вместе с кодом. Это удобно, но опасно: если вы случайно обновите snapshot, скрывающий регрессию, CI пропустит баг. Всегда проверяйте diff обновлённых snapshot-файлов перед коммитом.
Масштаб: почему без автоматической генерации не обойтись
Несколько цифр для контекста. Репозиторий PyTorch содержит десятки тысяч тестовых методов, которые после инстанциации превращаются в сотни тысяч конкретных тест-кейсов. Каждый оператор может тестироваться на 4+ устройствах и 6+ типах данных — один метод в исходнике порождает 24+ реальных теста. Добавьте сюда OpInfo-тесты, которые для каждого из сотен операторов запускают одинаковые проверки на корректность, градиенты, compile-совместимость и MetaTensor-валидацию.
Именно поэтому PyTorch CI — это не один прогон pytest, а десятки шардов на разных машинах с разными GPU, работающие параллельно. Скрипт test/run_test.py умеет разделять тестовое пространство на шарды так, чтобы каждый воркер получил примерно одинаковое время выполнения. Когда ваш pull request проходит CI, фактически десятки машин параллельно проверяют тысячи сгенерированных тестов — и все они ведут происхождение от нескольких сотен шаблонных методов, написанных людьми.
Понимание этой системы — не академическое упражнение. Это практический навык, который определяет, насколько быстро вы сможете контрибьютить в PyTorch. Контрибьютор, который знает, как читать сгенерированные имена и воспроизводить сбои, решает проблему за десять минут. Контрибьютор, который не знает — тратит часы на попытки запустить тест по исходному имени и в конце концов открывает issue с вопросом «test not found».
Что делать, если вы контрибьютор
Если вы пишете новый тест для PyTorch, несколько правил сэкономят вам часы отладки. Используйте аргумент device, который предоставляет сгенерированный тест — не хардкодьте device="cuda". Это сохраняет переносимость между устройствами. Для dtype-generic тестов используйте make_tensor вместо torch.randn. Для фильтрации при локальном запуске используйте pytest -k с сгенерированным именем, а не пытайтесь вызвать шаблонный класс напрямую.
Если ваш тест работает локально, но падает в CI — проверьте, не зависит ли он от порядка выполнения. Запустите его в изоляции (pytest -x) и в полном файле (pytest test/test_torch.py). Если результаты отличаются — у вас test pollution. Используйте @parametrize и @dtypes вместо ручного дублирования. Проверьте, что ваш тест проходит на всех устройствах, которые вы заявляете — @onlyCUDA не должен стоять на тесте, который технически работает и на CPU, иначе вы упускаете покрытие.
Часто задаваемые вопросы
Почему мой тест называется иначе в CI, чем в исходнике?
Потому что PyTorch генерирует тесты во время импорта файла. Ваш шаблонный метод test_matmul разворачивается в конкретные методы для каждого устройства и dtype: test_matmul_cuda_float32, test_matmul_cpu_float64 и так далее. CI показывает сгенерированные имена, а не шаблонные.
Как запустить конкретный тест, если его имя в CI не совпадает с исходником?
Используйте pytest -k "pattern" с частью сгенерированного имени. Например, pytest test/test_torch.py -k "test_matmul_cuda_float32". Не пытайтесь вызвать шаблонный класс напрямую — после инстанциации он не существует в виде отдельного discoverable объекта.
Что такое OpInfo и зачем они нужны?
OpInfo — это метаданные об операторе PyTorch: имя, варианты вызова, поддерживаемые dtype, примеры входных данных, tolerance-правила. Они позволяют одному обобщённому тесту проверять сотни операторов без написания отдельного теста для каждого. Когда вы добавляете новый оператор, достаточно зарегистрировать его OpInfo — и он автоматически получит покрытие от всех обобщённых тестов.
Почему torch.randn не работает в моих dtype-generic тестах?
Потому что torch.randn генерирует числа из нормального распределения и работает только для floating-point и complex dtype. Для integer, boolean и других типов данных используйте make_tensor — он корректно обрабатывает все категории dtype и принимает явные device и dtype.
Итог
Тестовая инфраструктура PyTorch — это не хаос из сгенерированных имён, а продуманная система масштабирования. Device-generic шаблоны, OpInfos и CI-шардирование вместе позволяют проверять тысячи комбинаций без тысяч рукописных тестов. Как только вы понимаете, что имя в CI — это сгенерированная развёртка вашего шаблона, отладка перестаёт быть мистикой. Читайте сгенерированное имя, воспроизводите через pytest -k, проверяйте атомарность теста — и pull request пойдёт зелёным.