This is an automated email from the ASF dual-hosted git repository. tqchen pushed a commit to branch tvm-simplify-jenkins-pytest-sharding in repository https://gitbox.apache.org/repos/asf/tvm.git
commit a3048810fb71c0907c18642eb47ee8d6b09fdc77 Author: Tianqi Chen <[email protected]> AuthorDate: Sun Jul 5 02:14:27 2026 +0000 [CI] Simplify Jenkins pytest execution Enable standard pytest-xdist automatic parallelism in the canonical launchers while removing external sharding, obsolete wrapper machinery, JUnit reporting, and its XML consumer. Repair broad-suite targets and failure propagation, and bound active nested MetaSchedule test parallelism without blanket serialization. --- .github/workflows/main.yml | 2 +- ci/jenkins/generated/arm_jenkinsfile.groovy | 11 +- ci/jenkins/generated/cpu_jenkinsfile.groovy | 98 ++------------- ci/jenkins/generated/docker_jenkinsfile.groovy | 11 +- ci/jenkins/generated/gpu_jenkinsfile.groovy | 129 ++----------------- ci/jenkins/generated/wasm_jenkinsfile.groovy | 12 +- ci/jenkins/templates/cpu_jenkinsfile.groovy.j2 | 4 +- ci/jenkins/templates/gpu_jenkinsfile.groovy.j2 | 10 +- ci/jenkins/templates/utils/Build.groovy.j2 | 7 -- ci/jenkins/templates/utils/base.groovy.j2 | 2 +- ci/jenkins/templates/utils/macros.j2 | 30 +---- ci/jenkins/templates/wasm_jenkinsfile.groovy.j2 | 1 - ci/scripts/jenkins/pytest_ids.py | 43 ------- ci/scripts/jenkins/pytest_wrapper.py | 137 --------------------- conftest.py | 54 -------- docker/install/ubuntu_install_python_package.sh | 2 - docs/contribute/pull_request.rst | 2 +- python/tvm/testing/plugin.py | 9 -- .../meta_schedule/test_meta_schedule_cost_model.py | 11 +- .../test_meta_schedule_search_strategy.py | 1 + .../test_meta_schedule_task_scheduler.py | 13 ++ .../test_s_tir_transform_lower_match_buffer.py | 2 +- tests/python/testing/test_tvm_testing_features.py | 3 +- tests/scripts/setup-pytest-env.sh | 98 --------------- tests/scripts/task_clear_pytest.sh | 8 +- tests/scripts/task_python_docs.sh | 2 +- tests/scripts/task_python_integration.sh | 2 +- tests/scripts/task_python_nightly.sh | 33 ----- tests/scripts/task_python_unittest.sh | 29 +++-- tests/scripts/task_python_unittest_gpuonly.sh | 11 +- 30 files changed, 87 insertions(+), 690 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a0e4cb0490..54dc54316d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -69,7 +69,7 @@ jobs: python -m pytest -v -s 'tests/python/codegen/test_target_codegen_metal.py::test_func_with_trailing_pod_params' - name: Minimal Metal Compile-and-Run shell: bash -l {0} - run: >- + run: | python -m pytest -v -s 'tests/python/codegen/test_target_codegen_metal.py' python -m pytest -v -s 'tests/python/codegen/test_target_codegen_gpu_common.py' python -m pytest -v -s 'tests/python/codegen/test_gpu_codegen_allreduce.py::test_allreduce_sum[dims0-metal]' diff --git a/ci/jenkins/generated/arm_jenkinsfile.groovy b/ci/jenkins/generated/arm_jenkinsfile.groovy index d6826878bf..3442caded7 100644 --- a/ci/jenkins/generated/arm_jenkinsfile.groovy +++ b/ci/jenkins/generated/arm_jenkinsfile.groovy @@ -60,7 +60,7 @@ // 'python3 jenkins/generate.py' // Note: This timestamp is here to ensure that updates to the Jenkinsfile are // always rebased on main before merging: -// Generated at 2026-06-23T03:28:14.520193 +// Generated at 2026-07-05T01:05:57.719405 import org.jenkinsci.plugins.pipeline.modeldefinition.Utils // These are set at runtime from data in ci/jenkins/docker-images.yml, update @@ -97,7 +97,7 @@ properties([ upstream_revision = null // command to start a docker container -docker_run = 'docker/bash.sh --env CI --env PLATFORM --env TVM_SHARD_INDEX --env TVM_NUM_SHARDS --env RUN_DISPLAY_URL --env PLATFORM --env SKIP_SLOW_TESTS --env TEST_STEP_NAME' +docker_run = 'docker/bash.sh --env CI --env PLATFORM --env SKIP_SLOW_TESTS --env TEST_STEP_NAME' docker_build = 'docker/build.sh' // timeout in minutes max_time = 180 @@ -404,13 +404,6 @@ def prepare(node_type) { } } } -def ci_setup(image) { - sh ( - script: "${docker_run} ${image} ./tests/scripts/task_clear_pytest.sh", - label: 'Clean up old workspace', - ) -} - def python_unittest(image) { sh ( script: "${docker_run} ${image} ./tests/scripts/task_python_unittest.sh", diff --git a/ci/jenkins/generated/cpu_jenkinsfile.groovy b/ci/jenkins/generated/cpu_jenkinsfile.groovy index 879cd8b79f..0d3e408235 100644 --- a/ci/jenkins/generated/cpu_jenkinsfile.groovy +++ b/ci/jenkins/generated/cpu_jenkinsfile.groovy @@ -60,7 +60,7 @@ // 'python3 jenkins/generate.py' // Note: This timestamp is here to ensure that updates to the Jenkinsfile are // always rebased on main before merging: -// Generated at 2026-06-23T03:28:14.505504 +// Generated at 2026-07-05T01:05:57.741181 import org.jenkinsci.plugins.pipeline.modeldefinition.Utils // These are set at runtime from data in ci/jenkins/docker-images.yml, update @@ -97,7 +97,7 @@ properties([ upstream_revision = null // command to start a docker container -docker_run = 'docker/bash.sh --env CI --env PLATFORM --env TVM_SHARD_INDEX --env TVM_NUM_SHARDS --env RUN_DISPLAY_URL --env PLATFORM --env SKIP_SLOW_TESTS --env TEST_STEP_NAME' +docker_run = 'docker/bash.sh --env CI --env PLATFORM --env SKIP_SLOW_TESTS --env TEST_STEP_NAME' docker_build = 'docker/build.sh' // timeout in minutes max_time = 180 @@ -404,13 +404,6 @@ def prepare(node_type) { } } } -def ci_setup(image) { - sh ( - script: "${docker_run} ${image} ./tests/scripts/task_clear_pytest.sh", - label: 'Clean up old workspace', - ) -} - def python_unittest(image) { sh ( script: "${docker_run} ${image} ./tests/scripts/task_python_unittest.sh", @@ -510,52 +503,7 @@ build() - -def shard_run_unittest_CPU_1_of_2(node_type) { - echo 'Begin running on node_type ' + node_type - if (!skip_ci && is_docs_only_build != 1) { - node(node_type) { - ws("workspace/exec_${env.EXECUTOR_NUMBER}/tvm/ut-python-cpu") { - // NOTE: if exception happens, it will be caught outside - init_git() - docker_init(ci_cpu) - timeout(time: max_time, unit: 'MINUTES') { - withEnv([ - 'PLATFORM=cpu', - 'TEST_STEP_NAME=unittest: CPU', - 'TVM_NUM_SHARDS=2', - 'TVM_SHARD_INDEX=0', - "SKIP_SLOW_TESTS=${skip_slow_tests}"], { - sh( - script: "./${jenkins_scripts_root}/s3.py --action download --bucket ${s3_bucket} --prefix ${s3_prefix}/cpu", - label: 'Download artifacts from S3', - ) - - ci_setup(ci_cpu) - cpp_unittest(ci_cpu) - python_unittest(ci_cpu) - }) - } - // only run upload if things are successful - try { - sh( - script: "./${jenkins_scripts_root}/s3.py --action upload --bucket ${s3_bucket} --prefix ${s3_prefix}/pytest-results/unittest_CPU --items build/pytest-results", - label: 'Upload JUnits to S3', - ) - - junit 'build/pytest-results/*.xml' - } catch (Exception e) { - echo 'Exception during JUnit upload: ' + e.toString() - } - } - } - echo 'End running on node_type ' + node_type - } else { - Utils.markStageSkippedForConditional('unittest: CPU 1 of 2') - } -} - -def shard_run_unittest_CPU_2_of_2(node_type) { +def run_unittest_CPU(node_type) { echo 'Begin running on node_type ' + node_type if (!skip_ci && is_docs_only_build != 1) { node(node_type) { @@ -567,35 +515,21 @@ def shard_run_unittest_CPU_2_of_2(node_type) { withEnv([ 'PLATFORM=cpu', 'TEST_STEP_NAME=unittest: CPU', - 'TVM_NUM_SHARDS=2', - 'TVM_SHARD_INDEX=1', "SKIP_SLOW_TESTS=${skip_slow_tests}"], { sh( script: "./${jenkins_scripts_root}/s3.py --action download --bucket ${s3_bucket} --prefix ${s3_prefix}/cpu", label: 'Download artifacts from S3', ) - ci_setup(ci_cpu) cpp_unittest(ci_cpu) python_unittest(ci_cpu) }) } - // only run upload if things are successful - try { - sh( - script: "./${jenkins_scripts_root}/s3.py --action upload --bucket ${s3_bucket} --prefix ${s3_prefix}/pytest-results/unittest_CPU --items build/pytest-results", - label: 'Upload JUnits to S3', - ) - - junit 'build/pytest-results/*.xml' - } catch (Exception e) { - echo 'Exception during JUnit upload: ' + e.toString() - } } } echo 'End running on node_type ' + node_type } else { - Utils.markStageSkippedForConditional('unittest: CPU 2 of 2') + Utils.markStageSkippedForConditional('unittest: CPU') } } @@ -606,27 +540,9 @@ def test() { SKIP_SLOW_TESTS = "${skip_slow_tests}" } parallel( - 'unittest: CPU 1 of 2': { - try { - shard_run_unittest_CPU_1_of_2('CPU-SMALL-SPOT') - } catch (Throwable ex) { - echo 'Exception during SPOT run ' + ex.toString() - if (is_last_build()) { - // retry if at last build - // mark the current stage as success - // and try again via on demand node - echo 'Retry on-demand given it is last build' - currentBuild.result = 'SUCCESS' - shard_run_unittest_CPU_1_of_2('CPU-SMALL') - } else { - echo 'Exit since it is not last build' - throw ex - } - } - }, - 'unittest: CPU 2 of 2': { + 'unittest: CPU': { try { - shard_run_unittest_CPU_2_of_2('CPU-SMALL-SPOT') + run_unittest_CPU('CPU-SMALL-SPOT') } catch (Throwable ex) { echo 'Exception during SPOT run ' + ex.toString() if (is_last_build()) { @@ -635,7 +551,7 @@ def test() { // and try again via on demand node echo 'Retry on-demand given it is last build' currentBuild.result = 'SUCCESS' - shard_run_unittest_CPU_2_of_2('CPU-SMALL') + run_unittest_CPU('CPU-SMALL') } else { echo 'Exit since it is not last build' throw ex diff --git a/ci/jenkins/generated/docker_jenkinsfile.groovy b/ci/jenkins/generated/docker_jenkinsfile.groovy index f5409e23aa..8ffe78942a 100644 --- a/ci/jenkins/generated/docker_jenkinsfile.groovy +++ b/ci/jenkins/generated/docker_jenkinsfile.groovy @@ -60,7 +60,7 @@ // 'python3 jenkins/generate.py' // Note: This timestamp is here to ensure that updates to the Jenkinsfile are // always rebased on main before merging: -// Generated at 2026-06-23T03:28:14.487314 +// Generated at 2026-07-05T01:05:57.759905 import org.jenkinsci.plugins.pipeline.modeldefinition.Utils // These are set at runtime from data in ci/jenkins/docker-images.yml, update @@ -97,7 +97,7 @@ properties([ upstream_revision = null // command to start a docker container -docker_run = 'docker/bash.sh --env CI --env PLATFORM --env TVM_SHARD_INDEX --env TVM_NUM_SHARDS --env RUN_DISPLAY_URL --env PLATFORM --env SKIP_SLOW_TESTS --env TEST_STEP_NAME' +docker_run = 'docker/bash.sh --env CI --env PLATFORM --env SKIP_SLOW_TESTS --env TEST_STEP_NAME' docker_build = 'docker/build.sh' // timeout in minutes max_time = 180 @@ -404,13 +404,6 @@ def prepare(node_type) { } } } -def ci_setup(image) { - sh ( - script: "${docker_run} ${image} ./tests/scripts/task_clear_pytest.sh", - label: 'Clean up old workspace', - ) -} - def python_unittest(image) { sh ( script: "${docker_run} ${image} ./tests/scripts/task_python_unittest.sh", diff --git a/ci/jenkins/generated/gpu_jenkinsfile.groovy b/ci/jenkins/generated/gpu_jenkinsfile.groovy index 28c9a6040e..792b7eadc4 100644 --- a/ci/jenkins/generated/gpu_jenkinsfile.groovy +++ b/ci/jenkins/generated/gpu_jenkinsfile.groovy @@ -60,7 +60,7 @@ // 'python3 jenkins/generate.py' // Note: This timestamp is here to ensure that updates to the Jenkinsfile are // always rebased on main before merging: -// Generated at 2026-06-23T03:28:14.533751 +// Generated at 2026-07-05T01:05:57.780838 import org.jenkinsci.plugins.pipeline.modeldefinition.Utils // These are set at runtime from data in ci/jenkins/docker-images.yml, update @@ -97,7 +97,7 @@ properties([ upstream_revision = null // command to start a docker container -docker_run = 'docker/bash.sh --env CI --env PLATFORM --env TVM_SHARD_INDEX --env TVM_NUM_SHARDS --env RUN_DISPLAY_URL --env PLATFORM --env SKIP_SLOW_TESTS --env TEST_STEP_NAME' +docker_run = 'docker/bash.sh --env CI --env PLATFORM --env SKIP_SLOW_TESTS --env TEST_STEP_NAME' docker_build = 'docker/build.sh' // timeout in minutes max_time = 180 @@ -404,13 +404,6 @@ def prepare(node_type) { } } } -def ci_setup(image) { - sh ( - script: "${docker_run} ${image} ./tests/scripts/task_clear_pytest.sh", - label: 'Clean up old workspace', - ) -} - def python_unittest(image) { sh ( script: "${docker_run} ${image} ./tests/scripts/task_python_unittest.sh", @@ -515,8 +508,7 @@ def build() { build() - -def shard_run_unittest_GPU_1_of_2(node_type) { +def run_unittest_GPU(node_type) { echo 'Begin running on node_type ' + node_type if (!skip_ci && is_docs_only_build != 1) { node(node_type) { @@ -528,65 +520,12 @@ def shard_run_unittest_GPU_1_of_2(node_type) { withEnv([ 'PLATFORM=gpu', 'TEST_STEP_NAME=unittest: GPU', - 'TVM_NUM_SHARDS=2', - 'TVM_SHARD_INDEX=0', "SKIP_SLOW_TESTS=${skip_slow_tests}"], { sh( script: "./${jenkins_scripts_root}/s3.py --action download --bucket ${s3_bucket} --prefix ${s3_prefix}/gpu", label: 'Download artifacts from S3', ) - ci_setup(ci_gpu) - sh ( - script: "${docker_run} ${ci_gpu} ./tests/scripts/task_python_unittest_gpuonly.sh", - label: 'Run Python GPU unit tests', - ) - sh ( - script: "${docker_run} ${ci_gpu} ./tests/scripts/task_python_integration_gpuonly.sh", - label: 'Run Python GPU integration tests', - ) - }) - } - // only run upload if things are successful - try { - sh( - script: "./${jenkins_scripts_root}/s3.py --action upload --bucket ${s3_bucket} --prefix ${s3_prefix}/pytest-results/unittest_GPU --items build/pytest-results", - label: 'Upload JUnits to S3', - ) - - junit 'build/pytest-results/*.xml' - } catch (Exception e) { - echo 'Exception during JUnit upload: ' + e.toString() - } - } - } - echo 'End running on node_type ' + node_type - } else { - Utils.markStageSkippedForConditional('unittest: GPU 1 of 2') - } -} - -def shard_run_unittest_GPU_2_of_2(node_type) { - echo 'Begin running on node_type ' + node_type - if (!skip_ci && is_docs_only_build != 1) { - node(node_type) { - ws("workspace/exec_${env.EXECUTOR_NUMBER}/tvm/ut-python-gpu") { - // NOTE: if exception happens, it will be caught outside - init_git() - docker_init(ci_gpu) - timeout(time: max_time, unit: 'MINUTES') { - withEnv([ - 'PLATFORM=gpu', - 'TEST_STEP_NAME=unittest: GPU', - 'TVM_NUM_SHARDS=2', - 'TVM_SHARD_INDEX=1', - "SKIP_SLOW_TESTS=${skip_slow_tests}"], { - sh( - script: "./${jenkins_scripts_root}/s3.py --action download --bucket ${s3_bucket} --prefix ${s3_prefix}/gpu", - label: 'Download artifacts from S3', - ) - - ci_setup(ci_gpu) sh ( script: "${docker_run} ${ci_gpu} ./tests/scripts/task_java_unittest.sh", label: 'Run Java unit tests', @@ -601,29 +540,17 @@ def shard_run_unittest_GPU_2_of_2(node_type) { ) }) } - // only run upload if things are successful - try { - sh( - script: "./${jenkins_scripts_root}/s3.py --action upload --bucket ${s3_bucket} --prefix ${s3_prefix}/pytest-results/unittest_GPU --items build/pytest-results", - label: 'Upload JUnits to S3', - ) - - junit 'build/pytest-results/*.xml' - } catch (Exception e) { - echo 'Exception during JUnit upload: ' + e.toString() - } } } echo 'End running on node_type ' + node_type } else { - Utils.markStageSkippedForConditional('unittest: GPU 2 of 2') + Utils.markStageSkippedForConditional('unittest: GPU') } } - -def shard_run_docs_GPU_1_of_1(node_type) { +def run_docs_GPU(node_type) { echo 'Begin running on node_type ' + node_type if (!skip_ci) { node(node_type) { @@ -635,15 +562,12 @@ def shard_run_docs_GPU_1_of_1(node_type) { withEnv([ 'PLATFORM=gpu', 'TEST_STEP_NAME=docs: GPU', - 'TVM_NUM_SHARDS=1', - 'TVM_SHARD_INDEX=0', "SKIP_SLOW_TESTS=${skip_slow_tests}"], { sh( script: "./${jenkins_scripts_root}/s3.py --action download --bucket ${s3_bucket} --prefix ${s3_prefix}/gpu", label: 'Download artifacts from S3', ) - ci_setup(ci_gpu) sh ( script: "${docker_run} ${ci_gpu} ./tests/scripts/task_python_docs.sh", label: 'Build docs', @@ -659,22 +583,11 @@ def shard_run_docs_GPU_1_of_1(node_type) { ) }) } - // only run upload if things are successful - try { - sh( - script: "./${jenkins_scripts_root}/s3.py --action upload --bucket ${s3_bucket} --prefix ${s3_prefix}/pytest-results/docs_GPU --items build/pytest-results", - label: 'Upload JUnits to S3', - ) - - junit 'build/pytest-results/*.xml' - } catch (Exception e) { - echo 'Exception during JUnit upload: ' + e.toString() - } } } echo 'End running on node_type ' + node_type } else { - Utils.markStageSkippedForConditional('docs: GPU 1 of 1') + Utils.markStageSkippedForConditional('docs: GPU') } } @@ -686,27 +599,9 @@ def test() { SKIP_SLOW_TESTS = "${skip_slow_tests}" } parallel( - 'unittest: GPU 1 of 2': { - try { - shard_run_unittest_GPU_1_of_2('GPU-SPOT') - } catch (Throwable ex) { - echo 'Exception during SPOT run ' + ex.toString() - if (is_last_build()) { - // retry if at last build - // mark the current stage as success - // and try again via on demand node - echo 'Retry on-demand given it is last build' - currentBuild.result = 'SUCCESS' - shard_run_unittest_GPU_1_of_2('GPU') - } else { - echo 'Exit since it is not last build' - throw ex - } - } - }, - 'unittest: GPU 2 of 2': { + 'unittest: GPU': { try { - shard_run_unittest_GPU_2_of_2('GPU-SPOT') + run_unittest_GPU('GPU-SPOT') } catch (Throwable ex) { echo 'Exception during SPOT run ' + ex.toString() if (is_last_build()) { @@ -715,16 +610,16 @@ def test() { // and try again via on demand node echo 'Retry on-demand given it is last build' currentBuild.result = 'SUCCESS' - shard_run_unittest_GPU_2_of_2('GPU') + run_unittest_GPU('GPU') } else { echo 'Exit since it is not last build' throw ex } } }, - 'docs: GPU 1 of 1': { + 'docs: GPU': { try { - shard_run_docs_GPU_1_of_1('GPU-SPOT') + run_docs_GPU('GPU-SPOT') } catch (Throwable ex) { echo 'Exception during SPOT run ' + ex.toString() if (is_last_build()) { @@ -733,7 +628,7 @@ def test() { // and try again via on demand node echo 'Retry on-demand given it is last build' currentBuild.result = 'SUCCESS' - shard_run_docs_GPU_1_of_1('GPU') + run_docs_GPU('GPU') } else { echo 'Exit since it is not last build' throw ex diff --git a/ci/jenkins/generated/wasm_jenkinsfile.groovy b/ci/jenkins/generated/wasm_jenkinsfile.groovy index ab64444e4a..7421fa86ca 100644 --- a/ci/jenkins/generated/wasm_jenkinsfile.groovy +++ b/ci/jenkins/generated/wasm_jenkinsfile.groovy @@ -60,7 +60,7 @@ // 'python3 jenkins/generate.py' // Note: This timestamp is here to ensure that updates to the Jenkinsfile are // always rebased on main before merging: -// Generated at 2026-06-23T03:28:14.550555 +// Generated at 2026-07-05T01:05:57.802104 import org.jenkinsci.plugins.pipeline.modeldefinition.Utils // These are set at runtime from data in ci/jenkins/docker-images.yml, update @@ -97,7 +97,7 @@ properties([ upstream_revision = null // command to start a docker container -docker_run = 'docker/bash.sh --env CI --env PLATFORM --env TVM_SHARD_INDEX --env TVM_NUM_SHARDS --env RUN_DISPLAY_URL --env PLATFORM --env SKIP_SLOW_TESTS --env TEST_STEP_NAME' +docker_run = 'docker/bash.sh --env CI --env PLATFORM --env SKIP_SLOW_TESTS --env TEST_STEP_NAME' docker_build = 'docker/build.sh' // timeout in minutes max_time = 180 @@ -404,13 +404,6 @@ def prepare(node_type) { } } } -def ci_setup(image) { - sh ( - script: "${docker_run} ${image} ./tests/scripts/task_clear_pytest.sh", - label: 'Clean up old workspace', - ) -} - def python_unittest(image) { sh ( script: "${docker_run} ${image} ./tests/scripts/task_python_unittest.sh", @@ -471,7 +464,6 @@ def run_build(node_type) { cmake_build(ci_wasm, 'build') make_cpp_tests(ci_wasm, 'build') cpp_unittest(ci_wasm) - ci_setup(ci_wasm) sh ( script: "${docker_run} ${ci_wasm} ./tests/scripts/task_web_wasm.sh", label: 'Run WASM lint and tests', diff --git a/ci/jenkins/templates/cpu_jenkinsfile.groovy.j2 b/ci/jenkins/templates/cpu_jenkinsfile.groovy.j2 index d2e479d5e8..62b7c3dfea 100644 --- a/ci/jenkins/templates/cpu_jenkinsfile.groovy.j2 +++ b/ci/jenkins/templates/cpu_jenkinsfile.groovy.j2 @@ -37,16 +37,14 @@ {% set test_method_names = [] %} -{% call(shard_index, num_shards) m.sharded_test_step( +{% call m.test_step( name="unittest: CPU", ws="tvm/ut-python-cpu", platform="cpu", - num_shards=2, docker_image="ci_cpu", test_method_names=test_method_names, ) %} {{ m.download_artifacts(tag='cpu') }} - ci_setup(ci_cpu) cpp_unittest(ci_cpu) python_unittest(ci_cpu) {% endcall %} diff --git a/ci/jenkins/templates/gpu_jenkinsfile.groovy.j2 b/ci/jenkins/templates/gpu_jenkinsfile.groovy.j2 index 7ab5256419..3a13dfe7f3 100644 --- a/ci/jenkins/templates/gpu_jenkinsfile.groovy.j2 +++ b/ci/jenkins/templates/gpu_jenkinsfile.groovy.j2 @@ -38,22 +38,18 @@ {% set test_method_names = [] %} -{% call(shard_index, num_shards) m.sharded_test_step( +{% call m.test_step( name="unittest: GPU", - num_shards=2, ws="tvm/ut-python-gpu", platform="gpu", docker_image="ci_gpu", test_method_names=test_method_names, ) %} {{ m.download_artifacts(tag='gpu') }} - ci_setup(ci_gpu) - {% if shard_index == 2 or num_shards < 2 %} sh ( script: "${docker_run} ${ci_gpu} ./tests/scripts/task_java_unittest.sh", label: 'Run Java unit tests', ) - {% endif %} sh ( script: "${docker_run} ${ci_gpu} ./tests/scripts/task_python_unittest_gpuonly.sh", label: 'Run Python GPU unit tests', @@ -65,9 +61,8 @@ {% endcall %} -{% call(shard_index, num_shards) m.sharded_test_step( +{% call m.test_step( name="docs: GPU", - num_shards=1, ws="tvm/docs-python-gpu", platform="gpu", docker_image="ci_gpu", @@ -75,7 +70,6 @@ test_method_names=test_method_names, ) %} {{ m.download_artifacts(tag='gpu') }} - ci_setup(ci_gpu) sh ( script: "${docker_run} ${ci_gpu} ./tests/scripts/task_python_docs.sh", label: 'Build docs', diff --git a/ci/jenkins/templates/utils/Build.groovy.j2 b/ci/jenkins/templates/utils/Build.groovy.j2 index c20a1c5c43..42bd4a991f 100644 --- a/ci/jenkins/templates/utils/Build.groovy.j2 +++ b/ci/jenkins/templates/utils/Build.groovy.j2 @@ -1,10 +1,3 @@ -def ci_setup(image) { - sh ( - script: "${docker_run} ${image} ./tests/scripts/task_clear_pytest.sh", - label: 'Clean up old workspace', - ) -} - def python_unittest(image) { sh ( script: "${docker_run} ${image} ./tests/scripts/task_python_unittest.sh", diff --git a/ci/jenkins/templates/utils/base.groovy.j2 b/ci/jenkins/templates/utils/base.groovy.j2 index 813ceee886..a8fda460b7 100644 --- a/ci/jenkins/templates/utils/base.groovy.j2 +++ b/ci/jenkins/templates/utils/base.groovy.j2 @@ -82,7 +82,7 @@ properties([ upstream_revision = null // command to start a docker container -docker_run = 'docker/bash.sh --env CI --env PLATFORM --env TVM_SHARD_INDEX --env TVM_NUM_SHARDS --env RUN_DISPLAY_URL --env PLATFORM --env SKIP_SLOW_TESTS --env TEST_STEP_NAME' +docker_run = 'docker/bash.sh --env CI --env PLATFORM --env SKIP_SLOW_TESTS --env TEST_STEP_NAME' docker_build = 'docker/build.sh' // timeout in minutes max_time = 180 diff --git a/ci/jenkins/templates/utils/macros.j2 b/ci/jenkins/templates/utils/macros.j2 index b1bd3679ac..959d91725f 100644 --- a/ci/jenkins/templates/utils/macros.j2 +++ b/ci/jenkins/templates/utils/macros.j2 @@ -19,18 +19,8 @@ "workspace/exec_${env.EXECUTOR_NUMBER}/{{ folder }}" {%- endmacro -%} -{% macro junit_to_s3(test_dir_name) %} -sh( - script: "./${jenkins_scripts_root}/s3.py --action upload --bucket ${s3_bucket} --prefix ${s3_prefix}/pytest-results/{{ test_dir_name }} --items build/pytest-results", - label: 'Upload JUnits to S3', - ) -{% endmacro %} - -{% macro sharded_test_step(name, num_shards, ws, docker_image, platform, test_method_names, condition="!skip_ci && is_docs_only_build != 1") %} - -{% for shard_index in range(1, num_shards + 1) %} -{% set method_name = "shard_run_" + name.replace(":", "").replace(" ", "-").replace("-", "_") + "_" + shard_index|string + "_of_" + num_shards|string %} -{% set test_dir_name = name.replace(":", "").replace(" ", "-").replace("-", "_")|string %} +{% macro test_step(name, ws, docker_image, platform, test_method_names, condition="!skip_ci && is_docs_only_build != 1") %} +{% set method_name = "run_" + name.replace(":", "").replace(" ", "-").replace("-", "_") %} def {{ method_name }}(node_type) { echo 'Begin running on node_type ' + node_type if ({{ condition }}) { @@ -43,29 +33,19 @@ def {{ method_name }}(node_type) { withEnv([ 'PLATFORM={{ platform }}', 'TEST_STEP_NAME={{ name }}', - 'TVM_NUM_SHARDS={{ num_shards }}', - 'TVM_SHARD_INDEX={{ shard_index - 1 }}', "SKIP_SLOW_TESTS=${skip_slow_tests}"], { - {{ caller(shard_index, num_shards) | trim | indent(width=12) }} + {{ caller() | trim | indent(width=12) }} }) } - // only run upload if things are successful - try { - {{ junit_to_s3(test_dir_name) }} - junit 'build/pytest-results/*.xml' - } catch (Exception e) { - echo 'Exception during JUnit upload: ' + e.toString() - } } } echo 'End running on node_type ' + node_type } else { - Utils.markStageSkippedForConditional('{{ name }} {{ shard_index }} of {{ num_shards }}') + Utils.markStageSkippedForConditional('{{ name }}') } } -{% set _ = test_method_names.append((name + " " + shard_index|string + " of " + num_shards|string, method_name)) %} +{% set _ = test_method_names.append((name, method_name)) %} -{% endfor %} {% endmacro %} {% macro invoke_build(name, condition, node, docker_image, ws, platform) %} diff --git a/ci/jenkins/templates/wasm_jenkinsfile.groovy.j2 b/ci/jenkins/templates/wasm_jenkinsfile.groovy.j2 index 0d119f043e..8e7d909bd1 100644 --- a/ci/jenkins/templates/wasm_jenkinsfile.groovy.j2 +++ b/ci/jenkins/templates/wasm_jenkinsfile.groovy.j2 @@ -32,7 +32,6 @@ cmake_build(ci_wasm, 'build') make_cpp_tests(ci_wasm, 'build') cpp_unittest(ci_wasm) - ci_setup(ci_wasm) sh ( script: "${docker_run} ${ci_wasm} ./tests/scripts/task_web_wasm.sh", label: 'Run WASM lint and tests', diff --git a/ci/scripts/jenkins/pytest_ids.py b/ci/scripts/jenkins/pytest_ids.py deleted file mode 100755 index 548069e855..0000000000 --- a/ci/scripts/jenkins/pytest_ids.py +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env python3 -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -import argparse -import io -from contextlib import redirect_stdout - -import pytest - - -class NodeidsCollector: - def pytest_collection_modifyitems(self, items): - self.nodeids = [item.nodeid for item in items] - - -def main(folder): - collector = NodeidsCollector() - f = io.StringIO() - with redirect_stdout(f): - pytest.main(["-qq", "--collect-only", folder], plugins=[collector]) - for nodeid in collector.nodeids: - print(nodeid) - - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="List pytest nodeids for a folder") - parser.add_argument("--folder", required=True, help="test folder to inspect") - args = parser.parse_args() - main(args.folder) diff --git a/ci/scripts/jenkins/pytest_wrapper.py b/ci/scripts/jenkins/pytest_wrapper.py deleted file mode 100755 index a667810293..0000000000 --- a/ci/scripts/jenkins/pytest_wrapper.py +++ /dev/null @@ -1,137 +0,0 @@ -#!/usr/bin/env python3 -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -# ruff: noqa: E501 -import argparse -import logging -import os -import textwrap -import urllib.parse -from pathlib import Path - -import junitparser -from cmd_utils import init_log - -REPO_ROOT = Path(__file__).resolve().parent.parent.parent.parent - - -def lstrip(s: str, prefix: str) -> str: - if s.startswith(prefix): - s = s[len(prefix) :] - return s - - -def classname_to_file(classname: str) -> str: - classname = lstrip(classname, "cython.") - classname = lstrip(classname, "ctypes.") - return classname.replace(".", "/") + ".py" - - -def failed_test_ids() -> list[str]: - FAILURE_TYPES = (junitparser.Failure, junitparser.Error) - junit_dir = REPO_ROOT / "build" / "pytest-results" - failed_node_ids = [] - for junit in junit_dir.glob("*.xml"): - xml = junitparser.JUnitXml.fromfile(str(junit)) - for suite in xml: - # handle suites - for case in suite: - if case.result is None: - logging.warn(f"Incorrectly formatted JUnit found, result was None on {case}") - continue - - if len(case.result) > 0 and isinstance(case.result[0], FAILURE_TYPES): - node_id = classname_to_file(case.classname) + "::" + case.name - failed_node_ids.append(node_id) - - return list(set(failed_node_ids)) - - -def repro_command(build_type: str, failed_node_ids: list[str]) -> str | None: - """ - Parse available JUnit XML files and output a command that users can run to - reproduce CI failures locally - """ - test_args = [f"--tests {node_id}" for node_id in failed_node_ids] - test_args_str = " ".join(test_args) - return f"python3 tests/scripts/ci.py {build_type} {test_args_str}" - - -def make_issue_url(failed_node_ids: list[str]) -> str: - names = [f"`{node_id}`" for node_id in failed_node_ids] - run_url = os.getenv("RUN_DISPLAY_URL", "<insert run URL>") - test_bullets = [f" - `{node_id}`" for node_id in failed_node_ids] - params = { - "labels": "test: flaky", - "title": "[Flaky Test] " + ", ".join(names), - "body": textwrap.dedent( - """ - These tests were found to be flaky (intermittently failing on `main` or failed in a PR with unrelated changes). See [the docs](https://github.com/apache/tvm/blob/main/docs/contribute/ci.rst#handling-flaky-failures) for details. - - ### Tests(s)\n - """ - ) - + "\n".join(test_bullets) - + f"\n\n### Jenkins Links\n\n - {run_url}", - } - return "https://github.com/apache/tvm/issues/new?" + urllib.parse.urlencode(params) - - -def show_failure_help(failed_suites: list[str]) -> None: - failed_node_ids = failed_test_ids() - - if len(failed_node_ids) == 0: - return - - build_type = os.getenv("PLATFORM") - - if build_type is None: - raise RuntimeError("build type was None, cannot show command") - - repro = repro_command(build_type=build_type, failed_node_ids=failed_node_ids) - if repro is None: - print("No test failures detected") - return - - print(f"Report flaky test shortcut: {make_issue_url(failed_node_ids)}") - print("=============================== PYTEST FAILURES ================================") - print( - "These pytest suites failed to execute. The results can be found in the " - "Jenkins 'Tests' tab or by scrolling up through the raw logs here. " - "If there is no test listed below, the failure likely came from a segmentation " - "fault which you can find in the logs above.\n" - ) - if failed_suites is not None and len(failed_suites) > 0: - print("\n".join([f" - {suite}" for suite in failed_suites])) - print("") - - print("You can reproduce these specific failures locally with this command:\n") - print(textwrap.indent(repro, prefix=" ")) - print("") - - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Print information about a failed pytest run") - args, other = parser.parse_known_args() - init_log() - - try: - show_failure_help(failed_suites=other) - except Exception as e: - # This script shouldn't ever introduce failures since it's just there to - # add extra information, so ignore any errors - logging.exception(e) diff --git a/conftest.py b/conftest.py index dd32937620..dd294ea548 100644 --- a/conftest.py +++ b/conftest.py @@ -14,8 +14,6 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -# ruff: noqa: E501 -import hashlib import os import sys from pathlib import Path @@ -25,58 +23,6 @@ IS_IN_CI = os.getenv("CI", "") == "true" REPO_ROOT = Path(__file__).resolve().parent -# These are long running tests (manually curated and extracted from CI logs) -# that should be allocated to test shards in a round-robin fashion. These are -# taken from the 20 (arbitrary number) of tests as from -# https://ci.tlcpack.ai/job/tvm/job/main/2907/testReport -_slowest_tests = [] -HARDCODED_ALLOCATIONS = {} -for idx, test in enumerate(_slowest_tests): - HARDCODED_ALLOCATIONS[test] = idx - -# These rely on running on the same node to pass successfully -FIXED_ALLOCATION_PREFIXES = { - "tests/python/testing/test_tvm_testing_features.py": 0, -} - - -def find_shard_index(nodeid: str, num_shards: int) -> int: - """ - Return the index of the shard that should run this test - """ - for prefix, target_shard_idx in FIXED_ALLOCATION_PREFIXES.items(): - if nodeid.startswith(prefix): - if target_shard_idx >= num_shards: - raise RuntimeError( - f"Cannot collect sharded tests, {nodeid} has hardcoded shard index {target_shard_idx} among only {num_shards} shards" - ) - return target_shard_idx - - if nodeid in HARDCODED_ALLOCATIONS: - hash = HARDCODED_ALLOCATIONS[nodeid] - else: - hash = hashlib.md5(nodeid.encode()) - hash = int(hash.hexdigest(), 16) - - return hash % num_shards - - -def pytest_collection_modifyitems(config, items): - if not all(k in os.environ for k in ["CI", "TVM_NUM_SHARDS", "TVM_SHARD_INDEX"]): - # Only apportion tests if in CI and in a job that is set up for it - return - - num_shards = int(os.environ["TVM_NUM_SHARDS"]) - shard_index = int(os.environ["TVM_SHARD_INDEX"]) - - print(f"Marking tests for shard {shard_index} of {num_shards}") - items_copy = list(items) - for item in items_copy: - item_shard_index = find_shard_index(item.nodeid, num_shards=num_shards) - if item_shard_index != shard_index: - items.remove(item) - - def pytest_sessionstart(): if IS_IN_CI: hook_script_dir = REPO_ROOT / "tests" / "scripts" / "request_hook" diff --git a/docker/install/ubuntu_install_python_package.sh b/docker/install/ubuntu_install_python_package.sh index d07f19ed3c..4445dfd83e 100755 --- a/docker/install/ubuntu_install_python_package.sh +++ b/docker/install/ubuntu_install_python_package.sh @@ -32,13 +32,11 @@ uv pip install --upgrade \ Pillow==12.1.1 \ "psutil~=7.0" \ "pytest~=8.3" \ - "pytest-profiling~=1.8" \ "pytest-xdist~=3.6" \ pytest-rerunfailures==16.1 \ "requests~=2.32" \ "scipy~=1.13" \ "Jinja2~=3.1" \ - junitparser==4.0.2 \ "six~=1.17" \ "tornado~=6.4" \ "ml_dtypes~=0.5" \ diff --git a/docs/contribute/pull_request.rst b/docs/contribute/pull_request.rst index fe960c709a..a6c41b755f 100644 --- a/docs/contribute/pull_request.rst +++ b/docs/contribute/pull_request.rst @@ -242,7 +242,7 @@ Necessary dependencies: .. code:: bash - pip install --user pytest Cython + pip install --user pytest pytest-xdist Cython If you want to run all tests: diff --git a/python/tvm/testing/plugin.py b/python/tvm/testing/plugin.py index dfb858f1b8..2869664842 100644 --- a/python/tvm/testing/plugin.py +++ b/python/tvm/testing/plugin.py @@ -33,7 +33,6 @@ directory as the test scripts. """ import _pytest -import pytest def pytest_collection_modifyitems(config, items): @@ -44,14 +43,6 @@ def pytest_collection_modifyitems(config, items): _sort_tests(items) -def pytest_sessionfinish(session, exitstatus): - # Don't exit with an error if we select a subset of tests that doesn't - # include anything - if session.config.option.markexpr != "": - if exitstatus == pytest.ExitCode.NO_TESTS_COLLECTED: - session.exitstatus = pytest.ExitCode.OK - - def _count_num_fixture_uses(items): # Helper function, counts the number of tests that use each cached # fixture. Should be called from pytest_collection_modifyitems(). diff --git a/tests/python/s_tir/meta_schedule/test_meta_schedule_cost_model.py b/tests/python/s_tir/meta_schedule/test_meta_schedule_cost_model.py index 1e209cacbc..97c9469ccf 100644 --- a/tests/python/s_tir/meta_schedule/test_meta_schedule_cost_model.py +++ b/tests/python/s_tir/meta_schedule/test_meta_schedule_cost_model.py @@ -149,7 +149,7 @@ def _dummy_result(num_samples: int = 4, max_run_sec: int = 10): @requires_xgboost def test_meta_schedule_xgb_model(): extractor = RandomFeatureExtractor() - model = XGBModel(extractor=extractor, num_warmup_samples=2) + model = XGBModel(extractor=extractor, num_warmup_samples=2, num_tuning_cores=1) update_sample_count = 10 predict_sample_count = 100 model.update( @@ -162,9 +162,10 @@ def test_meta_schedule_xgb_model(): @requires_xgboost def test_meta_schedule_xgb_model_no_feature(): - model = XGBModel(num_warmup_samples=0) + model = XGBModel(num_warmup_samples=0, num_tuning_cores=1) tune_ctx = TuneContext( FullModule, + num_threads=1, target={"kind": "llvm", "num-cores": 16}, space_generator="post-order-apply", search_strategy="evolutionary", @@ -177,7 +178,7 @@ def test_meta_schedule_xgb_model_no_feature(): @requires_xgboost def test_meta_schedule_xgb_model_reload(): extractor = RandomFeatureExtractor() - model = XGBModel(extractor=extractor, num_warmup_samples=10) + model = XGBModel(extractor=extractor, num_warmup_samples=10, num_tuning_cores=1) update_sample_count = 20 predict_sample_count = 30 model.update( @@ -221,7 +222,7 @@ def test_meta_schedule_xgb_model_reload(): @requires_xgboost def test_meta_schedule_xgb_model_reupdate(): extractor = RandomFeatureExtractor() - model = XGBModel(extractor=extractor, num_warmup_samples=2) + model = XGBModel(extractor=extractor, num_warmup_samples=2, num_tuning_cores=1) update_sample_count = 60 predict_sample_count = 100 model.update( @@ -252,7 +253,7 @@ def test_meta_schedule_xgb_model_callback_as_function(): # pylint: enable=import-outside-toplevel extractor = RandomFeatureExtractor() - model = XGBModel(extractor=extractor, num_warmup_samples=10) + model = XGBModel(extractor=extractor, num_warmup_samples=10, num_tuning_cores=1) update_sample_count = 20 predict_sample_count = 30 diff --git a/tests/python/s_tir/meta_schedule/test_meta_schedule_search_strategy.py b/tests/python/s_tir/meta_schedule/test_meta_schedule_search_strategy.py index 002741c6bf..6451a94ec9 100644 --- a/tests/python/s_tir/meta_schedule/test_meta_schedule_search_strategy.py +++ b/tests/python/s_tir/meta_schedule/test_meta_schedule_search_strategy.py @@ -103,6 +103,7 @@ def test_meta_schedule_replay_func( context = ms.TuneContext( mod=Matmul, + num_threads=1, space_generator=ms.space_generator.ScheduleFn(sch_fn=_schedule_matmul, postprocs=[]), search_strategy=TestClass(), ) diff --git a/tests/python/s_tir/meta_schedule/test_meta_schedule_task_scheduler.py b/tests/python/s_tir/meta_schedule/test_meta_schedule_task_scheduler.py index 61f5583c2a..6dc351761a 100644 --- a/tests/python/s_tir/meta_schedule/test_meta_schedule_task_scheduler.py +++ b/tests/python/s_tir/meta_schedule/test_meta_schedule_task_scheduler.py @@ -157,6 +157,7 @@ def test_meta_schedule_task_scheduler_single(): [ ms.TuneContext( MatmulModule, + num_threads=1, target=tvm.target.Target("llvm"), space_generator=_schedule_matmul, search_strategy=ms.search_strategy.ReplayTrace(), @@ -183,6 +184,7 @@ def test_meta_schedule_task_scheduler_multiple(): tasks = [ ms.TuneContext( MatmulModule, + num_threads=1, target=tvm.target.Target("llvm"), space_generator=_schedule_matmul, search_strategy=ms.search_strategy.ReplayTrace(), @@ -191,6 +193,7 @@ def test_meta_schedule_task_scheduler_multiple(): ), ms.TuneContext( MatmulReluModule, + num_threads=1, target=tvm.target.Target("llvm"), space_generator=_schedule_matmul, search_strategy=ms.search_strategy.ReplayTrace(), @@ -199,6 +202,7 @@ def test_meta_schedule_task_scheduler_multiple(): ), ms.TuneContext( BatchMatmulModule, + num_threads=1, target=tvm.target.Target("llvm"), space_generator=_schedule_batch_matmul, search_strategy=ms.search_strategy.ReplayTrace(), @@ -255,6 +259,7 @@ def test_meta_schedule_task_scheduler_override_next_task_id_only(): # pylint: d tasks = [ ms.TuneContext( MatmulModule, + num_threads=1, target=tvm.target.Target("llvm"), space_generator=_schedule_matmul, search_strategy=ms.search_strategy.ReplayTrace(), @@ -263,6 +268,7 @@ def test_meta_schedule_task_scheduler_override_next_task_id_only(): # pylint: d ), ms.TuneContext( MatmulReluModule, + num_threads=1, target=tvm.target.Target("llvm"), space_generator=_schedule_matmul, search_strategy=ms.search_strategy.ReplayTrace(), @@ -271,6 +277,7 @@ def test_meta_schedule_task_scheduler_override_next_task_id_only(): # pylint: d ), ms.TuneContext( BatchMatmulModule, + num_threads=1, target=tvm.target.Target("llvm"), space_generator=_schedule_batch_matmul, search_strategy=ms.search_strategy.ReplayTrace(), @@ -310,6 +317,7 @@ def test_meta_schedule_task_scheduler_multiple_gradient_based(): tasks = [ ms.TuneContext( MatmulModule, + num_threads=1, target=tvm.target.Target("llvm"), space_generator=_schedule_matmul, search_strategy=ms.search_strategy.ReplayTrace(), @@ -318,6 +326,7 @@ def test_meta_schedule_task_scheduler_multiple_gradient_based(): ), ms.TuneContext( MatmulReluModule, + num_threads=1, target=tvm.target.Target("llvm"), space_generator=_schedule_matmul, search_strategy=ms.search_strategy.ReplayTrace(), @@ -326,6 +335,7 @@ def test_meta_schedule_task_scheduler_multiple_gradient_based(): ), ms.TuneContext( BatchMatmulModule, + num_threads=1, target=tvm.target.Target("llvm"), space_generator=_schedule_batch_matmul, search_strategy=ms.search_strategy.ReplayTrace(), @@ -394,6 +404,7 @@ def test_meta_schedule_task_scheduler_gradient_based_with_null_search_strategy() tasks = [ ms.TuneContext( MatmulModule, + num_threads=1, target=tvm.target.Target("llvm"), space_generator=_schedule_matmul, search_strategy=NullSearchStrategy(rounds_with_empty_candidates=5), @@ -402,6 +413,7 @@ def test_meta_schedule_task_scheduler_gradient_based_with_null_search_strategy() ), ms.TuneContext( BatchMatmulModule, + num_threads=1, target=tvm.target.Target("llvm"), space_generator=_schedule_batch_matmul, search_strategy=NullSearchStrategy(rounds_with_empty_candidates=0), @@ -410,6 +422,7 @@ def test_meta_schedule_task_scheduler_gradient_based_with_null_search_strategy() ), ms.TuneContext( MatmulReluModule, + num_threads=1, target=tvm.target.Target("llvm"), space_generator=_schedule_matmul, search_strategy=ms.search_strategy.ReplayTrace(), diff --git a/tests/python/s_tir/transform/test_s_tir_transform_lower_match_buffer.py b/tests/python/s_tir/transform/test_s_tir_transform_lower_match_buffer.py index a174c771a2..eaa78cf626 100644 --- a/tests/python/s_tir/transform/test_s_tir_transform_lower_match_buffer.py +++ b/tests/python/s_tir/transform/test_s_tir_transform_lower_match_buffer.py @@ -486,7 +486,7 @@ def fail_buffer_bind(a: T.handle) -> None: # well-formed checker complains about redefinition of a stride variable @T.prim_func(check_well_formed=False, s_tir=True) -def fail_match_func_param(a: T.handle, m: T.handle, n: T.handle) -> None: +def fail_match_func_param(a: T.handle, m: T.int32, n: T.int32) -> None: A = T.match_buffer(a, (8, 8)) for i, j in T.grid(8, 2): with T.sblock(): diff --git a/tests/python/testing/test_tvm_testing_features.py b/tests/python/testing/test_tvm_testing_features.py index 549aedda6a..2f4c798947 100644 --- a/tests/python/testing/test_tvm_testing_features.py +++ b/tests/python/testing/test_tvm_testing_features.py @@ -14,10 +14,9 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -# ruff: noqa: F401, RUF012 +# ruff: noqa: RUF012 import os -import sys import pytest diff --git a/tests/scripts/setup-pytest-env.sh b/tests/scripts/setup-pytest-env.sh deleted file mode 100755 index 171ddbc2d0..0000000000 --- a/tests/scripts/setup-pytest-env.sh +++ /dev/null @@ -1,98 +0,0 @@ -#!/usr/bin/env bash -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. - -# NOTE: allow unbound variable here -set +u - -if [[ ! -z $CI_PYTEST_ADD_OPTIONS ]]; then - export PYTEST_ADDOPTS="-s -vv $CI_PYTEST_ADD_OPTIONS $PYTEST_ADDOPTS" -else - export PYTEST_ADDOPTS="-s -vv $PYTEST_ADDOPTS" -fi -set -ux - -export TVM_PATH=`pwd` -export PYTHONPATH="${TVM_PATH}/python" - -export TVM_PYTEST_RESULT_DIR="${TVM_PATH}/build/pytest-results" -mkdir -p "${TVM_PYTEST_RESULT_DIR}" -pytest_errors=() - -# This ensures that all pytest invocations that are run through run_pytest will -# complete and errors will be reported once Bash is done executing all scripts. -function cleanup() { - set +x - if [ "${#pytest_errors[@]}" -gt 0 ]; then - echo "These pytest invocations failed, the results can be found in the Jenkins 'Tests' tab or by scrolling up through the raw logs here." - python3 ci/scripts/jenkins/pytest_wrapper.py "${pytest_errors[@]}" - exit 1 - fi - set -x -} -trap cleanup 0 - -function run_pytest() { - ffi_type="cython" - set -e - local test_suite_name="$1" - shift - extra_args=( "$@" ) - if [ -z "${ffi_type}" -o -z "${test_suite_name}" ]; then - echo "error: run_pytest called incorrectly: run_pytest ${test_suite_name}" "${extra_args[@]}" - echo "usage: run_pytest <TEST_SUITE_NAME> [pytest args...]" - exit 2 - fi - - # Allow unbound variable here. - set +u - if [[ -z "${TVM_SHARD_INDEX}" ]]; then - current_shard="no-shard" - else - current_shard="shard-${TVM_SHARD_INDEX}" - fi - set -u - - has_reruns=$(python3 -m pytest --help 2>&1 | grep 'reruns=' || true) - if [ -n "$has_reruns" ]; then - if [[ ! "${extra_args[*]}" == *"--reruns"* ]]; then - extra_args+=('--reruns=3') - fi - fi - - suite_name="${test_suite_name}-${current_shard}-${ffi_type}" - - DEFAULT_PARALLELISM=1 - - if [[ ! "${extra_args[*]}" == *" -n"* ]] && [[ ! "${extra_args[*]}" == *" -dist"* ]]; then - extra_args+=("-n=$DEFAULT_PARALLELISM") - fi - - exit_code=0 - set +e - python3 -m pytest \ - -o "junit_suite_name=${suite_name}" \ - "--junit-xml=${TVM_PYTEST_RESULT_DIR}/${suite_name}.xml" \ - "--junit-prefix=${ffi_type}" \ - "${extra_args[@]}" || exit_code=$? - # Pytest will return error code -5 if no test is collected. - if [ "$exit_code" -ne "0" ] && [ "$exit_code" -ne "5" ]; then - pytest_errors+=("${suite_name}: $@") - fi - # To avoid overwriting. - set -e -} diff --git a/tests/scripts/task_clear_pytest.sh b/tests/scripts/task_clear_pytest.sh index 0a24f62531..430425791c 100755 --- a/tests/scripts/task_clear_pytest.sh +++ b/tests/scripts/task_clear_pytest.sh @@ -16,9 +16,7 @@ # specific language governing permissions and limitations # under the License. -set -euxo pipefail +set -euo pipefail -# Ensure no stale pytest-results remain from a previous test run. -pushd build -rm -rf pytest-results -popd +# PR jobs evaluate their Jenkinsfile from the trusted base branch. Keep this +# inert entry point until the Jenkinsfile update has landed on the base branch. diff --git a/tests/scripts/task_python_docs.sh b/tests/scripts/task_python_docs.sh index 7c3122cd92..11b2501b7b 100755 --- a/tests/scripts/task_python_docs.sh +++ b/tests/scripts/task_python_docs.sh @@ -18,7 +18,7 @@ set -euxo pipefail -source tests/scripts/setup-pytest-env.sh +export PYTHONPATH="$(pwd)/python" # to avoid CI CPU thread throttling. export TVM_BIND_THREADS=0 diff --git a/tests/scripts/task_python_integration.sh b/tests/scripts/task_python_integration.sh index dcd7b8ff47..c7cf1f4329 100755 --- a/tests/scripts/task_python_integration.sh +++ b/tests/scripts/task_python_integration.sh @@ -18,7 +18,7 @@ set -euxo pipefail -source tests/scripts/setup-pytest-env.sh +export PYTHONPATH="$(pwd)/python" export LD_LIBRARY_PATH="build:${LD_LIBRARY_PATH:-}" # to avoid CI CPU thread throttling. diff --git a/tests/scripts/task_python_nightly.sh b/tests/scripts/task_python_nightly.sh deleted file mode 100755 index b480563a8c..0000000000 --- a/tests/scripts/task_python_nightly.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. - -set -euxo pipefail - -source tests/scripts/setup-pytest-env.sh - -# setup tvm-ffi into python folder -uv pip install -v --target=python ./3rdparty/tvm-ffi/ - -# cleanup pycache -find . -type f -path "*.pyc" | xargs rm -f - -run_pytest python-topi-nightly tests/python/topi/nightly - -# Tensorflow device verification and network tests on nightly -export CI_ENV_NIGHTLY -python3 tests/python/relax/test_frontend_tflite.py diff --git a/tests/scripts/task_python_unittest.sh b/tests/scripts/task_python_unittest.sh index 7e09926cc0..0728743fb5 100755 --- a/tests/scripts/task_python_unittest.sh +++ b/tests/scripts/task_python_unittest.sh @@ -18,7 +18,8 @@ set -euxo pipefail -source tests/scripts/setup-pytest-env.sh +export PYTHONPATH="$(pwd)/python" +export PYTEST_ADDOPTS="-s -vv ${CI_PYTEST_ADD_OPTIONS:-} ${PYTEST_ADDOPTS:-}" # cleanup pycache find . -type f -path "*.pyc" | xargs rm -f @@ -26,23 +27,20 @@ find . -type f -path "*.pyc" | xargs rm -f # setup tvm-ffi into python folder uv pip install -v --target=python ./3rdparty/tvm-ffi/ -# NOTE: also set by task_python_unittest_gpuonly.sh. -if [ -z "${TVM_UNITTEST_TESTSUITE_NAME:-}" ]; then - TVM_UNITTEST_TESTSUITE_NAME=python-unittest +# First run the minimal platform test. A GPU-only run can select no tests here. +if [ ! -d tests/python/all-platform-minimal-test ]; then + echo "Missing pytest target: tests/python/all-platform-minimal-test" >&2 + exit 1 fi +python3 -m pytest -n auto tests/python/all-platform-minimal-test || [ "$?" -eq 5 ] -# First run minimal test on both ctypes and cython. -run_pytest ${TVM_UNITTEST_TESTSUITE_NAME}-platform-minimal-test tests/python/all-platform-minimal-test - -# Then run all unittests on both ctypes and cython. +# Then run all unit tests. TEST_FILES=( - "ffi" "arith" "ci" "codegen" "driver" "ir" - "meta_schedule" "runtime" "target" "te" @@ -51,6 +49,7 @@ TEST_FILES=( "s_tir/schedule" "s_tir/dlight" "s_tir/analysis" + "s_tir/meta_schedule" "s_tir/transform" "tirx-analysis" "tirx-base" @@ -62,7 +61,13 @@ TEST_FILES=( PYTEST_TARGETS=() for TEST_FILE in "${TEST_FILES[@]}"; do - PYTEST_TARGETS+=("tests/python/${TEST_FILE}") + TEST_PATH="tests/python/${TEST_FILE}" + if [ ! -e "${TEST_PATH}" ]; then + echo "Missing pytest target: ${TEST_PATH}" >&2 + exit 1 + fi + PYTEST_TARGETS+=("${TEST_PATH}") done -run_pytest "${TVM_UNITTEST_TESTSUITE_NAME}" "${PYTEST_TARGETS[@]}" +# Do not mask pytest's exit 5: an unexpectedly empty broad suite must fail CI. +python3 -m pytest -n auto --dist=loadgroup "${PYTEST_TARGETS[@]}" diff --git a/tests/scripts/task_python_unittest_gpuonly.sh b/tests/scripts/task_python_unittest_gpuonly.sh index be3ea07947..8194b23ac4 100755 --- a/tests/scripts/task_python_unittest_gpuonly.sh +++ b/tests/scripts/task_python_unittest_gpuonly.sh @@ -24,15 +24,18 @@ export PYTEST_ADDOPTS="-m gpu ${PYTEST_ADDOPTS:-}" # Test most of the enabled runtimes here. # TODO: disabled opencl tests due to segmentation fault. export TVM_TEST_TARGETS='cuda;metal;rocm;nvptx' -export TVM_UNITTEST_TESTSUITE_NAME=python-unittest-gpu ./tests/scripts/task_python_unittest.sh # Kept separate to avoid increasing time needed to run CI, testing # only minimal functionality of Vulkan runtime. export TVM_TEST_TARGETS='{"kind":"vulkan","from_device":0}' -export TVM_UNITTEST_TESTSUITE_NAME=python-codegen-vulkan -source tests/scripts/setup-pytest-env.sh +export PYTHONPATH="$(pwd)/python" +export PYTEST_ADDOPTS="-s -vv ${CI_PYTEST_ADD_OPTIONS:-} ${PYTEST_ADDOPTS:-}" -run_pytest ${TVM_UNITTEST_TESTSUITE_NAME}-1 tests/python/codegen/test_target_codegen_vulkan.py +if [ ! -f tests/python/codegen/test_target_codegen_vulkan.py ]; then + echo "Missing pytest target: tests/python/codegen/test_target_codegen_vulkan.py" >&2 + exit 1 +fi +python3 -m pytest -n auto tests/python/codegen/test_target_codegen_vulkan.py
